树是数据结构中最为重要的结构之一,尤其是二叉树。它与顺序表和单链表不同,它的结构更为复杂,运算更为抽象。
1. 树
1.1 概念
树是一种非线性的数据结构,它是由 n (n >=0) 个有限节点组成的一个具有层次关系的集合。它之所以称为树,是因为它的结构像是一颗倒挂的树。
- 节点的度: 一个节点的孩子节点的个数。如上图:A的度为3
- 树的度:一棵树中,最大节点的度成为数的度。如上图:该树的度为3
- 节点的层次:从根开始定义,根为第1层,根的子节点为第2层,以此类推
- 树的高度/深度:树中节点的最大层次。如上图:树的高度为3
- 兄弟节点:拥有相同的双亲结点。上图中,E 和 F 就是兄弟节点
- 森林:由两棵或者两棵以上的互不相交的树组成的集合,就称为森林。
1.2 树的表示形式
- 孩子兄弟表示法:
class Node {
int val; // 树中存储的数据
Node firstChild; // 第一个孩子引用
Node nextBrother; // 第一个兄弟引用
}
2. 孩子表示法:
class Node {
int val; // 树中存储的数据
Node left; // 左孩子引用, 常常代表左孩子为根的整棵子树
Node right; // 右孩子引用, 常常代表右孩子为根的整棵子树
}
- 孩子双亲表示法:
class Node {
int val; // 树中存储的数据
Node left; // 左孩子引用, 常常代表左孩子为根的整棵子树
Node right; // 右孩子引用, 常常代表右孩子为根的整棵子树
Node parent; // 当前节点的双亲节点
}
这里较为常用的是孩子表示法.
2. 二叉树
2.1 概念
二叉树是树中最为重要的概念.
一. 二叉树的特点:
- 每个节点最多有两棵子树. 即二叉树所有节点的度都 <= 2
- 二叉树有左右子树之分, 其次序不可以颠倒(因为二叉树是一棵有序树)
二: 二叉树的几种形态
空树 和 只有根节点的树都是二叉树
2. 2 满二叉树和完全二叉树
2.2.1 满二叉树
一个二叉树, 如果每一层的节点数都达到最大值, 那么这棵二叉树就称为满二叉树
满二叉树的特点:如果一棵树有k层, 那么第 k 层就有 2 ^ (k - 1) 个节点。 总共有 (2 ^ k) -1 个节点。
2.2.2 完全二叉树
完全二叉树是由满二叉树所引出的。
但是需要注意的是: 当且仅当其每一个节点都与深度为k的满二叉树中编号从 1 - n 的节点一 一对应时, 才可以称之为完全二叉树。
完全二叉树的特点: 若对节点进行编号, 每一个节点的编号都是连续的。
2. 3 二叉树的性质
- 若规定根节点的层数为1, 那么一棵非空二叉树第 i 层上最多有 2 ^ (i - 1) (i > 0)个节点。
- 若一棵二叉树的深度为 k , 那么这棵二叉树最多有: (2 ^ k) - 1 (k > 0) 个节点.
- 对于任意一棵二叉树, 设它的叶子节点的个数为 n0, 那么度为 2 的节点的个数为: n0 = n2 + 1
- 具有n个节点的完全二叉树的深度 k 为: log2 (n + 1) 向上取整
- 若已知孩子节点的编号 i, 则其父亲节点的编号为 (i - 1) / 2
- 若已知父亲节点的编号, 则:
左孩子的编号为: 2 * i + 1;
右孩子的编号为: 2 * i + 2;
2.4 二叉树的基本操作
2.4.1 二叉树的遍历
二叉树的遍历方式有 4 种, 分别是:
- 前序遍历(先根遍历): 根 -> 左 -> 右
public void preOrderTraversal(Node root) {
if(root == null) {
return;
}
// 打印根
System.out.print(root.val + " ");
// 遍历左子树
preOrderTraversal(root.left);
// 遍历右子树
preOrderTraversal(root.right);
}
- 中序遍历: 左 -> 根 -> 右
public void inOrderTraversal(Node root) {
if(root == null) {
return;
}
// 遍历左子树
inOrderTraversal(root.left);
// 打印根
System.out.print(root.val + " ");
// 遍历右子树
inOrderTraversal(root.right);
}
- 后序遍历: 左 -> 右 -> 根
public void postOrderTraversal(Node root) {
if(root == null) {
return;
}
// 遍历左子树
postOrderTraversal(root.left);
// 遍历右子树
postOrderTraversal(root.right);
// 打印根
System.out.print(root.val + " ");
}
- 层序遍历(需要借助堆来实现): 一层一层的进行遍历
关于二叉树的问题, 大多数情况下, 我们会使用递归进行求解. 通常情况下, 会将整棵二叉树化成子问题进行求解.
2.4.2 二叉树的层序遍历
二叉树的层序遍历需要借助队列进行.
// 二叉树的层序遍历 需要借助一个队完成
// 出队元素若不为空, 判断它是否有左右孩子, 若存在, 就将孩子节点进队, 若不存在, 就不进队
public void levelOrderTraversal(Node root) {
Queue<Node> queue = new LinkedList<>();
if(root == null) {
return;
}
// root 不为空
queue.offer(root);
while (!queue.isEmpty()) {
// 用 cur 接收出队元素
Node cur = queue.poll();
if(cur != null) {
System.out.print(cur.val + " ");
// 如果 cur 有左孩子就让左孩子进队
if(cur.left != null) {
queue.offer(cur.left);
}
// 如果 cur 有右孩子就让右孩子进队
if(cur.right != null) {
queue.offer(cur.right);
}
}
}
System.out.println();
}
2.4.3 判断一棵树是否是完全二叉树
// 判断一棵树是不是完全二叉树
public boolean isCompleteTree(Node root) {
Queue<Node> queue = new LinkedList<>();
if(root == null) {
// 空树也是一棵完全二叉树
return true;
}
queue.offer(root);
while(!queue.isEmpty()) {
Node cur = queue.poll();
if(cur != null) {
// 只要cur != null, 就将其左孩子, 右孩子都进队
queue.offer(cur.left);
queue.offer(cur.right);
} else {
// 如果cur == null, 就立即停止入队
break;
}
}
// 如果是因为cur == null
while (!queue.isEmpty()) {
Node cur1 = queue.peek();
if(cur1 != null) {
// 队中存在不为 null 的元素
return false;
}
queue.poll();
}
return true;
}