树的遍历是当且仅当访问树中每个节点一次的过程。遍历可以解释为把所有的节点放在一条线上,或者将树线性化。
遍历的定义只指定了一个条件:每个节点仅访问一次,没有指定这些节点的访问顺序。因此,节点有多少种排列方式,就有多少种遍历方法。对于有n个节点的树,共有n!个不同的遍历方式。然而大多数遍历方式是混乱的,很难从中找到规律,因此实现这样的遍历缺乏普遍性。经过前人总结,这里介绍两种遍历方式,即广度优先遍历与深度优先遍历。
  • 广度优先遍历
广度优先遍历是从根节点开始,向下逐层访问每个节点。当使用队列时,这种遍历方式的实现相当直接。假设从上到下、从左到右进行广度优先遍历。在访问了一个节点之后,它的子节点就放到队列的末尾,然后访问队列头部的节点。对于层次为n的节点,它的子节点位于第n+1层,如果将该节点的所有子节点都放到队列的末尾,那么,这些节点将在第n层的所有节点都访问之后再访问。
代码实现如下:
//广度优先遍历
void breadthFirst(Node* &bt){queue<Node*> que;Node* BT = bt;que.push(BT);while (!que.empty()){BT = que.front();que.pop();printf("%c", BT->data);if (BT->lchild)que.push(BT->lchild);if (BT->rchild)que.push(BT->rchild);}
}复制代码
广度优先遍历的逻辑比较简单,这里就不过多介绍了。
  • 深度优先遍历
树的深度优先遍历相当有趣。深度优先遍历将尽可能的向下进行,在遇到第一个转折点时,向左或向右一步,然后再尽可能的向下发展。这一过程一直重复,直至访问了所有节点为止。然而,这一定义并没有清楚的指明什么时候访问节点:在沿着树向下进行之前还是在折返之后呢?因此,深度优先遍历有着几种变种。
假如现在有个简单的二叉树:
我们从左到右进行深度优先遍历:
你会发现其实每个节点遍历到了3次:
红色的是第一次遍历到的次序,蓝色的是第二次遍历到的次序,黑色的是第三次遍历到的次序。这其实就是二叉树从左到右的前序、中序、后序遍历,从右到左也有三种遍历,和上面遍历的类似。前辈们对这三种遍历方式进一步总结,将深度遍历分解成3个任务:
  • V-访问节点
  • L-遍历左子树
  • R-遍历右子树
