【导读】:树是数据结构中的重中之重,尤为以各种二叉树为学习的难点。在面试环节中,二叉树也是必考的模块。本文主要讲二叉树操做的相关知识,梳理面试常考的内容。请你们跟随小编一块儿来复习吧。node
本篇针对面试中常见的二叉树操做做个总结:web
前序遍历,中序遍历,后序遍历;面试
层次遍历;算法
求树的结点数;数组
求树的叶子数;数据结构
求树的深度;编辑器
求二叉树第k层的结点个数;工具
判断两棵二叉树是否结构相同;学习
求二叉树的镜像;开发工具
求两个结点的最低公共祖先结点;
求任意两结点距离;
找出二叉树中某个结点的全部祖先结点;
不使用递归和栈遍历二叉树;
二叉树前序中序推后序;
判断二叉树是否是彻底二叉树;
判断是不是二叉查找树的后序遍历结果;
给定一个二叉查找树中的结点,找出在中序遍历下它的后继和前驱;
二分查找树转化为排序的循环双链表;
有序链表转化为平衡的二分查找树;
判断是不是二叉查找树。
小编推荐一个学C语言/C++的学习裙【 712,284,705】,不管你是大牛仍是小白,是想转行仍是想入行均可以来了解一块儿进步一块儿学习!裙内有开发工具,不少干货和技术资料分享!
对于当前结点,先输出该结点,而后输出它的左孩子,最后输出它的右孩子。以上图为例,递归的过程以下:
输出 1,接着左孩子;
输出 2,接着左孩子;
输出 4,左孩子为空,再接着右孩子;
输出 6,左孩子为空,再接着右孩子;
输出 7,左右孩子都为空,此时 2 的左子树所有输出,2 的右子树为空,此时 1 的左子树所有输出,接着 1 的右子树;
输出 3,接着左孩子;
输出 5,左右孩子为空,此时 3 的左子树所有输出,3 的右子树为空,至此 1 的右子树所有输出,结束。
而非递归版本只是利用 stack 模拟上述过程而已,递归的过程也就是出入栈的过程。
对于当前结点,先输出它的左孩子,而后输出该结点,最后输出它的右孩子。以(1.1)图为例:
1-->2-->4,4 的左孩子为空,输出 4,接着右孩子;
6 的左孩子为空,输出 6,接着右孩子;
7 的左孩子为空,输出 7,右孩子也为空,此时 2 的左子树所有输出,输出 2,2 的右孩子为空,此时 1 的左子树所有输出,输出 1,接着 1 的右孩子;
3-->5,5 左孩子为空,输出 5,右孩子也为空,此时 3 的左子树所有输出,而 3 的右孩子为空,至此 1 的右子树所有输出,结束。
对于当前结点,先输出它的左孩子,而后输出它的右孩子,最后输出该结点。依旧以(1.1)图为例:
1->2->4->6->7,7 无左孩子,也无右孩子,输出 7,此时 6 无左孩子,而 6 的右子树也所有输出,输出 6,此时 4 无左子树,而 4 的右子树已所有输出,接着输出 4,此时 2 的左子树所有输出,且 2 无右子树,输出 2,此时 1 的左子树所有输出,接着转向右子树;
3->5,5 无左孩子,也无右孩子,输出 5,此时 3 的左子树所有输出,且 3 无右孩子,输出 3,此时 1 的右子树所有输出,输出 1,结束。
非递归版本中,对于一个结点,若是咱们要输出它,只有它既没有左孩子也没有右孩子或者它有孩子可是它的孩子已经被输出(由此设置 pre 变量)。若非上述两种状况,则将该结点的右孩子和左孩子依次入栈,这样就保证了每次取栈顶元素的时候,先依次遍历左子树和右子树。
不考虑数据内容。结构相赞成味着对应的左子树和对应的右子树都结构相同。
bool StructureCmp(Node * node1, Node * node2)
{
if (node1 == nullptr && node2 == nullptr)
return true;
else if (node1 == nullptr || node2 == nullptr)
return false;
return StructureCmp(node1->left, node2->left) && Str1uctureCmp(node1->right, node2->right);
}
对于每一个结点,咱们交换它的左右孩子便可。
最低公共祖先,即 LCA(Lowest Common Ancestor),见下图:
结点 3 和结点 4 的最近公共祖先是结点 2,即 LCA(3,4)=2。在此,须要注意到当两个结点在同一棵子树上的状况,如结点 3 和结点 2 的最近公共祖先为 2,即 LCA(3,2)=2。同理 LCA(5,6)=4,LCA(6,10)=1。
若是给定结点 5,则其全部祖先结点为 4,2,1。
1968 年,高德纳(Donald Knuth)提出一个问题:是否存在一个算法,它不使用栈也不破坏二叉树结构,可是能够完成对二叉树的遍历?随后 1979 年,James H. Morris 提出了二叉树线索化,解决了这个问题。(根据这个概念咱们又提出了一个新的数据结构,即线索二叉树,因线索二叉树不是本文要介绍的内容,因此有兴趣的朋友请移步线索二叉树)
前序,中序,后序遍历,不论是递归版本仍是非递归版本,都用到了一个数据结构--栈,为什么要用栈?那是由于其它的方式无法记录当前结点的 parent,而若是在每一个结点的结构里面加个 parent 份量显然是不现实的,而线索化正好解决了这个问题,其含义就是利用结点的右孩子空指针,指向该结点在中序序列中的后继。下面具体来看看如何使用线索化来完成对二叉树的遍历。
若是当前结点的左孩子为空,则输出当前结点并将其右孩子做为当前结点;
若是当前结点的左孩子不为空,在当前结点的左子树中找到当前结点在中序遍历下的前驱结点;
2.1若是前驱结点的右孩子为空,将它的右孩子设置为当前结点,输出当前结点并把当前结点更新为当前结点的左孩子;
2.2若是前驱结点的右孩子为当前结点,将它的右孩子从新设为空,当前结点更新为当前结点的右孩子;
重复以上步骤 1 和 2,直到当前结点为空。
再来看中序遍历,和前序遍历相比只改动一句代码,步骤以下:
若是当前结点的左孩子为空,则输出当前结点并将其右孩子做为当前结点;
若是当前结点的左孩子不为空,在当前结点的左子树中找到当前结点在中序遍历下的前驱结点;
2.1. 若是前驱结点的右孩子为空,将它的右孩子设置为当前结点,当前结点更新为当前结点的左孩子;
2.2. 若是前驱结点的右孩子为当前结点,将它的右孩子从新设为空,输出当前结点,当前结点更新为当前结点的右孩子;
重复以上步骤 1 和 2,直到当前结点为空。
最后看下后序遍历,后序遍历有点复杂,须要创建一个虚假根结点 dummy,令其左孩子是 root。而且还须要一个子过程,就是倒序输出某两个结点之间路径上的各个结点。步骤以下:
若是当前结点的左孩子为空,则将其右孩子做为当前结点;
若是当前结点的左孩子不为空,在当前结点的左子树中找到当前结点在中序遍历下的前驱结点;
2.1. 若是前驱结点的右孩子为空,将它的右孩子设置为当前结点,当前结点更新为当前结点的左孩子;
2.2. 若是前驱结点的右孩子为当前结点,将它的右孩子从新设为空,倒序输出从当前结点的左孩子到该前驱结点这条路径上的全部结点,当前结点更新为当前结点的右孩子;
重复以上步骤 1 和 2,直到当前结点为空。
dummy 用的很是巧妙,建议读者配合上面的图模拟下算法流程。
以上面图表为例,步骤以下:
根据前序可知根结点为1;
根据中序可知 4 7 2 为根结点 1 的左子树和 8 5 9 3 6 为根结点 1 的右子树;
递归实现,把 4 7 2 当作新的一棵树和 8 5 9 3 6 也当作新的一棵树;
在递归的过程当中输出后序。
固然咱们也能够根据前序和中序构造出二叉树,进而求出后序。
若设二叉树的深度为 h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层全部的结点都连续集中在最左边,这就是彻底二叉树(Complete Binary Tree)。以下图:
首先若一个结点只有右孩子,确定不是彻底二叉树;其次若只有左孩子或没有孩子,那么接下来的全部结点确定都没有孩子,不然就不是彻底二叉树,所以设置 flag 标记变量。
在后续遍历获得的序列中,最后一个元素为树的根结点。从头开始扫描这个序列,比根结点小的元素都应该位于序列的左半部分;从第一个大于跟结点开始到跟结点前面的一个元素为止,全部元素都应该大于跟结点,由于这部分元素对应的是树的右子树。根据这样的划分,把序列划分为左右两部分,咱们递归地确认序列的左、右两部分是否是都是二元查找树。
一棵二叉查找树的中序遍历序列,正好是升序序列。假如根结点的父结点为 nullptr,则:
若是当前结点有右孩子,则后继结点为这个右孩子的最左孩子;
若是当前结点没有右孩子;
2.1. 当前结点为根结点,返回 nullptr;
2.2. 当前结点只是个普通结点,也就是存在父结点;
2.2.1. 当前结点是父亲结点的左孩子,则父亲结点就是后继结点;
2.2.2. 当前结点是父亲结点的右孩子,沿着父亲结点往上走,直到 n-1 代祖先是 n 代祖先的左孩子,则后继为 n 代祖先或遍历到根结点也没找到符合的,则当前结点就是中序遍历的最后一个结点,返回 nullptr。
仔细观察上述代码,总以为有点啰嗦。好比,过多的 return,步骤 2 的层次太多。综合考虑全部状况,改进代码以下:
上述的代码是基于结点有 parent 指针的,若题意要求没有 parent 呢?网上也有人给出了答案,我的以为没有什么价值,有兴趣的朋友能够到这里查看。
而求前驱结点的话,只需把上述代码的 left 与 right 互调便可,很简单。
二分查找树的中序遍历即为升序排列,问题就在于如何在遍历的时候更改指针的指向。一种简单的方法时,遍历二分查找树,将遍历的结果放在一个数组中,以后再把该数组转化为双链表。若是题目要求只能使用 O(1)O(1) 内存,则只能在遍历的同时构建双链表,即进行指针的替换。
咱们须要用递归的方法来解决,假定每一个递归调用都会返回构建好的双链表,可把问题分解为左右两个子树。因为左右子树都已是有序的,当前结点做为中间的一个结点,把左右子树获得的链表链接起来便可。
咱们能够采用自顶向下的方法。先找到中间结点做为根结点,而后递归左右两部分。因此咱们须要先找到中间结点,对于单链表来讲,必需要遍历一边,可使用快慢指针加快查找速度。
由 f(n)=2f(\frac n2)+\frac n2f(n)=2f(2n)+2n 得,因此上述算法的时间复杂度为 O(nlogn)O(nlogn)。
不妨换个思路,采用自底向上的方法:
如此,时间复杂度降为 O(n)O(n)。
咱们假定二叉树没有重复元素,即对于每一个结点,其左右孩子都是严格的小于和大于。
下面给出两个方法:
方法 1:
方法 2:
利用二叉查找树中序遍历时元素递增来判断。