Morris Traversal遍历二叉树

本文转载自http://www.cnblogs.com/AnnieKim/archive/2013/06/15/MorrisTraversal.html

本文主要解决一个问题,如何实现二叉树的前中后序遍历,有两个要求:

  1. O(1) 空间复杂度,即只能使用常数空间;

  2. 二叉树的形状不能被破坏(中间过程允许改变其形状)。

通常,实现二叉树的前序(preorder)、中序(inorder)、后序(postorder)遍历有两个常用的方法:一是递归(recursive),二是使用栈实现的迭代版本(stack+iterative)。这两种方法都是 O(n) 的空间复杂度(递归本身占用stack空间或者用户自定义的stack),所以不满足要求。

Morris Traversal 方法可以做到这两点,与前两种方法的不同在于该方法只需要 O(1) 空间,而且同样可以在 O(n) 时间内完成。

要使用 O(1) 空间进行遍历,最大的难点在于,遍历到子节点的时候怎样重新返回到父节点(假设节点中没有指向父节点的p指针),由于不能用栈作为辅助空间。为了解决这个问题, Morris 方法用到了线索二叉树(threaded binary tree)的概念。在Morris方法中不需要为每个节点额外分配指针指向其前驱(predecessor)和后继节点(successor),只需要利用叶子节点中的左右空指针指向某种顺序遍历下的前驱节点或后继节点就可以了。

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

首先定义在这篇文章中使用的二叉树节点结构,即由 elemleftright 组成:

typedef int elem_t;

/**
 * @struct
 * @brief 二叉树节点.
 */
typedef struct bt_node_t {
    elem_t elem;
    struct bt_node_t *left;
    struct bt_node_t *right;
};

一.中序遍历

步骤:

  1. 初始化当前节点 curroot 节点.

  2. 如果 cur 没有左孩子,则输出当前节点并将其右孩子作为当前节点,即 cur = cur->right

  3. 如果 cur 有左孩子,在当前节点 cur 的左子树中找到当前节点 cur 在中序遍历下的前驱节点。

    a) 如果前驱节点的右孩子为空,将它的右孩子设置为当前节点。当前节点更新为当前节点的左孩子。

    b) 如果前驱节点的右孩子为当前节点,将它的右孩子重新设为空(恢复树的形状)。输出当前节点。当前节点更新为当前节点的右孩子。

  4. 重复以上2、3直到当前节点为空。

下图为每一步迭代的结果(从左至右,从上到下), cur 代表当前节点,深色节点表示该节点已输出。

中序遍历
/**
 * @brief 使用Morrris算法进行中序遍历.
 * @param[in] root 根节点
 * @param[in] visit 访问函数
 */
void 
in_order_mirrors(bt_node_t *root, int(*visit)(bt_node_t *))
{
    bt_node_t *cur;
    cur = root;
    while (cur != NULL) {
        if (cur->left == NULL) {
            visit(cur);
            cur = cur->right;   /* 将右孩子作为当前节点 */
        }
        else {
            /* 查找cur节点的前驱节点 */
            bt_node_t *node = cur->left;
            while (node->right != NULL && node->right != cur)
                node = node->right;

            if (node->right == NULL) { /* 还没有线索化,则建立线索 */
                node->right = cur;
                cur = cur->left;
            }
            else { /* 如果已经线索化了,则访问节点,并删除线索 */
                visit(cur);
                node->right = NULL;
                cur = cur->right;
            }
        }
    }
}

复杂度分析:

空间复杂度: O(1) ,因为只用了两个辅助指针。

时间复杂度: O(n) 。证明时间复杂度为 O(n),最大的疑惑在于寻找中序遍历下二叉树中所有节点的前驱节点的时间复杂度是多少,即以下两行代码:

while (prev->right != NULL && prev->right != cur)

    prev = prev->right;

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

复杂度分析

二.先序遍历

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

步骤:

  1. 初始化当前节点 curroot 节点.

  2. 如果 cur 没有左孩子,则输出当前节点并将其右孩子作为当前节点,即 cur = cur->right

  3. 如果 cur 有左孩子,在当前节点 cur 的左子树中找到当前节点 cur中序遍历下的前驱节点

    a) 如果前驱节点的右孩子为空,将它的右孩子设置为当前节点。输出当前节点(在这里输出,这是与中序遍历唯一一点不同)。当前节点更新为当前节点的左孩子。

    b) 如果前驱节点的右孩子为当前节点,将它的右孩子重新设为空。当前节点更新为当前节点的右孩子。

  4. 重复以上2、3直到 cur 为空。

先序遍历
/**
 * @brief 使用Morrris算法先序遍历
 * @param[in] root 根节点
 * @param[in] visit 访问函数
 */
