之前在链表ADT中涉及到了队列,LInkedList通过双链表实现了队列先进先出的功能
此次提出一个优先队列的概念,它依旧满足队列一端入队一端出队的原则,唯一的区别在于出列的工作是找出、返回并删除优先队列中的最小值
优先队列中的操作仅仅限于插入和最小值,不能排序、也不能find(非最小值)
一次insert(入队)的平均为O(1),删除最小元(出队)平均时间O(logN)
对优先队列的实现:
1 不使用链表:链表在表头以O(1)执行插入操作,遍历链表删除最小元将花费O(N)的时间
2 不使用二叉查找树:二叉查找树插入删除都花费O(logN)的时间,但是如果删除最小元必在左子树,每次都删除左子树会破坏树的平衡,但是最重要的是优先队列操作仅仅对最小值的操作,二叉树提供的确实对整个数值的排序,因此使用二叉树有些大材小用。
为了实现优先队列,使用堆ADT
1 二叉堆
二叉堆普遍使用于优先队列的实现
1.1 二叉堆结构
二叉堆是一棵被完全填满的二叉树,这就意味着只有底层可能存在单节点,其他上层都是两个子节点
如此我们可知二叉堆的性质:
1 一棵高为h的二叉堆的节点总数量为 2 h ~ 2 h+1-1,深为i(自上向下第i层)父节点数量为2i
2 一颗节点数为N的二叉堆的高位O(logN)
3 我们设定根节点的下标为1,则对于下标为i的元素其左儿子下标为2i , 右儿子下标为 2i+1,父节点下标为 i/2;如果我们设定根节点的下标为0,则对于下标为i的元素其左儿子下标为2i+1 , 右儿子下标为 2i+2,父节点下标为 (i-1)/2
4 我们设定根节点下标为1,则所有父节点的取值范围 [1 , N/2];如果我们设定根节点下标为0,则所有父节点的取值范围 [0 , N-1/2]
除此之外由于我们想快速找出最小元,应该让最小元处于根位置上(如果找最大元,就让最大元在根位置上),考虑到任意一个子树都是一个二叉堆,我们有堆排性质:
5 为快速找出最小元,要求对二叉堆中的每一个节点X,X的值应当小于或等于自己子节点的值(如果要找最大值,则大于等于)
依据5条性质,实现二叉堆的优先队列的操作,首先规定二叉堆的根节点下标由1开始,使用一个数组容器容纳二叉堆中的节点
1.2 插入
1.2.1 实现
插入元素T
因为基于数组的实现,所以不需要查找,一开始要插入的位置就是数组[length+1]的节点A位置
在此节点A上,如果父节点的值小于T,则可以直接插入
如果父节点的值大于T,则说明T应该上移到较上层的位置,此时将父节点移动到A的位置
再次基于这个父节点,比较它的父节点是否小于T。。。直到找到一个节点O,O的父节点小于T,或者O的父节点为null,此时O即为元素T的位置
(对于根的父节点我们可以用下标控制,下标>=1)
这个过程叫做 上滤
我个人对上滤的理解:上层结构是满足条件的,从底层向上遍历,每次和父节点作比较
public void enqueue(T t) {
//从下角标1 开始,父节点 i/2
//入队,上滤
// 上滤:从底层向上层查找适合t的位置,找父节点
//如果t的父节点小于t 直接插入
//否则把t的父节点拉下来,在t的父节点的位置上查找
this.size++;//先加1 这时size表示的是底层要插入的空穴i
int i = this.size;
for(this.heap[0] = t;i >= 1; i = i/2) { // i是父节点,向上范围 >=1
if(((T)this.heap[i/2]).compareTo(t) > 0){ //父节点比较大,下移
this.heap[i] = this.heap[i/2];
}else{
break;
}
}
this.heap[i] = t;
}
1.2.2 时间复杂度
一次insert的平均为O(1)
最差情况:插入元素是最小值,应该上滤到堆顶,经过logN层(堆的高)比较,时间复杂度为O(logN)
1.3 删除最小元
1.3.1 实现
出队的操作就是删除最小元,最小元位于堆顶,即数组下标为1的元素
将根删除之后要重新调整结构,同时一个元素被删除,队列size-1,那么原来在数组[size]的元素必然要移动(即最后一个元素)
既然如此,我们令数组[1]=数组[size],将删除问题转换为 为数组[1]堆顶元素T不符合性质,为其寻找合适位置 的问题
首先在比较元素T和根的子节点大小,如果元素T比较小,那么这个位置就是T的位置
如果元素T比较大,在子节点之间选择最小的元素将他赋值到根节点,此时有一个子节点的位置空闲
继续比较T和这个子节点的子节点的大小,还是和上面情况一样,如果T小,那么这个位置就是元素T的位置
如果T大,在子节点之间选择一个较小者代替父节点,空出较小子节点位置。。。。
继续比较,直至T比子节点的值小,或T的子节点为null
(子节点为null,同样就是叶节点,用数组下标控制,2i<=size)
这个过程叫做下滤
我个人对上滤的理解:下层结构是满足条件的,从堆顶向下遍历,每次和子节点作比较
private void downSort(int index) {
// index位置的元素不符合二叉堆特性, 下滤查找适合他的位置并排序
//下滤,从顶层向下查找适合当前元素的位置,找子节点
//下层是符合排序的
// i的子节点 2i 2i+1
// 如果i的子节点比i要小,把最小的子节点上移,在子节点的位置继续向下查找 直到i的子节点不比i小
T t = (T)this.heap[index];//拿到这个元素
//有两个子节点,先找最小的子节点
int i = index;
int child ;//第一个子节点
for(; i <= this.size / 2; i = child){ // i是父节点,向下 i <= size/2
child = 2 * i;
if(child != this.size && ((T)this.heap[child]).compareTo((T)this.heap[child+1]) > 0) { //找最小的子节点,如果只有一个节点那就是2i
child++;
}
if(((T)this.heap[child]).compareTo(t) < 0) {
//把child上移
this.heap[i] = this.heap[child];
}else{
break; //否则跳出,子节点大,i就是要放的位置
}
}
this.heap[i] = t;
}
如上定义了一个通用方法,用来对index位置的元素寻找它的正确位置,删除操作就变成了
public T dequeue() {
//出队,返回删除最小元素(root),把最后一个元素拿到root处,下滤排序
T min = (T)this.heap[1];
this.heap[1] = this.heap[this.size];
this.size--;
downSort(1);
System.out.println(min);
return min;
}
1.3.2 时间复杂度
删除最小元最差情况:放在根的元素需要下滤到最底层,经过logN层比较,时间复杂度O(logN)
而实际上,被放到根上的元素几乎都下滤到最底层(它来自的那一层)
因此平均时间O(logN)
1.4 构建堆
构建堆,是在构造函数时接收一组数组,将这组数组作为堆的元素构建
首先我们想到的是将这组N个元素一一insert,每个inset平均花费O(1),那构建N个元素的堆平均花费O(N)
一般情况下的思路为:将N项以任意顺序放入树中,从最大的父节点开始上滤 执行 下滤操作
public void buildHeap(T[] list) {
//利用下滤,对每个i位置上不满足序列的排序
// 下滤要求下层是排好序的
// 所以要从底到顶
this.heap = new Object[this.size+DEFALUT_HEAP_SIZE];
int i = 1;
for(T t : list){
this.heap[i++] = t;
}
for(int j = this.size/2; j >= 1; j--) {// 父节点开始--
downSort(j);
}
}
父节点的取值范围为[1,size/2],从最后一个父节点开始一一向上遍历,每一个父节点执行在此节点上的下滤操作,为当前的父节点寻找合适的插入位置,直至根节点完成,二叉堆也就构建完成
此操作的时间复杂度的求解:
根节点操作O(logN) * 1
第一层父节点(O(logN)-1) * 2
……
第i层父节点 (O(logN)-i) * 2i
由此求和为O(N)
构建堆的时间复杂度为O(N)
1.5 扩展 d-堆
d-堆是对二叉堆的简单扩展,它的规则与二叉堆一样,只是所有的父节点都有d个儿子
二叉堆也可以叫做2-堆
在这里对时间复杂度分析:
d-堆比二叉堆要浅,d-堆的高度为O(logdN)
因此一次insert的平均运行时间为O(logdN)
但是对于删除操作,虽然堆变浅了,但是原先只需要在两个子节点之间找最小值,比较一次;现在需要在d个子节点之间找最小值,比较d-1次
删除操作平均花费O(d logdN),依旧为O(logN)层次
对d的讨论:
d的改变对父节点、子节点的位置发生影响:(第一个节点下标为1)
第i个节点,它的第一个子节点就是i*d-d+1,
最后一个子节点为i*d+1
第i个节点,它的父节点为(i-2+d)/d
为找到父子节点,会经过复杂的计算,尤其是除法运算占据较大的运行时间,这样会增大运行时间
除非d是2的幂,可以通过移位运算实现除法
那么d-堆一般的应用场景:
在优先队列太大以至于不能完全装入主存时,可以考虑使用d-堆降低深度
2 JavaCollection实现
在Java1.5之后才实现了对优先队列的支持:泛型类PriorityQueue
在该类中调用
add
方法实现入队
element
实现返回最小元素但不删除
remove
实现出队,返回并删除最小元素
3 堆的合并
除了不能排序、不能find之外,堆ADT的一个明显缺点是:很难将两个堆ADT合并成一个堆ADT(这种合并操作merge)
为了实现堆的合并,引进堆ADT的优化结构:左式堆、斜堆、二项队列