用C++的類做三種優先隊列的實現


學過數據結構的都知道優先隊列這種東西,普通的隊列是依據入隊順序,先入隊的先出隊,而優先隊列則是依照鍵值,鍵值越大(或越小),就越先出隊。

所以,優先隊列基本支持push,pop,empty,size,top,這幾種操作。最近在看C++prime,學了類之后覺得非常適合用來實現高級數據結構,於是就動手做了一下,花了一周,終於弄好了。以下的實現都默認最小堆。

二叉堆:

最常用,最簡單的堆實現,因為二叉堆是完全二叉樹,所有可以使用順序存儲結構,也就是說,數組來保存數據。二叉堆主要基於上濾與下濾操作來維持堆序(任意節點大與等於或小於等於他的子節點)而完全二叉樹基本是平衡樹,所以這些操作都是log2 N的時間復雜度,最后順便一提,二叉堆可用於實現堆排序的算法。

#include <iostream>
#include <vector>

class Bheap{//默認最小堆
public:
    typedef int Element;
    Bheap(const Element &Most);
    Bheap(Element *arr, int size, const Element &Most);
    ~Bheap();
    void push(const Element &e);
    void pop();
    bool empty();
    const Element& top();
    unsigned size();
    void sort();
private:
    std::vector<Element> data;
    Element MostEle;
    void PercolateDown(int loca);
    void PercolateUp(int loca);
};

void Bheap::PercolateDown(int loca){
    unsigned j;
    Element cur = data[loca];
    for (j = loca; j*2<size();){
        j *=2;
        if (j + 1 < size() && data[j + 1] < data[j]){
            j++;
        }
        if (data[j]<cur)
            data[j / 2] = data[j];
        else{
            j /= 2;
            break;
        }
    }
    data[j] = cur;
}
void Bheap::PercolateUp(int loca){
    Element e = data[loca];
    while (1){
        if (e<data[loca /2]){
            data[loca] = data[loca / 2];
            loca /=2;
        }
        else{
            break;
        }
    }
    data[loca] = e;
}
Bheap::~Bheap()
{
}
Bheap::Bheap(Element *arr, int size, const Element &Most) :data(1, Most), MostEle(Most){
    for (int i = 0; i < size; i++){
        data.push_back(arr[i]);
    }
    for (int i = ((this->size()) /2); i > 0; i--){
        PercolateDown(i);
    }
}
Bheap::Bheap(const Element& Most) : data(1, Most), MostEle(Most)
{
}
void Bheap::push(const Element &e){
    data.push_back(e);
    PercolateUp(data.size() - 1);
}
void Bheap::pop(){
    data[1] = data[data.size() - 1];
    PercolateDown(1);
    data.pop_back();
}
bool Bheap::empty(){
    return data.size() <= 1;
}
const Bheap::Element& Bheap::top(){
    return data[1];
}
unsigned Bheap::size(){
    return data.size() - 1;
}
void Bheap::sort(){
    for (int i = data.size() - 1; i > 0; i--){
        Element t = data[1];
        data[1] = data[i];
        data[i] = t;
        int j;
        Element cur = data[1];
        for (j = 1; j * 2<i;){
            j *=2;
            if (j + 1 < i && data[j + 1] < data[j]){
                j++;
            }
            if (data[j]<cur)
                data[j / 2] = data[j];
            else{
                j /= 2;
                break;
            }
        }
        data[j] = cur;
    }
}
View Code

 

左式堆:

左式堆是一顆二叉樹,一般來說,我們更都喜歡平衡的樹,平衡的樹深度小,各種操作起來效率都更高,但是,左式堆卻反其道而行之,最求不平衡的樹,在實現avl樹時,我們使用了左右子樹的深度來作為樹是否“比較平衡”的指標,而在左式堆中,我們也有類似的指標,npl(NULL path length),規定子節點數為0與1的節點的npl為0,空節點的npl為-1,任意節點的npl為他所有子節點中npl小的節點的npl+1。左式堆就是保持任意節點的左子樹npl始終大於等於右子樹的npl,這樣的效果就是左子樹始終比右子樹深,雖然左子樹深了,但是右子樹就淺了啊,於是就可以大概保證push,pop操作為log2N,而且,不同於二叉堆,左式堆可以高效地支持合並操作,時間復雜度也是log2N左右,甚至,左式堆的所有操作都是基於合並操作。那么具體是怎么合並呢?因為是堆,於是保持着堆序,對於兩個左式堆,我們比較根處的值,將小的堆的右子樹與大的堆合並,並作為小的堆新的右子樹,並且要更新npl,並根據npl決定是否交換子樹,上面說過,左子樹深了,右子樹就淺了,於是就不用擔心遞歸實現會爆棧來,因為,右子樹非常淺(不大於log2N),深到遞歸實現會爆棧所需的數據是誇張的,所有可以放心地遞歸實現,但是我用了堆棧做了非遞歸實現(說了那么多,你居然沒有用遞歸)。

#include <iostream>
#include <vector>
#include <stack>

class Lheap
{
private:
    typedef int Element;
    struct LheapNode
    {
        Element e;
        LheapNode*left;
        LheapNode*right;
        int npl;
        LheapNode();
        ~LheapNode();
        void swapchild();
        void renewnpl();
        const LheapNode& operator = (const LheapNode&a);
    };
    LheapNode *node;
    int Size;
    void Merge1(Lheap &a);
public:
    Lheap();
    Lheap(const Element &E);
    Lheap(const LheapNode* a);
    Lheap(const Element *arr, int size);
    ~Lheap();
    void Merge(const Lheap &a);
    void pop();
    const Element &top();
    bool empty();
    int size()const;
    void push(const Element &E);
};

Lheap::LheapNode::LheapNode() :left(NULL), right(NULL), npl(0){}
Lheap::LheapNode::~LheapNode(){
    delete left;
    delete right;
}
void Lheap::LheapNode::renewnpl(){
    if (right){
        if (left){
            if (right->npl > left->npl){
                swapchild();
            }
            npl = right->npl + 1;
        }
        else{
            swapchild();
            npl = 0;
        }
    }
    else{
        npl = 0;
    }
}
void Lheap::LheapNode::swapchild(){
    LheapNode* t = left;
    left = right;
    right = t;
}
const Lheap::LheapNode& Lheap::LheapNode:: operator = (const LheapNode&a){
    if (a.left){
        if (left == NULL)
            left = new LheapNode;
        *left = *a.left;
    }
    if (a.right){
        if (right == NULL)
            right = new LheapNode;
        *right = *a.right;
    }
    npl = a.npl;
    e = a.e;
    return *this;
}

Lheap::Lheap() :node(NULL),Size(0)
{
}
Lheap::Lheap(const Element *arr, int size) : node(NULL), Size(0)
{
    node = new LheapNode;
    node->e = arr[0];
    for (int i = 1; i < size; i++)
        push(arr[i]);
}
Lheap::Lheap(const Element &E) : node(NULL), Size(0){
    node = new LheapNode;
    node->e = E;
}
Lheap::~Lheap()
{
    delete node;
}
bool Lheap::empty(){
    return node == NULL;
}
void Lheap::Merge1(Lheap &a){
    if (node == NULL){
        node = a.node;
        a.node = NULL;
        return;
    }
    std::stack<LheapNode*> s;
    s.push(node);
    LheapNode *b = a.node,*t;
    while (1){
        if (s.top()->e < b->e){
            if (s.top()->right){
                s.push(s.top()->right);
            }
            else{
                break;
            }
        }
        else{
            if (b->right){
                t = s.top();s.pop();
                s.push(b);
                b = t;
                s.push(s.top()->right);
            }
            else{
                t = s.top();s.pop();
                s.push(b);
                b = t;
                break;
            }
        }
    }
    while (!s.empty()){
        t = s.top(); s.pop();
        t->right = b;
        b = t;
        b->renewnpl();
    }
    node = b;
    a.node = NULL;
}
void Lheap::Merge(const Lheap &a){
    Lheap t;
    t.node = new LheapNode;
    *t.node = *a.node;//此處可根據需要選擇是否要保留被並入的堆,不需要的話直接Merge1(a)就好了
    Merge1(t);
    Size += a.Size;
}
void Lheap::push(const Element &E){
    Lheap t(E);
    Merge1(t);
    Size++;
}
void Lheap::pop(){
    LheapNode *L = node->left,*R=node->right;
    node->left = node->right =NULL;
    delete node;
    node = L;
    if (R){
        Lheap t;
        t.node = R;
        Merge1(t);
    }
    Size--;
}
const Lheap::Element& Lheap::top(){
    return node->e;
}
int Lheap::size()const{
    return Size;
}
View Code

