BST(Binary Search Tree)
基本特点:
- 二叉树
- 集合中的数据具有可比较大小的关键码
- 数据之间满足BST特性
- 中序遍历可得到一个递增的数据序列(可作为判断一棵二叉树是否是BST的方法)
- 同一个数据集合,可存在多个不同形态的BST树
基本操作
- 问题描述+求解动机+算法思想+算法步骤+性能分析
- 进行操作,都需要:先找到要操作的数(位置),进行操作,保证BST的特性,保障优的算法性能。
查找(logn) | 插入(logn ~ n) |
---|---|
若给定值小于根结点的关键字,则继续 在左子树上进行查找 | 若给定值小于根结点的关键字,则继续在左子树上进行插入; 将返回值(结点指针)设置为(当前)根结点的左孩子 |
若给定值大于根结点的关键字,则继续 在右子树上进行查找 | 若给定值大于根结点的关键字,则继续在右子树上进行插入; 将返回值(结点指针)设置为(当前)根结点的右孩子 |
若给定值等于根结点的关键字,则查找 成功 | / |
删除操作:
从最简单的情况开始——删除最小值;
由BST特性可知,BST中最小值一定在左子树的最左边取到,于是去get到它。
BST删除最小值算法思想(非递归)
1)定义一个指向BST树结点的临时指针变量p和pp。
2)从BST的根结点开始;将其值赋值给p。
3)若p的左孩子不等于空指针,将p的值赋值给pp;然后,将p的左孩子指针的值赋值给p;
4)重复第3步,直至p的左孩子的值等于空,结束查找。
5)p所指结点即为最小值结点。
6)若最小值结点不是根结点(BST根结点的左孩子不为空),则将p的右孩子指针作为pp的左孩子,否则将根结点的右孩子作为新的根结点。
算法分析
BST的最小值结点,从根结点的左孩子开始, 第一个左孩子为空的结点。
算法的步骤分为查找和接入。
- BST删除最小值的时间复杂度等于查找时间复杂度。
- 如果二叉树是平衡的,则有n个结点的二叉树 的高度约为logn,但是,如果二叉树完全不平衡(如成一个链表的形状),则其高度可以达到n。
BST删除算法思想
- 定位到待删除结点,此时有三种情况,设要删除结点为x,其父节点为px
1) 无子节点:直接将该 px->child 置空;
2) 有一个子节点:px->child = px->child->child;
3) 有两个子节点:找到X结点右子树中的最小值结点(后继结点)或者是左子树中的最大值(前驱节点),交换X结点的值和该节点的值(实际上实现了两个结点的互换),删除该结点,设置被删除结点的地址为该结点右子树的的最小值地址。 - 此时要注意的是,这里指的删除是指删除某一个值,而不是非要删除这个结点(用其他结点代替它被删除也行,反正值不会再出现在二叉树中了)。
# Heap 堆树或是一棵空树,或是具有下列性质的一棵**完全二叉树**(堆的局部有序特性):
- 每一个结点存储的值都小于等于其子节点存储的值——最小值堆(小顶堆)
- 每一个结点存储的值都大于等于其子节点存储的值——最大值堆(大顶堆)
由于是完全二叉树,用顺序表就十分方便存储
同一个数据集合,可以存在多个不同形态的堆
基本操作
插入:插入新值,新增结点,保持堆特性,算法代价要小
在哪最容易插入呢?叶子结点 最后的那个
算法思想:
均以最大值堆为例
- 在堆的末尾新增一个结点,存放新元素值val;
- 对其进行调整以维持堆的特性:
1.若它大于父节点的值,则将其与父节点交换,继续进行比较;
2.若它小于等于父节点的值或者是根节点了,则已处在正确位置,结束。
**算法分析**
- 堆的插入操作,插入结点都是作为一个叶子 结点插入到堆中。
- 算法的步骤分为插(接)入和交换值。
- 接入过程不需要移动结点,也不会整体改动树,所以时间开销为常数。
- 堆的插入时间复杂度取决于交换的次数。
- 堆是完全二叉树,则有n个结点的堆的高度为logn。插入新元素时,最佳情况时不用交换,最差情况交换logn,平均情况为logn。
构建
- 逐个插入法:新建一棵空堆,然后把数据集中的n个元素依次取出,逐个执行堆的 插入基本操作。
- 交换法建堆:新建一棵有n个结点的完全二叉树;然后把数据集中的n个元素任意的存储到完全二叉树中;判断完全二叉树中所有的父子结点对,若父结点中的元素值小于子结点中的元素值,则交换,直至完全二叉树满足堆的性质。
仅对第二种方法进行介绍。
交换法建堆算法思想
先要掌握调整结点的两种方法:向上调整and向下调整
- 向上调整(上拉):将要调整的结点x与其父节点进行比较,若比其大,则将这俩交换位置,再继续将x与现在的父节点进行比较;直至x为根节点或x小于等于其父节点的值为止。
- 向下调整(下拉):将要调整的结点x与其两个子节点进行比较,若小于它们,则取其中较大的那个与x交换,继续将x与其子节点进行比较;直至x大于它两个子节点的值或者为叶节点为止。
掌握了上面两种方法,那么建堆就容易了。 简述思路: 1. 直接将要存的数据按输入顺序存放到数组中; 2. **从[n/2]号位开始倒着枚举,对每个遍历到的结点i进行[i,n]范围内的调整** 为什么倒着枚举呢? 首先说为什么从[n/2]号位开始吧,因为其叶节点无法向下调整了,只需对所有非叶节点进行调整即可; 每一次调整都**将一个结点调整到了合适的位置,也就是说,以该节点为根结点的树已经满足了堆的特性,当前子树中权值最大的结点就会处在根节点的位置,这样当遍历到其父亲结点时,就可以直接使用这个结果**。
这种做法保证每个结点都是以其为根节点的子树中的权值最大的结点。
删除
同样,我们先讨论最简单的情况——删除堆中最大值。
由堆的性质可知,堆的最大值就是根节点,那么就删除第一个元素就可以了。那怎么删?
算法思想
- 在堆树中找到保存最大值的结点
1)最大值堆的最大值结点是最大值堆树的根结点。 - 删除最大值
2)保存最大值,交换堆的根结点的(最大)值与堆的最后一个 结点的值,堆的元素个数减一。 - 保持树的堆特性
若新树不空,对新的根结点的值R ,执行下拉(shiftdown)操作
3)R的值大于或等于其两个子结点,此时堆结构已经完成;
4)R的值小于某一个或全部两个子结点的值,此时R应与两个子结点中值较大的一个交换,若R仍然小于其新子结点的一个或两个。在这种情况下,只需要简单地继续这种将“R拉下来” 的过程,直至到达某一层使它大于它的子结点,或者它成为叶结点。
算法分析
- 删除最大值堆的最大值,即删除最大值堆的根结点。
- 如果基于数组实现最大值堆,最大值在数组 的基地址。
- 算法的步骤分为交换和维持堆性质。
- 堆删除最大值的时间复杂度等于对根结点执行一次下拉操作的时间复杂度。
- 堆是完全二叉树,则有n个结点的堆的高度为logn。下拉根结点时,最佳情况时不用交换,最差情况交换logn,平均情况为logn。
删除算法:
算法思想
- 在堆树中找到保存被删除的值结点
1)遍历最大值堆树,查找被删除的值,记住值的结点地址。 删除该值
2)交换堆的结点的值与堆的最后一个结点的值,堆的元素个数减一。 - 保持新树的堆特性
3)这个结点位置新的值,可能比父结点的值大,要向上交换,直到 其小于或等于其父结点的值,或达到根结点位置
4)然后这个值(新的位置)很可能比它的另一个子树的结点小,要执行下拉(shiftdown)操作:
4.1)R的值大于或等于其两个子结点,此时堆结构已经完成;
4.2)R的值小于某一个或全部两个子结点的值,此时R应与两个子 结点中值较大的一个交换,若R仍然小于其新子结点的一个或两个。在这种情况下,只需要简单地继续这种将“R拉下来”的过程,直至到达某一层使它大于它的子结点,或者它成为叶结点。
算法分析
- 删除最大值堆的某个值。
- 算法的步骤分为查找,交换和维持堆性质。
- 如果基于数组实现最大值堆,堆删除值的时间复杂度取决于查找,交换和维持堆性质的的时间复杂度。
- 堆是完全二叉树,则有n个结点的堆的高度为 logn。堆的查找操作时间复杂度为O(n),交换 的时间开销是常数,维持堆性质的上推和下拉操作的时间复杂度为O(logn),所以堆删除操作的时间复杂度为O(logn)
那么我们来思考一下,为什么插入的时候将元素放到末尾只需将元素向上调整,而删除的时候,交换待删的元素与最后一个,需要向上调整再向下调整呢?为什么多了一步?
其实仔细想想还是比较容易想到的。插入的时候,原有的堆就已经具有堆的特性,只是在最后加了一个元素需要调整。举个例子,就是说上面的所有父节点的值都大于两个子节点的值,一旦大于父节点,毫不犹豫需要上移且不可能需要再次下移。
而删除的时候,由于最后一个元素移到了中间某个位置,整个堆的特性都已经被破坏。举个例子,该值可能比它的子节点的值要小,如果仅向上调整的话会导致堆特性没法恢复。
堆排序
由堆的特性可知,堆顶元素是最大的,因此堆排序的直观思路就是取出堆顶元素,然后将堆的最后一个元素替换至堆顶,再进行依次针对堆顶元素的向下调整——如此重复直至堆中仅有一个元素未被遍历。
具体实现时,为了节省空间,可以倒着遍历数组。假设当前访问到i号为,那么将堆顶元素与i号为元素交换,接着在[1,i-1]范围内对堆顶元素进行一次向下调整即可。
让我们来对比一下BST和堆吧
| BST | Heap |
---|---|
insert | logn |
find | n |
delete | logn |
create | n |
Traverse | n |