Blog. Morris 树遍历算法 [blog/data-structure/morris]

Morris 遍历算法能够在 O(n) 时间复杂度和 O(1) 空间复杂度下完成二叉树的中序遍历。算法通过动态修改树的结构,将原始的二叉树转换为右偏树,从而实现无需递归栈或显式栈的树遍历。

传统遍历方法的限制

递归中序遍历

/// Traditional recursive inorder traversal
pub fn[A] inorder_recursive(root : Node[A]?, f : (A) -> Unit) -> Unit {
  match root {
    None => ()
    Some({ left, data, right }) => {
      inorder_recursive(left, f)
      f(data)
      inorder_recursive(right, f)
    }
  }
}

空间复杂度:O(h),其中 h 是树的高度

迭代中序遍历(使用栈)

/// Traditional iterative inorder traversal using stack
pub fn[A] inorder_iterative(root : Node[A]?, f : (A) -> Unit) -> Unit {
  let stack : Array[Node[A]] = []
  let mut current = root
  
  while not(stack.is_empty()) || current is Some(_) {
    while current is Some(_) {
      stack.push(current.unwrap())
      current = current.unwrap().left
    }
    
    let node = stack.unsafe_pop()
    f(node.data)
    current = node.right
  }
}

空间复杂度:O(h)

Morris 算法的核心思想

Morris 算法在遍历过程中动态重构树,将其转换为右偏树。

右偏树

右偏树是一种特殊的二叉树:

  • 每个节点没有左子树或左子树为空
  • 所有节点主要通过右指针连接
  • 可以像链表一样线性遍历
原始二叉树:        转换后的右偏树:
      4                  1
     / \                  \
    2   6                  2
   / \ / \                  \
  1 3 5 7                   3
                             \
                              4
                               \
                                5
                                 \
                                  6
                                   \
                                    7

算法工作机制

线索化步骤

  1. 识别中序前驱:对于每个有左子树的节点,找到其左子树中的最右节点
  2. 创建线索:将前驱节点的右指针指向当前节点
  3. 移除左连接:暂时移除当前节点的左指针
  4. 线性遍历:遍历转换后的右偏结构

执行步骤示例

初始树:
      4
     / \
    2   6
   / \ / \
  1 3 5 7
  1. 从根节点 4 开始,找到左子树的最右节点 3,创建线索 3.right = 4,移除 4.left,移动到节点 2
  2. 处理节点 2,找到左子树的最右节点 1,创建线索 1.right = 2,移除 2.left,移动到节点 1
  3. 处理节点 1,没有左子树,访问节点 1,通过线索回到节点 2
  4. 再次处理节点 2,访问节点 2,移动到节点 3
  5. 处理节点 3,访问节点 3,通过线索回到节点 4

MoonBit 实现

pub fn[A] morris_tree_traversal_pattern_match(
  root : Node[A]?,
  f : (A) -> Unit,
) -> Unit {
  loop root {
    None => break
    // This part of the code is responsible for transforming the tree into a right-skewed tree
    Some({ left: Some(_) as left, .. } as cur_) as cur => {
      // The rightmost node of the left subtree is the predecessor of the last node visited in the left subtree during inorder traversal, where the last node is the root of the left subtree
      guard right_most_node(left) is Some(left_subtree_last_visited_node)
      // Create a back edge here so that after visiting the left subtree, we can return to the root of the left subtree
      left_subtree_last_visited_node.right = cur
      let cur_left = cur_.left

      // This step prevents infinite loops when traversing through the previously created back edge
      cur_.left = None

      // Continue visiting the left subtree
      continue cur_left
    }
    Some(cur_) => {
      // At this point the tree has become a right-skewed tree, with no more left subtrees
      f(cur_.data)
      // Simply continue visiting the right subtree
      continue cur_.right
    }
  }
}

关键实现细节

  1. 模式匹配匹配有左子树的节点
  2. 使用 loop 和 continue 实现状态转换
  3. 创建线索并移除左连接

复杂度分析

时间复杂度:O(n)

每个节点最多被访问 3 次,总体仍然是线性时间。

空间复杂度:O(1)

  • 只使用常数个额外变量
  • 没有递归栈或显式栈
  • 使用树本身的空间进行临时修改

优势与限制

优势

  1. O(1) 空间复杂度
  2. 线性访问模式对 CPU 缓存友好
  3. 避免栈溢出,适合处理极深的树
  4. 证明了树遍历可以在常数空间内完成

限制

  1. 临时修改原始树结构
  2. 实现比递归方法复杂
  3. 常数因子比简单递归略大
  4. 需要额外的指针操作

应用场景

Morris 算法适用于:

  1. 内存受限环境
  2. 大规模数据处理
  3. 系统级编程中需要精确控制内存使用的场景
  4. 需要最优空间复杂度的算法题目

总结

Morris 遍历算法通过改变问题的表示形式实现了 O(1) 空间的树遍历。算法将二叉树动态转换为右偏树,从而突破了传统方法的空间限制。MoonBit 的模式匹配和循环表达式使得这个算法的实现更加清晰。