基础知识
大顶(根)堆与小顶(根)堆都是一种数据结构,它们都是完全二叉树,但不是用指针链表表示的二叉树,而是使用顺序存储的数组存储元素值。大顶堆即堆顶元素为整个堆中的最大值(小顶堆类似),它可以在O(1)
时间内获取最大值。下面以大顶堆讲解一些堆的相关知识,并给出大、小顶堆的代码。
首先,既然是顺序存储,且又是完全二叉树的结构,其节点如何存储呢?此处使用二叉树的层次遍历方式存储节点值,本文使用的节点索引如下:
父节点:(root-1) / 2
左子节点:root * 2 + 1
右子节点:root * 2 + 2
即:
def left(root):
return 2 * root + 1
def right(root):
return 2 * root + 2
def parent(root):
return (root - 1) // 2
这种索引方式下根节点在数组的0
位置,也有将数组0
位置不用,而将根节点放在1
位置的方式,那种方式的索引和本文使用的稍有不同,此处也记录下:
父节点:root / 2
左节点:root * 2
右节点:root * 2 + 1
对于两种方式,完全可以分别使用[0,1,2]
和[1,2,3]
分别代表父节点、左右子节点来尝试。
在介绍具体如何构建大顶堆之前,先介绍两种操作:sink
和swim
sink: 将当前节点的值往下沉至合适的位置,比如当前值小于其某个子节点,则需要做sink操作
swim: 将当前节点的值往上移动至合适的位置,比如当前值大于其父节点,则需要做swim操作
首先,swim
操作比较简单,只要当前节点值大于其父节点值,就一直将当前节点的值与父节点交换,代码如下:
def swim(nums,root):
while root > 0 and nums[parent[root]]<nums[root]:
nums[root],nums[parent(root)] = nums[parent(root)],nums[root]
root = parent(root)
注意条件中的root>0
,因为对于根节点来说,显然是没有父节点的。
swim
操作比较简单,只需比较当前节点与父节点的值,但是对于sink
操作来说,它需要比较当前节点与左右子节点的值。首先,我们需要清楚什么时候需要执行sink
操作,因为是大顶堆,父节点必须大于等于左右子节点,因此只要当前节点不同时大于其左右子节点,那么就需要交换。那么,怎么交换?和谁交换呢?我们可以先获得左右子节点的最大值older
,如果当前节点的值大于等于older
,说明它无需再往下做sink
操作了;如果小于older
,将当前节点的值与older
对应的节点的值交换,即可在完成当前点的下沉的同时,满足大顶堆的性质(父节点大于等于子节点)。那么,下沉到何时结束呢?下沉至当前节点没有左右子节点即可。
def sink(nums,root):
length = len(nums)
while left(root) < length:
# 先假设左节点是较大的子节点,注意此处older是最大值的索引
older = left(root)
# 若右子节点大于左子节点,更新older
while right(root) < length and nums[right(root)] > older:
older = right(root)
# 若当前节点大于等于older,说明下沉结束
if nums[older] <= nums[root] :
break
# 交换
nums[root],nums[older] = nums[older],nums[root]
root = older
以上,sink
和swim
操作就完成了,在具体将如何构建大顶堆之前,需要指出一点,在sink
和swim
中,频繁地使用了交换操作,当树的结构很大时,其实这些交换操作会很耗时。实际上,我们可以无需每次进行交换,而是保存一开始需要操作的节点值,直到找到它该放的位置再赋值,期间只需不断更新中间节点的值即可。
对于sink
操作来说,首先保存当前节点的值为temp
,在while
循环中,判断older
的值与temp
的值,若older
大于temp
,将当前节点的值设置为older
,继续往下搜索,否则,将当前节点的值赋值为temp
。这个过程拿纸笔画一下,就很清晰了。
修改后代码如下所示:
def sink(nums,root):
length = len(nums)
# 记录需要下沉的点的值
temp = nums[root]
while left(root) < length:
# 先假设左节点是较大的子节点,注意此处older是最大值的索引
older = left(root)
# 若右子节点大于左子节点,更新older
while right(root) < length and nums[right(root)] > older:
older = right(root)
# 若temp大于等于older,说明下沉结束
if nums[older] <= temp:
break
# 直接赋值即可
nums[root] = nums[older]
root = older
# 最后把temp放在该放的位置
nums[root] = temp
对于swim
操作来说,同样可以记录欲上升的点为temp
,然后每次判断父节点与temp
的关系,代码如下:
def swim(nums,root):
temp = nums[root]
while root > 0 and nums[parent(root)] < temp:
# 若父节点的值小于temp,将当前值赋值为父节点的值
nums[root] = nums[parent(root)]
root = parent(root)
# 将temp放在合适的位置
nums[root] = temp
上述两个操作,对于单个节点来说,时间复杂度都为
O
(
l
o
g
N
)
O(logN)
O(logN)。
至此,swim
和sink
操作就讲完了,在讲构建之前,再讲一下大顶堆的插入和删除操作。
首先,对于插入操作来说,可以直接将插入的值放在堆底,然后对其进行swim
操作即可。
对于删除操作(大顶堆的删除指删除堆顶元素)来说,先将堆底的值赋值给堆顶,然后将堆底的值弹出,再将此时的堆顶进行sink
操作即可。
可见,插入和删除操作复杂度都是
O
(
l
o
g
N
)
O(logN)
O(logN)。
大顶堆
下面,开始讲大顶堆的创建。
创建最大堆有两种方法:
- 先创建一个空堆,然后每次往其中插入一个点,故复杂度为 O ( N l o g N ) O(NlogN) O(NlogN)
- 先将N个元素顺序放入堆中,然后从最后一个非叶子节点开始,依次进行
sink
操作,这个复杂度肯定是小于 O ( N l o g N ) O(NlogN) O(NlogN)。因为它没有对N个点进行操作。
此处使用第二种方法,从最后一个非叶子节点开始依次往上进行sink
操作。那么,首先得定位最后一个非叶子节点,最后一个非叶子节点即是堆底节点的父节点,即它的索引是parent(length-1)
。
以下,贴出大顶堆的python
代码。
class MaxHeap:
def __init__(self,nums):
self.nums = nums
self.length = len(nums)
# 此处加1是因为,python的range无法取到最后一个元素
for i in reversed(range(self.parent(self.length-1)+1)):
self.sink(i)
def left(self,root):
return 2 * root + 1
def right(self,root):
return 2 * root + 2
def parent(self,root):
return (root - 1) // 2
def sink(self,root):
temp = self.nums[root]
while self.left(root) < self.length:
# 先假设左孩子较大
older = self.left(root)
# 若右孩子较大,更新older
if self.right(root) < self.length and self.nums[self.right(root)] > self.nums[older]:
older = self.right(root)
# 若最大值小于等于temp,停止下沉
if self.nums[older] <= temp:
break
# 更新
self.nums[root] = self.nums[older]
root = older
# 将temp放在该放的位置上
self.nums[root] = temp
def swim(self,root):
temp = self.nums[root]
while root > 0 and self.nums[self.parent(root)] < temp:
self.nums[root] = self.nums[self.parent(root)]
root = self.parent(root)
self.nums[root] = temp
def insert(self,value):
self.nums.append(value)
self.length += 1
self.swim(self.length-1)
def delMax(self,):
self.nums[0] = self.nums[-1]
self.nums.pop()
self.length -= 1
self.sink(0)
@property
def max(self,):
return self.nums[0]
# 测试代码
print('\n')
print('原始数组为:')
nums = [21,25,49,25,16,8]
print(nums)
a = MaxHeap(nums)
print('构建好的大顶堆:')
print(a.nums)
print('最大值为:')
print(a.max)
print('插入元素50: ')
a.insert(50)
print(a.nums)
print('最大值为:')
print(a.max)
print('删除最大值后:')
a.delMax()
print(a.nums)
print('最大值为:')
print(a.max)
测试:
小顶堆
小顶堆其实和大顶堆很类似,只需要修改sink
和swim
操作的比较规则即可,此处直接贴代码。
class MinHeap:
def __init__(self,nums):
self.nums = nums
self.length = len(self.nums)
for i in reversed(range(self.parent(self.length-1)+1)):
self.sink(i)
def left(self,root):
return 2 * root + 1
def right(self,root):
return 2 * root + 2
def parent(self,root):
return (root - 1) // 2
def sink(self,root):
temp = self.nums[root]
while self.left(root) < self.length:
smaller = self.left(root)
if self.right(root) < self.length and self.nums[self.right(root)] < self.nums[smaller]:
smaller = self.right(root)
if temp <= self.nums[smaller]:
break
self.nums[root] = self.nums[smaller]
root = smaller
self.nums[root] = temp
def swim(self,root):
temp = self.nums[root]
while root > 0 and self.nums[self.parent(root)] > temp:
self.nums[root] = self.nums[self.parent(root)]
root = self.parent(root)
self.nums[root] = temp
def insert(self,value):
self.nums.append(value)
self.length += 1
self.swim(self.length-1)
def delMin(self,):
self.nums[0] = self.nums[-1]
self.nums.pop()
self.length -= 1
self.sink(0)
@property
def min(self,):
return self.nums[0]
# 测试代码
nums = [49,25,21,25,16,8]
print('\n')
print('原始数组为:')
print(nums)
a = MinHeap(nums)
print('构建好的小顶堆:')
print(a.nums)
print('最小值为:')
print(a.min)
print('插入元素2: ')
a.insert(2)
print(a.nums)
print('最小值为:')
print(a.min)
print('删除最小值后:')
a.delMin()
print(a.nums)
print('最小值为:')
print(a.min)
堆排序
此处讲升序排序,堆排序是基于最大堆进行的排序方法,其时间复杂度为
O
(
N
l
o
g
N
)
O(NlogN)
O(NlogN),额外空间复杂度为
O
(
1
)
O(1)
O(1),同时兼具了归并排序和插入排序列的优点(分别是时间复杂度
O
(
N
l
o
g
N
)
O(NlogN)
O(NlogN)和额外空间复杂度为
O
(
1
)
O(1)
O(1))。
其思想为:
先构建一个长度为N的大顶堆,然后交换堆顶和堆底的元素,将前N-1个元素看作一个大小为N-1的大顶堆重新调整(即对堆顶进行sink),然后继续交换堆顶和堆底,以此类推,最后即完成了升序排序。
class HeapSort:
def __init__(self,nums):
self.nums = nums
self.length = len(nums)
for i in reversed(range(self.parent(self.length-1)+1)):
self.sink(i)
def left(self,root):
return 2 * root + 1
def right(self,root):
return 2 * root + 2
def parent(self,root):
return (root - 1) // 2
def sink(self,root):
temp = self.nums[root]
while self.left(root) < self.length:
older = self.left(root)
if self.right(root) < self.length and self.nums[self.right(root)] > self.nums[older]:
older = self.right(root)
if self.nums[older] <= temp:
break
self.nums[root] = self.nums[older]
root = older
self.nums[root] = temp
def heap_sort(self,):
# 获取长度
length = self.length
for i in reversed(range(1,length)):
self.nums[0],self.nums[i] = self.nums[i],self.nums[0]
# 堆的大小减一
self.length -= 1
self.sink(0)
# 恢复堆的真实长度
self.length = length
# 测试代码
nums = [21,25,49,25,16,8]
print('\n')
print('原始数组为:')
a = HeapSort11(nums)
print('构建好的大顶堆:')
print(a.nums)
a.heap_sort()
print('堆排序后:')
print(a.nums)
如果想要降序排序,只需修改大顶堆为小顶堆即可。