void
pre_order_morris(bt_node_t *root, int(*visit)(bt_node_t *))
{
 bt_node_t *cur;

 cur = root;
 while (cur != NULL) {
     if (cur->left == NULL) {
         visit(cur);
         cur = cur->right;
     }
     else {
         /* 查找前驱 */
         bt_node_t *node = cur->left;
         while (node->right != NULL && node->right != cur)
             node = node->right;

         if (node->right == NULL) {
             visit(cur);
             node->right = cur;
             cur = cur->left;
         }
         else { /* 已经线索化了,则删除线索 */
             node->right = NULL;
             cur = cur->right;
         }
     }
 }
}

复杂度分析

时间复杂度与空间复杂度都与中序遍历时的情况相同。

三.后序遍历

后续遍历稍显复杂,需要建立一个临时节点 dump ,令其左孩子是 root。并且还需要一个子过程,就是倒序输出某两个节点之间路径上的各个节点。

步骤:

当前节点设置为临时节点 dump

  1. 初始化当前节点 curroot 节点.

  2. 如果 cur 没有左孩子,则将其右孩子作为当前节点,即 cur = cur->right

  3. 如果 cur 有左孩子,在当前节点 cur 的左子树中找到当前节点 cur 在中序遍历下的前驱节点。

    a) 如果前驱节点的右孩子为空,将它的右孩子设置为当前节点。当前节点更新为当前节点的左孩子。

    b) 如果前驱节点的右孩子为当前节点,将它的右孩子重新设为空。 倒序输出从当前节点的左孩子到该前驱节点这条路径上的所有节点 。当前节点更新为当前节点的右孩子。

  4. 重复以上2、3直到当前节点为空。

后序遍历
/**
 * @brief 将指针反转
 * @param[in] from 反转路径的起点.
 * @param[in] to 反转路径的终点。
 */
static void 
reverse(bt_node_t *from, bt_node_t *to) 
{
 bt_node_t *x = from, *y = from->right, *z;
 if (from == to) return;
 /* 需要注意的是,这里并不是完全的路径反转,但是用在这里足够了.所以这里只是一个局部函数. */
 while (x != to) {
     z = y->right;
     y->right = x;
     x = y;
     y = z;
 }
}

/**
 * @brief 访问逆转后的路径上的所有节点.
 * @param[in] from 需要访问路径的起点,也就是应该最后一个访问的节点.
 * @param[in] to 需要访问路径的终点,也是第一个应该被访问的节点.
 */
static void
visit_reverse(bt_node_t* from, bt_node_t *to, int (*visit)(bt_node_t *))
{
 bt_node_t *p = to;
 reverse(from, to);
 while (1) {
     visit(p);
     if (p == from)
         break;
     p = p->right;
 }
 reverse(to, from);
}

/**
 * @brief 使用Morrris算法进行后序遍历.
 * @param[in] root 根节点
 * @param[in] visit 访问函数
 */
void
post_order_morris(bt_node_t *root, int(*visit)(bt_node_t *))
{
 bt_node_t dummy = { 0, NULL, NULL };
 bt_node_t *cur, *prev = NULL;

 dummy.left = root;
 cur = &dummy;
 while (cur != NULL) {
     if (cur->left == NULL) {
         prev = cur;
         cur = cur->right;
     }
     else {
         bt_node_t *node = cur->left;
         while (node->right != NULL && node->right != cur)
             node = node->right;

         if (node->right == NULL) { /* 尚未线索化,立即线索化 */
             node->right = cur;
             prev = cur;
             cur = cur->left;
         }
         else {
             /* visit_reverse其实有一个小问题,那就是prev->right在访问的时候会别修改 */
             visit_reverse(cur->left, prev, visit); 
             prev->right = NULL;
             prev = cur;
             cur = cur->right;
         }
     }
 }
}

复杂度分析

空间复杂度同样是 O(1) ;时间复杂度也是 O(n) ,倒序输出过程只不过是加大了常数系数。

四. 代码

#include <stdio.h>
#include <stdlib.h>

typedef int elem_t;

/**
 * @struct
 * @brief 二叉树节点.
 */
typedef struct bt_node_t {
 elem_t elem;
 struct bt_node_t *left;
 struct bt_node_t *right;
};

/**
 * @brief 使用Morrris算法进行中序遍历.
 * @param[in] root 根节点
 * @param[in] visit 访问函数
 */
void 
in_order_mirrors(bt_node_t *root, int(*visit)(bt_node_t *))
{
 bt_node_t *cur;
 cur = root;
 while (cur != NULL) {
     if (cur->left == NULL) {
         visit(cur);
         cur = cur->right;   /* 将右孩子作为当前节点 */
     }
     else {
         /* 查找cur节点的前驱节点 */
         bt_node_t *node = cur->left;
         while (node->right != NULL && node->right != cur)
             node = node->right;

         if (node->right == NULL) { /* 还没有线索化,则建立线索 */
             node->right = cur;
             cur = cur->left;
         }
         else { /* 如果已经线索化了,则访问节点,并删除线索 */
             visit(cur);
             node->right = NULL;
             cur = cur->right;
         }
     }
 }
}

