算法思想(四)—— 堆和堆排序

为什么使用堆?


优先队列

普通队列:先进先出;后进后出
优先队列:出队顺序和入队顺序无关;和优先级相关

为什么使用优先队列?

  • 对于动态问题
    例如操作系统执行任务,每个任务都有优先级,动态选择优先级最高的任务执行,使用优先队列。
    比如不同用户对同一界面的请求,回应请求优先队列。
    或者游戏中只能选择角色攻击敌人的顺序,角色的攻击范围是一定的,该角色由电脑控制选择敌人攻击,使用优先队列。

  • 对于静态问题:
    在10000000个元素中选出前一百名?(即在N各元素中选出前M个元素问题)
    使用排序? NlogN
    也可以使用优先队列进行优化–》 NlogM

优先队列的操作:
入队,出队(取优先级最高的元素)

优先队列的实现:

入队出队
普通数组O(1)O(n)
顺序数组O(n)O(1)
O(lgn)O(lgn)

使用堆得时间效率>使用数组

对于总共N个请求:

  • 使用普通数组或者顺序数组,最差情况O(n2)
  • 使用堆:O(nlgn)

堆的基本存储


二叉堆是一棵完全二叉树
最大堆:堆中节点的值总是不大于其父节点的值,堆总是一颗完全二叉树、
最小堆

用数组存储二叉堆

经典实现:根节点从1标记…

parent(i)=i/2
left child(i)=2i;
right child(i)=2
i+1;

template<typename Item>
class MaxHeap{

private:
    Item *data;
    int count;
    int capacity;

    void shiftUp(int k){
        while( k > 1 && data[k/2] < data[k] ){
            swap( data[k/2], data[k] );
            k /= 2;
        }
    }

public:
    // 构造函数, 构造一个空堆, 可容纳capacity个元素
    MaxHeap(int capacity){
        data = new Item[capacity+1];
        count = 0;
        this->capacity = capacity;
    }

    ~MaxHeap(){
        delete[] data;
    }

    // 返回堆中的元素个数
    int size(){
        return count;
    }

    // 返回一个布尔值, 表示堆中是否为空
    bool isEmpty(){
        return count == 0;
    }

    // 像最大堆中插入一个新的元素 item
    void insert(Item item){
        assert( count + 1 <= capacity );
        data[count+1] = item;
        count ++;
        shiftUp(count);
    }


public:
    // 以树状打印整个堆结构
    void testPrint(){
    //...
    }
};

测试:

在这里插入代码片// 测试 MaxHeap
int main() {

    MaxHeap<int> maxheap = MaxHeap<int>(100);

    srand(time(NULL));
    for( int i = 0 ; i < 15 ; i ++ )
        maxheap.insert( rand()%100 );

    maxheap.testPrint();

    return 0;
}

在这里插入图片描述

shiftup


 void shiftUp(int k){
        while( k > 1 && data[k/2] < data[k] ){
            swap( data[k/2], data[k] );
            k /= 2;
        }
    }
// 像最大堆中插入一个新的元素 item
    void insert(Item item){
        assert( count + 1 <= capacity );
        data[count+1] = item;
        count ++;
        shiftUp(count);
    }

shift down


对于堆,只能取根节点的元素,然后把最后一个元素放在根节点的位置,count(堆中元素个数成员变量)–;

然后,调整元素位置使其保持最大堆的性质,即将根节点的元素一步一步向下挪,最终找到其合适的位置,即shift down。

Item extractMax()
    {
        assert(count>0);

        Item ret = data[1];

        swap(data[1],data[count]);
        count--;
        shiftDown(1);
        return ret;
    }
void shiftDown(int k){

        while(2*k<=count){//判断该节点有左孩子即可

           int j=2*k;//在此轮循环中,data[k]和data[j]交换位置
           if(j+1<=count&&data[j+1]>data[j])
            j+=1;
    
           if(data[k]>=data[j])
            break;
    
           swap(data[k],data[j]);
           k=j;
        }
    }

代码中的swap操作非常费事,可以进行优化,使用赋值。

// 测试 MaxHeap

