今天算法课上,老师比较纤细的讲解了堆排序的相关始末,弥补了数据结构课上没有明白的情况,现总结如下。
引言:其实很多数据结构都是解决一些很现实的问题而被创建出来的。比如从最简单的一组数字开始说明;我们对于一串任意排列的数字,一般的操作有什么呢?无非就是
1.排序(排序之后可以很快的查到相应的数字,但数组插入数据是就涉及到相应的维护有序的工作,于是乎就会涉及很多的移动);
对于这一点,我们可以采取一些策略优化针对插入时庞大的维护量,比如建立链表,抑或有序二叉树等;
2.查找比较特殊的值(比如说最大,最小值);
其实就基于第一点而言,如果提供了有序数组,一般能够比较快的给出最大最小值,这不必多说!
但是,如果仅仅只是为了快速查找最值,那我们可以对数据结构做一番大的优化,第一,我们不再需要完全有序的序列,因为我们只需要提供最值;第二,我们甚至不再需要什么很占空间的链表或者树来存储信息,我们在此引入“堆”的概念,我们仅仅需要一个数组空间,然后根据树的位置顺序进行建堆和修改即可;
这次我们“大根堆”为例(大根堆就是每一个父节点都要比他的儿子节点数值大;另外,儿子节点间的大小顺序任意):
那么,它的Insert和delete等操作都要基于一下的两个基本操作:
sift-up:
bool done=false;
if(i==1)
break;
while(!done || i>=1)
{
if(A[i]>A[i/2])
{
temp=A[i];
A[i]=A[i/2];
A[i/2]=temp;
}
else
done=true;
i=i/2;
}
sift_down:
bool done=false;
if(2*i>n)
break;
while(!done || i<=n)
{
if(A[i]<A[2*i])
{
int temp=A[];
A[i]=A[2*i];
A[2*i]=temp;
}
else
done=true;
i=i*2;
}
”上浮“:完成了将不适合做儿子的节点进行上移,直到合适为止;
”下沉“:完成了将不适合做父亲的节点进行下移,直到合适为止;
那么现在,我们来看插入功能的实现。
根据堆的数据结构特点(完全二叉树和利用数组),我们先将数字加到数组末尾;将它进行sift-up操作;这样,他就可以到达合适的位置,并且保证整个队的性质。
n=n+1;
A[n]=x;
sift-up(A,n);
紧接着,我们来看看删除功能。
首先,去最后一个数值来覆盖现在要删除的数值;接着,对新移动的数值进行sift-up或sift-down操作,具体是情况而定。(那么,如果删除的是最顶端的---也就是最大值,那么显而易见,只需要对移动后的它做sift-down操作即可)
if(i==n)
return;//如果是尾结点,直接删除
x=A[i]; y=A[n];
n=n-1;A[i]=y;
if(A[i]>A[i/2] && i>1)
sift-up(A,i);
else
sift-down(A,i);
最后,关于建堆:
1.如果从零开始一个一个添加,并对每一个进行sift-up操作即可建堆;
2.如果已经给定一个任意的数组,把它调整成“堆”只需要对非叶节点进行sift-down操作,目的就是自下而上的建堆,也就是说先保证每个小堆建成,随后一层层向上将堆建高,最总一定会建成一棵大的二叉树!
for(int i=n/2; i>=1; i--)
sift-down(A,i);
基本的对操作就这么多,那么最后的最后,我们引出期待已久的 堆排序:
基本思想是,每次将最大值和末位交换,并将新交换到首位的进行调整,即经过调整后,A[n] 的前n-1位重新成为一个大顶堆;
for(int i=n; i>=2; i--)
{
temp=A[i];
A[i]=A[1];
A[1]=temp;
sift-down(A,1);
}
这样,我们不需要额外的空间,并且以O(n*logn)的时间复杂度为代价,就可以生成一个生序排列的数组喽!