本篇针对面试中常见的二叉树操作作个总结:
(1)前序遍历,中序遍历,后序遍历;
(2)层次遍历;
(3)求树的节点数;
(4)求树的叶子数;
(5)求树的深度;
(6)求二叉树第k层的节点个数;
(7)判断两棵二叉树是否结构相同;
(8)求二叉树的镜像;
(9)求两个节点的最低公共祖先节点;
(10)求任意两节点距离;
(11)找出二叉树中某个节点的所有祖先节点;
(12)不使用递归和栈遍历二叉树;
(13)二叉树前序中序推后序;
(14)判断二叉树是不是完全二叉树;
(15)判断是否是二叉查找树的后序遍历结果;
(16)给定一个二叉查找树中的节点,找出在中序遍历下它的后继和前驱;
(17)二分查找树转化为排序的循环双链表;
(18)有序链表转化为平衡的二分查找树。
1-4
参见二叉树基础。
5 求树的深度
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
6 求二叉树第k层的节点个数
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
7 判断两棵二叉树是否结构相同
不考虑数据内容。结构相同意味着对应的左子树和对应的右子树都结构相同。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
8 求二叉树的镜像
对于每个节点,我们交换它的左右孩子即可。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
9 求两个节点的最低公共祖先节点
最低公共祖先,即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。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
10 求任意两节点距离
首先找到两个节点的LCA,然后分别计算LCA与它们的距离,最后相加即可。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
11 找出二叉树中某个节点的所有祖先节点
如果给定节点5,则其所有祖先节点为4,2,1。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
12 不使用递归和栈遍历二叉树
1968年,高德纳(Donald Knuth)提出一个问题:是否存在一个算法,它不使用栈也不破坏二叉树结构,但是可以完成对二叉树的遍历?随后1979年,James H. Morris提出了二叉树线索化,解决了这个问题。(根据这个概念我们又提出了一个新的数据结构,即线索二叉树,因线索二叉树不是本文要介绍的内容,所以有兴趣的朋友请移步线索二叉树。)
前序,中序,后序遍历,不管是递归版本还是非递归版本,都用到了一个数据结构–栈,为何要用栈?那是因为其它的方式没法记录当前节点的parent,而如果在每个节点的结构里面加个parent分量显然是不现实的,而线索化正好解决了这个问题,其含义就是利用节点的右孩子空指针,指向该节点在中序序列中的后继。下面具体来看看如何使用线索化来完成对二叉树的遍历。
先看前序遍历,步骤如下:
(1)如果当前节点的左孩子为空,则输出当前节点并将其右孩子作为当前节点;
(2)如果当前节点的左孩子不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点;
(2.1)如果前驱节点的右孩子为空,将它的右孩子设置为当前节点,输出当前节点并把当前节点更新为当前节点的左孩子;
(2.2)如果前驱节点的右孩子为当前节点,将它的右孩子重新设为空,当前节点更新为当前节点的右孩子;
(3)重复以上(1)和(2),直到当前节点为空。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
再来看中序遍历,和前序遍历相比只改动一句代码,步骤如下:
(1)如果当前节点的左孩子为空,则输出当前节点并将其右孩子作为当前节点;
(2)如果当前节点的左孩子不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点;
(2.1)如果前驱节点的右孩子为空,将它的右孩子设置为当前节点,当前节点更新为当前节点的左孩子;
(2.2)如果前驱节点的右孩子为当前节点,将它的右孩子重新设为空,输出当前节点,当前节点更新为当前节点的右孩子;
(3)重复以上(1)和(2),直到当前节点为空。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
最后看下后序遍历,后续遍历有点复杂,需要建立一个虚假根节点dummy,令其左孩子是root。并且还需要一个子过程,就是倒序输出某两个节点之间路径上的各个节点。
步骤如下:
(1)如果当前节点的左孩子为空,则将其右孩子作为当前节点。
(2)如果当前节点的左孩子不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点。
(2.1)如果前驱节点的右孩子为空,将它的右孩子设置为当前节点,当前节点更新为当前节点的左孩子;
(2.2)如果前驱节点的右孩子为当前节点,将它的右孩子重新设为空,倒序输出从当前节点的左孩子到该前驱节点这条路径上的所有节点,当前节点更新为当前节点的右孩子;
(3)重复以上(1)和(2),直到当前节点为空。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
dummy用的非常巧妙,建议读者配合上面的图模拟下算法流程。
13 二叉树前序中序推后序
前序:[1 2 4 7 3 5 8 9 6]
中序:[4 7 2 1 8 5 9 3 6]
后序:[7 4 2 8 9 5 6 3 1]
以上式为例,步骤如下:
第一步:根据前序可知根节点为1;
第二步:根据中序可知4 7 2为根节点1的左子树和8 5 9 3 6为根节点1的右子树;
第三步:递归实现,把4 7 2当做新的一棵树和8 5 9 3 6也当做新的一棵树;
第四步:在递归的过程中输出后序。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
当然我们也可以根据前序和中序构造出二叉树,进而求出后序。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
14 判断二叉树是不是完全二叉树
若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树(Complete Binary Tree)。如下图:
首先若一个节点只有右孩子,肯定不是完全二叉树;其次若只有左孩子或没有孩子,那么对于一个高度为h的完全二叉树,当前节点的高度肯定是h-1,也就是高度h的所有节点都没有孩子,否则不是完全二叉树,因此设置flag标记当前节点是不是到了h-1高度。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
15 判断是否是二叉查找树的后序遍历结果
在后续遍历得到的序列中,最后一个元素为树的根结点。从头开始扫描这个序列,比根结点小的元素都应该位于序列的左半部分;从第一个大于跟结点开始到跟结点前面的一个元素为止,所有元素都应该大于跟结点,因为这部分元素对应的是树的右子树。根据这样的划分,把序列划分为左右两部分,我们递归地确认序列的左、右两部分是不是都是二元查找树。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
16 给定一个二叉查找树中的节点,找出在中序遍历下它的后继和前驱
一棵二叉查找树的中序遍历序列,正好是升序序列。
如果节点中有指向父亲节点的指针(假如根节点的父节点为nullptr),则:
(1)如果当前节点有右孩子,则后继节点为这个右孩子的最左孩子;
(2)如果当前节点没有右孩子;
(2.1)当前节点为根节点,返回nullptr;
(2.2)当前节点只是个普通节点,也就是存在父节点;
(2.2.1)当前节点是父亲节点的左孩子,则父亲节点就是后继节点;
(2.2.2)当前节点是父亲节点的右孩子,沿着父亲节点往上走,直到n-1代祖先是n代祖先的左孩子,则后继为n代祖先)或遍历到根节点也没找到符合的,则当前节点就是中序遍历的最后一个节点,返回nullptr。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
仔细观察上述代码,总觉得有点啰嗦。比如,过多的return,(2)的层次太多。综合考虑所有情况,改进代码如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
上述的代码是基于节点有parent指针的,若题意要求没有parent呢?网上也有人给出了答案,个人觉得没有什么价值,有兴趣的朋友可以到这里查看。
而求前驱节点的话,只需把上述代码的left与right互调即可,很简单。
17 二分查找树转化为排序的循环双链表
二分查找树的中序遍历即为升序排列,问题就在于如何在遍历的时候更改指针的指向。一种简单的方法时,遍历二分查找树,将遍历的结果放在一个数组中,之后再把该数组转化为双链表。如果题目要求只能使用
O(1)
内存,则只能在遍历的同时构建双链表,即进行指针的替换。
我们需要用递归的方法来解决,假定每个递归调用都会返回构建好的双链表,可把问题分解为左右两个子树。由于左右子树都已经是有序的,当前节点作为中间的一个节点,把左右子树得到的链表连接起来即可。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
18 有序链表转化为平衡的二分查找树(Binary Search Tree)
我们可以采用自顶向下的方法。先找到中间节点作为根节点,然后递归左右两部分。所有我们需要先找到中间节点,对于单链表来说,必须要遍历一边,可以使用快慢指针加快查找速度。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
由
f(n)=2f(n2)+n2
得,所以上述算法的时间复杂度为
O(nlogn)
。
不妨换个思路,采用自底向上的方法:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
如此,时间复杂度降为 O(n) 。