int main() {

    MaxHeap<int> maxheap = MaxHeap<int>(100);
    
    srand(time(NULL));
    for( int i = 0 ; i < 15 ; i ++ )
        maxheap.insert( rand()%100 );

   while(!maxheap.isEmpty())

    cout<<maxheap.extractMax()<<" ";

   cout<<endl;

    return 0;

}

在这里插入图片描述

基础堆排序和 Heapify



template<typename T>

void heapSort1(T arr[],int n)

{

    MaxHeap<T> maxheap = MaxHeap<T>(n);

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

        maxheap.insert(arr[i]);

    for(int i=n-1;i>=0;i--)
        arr[i]=maxheap.extractMax();

}

给定一个数组,让这个数组的排列形成一个堆的形状,这个过程就是Heapfiy。

  • heapify
    所有叶子节点本身就是一个最大堆,每个堆的元素只有一个
    对于非叶子节点,第一个非叶子节点的索引是n/2,从后向前考查每一个非叶子节点。
 MaxHeap(Item arr[],int n)

    {

        data = new Item[n+1];

        capacity=n;

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

            data[i+1]=arr[i];

        count=n;        

        for(int i=count/2;i>=1;i--)
            shiftDown(i);
    }

template<typename T>

void heapSort2(T arr[],int n)

{

    MaxHeap<T> maxheap = MaxHeap<T>(arr,n);

    for(int i=n-1;i>=0;i--)

        arr[i]=maxheap.extractMax();

}

测试可得堆排序的效率依然不如归并排序和快速排序,系统级别的排序很少使用堆排序,堆这种结构更多用于动态数据的维护

两种建堆过程使得堆排序的效率也不一样。

heapSort2比1块。

即一个很有用的结论:
Heapify的算法复杂度
1.将n个元素逐个插入到一个空堆中,算法复杂度是O(nlogn)
2.heapify的过程,算法复杂度为O(n)(heapify的过程一开始就将n/2的元素直接不考虑了,算法肯定要快一些hhhh)

  • 总结:
    这一小节介绍了将一个数组构建成堆,同时实现了两个版本的堆排序。同时,在堆排序中,我们都需要将数组的内容逐一放入一个堆中,实际上我们额外开了一个大小为n的空间,空间复杂度为O(n)。

我们可以改造,使得不使用任何额外空间,原地对数组进行排序

优化的堆排序


原地堆排序

在这里插入图片描述

