一、为什么说堆排序很“神秘”?
堆排序相较于其他常见的线性排序(冒泡、选择、插入、快排等),它比较不一样(我们不一样,不一样)。堆排序使用“堆”结构进行非线性排序,这里的堆就是数据结构里的二叉树,确切的说应该是【完全二叉树】。说到树结构,可能会想到二分法的“分治”思想,难道跟快排有些许关系?不!快排是使用了二分法的思想,但是没有使用二叉树的组织结构,数据还是线性列表的;而堆排序则不同。
另外,在我们刚开始接触算法的时候,可能大部分都是从前面说的几种简单的排序算法开始的,好理解,写起来又简单。堆排序可能就被冷落了一些。并且堆排序的逻辑比那几种算法稍微复杂一点,从下面的代码量就可以看出,比前面说的几种算法多很多代码。
二、堆排序的特性?
堆排序的时间复杂度:O(n) = n*log(n)
堆排序的稳定性:不稳定(假设在数列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面;并且排序之后,a[i]仍然在a[j]前面。则这个排序算法是稳定的!)
三、堆排序的原理是怎样的?
首先,我们需要理解什么大根堆、小根堆、父节点和子节点之间的索引关系。
1、大根堆(大顶堆):在完全二叉树的条件下,任意父节点的值都大于等于其左右子节点。
2、小根堆(小顶堆):在完全二叉树的条件下,任意父节点的值都小于等于其左右子节点。
3、索引关系:
若某一节点A的索引为x时:
A的左子节点索引为:2*x+1
A的右子节点索引为:2*x+2
A的父节点的索引为:(x-1)//2 ('//'为除法向下取整符号)
下图是是数列到堆的映射关系:
本程序示例中使用的是大根堆方法,基本逻辑是:
1、把数列初始化成一个大根堆(或者小根堆)。
2、取出最大值(根节点值)放到数列最后。
3、把剩余的数列重新调整为一个大根堆。
4、重复第2、3步骤。
import numpy
def heap_sort_asc(arr):
"""
堆排序(升序)。
时间复杂度:n*log(n)
稳定性:不稳定(假设在数列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面;并且排序之后,a[i]仍然在a[j]前面。则这个排序算法是稳定的!)
此处使用大根堆(即:对于任意节点,其孩子节点都不大于该节点)
:param arr: 待排序的数列
:return: 排好序的数列(升序)
"""
arr_size = len(arr)
# 把数列排列成大根堆形式
set_big_heap(arr, arr_size)
for i in range(arr_size): # 循环n次
heap_size = arr_size - i
# 交换。[0]处的数值是最大值,交换放到数列最后
arr[0], arr[heap_size - 1] = arr[heap_size - 1], arr[0]
# 把剩余的数列元素重新调整成大根堆,最多循环log(n)次,即二叉树的深度
adjust_to_big_heap(arr, heap_size - 1)
return arr
def set_big_heap(arr, size):
"""
把一个数列处理成一个大根堆
注意:若当前节点索引为x,则:其父节点索引为:(x-1)//2
:param arr: 数列
:param size: 要处理的数列元素数量
:return:
"""
# 遍历每一个数据
for i in range(size):
this_index = i
parent_index = (this_index - 1) // 2 # 父节点索引
if parent_index < 0: # 没有父节点了
continue
# 循环查找,直到当前节点不大于父节点
while arr[this_index] > arr[parent_index]:
# 交换当前节点和父节点
arr[this_index], arr[parent_index] = arr[parent_index], arr[this_index]
this_index = parent_index # 当前节点指向交换之前的父节点位置
parent_index = (this_index - 1) // 2 # 计算父节点
if parent_index < 0: # 找不到父节点,则退出循环
break
return arr
def adjust_to_big_heap(arr, size):
"""
调整为大根堆.
该函数处理的堆有一个“异常值”,那就是根节点上的值,它是由堆的最大值(原根节点)和堆的最后一个节点值交换而来。
所以这个函数的任务就是给这个“异常值”,寻找一个合适的家(位置)。
注意:若当前节点索引为x,则:
其左孩子索引为:2*x+1
其右孩子索引为:2*x+2
:param arr: 是一个被替换掉跟节点的大根堆(此时,除了根节点,其他及节点都满足大根堆的要求)。正因为如此,所以需要重新调整为大根堆
:param size: 堆的大小,即在数组中的索引范围[0, size)
:return: 调整后的原数组
"""
current_index = 0 # 最大的值的索引(因为是大根堆,所以索引0的值最大)
while current_index * 2 + 1 < size: # 有左孩子
left_node_index = current_index*2+1
# 寻找最大孩子节点
max_child_index = left_node_index
if left_node_index+1 < size: # 有右孩子
if arr[left_node_index] < arr[left_node_index+1]:
max_child_index = left_node_index+1
# 孩子节点大于父节点(当前节点),则交换
if arr[max_child_index] > arr[current_index]:
arr[max_child_index], arr[current_index] = arr[current_index], arr[max_child_index]
# 更新当前索引指向(当前索引一直指向最初那个根节点上的值,因为只有这个值是“异常值”,需要给它寻找合适的位置)
current_index = max_child_index
else:
break
if __name__ == '__main__':
array = list(numpy.random.randint(0, 100, 20))
print(array)
a = heap_sort_asc(array)
print(a)
排序前:[28, 93, 13, 51, 9, 97, 42, 8, 1, 57, 89, 10, 24, 73, 97, 24, 13, 41, 11, 1]
排序后:[1, 1, 8, 9, 10, 11, 13, 13, 24, 24, 28, 41, 42, 51, 57, 73, 89, 93, 97, 97]