【问题】介绍一种时间复杂度O(N),额外空间复杂度O(1)的二叉树的遍
历方式,N为二叉树的节点个数
无论是递归还是非递归,避免不了额外空间为O(h),h 为二叉树的高度
使用morris遍历,即利用空节点空间
morris遍历:
【思路:】
空间复杂度O(1)的要求很严格。常规的递归实现是显然不能满足要求的[其空间复杂度是树的深度O(h)]。本篇文章介绍著名的Morris遍历,该方法利用了二叉树结点中大量指向null的指针。

常规的栈结构遍历方式,遍历到某个节点之后并不能回到上层的结点,这是由二叉树本身的结构所限制的,每个结点并没有指向父节点的指针,因此需要使用栈来完成回到上层结点的步骤。

Morris遍历避免了使用栈结构,让下层有指向上层的指针,但并不是所有的下层结点都有指向上层的指针([这些指针也称为空闲指针])。

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

一、中序遍历步骤:

1. 来到当前节点cur, 如果当前节点的左孩子为空,则输出当前节点并将其右孩子作为当前节点, 即cur = cur->right。
2. 如果当前节点的左孩子不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点, 即找到当前节点左子树上的最右节点,记为mostRight
a) 如果mostRight的右孩子为空,将它的右孩子设置指向为当前节点cur。当前节点更新为当前节点的左孩子, 即cur = cur->left。
b) 如果mostRight的右孩子为当前节点cur,将它的右孩子重新设为空(恢复树的形状)。输出当前节点。当前节点更新为当前节点的右孩子, 即cur = cur->right。
3. 重复以上1、2直到当前节点为空。
图示:

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

代码:

  1. //中序遍历
  2. void morrisIn(Node* head)
  3. {
  4. if(head==null)
  5. return;
  6. Node* cur1=head;
  7. Node* cur2=null;
  8. while(cur1 != null)
  9. {
  10. cur2=cur1.left;
  11. if(cur2 != null)
  12. {
  13. while(cur2.right != null && cur2.right !=cur1)
  14. cur2=cur2. right; //找到最右节点
  15. if(cur2. right==null)
  16. {
  17. cur2. right=cur1; //辅助节点
  18. cur1=cur1. left;
  19. continue;
  20. }
  21. else
  22. cur2. right=null;
  23. }
  24. cout<<cur1.value<<" ";
  25. cur1=cur1. right;
  26. }
  27. cout<<endl;
  28. }

复杂度分析:
空间复杂度:O(1),因为只用了两个辅助指针。
时间复杂度:O(n)。证明时间复杂度为O(n),最大的疑惑在于寻找中序遍历下二叉树中所有节点的前驱节点的时间复杂度是多少,即以下两行代码:
1 while (prev->right != NULL && prev->right != cur)
2 prev = prev->right;
直觉上,认为它的复杂度是O(nlgn),因为找单个节点的前驱节点与树的高度有关。但事实上,寻找所有节点的前驱节点只需要O(n)时间。n个节点的二叉树中一共有n - 1条边,整个过程中每条边最多只走2次,一次是为了定位到某个节点,另一次是为了寻找上面某个节点的前驱节点,如下图所示,其中红色是为了定位到某个节点,黑色线是为了找到前驱节点。所以复杂度为O(n)。

二、前序遍历

前序遍历与中序遍历相似,代码上只有一行不同,不同就在于输出的顺序。
步骤:
1. 如果当前节点的左孩子为空,则输出当前节点并将其右孩子作为当前节点。
2. 如果当前节点的左孩子不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点。
a) 如果前驱节点的右孩子为空,将它的右孩子设置为当前节点。输出当前节点(在这里输出,这是与中序遍历唯一一点不同)。当前节点更新为当前节点的左孩子。
b) 如果前驱节点的右孩子为当前节点,将它的右孩子重新设为空。当前节点更新为当前节点的右孩子。
3. 重复以上1、2直到当前节点为空。
图示:

