用python实现堆排序
前几天,写了用python实现十大经典排序,但是后面三个都是照搬百度,自己也没有很好的理解堆这个数据结构,现在又来埋坑了。
什么是堆?堆这种数据结构应用场景非常多,所以还是必须熟练掌握的。
在了解堆之前,我们需要了解一下,什么是完全二叉树
完全二叉树
下面是百度的定义:
一棵深度为k的有n个结点的二叉树,对树中的结点按从上至下、从左到右的顺序进行编号,如果编号为i(1≤i≤n)的结点与满二叉树中编号为i的结点在二叉树中的位置相同,则这棵二叉树称为完全二叉树。
看完这个,是不是有点不理解,那么我们看看他的特点:
完全二叉树的特点:叶子结点只能出现在最下层和次下层,且最下层的叶子结点集中在树的左部。需要注意的是,满二叉树肯定是完全二叉树,而完全二叉树不一定是满二叉树。
如果还是不懂,那我们可以来简单的上几个图给到大家理解一下
则上面三个图,只有第三个是完全二叉树,这是因为所有的叶子节点在最后一层和倒数第二层,并且最后一层的叶子节点全部在左边。而第一个图,很明显并不是完全二叉树,而第二个的叶子节点并不是都在左边。
当我们知道完全二叉树之后,我们就可以来了解堆了。
堆
堆(heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵树的数组对象。堆总是满足下列性质:
- 堆中某个结点的值总是不大于或不小于其父结点的值;
- 堆总是一棵完全二叉树。
看到第一个性质,我们就可以把堆分成两种堆,一个是大根堆:即每个节点的值都大于等于他的子节点。而另一个就是小根堆:即每个节点的值都小于等于子节点的值。
我们来看看两种堆的具体例子:
看到这里,你就已经基本上了解完全二叉树和二叉堆是什么一种数据结构了。但是我们如何存储堆呢?
堆的存储
还记得二叉树的一个定义么?从上到下,从左到右依次编号,编号i的节点刚好与二叉树的节点相同。我们在利用一个图来进一步演示一下。
看到了吗,我们的堆刚好可以完整的放入一个列表,但你可能又会问,该如何表示这个节点的两个子节点呢?我们那下标为2的节点(2)来举例,它的两个子节点分别是(12)和(9)。而(12)的下标是5,(9)的下标为6。有发现么?5刚好是2的两倍+1,而6则是2的两倍+2,那么我们是否可以得出在这样一个结论:
- 对于一个下标为i的节点(非叶子节点),那么它的左节点下标为(
2*i+1
),而右节点为(2*i+2
)。
既然得到了这个公式,那么使用列表就完全可以来进行存储堆里面的元素。
现在,我们已经知道什么是堆并且已经知道可以使用列表来进行堆元素的存储。那我们应该如何进行堆排序呢?要进行堆排序,我们就需要知道堆排序需要进行两个步骤:
- 建堆
- 排序
下面,我们就来说一说建堆。
建堆
我们知道,堆其实分为两种,大根堆和小根堆。并且数组来存储堆是完全可行的。但如果随机给我们一个数组,可能并不满足堆的性质,我们就需要将他进行调整,使其满足大根堆或小根堆的要求。这个就是建堆,也称为堆化。
对于建堆,我们有两种方式:上浮操作和下沉操作。也就是从堆底插入,然后寻找到合适的位置放入与循环早一个元素,不断让其下沉直到找到合适位置。下面我们就一起来看看吧。
上浮建堆
在说上浮建堆之前,我们就需要知道,如何进行上浮。
假设已经存在了一个堆,他们的元素存储形式如下:heap = [2、4、5、9、6、7、8、11、16]
。如果我们想加入1这个元素,那就在列表的最后先放入1。则现在的堆列表就变成了:heap = [2、4、5、9、6、7、8、11、16、 1]
,因为1的下标是9,则要获得他的父节点,只需要整除2即可。
f
a
t
h
e
r
N
o
d
e
=
(
c
h
i
l
d
r
e
n
N
o
d
e
−
1
)
/
2
fatherNode = (childrenNode - 1 )/ 2
fatherNode=(childrenNode−1)/2,这样就可以发现,1的父节点下标是4。
接下来就是判断这两个数的大小,因为现在创建的是小根堆,因此如果插入的元素小于他的父元素,则需要交换位置,如果大于则直接返回即可:
if heap[fatherNode] > heap[childrenNode]:
heap[fatherNode], heap[childrenNode] = heap[childrenNode], heap[fatherNode]
else:
return
经过交换,我们现在的存储形式就变成了如下:heap = [2、4、5、9、1、7、8、11、16、 6]
,接下来,就是用4来和它的父节点进行对比,直到与根节点进行对比。
现在我们也已经直到上浮建堆的操作了。那么我们就利用python来是实现这个方法吧。
# -*- coding: utf-8 -*-
# @Auther:Summer
def up_to_heap(num):
chiledNode = len(heap)
heap.append(num)
while chiledNode > 0:
if chiledNode % 2 == 1: # 左节点
fatherNode = (chiledNode - 1) // 2
else: # 右节点
fatherNode = (chiledNode - 2) // 2
if num < heap[fatherNode]:
heap[chiledNode] = heap[fatherNode]
chiledNode = fatherNode
else:
break
heap[chiledNode] = num
if __name__ == '__main__':
heap = [2, 4, 5, 9, 6, 7, 8, 11, 16]
up_to_heap(1)
print(heap)
上浮操作已经搞懂了,那我们就来看看下沉操作吧
下沉操作
假设,给我的们列表是不满足堆的要求的,例如下面这张图:
这里面,7在堆顶,但是不满足小根堆的要求,此时我们就需要把这个7进行下沉。
那如何下沉呢?我们只需要将该节点与它的两个子节点中较小者进行交换即可。比如这里的7我们就把7与2进行交换,而7又与3进行交换。这很容易理解,因为要创建小根堆,我们就需要把最小的往上浮。那如何判断已经到达了合适位置呢?
- 如果带下沉元素小于两个子节点,此时符合堆的规则,则无需下沉
- 下沉到了叶子节点,此时并没有任何子节点可供下沉,则结束下沉
知道了这些,那你就知道了什么是下沉操作了,但下沉和建堆有什么关系呢?其实我们还是在堆底放入元素,然后从第一个非叶子节点开始依次往上递归,每个节点进行下沉操作,经过这样一顿操作之后,所得到的堆就是一个我们想要的最大堆或者最小堆了。
下面就是用python来实现下沉建堆的操作:
# -*- coding: utf-8 -*-
# @Auther:Summer
def dowm_to_heap(num):
childrenNode = len(heap)
heap.append(num)
if childrenNode % 2 == 0:
fatherNode = (childrenNode - 1) // 2
else:
fatherNode = (childrenNode - 2) // 2
for i in range(fatherNode, -1, -1):
while True:
j = i * 2 + 1
if j < len(heap) - 1 and heap[j] > heap[j + 1]:
j += 1
if j < len(heap) and heap[i] > heap[j]:
heap[i], heap[j] = heap[j], heap[i]
else:
break
i = j
if __name__ == '__main__':
heap = [8, 5, 7, 9, 2, 10, 1, 4, 6, 3]
dowm_to_heap(11)
print(heap)
注意:上面的代码是实现最小堆的,如果要实现最大堆,则可以把代码中大于号都换成小于号即可。
到这里,我们已经知道了什么是完全二叉树、堆、堆的存储与建堆的两种方式。那接下来我们就来看看如何进行堆排序。
同样,在了解堆排序之前,我们需要先了解一下如何删除堆顶元素。我们需要知道,当我们删除了堆顶元素之后,整个堆还是必须满足大顶堆或者小顶堆。
删除堆顶元素
我们先看下图:
如果我们想要删除堆顶11这个元素,我们就需要把11与最后一个元素进行交换使其变成heap = [2,9,10,8,5,7,4,6,3,11]
,然后将2进行下沉操作。
代码就不给出了,很简单,开头先将堆顶元素与堆尾元素进行交换,然后堆顶元素进行下沉操作即可。
那我们不妨想一下,假设我们有一个大顶堆,这样我们就知道,堆顶的元素肯定是最大的。然后将堆顶元素放在堆尾,然后依次执行下沉和递归操作,这样整个堆不就完成了排序了吗?没错,堆排序的思路就是这样。
那么,我们就用python来实现堆排序吧。
# -*- coding: utf-8 -*-
# @Auther:Summer
def dowm_heap(i, end):
"""
:param i: 表示当前需要下沉的下标
:param end: 表示下沉的重点(在第一个元素与最后一个元素交换之后,最后元素就不能在动了, 因此需要被保护起来)
:return:
"""
while i * 2 < end:
j = i * 2 + 1
if j < end - 1 and heap[j] < heap[j + 1]:
j += 1
if j < end and heap[i] < heap[j]:
heap[i], heap[j] = heap[j], heap[i]
else:
break
i = j
def sortArry():
"""
进行排序,依次从k到1进行交换,然后执行下沉操作
:return:
"""
k = len(heap) - 1
while k > 0:
heap[0], heap[k] = heap[k], heap[0]
dowm_heap(0, k)
k -= 1
def main():
"""
主函数,来实现建堆与排序
:return:
"""
# 建堆(大根堆)
n = len(heap) - 1
if n % 2 == 0:
fatherNode = (n - 1) // 2
else:
fatherNode = (n - 1) // 2
for i in range(fatherNode, -1, -1):
dowm_heap(i, n)
# 排序
sortArry()
if __name__ == '__main__':
heap = [1, 3, 8, 5, 985, 4, 8, 975, 6, 3, 2, 3, 6, 9, 7, 4, 322, 1, 14, 68, 87, 8, 9, 6, 43, 896]
main()
print(heap)
至此,我们的堆排序也就完成了。总的来说堆排序还是比较简单的,首先要理解什么是堆,然后比忘了两步:建堆与排序。
时间复杂度分析
我们建堆的时间复杂度为O(n),排序过程的时间复杂度为O(nlogn),所以总的时间复杂度为O(nlogn)
空间复杂度分析
因为我们的堆排序并没有消耗其余的空间,因此空间复杂度为O(1)