目录
- 引言
- 1. 优先队列
- 2. 优先队列的实现
- 2.1 堆的概念
- 2.2 堆的创建
- 2.2.1 堆的向下调整
- 2.3 堆的插入
- 2.4 堆的删除
- 3. 总结
引言
前面一篇文章讲了二叉树,本篇将讲述数据结构中的优先队列,优先队列则需要用到完全二叉树来实现。
1. 优先队列
队列(Queue)是一个线性数据结构,它的理念是“先进先出”,而优先队列(Priority Queue)是一种特殊的队列数据结构,其中每个元素都有一个优先级,元素按照优先级进行排序。也就是说,要保证每次都是优先级最高的元素进行出列。
2. 优先队列的实现
既然队列是线性数据结构,为何还需要用二叉树?
优先队列本身确实是线性结构,但是优先队列的核心需求是高效地插入元素和删除具有最高优先级的元素。为了满足这一要求,需要用到堆这一数据结构。下面将对堆来进行介绍,并使用堆来完成优先队列的相关操作。
2.1 堆的概念
堆(Heap)是一种特殊的完全二叉树数据结构,分为大顶堆(也叫大根堆)(Max Heap)和小顶堆(小根堆)(Min Heap)。堆是一棵完全二叉树,以数组方式存储,且数组中的元素 Ki 满足 Ki <= K2i+1 且Ki <= K2i+2,这则是小顶堆,若是>= 则是大顶堆。
堆的存储:
{1,2,3,4,5,6,7,8,9,10}
2.2 堆的创建
以如下集合来创建堆:{65,50,23,81,75,32,16,7,43,3}
2.2.1 堆的向下调整
下面将以上面的乱序集合来建立堆。假设要构建大顶堆。不妨先将其中一个节点(23)进行调整,
用parent标记需要调整的节点,用child标记其左孩子。
然后判断child是否存在(即child小于usedSize)
存在则比较左孩子和右孩子的大小,将child下标改为值更大的下标。
而后比较child和parent对应的值大小,若child>parent,则交换,否则结束。
如此反复循环,直到child>=usedSize或者child<=parent。
代码如下:
private void siftDown(int parent, int usedSize) {
int child = 2 * parent + 1;
while(child < usedSize){
if(child + 1 < usedSize && elem[child] < elem[child + 1]){
child++;
}
if(elem[child] > elem[parent]){
swap(child, parent);
parent = child;
child = 2 * parent + 1;
}else{
break;
}
}
}
如果要全部调整,则需要从最后一个非叶子进行遍历,往前进行向下调整:
public void createHeap(int[] array) {
for(int parent = (array.length - 1 - 1) / 2; parent >= 0; parent--){
siftDown(parent, array.length);
}
}
具体步骤如下:
步骤 1:调整节点索引 4(值为 75)
左子节点索引为 24 + 1 = 9,值为 3
右子节点索引为 24 + 2 = 10,不存在
节点 75 大于其左子节点 3,不需要调整。
步骤 2:调整节点索引 3(值为 81)
左子节点索引为 23 + 1 = 7,值为 7
右子节点索引为 23 + 2 = 8,值为 43
节点 81 大于其子节点 7 和 43,不需要调整。
步骤 3:调整节点索引 2(值为 23)
左子节点索引为 22 + 1 = 5,值为 32
右子节点索引为 22 + 2 = 6,值为 16
比较节点 23 和其左子节点 32,交换它们的位置:
索引: 0 1 2 3 4 5 6 7 8 9
数组: [65, 50, 32, 81, 75, 23, 16, 7, 43, 3]
继续调整节点 23 的位置:
左子节点索引为 25 + 1 = 11,不存在
右子节点索引为 25 + 2 = 12,不存在
节点 23 没有子节点,不需要进一步调整。
步骤 4:调整节点索引 1(值为 50)
左子节点索引为 21 + 1 = 3,值为 81
右子节点索引为 21 + 2 = 4,值为 75
比较节点 50 和其左子节点 81,交换它们的位置:
索引: 0 1 2 3 4 5 6 7 8 9
数组: [65, 81, 32, 50, 75, 23, 16, 7, 43, 3]
继续调整节点 50 的位置:
左子节点索引为 23 + 1 = 7,值为 7
右子节点索引为 23 + 2 = 8,值为 43
节点 50 大于其子节点 7 和 43,不需要进一步调整。
步骤 5:调整节点索引 0(值为 65)
左子节点索引为 20 + 1 = 1,值为 81
右子节点索引为 20 + 2 = 2,值为 32
比较节点 65 和其左子节点 81,交换它们的位置:
索引: 0 1 2 3 4 5 6 7 8 9
数组: [81, 65, 32, 50, 75, 23, 16, 7, 43, 3]
继续调整节点 65 的位置:
左子节点索引为 21 + 1 = 3,值为 50
右子节点索引为 21 + 2 = 4,值为 75
比较节点 65 和其右子节点 75,交换它们的位置:
索引: 0 1 2 3 4 5 6 7 8 9
数组: [81, 75, 32, 50, 65, 23, 16, 7, 43, 3]
继续调整节点 65 的位置:
左子节点索引为 24 + 1 = 9,值为 3
右子节点索引为 24 + 2 = 10,不存在
节点 65 大于其左子节点 3,不需要进一步调整。
最后调整后的数组如下:{81 75 32 50 65 23 16 7 43 3}
2.3 堆的插入
插入操作如下:
先将新元素插入到最后一个位置,而后向上调整这个元素。
向上调整和向下调整反过来,将子节点与其父节点进行比较而后调整。
private void siftDown(int parent, int usedSize) {
int child = 2 * parent + 1;
while(child < usedSize){
if(child + 1 < usedSize && elem[child] < elem[child + 1]){
child++;
}
if(elem[child] > elem[parent]){
swap(child, parent);
parent = child;
child = 2 * parent + 1;
}else{
break;
}
}
}
插入操作:
public void offer(int val){
if(isFull()){
elem = Arrays.copyOf(elem, elem.length * 2);
}
elem[usedSize] = val;
siftUp(usedSize);
usedSize++;
}
假设插入一个值为80的元素,操作如下:
2.4 堆的删除
堆的每次删除是指定位置,即删除根节点的元素。
操作如下:
交换根节点和最后一个节点的位置,usedSize减少1,而后对堆顶元素进行向下调整。 代码如下:
public int poll(){
if(isEmpty()){
throw new UnsupportedOperationException("队列为空");
}
int ret = elem[0];
elem[0] = elem[--usedSize];
siftDown(0, usedSize);
return ret;
}
执行删除操作后:
3. 总结
到这就是优先队列的基本内容了,此外,使用优先队列可以对数据进行堆排序,将在后面讲述。