二叉树——遍历篇

二叉树很多算法题都与其遍历相关,笔者经过大量学习、思考,整理总结写下二叉树的遍历篇,涵盖递归和非递归实现。

1、二叉树数据结构及访问函数

  1. #include <stdio.h>
  2. #include <iostream>
  3. #include <stack>
  4. using namespace std;
  5. struct BTNode
  6. {
  7. int value;
  8. struct BTNode *left, *right;
  9. BTNode(int value_) :value(value_),left(NULL),right(NULL){};
  10. };
  11. //访问函数:可根据实际进行修改
  12. void visit(BTNode* node)
  13. {
  14. cout << node->value << " ";
  15. }

2、二叉树的遍历——深度优先遍历(DFS)(先序、中序、后序)

2.1、具体遍历顺序

  • 先序遍历:访问根节点、先序遍历左孩子、先序遍历右孩子 (根、左、右)

  • 中序遍历:中序遍历左孩子、访问根节点、中序遍历右孩子 (左、根、右)

  • 后序遍历:后序遍历左孩子、后序遍历右孩子、访问根节点 (左、右、根)

2.2、递归遍历

  1. /**
  2. * 先序遍历二叉树
  3. */
  4. void PreOrder(BTNode* root)
  5. {
  6. if (root)
  7. {
  8. visit(root);
  9. PreOrder(root->left);
  10. PreOrder(root->right);
  11. }
  12. }
  13. /**
  14. * 中序遍历二叉树
  15. */
  16. void InOrder(BTNode* root)
  17. {
  18. if (root)
  19. {
  20. InOrder(root->left);
  21. visit(root);
  22. InOrder(root->right);
  23. }
  24. }
  25. /**
  26. * 后序遍历二叉树
  27. */
  28. void PostOrder(BTNode* root)
  29. {
  30. if (root)
  31. {
  32. PostOrder(root->left);
  33. PostOrder(root->right);
  34. visit(root);
  35. }
  36. }

2.3、非递归遍历——借助栈

  • 借助栈,可以实现非递归遍历。
  • 在这里三种非递归遍历都总结和介绍一种算法思路,其栈中保存的节点可以用于路径搜索类的题目,即保存着从根节点到当前访问节点最短路径的所有节点信息,以*标记。
  • 介绍仅用于遍历访问的简单思路,栈中信息难以应用于搜索路径类的题目。

2.31 先序非递归遍历

* PreOrder_1a

算法过程:

由先序遍历过程可知,先序遍历的开始节点是根节点,然后用指针p 指向当前要处理的节点,沿着二叉树的左下方逐一访问,并将它们一一进栈,直至栈顶节点为最左下节点,指针p为空。此时面临两种情况。(1)对栈顶节点的右子树访问(如果有的话,且未被访问),对右子树进行同样的处理;(2)若无右子树,则用指针last记录最后一个访问的节点,指向栈顶节点,弹栈。如此重复操作,直至栈空为止。

  1. void PreOrder_1a(BTNode *root)
  2. {
  3. if (NULL == root) return;
  4. stack<BTNode*> stack;
  5. BTNode *p = root;
  6. BTNode *last = NULL;
  7. do {
  8. while (p)
  9. {
  10. visit(p);
  11. stack.push(p);
  12. p = p->left;
  13. }
  14. //此时p = NULL,栈顶节点为左子树最左节点
  15. if (!stack.empty())
  16. {
  17. BTNode *t = stack.top();
  18. if (t->right != NULL && t->right != last)
  19. {
  20. p = t->right;
  21. }
  22. else
  23. {//若无右子树,则指针P仍为空,则不断弹栈(沿着双亲方向)寻找有未被访问的右子树的节点
  24. last = t;
  25. stack.pop();
  26. }
  27. }
  28. } while (!stack.empty());
  29. }
* PreOrder_1b