/**
 * @brief 使用Morrris算法先序遍历
 * @param[in] root 根节点
 * @param[in] visit 访问函数
 */
void
pre_order_morris(bt_node_t *root, int(*visit)(bt_node_t *))
{
 bt_node_t *cur;

 cur = root;
 while (cur != NULL) {
     if (cur->left == NULL) {
         visit(cur);
         cur = cur->right;
     }
     else {
         /* 查找前驱 */
         bt_node_t *node = cur->left;
         while (node->right != NULL && node->right != cur)
             node = node->right;

         if (node->right == NULL) {
             visit(cur);
             node->right = cur;
             cur = cur->left;
         }
         else { /* 已经线索化了,则删除线索 */
             node->right = NULL;
             cur = cur->right;
         }
     }
 }
}

/**
 * @brief 将指针反转
 * @param[in] from 反转路径的起点.
 * @param[in] to 反转路径的终点。
 */
static void 
reverse(bt_node_t *from, bt_node_t *to) 
{
 bt_node_t *x = from, *y = from->right, *z;
 if (from == to) return;
 /* 需要注意的是,这里并不是完全的路径反转,但是用在这里足够了.所以这里只是一个局部函数. */
 while (x != to) {
     z = y->right;
     y->right = x;
     x = y;
     y = z;
 }
}

/**
 * @brief 访问逆转后的路径上的所有节点.
 * @param[in] from 需要访问路径的起点,也就是应该最后一个访问的节点.
 * @param[in] to 需要访问路径的终点,也是第一个应该被访问的节点.
 */
static void
visit_reverse(bt_node_t* from, bt_node_t *to, int (*visit)(bt_node_t *))
{
 bt_node_t *p = to;
 reverse(from, to);
 while (1) {
     visit(p);
     if (p == from)
         break;
     p = p->right;
 }
 reverse(to, from);
}

/**
 * @brief 使用Morrris算法进行后序遍历.
 * @param[in] root 根节点
 * @param[in] visit 访问函数
 */
void
post_order_morris(bt_node_t *root, int(*visit)(bt_node_t *))
{
 bt_node_t dummy = { 0, NULL, NULL };
 bt_node_t *cur, *prev = NULL;

 dummy.left = root;
 cur = &dummy;
 while (cur != NULL) {
     if (cur->left == NULL) {
         prev = cur;
         cur = cur->right;
     }
     else {
         bt_node_t *node = cur->left;
         while (node->right != NULL && node->right != cur)
             node = node->right;

         if (node->right == NULL) { /* 尚未线索化,立即线索化 */
             node->right = cur;
             prev = cur;
             cur = cur->left;
         }
         else {
             /* visit_reverse其实有一个小问题,那就是prev->right在访问的时候会别修改 */
             visit_reverse(cur->left, prev, visit); 
             prev->right = NULL;
             prev = cur;
             cur = cur->right;
         }
     }
 }
}

/**
 * @breif 分配一个新节点.
 * @param[in] e 新节点的数据
 */
bt_node_t* new_node(int e)
{
 bt_node_t* node = (bt_node_t *)malloc(sizeof(bt_node_t));
 node->elem = e;
 node->left = NULL;
 node->right = NULL;
 return node;
}

static int
print(bt_node_t *node)
{
 printf(" %d", node->elem);
 return 0;
}

void 
test_01()
{
 bt_node_t n1, n2, n3, n4;
 n1.elem = 1, n1.left = NULL, n1.right = &n2;
 n2.elem = 2, n2.left = NULL, n2.right = &n3;
 n3.elem = 3, n3.left = NULL, n3.right = &n4;
 n4.elem = 4, n4.left = NULL, n4.right = NULL;
 reverse(&n1, &n4);
 reverse(&n4, &n1);
}

int main()
{
 bt_node_t *n1 = new_node(1);
 bt_node_t *n2 = new_node(2);
 bt_node_t *n3 = new_node(3);
 bt_node_t *n4 = new_node(4);
 bt_node_t *n5 = new_node(5);
 bt_node_t *n6 = new_node(6);
 bt_node_t *n7 = new_node(7);
 bt_node_t *n8 = new_node(8);
 bt_node_t *n9 = new_node(9);

 n6->left = n2; n6->right = n7;
 n2->left = n1; n2->right = n4;
 n1->left = NULL; n1->right = NULL;
 n4->left = n3; n4->right = n5;
 n3->left = n3->right = NULL;
 n5->left = n5->right = NULL;
 n7->left = NULL; n7->right = n9;
 n9->left = n8; n9->right = NULL;
 n8->left = n8->right = NULL;
 
 pre_order_morris(n6, print);
 printf("\n");
 
 in_order_mirrors(n6, print);
 printf("\n");
 
 post_order_morris(n6, print);
 printf("\n");
 getchar();
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容