1、什么是堆
(1) 堆是具有以下性质的完全二叉树(那么,什么是完全二叉树呢?完全二叉树是一种除了最后一层之外的其他每一层都被完全填充,并且所有结点都保持向左对齐的树):每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。如下图:
同时,我们对堆中的结点按层进行编号,将这种逻辑结构映射到数组中就是下面这个样子
该数组从逻辑上讲就是一个堆结构,我们用简单的公式来描述一下堆的定义就是:
大顶堆:arr[i] >= arr[2*i+1] && arr[i] >= arr[2*i+2]
小顶堆:arr[i] <= arr[2*i+1] && arr[i] <= arr[2*i+2]
比如20 10 15 这三个数的关系,i表示坐标,i=3时,对应的arr值为20,i=3*2+1=7,arr值为10,i=3*2+2=8,arr值为15,所以2*i+1和2*i+2是这么来的。
(2) 堆的起始坐标从1开始算:
比如说当前节点在list中的坐标为i
那么他的左节点坐标:2i
那么他的右节点坐标:2i + 1
找到它的父节点:i//2
(3) 堆的坐标从0开始算:
比如说当前节点在list中的坐标为i
那么他的左节点坐标:2i+1
那么他的右节点坐标:2i + 2
找到它的父节点:(i-1)//2
(4) 堆也叫做优先队列
(5) 上滤:新插入元素和父节点比对,发生交换,当发生了新插入结点和父结点没有交换的情况,那么上滤过程结束
(6) 下滤:左右节点比对后发生交换,将大的元素上调,不断重复,直到不需要调整或者调整到堆底,如下图
2、什么是二叉树及二叉树的一些概念
(1) 结点的度:结点拥有的子树的数目
(2) 叶子:没有子节点,度为零
(3) 森林:多个不相交的树,多个树
(4) 满二叉树:每个结点下都挂了2个子节点
(5) 完全二叉树:叶子结点只能出现在最下层和次下层,最下层的叶子结点集中在树的左部。满二叉树一定是完全二叉树,一个完全二叉树不一定是满二叉树。
(6) 二叉搜索树(又叫二叉查找树):左边的结点都小于根结点,右边的结点都大于根结点
3、堆排序介绍
堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(n*logn),它也是不稳定排序。
4、堆排序的核心算法
(1) 建堆:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点
(2) 把堆顶的元素和最下最右一个元素交换,此时,最大的元素就排到了最后面
(3) 对于新的root节点,继续建堆,这样会得到n个元素的次小值
(4) 重复上面的1到3步,便能得到一个有序序列了
5、堆排序基本思想及步骤
步骤一 构造初始堆(从堆最下层开始构造,并且需要进行上虑操作)。将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)。
假设给定无序序列结构如下:
此时我们从最后一个非叶子结点开始(叶结点自然不用调整,最后一个非叶子结点 arr.length/2-1=5/2-1=1,也就是下面的6结点),从后往前,从下往上的第一个非叶子节点进行调整。
找到第二个非叶节点4,由于[4,9,8]中9元素最大,4和9交换。
这时,交换导致了子根[4,5,6]结构混乱,继续调整,[4,5,6]中6最大,交换4和6。
此时,我们就将一个无需序列构造成了一个大顶堆。
步骤二 将堆顶元素与末尾元素进行交换,也就是与堆的最下最右的元素进行交换,使末尾元素最大。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。
将堆顶元素9和末尾元素4进行交换
重新调整结构,使其继续满足堆定义
再将堆顶元素8与末尾元素5进行交换,得到第二大元素8
后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序
总结:对于堆排序的核心算法就是堆结点的调整
- 度数为2的结点A(有两个孩子结点),如果它的左右孩子结点的最大值比它大的,将这个最大值和该结点交换
- 度数为1的结点A(有左孩子结点),如果它的左孩子的值大于它,则交换
- 如果结点A被交换到新的位置,还需要和其孩子结点重复上面的过程堆排序的代码
6、堆排序代码
# encoding=utf-8
#左叶子、右叶子和父节点,三个元素,找到最大的一个
def maxHeap(heap,heapSize,i):
#i为某个节点
#它的左节点坐标:2i
#那么它的右节点坐标:2i + 1
#它的父节点:i/2
left = 2*i +1
right = 2*i +2
larger = i
#通过2次if的比较,将left、right和lager三者的最大值找到,这里假设当前是一个已经构建好的堆
if left < heapSize and heap[larger] < heap[left]:
larger = left
if right < heapSize and heap[larger] < heap[right]:
larger = right
#如果lager的值不是i,说明i的值需要和最大值进行交换
#因为i的坐标是最大堆的堆顶,所以必须是最大值
if larger != i:
heap[i],heap[larger] = heap[larger],heap[i]
maxHeap(heap,heapSize,larger)
#以上步骤完成,堆顶坐标为i坐标的最大子堆建立好了
def buildMaxHeap(heap):
#heap参数是未排序,未建堆的list
heapSize = len(heap)
#堆的长度//2可以找到堆里面的
#最后一个带有子节点的节点
#循环可以实现从堆的最下层节点开始建堆
#每次建立的堆都是一个最大堆
#简单来说把所有字段都建成最大堆
#然后组成了最终的最大堆
# (heapSize-1)//2当坐标从0开始算时,算出了当前堆中最后一个含有子节点的结点坐标
#这个循环要理解一下:这个循环调用,表示从最下层的子树,开始实现最大堆,这个就是我刚才说的从最下层开始建立最大堆的过程
for i in range((heapSize-1)//2,-1,-1):
maxHeap(heap,heapSize,i)
def heapSort(heap):
#先把所有元素先建立一个最大堆
buildMaxHeap(heap)
#将堆中所有的元素都遍历一遍
#让每个元素都做一次堆顶
#然后将堆顶的每个元素都换到堆的最后一个节点
for i in range(len(heap)-1,-1,-1):
heap[0],heap[i] = heap[i],heap[0]
#maxheap中的i是列表的长度,这样可以防止追加到
#堆后面的元素重新被当做最大堆元素进行建队
maxHeap(heap,i,0)
return heap
#第一次循环时,把最大值放到了列表最后面
#把最后一个值(肯定不是最大值)放到了堆顶
#把不包含最后一个元素的剩余元素,重新进行最大堆
#第二次循环的时候,把堆顶(次大值)放到了列表倒数的第二位置
#然后把不包含最后2个元素的剩余元素,重新进行建立最大堆
………
#循环结束,那么列表的数据就排好了
if __name__ == '__main__':
heap1 = [3,4,5,6,23,4,1,1,23,45,6678]
print (heap1)
heapSort(heap1)
print (heap1)
7、堆排序的时间复杂度
堆排序是一种选择排序,整体主要由构建初始堆+交换堆顶元素和末尾元素并重建堆两部分组成。其中构建初始堆经推导复杂度为O(n),在交换并重建堆的过程中,需交换n-1次,而重建堆的过程中,根据完全二叉树的性质,[log2(n-1),log2(n-2)...1]逐步递减,近似为nlogn。所以堆排序时间复杂度一般认为就是O(N*logN)级。