美文网首页
不用递归或栈遍历二叉树

不用递归或栈遍历二叉树

作者: 顽强的猫尾草 | 来源:发表于2018-05-16 21:27 被阅读14次

上回说到二叉树的各类遍历方法,其中前中后序遍历都是用栈或者递归(实际上还是栈)来实现的。

如果只给你 O(1) 空间,不能使用栈,而且不能破坏二叉树的形状(中间允许暂时改变形状),应该怎样实现呢?

要使用 O(1) 空间进行遍历,最大的难点在于,遍历到子节点的时候怎样重新返回父节点(假设节点中没有指向父节点的指针),由于不能用栈作为辅助空间。为了解决这个问题,Morris 方法用到了线索二叉树的概念。

线索二叉树:n 个结点的二叉树有 n+1 个空指针域,利用这些空指针域存放在某种遍历次序下该结点的前驱和后继结点的指针,这些指针称为线索。

Morris 只提供了中序遍历的方法,在此基础上稍加修改可以实现前序,而后续就要再费点心思了。所以先从中序开始介绍。

节点定义

struct TreeNode {
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 };

中序遍历

步骤:

  1. 如果当前节点 cur 的左孩子为 NULL,则输出 cur 并将其右孩子作为新的 cur。
  2. 如果当前节点 cur 的左孩子不为 NULL,在 cur 的左子树中找到中序遍历下的前驱节点(即左子树的最右叶子节点)。
    2.1 如果前驱节点的右孩子为 NULL,那么将当前节点 cur 设置为前驱节点的右孩子。把 cur 的左孩子设置为新的 cur。
    2.2 如果前驱节点的右孩子为当前节点 cur ,将前驱节点的右孩子重新设为 NULL(即恢复树的形状)。然后输出 cur ,并将 cur 的右孩子设置为新的 cur。
  3. 重复以上 1、2 直到当前节点 cur 为 NULL。

结合图来理解,深色节点表示该节点已输出:

代码:

void inorderMorrisTraversal(TreeNode *root) {
     TreeNode *cur = root, *prev = NULL;
     while(cur != NULL) {
         if(cur->left == NULL) {    // 1. cur 的左孩子为 NULL
             printf("%d ", cur->val);
             cur = cur->right;
         }
         else {
             // 寻找前驱节点
             prev = cur->left;
             while(prev->right != NULL && prev->right != cur) prev = prev->right;

             if(prev->right == NULL) {    // 2.1. 前驱节点的右孩子为 NULL
                 prev->right = cur;
                 cur = cur->left;
             }
             else {                       // 2.2. 前驱节点的右孩子不为 NULL(为当前节点 cur)   
                 prev->right = NULL;
                 printf("%d ", cur->val);
                 cur = cur->right;
             }
         }
     }
 }

复杂度分析:

  • 空间复杂度:O(1),只用了两个辅助指针。
  • 时间复杂度:O(n)。时间复杂度最大的疑惑在于寻找中序遍历下二叉树中所有节点的前驱节点:
 while(prev->right != NULL && prev->right != cur) prev = prev->right;

直觉上,认为它的复杂度是 O(nlgn),因为找单个节点的前驱节点与树的高度有关。但事实上,寻找所有节点的前驱节点只需要 O(n) 时间。n 个节点的二叉树中一共有 n-1 条边,整个过程中每条边最多只走 2 次,一次是为了定位到某个节点,另一次是为了寻找上面某个节点的前驱节点,如下图所示,其中红色是为了定位到某个节点,黑色线是为了找到前驱节点。所以复杂度为 O(n)。

前序遍历

前序遍历与中序遍历相似,代码上只有一行不同,不同就在于输出的顺序。

步骤:

  1. 如果当前节点 cur 的左孩子为 NULL,则输出 cur 并将其右孩子作为新的 cur。
  2. 如果当前节点 cur 的左孩子不为 NULL,在 cur 的左子树中找到 cur 在中序遍历下的前驱节点。
    2.1 如果前驱节点的右孩子为 NULL,将它的右孩子设置为 cur。输出 cur(与中序遍历唯一不同)。cur 更新为当前节点的左孩子。
    2.2 如果前驱节点的右孩子为当前节点 cur ,将它的右孩子重新设为 NULL。cur 更新为 cur 的右孩子。
  3. 重复以上 1、2 直到当前节点 cur 为 NULL。

图示:

代码:

void inorderMorrisTraversal(TreeNode *root) {
     TreeNode *cur = root, *prev = NULL;
     while(cur != NULL) {
         if(cur->left == NULL) {    // 1. cur 的左孩子为 NULL
             printf("%d ", cur->val);
             cur = cur->right;
         }
         else {
             // 寻找前驱节点
             prev = cur->left;
             while(prev->right != NULL && prev->right != cur) prev = prev->right;

             if(prev->right == NULL) {    // 2.1. 前驱节点的右孩子为 NULL
                 printf("%d ", cur->val);    // 与中序的唯一不同
                 prev->right = cur;
                 cur = cur->left;
             }
             else {                       // 2.2. 前驱节点的右孩子不为 NULL(为当前节点 cur)   
                 prev->right = NULL;
                 cur = cur->right;
             }
         }
     }
 }

