`
frank-liu
  • 浏览: 1667332 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

堆的几个应用

 
阅读更多

简介

    关于堆和PriorityQueue的思想和实现,前面的几篇文章我都有详细的描述,比如堆排序的实现PriorityQueue的实现。在这些实现的基础上,实际上还有很多实际应用中的变体,比如在某些情况下我们会用到多路归并算法,还有一个就是在一些图算法的应用场景里,我们需要一个动态保持最小若干元素的集合。这些东西都离不开堆的思想和它们的应用。这里,我们针对这些变体的实现思路做一个详细的讨论。

 

在讨论我们这个具体应用的数据结构之前,让我们先看看以前一些实现里所面临的问题。

 

存在的几个问题

多路归并算法

    在前面的一些文章里,我们提到。针对一些大数据文件的处理和归并,少不了要使用多路归并算法来实现。在多路归并算法里,它的基本思路如下:

1. 首先输入是一个输入流数组比如In[],假设长度为k。 从这k个流里每个都取一个元素来构建一个大小为k的最小堆。

2. 每次取出最小堆里顶端的元素,然后再从最顶端元素对应流里取出下一个元素来加入到堆中,并调整堆。如果最小元素对应的流已经读取空了则直接调整堆。

3. 重复过程2直到所有流元素都处理完毕。

    在我们具体实现的时候,可能就会有这么一系列的问题。首先一个就是我们给定的输入流里,每个流是对应着这个数组里的某个索引的。但是我们建的这个堆是要经常做动态调整,比如说我们前面流In[1]里的元素比In[2]里读出来的元素大,那么它们就需要交换位置。在堆里交换位置之后,我们怎么能让In[1]的这个元素和In[1]这个流对上号呢?所以,我们就需要知道,当我们给定流数组里某个流的时候,比如In[2],我们需要知道它所对应的那个读出的元素在堆里的位置。

    另外,假设我们堆经过一系列的排序调整,最终得到最小的元素是堆顶的那个。它在堆里对应的索引根据我们的实现可能是queue[0]或者queue[1]。按照前面的过程,我们需要将它移除,同时再找到它对应的流。因为还要从这个对应的流里去读后续的内容来加入到堆里。那么,我们怎么去找呢?所以,这里很可能也需要有一个对应的关系。

 

Dijkstra算法

    在我前面的一篇文章里有提到过该算法。这个算法有一个思路就是从一个源节点开始根据关联关系保存一个到该节点距离的最小堆,每次当有新的边和节点加入进来的时候,要根据新加入节点和原有节点的距离以及原来推算的节点间距离进行比较,然后碰到合适的距离要更新整个堆。这就好比是更新了堆中间的某个元素。只是相对来说是让一个节点的元素变得更小了。默认的优先队列里并没有更新元素值然后自动调整堆的方法支持。同时,也和前面一样的问题,我们新加入元素和距离更新之后,会调整整个堆的大小。这里对应的堆元素是对应到哪个节点呢?

    所以,根据前面这两个部分反映出来的问题,我们至少要保证修改后的堆结构要支持一下几个功能。

1. 根据堆里的元素索引能找到对应的源输入流索引。

2. 根据对应的输入流索引可以找到对应的堆元素索引。

3. 可以动态调整堆里任意元素的值并进行调整,而不仅仅只是堆顶和新加入到堆尾的元素。

    于是,有了这几个需要增强的点,我们来考虑如下的增强堆结构:

 

IndexMinPQ

    这是我们要介绍的变体,当然,一开始说这个东西的时候我们可能会有点疑惑,这个数据结构到底是什么样的呢?它和我们常用的PriorityQueue有什么区别呢?我们这里就一一比对过来。

 

结构

    我们传统的堆结构本质上是一个数组,它只是以一种二叉树的样式来处理和访问。所以假设我们有如下的二叉树:

     它对应的存储数组则如下图所示:

     根据我们前面讨论的问题描述,对于一个堆来说,它里面的元素会动态调整,而对应的数据流却是相对固定的,所以我们需要有一个表来保存它们之间的映射关系。

 于是我们这里就有一个表pq, 其中pq定义为int[]的数组,它保存的是指向具体堆元素的索引。它是一个形式上的最小堆,不过它里面的元素只是索引而已。具体参加比较的还是对应的堆元素。因为中间有了这么一道转换,看起来稍微有点困难。我们可以这样来理解。假设我们要构成前面示例中的最小堆,它的二叉树结构里应该保存的样式是上图那样的。但是,因为我们考虑到具体的参加比较的堆元素如果变动位置的话比较麻烦,就让它们的位置不动。也就是说,前面的线性结构元素应该是上图那样的,但是因为我们这里不让堆元素动,所以它可能本身存的是如下的这样一个数组结构:

    但是因为我们需要构成一个最小堆,所以我们这里只能通过一个间接指向这些元素的一个引用数组来调整了。这就是我们所说的int[] pq这个数组。比如说以这个元素数组为例,对应的int[] pq数组则应该如下(假定我们对应的数组下标是从1开始):

 

    显示成一个线性数组的样式则如下:

     通过这样的映射关系,当我们要取堆最小的元素,则首先取得pq[1],这是对应的最小的堆元素所在下标,然后再从堆元素所在的数组key[]里取。对应的是key[pq[1]]。通过我们建立的这个数组pq,我们可以很容易的根据堆形式的结构来找到具体堆的元素。

    另外,因为我们堆会经常调整,比如有时候我们需要更新堆元素的值,因为堆的元素发生了变化,它就需要和它的父节点或者子节点进行比较以及交换。这个时候,因为调整的是pq的值,但是当我们比如说key[3]发生了变化。那么我们首先需要的还是要找到它对应的堆索引数组里的元素位置。更直白的说就是,我们知道了key[3]发生了变化,但是我们需要找到是pq[]里的哪个元素指向key[3]。所以,为了方便的找到这个对应的关系元素,我们还需要另外一个关系映射表。int[] qp。它保留的关系正好和int[] pq的相反。它们之间应该满足这样的一个关系: qp[pq[i]] = pq[qp[i]] = i

比如以前面的图为例,我们对应的int[] qp则应该如下:

    该怎么来看这个表呢?我们应该从key[]所在的索引来看,比如说当前我在访问的元素是key[2],那么当前的这个key索引2, 再通过qp[2]得到3, 就可以知道它实在对应的映射堆的位置pq[3]。

    其实,就是这两个看起来不起眼的映射在后面的实现里起到非常大的作用。有了这些描述,我们整个的堆数据结构可以定义如下:

public class IndexMinPQ<Key extends Comparable<Key>> implements Iterable<Integer> {
    private int NMAX;        // maximum number of elements on PQ
    private int N;           // number of elements on PQ
    private int[] pq;        // binary heap using 1-based indexing
    private int[] qp;        // inverse of pq - qp[pq[i]] = pq[qp[i]] = i
    private Key[] keys;      // keys[i] = priority of i

    /**
     * Initializes an empty indexed priority queue with indices between 0 and NMAX-1.
     * @param NMAX the keys on the priority queue are index from 0 to NMAX-1
     * @throws java.lang.IllegalArgumentException if NMAX < 0
     */
    public IndexMinPQ(int NMAX) {
        if (NMAX < 0) throw new IllegalArgumentException();
        this.NMAX = NMAX;
        keys = (Key[]) new Comparable[NMAX + 1];    // make this of length NMAX
        pq   = new int[NMAX + 1];
        qp   = new int[NMAX + 1];                   // make this of length NMAX
        for (int i = 0; i <= NMAX; i++) qp[i] = -1;
    }
}

    从代码里面可以看到我们故意多申请一个元素,然后将访问的最小元素设定为下标1。这样在一些求节点的父节点和子节点的时候会稍微简化一些。初始化的时候,一个有意思的地方就是我们将qp里面所有元素都设置成-1。这在后面的一些结构调整方法里有奇效。

 

结构调整

    我们知道,堆里面元素的调整是构建这个堆最基础的条件,在一般的情况下可能包含有向下交换和向上交换。比如说在我们目前的最小堆情况下,当我们新增加一个元素进来的时候,我们可能要进行向上交换。而如果我们将顶点的元素移除并替换成其他节点的时候,就需要向下交换。我们一个个的讨论过来。

    首先,在原来默认堆排序的实现里,如果我们要交换两个节点,只要节点元素换一下就可以了,因为这里基本的元素Key[]不动,那么要交换和调整的就是变动int[] pq和int[] qp了。它的实现如下:

private void exch(int i, int j) {
        int swap = pq[i]; pq[i] = pq[j]; pq[j] = swap;
        qp[pq[i]] = i; qp[pq[j]] = j;
    }

    其中对qp[]的更新利用pq[qp[i]] == i这样的特性,可以很简单的实现。

     那么,对于向下交换的过程,则可以实现如下:

private void sink(int k) {
        while (2*k <= N) {
            int j = 2*k;
            if (j < N && greater(j, j+1)) j++;
            if (!greater(k, j)) break;
            exch(k, j);
            k = j;
        }
    }

    因为以1开始作为下标,每个元素乘以2则表示它的左子节点。这里只要和它的节点比较就可以了。因为我们前面定义的Key是实现Comparable,所以这里greater方法就是Key的比较:

private boolean greater(int i, int j) {
        return keys[pq[i]].compareTo(keys[pq[j]]) > 0;
    }

 

    同理,我们也可以得到向上调整的方法实现:

private void swim(int k)  {
        while (k > 1 && greater(k/2, k)) {
            exch(k, k/2);
            k = k/2;
        }
    }

    结合前面对堆排序等描述的文章,这些方面的理解基本无压力。所以这部分实现的要点就是要每次都注意好更新pq和qp。

    现在,我们再来看看对改进堆的一些操作实现。

 

    我们这里要实现的是对Key[]里面的索引i位置插入元素。因为这个结构可以支持动态对元素的调整。所以要注意检查这个原来的位置是不是已经被占用了。这里的判断是用contains(i)这个方法。

 

public void insert(int i, Key key) {
        if (i < 0 || i >= NMAX) throw new IndexOutOfBoundsException();
        if (contains(i)) throw new IllegalArgumentException("index is already in the priority queue");
        N++;
        qp[i] = N;
        pq[N] = i;
        keys[i] = key;
        swim(N);
    }

     代码里我们因为要新加入一个元素到堆中,对应的堆结构映射其实是相当于添加到堆最后的那个元素然后在来向上调整。这也就是为什么我们这里设置的N用来表示当前堆里元素的个数,它在这里起到了设置调整以及映射关系的作用。

 

    contains()的方法实现如下:

public boolean contains(int i) {
        if (i < 0 || i >= NMAX) throw new IndexOutOfBoundsException();
        return qp[i] != -1;
    }

    它的时间复杂度为常数。因为我们开始设置的qp里都是-1的,除非已经设置了别的值了。所以这对于用来查询该索引是否被占用的效率非常高。当然,结合我们这里的问题,我们也可以直接通过堆元素所在的数组来判断,比如keys[i] == null之类的。

 

    删除元素这里主要的是删除最小元素,也包含删除某个特定的元素。我们先看删除最小的元素。我们都知道,在最小堆里,删除最小的元素就是去掉映射堆里索引为1的那个元素。然后再用最后的元素来替换它,接着进行向下调整。具体的实现如下:

 

public int delMin() { 
        if (N == 0) throw new NoSuchElementException("Priority queue underflow");
        int min = pq[1];        
        exch(1, N--); 
        sink(1);
        qp[min] = -1;            // delete
        keys[pq[N+1]] = null;    // to help with garbage collection
        pq[N+1] = -1;            // not needed
        return min; 
    }

   我们要注意到的细节就是当真的删除这个元素时,对应的keys[]数组以及pq, qp数组的元素调整。keys里面将对应元素设置成null是为了防止内存泄漏的隐患。另外,特别注意到我们这里的delMin返回的是这个min所对应的Key的索引。这在多路归并里就很有用了。这就好比我删除这个最小元素的时候,就知道它所在的流位置。

 

    除了前面的删除最小元素,我们还有一个就是支持删除指定索引的元素,它的实现如下:

public void delete(int i) {
        if (i < 0 || i >= NMAX) throw new IndexOutOfBoundsException();
        if (!contains(i)) throw new NoSuchElementException("index is not in the priority queue");
        int index = qp[i];
        exch(index, N--);
        swim(index);
        sink(index);
        keys[i] = null;
        qp[i] = -1;
    }

   因为这里要将中间的某个元素给去掉,我们用最后面的元素替换这个元素,然后要同时进行向上交换和向下交换。向下交换我们可以理解,不过向上交换也是必须的。在某些情况下,这个堆最后面的元素可能比另外一个子树的父节点小。

 

    对元素的查找除了前面的contains()方法以外,还有查找最小元素的索引,最小元素的key值,给定某个索引的key值,它们的实现相对不是很复杂。一一列举如下:

public int minIndex() { 
        if (N == 0) throw new NoSuchElementException("Priority queue underflow");
        return pq[1];        
    }

 

public Key minKey() { 
        if (N == 0) throw new NoSuchElementException("Priority queue underflow");
        return keys[pq[1]];        
    }

 

public Key keyOf(int i) {
        if (i < 0 || i >= NMAX) throw new IndexOutOfBoundsException();
        if (!contains(i)) throw new NoSuchElementException("index is not in the priority queue");
        else return keys[i];
    }

   因为实现很简单,也很容易明白,这里就不再详细的解释了。

 

    对一些元素值的更新包括有增加key值,减少key值或者就是简单的修改key值。它们的实现如下:

public void increaseKey(int i, Key key) {
        if (i < 0 || i >= NMAX) throw new IndexOutOfBoundsException();
        if (!contains(i)) throw new NoSuchElementException("index is not in the priority queue");
        if (keys[i].compareTo(key) >= 0) throw new IllegalArgumentException("Calling increaseKey() with given argument would not strictly increase the key");
        keys[i] = key;
        sink(qp[i]);
    }

    因为是增加key的值,所以我们只需要考虑向下交换就可以了。这里通过qp[i]将对应的qp[]索引返回进行交换就可以了。

 

   相应的,减少key值,则需要向上交换,对应的实现如下:

public void decreaseKey(int i, Key key) {
        if (i < 0 || i >= NMAX) throw new IndexOutOfBoundsException();
        if (!contains(i)) throw new NoSuchElementException("index is not in the priority queue");
        if (keys[i].compareTo(key) <= 0) throw new IllegalArgumentException("Calling decreaseKey() with given argument would not strictly decrease the key");
        keys[i] = key;
        swim(qp[i]);
    }

    这里唯一稍微有点不同的就是修改key值,因为我们不知道这个key值是增加了还是减少了,所以需要同时在上下两个方向进行交换:

public void changeKey(int i, Key key) {
        if (i < 0 || i >= NMAX) throw new IndexOutOfBoundsException();
        if (!contains(i)) throw new NoSuchElementException("index is not in the priority queue");
        keys[i] = key;
        swim(qp[i]);
        sink(qp[i]);
    }

 

    除了上述的基本操作,还有一些方法比如判断堆是否空,取得堆长度之类的方法,因为比较简单,这里也就不再赘述了。

 

K路归并的实现

   在前面一些讨论多路归并的文章里,我一再提到过,这个问题的关键就在于当我们找到某个最小元素的时候,需要找到它同时所对应的堆。在IndexMinPQ里的delMin恰好就解决了这个问题。所以在下面的实现里,利用这个我们可以不断的将对应的元素加入到堆中间进行调整。

 

private static void merge(In[] streams) { 
        int N = streams.length; 
        IndexMinPQ<String> pq = new IndexMinPQ<String>(N); 
        for (int i = 0; i < N; i++) 
            if (!streams[i].isEmpty()) 
                pq.insert(i, streams[i].readString()); 

        // Extract and print min and read next from its stream. 
        while (!pq.isEmpty()) {
            StdOut.print(pq.minKey() + " "); 
            int i = pq.delMin(); 
            if (!streams[i].isEmpty()) 
                pq.insert(i, streams[i].readString()); 
        }
    } 

    从代码中我们还可以看到,我们需要判断一下,每次删除这个元素的时候,判断一下这个流还有没有空,如果没有空的话,还要读入新的元素加入到堆里。这里是以String类型作为示例。详细的实现可以参照后面的参考材料。

 

总结

    关于堆的一些变体在一些具体的应用,比如多路归并的实现,图算法的实现里都有一定的应用。只是它们的应用和一些特性的需求采用传统的堆功能并不能满足所有需求,所以才在这里引入了IndexMinPQ等实现。和传统的实现比起来,它支持对堆里的元素按照索引的方式来修改,好像我们访问一个数组一样,同时它的访问时间复杂度很低,一般是常熟或者logN级别。当然,这里良好性能的保障在于我们付出了额外两个数组来保存元素和堆索引之间的映射。可以说是一种典型的空间换时间思路。里面的很多细节值得细细推敲。

 

参考材料

http://algs4.cs.princeton.edu/24pq/IndexMinPQ.java.html

http://docs.oracle.com/javase/7/docs/api/java/util/PriorityQueue.html

http://algs4.cs.princeton.edu/24pq/Multiway.java.html

  • 大小: 4.3 KB
  • 大小: 4.6 KB
  • 大小: 7 KB
  • 大小: 4.4 KB
  • 大小: 4.5 KB
分享到:
评论

相关推荐

    关于整流堆原理及应用之浅析

    整流二极管一种将交流电能转变为直流电能的半导体器件。通常它包含一个PN结,有正极和负极两个端子。二极管最重要的特性就是单...这种器件的结面积较大,能通过较大电流(可达上千安),但工作频率不高,一般在几十千赫

    详细解析可堆叠交换机的优势与挑战

    堆叠交换机将几个交换机通过专用的堆叠模块相连可以成倍地提高网络接入层的端口密度。有些厂家的交换机经过堆叠后可以作为一个网元管理,这更简化了网络结构。可堆叠交换机具有迅速部署、良好的价值、可伸缩性以及...

    栈的各种应用

    包括栈的几种基本应用,封装成一个程序,包括表达式求值、括号匹配、进制转换的系列经典算法。

    Exploit 编写系列教程--堆喷射技术揭秘

    英文文档翻译而来,详细描述了堆喷射技术的原理与应用,附带几个实例。

    美国AstroNova堆叠式数据采集系统Daxus

    美国AstroNova堆叠式数据采集系统DAXUS,是一台异常小巧却功能强大的数据采集设备,可以单独使用进行故障排查和维护,多台设备叠加实现更多通道的数据采集,用户可以记录几个或几百个必要的参数,来保证设备的高速...

    计算机应用基础学习过程表现——国开大.docx

    要强迫自己做几个综合实例,分别详细地进行文字编辑,使自己可以从全局的角度掌握整个编辑过程,力争使自己学习完word之后就可以投身到实际的工作中去。 计算机应用基础学习过程表现——国开大全文共2页,当前为第2...

    国开计算机应用基础(本)形考学习过程表现.doc

    要强 迫自己做几个综合实例,分别详细地进行文字编辑,使自己可以从全局的角度掌握整个 编辑过程,力争使自己学习完word之后就可以投身到实际的工作中去。 二、学习建议 1、常见问题要弄懂 对于经常出现的问题,要...

    RFID技术中的详细解析可堆叠交换机的优势与挑战

    堆叠交换机将几个交换机通过专用的堆叠模块相连可以成倍地提高网络接入层的端口密度。  有些厂家的交换机经过堆叠后可以作为一个网元管理,这更简化了网络结构。  可堆叠交换机的优势  可堆叠交换机具有迅速...

    计算机应用基础(本)的课程学习过程表现.docx

    要强迫自己做几个综合实例,分别详细地进行文字编辑,使自己可以从全局的角度掌握整个编辑过程,力争使自己学习完word之后就可以投身到实际的工作中去。 计算机应用基础(本)的课程学习过程表现全文共2页,当前为第2...

    下一代中基线反应堆实验研究中微子振荡

    五十多年来,反应堆实验在中微子物理学中的发现和精确测量方面都发挥了重要作用。 验证中微子存在的方法... 在本文中,我们使用GloBES仿真软件包研究了RENO-50的几个特性,该特性是未来的中基线反应堆中微子振荡实验。

    海康监控平台容器,通过web访问的方式整合几个不同时期建设的监控平台以及一大堆nvr。

    结构相当简单,就是一个有单点登录功能的容器而已,在首页的列表中点击监控点后会链接到相应的平台并自动登录。因为缺乏接口,花费时间最多是在模拟登录的环节。因为是web模拟登录,代码中添加了相当多try,catch,...

    嵌入式系统通用的应用软件结构研究

    本文就μC/OS-II内核的任务管理和内存管理进行基本的介绍,并介绍一个通用的应用软件结构。然后,与之相对应,提供两个不同操作系统下的应用实例。 关键词:嵌入式系统 多任务 编程 引言 嵌入式系统的面向...

    大数据时代-几个例子告诉你什么叫大数据.docx

    大数据时代,几个例子告诉你什么是大数据 大数据时代-几个例子告诉你什么叫大数据全文共2页,当前为第1页。工具类厂商蓄意炒作大数据,以达到售卖产品的目的,但导致的结果是很多人对大数据这一概念云里雾里。实际上...

    链式存储结构的基本操作

    (2)先定义堆栈的几个基本操作,再设计一主函数利用堆的操作完成以下功能:假设一个算术表达式中可以包含三种括号:()[]{},且这三种括号可以按任意次序嵌套使用(如:...[...{...}...[...]...]...(...))。...

    net学习笔记及其他代码应用

    创建了几个String Object? 答:两个对象,一个是“xyx”,一个是指向“xyx”的引用对象s。 38.abstract class和interface有什么区别? 答: 声明方法的存在而不去实现它的类被叫做抽象类(abstract class),它用于...

    suggestanapp:我的 12 个月挑战中的 12 个应用程序中的第 1 个项目。 此应用程序允许用户注册并提交应用程序创意以供开发人员创建。 使用 MeteorJS 和 Telescope 构建

    我选择构建它有几个原因: 我想接触 Meteor,而不必从头开始 我打算请我的朋友提出挑战的想法,所以有一个地方让他们提出建议似乎是个好主意 堆 建议一个应用程序是使用和构建的,并且主要是开箱即用的功能。 我...

    generator-lego:这是一个构建Web应用程序的概念,其基础是构建应用程序应类似于构建Lego之家。

    乐高发电机 ... 在node js中,有几个模块可以执行这种操作,其中最流行的是express js,因此生成器lego的工作将每个块的功能分开,从而将另一个功能堆叠在一起。 在此版本中,有两个块: 表示 mongo

    研究论文-Dijkstra改进算法在车辆导航系统中的应用与仿真.pdf

    车辆导航系统的最基本功能是最短路径的搜索,车载导航是单源单目标的...根据节点的分布情况将搜索过程分为几个阶段,引入了动态限制搜索区域机制.最后在实际道路网络中的测试及仿真结果表明了改进算法的可行性和优越性.

    00808计算机应用基础(本)学习报告.doc

    要强迫自己做几个综合实例分别详细地进行文字编辑使自己可以从全局的角度掌 捏整个编辑过程力争使自己学习完Word之后就可以投身到实际的工作中去。 二、学习建议 1、常见问题要弄懂对于经常出现的问题要及时解决。...

    LINUX操作系统原理及应用

    前祭天上传过一份和这个资源一样的LINUX操作系统原理和运用的教材,由于很少用分割工具,分割的时候没有分割好,今天下载下来才知道是不能看的,还害了一个人全部下载了那堆垃圾,花了几分,今天重新补上,算做是...

Global site tag (gtag.js) - Google Analytics