目录
1 树基础
1.1 基本概念
1.1.1 树
树是非线性存储,而链表,数组是线形存储。
- 节点的高度
节点到叶子结点的最长路径(边数) - 节点深度
根节点到这个节点所经历的节点的个数(数据结构与算法之美中的定义是边的个数) - 节点层数
节点深度+1 - 树的高度
根节点的高度
高度 深度 层
1 3 1 1
/ \
2 3 2 2 2
/ \ / \
4 5 6 7 1 3 3
/ \ /
8 9 10 0 4 4
1.1.2 二叉树
满二叉树
- 叶子结点全部在最底层
- 除叶子结点外,每个节点均有左右子节点
1
/ \
2 3
/ \ / \
4 5 6 7
完全二叉树
- 叶子结点在最底下两层
- 倒数第二层的节点均有左右子节点
- 最下面一层的叶子结点靠左边排列
1
/ \
2 3
/ \ / \
4 5 6 7
/ \ /
8 9 10
二叉搜索树
- 树中任意节点,其左子树不为空,则左子树中每个节点的值小于当前节点
- 树中任意节点,其右子树不为空,则右子树中每个节点的值大于当前节点
平衡二叉查找树
- 二叉树中任意节点的左右子树的高度不能大于1
1.1.3二叉树的性质
- 二叉树的
i
层,节点数最多是2^(i-1)
,i>=1
- 二叉树的深度是
k
,二叉树节点数最多是2^k - 1
,k>=1
- 具有
n
个节点的完全二叉树的深度最大是log(n) + 1
- 满二叉树的深度是
k
,节点总数是2^k - 1
1.1.4 霍夫曼树
1.1.4.1 概念
给定n个权值作为n个叶子结点,如果带权路径最小,这样的书就是霍夫曼树。如下例子所示,树B就是霍夫曼树。
例如给定4个数字:7,5,2,4
树A,带权路径值:2*7 +2*5+2*2+2*4=36
0
/ \
0 0
/ \ / \
7 5 2 4
树B,带权路径值:1*7 +2*5+3*2+3*4=35
0
/ \
7 0
/ \
5 0
/ \
2 4
1.1.4.2 建造步骤
- 在 n 个权值中选出两个最小的权值
- 对应的两个结点组成一个新的二叉树,且新二叉树的根结点的权值为左右孩子权值的和
- 在原有的 n 个权值中删除那两个最小的权值,同时将新的权值加入到 n–2 个权值的行列中
- 重复 1,2,3 ,直到所以的结点构建成了一棵二叉树为止,这棵树就是哈夫曼树。
1.1.5 B树
每个节点最多包含k个子节点,k就是B树的阶。
B树有以下特点:
- 根结点至少有两个子女。
- 每个中间节点都包含k-1个元素和k个孩子,其中 m/2 <= k <= m
- 每一个叶子节点都包含k-1个元素,其中 m/2 <= k <= m
- 所有的叶子结点都位于同一层。
- 每个节点中的元素从小到大排列,节点当中k-1个元素正好是k个孩子包含的元素的值域分划。
下图是3阶的B树示意图,可以参照B树特点加深理解。
1.1.6 B+树
一个m阶的B+树具有如下几个特征:
- 有k个子树的中间节点包含有k个元素(B树中是k-1个元素),每个元素不保存数据,只用来索引,所有数据都保存在叶子节点
- 所有的叶子结点中包含了全部元素的信息,及指向含这些元素记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。
- 每一个父节点的元素都会出现在子节点,是子节点的最大/最小元素。
B+树与B树区别
B树 | B+树 | |
---|---|---|
根节点与子节点均存储数据,叶子结点也会存储数据 | 根节点与子节点存储的是数据的地址,叶子结点存储所有的数据 | |
区间查找需要对树中序遍历 | 区间查找就是遍历有序链表 | |
叶子节点之间无连接 | 叶子结点之间是链表链接 | |
区间查找,最好的情况是存在于根节点,最坏的情况是存在于叶子结点,不稳定。 | 稳定 | |
B树的IO次数会更多 | 中间节点不存储数据,只存储指针,同样大小的磁盘页可以容纳更多的节点元素。相同数据量,B+更加矮胖,IO次数也会减少。 | |
IO次数更少:由于B+树在内部节点上不包含数据信息,因此在内存页中能够存放更多的key。 数据存放的更加紧密,具有更好的空间局部性。因此访问叶子节点上关联的数据也具有更好的缓存命中率。 | ||
遍历更加方便:B+树的叶子结点都是相链的,因此对整棵树的遍历只需要一次线性遍历叶子结点即可。而且由于数据顺序排列并且相连,所以便于区间查找和搜索。而B树则需要进行每一层的递归遍历。相邻的元素可能在内存中不相邻,所以缓存命中性没有B+树好。 |
-
B+树的磁盘读写代价更低:B+树的内部节点并没有指向关键字具体信息的指针,因此其内部节点相对B树更小,如果把所有同一内部节点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多,一次性读入内存的需要查找的关键字也就越多,相对IO读写次数就降低了。
-
B+树的查询效率更加稳定:由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。
-
B+树更便于遍历:由于B+树的数据都存储在叶子结点中,分支结点均为索引,方便扫库,只需要扫一遍叶子结点即可,但是B树因为其分支结点同样存储着数据,我们要找到具体的数据,需要进行一次中序遍历按序来扫,所以B+树更加适合在区间查询的情况,所以通常B+树用于数据库索引。
-
B+树更适合基于范围的查询:B树在提高了IO性能的同时并没有解决元素遍历的我效率低下的问题,正是为了解决这个问题,B+树应用而生。B+树只需要去遍历叶子节点就可以实现整棵树的遍历。而且在数据库中基于范围的查询是非常频繁的,而B树不支持这样的操作或者说效率太低。
1.2 树的遍历
树的结构如下:
A
/ \
B C
/ \ / \
D E F G
1.2.1深度优先搜索(DFS)
前序遍历
对于树中的任意节点来说,先打印这个节点,然后再打印它的左子树,最 后打印它的右子树。
中序遍历
对于树中的任意节点来说,先打印它的左子树,然后再打印它本身,最后 打印它的右子树。
后序遍历
对于树中的任意节点来说,先打印它的左子树,然后再打印它的右子树, 最后打印这个节点本身。
遍历方式
前序遍历:A->B->D->E->C->F->G
中序遍历:D->B->E->A->F->C->G
后序遍历:D->E->B->F->G->C->A
1.2.2 广度优先遍历(BFS)
- 也叫层次遍历
- 会先访问离根节点最近的节点
- 从左到右,从上到下的方法遍历
A
/ \
B C
/ \ / \
D E F G
广度优先遍历:A->B->C->D->E->F->G
1.2 复杂度分析
O(n)
待续
2 应用场景
深度优先遍历-栈
广度优先遍历-队列
1
/ \
2 3
/ \ / \
4 5 6 7
/ \ /
8 9 10
1、节点1,插入队列【1】
2、取出节点1,插入1的子节点2,3 ,节点2在队列的前端【2,3】
3、取出节点2,插入2的子节点4,5,节点3在队列的最前端【3,4,5】
4、取出节点3,插入3的子节点6,7,节点4在队列的最前端【4,5,6,7】
5、取出节点4,插入3的子节点8,9,节点5在队列的最前端【5,6,7,8,9】
6、取出节点5,插入5的子节点10,节点6在队列的最前端【6,7,8,9,10】
7、取出节点6,没有子节点,不插入,节点7在队列的最前端【7,8,9,10】
8、取出节点7,没有子节点,不插入,节点8在队列的最前端【8,9,10】
9、取出节点8,没有子节点,不插入,节点9在队列的最前端【9,10】
10、取出节点9,没有子节点,不插入,节点10在队列的最前端【10】
11、取出节点10,队列为空,算法结束
3 模版代码
3.1 遍历树
/**
* 中序遍历
*/
public void inorderTraversal(TreeNode root) {
Stack<TreeNode> stack = new Stack<>();
if (root == null) {
return list;
}
stack.push(root);
while (stack.size() > 0) {
TreeNode node = stack.pop();
if (node.left == null && node.right == null) {
// 叶子结点
// 处理叶子结点
// 找到叶子结点,还需要继续迭代,不能向下走,不可执行stack.push(node)
continue;
}
// 右子节点入栈->本节点入栈->左子节点入栈,出栈的顺序就是左中右
if (node.right != null) {
stack.push(node.right);
node.right = null;
}
stack.push(node);
if (node.left != null) {
stack.push(node.left);
node.left = null;
}
}
}
/**
*
* 前序遍历
*/
public List<Integer> preorderTraversal(TreeNode root) {
if (root == null) {
return list;
}
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while (stack.size() > 0) {
TreeNode node = stack.pop();
// 处理节点数据
if (node.right != null) {
stack.push(node.right);
}
if (node.left != null) {
stack.push(node.left);
}
}
return list;
}
/**
* 后续遍历
* 按照中-右-左顺序遍历,然后逆序输出,就变成左->右->中。这个不是严格的后续遍历,只是输出结果与后续遍历一致
*/
public List<Integer> postorderTraversal(TreeNode root) {
ArrayList<Integer> list = new ArrayList<>();
if (root == null) {
return list;
}
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode node = stack.pop();
if (node.left == null && node.right == null) {
// 叶子结点
// 处理叶子结点
list.add(node.val);
// 找到叶子结点,还需要继续迭代,不能向下走,不可执行stack.push(node)
continue;
}
if (node.left != null) {
stack.push(node.left);
node.left = null;
}
if (node.right != null) {
stack.push(node.right);
node.right = null;
}
stack.push(node);
}
// 按照中-右-左顺序遍历,然后逆序输出
Collections.reverse(list);
return list;
}
引用
算法第四版