算法过程:此算法与PreOrder_1a有异曲同工之处,巧妙之处在于对上述两种情况的处理。指针p记录当前栈顶结点的前一个已访问的结点。若无右子树、或者右子树已被访问,则用指针p记录当前栈顶节点,弹栈,不断沿着双亲方向寻找有未访问右子树的节点,找到即退出循环,否则直至栈空。当栈顶节点的右孩子是p时,则将cur指向右孩子,设置flag =0 ,退出当前搜索循环(不断弹栈,搜索有右节点且未被访问的祖先节点),然后对右子树进行同样的处理。如此反复操作,直至栈空为止。

  1. void PreOrder_1b(BTNode *root)
  2. {
  3. if (NULL == root) return;
  4. stack<BTNode*>stack;
  5. int flag;
  6. BTNode *cur = root,*p;
  7. do{
  8. while (cur)
  9. {
  10. visit(cur);
  11. stack.push(cur);
  12. cur = cur->left;
  13. }
  14. //执行到此处时,栈顶元素没有左孩子或左子树均已访问过
  15. p = NULL; //p指向栈顶结点的前一个已访问的结点
  16. flag = 1; //表示*cur的左孩子已访问或者为空
  17. while (!stack.empty() && flag == 1)
  18. {
  19. cur = stack.top();
  20. if (cur->right == p) //表示右孩子结点为空或者已经访问完右孩子结点
  21. {
  22. stack.pop();
  23. p = cur; //p指向刚访问过的结点
  24. }
  25. else
  26. {
  27. cur = cur->right; //cur指向右孩子结点
  28. flag = 0; //设置未被访问的标记
  29. }
  30. }
  31. } while (!stack.empty());
  32. }
PreOrder_2a && PreOrder_2b

PreOrder_2a 和 PreOrder_2b 算法思路大体相同,PreOrder_2a 实现比较简洁

算法过程:

用指针p指向当前要处理的节点,沿着左下方向逐一访问并压栈,直至指针P为空,栈顶节点为最左下节点。然后p指向栈顶节点的右节点(不管是否空);若右节点为空,则继续弹栈。若右节点非空,则按上述同样处理右节点。如此重复操作直至栈空为止。

缺陷:PreOrder_2 当访问栈顶节点的右节点时,会丢失当前栈顶节点信息,导致从根节点到当前栈顶节点的右节点路径不完整。

优点:算法思路清晰易懂,逻辑简单。

  1. void PreOrder_2a(BTNode *root)
  2. {
  3. if (NULL == root) return;
  4. BTNode *p = root;
  5. stack<BTNode*> stack;
  6. while (p || !stack.empty())
  7. {
  8. if (p)
  9. {
  10. visit(p);
  11. stack.push(p);
  12. p = p->left;
  13. }
  14. else
  15. {
  16. BTNode *top = stack.top();
  17. p = top->right;
  18. stack.pop();
  19. }
  20. }
  21. }
  22. void PreOrder_2b(BTNode *root)
  23. {
  24. if (NULL == root) return;
  25. BTNode *p = root;
  26. stack<BTNode*> stack;
  27. while (!stack.empty() ||p)
  28. {
  29. while (p)
  30. {
  31. visit(p);
  32. stack.push(p);
  33. p = p->left;
  34. }
  35. if (!stack.empty())
  36. {
  37. BTNode *top = stack.top();
  38. p = top->right;
  39. stack.pop();
  40. }
  41. }
  42. }
PreOrder_3

算法过程:

用指针p指向当前要处理的节点。先把根节点压栈,栈非空时进入循环,出栈栈顶节点并访问,然后按照先序遍历先左后右的逆过程把当前节点的右节点压栈(如果有的话),再把左节点压栈(如果有的话)。如此重复操作,直至栈空为止。

特点:栈顶节点保存的是先序遍历下一个要访问的节点,栈保存的所有节点不是根到要访问节点的路径。

  1. void PreOrder_3(BTNode *root)
  2. {
  3. if (NULL == root) return;
  4. BTNode *p = root;
  5. stack<BTNode *> stack;
  6. stack.push(p);
  7. while (!stack.empty())
  8. {
  9. p = stack.top();
  10. stack.pop();
  11. visit(p);
  12. if (p->right)
  13. stack.push(p->right);
  14. if (p->left)
  15. stack.push(p->left);
  16. }
  17. }

2.32 中序非递归遍历

中序遍历非递归算法要把握访问栈顶节点的时机。

* InOrder_1

算法过程:

用指针cur指向当前要处理的节点。先扫描(并非访问)根节点的所有左节点并将它们一一进栈,直至栈顶节点为最左下节点,指针cur为空。

此时主要分两种情况。(1)栈顶节点的左节点为空或者左节点已访问,访问栈顶节点(访问位置很重要!),若有右节点则将cur指向右节点,退出当前while循环,对右节点进行上述同样的操作,若无右节点则用指针p记录站顶节点并弹栈;(2)站顶节点的右节点为空或已访问,用指针p记录站顶节点并弹栈。

