堆是完全二叉树结构,了解堆排序前需要先了解树的概念及其性质。
在次简要说明下其性质,详细定义及说明请参考二叉树、堆
定义:
- 一种非线性结构,是n(n>=0)个元素的集合;
- 只有一个没有前驱结点(父结点)的元素称为根;
- 树中除根结点外,其余元素只能有一个前驱结点(父结点),可以有零个或多个后继结点(子结点),二叉树最多有两个子结点;
性质:
-
在二叉树的第i层上至多有2i-1个结点(i >= 1);
-
深度为k的二叉树,至多有2k-1个结点;
-
含有n(n >= 1)个结点的完全二叉树的深度为math.ceil(log2(n+1)),不小于对数值的最小整数向上取整;
-
如果有一颗n个结点的完全二叉树,结点按照层序编号,如下图所示:
4.1 如果i=1,则结点i是二叉树的根,无双亲; 如果i>1,则双亲为int(i/2),向下取整。就是子结点的编号整除2得到的就是父结点的编号,如果父结点是i,那么左孩子结点就是2i,右孩子结点就是2i+1;4.2 如果2i>n,则结点i无左孩子,即结点i为叶子结点;否则其左孩子节点存在编号为2i;
4.3 如果2i+1>n,则结点i无右孩子,注意在这里并不能说明结点i有没有左孩子;否则右孩子结点存在编号为2i+1;
堆排序
核心算法步骤:
- 构建完全二叉树
- 构建大顶堆,根为列表中最大的数
- 排序
1、构建完全二叉树
假设给定数组[30,20,80,40,50,10,60,70,90],以此构建完全二叉树如下图所示:
为了数组的索引和树的编码对应,在数组的首位增加一个占位字符0,调整后数组为[0,30,20,80,40,50,10,60,70,90]此时数字的索引和树的结点编号一致;
堆结点调整思路:
1、度数为2的结点A,如果它的左右孩子结点的最大值比A大,将最大值和结点A交换;
2、度数为1的结点A,如果它的左孩子结点的值比A大,则和结点A交换;
3、如果结点A被交换到新的位置,需要和其孩子结点重复上面的交换逻辑。
#初始数组,首位增加的占位,保证索引和树结点编号对应
origin = [0,30,20,80,40,50,10,60,70,90]
def heap_adjust(n,i,array:list):
'''
调整结点
n:待比较的数据的个数
i:当前结点的编号(数据的索引)
array:待排序的数据
'''
#根据性质4.2,限制循环条件为:存在左孩子节点
while 2 * i <= n:
#根据性质4.2,当左孩子存在时,节点编号为2*i,同时假设左孩子节点为最大孩子节点
lchild_index, max_chile_index = 2 * i
#根据性质4.3,当n<2*i时说明有右孩子节点,同时判断
if 2 * i > n and array[lchild_index +1] > array[lchild_index]:
max_child_index = lchild_index
if array[max_child_index] > array[i]:
array[max_child_index],array[i] = array[i],array[max_child_index]
i = max_child_index
else:
break
到目前为止解决了单个结点的调整,接下来需要使用循环从起始结点开始以此解决比起始结点小的结点
2、构建大顶堆
起始结点选择:
根据性质4.1,当i=n时,取得最后一个有子结点的结点,所以调整的起始结点就是n//2,保证所有结点都有孩子结点;
下一结点:
由于之前构造了一个占位0,所以树结点的编号和列表的索引正好相对应,所以每循环以此,索引-1就是下一个需要调整的结点,直到索引为1.
# 因为列表中加了占位的0,所以元素个数要-1
total = len(origin) - 1
def max_heap(total,array:list):
#从最后一个有子结点的结点开始,每次执行后树结点编号-1
for i in range(total//2,0,-1):
heap_adjust(total,i,array:list)
return array
3、排序
排序思路:
- 每次都让堆顶的元素和最后一个元素交换,然后排除最后一个元素(进入有序区);
- 剩余元素再次调整构建大顶堆;
- 重复前两步,直至剩余一个元素,此时列表的元素就是按照升序排列的。
def sort(total,array:list):
#循环至列表内只有一个元素
while total > 1:
array[1],array[total] = array[total],array[1]
#每次交换后,列表总数-1
total -= 1
#重新调整堆顶
heap_adjust(total,i,array)
return array