前序遍历其实是VLR,中序遍历是LVR,后序遍历是LRV
我们来讨论深度遍历的实现方法,毫无疑问,递归是非常简单并且容易理解的方式,代码如下:
//从左向右前中后递归遍历
void leftPreOrder(Node *bt)
{if (bt){printf("%c", bt->data);leftPreOrder(bt->lchild);leftPreOrder(bt->rchild);}
}void leftInOrder(Node *bt)
{if (bt){leftInOrder(bt->lchild);printf("%c", bt->data);leftInOrder(bt->rchild);}
}void leftPostOrder(Node *bt)
{if (bt){leftPostOrder(bt->lchild);leftPostOrder(bt->rchild);printf("%c", bt->data);}
}复制代码
可以看到三种遍历方式的实现代码差别很小,其实差别就是V、L、R的顺序不同,递归方式没啥难点,这里就不多讲了。重点在于下面的非递归实现方式。
递归代码是有缺陷的,毫无疑问会给运行时栈带来沉重的负担,因为每次递归(调用函数)都会在运行时栈上开辟新的函数栈空间,如果二叉树的高度比较大,程序运行可能直接爆掉栈空间,这是不能容忍的。因此,我们要实现非递归方式的二叉树深度遍历。
寻常的非递归遍历方式实质上就是模拟上面动态图中箭头的转移过程,在三种遍历方式中前序遍历是最简单的,无论是哪一种遍历方式,都需要借助栈来实现。因为在深度遍历中会有回溯的过程,这就要求程序可以记住以往遍历过的节点,只有满足这一点才能回溯成功,这就需要借助栈来保存遍历节点的次序。下面是非递归遍历方式的代码:
节点定义:typedef struct BiNode{int data;struct BiNode * lchild;struct BiNode * rchild;
}BiNode,*BiTree;前序遍历:void preOrderBiTree(BiNode * root){if(root == NULL)return;BiNode * node = root;stack<BiNode*> nodes;while(node || !nodes.empty()){while(node != NULL){nodes.push(node);printf("%d",node->data);node = node -> lchild;}node = nodes.top();//回溯到父节点nodes.pop();node = node -> rchild;}
}复制代码
可以看到,在将节点入栈之前已经访问过节点,栈在这里的作用只有回溯。
中序遍历代码如下:
void inOrderBinaryTree(BiNode * root){if(root == NULL)return;BiNode * node = root;stack<BiNode*> nodes;while(node || !nodes.empty()){while(node != NULL){nodes.push(node);node = node ->lchild;}node = nodes.top();printf("%d ",node ->data);nodes.pop();node = node ->rchild;}
}复制代码
中序遍历中访问节点发生在出栈之前,栈在这里的作用不仅仅是回溯,出栈还代表着第二次遍历到该节点。后序遍历更是需要程序实现V之前L和R操作已经完成,这就需要程序具有记忆功能。
后序遍历代码如下:
void PostOrderS(Node *root) {Node *p = root, *r = NULL;stack<Node*> s;while (p!=NULL || !s.empty()) {if (p!=NULL) {//走到最左边s.push(p);p = p->left;}else {p = s.top();if (p->right!=NULL && p->right != r)//右子树存在,未被访问p = p->right;else {s.pop();visit(p->val);r = p;//记录最近访问过的节点p = NULL;//节点访问完后,重置p指针}}//else}//while
}复制代码
后序遍历中最重要的逻辑是保证节点的左右子树访问之后再访问该节点,程序中使用变量保存了上次访问的节点。如果节点i存在右子树,并且上个访问的节点是右子树的根节点,才能访问i节点。
毫无疑问,以上通过程序模拟前中后三种遍历过程,就是我们常说的过程式代码,如果你在面试中遇到这类问题,情急之下不一定能写的出来。究其原因在于思想不统一,前中后遍历的三种代码实现方式是割裂开的。怎么办?笔者探究了一种实现方式,从思想上统一前中后三种遍历过程。
首先用自然语言描述实现逻辑:
假如有以下二叉树:
我们来实现后序遍历,后序遍历的逻辑是LRV,也就是说,对于一颗二叉树,我们希望首先遍历左子树,然后是右子树,最后是根节点,放到栈里就是这样的:
注意这里,这里看似只是压入了三个节点,实质上已经将整棵树压入到栈里了。因为13在这里代表的不是一个节点,而是整棵左子树,23也是代表着整棵右子树。这里就有疑问了,以这样的方式入栈有个锤子用处,你告诉我怎么访问13这颗左子树?如何解决这个矛盾点才是核心逻辑。我们可以继续分解左子树,就像这样:
可以看到,13代表的左子树以LRV的方式分解成3部分。这时栈中存在两个节点三颗树,程序的下一步继续弹出栈顶节点,如果是树就继续分解,如果是节点就代表着该节点是第一个可以访问到的节点。
使用这种方式来遍历二叉树,前中后三种遍历的差别只有一点,就是如何分解树。前序是VLR,中序是LVR,后序是LRV,程序的其他部分没有差别。
完整逻辑如下:
首先将根节点压入栈,随后弹出栈顶节点,发现是棵树,分解该树,按照指定的方式将左子树、右子树、根节点压入栈中。随后弹出栈顶节点,如果是棵树就继续分解,如果是节点就访问该节点。程序的结束标志是栈为空,代表整棵树的节点都已经访问完毕。
这种遍历方式笔者称之为分解遍历,因为核心逻辑是如何分解栈中代表树的节点。分解遍历使得前中后非递归遍历以统一的逻辑来处理。
分解遍历要求栈中节点保存额外的属性,即该节点是树还是结点,就是说我们需要一种方式知道弹出节点是树还是结点。方法有很多,可以修改节点定义,添加一个额外的成员变量,或者栈中存储字典,key是节点,value代表该节点是树还是结点,或者修改栈,为每个节点保存额外的属性。笔者使用的方法是修改栈,为栈中每个节点保存额外属性。
参考代码如下:
/*二叉树节点*/
typedef struct node
{char data;struct node *lchild, *rchild;
}Node;/*二叉树栈*/
typedef struct
{int top;//指向二叉树节点指针的指针struct node* data[MAX];bool   isNode[MAX];
}SqStack;void push(SqStack *&s, Node *bt, bool isNode = true)
{if (s == NULL)return;if (s->top == MAX - 1){printf("栈满,不能再入栈");}else{s->top++;s->data[s->top] = bt;s->isNode[s->top] = isNode;}
}Node* pop(SqStack *&s)
{if (s->top == -1){printf("栈空,不能出栈");}else{Node* temp;temp = s->data[s->top];s->top--;return temp;}
}bool topIsNode(SqStack *&s){if (s->top != -1)return s->isNode[s->top];return false;
}//从左向右前中后非递归遍历
void leftPreOrder(Node *bt)
{Node *BT = bt;bool isNode = false;push(s, BT, isNode);while (!EmptyStack(s)){isNode = topIsNode(s);BT = pop(s);if (isNode){printf("%c", BT->data);}else{if (BT->rchild != NULL)push(s, BT->rchild, false);if (BT->lchild != NULL)push(s, BT->lchild, false);push(s, BT, true);}     }
}void leftSimPreOrder(Node *bt)
{Node *BT = bt;push(s, BT);while (!EmptyStack(s)){BT = pop(s);printf("%c", BT->data);if (BT->rchild != NULL)push(s, BT->rchild);if (BT->lchild != NULL)push(s, BT->lchild);}
}void leftInOrder(Node *bt)
{Node *BT = bt;bool isNode = false;push(s, BT, isNode);while (!EmptyStack(s)){isNode = topIsNode(s);BT = pop(s);if (isNode){printf("%c", BT->data);}else{if (BT->rchild != NULL)push(s, BT->rchild, false);push(s, BT, true);if (BT->lchild != NULL)push(s, BT->lchild, false);}}
}void leftPostOrder(Node *bt)
{Node *BT = bt;bool isNode = false;push(s, BT, isNode);while (!EmptyStack(s)){isNode = topIsNode(s);BT = pop(s);if (isNode){printf("%c", BT->data);}else{push(s, BT, true);if (BT->rchild != NULL)push(s, BT->rchild, false);if (BT->lchild != NULL)push(s, BT->lchild, false);}}
}复制代码
参考代码中前中后非递归遍历的逻辑几乎一致,差别在于分解树之后,以怎样的方式入栈。而且对于前序遍历,笔者进行了简化。因为前序遍历中,树以VLR的方式分解,入栈之后,栈顶总是结点,因此可以省去判断栈顶是树还是结点的逻辑。
使用栈的二叉树遍历到此已经探究完毕,更多内容请看下一章节。

数据结构与算法-二叉查找树

数据结构与算法-二叉树遍历相关推荐

  1. javascript数据结构与算法--二叉树遍历(中序)

    javascript数据结构与算法--二叉树遍历(中序) 中序遍历按照节点上的键值,以升序访问BST上的所有节点 代码如下: /**二叉树中,相对较小的值保存在左节点上,较大的值保存在右节点中*** ...

  2. 树的基本概念和遍历规则 数据结构和算法 二叉树遍历(前序、中序、后序、层次、深度优先、广度优先遍历)

    zsychanpin 博客园 首页 新随笔 联系 订阅 管理 树的基本概念和遍历规则 树的递归定义 树是n(n>0)个结点的有限集,这个集合满足下面条件:       ⑴有且仅有一个结点没有前驱 ...

  3. 数据结构与算法-- 二叉树后续遍历序列校验

    二叉树后续遍历序列校验 题目:输入一个整数数组,判断改数组是否是某个二叉搜索树的后续遍历结果,如果是返回true否则false,假设输入数组的任意两个数字不相同. 例如输入{5,7,6,9,11,10 ...

  4. 数据结构与算法--二叉树第k个大的节点

    二叉树第k个大的节点 二叉树文章列表: 数据结构与算法–面试必问AVL树原理及实现 数据结构与算法–二叉树的深度问题 数据结构与算法–二叉堆(最大堆,最小堆)实现及原理 数据结构与算法–二叉查找树转顺 ...

  5. 数据结构与算法-- 二叉树中和为某一值的路径

    二叉树中和为某一值的路径 题目:输入一颗二叉树和一个整数,打印出二叉树中节点值的和为给定值的所有路径.从树的根节点开始往下一只到叶子节点所经过的节点形成一条路径. 我们用二叉树节点的定义沿用之前文章中 ...

  6. 常考数据结构与算法:二叉树的之字形层序遍历

    题目描述 给定一个二叉树,返回该二叉树的之字形层序遍历,(第一层从左向右,下一层从右向左,一直这样交替) 例如: 给定的二叉树是{3,9,20,#,#,15,7}, 该二叉树之字形层序遍历的结果是 [ ...

  7. 数据结构与算法 -- 二叉树 ADT

    树的类型有很多,这里我们只讲二叉树. 一.二叉树的基本概念 1.什么是二叉树 在计算机科中,二叉树是每个节点最多有两个子树的树结构.通常子树被称作"左子树"和"右子树&q ...

  8. 数据结构与算法--二叉树的深度问题

    二叉树的深度 题目:输入一颗二叉树的根,求该树的深度.从根节点到叶子节点一次进过的节点形成的一条路径,最长的路径的长度为树的深度. 如下图中二叉树的额深度4,因为从根节点A到叶子节点的路径中有4个节点 ...

  9. 数据结构与算法--二叉树实现原理

    二叉树 二叉树(binary tree)是一棵树,其中每个节点都不能有多于两个的子节点 二叉树的一个性质是一颗平均二叉树的深度要比节点个数N小得多(重点),对二叉树的分析得出其平均深度为O(N\sqr ...

  10. 数据结构与算法 | 二叉树四种的遍历方法(递归与非递归)

    二叉树的遍历是指从根节点出发,按照某种次序依次访问二叉树的所有节点,使得每个节点被访问且只访问一次. 而一般有四种遍历方法: 前序.中序.后序.层序,下面就分别讲一下四个遍历的思路以及代码实现 例如我 ...

最新文章

  1. 快速排序C实现(阿里巴巴 2012年全国校招笔试题)
  2. 在Asp.Net MVC中使用ModelBinding构造Array、List、Collection以及Dictionary
  3. 处理程序“PageHandlerFactory-Integrated”在其模块列表中有一个错误模块“ManagedPipelineHandler”...
  4. php在函数内使用全局变量
  5. Java中的ThreadPoolExecutor类
  6. php的excel源码下载,PHPExcel-5 - 源码下载|Windows编程|其他小程序|源代码 - 源码中国...
  7. python的gui界面 可视化_使用可视化设计窗体的GUI程序
  8. 字符串经典题之扑克牌的大小
  9. 响应式禁用(Bootstrap PK AmazeUI)
  10. 工作学习资料备份记录
  11. 微信抢红包代码 python_python实现红包裂变算法
  12. python有多少个模块_python绘图模块有哪些
  13. iOSQuartz2D-04-手动剪裁图片并保存到相册
  14. 移卡旗下全新餐饮SaaS产品米粒餐收银发布 科技驱动零售餐饮业态场景创新
  15. Vs2010中文版安装silverlight5bate方法
  16. Promise初步详解(resolve,reject,catch)
  17. 躲在被窝里偷偷学爬虫(6)---处理cookie模拟登录及代理IP
  18. unity开发 宝箱掉落与产出
  19. 【c语言】printf和scanf中* # %g的作用
  20. 小红书笔记发布软件 批量上传视频

热门文章

  1. Atitit 时间的展示格式与存储格式 目录 1.1. 赛事时间的格式起源 1 1.1.1. 六十[编辑] 1 1.2. 1h 12m 23s 模式 (可读性最好 2 1.3. 日常模式 1:45:
  2. Atitit word结构化提取考试试题读取 poi读取word nlp aiprj 目录 1.1. 结构化后数据 1 1.2. 文字读取 1 1.3. 图片读取 1 1.4. Doc的表格读取 /
  3. Atitit maven配置pom文件 配置法v2 t33 目录 1. Maven打包war原理 1 1.1. 在target目录生成war包ori目录。。。里面就是所有的资源 1 1.2. 去掉
  4. atitit.错误:找不到或无法加载主类 的解决 v4 qa15.doc 艾提拉总结 attilax总结 1.1. 修改此java文件,让他启动编译,还是不能生成了新的class, 1 1.2. 查
  5. Atitit 引流矩阵与矩阵引流 推广方法 attilax总结
  6. Atitit Atitit 零食erp数据管理---世界著名零食系列数据.docx世界著名零食
  7. Atitit数据库层次架构表与知识点 attilax 总结
  8. 屏幕取词技术实现原理
  9. atitit.提升兼容性最佳实践 o9o
  10. paip.java 架构师之路以及java高级技术