内部第二个while循环可称为访问搜索循环,在栈顶节点的左节点为空或者左节点已访问的情况下,访问栈顶节点,若有右节点,则退出循环,否则不断弹栈。

如此重复操作,直至栈空为止。

  1. void InOrder_1(BTNode *root)
  2. {
  3. if (NULL == root) return;
  4. stack<BTNode *>stack;
  5. BTNode *cur = root,*p = NULL;
  6. int flag = 1;
  7. do{
  8. while (cur){
  9. stack.push(cur);
  10. cur = cur->left;
  11. }
  12. //执行到此处时,栈顶元素没有左孩子或左子树均已访问过
  13. p = NULL; //p指向栈顶结点的前一个已访问的结点
  14. flag = 1; //表示*cur的左孩子已访问或者为空
  15. while (!stack.empty() && flag)
  16. {
  17. cur = stack.top();
  18. if (cur->left == p) //左节点为空 或者左节点已访问
  19. {
  20. visit(cur); //访问当前栈顶节点
  21. if (cur->right) //若有右节点 当前节点指向右节点,并退出当前循环,进入上面的压栈循环
  22. {
  23. cur = cur->right;
  24. flag = 0; //flag = 0 标记右节点的左子树未访问
  25. }
  26. else //当前节点没有右节点,P记录访问完的当前节点,弹栈
  27. {
  28. p = cur;
  29. stack.pop();
  30. }
  31. }
  32. else // 此时 cur->right == P 即访问完右子树 ,P记录访问完的当前节点,弹栈
  33. {
  34. p = cur;
  35. stack.pop();
  36. }
  37. }
  38. } while (!stack.empty());
  39. }

InOrder_2a && InOrder_2b

算法过程:

用指针p指向当前要处理的节点。先扫描(并非访问)根节点的所有左节点并将它们一一进栈,当无左节点时表示栈顶节点无左子树,然后出栈这个节点,并访问它,将p指向刚出栈节点的右孩子,对右孩子进行同样的处理。如此重复操作,直至栈空为止。

需要注意的是:当节点*p的所有左下节点入栈后,这时的栈顶节点要么没有左子树,要么其左子树已访问,就可以访问栈顶节点了!

InOrder_2a 、 InOrder_2b 与 PreOrder_1a 、PreOrder_1b 代码基本相同,唯一不同的是访问节点的时机,把握好可方便理解和记忆。

  1. void InOrder_2a(BTNode *root)
  2. {
  3. if (NULL == root) return;
  4. BTNode *p = root;
  5. stack<BTNode *>stack;
  6. while (p || !stack.empty())
  7. {
  8. while (p)
  9. {
  10. stack.push(p);
  11. p = p->left;
  12. }
  13. if (!stack.empty())
  14. {
  15. p = stack.top();
  16. visit(p);
  17. stack.pop();
  18. }
  19. }
  20. }
  21. void InOrder_2b(BTNode *root)
  22. {
  23. if (NULL == root) return;
  24. stack<BTNode *>stack;
  25. BTNode *p = root;
  26. while (p || !stack.empty())
  27. {
  28. while (p)
  29. {
  30. stack.push(p);
  31. p = p->left;
  32. }
  33. if (!stack.empty())
  34. {
  35. p = stack.top();
  36. visit(p);
  37. p = p->right;
  38. stack.pop();
  39. }
  40. }
  41. }

2.33 后序非递归遍历

*PostOrder_1

算法过程:

用指针cur指向当前要处理的节点。先扫描(并非访问)根节点的所有左节点并将它们一一进栈,直至栈顶节点为最左下节点,指针cur为空。此时有两种情况。(1)栈顶节点的右节点为空或已访问,访问当前栈顶节点(访问时机很重要!),用指针p保存刚刚访问过的节点(初值为NULL),弹栈;(2)栈顶节点有未被访问的右节点,设置flag,退出当前访问搜索循环。如此重复处理,直至栈空为止。

  1. void PostOrder_1(BTNode *root)
  2. {
  3. if (NULL == root) return;
  4. stack<BTNode*>stack;
  5. int flag;
  6. BTNode *cur = root, *p;
  7. do{
  8. while (cur)
  9. {
  10. stack.push(cur);
  11. cur = cur->left;
  12. }
  13. //执行到此处时,栈顶元素没有左孩子或左子树均已访问过
  14. p = NULL; //p指向栈顶结点的前一个已访问的结点
  15. flag = 1; //表示*cur的左孩子已访问或者为空
  16. while (!stack.empty() && flag == 1)
  17. {
  18. cur = stack.top();
  19. if (cur->right == p)
  20. {//表示右孩子结点为空,或者已经访问cur的右子树(p必定是后序遍历cur的右子树最后一个访问节点)
  21. visit(cur);
  22. p = cur; //p指向刚访问过的结点
  23. stack.pop();
  24. }
  25. else
  26. {
  27. cur = cur->right; //cur指向右孩子结点
  28. flag = 0; //表示*cur的左孩子尚未访问过
  29. }
  30. }
  31. } while (!stack.empty());
  32. }

