堆的实现与优先队列

原创 2016年08月31日 09:34:18

堆(Heap)这种数据结构对我们来说是极为有用的。因为它可以非常方便地帮我们完成高效的动态的排序。接下来,我将给出堆的原理以及实现堆的具体操作。


先看一下堆的结构,在此,只需要牢记两点:

1. 堆是一棵完全二叉树

2. 堆中每个节点都大于等于其任何子节点


所谓完全二叉树,一定要满足这个特征:这棵树除最后一层外,每一层都是满的,且最后一层如果不满,所有节点都位于最左边。看下图就明白了:


其中,左图是堆,而右图则不是。



所以,如果比较形象地描述堆的话,就是“不留空隙地将元素放入二叉树中”。


正因为堆跟普通的二叉树比有这样的特点,所以,我们可以以按层遍历的顺序将这个二叉树的所有节点存储在一个列表当中。比如上面的左图,用列表存储就是:[A, B, C, D, E, F],为什么可以这样做呢,因为堆的性质保证了在给出一个节点索引的前提下,我们能通过公式推出其左孩子,右孩子,以及其父节点(如果有的话)的索引。公式如下:


left_child_index = (2 * cur) + 1
right_child_index = (2 * cur) + 2
par = (cur - 1) // 2

其中,cur为当前节点的索引。验证一下就知道是没问题的,比如现在扫描到节点A,索引为0,左右孩子B, C的索引分别是1, 2,正好1 = 2 * 0 + 1;2 = 2 * 0 + 2,而B, C的父节点为A:(1 - 1) // 2 = 0, (2 - 1) // 2 = 0


而且,数组存储堆肯定比用节点 + 指针的方式要更高效,无论在从空间还是时间上。


现在,我们来看堆的第二个特点:所有节点都大于他的子节点。这个特点保证了在堆的同一棵子树当中,越大的节点越在上层。显然,这也保证了堆的最大节点就是二叉树的根节点。我们把这样的堆成为“最大堆”,若是所有节点的值都比它的孩子小,我们则称之为“最小堆”,当然,一般情况下,不加特别说明的堆都指最大堆。本文当中也是如此,因为最大最小并不影响对堆的原理的分析。


此时,用堆排序的逻辑已经浮现了:假设现在有一个已经建好的堆,堆中一共有n个节点,我们可以先将堆的根节点(放到列表里面来看,就是列表的首元素)删除,这当然就是n个节点当中,值最大的。然后按照堆的规律重新对剩下的节点构建成新的堆(此时新堆中只有n - 1个节点了),再将这个新堆的根节点删除,得到之前n个节点中值第二大的。。。依次类推,最终可以按照值从大到小的顺序输出这n个节点。


具体的删除方法如下所示:显然,删除堆的根节点是由节点删除和堆重构两部分组成的。


1. 将堆的根节点与最后一层叶子节点中的最右端的节点交换位置

2. 将最后这个节点(也就是之前的根节点)删除

3. 比较现在的新的根节点与他的两个孩子的大小,若比他的两个孩子中最大的孩子小,则将这个节点与最大的孩子交换。

4. 反复执行第3步,直到不能执行为止,新堆构建完毕


3, 4两步是重构堆的操作,保证了父亲节点始终比孩子要大。当然,若你建立的是最小堆,那么反过来,保证父亲比孩子节点小就行。

以上操作也可以用下图表示:



仿照这种上下交换的原理,也可以设计出将节点添加到堆中的算法:

1. 将需要添加的节点放置在堆的最末端。如果不满,则放在最后一层的最右端;如果满了,则放置在最后一层下一层的最左端(相当于是位二叉树新建了一层)

2. 若这个节点的值比它的父亲大,则与其父亲交换,直到不能交换为止(交换到了根节点或者比他的父亲小了)


有了添加和删除两个算法,堆的构建就完整了。前面我们已经说过堆的特点使得它方便用列表来表示,接下来就看看如何具体使用列表实现堆。


我是建立了一个堆的类,一个数据成员:vector为列表,存储堆中节点;两个方法:pop()删除根节点(也就是列表首元素),add()为堆添加元素


代码如下:我设计的方法是在建立堆类时,将一个列表作为参数

