【堆排序前言】
树与二叉树
1.树是一种数据结构
2.树是一种可以递归定义堆数据结构
3.树是由n个节点组成的集合:
1.如果n=0,则是一个空树
2.如果n>0,那存在一个节点作为树的根节点,其他节点可以分为m个集合,每个集合本身又是一棵树。
树的基本概念
1.根节点(图中的A)
2.叶子节点(图中的B,C,H,I,P,Q,K,L,M,N)没有分叉就是叶子的节点
3.树的深度(看树的最深有多少层数) P,Q就是4
4.节点的度(对于E这个节点,是有两个度)几个分叉就有几个度
5.树的度:就是整个树里最大的节点的度(A分叉最多,所以树的度是6)
6.孩子节点/父节点:上面的是父节点,下面的是子节点(E是I的父节点,I是E的子节点)
7.子树:从树中单独拎出来的叫做子树(E,I,J,P,Q)
二叉树基本概念
1. 二叉树:度不超过2的树
2. 每个节点最多有两个孩子节点
3. 两个孩子节点被区分为左孩子节点和右孩子节点
4. 深度为h的非空二叉树最多有2^h-1个节点(深度为4的非空二叉树最多有15个节点)
满二叉树:
一个二叉树,如果每一层的结点树都达到最大值,则这个二叉树是满二叉树
完全二叉树:
简单来说,就是在满二叉树的前提下,最后一层少几个节点,但中间不能断,得连续(8,9,10,11,12 可以少11,12,不能中间少10或者别的树)。
叶节点只能出现在最下面一层和次下层,并且最下面一层的结点都集中在该层最左边的若干位置的二叉树
(c)4节点下少了两个结点;(d)3节点下少了一个结点
二叉树的存储方式(表示方式)
1.链式存储方式
2.顺序储存方式(堆排序)
二叉树的顺序储存方式
1.首先将完全二叉树从左往右依次插入到列表中
2.观察父节点和左孩子节点的下标编号有什么关系?
可以观察到 0==>1, 1==>3, 3==>7
父亲找左孩子:
即规律:左孩子节点是父节点的两倍+1 (i==>2*i+1)
2.父节点和右孩子节点的编号下标同左孩子类似。
0==>2 ,2==>6, 3==>8,4==>10
父亲找右孩子:
即规律是:右孩子节点是父节点的两倍+2 (i==>2*i+2)
3.左右孩子节点n找父亲父亲节点:(n-1)//2
【堆排序】
1. 堆:一种特殊的完全二叉树结果
1.大根堆:一棵完全二叉树,满足任一节点都比其孩子节点大(父亲比孩子都大)
2.小根堆:一棵完全二叉树,满足任一节点都比其孩子节点小(父亲比孩子都小)
2.堆的向下调整
假设:节点堆左右子树都是堆,但自身不是堆
则可以通过一次向下调整来将其变化成一个堆。
实现方式:从下一层中最大的节点和父节点交换
直到满足大小堆的条件,结束调整
3.向下调整函数的实现
【思路】
1.创建两个参数i,j,tmp
i表示父节点,先获取堆顶的下标
j表示左孩子的子节点
tmp表示堆顶的值
2.判断tmp和li[j]的大小,然后确定位置
3.判断完成,将i变到j的位置,将j变到下面的位置
【具体代码】
#向下调整函数
def sift(li,low,high): #列表,堆的根节点位置,堆的最后一个元素位置(用于判断是否越界的作用)
i=low #i指最开始指向的根节点
j=i*2+1 #指最开始的左孩子节点
tmp=li[low] #将堆顶存起来
while j<=high: #当孩子节点大于堆的最后一个元素,则代表父节点i是最后一层
if j+1<=high and li[j+1]>li[j]: #如果右节点存在并且大于左节点
j=j+1 #将j改为右节点
if li[j] > tmp:#如果子节点比堆顶大
li[i]=li[j] #将子节点和堆顶换
i=j #将子节点的值赋值给i
j=2*i+1 #重新计算子节点
else: #tmp更大,把tmp放到i的位置上
li[i]=tmp
break #向下调整结束
else: #当j>high,i是最后一行,则将堆顶存放到i的位置
li[i]=tmp
4.堆排序的实现
一、堆排序过程/步骤
1.建立堆。(构造堆,从最后的节点出发,依次和父节点交换,看下图)
2.得到堆顶元素i,为最大元素。(找到3,9,1,8,6这样的根节点)
(根节点的下标由子节点计算出)
3.去掉堆顶,将堆最后一个元素放到堆顶,此时可通过一次调整重新使堆有序。
4.堆顶元素为第二大元素。
5.重复步骤3,直到堆变空
构造堆图解
首先做3,5这个子堆的堆排序,依次类推
二、实现建堆的代码(还没有完成) 初步完成
【实现代码】
#堆排序
def heap_sort(li):
n=len(li)-1 #代表最后一个元素
#(n-1)//2 代表父元素
for i in range((n-1)//2,-1,-1): #从最后一个父元素节点开始调整
#i表示建堆的时候调整的部分的根下标(5,9,1,8,6)
sift(li,i,n-1)#n-1的说明,看下面文字
#建堆完成
print(li)
#做个例子
li=[i for i in range(10)]
import random #导入随机元素
random.shuffle(li)#打乱列表元素
print(li)
heap_sort(li)
*向下调整函数中的high是为了防止父元素i下标越界而定义的,所以在求7这个元素的子节点时,可以直接将最后的3定义为high,省去了大量时间去计算high的值,7的下标的子节点肯定不会跑到5下面(2*父元素+1)
【运算结果】
[8, 5, 7, 6, 9, 4, 2, 0, 3, 1] #原列表
[9, 8, 7, 6, 5, 4, 2, 0, 3, 1] #做了建堆操作的列表
这样看可能不好看,前一个对应后两个(9的子节点是8和7)
三、实现挨个出数
1.首先遍历i,从最后一个元素到第一个元素
2.为了减少内存,把堆顶max的数与i进行交换,存放在i原来的位置上
3.通过向下调整的函数再将第二大的数放到堆顶
4.将i向前挪一位
【整体代码实现】
#向下调整函数
def sift(li,low,high): #列表,堆的根节点位置,堆的最后一个元素位置(用于判断是否越界的作用)
i=low #i指最开始指向的根节点
j=i*2+1 #指最开始的左孩子节点
tmp=li[low] #将堆顶存起来
while j<=high: #当孩子节点大于堆的最后一个元素,则代表父节点i是最后一层
if j+1<=high and li[j+1]>li[j]: #如果右节点存在并且大于左节点
j=j+1 #将j改为右节点
if li[j] > tmp:#如果子节点比堆顶大
li[i]=li[j] #将子节点和堆顶换
i=j #将子节点的值赋值给i
j=2*i+1 #重新计算子节点
else: #tmp更大,把tmp放到i的位置上
li[i]=tmp
break #向下调整结束
else: #当j>high,i是最后一行,则将堆顶存放到i的位置
li[i]=tmp
#堆排序
def heap_sort(li):
n=len(li)-1 #代表最后一个元素
#(n-1)//2 代表父元素
for i in range((n-1)//2,-1,-1): #从最后一个父元素节点开始调整
#i表示建堆的时候调整的部分的根下标(5,9,1,8,6)
sift(li,i,n-1)#n-1的说明,看下面文字
#建堆完成
#挨个出数
for i in range(n,-1,-1):#i指当前堆的最后一个元素
li[0],li[i]=li[i],li[0] #交换位置
sift(li,0,i-1)#i向前挪一位,表示新的high,而不是i的位置,i的位置上的数是用来存放堆顶元素的
print(li)
#做个例子
li=[i for i in range(10)]
import random #导入随机元素
random.shuffle(li)#打乱列表元素
print(li)
heap_sort(li)
【运行结果】
[5, 9, 7, 1, 2, 4, 3, 8, 0, 6]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
堆排序的时间复杂度 O(nlogn)
四、堆排序——内置模块
1.Python内置模块——heapq
2.常用函数
heapify(x) #建最小堆
heappop(heap) #排序
【代码实现】
import random #导入随机元素
import heapq #q -> queue 优先队列
li=[i for i in range(10)]
random.shuffle(li)#打乱列表元素
print(li)
heapq.heapify(li)#建堆
for i in range(len(li)):
print(heapq.heappop(li),end=',')
【实验结果】
[0, 4, 6, 2, 1, 8, 7, 9, 5, 3]
0,1,2,3,4,5,6,7,8,9
五、堆排序——topk问题
【问题描述】
现在有n个数,设计一个算法得到前k大大数(k<n)
【解决思路】
1.排序后切片 O(nlogn)
2.排序LowB三人组 O(kn)
3.堆排序思路 O(nlogk) 最快
【topk问题堆排序思路】
1. 取列表前k个元素建立其一个小根堆。堆顶就是目前第k大的数。
2.依次向后遍历原列表,对于列表中的元素,如果小于堆顶,忽略;如果大于堆顶,则将堆顶更换成该元素,并且对堆进行一次向下调整。
3.遍历列表所有元素后,倒序弹出堆顶
【实现代码】
只需要把上面写的向下调整函数的最大堆改为最小堆(把子节点大的数输出改成把子节点小的数输出即可)
#向下调整函数
def sift(li,low,high): #列表,堆的根节点位置,堆的最后一个元素位置(用于判断是否越界的作用)
i=low #i指最开始指向的根节点
j=i*2+1 #指最开始的左孩子节点
tmp=li[low] #将堆顶存起来
while j<=high: #当孩子节点大于堆的最后一个元素,则代表父节点i是最后一层
if j+1<=high and li[j+1]<li[j]: #如果右节点存在并且大于左节点
j=j+1 #将j改为右节点
if li[j] < tmp:#如果子节点比堆顶大
li[i]=li[j] #将子节点和堆顶换
i=j #将子节点的值赋值给i
j=2*i+1 #重新计算子节点
else: #tmp更大,把tmp放到i的位置上
li[i]=tmp
break #向下调整结束
else: #当j>high,i是最后一行,则将堆顶存放到i的位置
li[i]=tmp
def topk(li,k):
n=k-1 #最后一个元素
heap=li[0:k] #前k的列表
#建堆
for i in range((n-1)//2,-1,-1):
sift(heap,i,n)
#遍历k后面的元素
for i in range(k,len(li)):
if heap[0]<li[i]:
heap[0]=li[i]
sift(heap,0,n)
#挨个出数
for i in range(n,-1,-1):
heap[0],heap[i]=heap[i],heap[0]
sift(heap,0,i-1)
return heap
import random #导入随机元素
import heapq #q -> queue 优先队列
li=[i for i in range(10)]
random.shuffle(li)#打乱列表元素
print(li)
print(topk(li,5))
【实验结果】