堆排序,顾名思义,就是一种基于堆这种数据结构来实现排序的一种算法,那么何谓堆呢?简单点说,堆是一个近似完全二叉树的结构,同时满足即子结点的键值或索引总是小于(或者大于)它的父节点,由此而生,堆分为两种,分别是小顶堆和大顶堆。
- 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列
- 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列
从定义和特征上来看,堆排序的主要步骤就是构建一个堆,然后每次从堆里面取出最大(最小)的数来交换堆最后一个元素,知道堆的尺寸为1,就完成排序了。堆排序的步骤如下: -
1:创建一个堆 H[0……n-1];
2:把堆首(最大值)和堆尾互换;
3:把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置;
4:重复步骤 2,直到堆的尺寸为 1。
这里附上两个有图解的链接
1:堆排序
2:【算法】排序算法之堆排序 - 堆的特点:如果一个结点位置为k,则其父节点位置为k/2,其两个子节点的位置分别为2k和2k+1。因此,从a[k]向上一层,令k=k/2,向下一层令k=2k或2k+1,我们可以基于这个特点来实现一个堆。这样有点抽象,我们再具体一点,我们迭代输入是原数组和k,那么我们比较的就是k、k=2k、2k+1,这三个位置的值,假如这三个位置k处为最大,表示这里已经是一个大顶堆,无需操作,如果2k+1处最大,表示这里不是一个大顶堆,需要进行交换,接下来就要把2k+1对应的值交换到k处,同时2k+1处不一定是大顶堆,需要进行迭代,那么为什么2k不用迭代呢?因为我们会用一个循环来控制迭代,0、1、2…k…len(arr)//2,采用倒序遍历,因此2k处必然已经是大顶堆,2k+1处由于已经发生了值的变动,不能保证仍然是大顶堆,所以需要进行迭代。生成堆的代码如下:
def buildMaxHeap(nums):
"""
根据数组,在原数组基础上将其变成堆结构
>>>nums = [3,38, 5, 44, 15, 36]
>>>buildMaxHeap(nums)
>>>[44, 38, 36, 3, 15, 5]
"""
for i in range(len(nums)//2, -1, -1):
heapify(nums, i)
def heapify(nums, i):
left = 2*i + 1
right = 2*i + 2
largest = i
n = len(nums)
if left < len(nums) and nums[left] > nums[largest]:
largest = left
if right < len(nums) and nums[right] > nums[largest]:
largest = right
if largest != i:
nums[i], nums[largest] = nums[largest], nums[i]
heapify(nums, largest)
nums = [3,38, 5, 44, 15, 36]
buildMaxHeap(nums)
nums
- 注:这里跟上面讲的有点不一样,左右节点分别是left = 2*i + 1, right = 2*i + 2,这是因为数组的索引是从0开始的。
理解的堆之后,堆排序就不成问题了,每次将堆的第一个元素(最大值)和最后一个元素交换,然后维护[0, n-k]长度的堆,其中k为迭代次数。堆排序的所有代码如下:
def buildMaxHeap(nums):
for i in range(len(nums)//2, -1, -1):
heapify(nums, i)
def heapify(nums, i):
left = 2*i + 1
right = 2*i + 2
largest = i
n = len(nums)
if left < currentLen and nums[left] > nums[largest]:
largest = left
if right < currentLen and nums[right] > nums[largest]:
largest = right
if largest != i:
nums[i], nums[largest] = nums[largest], nums[i]
heapify(nums, largest)
def heapSort(nums):
"""
堆排序
>>>nums = [3,38, 5, 44, 15, 36]
>>>heapSort(nums)
>>>[3, 5, 15, 36, 38, 44]
"""
global currentLen
currentLen = len(nums)
buildMaxHeap(nums)
for i in range(len(nums)-1, 0, -1):
nums[0], nums[i] = nums[i], nums[0]
currentLen -= 1
heapify(nums, 0)
return nums
nums = [3,38, 5, 44, 15, 36]
heapSort(nums)
- 这里要注意由于每次迭代需要维护堆的长度发了变化,因此用一个全局变量currentLen来引导heapify(nums, 0)的正确维护。
- 算法解析:堆排序全程在原数组上进行操作,因此空间复杂度为O(1)。时间复杂度要分为两个部分来看,首先buildMaxHeap()方法的时间复杂度很好计算为n/2*log n,也就是O(n log n),再看heapSort方法,从for循环开始,很显然时间复杂度为(n-1)*|log n|(|a|表示一个比a小的数),也就是O(n log n),总的来说该算法的时间复杂度为2*O(n log n),也就还是O(n log n)。
- 稳定性分析:不看堆的生成,我们只看循环体内的交换,他是每次最大值和最末尾一个元素的交换,加入两个相同元素有同一个父节点,那么他们在堆中连号,当遍历到这两个元素时,处在较后面的元素必然会交换到0索引位置,所以必然会发生相同元素相对位置的改变,因此是不稳定排序算法。