优先队列实现原理分析

原文出处: ziwenxie

优先队列是在实际工程中被广泛应用的一种数据结构,不管是在操作系统的进程调度中,还是在相关的图算法比如Prim算法和Dijkstra算法中,我们都可以看到优先队列的身影,本文我们就来分析一下优先队列的实现原理。

优先队列

以操作系统的进程调度为例,比如我们在使用手机的过程中,手机分配给来电的优先级都会比其它程序高,在这个业务场景中,我们不要求所有元素全部有序,因为我们需要处理的只是当前键值最大的元素(优先级最高的进程)。在这种情况下,我们需要实现的只是删除最大的元素(获取优先级最高的进程)和插入新的元素(插入新的进程),这种数据结构就叫做优先队列。

我们先来定义一个优先队列,下面我们将使用pq[]来保存相关的元素,在构造函数中可以指定堆的初始化大小,如果不指定初始化大小值,默认初始化值为1。p.s: 在下面我们会实现相关的resize()方法用来动态调整数组的大小。


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

public class MaxPQ<Key> implements Iterable<Key> {

    private Key[] pq;                    // store items at indices 1 to n

    private int n;                       // number of items on priority queue

    private Comparator<Key> comparator;  // optional Comparator

    /**

     * Initializes an empty priority queue with the given initial capacity.

     *

     * @param  initCapacity the initial capacity of this priority queue

     */

    public MaxPQ(int initCapacity) {

        pq = (Key[]) new Object[initCapacity + 1];

        n = 0;

    }

    /**

     * Initializes an empty priority queue.

     */

    public MaxPQ() {

        this(1);

    }

}

堆的基本概念

在正式进入优先队列分析之前,我们有必要先了解一下对于堆的相关操作。我们定义当一棵二叉树的每个结点都要大于等于它的两个子结点的时候,称这棵二叉树堆有序。如下图就是一棵典型的堆有序的完全二叉树。

堆上浮和下沉操作

对了保证堆有序,对于堆我们要对它进行上浮和下沉操作,我们先来实现两个常用的工具方法,其中less()用于比较两个元素的大小,exch()用于交换数组的两个元素:


1

2

3

4

5

6

7

8

9

10

11

12

13

private boolean less(int i, int j) {

    if (comparator == null) {

        return ((Comparable<Key>) pq[i]).compareTo(pq[j]) < 0;

    }

    else {

        return comparator.compare(pq[i], pq[j]) < 0;

    }

}

private void exch(int i, int j) {

    Key swap = pq[i];

    pq[i] = pq[j];

    pq[j] = swap;

}

上浮操作

根据下图我们首先来分析一下上浮操作,以swim(5)为例子,我们来看一下上浮的过程。对于堆我们进行上浮的目的是保持堆有序性,即一个结点的值大于它的子结点的值,所以我们将a[5]和它的父结点a[2]相比较,如果它大于父结点的值,我们就交换两者,然后继续swim(2)。

具体的实现代码如下:


1

2

3

4

5

6

private void swim(int k) {

    while (k > 1 && less(k/2, k)) {

        exch(k, k/2);

        k = k/2;

    }

}

下沉操作

根据下图我们来分析一下下沉操作,以sink(2)为例子,我们先将结点a[2]和它两个子结点中较小的结点相比较,如果小于子结点,我们就交换两者,然后继续sink(5)。

具体的实现代码如下:


1

2

3

4

5

6

7

8

9

private void sink(int k) {

    while (2*k <= n) {

        int j = 2*k;

        if (j < n && less(j, j+1)) j++;

        if (!less(k, j)) break;

        exch(k, j);

        k = j;

    }

}

实现

我们来分析一下插入一个元素的过程,如果我们要在堆中新插入一个元素S的话,首先我们默认将这个元素插入到数组中pq[++n] 中(数组是从1开始计数的)。当我们插入S后,打破了堆的有序性,所以我们采用上浮操作来维持堆的有序性,当上浮操作结束之后,我们依然可以保证根结点的元素是数组中最大的元素。

接下来我们来看一下删除最大元素的过程,我们首先将最大的元素a[1]和a[n]交换,然后我们删除最大元素a[n],这个时候堆的有序性已经被打破了,所以我们继续通过下沉操作来重新维持堆的有序性,保持根结点元素是所有元素中最大的元素。

插入的实现代码如下:


1

2

3

4

5

6

7

8

9

10

11

12

13

/**

 * Adds a new key to this priority queue.

 *

 * @param  x the new key to add to this priority queue

 */

public void insert(Key x) {

    // double size of array if necessary

    if (n >= pq.length - 1) resize(2 * pq.length);

    // add x, and percolate it up to maintain heap invariant

    pq[++n] = x;

    swim(n);

    assert isMaxHeap();

}