class Heap(object):
    def __init__(self, elements):
        self.vector = []
        for element in elements:
            self.add(element)

    def add(self, element):

        # 添加新元素到列表末尾
        self.vector.append(element)

        # 找到新添加节点的父亲
        cur = len(self.vector) - 1
        par = (cur - 1) // 2

        # 逐层交换
        while cur != 0 and self.vector[cur] > self.vector[par]:
            self.vector[par], self.vector[cur] = self.vector[cur], self.vector[par]
            cur = par
            par = (cur - 1) // 2

    def pop(self):

        # Next决定是否还需要进行下一步交换
        Next = True

        # 首尾交换
        self.vector[0], self.vector[-1] = self.vector[-1], self.vector[0]

        # 将尾元素(其实是根节点)删除出来
        result = self.vector.pop()

        cur = 0

        # 我们认为只要这一步进行交换了,且还能交换(当前节点还有孩子),就继续交换
        while cur < len(self.vector) and Next:
            Next = False

            # 找左右孩子的索引
            left_child, right_child = (2 * cur) + 1, (2 * cur) + 2

            # 左孩子索引越界,交换终止
            if left_child >= len(self.vector):
                break

            # 右孩子存在
            if right_child < len(self.vector):

                # max_index:较大孩子的索引
                max_index = right_child if self.vector[left_child] < self.vector[right_child] else left_child
                if self.vector[cur] < self.vector[max_index]:
                    self.vector[cur], self.vector[max_index] = self.vector[max_index], self.vector[cur]
                    cur = max_index
                    Next = True

            # 右孩子不存在
            elif self.vector[cur] < self.vector[left_child]:
                self.vector[cur], self.vector[left_child] = self.vector[left_child], self.vector[cur]
                cur = left_child
                Next = True

        # 返回被删除的值
        return result

既然堆有这种最大先出的性质,那么利用堆,就可以构建优先队列。大家知道普通的队列是“先进先出”的原则,而优先队列则是“最大先出”的原则,也就是说,队列中的每一个元素都有一个权重,权重大的最先出队。这种思路最常见的应用是医院排队,一种原则是让病情更严重的患者最先就诊,那如果按照这个原则设计一个就医系统的话,优先队列就是最恰当的选择。


一样的,我尝试建立一个优先队列的类,代码如下:这里稍微改动一下,让优先队列的构造函数__init__()没有参数,只能通过添加元素往队列中添加

import heap


class PriorityQueue(object):
    def __init__(self):
        self.record = heap.Heap([])

    def add_queue(self, element):
        self.record.add(element)

    def pop_queue(self):
        return self.record.pop()

    def size(self):
        return len(self.record.vector)

数据成员record是一个堆,三个函数,分别用于添加,删除和返回队列大小。


版权声明:本文为博主原创文章,未经博主允许不得转载。

相关文章推荐

优先队列算法实现(Java)

HDU 4006 The kth great number(优先队列、堆实现)

/* 题意:"I"表示输入数据,"Q"表示输出第k大数据 题解:优先队列,从大到小,队列中只需要k个元素。每次输出最小的即可 我用小顶堆实现,如果插入元素比tree[1]小,则直接舍掉。否则,更新。...

[算法学习笔记]基于最大堆实现最大优先队列

何为优先队列 优先队列是计算机科学中的一类抽象数据类型。优先队列中的每个元素都有各自的优先级,优先级最高的元素最先得到服务;优先级相同的元素按照其在优先队列中的顺序得到服务。优先队列往往用堆来实现...

简单优先队列实现-基于最小堆

一、什么是优先队列优先队列不是按照普通对象先进先出原FIFO则进行数据操作,其中的元素有优先级属性,优先级高的元素先出队。本文提到的优先队列,是基于最小堆原理实现。 二、什么是最小堆最小堆是一个完全二...

利用优先队列实现堆排序(自顶向下自底向上堆化完全二叉树的运用)

源代码如下: #include #include typedef struct Item *node; struct Item{ int data; char c; }; stati...

算法(第四版)学习笔记之java实现基于堆的优先队列

一台电脑之所以能同时运行多个应用程序的时候,是通过为每个应用程序的事件分配一个优先级,并总是处理下一个优先级最高的事件来实现的。在这种情况下,一个合适的数据结构应该支持两种操作:删除最大元素和插入元素...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:深度学习:神经网络中的前向传播和反向传播算法推导
举报原因:
原因补充:

(最多只允许输入30个字)