复杂度分析(同中序):

  • 空间复杂度:O(1)。
  • 时间复杂度:O(n)。

后序遍历

后续遍历需要借助一个临时节点 dump,并且还需要一个子过程,倒序输出某两个节点之间路径上的各个节点。

步骤:

  1. 建立一个临时节点 dump,令其左孩子是 root。当前节点 cur 设置为临时节点 dump。
  2. 如果当前节点 cur 的左孩子为 NULL,则将其右孩子作为 cur。
  3. 如果当前节点 cur 的左孩子不为 NULL,在 cur 的左子树中找到 cur 在中序遍历下的前驱节点。
    3.1 如果前驱节点的右孩子为 NULL,将前驱节点的右孩子设置为 cur。cur 更新为 cur 的左孩子。
    3.2 如果前驱节点的右孩子为当前节点 cur,将它的右孩子重新设为 NULL。倒序输出从 cur 的左孩子到该前驱节点这条路径上的所有节点。cur 更新为 cur 的右孩子。
  4. 重复以上 2、3 直到当前节点为 NULL。

图示:

代码:

void reverse(TreeNode *from, TreeNode *to) {    // 翻转 from -> to 的路径, 因为不让用栈...
    if(from == to) return;
    TreeNode *x = from, *y = from->right, *z;
    while(true) {
        z = y->right;
        y->right = x;
        x = y;
        y = z;
        if(x == to) break;
    }
}

void printReverse(TreeNode* from, TreeNode *to) {    // 打印 from -> to 的节点
    reverse(from, to);
    TreeNode *p = to;
    while(true) {
        printf("%d ", p->val);
        if(p == from) break;
        p = p->right;
    }
    reverse(to, from);    // 恢复节点顺序
}

void postorderMorrisTraversal(TreeNode *root) {
    TreeNode dump(0);    // 1. 建立临时节点
    dump.left = root;
    TreeNode *cur = &dump, *prev = NULL;
    while(cur) {
        if(cur->left == NULL) cur = cur->right;    // 2. 如果 cur 的左孩子为 NULL
        else {                                     // 3. 如果 cur 的左孩子不为 NULL
            prev = cur->left;
            // 寻找前驱节点
            while(prev->right != NULL && prev->right != cur) prev = prev->right;
            if(prev->right == NULL) {    // 3.1. 如果前驱节点的右孩子为 NULL
                prev->right = cur;
                cur = cur->left;
            }
            else {                       // 3.2. 如果前驱节点的右孩子为 cur
                printReverse(cur->left, prev);
                prev->right = NULL;
                cur = cur->right;
            }
        }
    }
}

复杂度分析:

  • 空间复杂度:O(1)。
  • 时间复杂度:O(n)。倒序输出过程只不过加大了常数系数。

相关文章

  • 二叉树

    结构体 创建二叉树 递归遍历 栈操作 非递归遍历 层次遍历 完整代码

  • Morris Traversal

    Morris Traversal方法遍历二叉树(非递归,不用栈,O(1)空间) - AnnieKim - 博客园

  • 二叉树,非递归法

    递归法 二叉树的递归,有前序遍历、中序遍历、后序遍历,一般采用递归法,比较简单 非递归法 二叉树非递归法,采用栈来实现

  • 二叉树的相关操作(一)

    上次写了二叉树遍历,其中在非递归的遍历中,只写了前序遍历,没有写非递归中序遍历和后续遍历。非递归要用到栈,只要根据...

  • 二叉树的非递归遍历一(先序/JAVA)

    前言 二叉树的非递归遍历对于初学者来说可能不太容易掌握,其实它源于递归遍历,只是把递归栈换成了我们自己的栈。掌握了...

  • 不用递归或栈遍历二叉树

    上回说到二叉树的各类遍历方法,其中前中后序遍历都是用栈或者递归(实际上还是栈)来实现的。 如果只给你 O(1) 空...

  • 面试题----树

    94. 二叉树的非递归前序遍历 直接进出入栈,拿出数据: 94. 二叉树的非递归中序遍历 首先是根非空入栈,对子树...

  • 二叉树基础算法

    1. 二叉树利用栈的非递归先序遍历 2. 二叉树利用栈的非递归后序遍历 3. 找二叉树最近的共同祖先 找二叉排序树...

  • Java 实现的二叉树的递归、非递归遍历

    1.二叉树的递归遍历 递归遍历的关键是,将输出语句插入到合适的位置 2.二叉树的非递归先序遍历 利用一个栈来实现先...

  • 不一样的二叉树非递归遍历

    本篇将讨论模拟系统栈的二叉树非递归遍历,并由此讨论一般化的将递归程序改为非递归程序的方式。 二叉树的非递归遍历 就...

网友评论

      本文标题:不用递归或栈遍历二叉树

      本文链接:https://www.haomeiwen.com/subject/cfumdftx.html