删除的实现代码如下:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

/**

 * Removes a maximum key and returns its associated index.

 *

 * @return an index associated with a maximum key

 * @throws NoSuchElementException if this priority queue is empty

 */

public Key delMax() {

    if (isEmpty()) throw new NoSuchElementException("Priority queue underflow");

    Key max = pq[1];

    exch(1, n);

    n--;

    sink(1);

    pq[n+1] = null;     // to avoid loiterig and help with garbage collection

    if ((n > 0) && (n == (pq.length - 1) / 4)) resize(pq.length / 2);

    assert isMaxHeap();

    return max;

}

上面我们在insert()过程中用到了resize()函数,它用于动态数组的大小,具体的实现代码如下:


1

2

3

4

5

6

7

8

9

10

11

12

// helper function to double the size of the heap array

private void resize(int capacity) {

    assert capacity > n;

    Key[] temp = (Key[]) new Object[capacity];

    for (int i = 1; i <= n; i++) {

        temp[i] = pq[i];

    }

    pq = temp;

}

public boolean isEmpty() {

    return n == 0;

}

而isMaxHeap()则用于判断当前数组是否满足堆有序原则,这在debug的时候非常的有用,具体的实现代码如下:


1

2

3

4

5

6

7

8

9

10

11

12

13

// is pq[1..N] a max heap?

private boolean isMaxHeap() {

    return isMaxHeap(1);

}

// is subtree of pq[1..n] rooted at k a max heap?

private boolean isMaxHeap(int k) {

    if (k > n) return true;

    int left = 2*k;

    int right = 2*k + 1;

    if (left  <= n && less(k, left))  return false;

    if (right <= n && less(k, right)) return false;

    return isMaxHeap(left) && isMaxHeap(right);

}

到此我们的优先队列已经差不多完成了,注意我们上面实现了Iterable<Key>接口,所以我们来实现iterator()方法:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

/**

 * Returns an iterator that iterates over the keys on this priority queue

 * in descending order.

 * The iterator doesn‘t implement remove() since it‘s optional.

 *

 * @return an iterator that iterates over the keys in descending order

 */

public Iterator<Key> iterator() {

    return new HeapIterator();

}

private class HeapIterator implements Iterator<Key> {

    // create a new pq

    private MaxPQ<Key> copy;

    // add all items to copy of heap

    // takes linear time since already in heap order so no keys move

    public HeapIterator() {

        if (comparator == null) copy = new MaxPQ<Key>(size());

        else                    copy = new MaxPQ<Key>(size(), comparator);

        for (int i = 1; i <= n; i++)

            copy.insert(pq[i]);

    }

    public boolean hasNext()  { return !copy.isEmpty();                     }

    public void remove()      { throw new UnsupportedOperationException();  }

    public Key next() {

        if (!hasNext()) throw new NoSuchElementException();

        return copy.delMax();

    }

}

堆排序

将上面的优先队列稍微做一下改进,我们便可以实现堆排序,即对pq[]中的元素进行排序。对于堆排序的具体实现,下面我们分为两个步骤:

  • 首先我们先来构造一个堆。
  • 然后通过下沉的方式进行排序。

堆排序的实现代码非常的简短,我们首先来看一下具体的代码实现,然后我们再具体分析它的实现原理:


1

2

3

4

5

6

7

8

9

10

11

12

13

/**

 * Rearranges the array in ascending order, using the natural order.

 * @param pq the array to be sorted

 */

public static void sort(Comparable[] pq) {

    int n = pq.length;

    for (int k = n/2; k >= 1; k--)

        sink(pq, k, n);

    while (n > 1) {

        exch(pq, 1, n--);

        sink(pq, 1, n);

    }

}

首先我们来看一下堆的构造过程(下图中的左图)。我们采用的方法是从右至左用sink()方法构造子堆。我们只需要扫描数组中的一半元素,即5, 4, 3, 2, 1。这样通过这几个步骤,我们可以得到一个堆有序的数组,即每个结点的大小都大于它的两个结点,并使最大元素位于数组的开头。

接下来我们来分析一下下沉排序的实现(下图中的右图),这里我们采取的方法是每次都将最大的元素删除,然后重新通过sink()来维持堆有序,这样每一次sink()操作我们都可以的到数组中最大的元素。

Referencs

ALGORITHM-4TH

from: http://www.importnew.com/25306.html

时间: 2024-08-10 23:30:48

优先队列实现原理分析的相关文章

kafka producer实例及原理分析

1.前言 首先,描述下应用场景: 假设,公司有一款游戏,需要做行为统计分析,数据的源头来自日志,由于用户行为非常多,导致日志量非常大.将日志数据插入数据库然后再进行分析,已经满足不了.最好的办法是存日志,然后通过对日志的分析,计算出有用的数据.我们采用kafka这种分布式日志系统来实现这一过程. 步骤如下: 搭建KAFKA系统运行环境 如果你还没有搭建起来,可以参考我的博客: http://zhangfengzhe.blog.51cto.com/8855103/1556650 设计数据存储格式

