优先队列与堆排序的个人笔记

优先队列–堆 有什么用

许多应用程序到需要处理有序的元素,但有时候不一定要完全有序,或者不一定一次就将无序数组变为完全有序。很多时候,只需要数组中最大的元素。将数组使用排序算法,变成完全有序后在寻找最大元素,当数据不大的时候,固然是可行的。但一旦数据多起来,所花费的时间的不可以接受的。这时优先队列就发挥作用了。

优先队列所提供的功能

优先队列是一个API(抽象数据类型)
支持操作:

  1. 删除最大元素
  2. 插入元素

问题:输入N个字符串,每一个字符串都对应一个整数,需要找出最大(或者最小)M个整数
《算法》第四版

面对这种问题,只要我们能够高效的实现优先队列,解决这种问题very easy。

优先队列的实现

实现优先队列有很多方法:

  1. 数组实现(无序)
  2. 数组实现(有序)
  3. 链表实现
  4. 堆实现(最重要)

堆的定义

需要明白二叉树,完全二叉树相关概念:
二叉树的结点的值一定要 >= 两个子节点的值(堆有序)。每一个结点的值要<=父结点的值(如果存在父节点)。可推出,根结点是二叉树中值最大的值。
二叉堆
在这里插入图片描述

表示二叉堆可以用结点+指针,但如果是完全二叉树的话,表示就可以用数组。上图也是一个完全二叉树。
将二叉树的结点按照层级顺序放入数组
[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]

  1. 对数列进行对有序化
    [0, 18, 9, 10, 1, 8, 6]
  2. 将根结点和末尾一项进行交换,可以看到最后一项的位置已经确定[0, 6, 9, 10, 1, 8, 18]
  3. 对数列除最后一项进行有序化 [0, 10, 9, 8, 0, 1, 6, 18],然后再跟除倒数第二项进行交换 [0, 6, 9, 8, 0, 1, 10, 18]。
  4. 重复上述操作,直到根结点和末尾项相同。完成排序[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())

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值