PostOrder_2

算法过程:

先把根节点压栈,用指针p记录上一个被访问的节点。在栈为空时进入循环,取出栈顶节点。

此时有两种情况:

(1)访问当前节点,注意把握访问的时机,如果当前节点是叶子节点,访问当前节点;如果上一个访问的节点是当前节点的左节点(说明无右节点),访问当前节点;如果上一个访问的节点是当前节点的右节点(说明左右节点都有),访问当前节点;指针p记录当前访问的节点,弹栈。 所以,只有当前节点是叶子节点,或者上一个访问的节点是当前节点的左节点(无右) 或右节点(左右都有) ,才可以访问当前节点。

(2)压栈。 后序遍历顺序为 左、右、根,按照逆序,先把右压栈,再把左压栈(如果有的话)。

如此重复操作,直至栈空为止。

  1. void PostOrder_2(BTNode* root)
  2. {
  3. if (NULL == root) return;
  4. BTNode *cur = root, *p=NULL;
  5. stack<BTNode*> stack;
  6. stack.push(root);
  7. while (!stack.empty())
  8. {
  9. cur = stack.top();
  10. if (cur->left == NULL && cur->right == NULL || (p!= NULL && (cur->left==p || cur->right == p)))
  11. {
  12. visit(cur);
  13. stack.pop();
  14. p = cur;
  15. }
  16. else
  17. {
  18. if (cur->right)
  19. stack.push(cur->right);
  20. if (cur->left)
  21. stack.push(cur->left);
  22. }
  23. }
  24. }

3、二叉树的遍历——广度优先遍历(BFS)

  • 二叉树的广度优先遍历,就是层次遍历,借助队列实现

    在进行层次遍历时,对某一层的节点访问完后,再按照对它们的访问次序对各个节点的左、右孩子顺序访问。这样一层一层进行,先访问的节点其左、右孩子也要先访问,与队列的操作原则比较吻合,且符合广度优先搜索的特点。

    算法过程:先将根节点进队,在队不空时循环;从队列中出列一个节点,访问它;若它有左孩子节点,将左孩子节点进队;若它有右孩子节点,将右孩子节点进队。如此重复操作直至队空为止。

  1. void LevelOrder(BTNode *root)
  2. {
  3. if (NULL == root) return;
  4. queue<BTNode*> queue;
  5. queue.push(root);
  6. BTNode* p;
  7. while (!queue.empty())
  8. {
  9. p = queue.front();
  10. queue.pop();
  11. visit(p);
  12. if (p->left)
  13. queue.push(p->left);
  14. if (p->right)
  15. queue.push(p->right);
  16. }
  17. }

原创所有,转载务必注明出处。