android脱壳之DexExtractor原理分析[zhuan]

http://www.cnblogs.com/jiaoxiake/p/6818786.html内容如下 导语: 上一篇我们分析android脱壳使用对dvmDexFileOpenPartial下断点的原理,使用这种方法脱壳的有2个缺点: 1.  需要动态调试 2.  对抗反调试方案 为了提高工作效率, 我们不希望把宝贵的时间浪费去和加固的安全工程师去做对抗.作为一个高效率的逆向分析师, 笔者是忍不了的,所以我今天给大家带来一种的新的脱壳方法——DexExtractor脱壳法. 资源地址: Dex

android脱壳之DexExtractor原理分析

导语: 上一篇我们分析android脱壳使用对dvmDexFileOpenPartial下断点的原理,使用这种方法脱壳的有2个缺点: 1.  需要动态调试 2.  对抗反调试方案 为了提高工作效率, 我们不希望把宝贵的时间浪费去和加固的安全工程师去做对抗.作为一个高效率的逆向分析师, 笔者是忍不了的,所以我今天给大家带来一种的新的脱壳方法--DexExtractor脱壳法. 资源地址: DexExtractor源码:https://github.com/bunnyblue/DexExtracto

Adaboost算法原理分析和实例+代码(简明易懂)

Adaboost算法原理分析和实例+代码(简明易懂) [尊重原创,转载请注明出处] http://blog.csdn.net/guyuealian/article/details/70995333     本人最初了解AdaBoost算法着实是花了几天时间,才明白他的基本原理.也许是自己能力有限吧,很多资料也是看得懵懵懂懂.网上找了一下关于Adaboost算法原理分析,大都是你复制我,我摘抄你,反正我也搞不清谁是原创.有些资料给出的Adaboost实例,要么是没有代码,要么省略很多步骤,让初学者

Android视图SurfaceView的实现原理分析

附:Android控件TextView的实现原理分析 来源:http://blog.csdn.net/luoshengyang/article/details/8661317 在Android系统中,有一种特殊的视图,称为SurfaceView,它拥有独立的绘图表面,即它不与其宿主窗口共享同一个绘图表面.由于拥有独立的绘图表面,因此SurfaceView的UI就可以在一个独立的线程中进行绘制.又由于不会占用主线程资源,SurfaceView一方面可以实现复杂而高效的UI,另一方面又不会导致用户输

AbstractQueuedSynchronizer的介绍和原理分析(转)

简介 提供了一个基于FIFO队列,可以用于构建锁或者其他相关同步装置的基础框架.该同步器(以下简称同步器)利用了一个int来表示状态,期望它能够成为实现大部分同步需求的基础.使用的方法是继承,子类通过继承同步器并需要实现它的方法来管理其状态,管理的方式就是通过类似acquire和release的方式来操纵状态.然而多线程环境中对状态的操纵必须确保原子性,因此子类对于状态的把握,需要使用这个同步器提供的以下三个方法对状态进行操作: java.util.concurrent.locks.Abstra

linux中mmap系统调用原理分析与实现

参考文章:http://blog.csdn.net/shaoguangleo/article/details/5822110 linux中mmap系统调用原理分析与实现 1.mmap系统调用(功能)      void* mmap ( void * addr , size_t len , int prot , int flags ,int fd , off_t offset )      内存映射函数mmap, 负责把文件内容映射到进程的虚拟内存空间, 通过对这段内存的读取和修改,来实现对文件的

Android 4.4 KitKat NotificationManagerService使用详解与原理分析(一)__使用详解

概况 Android在4.3的版本中(即API 18)加入了NotificationListenerService,根据SDK的描述(AndroidDeveloper)可以知道,当系统收到新的通知或者通知被删除时,会触发NotificationListenerService的回调方法.同时在Android 4.4 中新增了Notification.extras 字段,也就是说可以使用NotificationListenerService获取系统通知具体信息,这在以前是需要用反射来实现的. 转载请

一个日期算法的原理分析

1.问题描述 在 OSC 问答频道有一个问题:时间算法:帮忙解答下 简单的复述一遍就是能够通过如下式子来计算month月day日是一年的第几天. 闰年是 day_of_year=(275*month)/9 - (month+9)/12 + day - 30 非闰年比这个少1天.可以简单的验证,这个式子中每个部分计算后都取整,整个结果总是对的. 我们知道1.3.5.7.8.10.12都是31天,2月的天数有点诡异,其他都是30天,正常情况下我们写程序会写很多if来判断月份,进而计算累积的天数.但是