代码:

  1. //前序遍历
  2. void morrisIn(Node* head)
  3. {
  4. if(head==null)
  5. return;
  6. Node* cur1=head;
  7. Node* cur2=null;
  8. while(cur1 != null)
  9. {
  10. cur2=cur1.left;
  11. if(cur2 != null)
  12. {
  13. while(cur2.right != null && cur2.right !=cur1)
  14. cur2=cur2. right; //找到最右节点
  15. if(cur2. right==null)
  16. {
  17. cur2. right=cur1; //辅助节点
  18. cout << cur1.value << " ";//前序遍历是先打印
  19. cur1=cur1. left;
  20. continue;
  21. }
  22. else
  23. cur2. right=null;
  24. }
  25. else
  26. cout<<cur1.value<<" ";
  27. cur1=cur1. right;
  28. }
  29. cout << endl;
  30. }

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

三、后序遍历
后续遍历稍显复杂,需要建立一个临时节点dump,令其左孩子是root。并且还需要一个子过程,就是倒序输出某两个节点之间路径上的各个节点。
步骤:
当前节点设置为临时节点dump。
1. 如果当前节点的左孩子为空,则将其右孩子作为当前节点。
2. 如果当前节点的左孩子不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点。
a) 如果前驱节点的右孩子为空,将它的右孩子设置为当前节点。当前节点更新为当前节点的左孩子。
b) 如果前驱节点的右孩子为当前节点,将它的右孩子重新设为空。倒序输出从当前节点的左孩子到该前驱节点这条路径上的所有节点。当前节点更新为当前节点的右孩子。
3. 重复以上1、2直到当前节点为空。
图示:

代码:

  1. //后序遍历
  2. Node* reverseEdge(Node* from)
  3. {
  4. Node* pre=null;
  5. Node* next=null;
  6. while(from != null)
  7. {
  8. next=from. right;
  9. from.right=pre;
  10. pre=from;
  11. from=next;
  12. }
  13. return pre;
  14. }
  15. void printEdge(Node* head)
  16. {
  17. Node* tail=reverseEdge(head);
  18. Node* cur=tail;
  19. while(cur!=null)
  20. {
  21. cout << cur.value << " ";
  22. cur=cur. right;
  23. }
  24. reverseEdge(tail);
  25. }
  26. void morrisPos(Node* head)
  27. {
  28. if(head==null)
  29. return;
  30. Node* cur1=head;
  31. Node* cur2=null;
  32. while(cur1 != null)
  33. {
  34. cur2=cur1.left;
  35. if(cur2!=null)
  36. {
  37. while(cur2.right != null && cur2.right!= cur1)
  38. cur2=cur2.right;//找到最右节点
  39. if(cur2.right==null)
  40. {
  41. cur2.right=cur1;
  42. cur1=cur1.left;
  43. continue;
  44. }
  45. else
  46. {
  47. cur2.right=null;
  48. printEdge(cur1.left);
  49. }
  50. }
  51. cur1=cur1. right;
  52. }
  53. printEdge(head);
  54. cout << endl;
  55. }

复杂度分析:

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

