java实现优先级队列以及堆排序

   1.优先级队列

1.1优先级队列定义

    优先级队列顾名思义首先是一种队列,但是和普通队列每次出队都是出队首元素不同,优先级队列每次出队出优先级最高的元素.

    首先想到使用数组或者链表来实现优先级队列。

    如果使用有序数组,数组按优先级队列排序,出队和队列类似,区别在于入队时要找出新元素的位置,还要移动新元素位置后面的元素;如果使用无序数组,入队与队列类似,只要将队尾元素出队就可以,但出队需要找出优先级最高的元素,然后将空出来的位置填满。

    如果使用链表,那么就类似有序数组,链表头为优先级最高的元素,那么出队直接将链表头元素出队即可,入队需要找到新元素的位置。

    综上,如果使用数组或者链表,优先级队列的插入和出队操作都需要o(n)复杂度,而使用堆可以将时间复杂度降到o(logn);

    

  插入 删除
无序数组 O(1) O(n)
有序数组 O(n) O(1)
链表 O(n) O(1)
O(logn) O(logn)

    因此,优先级队列这种数据结构使用一种叫最大堆的数据结构来实现,最大堆本质上是一种完全二叉树,二叉树的顶点(即最大堆的堆顶)是最大元素,二叉树的两个子树也是最大堆。因此最大堆满足如下性质:

1.最小堆是完全二叉树。   

2.父节点比子节点大。

    而二叉树也有两种实现方式,链表和数组,用链表实现的话,节点对象中需要存储自己和表示父节点以及两个子节点的数据。而数组实现方式,因为完全二叉树的性质,可以很容易的表示父节点和子节点:a[k]的父节点为a[k/2],a[k]的子节点为a[2*k]和a[2*k+1]。(二叉树数组a[]从a[1]开始存储最小堆)。

    1.2最大堆实现

    最大堆的基本api如下:

 public class MaxPQ>
{
    MaxPQ();//构造函数
    void insert(Key v);//插入元素
    Key delMax();//删除并返回最大元素
}

    在实现插入元素和删除元素之前,需要先实现两个辅助操作:

    上浮swim:由下至上的堆有序化,如果堆的有序状态因为某个元素变得比父元素大而导致堆不再有序,那么我们就要通过移动这个元素来使堆有序化,因为元素变大后还是肯定比子元素要大,所以要与父元素进行交换。交换后如果这个元素还是比父元素大,继续与父元素交换,直到比父元素小为止。

private void swim(int index)//向上堆化,index为无序的元素下表
{
        int cur = index;
        while (cur != 1 && less(pq[cur], pq[cur/2]) > 0 )
        {
            exch(cur,cur/2);
            cur = cur/2;
        }
}

    下沉sink:由上至下的堆有序化,如果堆的有序状态因为某个元素变得比子元素小而导致堆不再有序,那么我们就要通过移动这个元素来使堆有序化。和swim类似,但又有所不同,这时候这个元素要与两个子元素进行比较,然后与较大的子元素进行交换,然后再与两个子元素进行比较,直到比子元素大为止。

private void sink(int index)//向下堆化
{
        int cur = index;
        while ((2 *cur + 1) <= size)
        {
            int max = getMax(cur * 2,cur *2 + 1);
            if (less(pq[cur], pq[max]) < 0)
            {
                exch(cur,max);
                cur = max;
            }
            else
            {
                break;
            }
        }
}

    有了这两个辅助操作后,我们就可以实现插入和删除操作了。当往最大堆中插入一个元素时,将其放到最后,然后对其执行上浮操作,恢复堆的有序度。当从最大堆删除堆顶元素时,将其与数组最后一个元素交换,然后对堆顶元素进行下沉操作,恢复堆的有序度。

    

public void insert(Key x)//插入一个元素
{
        pq[++size] = x;
        swim(size);
}

