堆和堆排序

堆与队列基本概念

普通队列: FIFO
优先队列:有优先级的队列,可以用堆来实现。
二叉堆:最大堆MaxHeap满足父节点的值大于任一子节点,最小堆MinHeap反之。且二叉堆必须是一个完全二叉树,这个性质让它可以用数组来实现。

堆的基本存储

由于有完全二叉树的性质,我们把它们按层序遍历的方式编号,根节点是1(不是0!),以此类推。

设某节点的编号为k,则它的parent = k/2,左子树left_child = 2k, 右子树right_child = 2k+1。
注意边界条件!

向一个最大堆添加一个元素 Shift Up

思路:
把新元素添加到堆的末尾,此时count++,但是此时这个新元素未必在正确的位置,所以要将它shift up,将它与它的parent比较,如果大于parent,则和parent swap一下,由此循环,直到不比它的parent大为止。同时,要注意shift到根节点是边界条件:k/2==1。

public:
     void insert(Item item){
        assert( count + 1 <= capacity );//有空间存储新插入的元素 
        data[count+1] = item;
        shiftUp(count+1);
        count ++;
    }
 Private:
     void shiftUp(int k){
        while( k > 1 && data[k/2] < data[k] ){ //k>1保证k/2最少到1,不会超越根节点
            swap( data[k/2], data[k] );
            k /= 2;
        }
    }

从一个最大堆取出一个元素 Shift Down

思路:
把根节点的元素取出来,相应的操作是用一个变量先存储这个要取出来的元素,然后count–,这样之后也不会遍历到第count个元素,相当于把一个元素删了。然后把最后一个元素和根节点元素swap,之后为了保证我们的二叉堆还是满足最大堆性质,需要现在这个根节点的元素shift down到它适合的位置。先判断是否有孩子(2k<=count,因为如果有孩子则必有左孩子)跟它的左右child相比,如果都大于,则说明在正确的位置,如果小于,则将它与大的那个子节点swap,然后继续循环。
注意边界条件:判断是否有右孩子。
设一个变量j来存与data[k]交换的位置,初值设为2k,如果2k+1<=count且data[2k+1]>data[2k],则和右孩子交换。
如果有swap记得更新k。

public:
    // 从最大堆中取出堆顶元素, 即堆中所存储的最大数据
    Item extractMax(){
        assert( count > 0 );//保证堆中有元素
        Item ret = data[1];//取根节点

        swap( data[1] , data[count] );//最后一个元素swap到根结点然后shiftdown
        count --;
        shiftDown(1);

        return ret;
    }
private:
    void shiftDown(int k){
    while(2k <= count){
        int j = 2k;//要交换的元素的位置,初始化为2k
        if(j + 1 <= count && data[j] > data[i] )
            j += 1;

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

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

基础堆排序 (heapSort1)

需要开辟一个空堆。
把数组中的元素按顺序insert到一个空堆里,由于是最大堆,所以依次取出根节点的值(当前最大)存到数组中n-1, n-2, …, 0的位置里。

//不定义在类里
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();
    }
}

将n个元素逐个插入到一个空堆中,算法复杂度是 O(nlogn) O ( n l o g n )

注意对比不同方法的算法复杂度!

Heapify (heapSort2)

Heapify与基础堆排的不同点在于它不是直接创建一个空堆,而是创建一个数组元素按顺序依次存入堆(未排序),然后从第一个非叶子节点的位置开始shiftdown,(叶子节点已经是二叉堆了),由第一个非叶子节点逐个向上处理非二叉堆,这是在构造函数的工作,不是只传入一个元素个数n,而是还要传入这个数组。
由此就构造了二叉堆。(排好序的堆)

public:
    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 >= 0; i--)
        shiftDown(i);
}
//不定义在类里
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();
}

heapify的过程,算法复杂度为 O(n) O ( n )

重要性质:

在上面的做法,我们的方法都是用数组来表示堆下标从1开始,这种情况下有以下性质:
下标从1开始,对于下标k的节点来说,
1. parent = k/2
2. left_child = 2k
3. right_child = 2k+1
4. 最后一个非叶子节点的索引:count/2
下标从0开始,对于下标k的节点来说:
1. parent = (k-1)/2
2. left_child = 2k+1
3. right_child = 2k+2
4. 最后一个非叶子节点的索引:(count-1)/2

原地堆排序 (heapSort)–优化的堆排序

在原地排序,把整个数组当作一个未排好序的堆结构,之所以上面的heapSort要构建另一个堆是因为数组元素下标从0开始,而堆是从1开始的。那么这里,我们就把这个数组看成一个下标从0开始的堆结构。

n是数组中元素个数。一开始从第一个非叶子节点(下标(n-1)/2)开始往上逐个shiftdown,获得一个二叉堆,数组第一个元素为根节点,是最大值,应该放在数组末尾,于是和最后一个元素swap。之后只考虑除了已就位的最大数的剩下的n-1个元素即可。此时只需shiftdown第一个元素,因为其他位置都满足二叉堆定义。由此循环,直到到i=1。
Time: O(nlogn) O ( n l o g n )
Space: O(1) O ( 1 )

void heapSort(T arr[], int n){
    //heapify
    for(int i = (n-1)/2; i >= 0; i--)
        __shiftDown(arr, n, i);//完整的二叉堆

    for(int i = n-1; i > 0; i--){
        swap(arr[0],arr[i]);
        __shiftDown(arr, i, 0);
    }
}
void __shiftDown(T arr[], int n, int k){
    while(2*k+1 < n){ 因为数组下标是从0...n-1,故n不能取
        int j = 2*k+1;
        if(j+1 < n && arr[j+1] > arr[j])
            j += 1;

        if(arr[k] >= arr[j])
            break;

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

最大堆的完整代码实现

#include <iostream>
#include <algorithm>
#include <string>
#include <ctime>
#include <cmath>
#include <cassert>

using namespace std;


template<typename Item>
class MaxHeap{

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

    void shiftUp(int k){
        while( k > 1 && data[k/2] < data[k] ){ //k>1保证k/2最少到1,不会超越根节点
            swap( data[k/2], data[k] );
            k /= 2;
        }
    }

    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 ++;
            // data[j] 是 data[2*k]和data[2*k+1]中的最大值

            if( data[k] >= data[j] ) 
                break;
            swap( data[k] , data[j] );
            k = j;
        }
    }

public:
    // 构造函数, 构造一个空堆, 可容纳capacity个元素
    MaxHeap(int capacity){
        data = new Item[capacity+1];//考虑数组越界
        count = 0;
        this->capacity = capacity;//把用户传来的capacity赋值给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;
        shiftUp(count+1);
        count ++;
    }

    // 从最大堆中取出堆顶元素, 即堆中所存储的最大数据
    Item extractMax(){
        assert( count > 0 );
        Item ret = data[1];

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

        return ret;
    }

    // 获取最大堆中的堆顶元素
    Item getMax(){
        assert( count > 0 );
        return data[1];
    }

};


// 测试最大堆
int main() {

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

    srand(time(NULL));
    int n = 100;    // 随机生成n个元素放入最大堆中
    for( int i = 0 ; i < n ; i ++ ){
        maxheap.insert( rand()%100 );
    }

    int* arr = new int[n];
    // 将maxheap中的数据逐渐使用extractMax取出来
    // 取出来的顺序应该是按照从大到小的顺序取出来的
    for( int i = 0 ; i < n ; i ++ ){
        arr[i] = maxheap.extractMax();
        cout<<arr[i]<<" ";
    }
    cout<<endl;

    // 确保arr数组是从大到小排列的
    for( int i = 1 ; i < n ; i ++ )
        assert( arr[i-1] >= arr[i] );

    delete[] arr;


    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值