二項隊列:

二項樹,是一種特殊的樹,他的特點是,我們把一棵有2^k個節點的樹叫做K樹,k樹有k-1個子樹,分別是0到k-1樹,而二項隊列就是按k樹k值大小排列的隊列(說好的隊列呢),此外,二項隊列有一個最大的特點,那就是每種樹都只能存在一棵,如果存在兩顆呢?那我們就要將他們合並為一棵樹,比如說,如果有兩顆0樹,那么,我們就要將他們合並為一棵1樹,怎么合並呢?想想,我們要構造的是堆,於是二項樹就必須符合堆序,那我們就只要將根值大的作為小的的子樹。說到這些,有沒有覺得熟悉呢?我們看一下二進制的加法,

1001  8+0+0+1

0011  0+0+2+1

1100  8+4+0+0

明白了吧,二項隊列的具體結構與插入的值無關,雖然他是個堆,但是他長得怎么樣,和你給他什么值沒什么關系,很奇怪對不對,但真的是這樣,二項隊列的結構取決於插入元素的個數。二項隊列的特質,使他唯一對應於一個二進制數字,於是我們可以利用兩個堆大小對應二進制數字的加法來控制流程。然后,我們對它的操作做分析,首先對合並操作進行分析,首先,我們知道,對於一個有N個元素的二項隊列,長度不超過Log2N,因為,一個二項隊列對應一個二進制整數。合並相當於一次加法運算,二項隊列的合並就是對其隊列中的二項樹自小到大進行合並,插入下一位的操作(進位),那么復雜度是O(log2N),pop操作則是在隊列中尋找根最小的二項樹(花費Log2N),將其子樹當成新隊列,與除去了二項隊列中該樹,再將該隊列與新隊列合並,操作也是log2N,最后,就是push操作,二項隊列最大的特點就是,雖然不能保證每次操作都是o(1)的時間復雜度,但是可以保證M次push操作,平均的時間復雜度是(1 ),為什么呢?我們來分析一下,對於push操作,相當於一個二項隊列與對應1的二項隊列相加。首先,先明白這樣一件事,push操作花費的時間取決於二項隊列結尾出現第一個0之前1的個數,比如說,00就只需要一次插入,01就需要一次插入,一次合並,011就需要一次插入,兩次合並,假設一個二項隊列在進行push操作前大小是k,通過M次插入操作變為k+M,該過程中,最低位為1的概率有1/2,於是有一半的數需要一次合並,而顯然剩下的樹不需要再合並,然后在這一半的數中,又有一半的數第二位為1,他們需要第二次合並,而另一半則不需要,依此類推,從k到k+M-1這M個數,所需合並次數為 M/2+M/4+M/8....+1,假設M為偶數(因為奇數結果也差不了多少),則為M(1/2+1/4+。。。+1/M)=M-1,此外,每次操作無論合並幾次,都要插入一次,於是M-1次合並,M次插入,平均需要O(M-1+M) / M = O(1)的時間復雜度。這種分析方式就是所謂的攤還分析了,有的數據結構,比如伸展樹,比如斜堆,都有雖然不能保證每次操作是O(x),但是能保證M次操作為O(Mx)的特點,這種分析比較復雜,我到現在都不是很會,於是你會發現我在分析上面兩種實現時草草帶過,二叉堆是因為真的沒有什么好說的,左式堆是因為我真的沒有什么會說的。。。還是好好學習一下怎么進行攤還分析吧。另外,優先隊列的實現中沒有斜堆,因為他和左式堆差不多,區別就像AVL樹與伸展樹差不多,他不會通過npl這種東西來判斷是否交換子樹,他只是簡單粗暴地交換每個合並的節點的子樹。。。。最后吐槽一下,說好的隊列,結果我使用deque來作為隊列,但最后才發現,直接開個32大小的數組反而比較實在,畢竟可以存2^31大約10億個數據了,對於我這種沒見過世面的人來說已經妥妥的了。