二叉树——遍历篇(递归/非递归,C++)的更多相关文章

  1. 二叉树的先序、中序以及后序遍历(递归 && 非递归)

    树节点定义: class TreeNode { int val; TreeNode left; TreeNode right; TreeNode(int x) { val = x; } } 递归建立二 ...

  2. 二叉树的递归,非递归遍历(C++)

    二叉树是一种非常重要的数据结构,很多其它数据结构都是基于二叉树的基础演变而来的.对于二叉树,有前序.中序以及后序三种遍历方法.因为树的定义本身就是递归定义,因此采用递归的方法去实现树的三种遍历不仅容易 ...

  3. 树的广度优先遍历和深度优先遍历(递归非递归、Java实现)

    在编程生活中,我们总会遇见树性结构,这几天刚好需要对树形结构操作,就记录下自己的操作方式以及过程.现在假设有一颗这样树,(是不是二叉树都没关系,原理都是一样的) 1.广度优先遍历 英文缩写为BFS即B ...

  4. 【数据结构】——搜索二叉树的插入,查找和删除(递归&非递归)

    一.搜索二叉树的插入,查找,删除 简单说说搜索二叉树概念: 二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值 若它的右 ...

  5. Reverse Linked List 递归非递归实现

    单链表反转--递归非递归实现 Java接口: ListNode reverseList(ListNode head) 非递归的实现 有2种,参考 头结点插入法 就地反转 递归的实现 1) Divide ...

  6. Java实现二叉树的创建、递归/非递归遍历

    近期复习数据结构中的二叉树的相关问题,在这里整理一下 这里包含: 1.二叉树的先序创建 2.二叉树的递归先序遍历 3.二叉树的非递归先序遍历 4.二叉树的递归中序遍历 5.二叉树的非递归中序遍历 6. ...

  7. 递归/非递归----python深度遍历二叉树(前序遍历,中序遍历,后序遍历)

    递归代码:递归实现很简单 '二叉树结点类' class TreeNode: def __init__(self, x): self.val = x self.left = None self.righ ...

  8. C++二叉树前中后序遍历(递归&非递归)统一代码格式

    统一下二叉树的代码格式,递归和非递归都统一格式,方便记忆管理. 三种递归格式: 前序遍历: void PreOrder(TreeNode* root, vector<int>&pa ...

  9. 二叉树总结—建树和4种遍历方式(递归&&非递归)

    版权声明:本文为博主原创文章,未经博主同意不得转载. https://blog.csdn.net/u013497151/article/details/27967155 今天总结一下二叉树.要考离散了 ...

随机推荐

  1. hdu5418--Victor and World(floyd+状压dp)

    题目链接:点击打开链接 题目大意:有n个城市.在n个城市之间有m条双向路.每条路有一个距离.如今问从1号城市去游览其他的2到n号城市最后回到1号城市的最短路径(保证1能够直接或间接到达2到n).(n& ...

  2. XMind常用快捷方式汇总

    快捷键(Windows) 快捷键(Mac) 描述 Ctrl+N Command+N 建立新工作簿 Ctrl+O Command+O 开启工作簿 Ctrl+S Command+S 储存目前工作簿 Ctr ...

  3. 移动浏览器H5页面通过scheme打开本地应用

    在移动端浏览器H5页面中,点击按钮打开本地应用主要通过 scheme 协议.本文主要介绍如何在浏览器H5页面中通过 scheme 协议打开本地应用. scheme协议定义 scheme 是一种页面之间 ...

  4. POST/有道翻译 有bug

    1.发现在翻译时地址没有变,那是POST请求. 2.通过fidder抓包工具抓取url 3.对data分析,发现每次salt和sign都在变化. 4.查看源码,先用站长工具http://tool.ch ...

  5. Hadoop:Rack Awareness

    副本的放置对HDFS可靠性和性能至关重要. 优化副本放置HDFS有别于其他大多数分布式文件系统. 这是一个功能,需要大量的调优和经验. 基于机架感知(rack awareness)的副本放置策略的目的 ...

  6. intellij idea svn使用一 导入、更新、提交、解决冲突

    大体上是转载,针对版本14有一些特殊的添加. 查看svn的资源库: 下面的多出了一个svn的窗口,在左边有加号可以添加一个svn的库 输入svn的地址,我用的是本地的测试,所以地址为svn://127 ...

  7. 《程序员的思维修炼:开发认知潜能的九堂课》【PDF】下载

    <程序员的思维修炼:开发认知潜能的九堂课>[PDF]下载链接: https://u253469.ctfile.com/fs/253469-231196325 内容简介 运用一门程序设计语言 ...

  8. (转)为Xcode添加删除行、复制行快捷键

    转摘链接:http://www.jianshu.com/p/cc6e13365b7e 在使用eclipse过程中,特喜欢删除一行和复制一行的的快捷键.而恰巧Xcode不支持这两个快捷键,再一次的恰巧让 ...

  9. Java 银行家算法

    实验存档,代码特别烂.. 测试.java package operating.test; import operating.entity.bank.Bank; import operating.ent ...

  10. bzoj 2750: [HAOI2012]Road

    Description C国有n座城市,城市之间通过m条单向道路连接.一条路径被称为最短路,当且仅当不存在从它的起点到终点的另外一条路径总长度比它小.两条最短路不同,当且仅当它们包含的道路序列不同.我 ...