❶ 我看书上堆排序,每次都要重新建堆,然后再调外根节点和最后一个结点,感觉这样好麻烦你们是怎么做的呢
堆排序很重要的一个步骤是初始建堆,它保证了树中的每个子树的根结点都比其下的子结点大。
建堆后的过程基本上就是选择出最大值,然后将被交换到根结点位置的结点进行下沉的过程。而这些过程虽然对树的局部结构进行了调整,但严格来说,不算是重新建堆。
《算法导论》上对堆排序讲得很详细。它把堆排序算法分成了三个子算法:
一个将结点下沉的递归算法;一个初始建堆算法和一个排序算法。
具体过程是:
1、初始建堆算法调用结点下沉递归算法完成建堆;
2、排序算法在建好的堆上得到根结点(即最大值),然后调用结点下沉的递归算法将交换到根结点位置的结点进行下沉。反复第2步,整个算法就完成了。
思路很清晰,可以去看看。
❷ 用一组{14,15,30,28,5,10}关键字序列,写出初始建堆过程图示,再根据初始堆写出堆排序过程图示。
起始序列为14,15,30,28,5,10,
(1)因此起始堆的情况如下:
14
15 30
28 5 10
(2)假设是打算得到一个从小到大的c,所以需要建大顶堆,起始状态从下向上建堆:
第一步: 第二步:
14 30
28 30 28 14
25 5 10 25 5 10
(3)此时已经建立完了初始的堆。此时堆顶元素30即为最大元素,将堆顶元素与堆最后
一个元素进行交换,此时30是最大元素位于队尾,因此无需继续排序。所以堆如下图
所示:10 28 14 25 5
(4)此时由于除被交换到堆顶的10以外其他的都基本有序,所以自上而下建堆得到的堆
如下:
28
25 14
10 5
(5)重复(3)和(4)步骤确定了28的位置并得到堆如下:
25
10 14
5
(6)重复(3)和(4)步骤确定了25的位置并得到堆如下:
14
10 5
(7)重复(3)和(4)步骤确定了14的位置并得到堆如下:
10
5
(8)重复(3)和(4)步骤确定了10的位置,此时只有一个数5也位于了堆的第一个位置,
因此排序完成。
(2)建堆算法扩展阅读:
建堆效率
n个结点的堆,高度d =log2n。根为第0层,则第i层结点个数为2^i,考虑一个元素在堆中向下移动的距离。大约一半的结点深度为d-1,不移动(叶)。四分之一的结点深度为d-2,而它们至多能向下移动一层。树中每向上一层,结点的数目为前一层的一半,而子树高度加一。
这种算法时间代价为Ο(n)
由于堆有log n层深,插入结点、删除普通元素和删除最小元素的平均时间代价和时间复杂度都是
Ο(log n)。
操作实现
在程序中,堆用于动态分配和释放程序所使用的对象。在以下情况中调用堆操作:
1.事先不知道程序所需对象的数量和大小。
2.对象太大,不适合使用堆栈分配器。
堆使用运行期间分配给代码和堆栈以外的部分内存。
传统上,操作系统和运行时库随附了堆实现。当进程开始时,操作系统创建称为进程堆的默认堆。如果没有使用其他堆,则使用进程堆分配块。
语言运行时库也可在一个进程内创建单独的堆。(例如,C 运行时库创建自己的堆。)除这些专用堆外,应用程序或许多加载的动态链接库 (DLL) 之一也可以创建并使用单独的堆。Win32 提供了一组丰富的API用于创建和使用专用堆。有关堆函数的优秀教程,请参阅 MSDN 平台 SDK 节点。
❸ 堆排序中建堆过程时间复杂度O(n)怎么来的
建堆算法是从最后一个非叶子结点开始下溯(即 Max-Heapify操作),也可以把建堆过程想成先对左子树建堆(T(n/2)),再对右子树建堆(T(n/2)),最后对根下溯(O(lg n))
❹ 在堆排序的过程中为什么要从n/2到1的顺序进行建堆过程而不是反过来
【概念】堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。可以利用数组的特点快速定位指定索引的元素。堆分为大根堆和小根堆,是完全二叉树。大根堆的要求是每个节点的值都不大于其父节点的值,即A[PARENT[i]] >= A[i]。在数组的非降序排序中,需要使用的就是大根堆,因为根据大根堆的要求可知,最大的值一定在堆顶。
【起源】
1991年的计算机先驱奖获得者、斯坦福大学计算机科学系教授罗伯特·弗洛伊德(Robert W.Floyd)和威廉姆斯(J.Williams)在1964年共同发明了着名的堆排序算法( Heap Sort )。
【简介】
堆排序利用了大根堆(或小根堆)堆顶记录的关键字最大(或最小)这一特征,使得在当前无序区中选取最大(或最小)关键字的记录变得简单。
(1)用大根堆排序的基本思想
① 先将初始文件R[1..n]建成一个大根堆,此堆为初始的无序区
② 再将关键字最大的记录R[1](即堆顶)和无序区的最后一个记录R[n]交换,由此得到新的无序区R[1..n-1]和有序区R[n],且满足R[1..n-1].keys≤R[n].key
③由于交换后新的根R[1]可能违反堆性质,故应将当前无序区R[1..n-1]调整为堆。然后再次将R[1..n-1]中关键字最大的记录R[1]和该区间的最后一个记录R[n-1]交换,由此得到新的无序区R[1..n-2]和有序区R[n-1..n],且仍满足关系R[1..n-2].keys≤R[n-1..n].keys,同样要将R[1..n-2]调整为堆。
……
直到无序区只有一个元素为止。
(2)大根堆排序算法的基本操作:
①建堆,建堆是不断调整堆的过程,从len/2处开始调整,一直到第一个节点,此处len是堆中元素的个数。建堆的过程是线性的过程,从len/2到0处一直调用调整堆的过程,相当于o(h1)+o(h2)…+o(hlen/2) 其中h表示节点的深度,len/2表示节点的个数,这是一个求和的过程,结果是线性的O(n)。
②调整堆:调整堆在构建堆的过程中会用到,而且在堆排序过程中也会用到。利用的思想是比较节点i和它的孩子节点left(i),right(i),选出三者最大(或者最小)者,如果最大(小)值不是节点i而是它的一个孩子节点,那边交互节点i和该节点,然后再调用调整堆过程,这是一个递归的过程。调整堆的过程时间复杂度与堆的深度有关系,是lgn的操作,因为是沿着深度方向进行调整的。
③堆排序:堆排序是利用上面的两个过程来进行的。首先是根据元素构建堆。然后将堆的根节点取出(一般是与最后一个节点进行交换),将前面len-1个节点继续进行堆调整的过程,然后再将根节点取出,这样一直到所有节点都取出。堆排序过程的时间复杂度是O(nlgn)。因为建堆的时间复杂度是O(n)(调用一次);调整堆的时间复杂度是lgn,调用了n-1次,所以堆排序的时间复杂度是O(nlgn)[2]
注意:
①只需做n-1趟排序,选出较大的n-1个关键字即可以使得文件递增有序。
②用小根堆排序与利用大根堆类似,只不过其排序结果是递减有序的。堆排序和直接选择排序相反:在任何时刻堆排序中无序区总是在有序区之前,且有序区是在原向量的尾部由后往前逐步扩大至整个向量为止
【特点】
堆排序(HeapSort)是一树形选择排序。堆排序的特点是:在排序过程中,将R[l..n]看成是一棵完全二叉树的顺序存储结构,利用完全二叉树中双亲结点和孩子结点之间的内在关系(参见二叉树的顺序存储结构),在当前无序区中选择关键字最大(或最小)的记录
【算法分析】
堆排序的时间,主要由建立初始堆和反复重建堆这两部分的时间开销构成,它们均是通过调用Heapify实现的。
平均性能:O(N*logN)。
其他性能:由于建初始堆所需的比较次数较多,所以堆排序不适宜于记录数较少的文件。堆排序是就地排序,辅助空间为O(1)。它是不稳定的排序方法。(排序的稳定性是指如果在排序的序列中,存在前后相同的两个元素的话,排序前 和排序后他们的相对位置不发生变化)。
❺ 堆排序是怎么建堆的 关键字序列 42 13 24 91 23 16 05 88是怎样建堆的
首先把所有数据填进一个完全二叉树中。然后对非终端结点n/2向下进行调整。建小根堆的时候方法是:1.元素下调。比较它与两个孩子的大小。哪个孩子比它小也比兄弟小则把它调到那个孩子的位置。然后再判断该位置还要不要往下调。2.从n/2开始,对它之前的所有元素进行1操作。
本题解法为(按完全二叉树写)
一。把所有元素写进完全二叉树中得
42
13 24
91 23 16 05
88
二。1.对非叶子元素进行调整,即第n/2个元素,即本题的91.
因为91的孩子为88.比91小。所以调到88的位置。即91和88换
42
13 24
88 23 16 05
91
2.对n/2前一个元素进行调整。即本题的24.因为16和05都比24小,而05比16小,所以24和05调
42
13 05
88 23 16 24
91
3.对步骤2之前的一个元素,即本题的13进行调整,因为88和23都比13大,所以不用调。
4.对步骤3之前的一个元素,即本题的42进行调整。因为13和05都比42小,二05比13小。所以05和42调换位置。而调换位置后42的儿子为16和24,16比24小。所以42和16换位置。(此时已经对第一个元素进行了调整,就可以结束了,如果没错的话就是最终结果)
05
13 16
88 23 42 24
91
建的是小根堆,如果要建大根堆的话,也是往下调,但比较的是下面的哪个大。其他同理
❻ 数据结构与算法--堆和堆排序
堆排序是一种原地的、时间复杂度为 O(nlogn) 的排序算法。
堆是一种特殊的树。
只要满足这两点,它就是一个堆:
对于每个节点的值都大于等于子树中每个节点值的堆,我们叫做 “大顶堆” 。对于每个节点的值都小于等于子树中每个节点值的堆,我们叫做 “小顶堆” 。
完全二叉树比较适合用数组来存储。用数组来存储完全二叉树是非常节省存储空间的。下标可以直接计算出左右字数的下标。(数组中下标为 i 的节点,左子节点下标为 i∗2 ,右子节点下标为 i∗2+1,父节点的下标为 i/2 。)
如果我们把新插入的元素放到堆的最后,你可以看我画的这个图,是不是不符合堆的特性了?于是,我们就需要进行调整,让其重新满足堆的特性,这个过程我们起了一个名字,就叫做 堆化(heapify) 。
堆化实际上有两种,从下往上和从上往下。这里我先讲从下往上的堆化方法。
堆化非常简单,就是顺着节点所在的路径,向上或者向下,对比,然后交换。
我们把最后一个节点放到堆顶,然后利用同样的父子节点对比方法。对于不满足父子节点大小关系的,互换两个节点,并且重复进行这个过程,直到父子节点之间满足大小关系为止。这就是 从上往下的堆化方法 。
一个包含 n 个节点的完全二叉树,树的高度不会超过 log2n。堆化的过程是顺着节点所在路径比较交换的,所以堆化的时间复杂度跟树的高度成正比,也就是 O(logn)。插入数据和删除堆顶元素的主要逻辑就是堆化,所以,往堆中插入一个元素和删除堆顶元素的时间复杂度都是 O(logn)。
这里我们借助于堆这种数据结构实现的排序算法,就叫做堆排序。这种排序方法的时间复杂度非常稳定,是 O(nlogn),并且它还是原地排序算法。
从后往前处理数组,并且每个数据都是从上往下堆化。
因为叶子节点往下堆化只能自己跟自己比较,所以我们直接从最后一个非叶子节点开始,依次堆化就行了。
建堆的时间复杂度就是 O(n)。 推导过程见 极客时间--数据结构与算法之美
建堆结束之后,数组中的数据已经是按照大顶堆的特性来组织的。数组中的第一个元素就是堆顶,也就是最大的元素。我们把它跟最后一个元素交换,那最大元素就放到了下标为 n 的位置。
这个过程有点类似上面讲的“删除堆顶元素”的操作,当堆顶元素移除之后,我们把下标为 n 的元素放到堆顶,然后再通过堆化的方法,将剩下的 n−1 个元素重新构建成堆。堆化完成之后,我们再取堆顶的元素,放到下标是 n−1 的位置,一直重复这个过程,直到最后堆中只剩下标为 1 的一个元素,排序工作就完成了。
整个堆排序的过程,都只需要极个别临时存储空间,所以堆排序是原地排序算法。堆排序包括建堆和排序两个操作,建堆过程的时间复杂度是 O(n),排序过程的时间复杂度是 O(nlogn),所以,堆排序整体的时间复杂度是 O(nlogn)。
堆排序不是稳定的排序算法,因为在排序的过程,存在将堆的最后一个节点跟堆顶节点互换的操作,所以就有可能改变值相同数据的原始相对顺序。
堆这种数据结构几个非常重要的应用:优先级队列、求 Top K 和求中位数。
假设我们有 100 个小文件,每个文件的大小是 100MB,每个文件中存储的都是有序的字符串。我们希望将这些 100 个小文件合并成一个有序的大文件。这里就会用到优先级队列。
这里就可以用到优先级队列,也可以说是堆。我们将从小文件中取出来的字符串放入到小顶堆中,那堆顶的元素,也就是优先级队列队首的元素,就是最小的字符串。我们将这个字符串放入到大文件中,并将其从堆中删除。然后再从小文件中取出下一个字符串,放入到堆中。循环这个过程,就可以将 100 个小文件中的数据依次放入到大文件中。
我们可以用优先级队列来解决。我们按照任务设定的执行时间,将这些任务存储在优先级队列中,队列首部(也就是小顶堆的堆顶)存储的是最先执行的任务。
如何在一个包含 n 个数据的数组中,查找前 K 大数据呢?我们可以维护一个大小为 K 的小顶堆,顺序遍历数组,从数组中取出数据与堆顶元素比较。如果比堆顶元素大,我们就把堆顶元素删除,并且将这个元素插入到堆中;如果比堆顶元素小,则不做处理,继续遍历数组。这样等数组中的数据都遍历完之后,堆中的数据就是前 K 大数据了。
中位数,顾名思义,就是处在中间位置的那个数。
使用两个堆:一个大顶堆, 一个小顶堆。 小顶堆中的数据都大于大顶堆中的数据。
如果新加入的数据小于等于大顶堆的堆顶元素,我们就将这个新数据插入到大顶堆;否则,我们就将这个新数据插入到小顶堆。
也就是说,如果有 n 个数据,n 是偶数,我们从小到大排序,那前 2n 个数据存储在大顶堆中,后 2n 个数据存储在小顶堆中。这样,大顶堆中的堆顶元素就是我们要找的中位数。如果 n 是奇数,情况是类似的,大顶堆就存储 2n+1 个数据,小顶堆中就存储 2n 个数据。
极客时间--数据结构与算法之美--28 | 堆和堆排序:为什么说堆排序没有快速排序快?