public Key delmax() //删除堆顶元素
{
        if (size == 0)
        {
            throw new IllegalArgumentException("空队列");
        }
        Key result = pq[1];
        exch(1,size);
        pq[size--] = null;
        sink(1);

        return result;
}

    有了最大堆之后,优先级队列的实现就轻而易举了。

1.3优先级队列优化

1.上面的最大堆实现使用的是二叉树,如果使用m叉树,可以再次减少时间复杂度,而m的选择取决于插入和删除操作的比例。

以下为n个元素的二叉树和n个元素的m叉树的基本操作比较:

  插入 删除
二叉树 logn logn
m叉树 log{_{m}}^{n} mlog{_{m}}^{n}

2.使用索引优化队列来完成一些特定的任务,在优先级队列中加入一个索引数组来标识元素。

 

2.堆排序

2.1堆排序实现原理

     有了最大堆之后,堆排序的实现也很简单了。首先构造一个最大堆,然后每次取出最大元素放到因为取走元素而缩小的数组后面。所以问题就是如何根据一个数组构造最大堆。这也有两种实现方法。

    第一种,堆一开始只有第一个元素,然后插入第二个元素,插入第三个元素,直到最后。

    第二种,从叶子节点,往上,一层一层的下沉每一个节点,最后是根节点。即先让子堆有序,然后调整两个已经有序的子堆的父节点,只要sink这个父节点即可。因为叶子节点只有一个,已经有序了,不需要调整,所以从叶子节点上面一层开始调整即可。

    一般都是使用第二种实现,不仅是因为第二种实现一般比第一种快20%左右,而且是因为第二种实现堆排序只需要sink操作,可以不用实现swim操作,所以第二种实现的代码如下:

    void heapsort(Key[] keys)
    {
        int len = keys.length;
        pq = (Key[])new Comparable[len + 1];
        size = len;
        for (int i = 0; i < len; i++)
        {
            pq[i+1] = keys[i];
        }
        for (int i = len/2; i >= 1; i--)
        {
            sink(i);
        }
        for (int i = 0; i < len;i++)
        {
            keys[len-i-1] = delmax();
        }
    }

2.2堆排序的优势

堆排序是唯一同时实现了原地排序和稳定O(nlogn)的排序算法,与其相比,快速排序最差为O(n^2),归并排序不是原地排序。

 

2.3堆排序的劣势

     但是实际情况却是,快速排序才是使用最广泛的算法。

    在Arrays.sort()使用的是快速排序和归并排序的结合,当排序基本元素时,使用快排,排序对象时,使用归并排序,堆排序完全缺席,那堆排序为什么很少使用呢?

    原因在于堆排序的数据比较是跳着的,比如父节点和子节点,这样的话就对缓存很不友好,CPU缓存一般会缓存一块元素,所以堆排序破坏了局部性,实际性能并不如快速排序。

3.一些应用

3.1topk元素

    比如在一亿数据中选出最大的k个元素,有了优先级队列(最小堆)之后,就不需要将其先排序再拿出k个元素了,一亿数据排序不仅占时间,而且内存也不一定放得下。这个时候建一个大小为k的优先级队列,来一个元素,将其插入,然后删除堆顶元素即可。

3.2 求一堆数据的中位数

    建一个大顶堆,一个小顶堆,然后大顶堆中存储前半部分数据,小顶堆中存储后半部分数据,且小顶堆中的数据都大于大顶堆中的数据。也就是说,如果有 n 个数据,n 是偶数,我们从小到大排序,那前 2n​ 个数据存储在大顶堆中,后 2n​ 个数据存储在小顶堆中。这样,大顶堆中的堆顶元素就是我们要找的中位数。如果 n 是奇数,情况是类似的,大顶堆就存储 2n​+1 个数据,小顶堆中就存储 2n​ 个数据。如果新加入的数据小于等于大顶堆的堆顶元素,我们就将这个新数据插入到大顶堆;否则,我们就将这个新数据插入到小顶堆。当不符合约定时,再将一个堆中的元素调整到另一个堆。/

 

 

 

 

 

 

 

你可能感兴趣的:(数据结构和算法)