堆排序
2017-09-28 19:33
246 查看
由于堆也被引申为Java中的垃圾收集存储机制,在本文中使用堆的定义仅为堆数据结构。
完全二叉树的定义为:叶节点只能出现在最下层和次下层,并且最下面一层的结点都集中在该层最左边的若干位置的二叉树。
堆是一个数组,也可以被看作一个近似的完全二叉树。对堆可以分为两类:
大顶堆:每个结点的值都大于或等于其左右孩子的值
小顶堆:每个结点的值都大于或等于其左右孩子的值
左边的即为大顶堆,右边的即为小顶堆,堆也可用作构造一种有效的优先序列。
这里使堆元素从数组A[1]开始填充,即A[0]作为哨兵用于其他用途,即该树的根结点为A[1]。
如果按照层序遍历(即从上到下,从左到右)的方式对结点从1开始编号,则结点满足如下的关系:
堆排序:它的基本思想是,将待排序的序列构成一个大顶堆。此时,整个序列n的最大值即为堆顶的根结点。将根结点输出(其实就是将其与堆数组的最末尾元素进行交换,此时最末尾元素即为最大值,),然后将剩下的n-1个元素重新构成一个大顶堆,然后重复操作,便能得到一个有序的输出序列。
现在,基于上面的解释,我们需要完成两个任务即能解决问题:
如何由一个无序序列构成一个大顶堆?
如何在输出堆顶元素后,调整剩下的元素成为一个新的大顶堆?
4000
这里可以看出父结点和子结点的关系,即父结点为i,左右子树结点为2i,2i+1。
第一次循环,父结点i为4。确保最末端(最下层最右)的父结点为当前子树的最大值
i- -为3,第2次循环由于父结点为最大值,没有变化。
i值递减为2,使最大值到父结点
i值为1,将结点3的值交换到结点1,注意,此时s的值为子结点3的下标。对结点3而言,满足2*3<总结点数,故再次进入循环判断结点3(作为根结点)是否为最大值。
最终形成大顶堆。
第一个任务即建立大顶堆完成后,第二个任务就很简单了,需要的是将顶端元素输出后(使用swap函数交换),再次调整当前A[1…n-1]序列为大顶端即可。
左侧为swap()过程,右侧为对当前A[1..n-1]序列进行HeapAdjust()过程。
最后数组的排序效果如下:
这里,强调一点,由于数组从A[1]开始存储元素,即声明一个长度为10的数组,有效数据元素个数为9。传入HeapSort()函数的长度为数组长度减一。
最后总结
堆排序的运行时间主要消耗在初始化构建堆和重建堆的反复筛选上。由于初始化建堆所需的比较次数较多,堆排序不适合排序序列个数较少的情况。
堆排序最差时间也是O(nlogn)的,这点比快排好(平均时间复杂度为O(n*logn),最坏情况下为O(n*2))。
在构建堆的过程中,因为我们是完全二叉树从最下层最右边的非终端结点开始构建,将它与其孩子进行比较和若有必要的互换,对于每个非终端结点来说,其实最多进行两次比较和互换操作,因此整个构建堆的时间复杂度为O(n)。
在正式排序时,第i次取堆顶记录重建堆需要用O(logi)的时间(完全二叉树的某个结点到根结点的距离为[log2i]+1),并且需要取n-1次堆顶记录,因此,重建堆的时间复杂度为O(nlogn)。
在堆结构中,其优势是最快的找到最大/最小值,重构堆结构的时间复杂度只需要O[logn]。因此常用于优先调度算法中。
因为堆排序是就地排序,空间复杂度为常数:O(1)。
快排在递归进行部分的排序的时候,只会访问局部的数据,因此缓存能够更大概率的命中;而堆排序的建堆过程是整个数组各个位置都访问到的,后面则是所有未排序数据各个位置都可能访问到的,所以不利于缓存发挥作用。简答的说就是快排的存取模型的局部性(locality)更强,堆排序差一些。
堆排序是稳定时间的,快排是非稳定时间的,堆排序的排序时间与数据无关,快排与数据有关。
借鉴书籍:《大话数据结构》
借鉴链接:
https://www.zhihu.com/question/20842649
完全二叉树的定义为:叶节点只能出现在最下层和次下层,并且最下面一层的结点都集中在该层最左边的若干位置的二叉树。
堆是一个数组,也可以被看作一个近似的完全二叉树。对堆可以分为两类:
大顶堆:每个结点的值都大于或等于其左右孩子的值
小顶堆:每个结点的值都大于或等于其左右孩子的值
左边的即为大顶堆,右边的即为小顶堆,堆也可用作构造一种有效的优先序列。
这里使堆元素从数组A[1]开始填充,即A[0]作为哨兵用于其他用途,即该树的根结点为A[1]。
如果按照层序遍历(即从上到下,从左到右)的方式对结点从1开始编号,则结点满足如下的关系:
堆排序:它的基本思想是,将待排序的序列构成一个大顶堆。此时,整个序列n的最大值即为堆顶的根结点。将根结点输出(其实就是将其与堆数组的最末尾元素进行交换,此时最末尾元素即为最大值,),然后将剩下的n-1个元素重新构成一个大顶堆,然后重复操作,便能得到一个有序的输出序列。
现在,基于上面的解释,我们需要完成两个任务即能解决问题:
如何由一个无序序列构成一个大顶堆?
如何在输出堆顶元素后,调整剩下的元素成为一个新的大顶堆?
4000
//第一个for循环完成构建大顶堆的目标,第二个for循环即完成任务2,输出堆顶元素及调整 void HeapSort(int arr[],int n) { int i; for(i = n/2;i > 0;--i)//由完全二叉树可知,从1开始排序的n个结点的二叉树,它的最小(最下层最右)的非叶子结点为n/2 //i值的递减操作表示,非叶子结点从下到上执行HeapAdjust函数,确保每一子树的根结点都是该子树中的最大值 HeapAdjust(arr,i,n);//把无序序列构建为一个大顶堆 for(i = n;i > 1;--i) { swap(arr,1,i);//将堆顶元素输出 HeapAdjust(arr,1,i-1);//将A[1...i-1]重新调整为大顶堆 } }
这里可以看出父结点和子结点的关系,即父结点为i,左右子树结点为2i,2i+1。
//对于数组中A[1.....n]都满足堆的定义(A[0]为哨兵位) //将序列调整为一个大顶堆 void HeapAdjust(int arr[],int s,int m) { int temp,j; temp = arr[s]; //存储父结点s的值,同时也可用于与子结点比较大小 for(j = 2 * s;j <= m;j *= 2)//由二叉树的性质可知,左子结点为父结点下标的2倍,j *= 2即按照两倍的关系寻找一系列子结点 { if(j < m && arr[j] < arr[j+1])//j不为最后一个结点(即存在右兄弟) ++j;//确定左右子树中的较大值下标 if(temp > arr[j])//子结点并没有大于父结点的值,直接跳出循环 break; arr[s] = arr[j];//执行到这里,表示存在比父结点大的值,执行交换操作 s = j; //将子结点下标保留 } arr[s] = temp;//插入,将父结点临时存储在temp中的值赋给子结点下标,完成交换值操作 //下次循环,根结点将作为子结点时参与HeapAdjust函数,函数将当前子树中的最大值交换到父结点。 }
第一次循环,父结点i为4。确保最末端(最下层最右)的父结点为当前子树的最大值
i- -为3,第2次循环由于父结点为最大值,没有变化。
i值递减为2,使最大值到父结点
i值为1,将结点3的值交换到结点1,注意,此时s的值为子结点3的下标。对结点3而言,满足2*3<总结点数,故再次进入循环判断结点3(作为根结点)是否为最大值。
最终形成大顶堆。
void swap(int arr[],int i,int j)//输出顶端元素,即当前数组最大值按序放在数组末尾。 { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; }
第一个任务即建立大顶堆完成后,第二个任务就很简单了,需要的是将顶端元素输出后(使用swap函数交换),再次调整当前A[1…n-1]序列为大顶端即可。
左侧为swap()过程,右侧为对当前A[1..n-1]序列进行HeapAdjust()过程。
最后数组的排序效果如下:
这里,强调一点,由于数组从A[1]开始存储元素,即声明一个长度为10的数组,有效数据元素个数为9。传入HeapSort()函数的长度为数组长度减一。
最后总结
堆排序的运行时间主要消耗在初始化构建堆和重建堆的反复筛选上。由于初始化建堆所需的比较次数较多,堆排序不适合排序序列个数较少的情况。
堆排序最差时间也是O(nlogn)的,这点比快排好(平均时间复杂度为O(n*logn),最坏情况下为O(n*2))。
在构建堆的过程中,因为我们是完全二叉树从最下层最右边的非终端结点开始构建,将它与其孩子进行比较和若有必要的互换,对于每个非终端结点来说,其实最多进行两次比较和互换操作,因此整个构建堆的时间复杂度为O(n)。
在正式排序时,第i次取堆顶记录重建堆需要用O(logi)的时间(完全二叉树的某个结点到根结点的距离为[log2i]+1),并且需要取n-1次堆顶记录,因此,重建堆的时间复杂度为O(nlogn)。
在堆结构中,其优势是最快的找到最大/最小值,重构堆结构的时间复杂度只需要O[logn]。因此常用于优先调度算法中。
因为堆排序是就地排序,空间复杂度为常数:O(1)。
快排在递归进行部分的排序的时候,只会访问局部的数据,因此缓存能够更大概率的命中;而堆排序的建堆过程是整个数组各个位置都访问到的,后面则是所有未排序数据各个位置都可能访问到的,所以不利于缓存发挥作用。简答的说就是快排的存取模型的局部性(locality)更强,堆排序差一些。
堆排序是稳定时间的,快排是非稳定时间的,堆排序的排序时间与数据无关,快排与数据有关。
借鉴书籍:《大话数据结构》
借鉴链接:
https://www.zhihu.com/question/20842649
相关文章推荐
- 排序算法之堆排序
- 堆排序,C++模板编程
- 堆排序
- 堆排序
- 白话经典算法系列之七 堆与堆排序
- 最大的K个数—堆排序
- 改进排序:希尔排序、堆排序
- 堆与堆排序
- 《算法导论》读书笔记之第6章 堆排序
- 算法--堆排序
- Python 面试题 - 堆排序 & 演算过程
- 排序算法-堆排序
- 为什么从5000个数中找出10个最大的堆排序最快?
- 算法设计与分析基础-6.4、堆和堆排序
- 哈夫曼树结合堆排序 POJ(3253)
- 算法学习笔记----第二部分:排序和顺序统计量----第6章、堆排序
- 算法导论之插入排序,选择排序,归并排序,冒泡排序,希尔排序,堆排序,快速排序的c语言实现
- 堆排序 一个综合了插入排序和二路归并特点的排序算法(未测试)
- Heap-堆排序
- [字符串hash][堆排序][AC自动机][usaco3.1.5]Contact