二叉树解析
04_二叉树
1、树形结构
2、生活中的树形结构
- 使用树形结构可以大大提高效率
- 树形结构是算法面试的重点
3、树的基本概念
-
节点、根节点、父节点、子节点、兄弟节点
-
一棵树可以没有任何节点,称为空树
-
一棵树可以只有一个节点,也就是只有根节点
-
子树、左子树、右子树
我们把下面的这个树的一部分结构拿出来讲解:
如图:51为5的左子树,52则为右子树
-
节点的度:子树的个数
拿上图的1节点来说,它有2,3,4,5,6一共5个子节点,所以1这个节点的度为5
-
树的度:所有节点度中的最大值
树的度就是所有节点度中的最大值,上图所示中,节点最多的为根节点,所以,这颗树的度即为5
-
叶子节点:度为0的节点
如上图中的:21,31,51,52,61,221,222,223
-
非叶子节点:度不为0的节点
-
层数(level):根节点在第1层,根节点的子节点在第2层,以此类推
-
节点的深度(depth):从根节点到当前节点的唯一路径上的节点总数
如上图所示:根节点1的深度即为1->2->22->221|222|223,
所以节点1的深度就为4
-
节点的高度(height):从当前节点到最远叶子节点的路径上的节点总数
如上图所示:根节点1的高度即为最远路径到它的路径上的节点总数,所以节点1的高度即为4
-
树的深度:所有节点深度中的最大值
-
树的高度:所有节点高度中的最大值
-
树的深度等于树的高度
4、有序树、无序树、森林
- 有序数
- 树种任意节点的子节点之间有顺序
- 无序树
- 树种任意节点的子节点之间没有顺序关系
- 也称为“自由树”
- 森林
- 由m(m>=n)颗互不相交的树组成的集合
5、二叉树(Binary Tree)
- 二叉树的特点
- 每个结点的度最大为2(最多拥有2颗子树)
- 左子树和右子树是有顺序的
- 即使某节点只有一颗子树,也要区分左右子树
- 二叉树是有序树
如下图所示都为二叉树:
5.1、二叉树的性质
-
非空二叉树的第i层,最多有2^(i-1)个节点(i>=1)
-
在高度为h的二叉树上最多有2^h-1个节点(h>=1)
-
对于任何一颗非空二叉树,如果叶子节点个数为n0,度为2的节点个数为n2,则有:n0=n2+1
- 假设度为1的节点个数为n1,那么二叉树的节点总数n=n0+n1+n2
- 二叉树的边数T=n1+2*n2=n-1=n0+n1+n2-1
- 因此n0=n2+1
5.2、真二叉树(Proper Binary Tree)
真二叉树:所有节点的度都要么为0,要么为2
如图所示即为真二叉树:
如下图所示不是真二叉树:
5.3、满二叉树(Full Binary Tree)
- 满二叉树:最后一层节点的度都为0,其他节点的度都为2
- 在同样高度的二叉树中,满二叉树的叶子节点数量最多,总节点数量最多
- 满二叉树一定是真二叉树,真二叉树不一定是满二叉树
5.4、完全二叉树(Complete Binary Tree)
- 完全二叉树:对节点从上至下,从左至右开始编号,其所有编号都能与相同高度的满二叉树中的编号对应
- 叶子节点只会出现最后2层,最后1层的叶子节点都靠左对齐
- 完全二叉树从根节点至倒数第二层是一棵满二叉树
- 满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树
5.4.1、完全二叉树的性质
- 度为1的节点只有左子树
- 度为1的节点要么是1个,要么是0个
- 同样节点数量的二叉树,完全二叉树的高度最小
解析:节点最少的情况,就是最底下的一层只有一个节点,最多节点的对应情况其实就是满二叉树
-
一棵有n个节点的完全二叉树(n>0),从上到下,从左到右对节点从1开始进行编号,对任意第i个节点
-
如果i = 1,它是根节点
-
如果i > 1,它的父节点编号为floor(i / 2)
-
如果2i <= n,它的左子节点编号为2i
-
如果2i > n,它无左子节点
-
如果2i + 1 <= n,它的右子节点编号为2i + 1
-
如果2i + 1 > n,它无右子节点
-
-
一棵有n个节点的完全二叉树(n>0),从上到下,从左到右对节点从0开始进行编号,对任意第i个节点
- 如果i = 0,它是根节点
- 如果i > 0,它的父节点编号为floor( (i - 1) / 2 )
- 如果2i + 1 <= n - 1,它的左子节点编号为2i + 1
- 如果2i + 1 > n - 1,它无左子节点
- 如果2i + 2 <= n - 1,它的右子节点编号为2i + 2
- 如果2i + 2 > n - 1,它无右子节点
5.5、二叉树的遍历
- 遍历是数据结构中的常见操作
- 把所有元素都访问一遍
- 线性数据结构的遍历比较简单
- 正序遍历
- 逆序遍历
- 根据节点访问顺序的不同,二叉树的常见遍历有四种
- 前序遍历(Preorder Traversal)
- 中序遍历(Inorder Traversal)
- 后序遍历(Postorder Traversal)
- 层序遍历(Level Order Traversal)
5.5.1、前序遍历(Preorder Traversal)
-
访问顺序
-
根节点、前序遍历左子树、前序遍历右子树
-
如下图:前序遍历的结果是7,4,2,1,3,5,9,2,11,10,12
先遍历左子树上的节点,然后再遍历根节点,然后再遍历右子树上的节点
-
我们用递归的方法可以解决前序遍历的问题,具体代码如下:
/**
* 前序遍历
*//
public void preorderTraversal(){
preorderTraversal(root);
}
//**
* 前序遍历
*//
private void preorderTraversal(Node<E> node){
if(node == null) {
return;
}
System.out.println(node.element);
preorderTraversal(node.left);
preorderTraversal(node.right);
}
5.5.2、中序遍历(Inorder Traversal)
- 访问顺序
- 中序遍历左子树、根节点、中序遍历右子树
- 1,2,3,4,5,7,8,9,10,11,12
- 如果访问顺序是下面这样呢?
- 中序遍历右子树、根节点、中序遍历左子树
- 12,11,10,9,8,7,5,4,3,2,1
值得注意的是,二叉搜索树的中序遍历结果是升序或者是降序的
实现代码如下:
/**
* 中序遍历
*//
public void inorderTraversal(){
inorderTraversal(root);
}
//**
* 中序遍历
*//
private void inorderTraversal(Node<E> node){
if(node == null) {
return;
}
inorderTraversal(node.left);
System.out.println(node.element);
inorderTraversal(node.right);
}
5.5.3、后续遍历
- 访问顺序
- 后序遍历左子树、后序遍历左子树、根节点
- 1,3,2,5,4,8,10,12,11,9,7
代码实现如下:
/**
* 后序遍历
*//
public void postorderTraversal(){
postorderTraversal(root);
}
//**
* 后序遍历
*//
private void postorderTraversal(Node<E> node){
if(node == null) {
return;
}
postorderTraversal(node.left);
postorderTraversal(node.right);
System.out.println(node.element);
}
5.5.4、层序遍历(Level Order Traversal)
- 访问顺序
- 从上到下、从左到右依次访问每一个节点
- 7,4,9,2,5,8,11,1,3,10,12
- 实现思路:使用队列
- 1.将根节点入队
- 2.循环执行以下操作,直到队列为空
- 将A的左子节点入队
- 将A的右子节点入队
- 如上图所示,我们要用层序遍历实现对这个二叉树的访问,我们实现要使用队列Queue
来将这棵树的根节点放入队列中,当我们把这个root节点放入队列中时,此时我们的队列就有了一个元素,也就是我们上图中的根节点7 - 遍历完根节点之后,我们就将根节点poll出去,也即将根节点弹出,然后我们就将根节点的左右子树分别放入队列中,现在我们的队列中就有了俩个元素,分别是根节点7的左子节点4和右子节点9
- 然后我们就对队列进行遍历,当我们遍历完节点4之后,我们就将节点4的左右子节点放入队列中
- 然后我们继续遍历根节点7的右子节点,当根节点7的右子节点遍历完之后,我们就将根节点7的右子节点弹出队列,然后将9这个节点的左右子节点分别放入队列中
- 以此类推,知道所有的节点都被遍历即可
//**
* 层序遍历
*//
public void levelOrderTraversal(){
if(root == null){
return;
}
Queue<Node<E>> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()){
Node<E> node = queue.poll();
System.out.println(node.element);
if(node.left != null){
queue.offer(node.left);
}
if(node.right != null){
queue.offer(node.right);
}
}
}
5.6、二叉树的高度
遍历二叉树的高度,我们有俩种方法进行实现
第一种方法:递归
/**
* 二叉树的高度,递归方式
* @return
*/
public int height(){
return height(root);
}
/**
* 二叉树的高度,递归方式
* @return
*/
private int height(Node<E> node){
if(node == null){
return 0;
}
return Math.max(height(node.left),height(node.right)) + 1;
}
第二种方法:迭代
下面给的代码就是求二叉树高度的迭代方法:
其实这个方法的中心思想就是我们要求这个二叉树的高度的话
其实说到底就是对这个二叉树进行层序遍历的一个过程
定义一个height来记录二叉树的高度
同时用一个levelSize来记录每一层二叉树节点的个数,每遍历一个节点就让这个levelSize--,直到levelSize减为0为止,也就说明这个二叉树当前层已经被遍历完,hight++,同时重新维护这个levelSize,让它等于下一层的节点的数量,也就是queue.size()
/**
* 二叉树的高度,迭代方法
* @return 二叉树的高度
*/
public int high(){
if(root == null){
return 0;
}
Queue<Node<E>> queue = new LinkedList<>();
queue.offer(root);
// 记录二叉树的高度
int height = 0;
// 记录二叉树每一层的节点个数
int levelSize = 1;
while (!queue.isEmpty()){
Node<E> node = queue.poll();
levelSize--;
if(node.left != null){
queue.offer(node.left);
}
if(node.right != null){
queue.offer(node.right);
}
if(levelSize == 0){
levelSize = queue.size();
height++;
}
}
return height;
}
5.7、判断一棵树是不是完全二叉树
如下图所示:
我们要判断一颗二叉树,首先我们得知道二叉树的定义,如下图,这棵树就是一个完全二叉树。
我们需要保证的是,看下图所示,假如E这个节点的左子树为空,而它的右子树不为空,那么这个树就不是一个完全二叉树
如果我们要保证它是一个完全二叉树,那么我们就得保证E这个节点要么左右子树为空,要么左子树有,没有右子节点,同时我们必须得保证F,G节点都是叶子节点即可
代码如下:
/**
* 判断是否为完全二叉树
* @return
*/
public boolean isComplete(){
if(root == null){
return false;
}
Queue<Node<E>> queue = new LinkedList<>();
queue.offer(root);
boolean leaf = false;
while (!queue.isEmpty()){
Node<E> node = queue.poll();
if(leaf && !node.isLeaf()){
return false;
}
if(node.left != null){
queue.offer(node.left);
}else if(node.right != null){
return false;
}
if(node.right != null){
queue.offer(node.right);
}else {
leaf = true;
}
}
return true;
}
5.8、二叉树的前驱节点(Predecessor)
- 前驱节点:中序遍历时的前一个节点
- 如果是二叉搜索树,前驱结点就是前一个比它小的节点
- node.left != null
- 举例:6,13,8
- Predecessor = node.left.right.right....
- 终止条件:right为null
- node.left == null && node.parent != null
- 举例:7,11,9,1
- Predecessor = node.parent.parentparent....
- 终止条件:node在parent的右子树中
- node.left == null && node.parent == null
- 那就没有前驱节点
- 举例:没有左子树的根节点
如上图所示,我们进行分析:
我们要找一个节点的前驱节点,就需要先对左子树的查找有一定的了解,比如我们所知道的根节点是8的前驱节点是7,为什么是7,其实就是因为,7是最靠近8的前一个节点。
首先我们需要看第一种情况,假如左子树不为空,也就是node.left != null这种情况,我们以根节点8为例,根节点8的前驱结点是7,我们要想找到根节点8的前驱结点,其实就是要从左子树上去找,因为前驱结点就是要小于根节点,所以我们的前驱节点就是根节点8的左子树上找,然后我们在找左子树上的最大值,这个最大值就是根节点8的前驱结点,也就是我们要首先求Node
然后我们再看第二种情况,如果左子树是空,但是左子树的父节点不是空的话,也就是node.left == null && node.parent != null,如上图,假如我们要求节点7的前驱结点,我们很明显的就能看到节点7的前驱结点就是节点6,也就是它的父节点,其实这种情况就是我们不断的找这个节点的父节点,知道node在parent的右子树中即可
最后一种情况是,假如左子树为空,而且也没有父亲节点,那么就说明这个树没有前驱节点
代码如下:
/**
* 寻找前驱结点
* @param node
* @return 前驱结点
*/
private Node<E> predecessor(Node<E> node){
if(node == null){
return null;
}
Node<E> pre = node.left;
if(pre != null){
while (pre.right != null){
pre = pre.right;
}
return pre;
}
while (node.parent != null && node == node.parent.left){
node = node.parent;
}
return node.parent;
}
5.9、二叉树的后继节点(successor)
后继节点的考虑情况同样很简单,我就不一一赘述了
- 后继节点:中序遍历时的后一个节点
- 如果是二叉搜索树,后继节点就是后一个比它大的节点
- node.right != null
- 举例:1,8,4
- successor = node.right.left.left....
- 终止条件:left 为null
- node.right == null && node.parent != null
- 举例:7,6,3,11
- successor = node.parent .parent .parent ....
- 终止条件:node在parent的左子树中
- node.right == null && node.parent == null
- 那就没有后继节点
- 举例:没有右子树的根节点
代码如下:
/**
* 寻找前驱结点
* @param node
* @return 前驱结点
*/
private Node<E> successor(Node<E> node){
if(node == null){
return null;
}
Node<E> suc = node.right;
if(suc != null){
while (suc.left != null){
suc = suc.right;
}
return suc;
}
while (node.parent != null && node == node.parent.right){
node = node.parent;
}
return node.parent;
}