數據結構與算法之堆與堆排序


  在數據結構中,其實就是一棵完全二叉樹。我們知道內存中也有一塊叫做堆的存儲區域,但是這與數據結構中的堆是完全不同的概念。在數據結構中,堆分為大根堆小根堆,大根堆就是根結點的關鍵字大於等於任一個子節點的關鍵字,而它的左右子樹又分別都是大根堆;小根堆與大根堆恰好相反。在C++的STL中優先隊列priority_queue結構就是實現的堆結構。下來自己動手現實一個堆結構,包括heap_init,heap_insert,heap_top等操作。

1、堆的實現

  因為堆是一棵完全二叉樹,所以我們可以用順序表來實現,而且堆也只能用順序表。我們用vector。

  (1) 堆的初始化

  對堆的初始化基本思想:首先初始數組是一個雜亂無章的序列,但是如果堆中只有一個元素heap[0],那么heap[0]本身是一個堆,然后加入heap[1]調整堆;繼續加入heap[2].....直到完成所有元素的調整。

void sift_up(vector<int> &heap,int index){
    while((index+1)/2-1 >= 0){
        if(heap[(index+1)/2-1] < heap[index]){
            swap(&heap[(index+1)/2-1],&heap[index]);
            index = (index+1)/2-1;
        }else
            break;
    }
}

void heap_init(vector<int> &heap){
    if(heap.empty())
        return ;
    for(int i=1; i<heap.size(); i++){
        sift_up(heap,i);
    }
}

  (2) 向堆中插入元素

  把插入的元素放入堆的末尾,然后向上調整堆。

void heap_insert(vector<int> &heap,int element){
    heap.push_back(element);
    sift_up(heap,heap.size()-1);
}

  (3) 取出堆頂的元素

  取出一個元素后,用最后一個元素填補第一個元素的位置,然后向下依次調整堆。

void sift_down(vector<int> &heap,int index){
    while(index*2+2 < heap.size()){
        if(heap[index*2+1]>=heap[index*2+2] && heap[index]<heap[index*2+1]){
            swap(&heap[index],&heap[index*2+1]);
            index = index*2+1;
        }else if(heap[index*2+1]<heap[index*2+2] && heap[index]<heap[index*2+2]){
            swap(&heap[index],&heap[index*2+2]);
            index = index*2+2;
        }else
            break;
    }
}
bool heap_top(vector<int> &heap,int *res){
    if(heap.empty())
        return false;
    *res = heap[0];
    heap[0] = heap[heap.size()-1];
    heap.erase(heap.end()-1);
    sift_down(heap,0);
    return true;
}

2、堆排序

  首先初始化堆,然后依次取出堆頂的值。這里為大根堆,所以是從大到小排序。

void heap_sort(vector<int> &vec){
    heap_init(vec);
    int len = vec.size();
    while(len--){
        int num;
        heap_top(vec,&num);
        printf("%d ",num);
    }
}

  堆排序的時間復雜度為O(nlog2n),從上面排序的步驟可以看出它是不穩定的排序。但是它與選擇排序,歸並排序一樣時間復雜度不隨序列的分布變化而變化。而對於插入排序和冒泡排序來說,當輸入序列有序或者基本有序時,它們的復雜度會遞減為O(n),而快速排序則會退化成O(n2)。

  所以在具體應用中,要根據輸入序列來選擇哪種排序方法,具體問題具體分析。由於堆排序特殊的排序結構和優良的性能,所以在很多時候下都可以采用堆排序。

3、堆排序的應用

  在一個n個數的序列中取其中最大的k個數(Top k問題)。

    這是一個很常見的排序算法題。

  方法一:直接對這這n個數進行排序,然后取k個數。時間復雜度最少為O(nlog2n)。

  方法二:借鑒快排的思路,並不需要完整地實現快排,只需要實現快排的一部分即可得到最大的k個數。復雜度為O(nlog2k)。

  方法三:可以采用哈希排序,先把n中開始的k個數放入hash表中,然后依次從剩下的的n-k個數中取出一個,與hash表中的k個數比較,每次淘汰最小的那個數。時間復雜度為O((n-k)*k)。

  方法四:取出n中開始的k個數,建立一個小根堆,然后從剩下的n-k個數中,每次取出一個數插入小根堆中,然后刪除堆頂的那個元素(堆中的最小值)。時間復雜度為O(*(n-k)*lg2k)。

  不可否認,采用堆來求最大的k個數性能是最好的,但是好處還不止這么一點點!!我們試想一下,如果輸入的序列很大,也就是n值很大,以致於無法全部存放在內存中,那么這時候,方法一和方法二就不管用了,當然方法一采用歸並排序可以達到目的,但是這時候需要多少次IO??。如果選擇方法四,最多只需要(n-k)次IO,當然方法三也是如此,只是每次需要比較k次。

  完整代碼詳見:https://github.com/whc2uestc/DataStructure-Algorithm/tree/master/heap

  版權所有,歡迎轉載,轉載請注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM