系列文章
《算法导论》学习(一)---- 插入排序和归并排序
《算法导论》学习(七)----堆排序和优先队列(C语言)
《算法导论》学习(八)----快速排序(C语言)
《算法导论》学习(九)----为什么比较排序算法时间复杂度的下界是确定的?
《算法学习》学习(十)----计数排序,基数排序,桶排序(C语言)
文章目录
前言
本文主要讲解了堆的数据结构,并且用C语言实现了堆排序和优先队列
一、堆
1.什么是堆?
(二叉)堆从存储空间的本质上来讲是一个数组,但是在进行数据处理的时候,我们将它当作是一个近似的完全二叉树。
1.关于堆的结构,有以下的要点:
1.树的结点里面是存储的值,每个结点上方标记的是元素在数组中的索引。
2.根据图我们可以看出,堆的数据排列方式是从数组的第一个元素开始,自上到下,从左到右。
3.每个结点最多与下面的两个结点相连。
4.对于第4,8,9数据,它们组成了一个子堆。在这个子堆中,4是8,9的父结点,8是4的左孩子;9是4的右孩子。
5.最顶端的结点是根结点;最底层的结点是叶子结点。
6.n个元素的堆的高度是
2.那么根据堆的结构,我们可以得到如下结论
1.对于一个给定结点的序号为i(非根结点)
(1)父结点的序号:i/2(向下取整)。
(2)左孩子结点的序号:2i。
(3)右孩子结点的序号:2i+1。
2.最大堆和最小堆
最大堆和最小堆的性质:
1.最大堆的性质:除了根结点以外的所有结点都满足:
父结点都大于它们的左右孩子结点
2.最小堆的性质:除了根结点以外的所有结点都满足:
父结点都小于它们的左右孩子结点
二、堆排序
1.什么是堆排序?
堆排序就是运用一种叫“堆”的数据结构来进行排序。它是集合了插入排序和归并排序优点的一种算法。
1.与插入排序相似的是:它具有空间原址性
2.与归并排序相似的是:它的时间复杂度是O(nlgn)
2.最大堆的两种操作函数
维护最大堆函数
void MAXHEAP_IFY(int *x,int n,int size)
{
//得到n结点的左右孩子结点
int l=2*n;
int r=2*n+1;
int largest=n;//假设n结点比它的左右孩子都大,为它赋初值
/*
如果左右孩子存在的情况下,找到n的左右孩子和n结点三者之间的最大值
*/
if(l<=size&&x[l-1]>x[n-1])
{
largest=l;
}
if(r<=size&&x[r-1]>x[largest-1])
{
largest=r;
}
/*
如果n,n的左孩子,n的右孩子
三者中的最大值不是n的话
我们需要调整三者的关系
使满足最大堆的性质
*/
if(largest!=n)
{
int temp;
temp=x[n-1];
x[n-1]=x[largest-1];
x[largest-1]=temp;
//如果改变了结构
//那么可能导致下一层的最大堆性质不满足
//需要进一步维护
MAXHEAP_IFY(x,largest,size);
}
}
构造最大堆
void BUILD_MAXHEAP(int *x,int size)
{
int heap_size=size;
int first_node=heap_size/2;//得到编号最大的父结点
int i=0;
//从编号最大的父结点开始,自底向上地构建最大堆
//这个过程可以循环调用维护函数来实现
for(i=first_node;i>=1;i--)
{
MAXHEAP_IFY(x,i,size);
}
}
3.堆排序
C语言实现
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#define SIZE 10
void MAXHEAP_IFY(int *x,int n,int size)
{
//得到n结点的左右孩子结点
int l=2*n;
int r=2*n+1;
int largest=n;//假设n结点比它的左右孩子都大,为它赋初值
/*
如果左右孩子存在的情况下,找到n的左右孩子和n结点三者之间的最大值
*/
if(l<=size&&x[l-1]>x[n-1])
{
largest=l;
}
if(r<=size&&x[r-1]>x[largest-1])
{
largest=r;
}
/*
如果n,n的左孩子,n的右孩子
三者中的最大值不是n的话
我们需要调整三者的关系
使满足最大堆的性质
*/
if(largest!=n)
{
int temp;
temp=x[n-1];
x[n-1]=x[largest-1];
x[largest-1]=temp;
//如果改变了结构
//那么可能导致下一层的最大堆性质不满足
//需要进一步维护
MAXHEAP_IFY(x,largest,size);
}
}
void BUILD_MAXHEAP(int *x,int size)
{
int heap_size=size;
int first_node=heap_size/2;//得到编号最大的父结点
int i=0;
//从编号最大的父结点开始,自底向上地构建最大堆
//这个过程可以循环调用维护函数来实现
for(i=first_node;i>=1;i--)
{
MAXHEAP_IFY(x,i,size);
}
}
void HEAP_SORT(int *x,int size)
{
int i=0;
int j=0;
//先构建一个最大堆
//那么堆顶的元素就是所有元素中最大的
BUILD_MAXHEAP(x,size);
int temp;
//循环地取出堆顶元素,将该元素放到数组尾部,将尾部的元素放到堆顶
//一方面在每次取出后就针对堆顶元素进行维护堆
//另一方面将取到数组尾的“原堆顶” 排除在维护之外
//这样就会按大小次序将堆顶排列于数组尾
//等循环操作完成,排序也就完成了
for(i=size;i>=2;i--)
{
temp=x[i-1];
x[i-1]=x[0];
x[0]=temp;
//维护堆
MAXHEAP_IFY(x,1,i-1);
}
}
int main()
{
int a[SIZE];
srand((unsigned)time(NULL));
int i=0;
//得到随机数组
for(i=0;i<SIZE;i++)
{
a[i]=rand()%1000;
}
//将随机数组打印出来
for(i=0;i<SIZE;i++)
{
printf("%5d",a[i]);
}
printf("\n");
//对数组进行堆排序
HEAP_SORT(a,SIZE);
//将排序后的新数组打印出来
for(i=0;i<SIZE;i++)
{
printf("%5d",a[i]);
}
printf("\n");
return 0;
}
堆排序的逻辑分析
堆排序的核心关键在:
1.最大堆的堆顶是所有元素中的最大值
2.最大堆维护了一个相对从大到小的一个层级关系
(1)相对大的元素概率上靠前
(2)相对小的元素概率上靠后
3.从概率上讲不断维护堆的时间代价相对低
通过不断地取堆顶最大值和维护堆,是可以达到排序的目的
堆排序的时间复杂度分析
涉及的主方法可以参考文章:
《算法导论》学习(五)---- 分治策略(递归)的时间复杂度求解
(1)维护堆操作
对于维护堆的代码,我们可以有下面的时间表达式:
T
(
n
)
⩽
T
(
2
n
3
)
+
Θ
(
1
)
T(n)\leqslant T(\frac{2n}{3})+\Theta(1)
T(n)⩽T(32n)+Θ(1)
根据主方法,
T
(
n
)
=
O
(
l
g
n
)
=
O
(
h
)
,
其中
h
=
l
g
n
,是堆的高度。注意:这里的
l
g
n
=
l
o
g
2
n
,
不是
l
o
g
10
n
T(n)=O(lgn)=O(h),其中h=lgn,是堆的高度。注意:这里的lgn=log_2{n},不是log_{10}n
T(n)=O(lgn)=O(h),其中h=lgn,是堆的高度。注意:这里的lgn=log2n,不是log10n
(2)建立堆操作
根据代码分析,我们可以得到时间的表达式:
T
(
n
)
=
∑
h
=
0
⌊
l
g
n
⌋
⌈
n
2
h
+
1
⌉
O
(
h
)
=
O
(
n
∑
h
=
0
⌊
l
g
n
⌋
h
2
h
)
又因为:
∑
h
=
0
∞
h
2
h
=
1
/
2
(
1
−
1
/
2
)
2
=
2
于是我们可以化简时间表达式:
T
(
n
)
=
O
(
n
∑
h
=
0
⌊
l
g
n
⌋
h
2
h
)
=
O
(
n
∑
h
=
0
∞
h
2
h
)
=
O
(
n
)
T(n)=\sum^{\lfloor{lgn}\rfloor}_{h=0}\lceil{\frac{n}{2^{h+1}}}\rceil O(h)=O(n\sum_{h=0}^{\lfloor{lgn}\rfloor}\frac{h}{2^h})\\又因为:\sum^{\infty}_{h=0}\frac{h}{2^h}=\frac{1/2}{(1-1/2)^2}=2\\于是我们可以化简时间表达式:T(n)=O(n\sum_{h=0}^{\lfloor{lgn}\rfloor}\frac{h}{2^h})=O(n\sum^{\infty}_{h=0}\frac{h}{2^h})=O(n)
T(n)=h=0∑⌊lgn⌋⌈2h+1n⌉O(h)=O(nh=0∑⌊lgn⌋2hh)又因为:h=0∑∞2hh=(1−1/2)21/2=2于是我们可以化简时间表达式:T(n)=O(nh=0∑⌊lgn⌋2hh)=O(nh=0∑∞2hh)=O(n)
(3)堆排序
由于建立堆执行了一次,维护堆执行了n-1次
很容易我们可以得到
T
(
n
)
=
O
(
n
l
g
n
)
+
O
(
n
)
=
O
(
n
l
g
n
)
T(n)=O(nlgn)+O(n)=O(nlgn)
T(n)=O(nlgn)+O(n)=O(nlgn)
三、优先队列
1.什么是优先队列?
优先队列是一种用来维护由一组元素构成的集合S的数据结构。优先队列的每一个元素都有一个数值,被称为关键字—key(根据场景被赋予意义)。它可以用堆来实现
像堆一样,优先队列也分为最大优先队列和最小优先队列,可以用于持续保障数据的优先级。
下面是用C语言实现的优先队列的数据结构体:(包含了内存和数据大小的一些关系)
//由于存在插入操作,因此需要预留一定的存储空间
//假设存储空间的单元树是数据个数的2倍
#define MSIZE 30
#define DSIZE 15//数据的个数
typedef struct priority_queue
{
int *a;//指向队列的顺序存储空间
int heap_size;//队列中元素的个数
}pq;
2.优先队列的应用
最大优先队列的应用有很多。其中一个就是共享计算机系统的作业调度。最大优先队列记录将要执行的各个作业以及它们之间的相对优先级。当一个作业完成,调度器通过最大优先队列的操作函数,选出具有最高优先级的作业来执行。同时,任何时候,调度器可以通过最大优先队列的操作函数来加入新的作业。
最小优先队列可以被用于基于事件驱动的模拟器,队列中保存着要模拟的事件,每个事件都有一个发生事件作为其关键字。时间必须按照时间发生的次序进行模拟,在模拟过程中,一个事件的发生可能又会触及其它事件的发生模拟。模拟器会通过最小优先队列的操作函数来选择下一个要模拟的事件,以及插入新事件
下面我们仅仅介绍最大优先队列的实现函数
3.最大优先队列的四种操作函数
注意:
要用到之前堆的操作函数
插入元素并入队
void MAX_HEAP_INSERT(pq *x,int key)
{
x->heap_size=x->heap_size+1;
x->a[x->heap_size-1]=-1000;//我们假设-1000对于key值的所有可能来说是负无穷
//相当于增大最后一个元素的key值从负无穷到指定key值
HEAP_increase_key(x,x->heap_size,key);
}
返回优先队列的最值
int HEAP_MAXIMUM(pq *x)
{
//先判断队列是否还有值
if(x->heap_size<1)
{
printf("priority queue is underflow\n");
exit(0);
}
return x->a[0];//堆顶就是最大值所在
}
返回优先队列的最值并将其出队
int HEAP_EXTRACT_MAX(pq *x)
{
//先判断队列是否还有值
if(x->heap_size<1)
{
printf("priority queue is underflow\n");
exit(0);
}
int max=x->a[0];//找到最大值
//用最小的叶子结点填充堆顶的空缺
//之后维护堆
x->a[0]=x->a[x->heap_size-1];
x->heap_size=x->heap_size-1;
MAXHEAP_IFY(x->a,1,x->heap_size);
//返回最大值
return max;
}
增大优先队列元素的key至指定值
void HEAP_increase_key(pq *x,int n,int key)
{
//该函数是将队列中的key值增大到某一个值
//因此若新key值小于原key值,那么报错结束
if(key<x->a[n-1])
{
printf("new key is smaller than current key\n");
exit(0);
}
//修改key值
x->a[n-1]=key;
//准备循环变量
int i=0;
i=n;
int p_i=i/2;//父结点的编号
int temp=0;
//以修改的那个点为起点,向上与父结点进行循环比较
//直到达到最大优先队列的性质
while(i>1&&x->a[p_i-1]<x->a[i-1])
{
temp=x->a[p_i-1];
x->a[p_i-1]=x->a[i-1];
x->a[i-1]=temp;
i=p_i;
p_i=i/2;
}
}
4.最大优先队列的C语言实现
注意:
·
这里的最大优先队列是通过最大堆来实现的,因此最大堆的两个操作函数也是最大优先队列的基础
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
//由于存在插入操作,因此需要预留一定的存储空间
//假设存储空间的单元树是数据个数的2倍
#define MSIZE 30
#define DSIZE 15//数据的个数
typedef struct priority_queue
{
int *a;//指向队列的顺序存储空间
int heap_size;//队列中元素的个数
}pq;
void MAXHEAP_IFY(int *x,int n,int size)
{
//得到n结点的左右孩子结点
int l=2*n;
int r=2*n+1;
int largest=n;//假设n结点比它的左右孩子都大,为它赋初值
/*
如果左右孩子存在的情况下,找到n的左右孩子和n结点三者之间的最大值
*/
if(l<=size&&x[l-1]>x[n-1])
{
largest=l;
}
if(r<=size&&x[r-1]>x[largest-1])
{
largest=r;
}
/*
如果n,n的左孩子,n的右孩子
三者中的最大值不是n的话
我们需要调整三者的关系
使满足最大堆的性质
*/
if(largest!=n)
{
int temp;
temp=x[n-1];
x[n-1]=x[largest-1];
x[largest-1]=temp;
//如果改变了结构
//那么可能导致下一层的最大堆性质不满足
//需要进一步维护
MAXHEAP_IFY(x,largest,size);
}
}
void BUILD_MAXHEAP(int *x,int size)
{
int heap_size=size;
int first_node=heap_size/2;//得到编号最大的父结点
int i=0;
//从编号最大的父结点开始,自底向上地构建最大堆
//这个过程可以循环调用维护函数来实现
for(i=first_node;i>=1;i--)
{
MAXHEAP_IFY(x,i,size);
}
}
void HEAP_SORT(int *x,int size)
{
int i=0;
int j=0;
//先构建一个最大堆
//那么堆顶的元素就是所有元素中最大的
BUILD_MAXHEAP(x,size);
int temp;
//循环地取出堆顶元素,将该元素放到数组尾部,将尾部的元素放到堆顶
//一方面在每次取出后就针对堆顶元素进行维护堆
//另一方面将取到数组尾的“原堆顶” 排除在维护之外
//这样就会按大小次序将堆顶排列于数组尾
//等循环操作完成,排序也就完成了
for(i=size;i>=2;i--)
{
temp=x[i-1];
x[i-1]=x[0];
x[0]=temp;
//维护堆
MAXHEAP_IFY(x,1,i-1);
}
}
int HEAP_MAXIMUM(pq *x)
{
//先判断队列是否还有值
if(x->heap_size<1)
{
printf("priority queue is underflow\n");
exit(0);
}
return x->a[0];//堆顶就是最大值所在
}
int HEAP_EXTRACT_MAX(pq *x)
{
//先判断队列是否还有值
if(x->heap_size<1)
{
printf("priority queue is underflow\n");
exit(0);
}
int max=x->a[0];//找到最大值
//用最小的叶子结点填充堆顶的空缺
//之后维护堆
x->a[0]=x->a[x->heap_size-1];
x->heap_size=x->heap_size-1;
MAXHEAP_IFY(x->a,1,x->heap_size);
//返回最大值
return max;
}
void HEAP_increase_key(pq *x,int n,int key)
{
//该函数是将队列中的key值增大到某一个值
//因此若新key值小于原key值,那么报错结束
if(key<x->a[n-1])
{
printf("new key is smaller than current key\n");
exit(0);
}
//修改key值
x->a[n-1]=key;
//准备循环变量
int i=0;
i=n;
int p_i=i/2;//父结点的编号
int temp=0;
//以修改的那个点为起点,向上与父结点进行循环比较
//直到达到最大优先队列的性质
while(i>1&&x->a[p_i-1]<x->a[i-1])
{
temp=x->a[p_i-1];
x->a[p_i-1]=x->a[i-1];
x->a[i-1]=temp;
i=p_i;
p_i=i/2;
}
}
void MAX_HEAP_INSERT(pq *x,int key)
{
x->heap_size=x->heap_size+1;
x->a[x->heap_size-1]=-1000;//我们假设-1000对于key值的所有可能来说是负无穷
//相当于增大最后一个元素的key值从负无穷到指定key值
HEAP_increase_key(x,x->heap_size,key);
}
int main()
{
pq x;
//申请空间并且初始化
x.a=(int *)malloc(MSIZE*sizeof(int));
x.heap_size=DSIZE;
srand((unsigned)time(NULL));
int i=0;
//得到随机数组
for(i=0;i<x.heap_size;i++)
{
x.a[i]=rand()%1000;
}
//将随机数组打印出来
for(i=0;i<x.heap_size;i++)
{
printf("%5d",x.a[i]);
}
printf("\n");
//根据数组建立最大优先队列(基于最大堆)
BUILD_MAXHEAP(x.a,x.heap_size);
//顺序打印最大优先队列
for(i=0;i<x.heap_size;i++)
{
printf("%5d",x.a[i]);
}
printf("\n");
/*
先调用返回最值函数;
再连续调用两次最值出队函数,看看是否有改变
*/
printf("优先队列的最大key:%5d\n",HEAP_MAXIMUM(&x));
printf("优先队列的最大key:%5d\n",HEAP_EXTRACT_MAX(&x));
printf("优先队列的最大key:%5d\n",HEAP_EXTRACT_MAX(&x));
//调用增大值函数
HEAP_increase_key(&x,x.heap_size,1001);
//顺序打印最大优先队列
for(i=0;i<x.heap_size;i++)
{
printf("%5d",x.a[i]);
}
printf("\n");
//调用插入函数
MAX_HEAP_INSERT(&x,1002);
for(i=0;i<x.heap_size;i++)
{
printf("%5d",x.a[i]);
}
printf("\n");
//释放动态内存
free(x.a);
return 0;
}
总结
本文主要关注堆排序算法,对于优先队列仅仅停留于实现层次,并且仅仅实现了最大堆和最大优先队列
最小堆和最小优先队列读者可以举一反三,自行实现。
对于文章内容的不妥之处,请广大读者谅解并且指正。