一、优先队列
优先队列(Prior Queue):特殊的“队列”,取出元素的顺序是以招元素的优先权(关键字)大小,而不是元素进入队列的先后顺序
数组 | 插入——元素总是插入尾部 | Θ(1) |
删除——查找最大(或最小)关键字 | Θ(n) | |
移动数组元素后删去 | O(n) | |
链表 | 插入——元素总是插入头部 | Θ(1) |
删除——查找最大(或最小)关键字 | Θ(n) | |
删除结点 | Θ(1) | |
有序数组 | 插入——找到合适的位置 | O(n)/O(log2N) |
移动元素并插入 | O(n) | |
删除——删去最后一个元素 | Θ(1) | |
有序链表 | 插入——找到合适的位置 | O(n) |
插入元素 | Θ(1) | |
删除——删除首元素或最后元素 | Θ(1) |
二、堆的定义、性质及用途
堆是一种由完全二叉树(数组存储)表示的“优先队列”,不同于普通队列的进出队列先后次序优先,堆中元素按照其优先权的大小优先取出
堆的两大特性:
- 结构性:用数组表示的完全二叉树
- 有序性:最大堆(maxheap),大顶堆:任一结点的关键字是其子树所有结点的最大值;最小堆(minheap),小顶堆:任一结点的关键字是其子树所有结点的最小值
堆可应用于构建堆排序、查找集合最大(小)关键字、构建优先队列。从根结点到任意节点路径上结点序列都具备有序性。
而二叉排序树查找效率高(平衡二叉树效率更高,因为树矮),这是堆所没有的优势。此外,二叉排序树左子树<根<右子树或>,而堆只区分根和子树,子树间的大小未定。
三、堆的抽象数据类型
类型名称:最大堆(maxheap)
数据对象集:完全二叉树,每个结点的元素值不小于其子结点的元素值
操作集:最大堆H∈maxheap,元素item∈ElementType,主要操作:
- MaxHeap Create( int MaxSize ):创建一个空的最大堆
- Boolean IsFull( MaxHeap H ):判断最大堆是否已满
- Insert( MaxHeap H, ElementType item ):将元素item插入最大堆
- Boolean IsEmpty( Maxsize H ):判断最大堆H是否为空
- ElementType DeleteMax( MaxHeap ):返回H中最大的元素(高优先级)
MaxHeap Create( int MaxSize ):创建一个空的最大堆
typedef struct HeapStruct *MaxHeap;
struct HeapStruct {
ElementType *Elements;//为什么这里不定义成数组形式?
int Size;
int Capacity;
};
MaxHeap Create( int maxsize ){
MaxHeap H = malloc( sizeof( struct HeapStruct ) );
H->Elements = malloc( (maxsize+1) * sizeof(ElementType));
H->Size = 0;
H->Capacity = maxsize;
H->Elements[0] = maxdata;//将maxdata换成mindata,同样也适用于最小堆的创建
return H;
}
Boolean IsFull( MaxHeap H ):判断最大堆是否已满
Boolean IsFull( MaxHeap H ){
return H->Size;
}
Boolean IsEmpty( Maxsize H ):判断最大堆H是否为空
Boolean IsEmpty( MaxHeap H ){
return !H->Size;
}
Insert( MaxHeap H, ElementType item ):将元素item插入最大堆
插入结点:先将要插入的结点x放在最底层的最右边,插入后满足完全二叉树的特点,然后依次把x向上调整到合适的位置以满足父大子小的性质(找出一条x到根结点的路径,沿路径做“BubbleSort”)。
//算法:将新增结点插入到数组末尾,然后调整
// T(n) = O(logN)
void Insert( MaxHeap H, ElementType item ){
//将元素item插入最大堆H,其中 H->Elements[0]已经定义为哨兵
int i;
if( IsFull(H) ){
printf("最大堆已满");
return;
}
i = ++H->Size;//i指向插入后堆中最后一个元素的位置
//换言之,在堆末尾插入
for(; H->Elements[i/2] < item; i /= 2)
H->Elements[i] = H->Elements[i/2];
//比较此时新结点和父结点的大小(完全二叉树),父结点较小,则父结点被换下去
//退出循环时,标志找到新结点的正确位置
//H->Elements[0]是哨兵元素,它不小于堆中最大元素,控制循环结束
//否则for内第二个条件要改成 H->Elements[i/2] < item && i>1;
H->Elements[i] = item;
}
ElementType DeleteMax( MaxHeap ):返回H中最大的元素(高优先级)
删除结点:删除某个结点后完全二叉树会出现一个空洞,把最底层最右边的叶子结点的值赋给该孔并下调至合适的位置(只需要调整拿来补洞的结点相关的位置,其余已经有序、不受影响的结点不必移动,从上向下依次比较孩子,小的换下去)
//T(N) = O(logN)
//删除堆顶元素,将最后一个元素放到堆顶,和insert一样调整
ElementType DeleteMax( MaxHeap H ){
int Parent, Child;
ElementType MaxItem, temp;
if( IsEmpty(H) ){
printf("最大堆已空");
return;
}
MaxItem = H->Elements[1];
temp = H->Elements[H->Size--];
//此处数组顺序是按照从下标1开始生成二叉树,否则Child=Parent*2+1
for(Parent = 1; Parent*2<=H->Size; Parent = Child ){
Child = Parent * 2;
//找到较大的孩子
if( Child < H->Size && H->Elements[Child] < H->Elements[Child+1])
Child ++;
//Parent是待插入位置
//如果temp的值大于Child,那么此时Parent位置正确
//反之,Child的值上移,Parent的值下移
if( temp >= H->Elements[Child] ) break;
else
H->Elements[Parent] = H->Elements[Child];
}
//退出循环时找到了合适的位置插入
H->Elements[Parent] = temp;
return MaxItem;
}
最大堆的建立
建立最大堆:将n个已经存在的元素按照最大堆的要求存放在一维数组内
方法1:先建立空堆,通过Insert操作,将N个元素一个个相继插入到空堆中,其时间代价最大为O(N logN)。
方法2:在O(N)下建立最大堆
- 将N个元素按照输入顺序存入,先满足完全二叉树的结构特性
- 调整各个结点位置,以满足最大堆的有序特性
将序列存入一个数组中,得到顺序存储的完全二叉树。
建堆从[n/2]开始,即编号最大的第一个非叶结点开始(因为叶子节点没有左右孩子,已然满足堆的定义),然后从下向上依次调整:
四、堆排序
堆排序思想:将一个无序序列调整成一个堆,可以找出这个序列的最值,然后见这个值交换到序列的最后或最前。这样,有序序列关键字增加一个,无序序列关键字减少一个,对新的无序序列重复这样操作,最终实现排序。
最关键的操作是将序列调整为堆,整个排序过程是通过不断调整,使得不符合堆定义的完全二叉树变为符合堆定义的完全二叉树。
时间复杂度O(N logN):函数Sift()中,j走了一条从当前结点到叶子结点的路径,完全二叉树的高度为
,即对每个节点调整的时间复杂度为O(logN)。对于函数heapSort(),基本操作次数是两个并列的for循环的基本操作次数之和。
空间复杂度O(1):只申请了一个temp。
堆排序在最坏情况下的时间复杂度也是O(N logN),这是它相对于快速排序的最大优点,此外,堆排序的空间占用也小于快速排序的O(logN)。但是快速排序在同级别时间复杂度的算法中,系数是最小的,因此叫做快速排序。
//在数组R[low]到R[high]的范围内对在位置low上的结点进行调整
void Sift( int R[], int low, int high ){//从数组下标1开始存储
int i = low, j = 2*i;
int temp = R[i];
//找到插入位置i
while( j <= high ){
if( j < high && R[j] < R[j+1] ) ++j;//j指向较大的孩子
//如果当前j的值仍大于temp,那么交换位置,j指向其孩子结点
if( temp < R[j] ){
R[i] = R[j];
i = j;
j *= 2;
}
else break;
}
//temp找到合适的插入位置i
R[i] = temp;
}
void heapSort( int R[], int n ){
int i;
int temp;
//从最后一个非叶子结点开始建立最大堆
for( i = n/2; i >= 1;--i ) Sift(R, i, n );
//不断将堆顶结点换到序列末尾,新的无序序列结点数目减少,最终得到
//一个递增序列
for( i = n; i >= 1;--i ){
//换出根结点中的关键字,将其放入最终位置
temp = R[1];
R[1] = R[i];
R[i] = temp;
//在减少了一个关键字的无序序列中调整
Sift( R,1,i-1);
}
}