1. 二叉树
1.0 定义
1.1 性质
性质1:二叉树第 i 层上节点个数最多为 2 i-1
性质2:深度为 k 的二叉树,节点个数最多为 2 k -1
性质3:由性质2可知,包含 n 个节点的二叉树,高度至少为 log 2 (n+1)
性质4:在任意一颗二叉树中,若终端结点的个数为n0,度为2的结点数为n2,则n0=n2+1
推导:
1.2 分类
1.2.1 满二叉树 / 完美二叉树
满足条件:
- 深度为k
- 总的结点个数为 2k - 1 个结点
1.2.2 完全二叉树
特点:
- 前k-1层是满二叉树
- 第k层所有节点都在左边连续分布
1.2.3 完满二叉树
满足条件:
- 除了叶子节点之外的每个节点都有2个子节点
1.2.4 二叉排序树 / 二叉查找树 / 二叉搜索树 Binary Search Tree
满足条件:二叉搜索树的中序遍历结果是有序数组
- 所有子树上面的左节点的值都比根结点要小,右节点的值都比根结点要大
- 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值
- 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值
- 任意结点的左右子树也都是二叉查找树
1.2.5 平衡二叉树 / AVL树 Balanced Binary Tree
它是二叉查找树最优的情况。它很好的解决了二叉查找树退化成链表的问题,把插入,查找,删除的时间复杂度最好情况和最坏情况都维持在O(logN)。但是频繁旋转会使插入和删除牺牲掉O(logN)左右的时间,不过相对二叉查找树来说,时间上稳定了很多
它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
AVL树
失衡
LL
RR
LR
RL
参考链接:https://www.cnblogs.com/skywang12345/p/3576969.html
1.2.6 红黑树
一种自平衡二叉查找树, 通过对任何一条从根到叶子的路径上各个节点着色的方式的限制,红黑树确保从根到叶子节点的最长路径不会是最短路径的两倍,用非严格的平衡来换取增删节点时候旋转次数的降低,任何不平衡都会在三次旋转之内解决
红黑树的查询性能略微逊色于AVL树,因为他比avl树会稍微不平衡最多一层,也就是说红黑树的查询性能只比相同内容的avl树最多多一次比较,但是,红黑树在插入和删除上完爆avl树,avl树每次插入删除会进行大量的平衡度计算,而红黑树为了维持红黑性质所做的红黑变换和旋转的开销,相较于avl树为了维持平衡的开销要小得多
- 性质1:每个节点要么是黑色,要么是红色。
- 性质2:根节点是黑色。
- 性质3:每个叶子节点(NIL)是黑色。
- 性质4:每个红色结点的两个子结点一定都是黑色。
- 性质5:任意一结点到每个叶子结点的路径都包含数量相同的黑结点。
参考链接:https://www.jianshu.com/p/e136ec79235c
1.3 树的遍历
1 前序遍历:1、2、3、4、5、6
/ \
2 5 中序遍历:3、2、4、1、5、6
/ \ \
3 4 6 后序遍历:3、4、2、6、5、1
- 准备工作
public class Solution {
public static void main(String[] args) {
TreeNode root=new TreeNode(1);
TreeNode node1=new TreeNode(2);
TreeNode node2=new TreeNode(3);
TreeNode node3=new TreeNode(4);
TreeNode node4=new TreeNode(5);
TreeNode node5=new TreeNode(6);
root.left=node1;
root.right=node4;
node1.left=node2;
node1.right=node3;
node4.left=null;
node4.right=node5;
}
}
class TreeNode{
int val;
TreeNode left;
TreeNode right;
TreeNode(int val){
this.val=val;
}
}
1.3.1 前序遍历
- 思路:对任意子树,先访问根节点,遍历左子树,再遍历右子树
- 代码实现
递归实现
public static void preOrder(TreeNode root){
if(root== null)
return;
else{
System.out.println(root.val);
preOrder(root.left);
preOrder(root.right);
}
}
非递归实现1
public static void preOrder(TreeNode root){
List<Integer> res=new ArrayList<>();
if(root==null){
return res;
}
TreeNode cur=root;
Stack<TreeNode> stack=new Stack<>();
while(true){
while(cur != null) {
stack.push(cur);
res.add(cur.val);
cur = cur.left;
}
//不管有没有右节点
TreeNode temp = stack.pop();
//有右节点的话就去遍历右节点
if (temp.right != null) {
cur = temp.right;
}
if(stack.empty()&& cur==null) break;
}
return res;
}
非递归实现2:颜色标记法优化(未遍历过的节点,将节点入栈;遍历过,将节点值入栈),颜色标记法参考LeetCode题解
public static List<Integer> preOrder(TreeNode root) {
List<Integer> res=new ArrayList<>();
if(root==null){
return res;
}
TreeNode cur=root;
Stack<Object> stack=new Stack<>();
stack.push(cur);
while(!stack.empty()) {
Object o=stack.pop();
if(o instanceof TreeNode) {
TreeNode node=(TreeNode) o;
//因为前序遍历是根节点--左节点--右节点
//即出栈顺序为根节点--左节点--右节点,入栈顺序相反
if(node.right!=null) stack.push(node.right);
if(node.left!=null) stack.push(node.left);
stack.push(node.val);
}else {
res.add((int)o);
}
}
return res;
}
1.3.2 中序遍历
- 思路:对任意子树,先遍历左子树,访问根节点,再遍历右子树
- 代码实现
递归实现
public static void inOrder(TreeNode biTree){
if(biTree == null)
return;
else{
inOrder(biTree.left);
System.out.println(biTree.val);
inOrder(biTree.right);
}
}
非递归实现1
public static List<Integer> inOrder(TreeNode biTree){
List<Integer> res=new ArrayList<>();
if(root==null){
return res;
}
TreeNode cur=root;
Stack<TreeNode> stack=new Stack<>();
while(true){
while(cur != null) {
stack.push(cur);
cur = cur.left;
}
//不管有没有右节点
TreeNode temp = stack.pop();
res.add(temp.val);
//有右节点的话就去遍历右节点
if (temp.right != null) {
cur = temp.right;
}
if(stack.empty() && cur==null) break;
}
return res;
}
非递归实现2:颜色标记法优化(未遍历过的节点,将节点入栈;遍历过,将节点值入栈),颜色标记法参考LeetCode题解
public static List<Integer> inOrder(TreeNode root) {
List<Integer> res=new ArrayList<>();
if(root==null){
return res;
}
TreeNode cur=root;
Stack<Object> stack=new Stack<>();
stack.push(cur);
while(!stack.empty()) {
Object o=stack.pop();
if(o instanceof TreeNode) {
TreeNode node=(TreeNode) o;
//因为中序遍历是左节点--根节点--右节点
//即出栈顺序为左节点--根节点--右节点,入栈顺序相反
if(node.right!=null) stack.push(node.right);
stack.push(node.val);
if(node.left!=null) stack.push(node.left);
}else {
res.add((int)o);
}
}
return res;
}
1.3.3 后序遍历
- 思路:对任意子树,先遍历左子树,再遍历右子树,访问根节点
- 代码实现
递归实现
public static void postOrder(TreeNode root){
if(root== null)
return;
else{
postOrder(root.left);
postOrder(root.right);
System.out.println(root.val);
}
}
非递归实现1:逆序+前序遍历,使用LinkedList实现
public static List<Integer> postOrder(TreeNode root){
LinkedList<TreeNode> stack = new LinkedList<>();
LinkedList<Integer> res= new LinkedList<>();
if (root == null) {
return res;
}
stack.add(root);
while (!stack.isEmpty()) {
TreeNode node = stack.pollLast();
res.addFirst(node.val);
if (node.left != null) {
stack.add(node.left);
}
if (node.right != null) {
stack.add(node.right);
}
}
return res;
}
非递归实现2:破坏树结构
public List<Integer> postOrder(TreeNode root) {
List<Integer> res=new ArrayList<>();
if(root==null){
return res;
}
TreeNode cur=root;
//1.每拿到一个节点就把它保存在栈中
//2.继续对这个节点的左子树重复过程1,直到左子树为空
//3.因为保存在栈中的节点都遍历了左子树 但是没有遍历右子树,所以对栈中节点出栈并对它的右子树重复过程1
//4.直到遍历完所有节点
Stack<TreeNode> stack=new Stack<>();
while(true){
while(cur != null) {
stack.push(cur);
cur = cur.left;
}
//栈顶节点
TreeNode temp = stack.peek();
//没有右节点
if (temp.right == null) {
res.add(temp.val);
stack.pop();
}
//有的话就去遍历右节点
else {
cur = temp.right;
temp.right = null;
}
if(stack.empty()) break;
}
return res;
}
非递归实现3:不破坏树结构
public static List<Integer> postOrder(TreeNode root) {
List<Integer> res=new ArrayList<>();
if(root==null){
return res;
}
TreeNode cur=root;
Stack<TreeNode> stack=new Stack<>();
HashSet<TreeNode> set=new HashSet<>();//存放遍历过的节点
while(true){
while(cur != null) {
stack.push(cur);
cur = cur.left;
}
//栈顶节点
TreeNode temp = stack.peek();
//没有右节点
if (temp.right == null) {
res.add(temp.val);
set.add(temp);
stack.pop();
}
//有的话就去遍历右节点
else {
//右节点没被访问过
if(!set.contains(temp.right))
cur = temp.right;
else {
res.add(temp.val);
set.add(temp);
stack.pop();
}
}
if(stack.empty()) break;
}
return res;
}
非递归实现4:颜色标记法优化(未遍历过的节点,将节点入栈;遍历过,将节点值入栈),颜色标记法参考LeetCode题解
public static List<Integer> postOrder(TreeNode root) {
List<Integer> res=new ArrayList<>();
if(root==null){
return res;
}
TreeNode cur=root;
Stack<Object> stack=new Stack<>();
stack.push(cur);
while(!stack.empty()) {
Object o=stack.pop();
if(o instanceof TreeNode) {
TreeNode node=(TreeNode) o;
//因为后序遍历是左节点--右节点--根节点
//即出栈顺序为左节点--右节点--根节点,入栈顺序相反
stack.push(node.val);
if(node.right!=null) stack.push(node.right);
if(node.left!=null) stack.push(node.left);
}else {
res.add((int)o);
}
}
return res;
}
1.3.4 层序遍历
已知二叉树结构:
1
/ \
2 3
/ \ / \
4 5 6 7
输出:
[
[1],
[2,3],
[4,5,6,7]
]
非递归实现:颜色标记法优化(未遍历过的节点,将节点入栈;遍历过,将节点值入栈),颜色标记法参考LeetCode题解
1.3.5 已知任意两种遍历求树结构+第三种遍历结果
1.3.5.1 已知前序遍历+中序遍历求后序遍历
假设树中没有重复的元素,给出:
- 前序遍历 preorder = [3,9,20,15,7]
- 中序遍历 inorder = [9,3,15,20,7]
思路分析:
-
前序遍历第一个节点3是根节点
-
中序遍历3左面是左子树,右面是右子树,9是3的左节点
-
20是3的右节点,20左面是左子树15(即左节点),20右面是右子树(即右节点)
3 / \ 9 20 / \ 15 7
假设树中没有重复的元素,给出:
- 前序遍历 preorder = [1,2,4,5,3,6,7]
- 中序遍历 inorder = [4,2,5,1,6,3,7]
思路分析:
-
前序遍历第一个节点1是根节点
-
中序遍历1左面(4,2,5)是左子树,右面(6,3,7)是右子树
-
2属于左子树,2是1的左节点,2的左子树4(即左节点),右子树5(即右节点)
-
1的左子树遍历完毕
-
3属于右子树,3是1的右节点,3的左子树6(即左节点),右子树7(即右节点)
1 / \ 2 3 / \ / \ 4 5 6 7
代码实现:
在这里插入代码片
1.3.5.2 已知中序遍历+后序遍历求前序遍历
假设树中没有重复的元素,给出:
- 中序遍历 inorder = [9,3,15,20,7]
- 后序遍历 postorder = [9,15,7,20,3]
思路分析:
-
后序遍历最后一个节点3是根节点
-
中序遍历根节点3左侧(9)是左子树,右侧是右子树(15,7,20),9是3的左节点
-
后序遍历右子树(15,7,20)中最后一个节点20是右子树的根节点
-
中序遍历20左侧(15)是左子树(即左节点),右侧(7)是右子树(即右节点)
3 / \ 9 20 / \ 15 7
1.3.5.3 已知前序遍历+后序遍历求中序遍历
假设树中没有重复的元素,给出:
- 前序遍历 preorder = [1,2,4,5,3,6,7]
- 后序遍历 postorder = [4,5,2,6,7,3,1]
- 无法保证得到的二叉树结构唯一
思路分析:
-
前序遍历第一个节点1是根节点,也是后序遍历最后一个节点
-
假设2是1的左节点,即2是1的左子树的根节点
-
(4,5)可能分别是2的左右子节点,或者是2的右节点,如下图所示
-
同理对前序遍历中1的右子树(3,6,7),会有多种结果
1 1 / \ / \ 2 3 2 3 / \ / \ \ / \ 4 5 6 7 4 6 7 \ 5
2. 多叉树
2.1 B树 / B-树
是一种自平衡的多路搜索树(并不是二叉的),能够保证数据有序。同时它还保证了在查找、插入、删除等操作时性能都能保持在O(logn),为大块数据的读写操作做了优化,同时它也可以用来描述外部存储(支持对保存在磁盘或者网络上的符号表进行外部查找)
2.2 B+树
B+的搜索与B树也基本相同,区别是B+树只有达到叶子结点才命中(B树可以在非叶子结点命中),其性能也等价于在关键字全集做一次二分查找
参考链接
3. 哈夫曼编码
-
频率表 A:60、B:45、C:13、D:69、E:14、F:5、G:3
-
找出字符中最小的两个,小的在左边,大的在右边,组成二叉树。在频率表中删除此次找到的两个数,并加入此次最小两个数的频率和
- 频率表 A:60、B:45、C:13、D:69、E:14、FG:8
- 最小的是 FG:8与C:13,因此如图,并返回FGC的和:21给频率表