优先队列–堆 有什么用
许多应用程序到需要处理有序的元素,但有时候不一定要完全有序,或者不一定一次就将无序数组变为完全有序。很多时候,只需要数组中最大的元素。将数组使用排序算法,变成完全有序后在寻找最大元素,当数据不大的时候,固然是可行的。但一旦数据多起来,所花费的时间的不可以接受的。这时优先队列就发挥作用了。
优先队列所提供的功能
优先队列是一个API(抽象数据类型)
支持操作:
- 删除最大元素
- 插入元素
问题:输入N个字符串,每一个字符串都对应一个整数,需要找出最大(或者最小)M个整数
《算法》第四版
面对这种问题,只要我们能够高效的实现优先队列,解决这种问题very easy。
优先队列的实现
实现优先队列有很多方法:
- 数组实现(无序)
- 数组实现(有序)
- 链表实现
- 堆实现(最重要)
堆的定义
需要明白二叉树,完全二叉树相关概念:
二叉树的结点的值一定要 >= 两个子节点的值(堆有序)。每一个结点的值要<=父结点的值(如果存在父节点)。可推出,根结点是二叉树中值最大的值。
二叉堆
表示二叉堆可以用结点+指针,但如果是完全二叉树的话,表示就可以用数组。上图也是一个完全二叉树。
将二叉树的结点按照层级顺序放入数组
[10, 8, 9, 6, 5]
特别的:一棵含N个结点的完全二叉树高度为
l
g
N
lgN
lgN
一般而言我们会声明一个长度为N+1的数组(名字为nums)用来存放含N个结点的二叉堆。nums[0]不会使用,从nums[1]开始存放根结点。[0, 10, 8, 9, 6, 5]。
用数组表示法如何访问当前结点的父结点?
假设目前数组的指针 4 指向是6(即数组的末尾项,不要忘了,新增了首项0)如何访问他的父结点呢?数组的指针 / 2 (如果有小数则直接略去小数)(4 / 2 = 2),就可以得到数组的第二项就是8,即6的父结点。这也是为什么我们要新增nums[0],而不使用。
由下到上的堆有序化(上浮)
假设现在而二叉堆里,来了一个新的数字,我们首先将他放到数组末尾。
假设是11,目前数组为:[0, 10,8,9,6,5,11]
很显然11的到来打乱了堆的状态,现在我们要求11上浮到适合他的位置。
我们只需要访问当前结点的父结点是否要小于当前结点的值。如果小于,则交换,然后再访问父结点的父结点,重复执行以上操作,直到替换到根结点或者父结点的值 > 当前结点。
了解用数组表示法如何访问当前结点的父结点? 后实现该函数很简单。
上浮函数实现:
"""
函数名: 上浮函数
参数:
nums 堆数组
k 目前堆数组里需要上浮的函数的指针
"""
def swim(nums, k):
temp = int(k / 2)
while k > 1 and nums[temp] < nums[k]:
nums[k], nums[temp] = nums[temp], nums[k]
k = temp
temp = int(k / 2)
if __name__ == "__main__":
test = [0, 10, 8, 9, 6, 5, 11]
swim(test, 6)
print(test)
# 结果
[0, 11, 8, 10, 6, 5, 9]
由上到下的堆有序化(下沉)
当然如果堆数列中存在父结点的值比子结点要小,那自然父结点要往下走。
用数组表示法如何访问当前结点的子结点?
位置为k的结点的子结点位于2k,2k+1。当然可能不存或者只存在一个(2k)
假设数组为[0, 10,6,9,8,5],6明显不符合堆。
步骤:6的位置为2,2*2=4,子结点位置为4,位置为4的元素为8,6< 8符合条件,6与8进行交换。然后当6到达位置4时,如果还有子结点则再进行检查和交换。
代码实现:
"""
函数名: 下沉函数
参数:
nums 堆数组
k 目前堆数组里需要下沉的函数的指针
"""
def sink(nums, k):
length = len(nums)
while 2 * k <= length:
j = 2 * k
# 左子结点的值要小于有子结点的值,那么当前结点的值只需要跟其中大的值比较即可
if j < length and nums[j] < nums[j + 1]:
j += 1
# 如果比大的值要小,那么要进行交换,
if nums[j] > nums[k]:
nums[j], nums[k] = nums[k], nums[j]
else:
break
if __name__ == "__main__":
test = [0, 10, 6, 9, 8, 5]
sink(test, 2)
print(test)
# 结果
[0, 10, 8, 9, 6, 5]
插入元素:
先将新元素加入到元素末尾,然后调用上浮函数,上浮到合适的位置。
删除最大元素:
根结点是堆中最大的元素,那么先将根结点和末尾结点互换,删除当前末尾结点(根结点的值),然后再对当前的根结点(末尾结点的值)执行下沉操作。
堆排序
当实现了堆的有序化,插入元素和删除最大元素两个功能的时候,堆排序就十分简单了。因为堆的特性,我们的根结点就是最大值。紧紧抓住这一点。
以一个数列为例子:[1, 10, 6, 9, 8, 18]
首先 在数列首项插入一个0 [0, 1, 10, 6, 9, 8, 18]
- 对数列进行对有序化
[0, 18, 9, 10, 1, 8, 6] - 将根结点和末尾一项进行交换,可以看到最后一项的位置已经确定[0, 6, 9, 10, 1, 8, 18]
- 对数列除最后一项进行有序化 [0, 10, 9, 8, 0, 1, 6, 18],然后再跟除倒数第二项进行交换 [0, 6, 9, 8, 0, 1, 10, 18]。
- 重复上述操作,直到根结点和末尾项相同。完成排序[0, 1, 6, 8, 9,10, 18]
堆排序代码实现:
我将其包装成了类。
"""
优先队列类
作用: 将传入的数组变成一个优先队列
参数:
nums: 数组
方法: 1.删除最大元素
2.插入元素
"""
class Priority_queue(object):
def __init__(self, nums):
self.nums = nums
self.nums.insert(0, 0)
self.length = len(nums)
# 将数列堆化
self.__init_nums()
def __str__(self):
return str(self.nums)
"""
函数名: 上浮函数
参数:
nums 堆数组
k 目前堆数组里需要上浮的函数的指针
"""
def __swim(self, k):
temp = int(k / 2)
while k > 1 and self.nums[temp] < self.nums[k]:
self.nums[k], self.nums[temp] = self.nums[temp], self.nums[k]
k = temp
temp = int(k / 2)
"""
函数名: 下沉函数
参数:
nums 堆数组
k 目前堆数组里需要下沉的函数的指针
"""
def __sink(self, k):
while 2 * k <= self.length - 1:
j = 2 * k
# 左子结点的值要小于有子结点的值,那么当前结点的值只需要跟其中大的值比较即可
if j < self.length - 1 and self.nums[j] < self.nums[j + 1]:
j += 1
# 如果比大的值要小,那么要进行交换,
if self.nums[j] > self.nums[k]:
self.nums[j], self.nums[k] = self.nums[k], self.nums[j]
else:
break
"""
函数名: 将数列初始堆化
"""
def __init_nums(self):
for i in range(1, self.length):
self.__swim(i)
"""
函数名: 插入元素
"""
def insert(self, number):
self.nums.append(number)
self.length += 1
self.__swim(self.length - 1)
"""
函数名: 删除最大元素
返回值: 最大元素
"""
def delete_max(self):
# 根结点和数列末尾项互换
self.nums[self.length - 1], self.nums[1] = self.nums[1], self.nums[self.length - 1]
# 删除尾结点
re = self.nums.pop()
# 命令当前根结点下沉
self.__sink(1)
return re
"""
函数名: 堆排序
逻辑: 先进行堆的有序化(上浮or下沉),之后当前的根结点是最大值,然后根结点从数列的最后一项进行交换,那么最大的元素已经放置在数列末尾.
然后对除数列最后一项,使用有序化. 重复直到根结点就是最后一项.
"""
def sort(self):
for i in range(1, self.length):
# 当前的根结点是最大值,然后根结点从数列的最后一项进行交换
self.nums[self.length - i], self.nums[1] = self.nums[1], self.nums[self.length - i]
# 重新进行堆的有序化
for j in range(1, self.length - i):
self.__swim(j)
self.nums.pop(0)
return self.nums
if __name__ == "__main__":
test = [6, 9, 10, 1, 8]
pri = Priority_queue(test)
print(pri.sort())