优先队列和堆排序

本篇主要包括以下内容

1.优先队列的介绍
2.优先队列的几个初级实现
3.二叉堆实现优先队列
4.堆排序
1.优先队列是什么

在很多情况下,我们需要处理有序的元素,但是并不一定需要这些元素时时刻刻全部有序,或者并不需要他们一次性全部有序。很多情况下,我们只需要找出一堆元素里面键值最大的那个元素,然后继续收集更多的元素,在我们需要其中最大的元素是,这种数据结构能够为我们提供。这就叫优先队列。

一个典型的优先队列的抽象数据结构必须至少为我们提供两种操作:找到最大或者最小元素并且删除和插入元素

优先队列被应用于任务调度等具有优先级的事件处理中,还可以被用来计算topk问题。

2.优先队列的几种初级实现

优先队列的实现有很多方法,就我们目前了解的简单数据结构如栈,队列,链表等都可以实现优先队列。

2.1数组实现(无序)

我们可以使用数组来存储要处理的元素,并且提供一个普通的insert函数,在插入的时候我们并没有做什么特殊处理,数组中存储的元素序列依赖于插入的顺序,我们并不会对其进行干预,但是我们提供了一个可以产生优先效果的delmax()函数,每次调用delmax函数删除数组中的一个元素时,delmax总是找到数组中的最大或者最小元素与数组的边界元素进行交换,来保证每次删除的元素都是优先级最高的元素。

一个参考的delmax()函数代码如下:

//数组无序方式实现
int delmax( int data[], int length )
{
    if ( length <= 0 )
        return -1;
    int maxi = 0;
    int max;
    int i;
    for ( i = 1; i < length; ++i )
    {
        if ( data[maxi] < data[i] )
        {
            maxi = i;
        }
    }
    max = data[maxi];
    if ( maxi != length-1 )
        swap( &data[maxi], &data[length-1] );
    data[length-1] = -1;
    return max;
}

测试程序如下:

int main( void )
{
    int array[20] = { 5, 4, 3, 7,8, 98, 65, 43, 5, 54, 23, 12, 0, 78, 54, 43, 23, 43, 54 };

    //数组无序方式实现测试
    for ( int i = 0; i < 20; i++ )
        cout << delmax( array, 20-i ) << " ";
    cout << endl;
    return 0;
}

这种方法实现的优先队列插入操作复杂度为1,删除操作时间复杂度达到N.

2.2数组实现(有序)

使用数组实现优先队列的另一种方式就是每次在向数组中插入元素后,都要保证数组的有序性,即提供一个insert函数,这个insert函数实现类似于插入排序一样的效果,提供一个普通的delmax函数,delmax函数每次只需要删除并返回数组第一个元素即可。

一个参考的insert()函数代码如下:

//数组有序方式实现
void insert( int data[], int length, int key )
{
    int i = 0;
    int temp = 0;
    for ( ; i < length; ++i )
    {
        if ( data[i] >= key )
            break;
    }
    temp = i;
    i = length-1;
    while ( i >= temp )
    {
        data[i+1] = data[i];
        i--;
    }
    data[temp] = key;
}

数组有序实现优先队列测试:

int main( void )
{
    int array[20] = { 5, 4, 3, 7,8, 98, 65, 43, 5, 54, 23, 12, 0, 78, 54, 43, 23, 43, 54 };
    //数组有序方式实现测试
    int data[20];
    for ( int i = 0; i < 20; i++ )
        insert( data, i, array[i] );
    for ( int i = 0; i < 20; i++ )
        cout << data[i] << " ";
    cout << endl;
    return 0;
}

这种方法实现的优先队列插入操作时间复杂度为N,删除时间复杂度为1.

2.3链表实现

链表实现法和数组实现法一样,都是在insert函数和delmax函数上做文章,只不过将数组实现的底层容器换成了链表而已。

我们可以先看看优先队列的各种实现方法的时间复杂度。

这里写图片描述

3.二叉堆实现优先队列

3.1二叉堆的表示问题

一般情况下,二叉树的表示采用的都是链表表示法,如果使用链表表示二叉堆,那么每个元素都需要三个指针来找到它的上下结点(父结点两个,叶子结点一个),但是如果我们使用完全二叉树表示二叉堆,就非常方便,完全二叉树可以使用数组存储,相应结点的父结点可以使用k/2表示,两个子结点分别是2k,2k+1。

使用数组表示完全二叉树可以使用下图说明:

这里写图片描述

3.2堆算法

下来到了最关键的时刻了,即是如何使用二叉堆实现优先队列,要实现优先队列,我们需要借助的一个数据结构叫做最大堆(或者最小堆)。最大堆和最小堆之间的转换只需要在实现上进行一点小小的修改就可以完成。我们先以最大堆为例实现优先队列。

最大堆即是始终保持一个原则:父结点的值始终大于两个子结点。
最大堆的实现中,最核心的两个操作就是上浮和下沉操作。

由下至上的堆有序化即是上浮

如果堆得有序状态因为某个结点变得比它的父结点更大而被打破,那么我们就需要通过交换它和它的父结点来修复堆。交换后,这个结点比它的两个子结点都大(一个是曾经的父结点,另一个比它更小,因为它是曾经父结点的子结点),但这个结点仍然可能比它现在的父结点更大。我们可以一遍遍的用同样的方法恢复秩序,将这个结点不断向上移动直到我们遇到了一个更大的父结点。这个过程就是上浮。