左神算法书籍《程序员代码面试指南》——3_05Morris遍历二叉树的神级方法【★★★★★】的更多相关文章

  1. 《程序员代码面试指南》第三章 二叉树问题 遍历二叉树的神级方法 morris

    题目 遍历二叉树的神级方法 morris java代码 package com.lizhouwei.chapter3; /** * @Description:遍历二叉树的神级方法 morris * @ ...

  2. 程序员代码面试指南:IT名企算法与数据结构题目最优解

      第1章栈和队列 1设计一个有getMin功能的栈(士★☆☆☆) 1由两个栈组成的队列(尉★★☆☆) 5如何仅用递归函数和栈操作逆序一个栈(尉★★☆☆) 8猫狗队列(士★☆☆☆)10用一个栈实现另一 ...

  3. 程序员代码面试指南 IT名企算法与数据结构题目最优解

    原文链接 这是一本程序员面试宝典!书中对IT名企代码面试各类题目的最优解进行了总结,并提供了相关代码实现.针对当前程序员面试缺乏权威题目汇总这一痛点,本书选取将近200道真实出现过的经典代码面试题,帮 ...

  4. 左神算法书籍《程序员代码面试指南》——1_08构造数组的MaxTree

    [题目] 将一个没有重复数字的数组中的数据构造一个二叉树 每个节点都是该子树的最大值 [要求] 时间复杂度为O(N)[题解] 使用单调栈,栈的顺序是维持从大到小排序 通过使用单调栈,将数组中中所有数的 ...

  5. 左神算法书籍《程序员代码面试指南》——2_11将单链表的每K个节点之间逆序

    [题目]给定一个单链表的头节点head,实现一个调整单链表的函数,使得每K个节点之间逆序,如果最后不够K个节点一组,则不调整最后几个节点.例如:链表:1->2->3->4->5 ...

  6. 左神算法书籍《程序员代码面试指南》——1_01设计一个有getMin功能的栈

    [题目] 实现一个特殊的栈,在实现栈的基本功能的基础上,再实现返回栈中最小元素的操作. [要求] 1.pop.push.getMin操作的时间复杂度都是O(1).2.设计的栈类型可以使用现成的栈结构. ...

  7. 左神算法书籍《程序员代码面试指南》——2_03删除链表的中间节点和a/b处的节点

    [题目]给定链表的头节点head,实现删除链表的中间节点的函数.例如:不删除任何节点:1->2,删除节点1:1->2->3,删除节点2:1->2->3->4,删除节 ...

  8. 左神算法书籍《程序员代码面试指南》——2_02在单链表和双链表中删除倒数第k个字节

    [题目]分别实现两个函数,一个可以删除单链表中倒数第K个节点,另一个可以删除双链表中倒数第K个节点.[要求]如果链表长度为N,时间复杂度达到O(N),额外空间复杂度达到O(1).[题解]从头遍历链表, ...

  9. 左神算法书籍《程序员代码面试指南》——1_10最大值减去最小值小于或等于num的子数组数量

    [题目]给定数组arr和整数num,共返回有多少个子数组满足如下情况:max(arr[i.j]) - min(arr[i.j]) <= num max(arfi.j])表示子数组ar[ij]中的 ...

随机推荐

  1. SSH连接时,长时间不操作就断开的解觉办法

    1.第一次尝试失败 修改/etc/ssh/sshd_config文件, 找到 ClientAliveInterval 0 ClientAliveCountMax 3 并将注释符号("#&qu ...

  2. Android 防止切换横屏闪退

    <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="ht ...

  3. ruby on rails笔记

    一.新建rails项目步骤: 1.生成新项目 rails new demo cd demo vi Gemfile 末尾end前增加   gem 'execjs'   gem 'therubyracer ...

  4. github如何用浏览器直接打开项目里的html页面?

    very easy 第一步 点击html页面 第二步,在地址栏前加 htmlpreview.github.io/?就可以访问

  5. PaperWeekly 第五期------从Word2Vec到FastText

    PaperWeekly 第五期------从Word2Vec到FastText 张俊 10 个月前 引 Word2Vec从提出至今,已经成为了深度学习在自然语言处理中的基础部件,大大小小.形形色色的D ...

  6. bash数组总结

    bash数组操作 bash支持两种数组,一种是索引数组,一种是关联数组 索引数组 数组的值类型是任意的,索引也未必一定要连续,当做列表理解更好 下面总结下索引数组,即列表: 1. 声明 declare ...

  7. JVM内核-原理、诊断与优化学习笔记(一):初识JVM

    文章目录 JVM的概念 JVM是Java Virtual Machine的简称.意为Java虚拟机 虚拟机 有哪些虚拟机 VMWare或者Visual Box都是使用软件模拟物理CPU的指令集 JVM ...

  8. 把Debian 设置中文环境

    要支持区域设置,首先要安装locales软件包: apt-get install locales 然后配置locales软件包: dpkg-reconfigure locales 在界面中钩选上“zh ...

  9. 注解到处excel

    package com.cxy.domain.poi; import java.lang.annotation.ElementType; import java.lang.annotation.Ret ...

  10. HttpWebRequest 基础连接已经关闭: 接收时发生错误 GetRequestStream 因为算法不同,客户端和服务器无法通信。

    在代码行 HttpWebRequest objRequest = (HttpWebRequest)HttpWebRequest.Create(sUrl 前面加上 ServicePointManager ...