代码环境:python3.6
二叉树
本文涉及树这种数据结构,所以下面我们先简单回顾下树的相关概念。二叉树:每个节点最多有两个子树的树结构,如下图:满二叉树:每一层都挂满了节点的二叉树,上面图中的二叉树,就是满二叉树。
完全二叉树:假如有 n 层,从 1 到 n-1 层与满二叉树一样,最后一层有两种可能,要么是满节点,要么节点集中在最左边,如下图:叶子节点:没有孩子节点的节点,就是叶子节点,上面完全二叉树图中的节点5、6、7、8就是叶子节点。
二叉堆
堆排序,顾名思义需要用到堆,更具体的说是用到二叉堆。
二叉堆是一种特殊的完全二叉树,可分为以下两种:最大堆:根节点的值最大,任何一个父节点的值,都大于等于它左右孩子节点的值。
最小堆:根节点的值最小,任何一个父节点的值,都小于等于它左右孩子节点的值。
python 中的二叉堆存储在列表中,依靠列表的下标来定位各个节点。
假设父节点的下标是p,那么对应左孩子下标就是2*p+1,对应右孩子的下标就是2*p+2。
下面我们介绍二叉堆的常用两种操作,最大堆和最小堆思路都是一样的,本文我们以最大堆为例。
最大堆删除堆顶节点后的自我调整
根据最大堆的特性,当我们删除一个最大堆的堆顶,堆经过自我调整重新让每一个父节点都比其子节点大,这样第二大的节点就会被交换上来成为新最大堆的堆顶。
这个自我调整的过程,是后面堆排序的核心算法,我们来看看这个具体过程:
假设有最大堆[9, 7, 6, 5, 2, 1, 4, 3],如下图:
当删除值为9的堆顶节点时,为了维持完全二叉树的结构,我们把堆的最后一个节点3补到原本堆顶的位置,如下图:
此时父节点为3,比左孩子节点7小,父节点3下沉:
此时父节点3,比左孩子节点5小,父节点3下沉:
到这里新最大堆调整已经完成。
我们注意到,这个自我调整过程,其实就是最开始补到堆顶位置的节点3下沉的过程。
构建二叉堆
构建二叉堆,也就是把一个无序的完全二叉树调整为二叉堆,本质上就是:让不满足二叉堆特性的非叶子节点依次下沉,下面我们来看看这个过程。
给定一个无序的序列[2, 5, 4, 3, 1, 9, 7, 6]构建最大堆,我们先得到一个无序的完全二叉树如下:
我们从最后一个非叶子节点3开始。此时节点3比其子节点6小,节点3下沉:
轮到非叶子节点4,此时节点4比其子节点9小,节点4下沉:
轮到非叶子节点5,此时节点5比其子节点6小,节点5下沉:
轮到非叶子节点2,此时节点2比其子节点9小,节点2下沉:
注意到节点2依然比其子节点7小,节点2需要继续下沉:
到这里,最大堆构建完成。
可能我们会有个疑问,为什么不从根节点开始比较?其实也是可以的,但我们用这种方法重复上面的下沉过程,会发现多出大量的交换操作。
所以我们在节点下沉过程中,选择的路径是:从最后一个非叶子节点开始,从下往上、从右到左依次进行比较。
那么,如何确定最后一个非叶子节点的下标?
假设给定的序列长度为n,则最后一个孩子节点的下标为n-1,设其父节点下标为p,分两种情况讨论:最终节点为左孩子节点,则有n-1=2*p+1,即p=(n-2)/2
最终节点为右孩子节点,则有n-1=2*p+2,即p=(n-3)/2
根据地板除向下取整的特性,我们选择p=(n-2)//2就都能满足这两种情况。
堆排序算法的实现
从上面最大堆的两种常用操作过程中,我们知道其核心就是父节点与其较大子节点比较,判断父节点是否下沉的方法,下面代码中我们用方法heap_sink表示。
堆排序步骤:构建最大堆;
循环删除当前最大堆的堆顶(实际上会替换到序列最后面),heap_sink生成新最大堆。
def heap_sink(heap, heap_size, parent_index):
"""最大堆-下沉算法"""
child_index = 2 * parent_index + 1
# temp保存需要下沉的父节点,用于最后赋值
temp = heap[parent_index]
while child_index < heap_size:
# 如果有右孩子,且右孩子比左孩子大,则定位到右孩子
if child_index + 1 < heap_size and heap[child_index + 1] > heap[child_index]:
child_index += 1
# 如果父节点的值不小于左右孩子节点的值,可直接跳出循环
if temp >= heap[child_index]:
break
heap[parent_index] = heap[child_index]
parent_index = child_index
child_index = 2 * parent_index + 1
heap[parent_index] = temp
def heap_sort(mylist):
"""堆排序"""
n = len(mylist)
# 1. 无序列表构建成最大堆
for i in range(n - 2 // 2, -1, -1):
heap_sink(mylist, n, i)
# 2. 循环删除堆顶元素,移到列表尾部,调节堆产生新的堆顶
for i in range(n - 1, 0, -1):
mylist[0], mylist[i] = mylist[i], mylist[0]
heap_sink(mylist, i, 0)
if __name__ == "__main__":
mylist = [1, 3, 4, 5, 2, 6, 9, 7]
heap_sort(mylist)
print(mylist)
算法效率时间复杂度:O(nlogn)
空间复杂度:O(1)
稳定性:不稳定
原地排序