#include <iostream>
#include <vector>
const int Default_size=32;
class Bqueue
{
private:
    typedef int Element;
    struct BtreeNode
    {
        BtreeNode*child;
        BtreeNode*sibling;
        Element data;
        BtreeNode();
        ~BtreeNode();
    };
    typedef BtreeNode * Btree;
    std::vector<Btree> q;
    int Size;
public:
    Bqueue();
    Bqueue(int size);
    Bqueue(Element *arr, int size);
    ~Bqueue();
    void push(const  Element &e);
    void pop();
    Bqueue& Merge(Bqueue &Q);
    int size()const;
    const  Element& top()const;
    bool empty()const;
    Btree combineTree(Btree T1, Btree T2)const;
};

Bqueue::BtreeNode::BtreeNode():child(NULL), sibling(NULL){}
Bqueue::BtreeNode::~BtreeNode(){
    delete child;
    delete sibling;
}

Bqueue::Bqueue() :Size(0){
    q.resize(Default_size);
}
Bqueue::Bqueue(int size) : Size(size){
    q.resize(Default_size);
}
Bqueue::~Bqueue(){
    for (std::vector<Btree>::iterator p = q.begin(); p != q.end(); p++)
        delete *p;
}
Bqueue& Bqueue::Merge(Bqueue &Q){
    Btree T1, T2,Carry=NULL;
    Size += Q.size();
    for (int i = 0, j = 1;j<=Size; i++, j *= 2){
        T1 = q[i]; T2 = Q.q[i];
        switch (!!T1+2*!!T2+4*!!Carry)
        {
        case 0:
        case 1:
            break;
        case 2:
            q[i] = T2;
            Q.q[i] = NULL;
            break;
        case 3:
            Carry = combineTree(T1, T2);
            q[i] = Q.q[i] = NULL;
            break;
        case 4:
            q[i] = Carry;
            Carry = NULL; 
            break;
        case 5:
            Carry = combineTree(T1, Carry);
            q[i] = NULL;
            break;
        case 6:
            Carry = combineTree(Carry, T2);
            Q.q[i] = NULL;
            break;
        case 7:
            q[i] = Carry;
            Carry = combineTree(T1, T2);
            Q.q[i] = NULL;
            break;
        default:
            break;
        }
    }
    return *this;
}
int Bqueue::size()const{
    return Size;
}
void Bqueue::push(const Element &e){
    Btree T = new BtreeNode,T1;
    T->data = e;
    Size += 1;
    for (int i = 0,j=1; j <= Size&&T; i++,j*=2){
        T1 = q[i];
        switch (!!T*2+!!T1){
        case 2:
            q[i] = T;
            T = NULL;
                break;
        case 3:
            q[i] = NULL;
            T=combineTree(T, T1);
                break;
        default:
            break;
        }
    }
}
void Bqueue::pop(){
        int i, j, Mintree=0;
        Btree DeletedTree, Oldroot;
        while (q[Mintree]==NULL)
        {
            Mintree++;
        }
        for (i = Mintree+1, j = 1<<i; j <= Size; j *= 2,i++)
            if (q[i])
            Mintree = q[Mintree]->data<q[i]->data?Mintree:i;
        Oldroot = DeletedTree = q[Mintree];
        DeletedTree = DeletedTree->child;
        Oldroot->child = NULL;
        delete Oldroot;
        Bqueue DeleteQueue((1 << Mintree) - 1);
        for (i = Mintree - 1; i >= 0; i--){
            DeleteQueue.q[i] = DeletedTree;
            DeletedTree = DeletedTree->sibling;
            DeleteQueue.q[i]->sibling = NULL;
        }
        q[Mintree] = NULL;
        Size -= DeleteQueue.size() + 1;
        Merge(DeleteQueue);
}
bool Bqueue::empty()const{
    return Size == 0;
}
const Bqueue:: Element& Bqueue::top()const{
    int res=0;
    while (q[res] == NULL)
    {
        res++;
    }
    for (int i = res + 1, j = 1 << i; j <= Size; j *= 2, i++)
        if (q[i])
            res = q[res]->data<q[i]->data ? res : i;
    return q[res]->data;
}
Bqueue::Btree Bqueue::combineTree(Btree T1,Btree T2)const{
        if (T1->data > T2->data)
            return  combineTree(T2, T1);
        T2->sibling = T1->child;
        T1->child = T2;
        return T1;
    }