这里用数组存储二叉堆(元素从0开始索引
在这里插入图片描述
注意在这里进行Heapify的过程中,最后一个非叶子结点的索引为(count-1)/2


template<typename T>

void shiftDown(T arr[],int n,int k)

{

    while(2*k+1<n){

        int j=2*k+1;//在此轮循环中,arr[k]和arr[j]交换位置
        if(j+1<n&&arr[j+1]>arr[j])
            j+=1;
    
        if(arr[k]>=arr[j])
            break;
    
        swap(arr[k],arr[j]);
        k=j;
    }

}

//原地堆排序

template<typename T>

void heapSort(T arr[],int n)

{

    //heapify

    for(int i=(n-1)/2;i>=0;i--)//注意索引从0开始

        __shiftDown(arr,n,i);//arr数组;n个元素,对第i个元素进行shiftdown

    for(int i=n-1;i>0;i--){//注意这里是i>0不是i>=0,因为堆里只剩下一个元素的时候就不需要排序了
        swap(arr[0],arr[i]);
        __shiftDown(arr,i,0);//每一次循环有i个元素,对第0个元素进行shiftdown即可
    }//完成堆排序

}

由测试可知,原地堆排序比之前的两种堆排序都要快,因为省去了开辟空间,效率更高。

排序算法总结


现在我们来总结一下

在这里插入图片描述

总体而言,快速排序相对更快一些,所以,一般系统级别的排序都是使用快速排序来实现,特别是对于有可能有大量重复键值的排序,可以使用三路快速排序。

归并只能开辟额外的空间来完成归并的过程,才能完成归并排序,真因为如此,对于一个对空间敏感的系统,归并排序可能是最坏的选择。

对于插入和堆排序,都可以在数组上完成,额外空间是0(1),归并需要O(n),快排需要o(logn),对于快排,采用递归的方式进行排序,这个过程一共有logn这么多层,就需要有logn层的栈空间,来保存每一次递归过程的变量以便递归返回的时候继续使用,为此,我们要注意,快速排序虽然是一种原地的排序,但他所耗费的空间是logn级别

排序算法的稳定性?
  • 什么是排序算法的稳定性Stable?//和具体实现有关
    稳定排序:对于相等的元素,在排序后,原来靠前的元素依然靠前,相等元素的相对位置没有发生改变
    在这里插入图片描述
    稳定的排序在实际生活中有很多应用:比如一个学生的成绩单,是按照学生姓名的字典序排序,要按照分数排序,稳定的排序就能做到,对于相同分数的学生,其排序就是按照字典序来排序。
  • 可以通过自定义比较函数,让排序算法不存在稳定性的问题
    在这里插入图片描述

索引堆 Index Heap


为什么要引入索引堆?

对于与普通的堆,
排序前:
排序前
排序后:
排序后
比较这两页。在堆构建前和堆构建后,对于数组而言,数组的元素位置发生了改变,正是因为这些数组元素位置的改变,我们才可以将这个数组看作是一个堆。

但是在我们构建堆的过程中,改变元素的位置,会有一些局限性:

  1. 如果元素是非常复杂的结构,例如巨型字符串,交换元素本身消耗巨大。性能消耗
  2. 更致命的是,由于整个元素在数组中的位置发生了改变,使得当堆建成之后,很难索引到它,很难去改变他。例如说这些元素表示的是一个个系统任务,我们要改变索引为6的任务的优先级提高,若原来的数组中O(1)直接提取,建成堆后,堆的数组中索引不到它,我们可以对这些元素再添加一个属性,但是仍要将整个数组遍历一遍才能找到这个任务,这仍然是低效的

所以,我们引入索引堆:

建堆前:
在这里插入图片描述
建堆后:
在这里插入图片描述
可以index数组发生改变,表示堆。

可以看到,这样有两个优势

  1. 建堆的过程只是索引的位置发生变化,索引是简单的int型,如果data是复杂的元素,只交换int型的索引所提升的效率是非常高的
  2. 可以快速索引到某个元素

可以看到思路与之前一样,元素比较的时候比较的是data的元素,但真正进行交换的是int的索引

indexHeap.c

template<typename Item>
class IndexMaxHeap{
private:
    Item *data;
    int indexes;
    int count;
    int capacity;

    void shiftUp(int k){
        while( k > 1 && data[indexes[k/2]] < data[indexes[k]] ){
            swap( indexes[k/2], indexes[k] );
            k /= 2;
        }
    }

    void shiftDown(int k){
        while(2*k<=count){//判断该节点有左孩子即可

           int j=2*k;//在此轮循环中,data[k]和data[j]交换位置
           if(j+1<=count&&data[indexes[j+1]]>data[indexes[j]])
            j+=1;

           if(data[indexes[k]]>=data[indexes[j]])
            break;

           swap(indexes[k],indexes[j]);
           k=j;
        }
    }

public:
    // 构造函数, 构造一个空堆, 可容纳capacity个元素
    IndexMaxHeap(int capacity){
        data = new Item[capacity+1];
        indexes= new int[capacity+1];
        count = 0;
        this->capacity = capacity;
    }

    ~IndexMaxHeap(){
        delete[] data;
        delete[] indexes;
    }

    // 像最大堆中插入一个新的元素 item,索引为i
    //传入的i对用户而言,是从0索引的
    void insert(int i,Item item){
        assert( count + 1 <= capacity );
        assert(i+1>=1&&i+1<=capacity);

        i+=1;
        data[i] = item;
        indexes[count+1]=i;

        count ++;
        shiftUp(count);
    }

    //返回最大的元素的索引,将这个元素删除
    Item extractMaxIndex()
    {
        assert(count>0);

        //对于用户开始,从0开始的索引
        int ret = indexes[1]-1;

        swap(indexes[1],indexes[count]);
        count--;
        shiftDown(1);

        return ret;
    }

    Item getItem(int i)
    {
        return data[i+1];
    }

    void change(int i,Item newItem)
    {
        i+=1;
        data[i]=newItem;

        //找到indexs[j]=i;j表示data[i]在堆中的位置
        //之后shiftup(j).在shiftdown(j)
        for(int j=1;j<=count;j++)
            if(indexes[j]==i)
            {
                shiftUp(j);//这两个操作可以互换
                shiftDown(j);
                return;
            }
    }
};

对于修改元素,change,找到这个j,使用for循环,O(n),在change里,将索引为i的元素修改为新的item值,完成了操作
在这里怎么对change进行优化呢?

索引堆的优化


反向查找思路

在这里插入图片描述
rev[i]的意思是i这个索引,他在数组中的位置。
只要维护好rev这个数组,我们就能在更新的操作中,直接把一个索引对应在indexes数组中的位置直接找到, O(1)在这里插入图片描述
加入reverse,使得change变为o(nlogn)

template<typename Item>
class IndexMaxHeap{

private:
    Item *data;
    int* indexes;
    int* reverse;
    int count;
    int capacity;

    void shiftUp(int k){
        while( k > 1 && data[indexes[k/2]] < data[indexes[k]] ){
            swap( indexes[k/2], indexes[k] );
            reverse[indexes[k/2]]=k/2;
            reverse[indexer[k]]=k;
            k /= 2;
        }
    }

    void shiftDown(int k){
        while(2*k<=count){//判断该节点有左孩子即可

           int j=2*k;//在此轮循环中,data[k]和data[j]交换位置
           if(j+1<=count&&data[indexes[j+1]]>data[indexes[j]])
            j+=1;

           if(data[indexes[k]]>=data[indexes[j]])
            break;

           swap(indexes[k],indexes[j]);
           reverse[indexes[k]]=k;
           reverse[indexes[j]]=j;
           k=j;
        }
    }

public:
    // 构造函数, 构造一个空堆, 可容纳capacity个元素
    IndexMaxHeap(int capacity){
        data = new Item[capacity+1];
        indexes= new int[capacity+1];
        reverse= new int[capacity+1];
        for(int i=0;i<=capacity;i++)
            reverse[i]=0;

        count = 0;
        this->capacity = capacity;
    }

    ~IndexMaxHeap(){
        delete[] data;
        delete[] indexes;
        delete[] reverse;
    }

    // 返回堆中的元素个数
    int size(){
        return count;
    }

    // 返回一个布尔值, 表示堆中是否为空
    bool isEmpty(){
        return count == 0;
    }

    // 像最大堆中插入一个新的元素 item,索引为i
    //传入的i对用户而言,是从0索引的
    void insert(int i,Item item){
        assert( count + 1 <= capacity );
        assert(i+1>=1&&i+1<=capacity);

        i+=1;
        data[i] = item;
        indexes[count+1]=i;
        reverse[i]=count+1;

        count ++;
        shiftUp(count);
    }

    //返回最大的元素的索引,将这个元素删除
    Item extractMaxIndex()
    {
        assert(count>0);

        //对于用户开始,从0开始的索引
        int ret = indexes[1]-1;

        swap(indexes[1],indexes[count]);
        reverse[indexes[1]]=1;
        reverse[indexes[count]=0;//删除了这个元素
        count--;
        shiftDown(1);

        return ret;
    }

    bool contain(int i)
    {
        assert(i+1>=1&&i+1<=capacity);
        return reverse[i+1]!=0;
    }

    Item getItem(int i)
    {
        assert(contain(i));
        return data[i+1];
    }

    void change(int i,Item newItem)
    {
        assert(contain(i));
        i+=1;
        data[i]=newItem;
        int j =reverse[i];
        shiftUp(j);
        shiftDown(j);
    }

};

和堆相关的其他问题


  • 可以使用堆进行多路归并排序。

  • 二叉堆

  • d 叉堆 d-ary heap

  • 最大堆 最大索引堆

  • 堆的实现细节优化

    shiftup和shiftdown中使用赋值操作替换swap操作。
    表示堆的数组从0开始索引;
    没有capacity的限制,动态的调整堆中数组的大小。
    
  • 二项堆

  • 斐波那契堆

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值