【基础知识】之 堆排序(heap sorting)
Sorting
比较好的排序算法可以把时间复杂度控制在O(n*log2n),最糟的情况是O(n2).
应该将sort当作一项基础处理,因为对于很多问题,当其数据变成sorted items时,解决起来会很容易。
Stable Sorting
对于key相同的元素,有时我们需要保持它们原先的顺序。但不幸的是,很少有快速算法是stable的。不过,我们可以将元素的位置作为第二参考量,当元素a.key = b.key时,原先位置小的仍然排在前面。
选择排序(selection sort)
选择排序就是将一个item list分为两部分,一部分是sorted,一部分是unsorted。每次都从unsorted的部分中取出最小的值,与索引位置i所在的元素进行交换,交换后,索引i加1;一直这样下去,知道整个list完成排序。
交换一个元素所需的时间为O(1),但是在unsorted部分中寻找最小值则需要耗费O(n)的时间,这样所有的元素排序一趟下来需要的时间为O(n2)。在这个过程中,如果能优化unsorted的部分,让在其中寻找最小值的时间复杂度变小就可以优化整个选择排序算法,例如,如果把unsorted的部分改为balance tree结构的话,那么在其中寻找最小值的时间复杂度将下降至O(log2n),那么总的时间复杂度将下降至O(n*log2n)。
堆排序就是改进了数据结构的选择排序算法。
堆(heap)
堆是一种特别的结构,它的有序性比sorted order差,但是又比random order强。
例如最小堆(即某元素的父节点总比自己小,最大堆则正好相反)
它存储到数组中为:
堆的特点是它是一个完全二叉树,因此,它可以利用数组来方便地存储其树状结构(而不需要像二叉搜索树一样还需要额外的指针来维护树结构)。
对于完全二叉树,有如下特点:
①第l层的节点位于数组的2l-1至2l-1的位置
②对于节点k,其左孩子位于2k,右孩子位于2k+1,其父节点位于k/2
但它也有一些缺点,比如无法快速搜索到一个某个节点,只能用广搜一圈圈地搜,平均时间复杂度为O(n/2)
堆的构建
堆的构建采用冒泡的方式(ps:不是冒泡算法的那个冒泡)
对于一个新的元素k,将其放在数组的最后,然后将它与自己的父节点进行比较,若它比自己的父节点小(假设构建的是最小堆),那么将它与自己的父节点交换;一直这样往上冒泡,直到有一个父节点比自己小或者冒到根节点为止。
PS:父节点的位置为k/2.
堆节点的摘除
在使用堆来做选择排序时(也就是堆排序),会不停的摘掉堆的顶。
将堆的顶点摘除后,会留下一个洞,这时可以把数组的第n个(即最后一个元素)放置到顶部暂时充当根节点,然后将该节点和它的左孩子(2k)、右孩子(2k+1)进行比较,将三者中小的那个换到父节点的位置上;一直这样下去直到堆重新稳定(即父节点是比左孩子、右孩子都小)。
ps:顶点摘除后,将最后一个元素暂移至root处,然后开始往下冒泡。
快速建立堆的小窍门
假设有n个元素,那么第n个元素的父节点为n/2,也就是说从n/2以后的元素均为叶子节点。所以,在构建堆的时候,我们可以先填满后n/2个元素。
例如:1945 1865 1963 1776 1492 1783 1918 2001 1941 1804,我们将5个元素从下往上填
现在开始从下往上填第六个元素
现在开始往下冒泡,k=5,故2k=10,2k+1=null,所以,对比 array[5]=1783 和 array[10]=1945,显然1783 < 1945,故不需要进行交换。即在父节点、左孩子、右孩子中,父节点(1783)胜出。
接着,我们再从下往上填第七个元素
然后再进行父节点、左孩子、右孩子的递归比较
显然9号元素1865胜出,因此将4号元素与9号元素进行交换,变成
交换后,k变成9,由于2k=18不存在,因此递归终止。
接着,我们再从下往上填写第八个元素,即 2001
然后进行父节点、左孩子、右孩子的递归比较
6号元素1492胜出,因此将3号元素与6号元素换位
现在,k变成6,由于2k=12不存在,因此此次递归终止。
接下来再一次从下往上填写第九个元素1941,递归比较换位后,再填写第十个元素1804,再进行递归比较换位。最终的结果为:
整理,并画成树状结构为:
堆排序
在建立完堆结构以后,进行堆排序的过程就是摘顶,调整堆结构,再摘顶,再调整堆结构,直到堆被摘完为止,这样就可以得到一个有序序列。