本系列文章为浙江大学陈越、何钦铭数据结构学习笔记,前面的系列文章链接如下:
数据结构基础:P1-基本概念
数据结构基础:P2.1-线性结构—>线性表
数据结构基础:P2.2-线性结构—>堆栈
数据结构基础:P2.3-线性结构—>队列
数据结构基础:P2.4-线性结构—>应用实例:多项式加法运算
数据结构基础:P2.5-线性结构—>应用实例:多项式乘法与加法运算-C实现
数据结构基础:P3.1-树(一)—>树与树的表示
数据结构基础:P3.2-树(一)—>二叉树及存储结构
数据结构基础:P3.3-树(一)—>二叉树的遍历
数据结构基础:P3.4-树(一)—>小白专场:树的同构-C语言实现
数据结构基础:P4.1-树(二)—>二叉搜索树
数据结构基础:P4.2-树(二)—>二叉平衡树
数据结构基础:P4.3-树(二)—>小白专场:是否同一棵二叉搜索树-C实现
数据结构基础:P4.4-树(二)—>线性结构之习题选讲:逆转链表
数据结构基础:P5.1-树(三)—>堆
数据结构基础:P5.2-树(三)—>哈夫曼树与哈夫曼编码
数据结构基础:P5.3-树(三)—>集合及运算
数据结构基础:P5.4-树(三)—>入门专场:堆中的路径
数据结构基础:P5.5-树(三)—>入门专场:File Transfer
数据结构基础:P6.1-图(一)—>什么是图
数据结构基础:P6.2-图(一)—>图的遍历
数据结构基础:P6.3-图(一)—>应用实例:拯救007
数据结构基础:P6.4-图(一)—>应用实例:六度空间
数据结构基础:P6.5-图(一)—>小白专场:如何建立图-C语言实现
数据结构基础:P7.1-图(二)—>树之习题选讲:Tree Traversals Again
数据结构基础:P7.2-图(二)—>树之习题选讲:Complete Binary Search Tree
数据结构基础:P7.3-图(二)—>树之习题选讲:Huffman Codes
数据结构基础:P7.4-图(二)—>最短路径问题
数据结构基础:P7.5-图(二)—>哈利·波特的考试
数据结构基础:P8.1-图(三)—>最小生成树问题
数据结构基础:P8.2-图(三)—>拓扑排序
数据结构基础:P8.3-图(三)—>图之习题选讲-旅游规划
数据结构基础:P9.1-排序(一)—>简单排序(冒泡、插入)
数据结构基础:P9.2-排序(一)—>希尔排序
要理解堆排序,我们得先从选择排序开始。
每次我们从
A[i]到A[N-1]
这一段(未排序的部分
)里面去找一个最小元,把最小元的位置赋给一个变量MinPosition
。然后我们把找到的这个最小元与有序部分(A[0]到A[i-1])
后面一位上的元素(A[i])
互换。
实现代码如下:
void Selection_Sort ( ElementType A[], int N )
{
for ( i = 0; i < N; i ++ ) {
MinPosition = ScanForMin( A, i, N–1 );
/* 从A[i]到A[N–1]中找最小元,并将其位置赋给MinPosition */
Swap( A[i], A[MinPosition] );
/* 将未排序部分的最小元换到有序部分的最后位置 */
}
}
从上述代码中可以看到:
①当我们在交换这两个元素的时候,在大多数情况下他们不是挨着的,是可能跳了很远的距离做了一个交换。那么这一次交换就是一个好消息,可能一次交换就消掉了很多的逆序对。最坏情况下每次找到一个最小元都必须换一下,我们循环了多少N-1次
②瓶颈在ScanForMin这个函数。我要从A[i]到A[N-1]里面找最小元,选择排序是一个简单粗暴的方法,就是从A[i]一直扫描到A[N-1],然后把最小元挑出来,这样的话ScanForMin也是一个for循环。
③整个算法相当于外头套了一层for循环,里面也是一层for循环。很显然,无论如何选择排序的时间复杂度都是 Θ ( N 2 ) Θ(N^2) Θ(N2)这个数量级的,无所谓最好情况还是最坏情况的。
于是我们就想到:想要得到一个更快的算法,关键就是怎么把找到最小元这一步变快。看见最小元这个词你应该马上想到一个很有用的工具:最小堆。最小堆的特点就是:它的根结点就一定存的是那个最小元,所以就有了我们后面的堆排序。
我们先看一个比较傻的堆排序算法:
void Heap_Sort ( ElementType A[], int N )
{
BuildHeap(A); /* O(N) */
for ( i=0; i<N; i++ )
TmpA[i] = DeleteMin(A); /* O(logN) */
for ( i=0; i<N; i++ ) /* O(N) */
A[i] = TmpA[i];
}
算法的问题:
①需要额外开一个 O ( N ) O(N) O(N) 数量级的空间
②复制元素也是比较耗时的
算法的整体复杂度: O ( N l o g N ) O(NlogN) O(NlogN)
其实我们本来是没有必要做单独开辟一片空间,并在最后做元素复制。怎么能把这一步跳过去呢?我们先来看个例子。
假设我们有一个四个元素(a b c d)的数组,大小关系为a
我先将其调整成一个最大堆
我们知道而在一个排好序的数组里面,最大元素应该放在最后一个位置。所以我们把根结点跟最后一个结点做一个交换,把b放上去,把d放下来。
放下来以后,我们就把这个整个堆的规模-1,把这个d就排除在外。因为后面再做什么,这个d都不用动了,他已经放在他最终的正确的位置上了。
剩下这个堆还有三个元素,然后我要继续把这个堆调整成一个最大堆
然后重复前面的步骤,把c换到最后的位置上去
然后把c排除掉
剩下这个堆还有2个元素,我要继续把这个堆调整成一个最大堆
然后重复前面的步骤,把根结点b换到最后的位置上去
最后就完成了排序
在这里有个注意的地方:
我们说堆的元素是从第一个下标为1的元素开始计数的,A[0]是不放任何真的元素的,里面放的是一个哨兵。但是在我们的排序的算法里头,用户是从第0个元素就开始存的。所以堆排序里面的这个堆的元素是从0开始记的,那这也就导致了任何一个结点跟它的孩子结点的下标的关系就不一样了。
根结点
下标为i
,左孩子
下标为2i+1
,右孩子下标为2i+2
算法对应伪代码如下:
void Heap_Sort ( ElementType A[], int N )
{
for ( i=N/2-1; i>=0; i-- )/* BuildHeap */
PercDown( A, i, N );
for ( i=N-1; i>0; i-- ) {
Swap( &A[0], &A[i] ); /* DeleteMax */
PercDown( A, 0, i );
}
}
算法复杂度:
堆排序处理N个不同元素的随机排列的平均比较次数是 2 N l o g N − O ( N l o g l o g N ) \rm{2NlogN-O(NloglogN)} 2NlogN−O(NloglogN) 。
总体来说复杂度还是 O ( N l o g N ) \rm{O(NlogN)} O(NlogN)
虽然堆排序给出最佳平均时间复杂度,但实际效果不如用Sedgewick增量序列的希尔排。
void Swap( ElementType *a, ElementType *b )
{
ElementType t = *a; *a = *b; *b = t;
}
void PercDown( ElementType A[], int p, int N )
{ /* 改编代码4.24的PercDown( MaxHeap H, int p ) */
/* 将N个元素的数组中以A[p]为根的子堆调整为最大堆 */
int Parent, Child;
ElementType X;
X = A[p]; /* 取出根结点存放的值 */
for( Parent=p; (Parent*2+1)<N; Parent=Child ) {
Child = Parent * 2 + 1;
if( (Child!=N-1) && (A[Child]<A[Child+1]) )
Child++; /* Child指向左右子结点的较大者 */
if( X >= A[Child] ) break; /* 找到了合适位置 */
else /* 下滤X */
A[Parent] = A[Child];
}
A[Parent] = X;
}
void HeapSort( ElementType A[], int N )
{ /* 堆排序 */
int i;
for ( i=N/2-1; i>=0; i-- )/* 建立最大堆 */
PercDown( A, i, N );
for ( i=N-1; i>0; i-- ) {
/* 删除最大堆顶 */
Swap( &A[0], &A[i] ); /* 见代码7.1 */
PercDown( A, 0, i );
}
}
1、对N个记录进行堆排序,最坏的情况下时间复杂度是
A. O ( N ) O(N) O(N)
B. O ( 2 N ) O(2N) O(2N)
C. O ( N l o g N ) O(NlogN) O(NlogN)
D. O ( N 2 ) O(N^2) O(N2)
答案:C
2、有一组记录(46,77,55,38,41,85),用堆排序建立的初始堆为
A. 38,77,55,46,41,85
B. 38,41,46,77,55,85
C. 85,55,77,38,41,46
D. 85,77,55,38,41,46
答案:D
3、堆排序是稳定的 (错误)