|
|
@ -561,105 +561,144 @@ public class Stack<E> extends Vector<E> {
|
|
|
|
|
|
|
|
|
|
|
|
### 树(Tree)
|
|
|
|
### 树(Tree)
|
|
|
|
|
|
|
|
|
|
|
|
**树(Tree)**是一个分层的数据结构,由节点和连接节点的边组成。是一种特殊的图,它与图最大的区别是没有循环。树的结构十分直观,而树的很多概念定义都有一个相同的特点:递归。也就是说,一棵树要满足某种性质,往往要求每个节点都必须满足。而树的考题,无非就是要考查树的遍历以及序列化(serialization)。常见的树:
|
|
|
|
**树(Tree)**是一个分层的数据结构,由节点和连接节点的边组成,是一种特殊的图,它与图最大的区别是没有循环。树的结构十分直观,而树的很多概念定义都有一个相同的特点:递归。
|
|
|
|
|
|
|
|
|
|
|
|
- **普通二叉树**
|
|
|
|
各种树解决的问题以及面临的新问题:
|
|
|
|
- **平衡二叉树**
|
|
|
|
|
|
|
|
- **完全二叉树**
|
|
|
|
|
|
|
|
- **二叉搜索树**
|
|
|
|
|
|
|
|
- **四叉树(Quadtree)**
|
|
|
|
|
|
|
|
- **多叉树(N-ary Tree)**
|
|
|
|
|
|
|
|
- **红黑树(Red-Black Tree)**
|
|
|
|
|
|
|
|
- **自平衡二叉搜索树(AVL Tree)**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#### 二叉树(Binary Tree)
|
|
|
|
- **二叉查找树(BST)**:解决了排序的基本问题,但是由于无法保证平衡,可能退化为链表
|
|
|
|
|
|
|
|
- **平衡二叉树(AVL)**:通过旋转解决了平衡的问题,但是旋转操作效率太低
|
|
|
|
|
|
|
|
- **红黑树**:通过舍弃严格的平衡和引入红黑节点,解决了AVL旋转效率过低的问题,但是在磁盘等场景下,树仍然太高,IO次数太多
|
|
|
|
|
|
|
|
- **B树**:通过将二叉树改为多路平衡查找树,解决了树过高的问题
|
|
|
|
|
|
|
|
- **B+树**:在B树的基础上,将非叶节点改造为不存储数据的纯索引节点,进一步降低了树的高度;此外将叶节点使用指针连接成链表,范围查询更加高效
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#### 树的遍历
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**① 前序遍历(Preorder Traversal)**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**实现原理**:`先访问根节点,然后访问左子树,最后访问右子树`。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**应用场景**:运用最多的场合包括在树里进行搜索以及创建一棵新的树。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**② 中序遍历(Inorder Traversal)**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**实现原理**:`先访问左子树,然后访问根节点,最后访问右子树`。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**应用场景**:最常见的是二叉搜索树,由于二叉搜索树的性质就是左孩子小于根节点,根节点小于右孩子,对二叉搜索树进行中序遍历的时候,被访问到的节点大小是按顺序进行的。
|
|
|
|
|
|
|
|
|
|
|
|
**二叉树(Binary Tree)**是n(n>=0)个结点的有限集合,该集合或者为空集(空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成(子树也为二叉树)。
|
|
|
|

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**二叉树特点**
|
|
|
|
**③ 后序遍历(Postorder Traversal)**
|
|
|
|
|
|
|
|
|
|
|
|
- 每个结点最多有两棵子树,所以**二叉树中不存在度大于2的结点**
|
|
|
|
**实现原理**:`先访问左子树,然后访问右子树,最后访问根节点`。
|
|
|
|
- 左子树和右子树是有顺序的,次序不能任意颠倒
|
|
|
|
|
|
|
|
- 即使树中某结点只有一棵子树,也要区分它是左子树还是右子树
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**应用场景**:在对某个节点进行分析的时候,需要来自左子树和右子树的信息。收集信息的操作是从树的底部不断地往上进行,好比你在修剪一棵树的叶子,修剪的方法是从外面不断地往根部将叶子一片片地修剪掉。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
|
|
**二叉树性质**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- **性质1**:在二叉树的第 i 层上**最多**有2^(i-1)个结点(i≥1)
|
|
|
|
|
|
|
|
- **性质2**:深度为k的二叉树**最多**有2^k-1个结点(k≥1)
|
|
|
|
|
|
|
|
- **性质3**:对任何一二叉树,如果其终端结点数(叶子节点数)为X,度为2的结点数为Y,则X = Y+1
|
|
|
|
|
|
|
|
- **性质4**:n个结点的完全二叉树的深度为[log2 n ] + 1
|
|
|
|
|
|
|
|
- **性质5**:对于任意一个完全二叉树来说,如果将含有的结点按照层次从左到右依次标号,对于任意一个结点 i ,完全二叉树还有以下3个结论成立:
|
|
|
|
|
|
|
|
- 当 i>1 时,父亲结点为结点 [i/2] 。(i=1 时,表示的是根结点,无父亲结点)
|
|
|
|
|
|
|
|
- 如果 2×i>n(总结点的个数) ,则结点 i 肯定没有左孩子(为叶子结点);否则其左孩子是结点 2×i 。
|
|
|
|
|
|
|
|
- 如果 2×i+1>n ,则结点 i 肯定没有右孩子;否则右孩子是结点 2×i+1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#### 二叉树(Binary Tree)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**二叉树是每个节点最多有两个子节点的树**。二叉树的叶子节点有0个字节点,根节点或内部节点有一个或两个子节点。
|
|
|
|
|
|
|
|
|
|
|
|
**存储结构**
|
|
|
|

|
|
|
|
|
|
|
|
|
|
|
|
其中data是数据域,lchild和rchild都是指针域,分别指向左孩子和右孩子。
|
|
|
|
**存储结构**:
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
```java
|
|
|
|
public class TreeNode {
|
|
|
|
public class TreeNode {
|
|
|
|
public Object data;
|
|
|
|
// 数据域
|
|
|
|
public TreeNode leftChild;
|
|
|
|
private Object data;
|
|
|
|
public TreeNode rightChild;
|
|
|
|
// 左孩子指针
|
|
|
|
|
|
|
|
private TreeNode leftChild;
|
|
|
|
|
|
|
|
// 右孩子指针
|
|
|
|
|
|
|
|
private TreeNode rightChild;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#### 红黑树(Red Black Tree)
|
|
|
|
#### 二叉搜索树(Binary Search Tree)
|
|
|
|
|
|
|
|
|
|
|
|
红黑树全称是Red-Black Tree,一种特殊的二叉查找树。红黑树的每个节点上都有存储位表示节点的颜色,可以是红(Red)或黑(Black)。
|
|
|
|
二叉搜索树, 又叫**二叉查找树**,它是一棵空树或是具有下列性质的二叉树:
|
|
|
|
|
|
|
|
|
|
|
|

|
|
|
|
- **若左子树不空,则左子树上所有结点的值均小于它的根结点的值**
|
|
|
|
|
|
|
|
- **若右子树不空,则右子树上所有结点的值均大于它的根结点的值**
|
|
|
|
|
|
|
|
- **它的左、右子树也分别为二叉搜索树**
|
|
|
|
|
|
|
|
|
|
|
|
红黑树的特性:
|
|
|
|

|
|
|
|
|
|
|
|
|
|
|
|
- **每个节点或者是黑色,或者是红色**
|
|
|
|
**效率总结**
|
|
|
|
- **根节点是黑色**
|
|
|
|
|
|
|
|
- **每个叶子节点(NIL)是黑色。 注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点**
|
|
|
|
|
|
|
|
- **如果一个节点是红色的,则它的子节点必须是黑色的**
|
|
|
|
|
|
|
|
- **从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
正是红黑树的这5条性质,使一棵n个结点的红黑树始终保持了logn的高度,从而也就解释了“红黑树的查找、插入、删除的时间复杂度最坏为O(log n)”这一结论成立的原因。
|
|
|
|
- **访问/查找**:最好时间复杂度 `O(logN)`,最坏时间复杂度 `O(N)`
|
|
|
|
|
|
|
|
- **插入/删除**:最好时间复杂度 `O(logN)`,最坏时间复杂度 `O(N)`
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**① 左旋**
|
|
|
|
#### 平衡二叉树(AVL Tree)
|
|
|
|
|
|
|
|
|
|
|
|
逆时针旋转两个节点,让一个节点被其右子节点取代,而该节点成为右子节点的左子节点。
|
|
|
|
二叉查找树在最差情况下竟然和顺序查找效率相当,这是无法仍受的。事实也证明,当存储数据足够大的时候,树的结构对某些关键字的查找效率影响很大。当然,造成这种情况的主要原因就是BST不够平衡(左右子树高度差太大)。既然如此,那么我们就需要通过一定的算法,将不平衡树改变成平衡树。因此,AVL树就诞生了。
|
|
|
|
|
|
|
|
|
|
|
|
对x进行左旋,意味着"将x变成一个左节点"。
|
|
|
|
**平衡二叉树全称叫做 `平衡二叉搜索(排序)树`,简称 AVL树**。高度为 `logN`。本质是一颗二叉查找树,AVL树的特性:
|
|
|
|
|
|
|
|
|
|
|
|

|
|
|
|
- 它是**一棵空树**或**左右两个子树的高度差**的绝对值不超过 `1`
|
|
|
|
|
|
|
|
- 左右两个子树也都是一棵平衡二叉树
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
如下图,根节点左边高度是3,因为左边最多有3条边;右边高度而2,相差1。根节点左边的节点50的左边是1条边,高度为1,右边有两条边,高度为2,相差1。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
|
|
**② 右旋**
|
|
|
|
**效率总结**
|
|
|
|
|
|
|
|
|
|
|
|
顺时针旋转两个节点,让一个节点被其左子节点取代,而该节点成为左子节点的右子节点。
|
|
|
|
- 查找:时间复杂度维持在`O(logN)`,不会出现最差情况
|
|
|
|
|
|
|
|
- 插入:插入操作时最多需要 `1` 次旋转,其时间复杂度在`O(logN)`左右
|
|
|
|
|
|
|
|
- 删除:删除时代价稍大,执行每个删除操作的时间复杂度需要`O(2logN)`
|
|
|
|
|
|
|
|
|
|
|
|
对x进行左旋,意味着"将x变成一个左节点"。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#### 红黑树(Red-Black Tree)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
二叉平衡树的严格平衡策略以**牺牲建立查找结构(插入,删除操作)的代**价,换来了稳定的O(logN) 的查找时间复杂度。但是这样做是否值得呢? 能不能找一种折中策略,即不牺牲太大的建立查找结构的代价,也能保证稳定高效的查找效率呢? 答案就是:红黑树。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**红黑树是一种含有红、黑结点,并能自平衡的二叉查找树**。高度为 `logN`。其性质如下:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- **每个结点或是红色的,或是黑色的**
|
|
|
|
|
|
|
|
- **根节点是黑色的**
|
|
|
|
|
|
|
|
- **每个叶子节点(NIL)是黑色的**
|
|
|
|
|
|
|
|
- **如果一个节点是红色的,则它的两个子节点都是黑色的**
|
|
|
|
|
|
|
|
- **任意一结点到每个叶子结点的路径都包含数量相同的黑节点**
|
|
|
|
|
|
|
|
|
|
|
|
**③ 变色**
|
|
|
|
正是红黑树的这5条性质,使一棵n个结点的红黑树始终保持了logN的高度,从而也就解释了“红黑树的查找、插入、删除的时间复杂度最坏为O(logN)”这一结论成立的原因。
|
|
|
|
|
|
|
|
|
|
|
|
变颜色条件:两个连续红色节点,并且叔叔节点是红色。
|
|
|
|

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**效率总结**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- **查找**:最好情况下时间复杂度为`O(logN)`,但在最坏情况下比`AVL`要差一些,但也远远好于`BST`
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- **插入/删除**:改变树的平衡性的概率要远远小于`AVL`(RBT不是高度平衡的)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
因此需要旋转操作的可能性要小,且一旦需要旋转,插入一个结点最多只需旋转`2`次,删除最多只需旋转`3`次(小于`AVL`的删除操作所需要的旋转次数)。虽然变色操作的时间复杂度在`O(logN)`,但是实际上,这种操作由于简单所需要的代价很小
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
##### 左旋
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- **逆时针旋转两个节点,让一个节点被其右子节点取代,而该节点成为右子节点的左子节点**
|
|
|
|
|
|
|
|
- **对x进行左旋,意味着"将x变成一个左节点"**
|
|
|
|
|
|
|
|
- **左旋条件:两个连续红节点,并且叔叔节点是黑色 , 下面的红色节点在右子树**
|
|
|
|
|
|
|
|
|
|
|
|
**④ 左旋条件**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
两个连续红节点,并且叔叔节点是黑色 , 下面的红色节点在右子树。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**情况1**:如果当前节点是右子树,并且父节点是左子树。`形如:(900是新插入节点)`
|
|
|
|
**情况1**:如果当前节点是右子树,并且父节点是左子树。`形如:(900是新插入节点)`
|
|
|
|
|
|
|
|
|
|
|
@ -668,9 +707,11 @@ public class TreeNode {
|
|
|
|
要根据父节点左旋【899】(根据谁左旋,谁就变成子节点):
|
|
|
|
要根据父节点左旋【899】(根据谁左旋,谁就变成子节点):
|
|
|
|
|
|
|
|
|
|
|
|

|
|
|
|

|
|
|
|
【900】的左子树 挂到 【899】的右子树上 , 【899】变为子节点 , 【900】变为父节点`
|
|
|
|
【900】的左子树挂到 【899】的右子树上 , 【899】变为子节点 , 【900】变为父节点`
|
|
|
|
`此时不变色。
|
|
|
|
`此时不变色。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**情况2**:如果当前节点是右子树,并且父节点是右子树。形如:(【100】是当前节点)
|
|
|
|
**情况2**:如果当前节点是右子树,并且父节点是右子树。形如:(【100】是当前节点)
|
|
|
|
|
|
|
|
|
|
|
|

|
|
|
|

|
|
|
@ -683,9 +724,18 @@ public class TreeNode {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**⑤ 右旋条件**
|
|
|
|
##### 右旋
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- **顺时针旋转两个节点,让一个节点被其左子节点取代,而该节点成为左子节点的右子节点**
|
|
|
|
|
|
|
|
- **对x进行左旋,意味着"将x变成一个左节点"**
|
|
|
|
|
|
|
|
- **右旋条件:两个连续红节点,并且叔叔节点是黑色 , 下面的红色节点在左子树**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
两个连续红节点,并且叔叔节点是黑色 , 下面的红色节点在左子树。
|
|
|
|
|
|
|
|
**情况1**:如果当前节点是左子树,并且父节点也是右子树。形如:(【8000】是当前节点)
|
|
|
|
**情况1**:如果当前节点是左子树,并且父节点也是右子树。形如:(【8000】是当前节点)
|
|
|
|
|
|
|
|
|
|
|
|

|
|
|
|

|
|
|
@ -696,6 +746,8 @@ public class TreeNode {
|
|
|
|
|
|
|
|
|
|
|
|
【9000】变为子节点,【8000】变为父节点,【8000】的右子树 挂到 【9000】的左子树,此时不变色。
|
|
|
|
【9000】变为子节点,【8000】变为父节点,【8000】的右子树 挂到 【9000】的左子树,此时不变色。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**情况2**:如果当前节点是左子树,并且父节点也是左子树。形如:(【899】是当前节点)
|
|
|
|
**情况2**:如果当前节点是左子树,并且父节点也是左子树。形如:(【899】是当前节点)
|
|
|
|
|
|
|
|
|
|
|
|

|
|
|
|

|
|
|
@ -709,14 +761,20 @@ public class TreeNode {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**⑥ 旋转场景**
|
|
|
|
##### 变色
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
如果当前节点的父亲节点和叔叔节点均是红色,那么执行以下变色操作:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
父 --> 黑
|
|
|
|
|
|
|
|
叔 --> 黑
|
|
|
|
|
|
|
|
爷 --> 红
|
|
|
|
|
|
|
|
|
|
|
|
无法通过变色而进行旋转的场景分为以下四种:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**场景一:左左节点旋转**
|
|
|
|
|
|
|
|
这种情况下,父节点和插入的节点都是左节点,如下图(旋转原始图1)这种情况下,我们要插入节点 65。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
规则如下:以祖父节点【右旋】,搭配【变色】。
|
|
|
|
**无法通过变色而进行旋转的四种场景**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**场景一:左左节点旋转**
|
|
|
|
|
|
|
|
这种情况下,父节点和插入的节点都是左节点,如下图(旋转原始图1)这种情况下,我们要插入节点 65。规则如下:以祖父节点【右旋】,搭配【变色】。
|
|
|
|
|
|
|
|
|
|
|
|

|
|
|
|

|
|
|
|
|
|
|
|
|
|
|
@ -725,18 +783,12 @@ public class TreeNode {
|
|
|
|

|
|
|
|

|
|
|
|
|
|
|
|
|
|
|
|
**场景二:左右节点旋转**
|
|
|
|
**场景二:左右节点旋转**
|
|
|
|
这种情况下,父节点是左节点,插入的节点是右节点,在旋转原始图 1 中,我们要插入节点 67。
|
|
|
|
这种情况下,父节点是左节点,插入的节点是右节点,在旋转原始图 1 中,我们要插入节点 67。规则如下:先父节点【左旋】,然后祖父节点【右旋】,搭配【变色】。按照规则,步骤如下:
|
|
|
|
|
|
|
|
|
|
|
|
规则如下:先父节点【左旋】,然后祖父节点【右旋】,搭配【变色】。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
按照规则,步骤如下:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

|
|
|
|

|
|
|
|
|
|
|
|
|
|
|
|
**场景三:右左节点旋转**
|
|
|
|
**场景三:右左节点旋转**
|
|
|
|
这种情况下,父节点是右节点,插入的节点是左节点,如下图(旋转原始图 2)这种情况,我们要插入节点 68。
|
|
|
|
这种情况下,父节点是右节点,插入的节点是左节点,如下图(旋转原始图 2)这种情况,我们要插入节点 68。规则如下:先父节点【右旋】,然后祖父节点【左旋】,搭配【变色】。
|
|
|
|
|
|
|
|
|
|
|
|
规则如下:先父节点【右旋】,然后祖父节点【左旋】,搭配【变色】。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

|
|
|
|

|
|
|
|
|
|
|
|
|
|
|
@ -745,63 +797,74 @@ public class TreeNode {
|
|
|
|

|
|
|
|

|
|
|
|
|
|
|
|
|
|
|
|
**场景四:右右节点旋转**
|
|
|
|
**场景四:右右节点旋转**
|
|
|
|
这种情况下,父节点和插入的节点都是右节点,在旋转原始图 2 中,我们要插入节点 70。
|
|
|
|
这种情况下,父节点和插入的节点都是右节点,在旋转原始图 2 中,我们要插入节点 70。规则如下:以祖父节点【左旋】,搭配【变色】。按照规则,步骤如下:
|
|
|
|
|
|
|
|
|
|
|
|
规则如下:以祖父节点【左旋】,搭配【变色】。
|
|
|
|

|
|
|
|
|
|
|
|
|
|
|
|
按照规则,步骤如下:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#### B树(Balance Tree)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
对于在内存中的查找结构而言,红黑树的效率已经非常好了(实际上很多实际应用还对RBT进行了优化)。但是如果是数据量非常大的查找呢?将这些数据全部放入内存组织成RBT结构显然是不实际的。实际上,像OS中的文件目录存储,数据库中的文件索引结构的存储…. 都不可能在内存中建立查找结构。必须在磁盘中建立好这个结构。
|
|
|
|
|
|
|
|
在磁盘中组织查找结构,从任何一个结点指向其他结点都有可能读取一次磁盘数据,再将数据写入内存进行比较。大家都知道,频繁的磁盘IO操作,效率是很低下的(机械运动比电子运动要慢不知道多少)。显而易见,所有的二叉树的查找结构在磁盘中都是低效的。因此,B树很好的解决了这一个问题。
|
|
|
|
|
|
|
|
|
|
|
|
#### 树的遍历
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**① 前序遍历(Preorder Traversal)**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**实现原理**:`先访问根节点,然后访问左子树,最后访问右子树`。在访问左、右子树的时候,同样,先访问子树的根节点,再访问子树根节点的左子树和右子树,这是一个不断递归的过程。
|
|
|
|
**B树也称B-树、B-Tree,它是一颗多路平衡查找树**。描述一颗B树时需要指定它的阶数,阶数表示了一个结点最多有多少个孩子结点,一般用字母m表示阶数。当m取2时,就是我们常见的二叉搜索树。B树的定义:
|
|
|
|
|
|
|
|
|
|
|
|

|
|
|
|
- **每个结点最多有m-1个关键字**
|
|
|
|
|
|
|
|
- **根结点最少可以只有1个关键字**
|
|
|
|
|
|
|
|
- **非根结点至少有Math.ceil(m/2)-1个关键字**
|
|
|
|
|
|
|
|
- **每个结点中的关键字都按照从小到大的顺序排列,每个关键字的左子树中的所有关键字都小于它,而右子树中的所有关键字都大于它**
|
|
|
|
|
|
|
|
- **所有叶子结点都位于同一层,或者说根结点到每个叶子结点的长度都相同**
|
|
|
|
|
|
|
|
|
|
|
|
**应用场景**:运用最多的场合包括在树里进行搜索以及创建一棵新的树。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**B树的优势**
|
|
|
|
|
|
|
|
|
|
|
|
**② 中序遍历(Inorder Traversal)**
|
|
|
|
- **B树的高度远远小于AVL树和红黑树(B树是一颗“矮胖子”),磁盘IO次数大大减少**
|
|
|
|
|
|
|
|
- **对访问局部性原理的利用**。指当一个数据被使用时,其附近的数据有较大概率在短时间内被使用。当访问其中某个数据时,数据库会将该整个节点读到缓存中;当它临近的数据紧接着被访问时,可以直接在缓存中读取,无需进行磁盘IO
|
|
|
|
|
|
|
|
|
|
|
|
**实现原理**:`先访问左子树,然后访问根节点,最后访问右子树`。在访问左、右子树的时候,同样,先访问子树的左边,再访问子树的根节点,最后再访问子树的右边。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**应用场景**:最常见的是二叉搜索树,由于二叉搜索树的性质就是左孩子小于根节点,根节点小于右孩子,对二叉搜索树进行中序遍历的时候,被访问到的节点大小是按顺序进行的。
|
|
|
|
如下图(B树的内部节点可以存放数据,类似ZK的中间节点一样。B树不是每个节点都有足够多的子节点):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
上图是一颗阶数为4的B树。在实际应用中的B树的阶数m都非常大(通常大于100),所以即使存储大量的数据,B树的高度仍然比较小。每个结点中存储了关键字(key)和关键字对应的数据(data),以及孩子结点的指针。**我们将一个key和其对应的data称为一个记录**。**但为了方便描述,除非特别说明,后续文中就用key来代替(key, value)键值对这个整体**。在数据库中我们将B树(和B+树)作为索引结构,可以加快查询速速,此时B树中的key就表示键,而data表示了这个键对应的条目在硬盘上的逻辑地址。
|
|
|
|
|
|
|
|
|
|
|
|
**③ 后序遍历(Postorder Traversal)**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**实现原理**:`先访问左子树,然后访问右子树,最后访问根节点`。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

|
|
|
|
#### B+树(B+Tree)
|
|
|
|
|
|
|
|
|
|
|
|
**应用场景**:在对某个节点进行分析的时候,需要来自左子树和右子树的信息。收集信息的操作是从树的底部不断地往上进行,好比你在修剪一棵树的叶子,修剪的方法是从外面不断地往根部将叶子一片片地修剪掉。
|
|
|
|
**B+树是从B树的变体**。跟B树的不同:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- **内部节点不保存数据,只用于索引**
|
|
|
|
|
|
|
|
- **B+树的每个叶子节点之间存在指针相连,而且是单链表**,叶子节点本身依关键字的大小自小而大顺序链接
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
如下图(其实B+树上二叉搜索树的扩展,二叉搜索树是每次一分为二,B树是每次一分为多),现代操作系统中,磁盘的存储结构使用的是B+树机制,mysql的innodb引擎的存储方式也是B+树机制:
|
|
|
|
|
|
|
|
|
|
|
|
**案例一:二叉搜索中第K小的元素**
|
|
|
|

|
|
|
|
|
|
|
|
|
|
|
|
给定一个二叉搜索树,编写一个函数 kthSmallest 来查找其中第 k 个最小的元素。
|
|
|
|
**B+树与B树相比有以下优势**
|
|
|
|
|
|
|
|
|
|
|
|
**解题思路**
|
|
|
|
- **更少的IO次数**:B+树的非叶节点只包含键,而不包含真实数据,因此每个节点存储的记录个数比B数多很多(即阶m更大),因此B+树的高度更低,访问时所需要的IO次数更少。此外,由于每个节点存储的记录数更多,所以对访问局部性原理的利用更好,缓存命中率更高
|
|
|
|
|
|
|
|
- **更适于范围查询**:在B树中进行范围查询时,首先找到要查找的下限,然后对B树进行中序遍历,直到找到查找的上限;而B+树的范围查询,只需要对链表进行遍历即可
|
|
|
|
|
|
|
|
- **更稳定的查询效率**:B树的查询时间复杂度在1到树高之间(分别对应记录在根节点和叶节点),而B+树的查询复杂度则稳定为树高,因为所有数据都在叶节点。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**B+树劣势**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
由于键会重复出现,因此**会占用更多的空间**。但是与带来的性能优势相比,空间劣势往往可以接受,因此B+树的在数据库中的使用比B树更加广泛。
|
|
|
|
|
|
|
|
|
|
|
|
这道题考察了两个知识点:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- **二叉搜索树的性质**:对于每个节点来说,该节点的值比左孩子大,比右孩子小,而且一般来说,二叉搜索树里不出现重复的值。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- **二叉搜索树的遍历**:二叉搜索树的中序遍历是高频考察点,节点被遍历到的顺序是按照节点数值大小的顺序排列好的。即,中序遍历当中遇到的元素都是按照从小到大的顺序出现。
|
|
|
|
#### B*树
|
|
|
|
|
|
|
|
|
|
|
|
因此,我们只需要对这棵树进行中序遍历的操作,当访问到第 k 个元素的时候返回结果就好。
|
|
|
|
是B+树的变体,**在B+树的非根和非叶子结点再增加指向兄弟的指针**,且**定义了非叶子结点关键字个数至少为(2/3)×M**,即块的最低使用率为2/3(代替B+树的1/2):
|
|
|
|
|
|
|
|
|
|
|
|

|
|
|
|

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|