代码实现

//上浮
void swim(int data[],  int k )
{
    //如果没有到达根节点并且父结点小于子结点就进行交换
    while ( k > 1 && data[k/2] < data[k] )
    {
        swap( data[k/2], data[k] );
        //继续上溯
        k = k / 2;
    }
}

由上至下的堆有序化即是下沉

如果堆的有序状态因为某个结点变得比它的两个子结点或是其中之一更小了而被打破了,那么我们可以通过将它和它的两个子结点中的较大者交换来恢复堆。交换可能会在子结点处继续打破堆得有序状态,因此我们需要不断地用相同的方式将其修复,将结点向下移动直到它的子结点都比它更小或是到达了堆得底部。

代码实现

//下沉
//length data数组的大小,用来判断是否到达二叉堆的叶子结点
void sink( int data[], int length, int k )
{
    //如果data[k]还有子结点,就进行处理
    while ( 2*k < length )
    {
        int j = 2*k;
        //如果data[k]有两个子结点,先找出两个子结点中值最大的那个
        if ( j+1 < length && data[j] < data[j+1] )
            j++;
        //如果父结点不小于两个子结点中最大的,不需要进行其他操作
        if ( data[k] >= data[j] )
            break;
        swap( data[k], data[j] );
        //继续处理被改变的子结点
        k = j;
    }
}

上浮和下沉分别在对二叉树进行插入和删除操作时调用,具体的调用过程参见下图:

这里写图片描述

3.3由堆算法实现的优先队列

可以利用上面的swim和sink算法实现优先队列,参考代码如下,重点关注insert函数和delmax函数。

#define MAXN 1024
class MaxPQ
{
private:
    int pq[MAXN];
    int n;

public:
    MaxPQ() : n( 0 )
    {
        int i = 0;
        for ( ; i < MAXN; i++ )
            pq[i] = -1;
    }
    bool isEmpty()
    {
        return n == 0;
    }
    int size()
    {
        return n;
    }
    void insert( int key )
    {
        //pq[0] 不保存元素
        pq[++n] = key;
        //上浮
        swim( pq,  n );
    }
    int delmax()
    {
        if ( isEmpty()  )
            return -1;
        int max = pq[1];
        swap( &pq[1], &pq[n] );
        //n-- 删除一个元素之后将记录数组中元素个数的n减一
        pq[n--] = -1;
        //第二个参数必须是n+1,因为存储的最大下标是n,则数组中一共使用了n+1个位置
        sink( pq, n+1, 1 );
        return max;
    }

};

下面给出一个测试用例:

int main( void )
{
    int array[20] = { 5, 4, 3, 7,8, 98, 65, 43, 5, 54, 23, 12, 0, 78, 54, 43, 23, 43, 54 };
    MaxPQ maxpq;
    for ( int i = 0; i < 20; i++ )
    {
        maxpq.insert( array[i] );
        cout << array[i] << " ";
    }
    cout << endl;
    for ( int i = 0; i < 20; i++ )
        cout << maxpq.delmax() << " ";
    cout << endl;
    return 0;
}

3.4堆算法的几个改进方案
3.4.1 多叉堆

还可以使用数组表示的完全三叉树实现堆算法,相对于二叉树而言,三叉树的子结点k的父结点为(K+1)/3,而父结点k的三个子结点分别是3k-1,3k,3k+1.

3.4.2 调整数组大小

在堆算法实现的二叉树中,通过在insert函数里面添加使数组长度加倍的算法,和在delmax函数里面添加使数组长度减半的算法,实现队里长度的动态增长和减少,在向队列中添加和删除元素的时候,就不用考虑队列大小的问题。

3.4.3 索引优先队列

4.堆排序

4.1堆的构造

堆的构造可以有两种方法:

1.可以从左到右遍历数组,用swim(上浮)函数保证扫描指针左侧的所有元素都已经是一棵堆有序的完全树即可,就想连续向优先队列中插入元素一样。

2.从右至左用sink(下沉)函数构造子堆。数组的每个位置都已经是一个子堆得根结点了,sink函数对于这些子堆也适用。如果一个结点的两个结点都已经是堆了,那么在该结点上调用sink函数可以将它们变成一个堆,这个过程会递归的建立起堆得秩序,开始时我们只需扫描数组中的一半元素,因为我们可以跳过大小为1的子堆。最后我们在位置1上调用sink方法,扫描结束。

这两个方法中比较好的是第二种,第二种会更好的时间效率和更少的比较次数。

4.2堆排序

给定一个数组,将其使用堆排序进行排序。需要先使用sink函数将这个数组构造成最大堆,接下来每次讲堆顶的最大元素取下来与数组的最后一个元素交换,这时候最大堆得结构已经被破坏,将数组元素个数减一并且对新的堆顶元素调用sink函数使数组恢复最大堆,重复执行这个过程,直到所有元素都已排序。

一个堆排序算法的参考实现如下:

int heap_sort( int data[], int length )
{
    int n = length;
    for ( int k = n/2; k >= 1; k-- )
        sink( data, length, k );
    while ( n > 1 )
    {
        swap( &data[1], &data[n-1] );
        n--;
        sink( data, n, 1 );
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值