View Code

最后,我們實際看一下各種實現的效果,我們用合並果子這道經典貪心算法來測試一下。

二叉堆:

  • 測試點1 Accepted / 1ms / 12396kB
  • 測試點2 Accepted / 2ms / 12396kB
  • 測試點3 Accepted / 2ms / 12396kB
  • 測試點4 Accepted / 19ms / 12396kB
  • 測試點5 Accepted / 18ms / 12396kB
  • 測試點6 Accepted / 51ms / 12396kB
  • 測試點7 Accepted / 50ms / 12396kB
  • 測試點8 Accepted / 45ms / 12396kB
  • 測試點9 Accepted / 53ms / 12396kB
  • 測試點10 Accepted / 50ms / 12396kB

STL 的優先隊列《vector》

  

  • 測試點1 Accepted / 5ms / 12400kB
  • 測試點2 Accepted / 8ms / 12400kB
  • 測試點3 Accepted / 14ms / 12400kB
  • 測試點4 Accepted / 36ms / 12400kB
  • 測試點5 Accepted / 45ms / 12400kB
  • 測試點6 Accepted / 85ms / 12400kB
  • 測試點7 Accepted / 84ms / 12400kB
  • 測試點8 Accepted / 80ms / 12400kB
  • 測試點9 Accepted / 85ms / 12400kB
  • 測試點10 Accepted / 87ms / 12400kB

二項隊列:

  

  • 測試點1 Accepted / 3ms / 12396kB
  • 測試點2 Accepted / 17ms / 12396kB
  • 測試點3 Accepted / 20ms / 12396kB
  • 測試點4 Accepted / 77ms / 12396kB
  • 測試點5 Accepted / 101ms / 12528kB
  • 測試點6 Accepted / 202ms / 12660kB
  • 測試點7 Accepted / 196ms / 12660kB
  • 測試點8 Accepted / 195ms / 12660kB
  • 測試點9 Accepted / 211ms / 12660kB
  • 測試點10 Accepted / 190ms / 12660kB

左式堆:
  

  • 測試點1 Accepted / 2ms / 12400kB
  • 測試點2 Accepted / 29ms / 12400kB
  • 測試點3 Accepted / 30ms / 12400kB
  • 測試點4 Accepted / 143ms / 12528kB
  • 測試點5 Accepted / 164ms / 12528kB
  • 測試點6 Accepted / 391ms / 12784kB
  • 測試點7 Accepted / 379ms / 12784kB
  • 測試點8 Accepted / 381ms / 12784kB
  • 測試點9 Accepted / 376ms / 12784kB
  • 測試點10 Accepted / 357ms / 12784kB

可以看出,簡單除暴的二叉堆效率是最高的,左式堆最不給力。。。

以上代碼參考自《數據結構與算法分析:c語言描述》

 


免責聲明!

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



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