其基本思想为(大顶堆)
- 将初始待排序关键字序列(R1,R2....Rn)构建成大顶堆,此堆为初始的无序区
- 将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,......Rn-1)和新的有序区(Rn)
- 由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,......Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2....Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成
下图来张教材的图,是整个堆排序的过程: 整个过程的核心就是先初始化大顶堆,将最大数(堆顶)的放到堆的最后一个, 堆长度-1, 继续调整成大顶堆,直至有序序列为len(array_list)-1.
堆排序前42是在42后面,排序后42在42前面,因此堆排序是不稳定的。
下面举例说明:
给定一个列表array=[16,7,3,20,17,8],对其进行堆排序。
首先根据该数组元素构建一个完全二叉树,得到
然后需要构造初始堆,则从最后一个非叶节点开始调整,调整过程如下:
第一步: 初始化大顶堆(从最后一个有子节点开始往上调整最大堆)
20和16交换后导致16不满足堆的性质,因此需重新调整
这样就得到了初始堆。
第二步: 堆顶元素R[1]与最后一个元素R[n]交换,交换后堆长度减一
即每次调整都是从父节点、左孩子节点、右孩子节点三者中选择最大者跟父节点进行交换(交换之后可能造成被交换的孩子节点不满足堆的性质,因此每次交换之后要重新对被交换的孩子节点进行调整)。有了初始堆之后就可以进行排序了。
第三步: 重新调整堆。此时3位于堆顶不满堆的性质,则需调整继续调整(从顶点开始往下调整)
重复上面的步骤:
注意了,现在你应该了解堆排序的思想了,给你一串列表,你也能写出&说出堆排序的过程。
在写算法的过程中,刚开始我是很懵比。后来终于看懂了。请特别特别注意: 初始化大顶堆时 是从最后一个有子节点开始往上调整最大堆。而堆顶元素(最大数)与堆最后一个数交换后,需再次调整成大顶堆,此时是从上往下调整的。
不管是初始大顶堆的从下往上调整,还是堆顶堆尾元素交换,每次调整都是从父节点、左孩子节点、右孩子节点三者中选择最大者跟父节点进行交换,交换之后都可能造成被交换的孩子节点不满足堆的性质,因此每次交换之后要重新对被交换的孩子节点进行调整。我在算法中是用一个while循环来解决的。
def sift_down(arr, start, end):
root = start
while True:
# 从root开始对最大堆调整
child = 2 * root + 1
if child > end:
break
# 找出两个child中交大的一个
if child + 1 <= end and arr[child] < arr[child + 1]:
child += 1
if arr[root] < arr[child]:
# 最大堆小于较大的child, 交换顺序
arr[root], arr[child] = arr[child], arr[root]
# 正在调整的节点设置为root
root = child
else:
# 无需调整的时候, 退出
break
def heap_sort(arr):
# 从最后一个有子节点的孩子还是调整最大堆
first = len(arr) // 2 - 1
for start in range(first, -1, -1):
sift_down(arr, start, len(arr) - 1)
# 将最大的放到堆的最后一个, 堆-1, 继续调整排序
for end in range(len(arr) -1, 0, -1):
arr[0], arr[end] = arr[end], arr[0]
sift_down(arr, 0, end - 1)
def main():
# [7, 95, 73, 65, 60, 77, 28, 62, 43]
# [3, 1, 4, 9, 6, 7, 5, 8, 2, 10]
l = [3, 1, 4, 9, 6, 7, 5, 8, 2, 10]
print l
heap_sort(l)
print l
if __name__ == "__main__":
array = [16, 7, 3, 20, 17, 8]
print(array)
heap_sort(array)
print(array)