二叉樹及哈夫曼樹

二叉樹是一種常用的數據結構,熟練掌握二叉樹的各種算法,是必須的。本科時學過數據結構課程,但因當時課程繁多,且對很多概念理論不甚熟悉,數據結構課程也是得過且過,雖言考試順利通過,但有許多盲點,還有很多知識點,雖然知道其基本思路,但一旦說要動手實現,抓耳撓腮半響,還是一堆的error。痛定思痛,與其忍受這種痛苦,還不若痛下決心將其一一擊破。

問題描述一:
假設二叉樹採用二叉鏈存儲結構,設計一個算法,輸出從每個葉子結點到根結點的路徑。

有一個好的想法是簡單,但要實現它可不是容易的事,這在編程中經常體現。我使用的李春葆李老師的《數據結構》一書中使用了隊列實現,設計的隊列爲非循環隊列,將所有已掃描過的結點指針進隊列,並在隊列中保存雙親結點的位置。當找到一個葉子結點時,在隊列中通過雙親結點的位置輸出該葉子結點到根結點的路徑。

這種方法是可行的。但因爲我先前用堆實現過二叉樹遍歷的非遞歸算法,這一次我便想嘗試看用堆是否能夠實現。實現思想是先將根結點的所有左子結點入棧,並置所有棧內結點的標記爲0,代表是其右節點還未訪問,而後彈出棧頂結點,輸出其到根結點路徑,再取棧頂結點(它應該是剛彈出棧頂結點之父結點),將其標記置1, 代表其右結點已經訪問,然後將其右子節點與其右子結點的所有左子結點入棧,並置入棧結點標記爲0, 彈出棧頂結點,輸出其到根結點路徑,並不斷彈出標記爲1的棧頂結點,再取棧頂結點,將其標記置1,代表其右結點已經訪問,如上循環,直至棧空。

簡要思想是:將所有左結點入棧,輸出,彈出左右結點都已訪問的父結點,再訪問右結點。

算法實現如下:
void leaf2root(BiNode* _node)
{
    struct stack
    {
        BiNode* data;
        int flag;
    }st[MAX];
    int top=-1;
    BiNode* p, *s;

    while(_node!=NULL)
    {
        top++;
        st[top].data=_node;
        st[top].flag=0;
        _node=_node->lchild;
    }     //壓入根結點及其所有左子結點,並設標記爲0

    while(top>-1)
    {
        p=st[top].data;
        if(p->lchild==NULL&&p->rchild==NULL)
        {
            for(int i=top;i>=0;i--)
                cout<<st[i].data->data<<"->";
            cout<<endl;
            top--;
        }     //輸出葉子結點到根結點的路徑

        while(top>-1&&st[top].flag==1)
        {
            top--;
        }      //如果棧頂結點是父節點,且其左右子結點均已訪問過,就彈出之

        if(top>-1)
        {
            st[top].flag=1;
            p=st[top].data;
            s=p->rchild;
            while(s!=NULL)
            {
                top++;
                st[top].data=s;
                st[top].flag=0;
                s=s->lchild;
            }     //將棧頂結點的右子結點及其右子結點的所有左子結點壓棧,並設標記爲0
        }
    }
}

李老師的算法實現如下:
void leaf2root_1(BiNode* _node)
{
    struct snode
    {
        BiNode* node;
        int parent;
    }q[MAX];
    int front, rear, p;
    front=rear=-1;
    rear++;
    q[rear].node=_node;
    q[rear].parent=-1;
    while(front<rear)
    {
        front++;
        _node=q[front].node;
        if(_node->lchild==NULL&&_node->rchild==NULL)
        {
            p=front;
            while(q[p].parent!=-1)
            {
                cout<<q[p].node->data<<"->";
                p=q[p].parent;
            }
            cout<<q[p].node->data<<endl;
        }
        if(_node->lchild!=NULL)
        {
            rear++;
            q[rear].node=_node->lchild;
            q[rear].parent=front;
        }
        if(_node->rchild!=NULL)
        {
            rear++;
            q[rear].node=_node->rchild;
            q[rear].parent=front;
        }
    }
}

問題描述二:如何創建二叉樹?

二叉樹的創建既可用遞歸算法實現,也可用非遞歸實現。

遞歸實現:
輸入採用先序輸入,先構建根結點,而後左結點,右結點
void create_bitree(BiNode*& _node)
{
    char c;
    cin>>c;
    if(c=='*')
    {
        _node=NULL;
        return;
    }
    else
    {
        _node=(BiNode*)malloc(sizeof(BiNode));
        _node->data=c;
        _node->lchild=NULL;
        _node->rchild=NULL;
        create_bitree(_node->lchild);
        create_bitree(_node->rchild);
    }
    return;
}

非遞歸實現:
非遞歸實現使用一個string類型的字符串,將二叉樹序列用字符串表示起來,用逗號將左右結點隔開,用()將左右結點包括,表示一個結點的子結點。遍歷輸入字符串,若爲(,說明下一個輸入應該是左結點,若爲,,說明下一個結點應該是右結點,若爲),說明有一個結點輸入完畢,可以彈棧;若爲字母,壓棧。。。
void create_bitree_1(BiNode*& root, string str)
{
    BiNode* st[MAX], *p;
    int top=-1,flag=0;
    string::size_type i=0;
    p=root;
    while(i<str.size())
    {
        switch(str[i])
        {
            case '(':
                flag=1;
                break;
            case ',':
                if(str[i-1]!='(')
                    top--;
                flag=2;
                break;
            case ')':
                if(str[i-1]!=',')
                    top--;
                break;
            default :
                p=(BiNode*)malloc(sizeof(BiNode));
                p->data=str[i];
                p->lchild=NULL;
                p->rchild=NULL;

                if(flag==1)
                {
                    st[top]->lchild=p;
                }

                if(flag==2)
                {
                    st[top]->rchild=p;
                }
                top++;
                st[top]=p;
                if(i==0)
                    root=p;
                p=NULL;
        }
        i++;
    }
}

問題描述3:先序,中序,後序實現二叉樹遍歷

遍歷也有遞歸及非遞歸實現,遞歸實現起來較爲簡單,非遞歸實現用棧保存信息,較爲麻煩,但常是考點。

遞歸算法:
void pre_order(BiNode* _node)
{
    if(_node==NULL)
        return;
    cout<<_node->data<<" ";
    pre_order(_node->lchild);
    pre_order(_node->rchild);
}

void in_order(BiNode* _node)
{
    if(_node==NULL)
        return;
    in_order(_node->lchild);
    cout<<_node->data<<" ";
    in_order(_node->rchild);
}

void post_order(BiNode* _node)
{
    if(_node==NULL)
        return;
    post_order(_node->lchild);
    post_order(_node->rchild);
    cout<<_node->data<<" ";
}

非遞歸算法
先序算法1:
先將(根結點指針,1)進棧;
while(棧不空)
{
    if(棧頂元素未訪問過)
    {
        p=st[top].pt; 退棧;
        將*p結點的右孩子結點指針進棧,tag=1;
        將*p結點的左孩子結點指針進棧,tag=1;
        將*p結點指針進棧,tag=0;
    }
    else if(棧頂元素可直接訪問)
        訪問棧頂元素並退棧;
}
其中心思想也就是先將右孩子結點,再左孩子結點一一進棧,最後父親結點進棧,始終保證父親結點在孩子結點之前
void pre_order_1(BiNode* _node)
{
    struct stack_node
    {
        bool flag;
        BiNode* pt;
    }stack[MAX];
    int top=-1;

    top++;
    stack[top].flag=false;
    stack[top].pt=_node;

    while(top>-1)
    {
        if(!stack[top].flag)
        {
            BiNode* p=stack[top].pt;
            top--;
            if(p!=NULL)
            {
                top++;
                stack[top].flag=false;
                stack[top].pt=p->rchild;
                top++;
                stack[top].flag=false;
                stack[top].pt=p->lchild;
                top++;
                stack[top].flag=true;
                stack[top].pt=p;
            }
        }

        if(stack[top].flag&&top>=0)
        {
            cout<<(stack[top].pt)->data<<" ";
            top--;
        }
    }
}

中序算法1
中序算法1和先序算法1的思想相似,不過進棧順序有些變化,它先將右孩子結點進棧,再父親,再左孩子結點進棧
void in_order_1(BiNode* _node)
{
    struct stack_node
    {
        bool flag;
        BiNode* pt;
    }stack[MAX];
    int top=-1;

    top++;
    stack[top].flag=false;
    stack[top].pt=_node;

    while(top>-1)
    {
        if(!stack[top].flag)
        {
            BiNode* p=stack[top].pt;
            top--;

            if(p!=NULL)
            {
                top++;
                stack[top].flag=false;
                stack[top].pt=p->rchild;
                top++;
                stack[top].flag=true;
                stack[top].pt=p;
                top++;
                stack[top].flag=false;
                stack[top].pt=p->lchild;
            }
        }

        if(stack[top].flag&&top>-1)
        {
            cout<<stack[top].pt->data<<" ";
            top--;
        }
    }
}

後序算法1
後序算法1和先序算法1的思想亦類似,不過進棧順序不一樣,它先將父親結點進棧,再右孩子,再左孩子進棧
void post_order_1(BiNode* _node)
{
    struct stack_node
    {
        bool flag;
        BiNode* pt;
    }stack[MAX];
    int top=-1;

    top++;
    stack[top].flag=false;
    stack[top].pt=_node;

    while(top>-1)
    {
        if(stack[top].flag==false)
        {
            BiNode* p=stack[top].pt;
            top--;

            if(p!=NULL)
            {   
                top++;
                stack[top].flag=true;
                stack[top].pt=p;
                top++;
                stack[top].flag=false;
                stack[top].pt=p->rchild;
                top++;
                stack[top].flag=false;
                stack[top].pt=p->lchild;
            }
        }

        if(stack[top].flag&&top>-1)
        {
            cout<<stack[top].pt->data<<" ";
            top--;
        }
    }
}

先序算法2:
由先序遍歷過程可知,先訪問根結點,再訪問左子樹,最後訪問右子樹,因此,先將根結點進棧,在棧不空時循環,出棧p, 訪問*p結點,將其右孩子結點進棧,再將左孩子結點進棧
void post_order_1(BiNode* _node)
{
    struct stack_node
    {
        bool flag;
        BiNode* pt;
    }stack[MAX];
    int top=-1;

    top++;
    stack[top].flag=false;
    stack[top].pt=_node;

    while(top>-1)
    {
        if(stack[top].flag==false)
        {
            BiNode* p=stack[top].pt;
            top--;

            if(p!=NULL)
            {   
                top++;
                stack[top].flag=true;
                stack[top].pt=p;
                top++;
                stack[top].flag=false;
                stack[top].pt=p->rchild;
                top++;
                stack[top].flag=false;
                stack[top].pt=p->lchild;
            }
        }

        if(stack[top].flag&&top>-1)
        {
            cout<<stack[top].pt->data<<" ";
            top--;
        }
    }
}

中序遍歷算法2:
由中序遍歷過程可知,採用一個棧保存需要返回的結點指針,先掃描根結點的所有左結點將它們一一進棧,然後出棧一個結點,顯然該結點沒有左孩子結點或者左孩子結點已訪問過,訪問它,然後將該結點的右孩子結點,進棧,再將該右孩子結點的所有左孩子結點一一進棧,如此這樣,直到棧空爲止
void in_order_2(BiNode* _node)
{
    BiNode* st[MAX],*p;
    int top=-1;

    if(_node!=NULL)
    {
        p=_node;

        while(top>-1||p!=NULL)
        {
            while(p!=NULL)
            {   
                top++;
                st[top]=p;
                p=p->lchild;
            }

            if(top>-1)
            {
                p=st[top];
                top--;
                cout<<p->data<<" ";
                p=p->rchild;
            }
        }
    }
}

後序遍歷算法2:
後序遍歷採用一個棧保存需要返回的結點指針,先掃描根結點的所有左結點一一進棧,出棧一個結點,將該結點的右孩子結點入棧,再掃描該右孩子結點的所有左結點入棧。當一個結點的左右孩子結點均訪問後再訪問該結點,如此這樣,直到棧空。顯然,難點在於如何判斷左右結點俱已訪問。
void post_order_2(BiNode* _node)
{
    BiNode* st[MAX],*p;
    int flag,top=-1;
    if(_node!=NULL)
    {
        do
        {
            while(_node!=NULL)
            {
                top++;
                st[top]=_node;
                _node=_node->lchild;
            }
           
            p=NULL;
            flag=1;

            while(top!=-1&&flag)
            {
                _node=st[top];
                if(_node->rchild==p)
                {
                    cout<<_node->data<<" ";
                    top--;
                    p=_node;
                }
                else
                {
                    _node=_node->rchild;
                    flag=0;
                }
            }
        }while(top!=-1);
    }
}

問題描述3:如何銷燬二叉樹?
在二叉樹創建時,手動申請了內存,因此也要手動進行釋放,釋放過程中先保存父親結點指針,將父親結點佔用空間刪除後,再將左孩子及右孩子結點佔用的空間刪除
void del_tree(BiNode* _node)
{
    BiNode* left;
    BiNode* right;
    if(_node!=NULL)
    {
        left=_node->lchild;
        right=_node->rchild;
        delete _node;

        del_tree(left);
        del_tree(right);
    }
}

以下是一些簡單的算法實現,實現諸如二叉樹的高度,寬度等等
BiNode* find_node(BiNode* _node, char c)
{
    if(_node==NULL)
        return NULL;
    else if(_node->data==c)
        return _node;
    else
    {
        if(find_node(_node->lchild,c)!=NULL)
            return _node->lchild;
        else
            return find_node(_node->rchild,c);
    }
}

BiNode* lchild(BiNode* _node)
{
    return _node->lchild;
}

BiNode* rchild(BiNode* _node)
{
    return _node->rchild;
}

int tree_height(BiNode* _node)
{
    if(_node==NULL)
        return 0;
    int left=tree_height(_node->lchild);
    int right=tree_height(_node->rchild);
    return left>right?left+1:right+1;;
}

int tree_width(BiNode* _node)
{
    if(_node==NULL)
        return 0;
    if(_node->lchild==NULL&&_node->rchild==NULL)
        return 1;
    int left=tree_width(_node->lchild);
    int right=tree_width(_node->rchild);
    return left+right;
}

int node_total(BiNode* _node)
{
    if(_node==NULL)
        return 0;
    int left=node_total(_node->lchild);
    int right=node_total(_node->rchild);
    return left+right+1;
}

問題描述4:如何層次遍歷二叉樹?
層次遍歷有兩種方法
第一種方法,我們可以使用兩個棧a,和b
開始將根結點壓入棧a
while(棧a及棧b都不空)
{
    while(棧a不空)
    {
        將a中所有元素彈棧,輸出
        將a中所有元素的孩子結點壓入棧b
    }
    while(棧b不空)
    {
        將b中所有元素彈棧,輸出
        將b中所有元素的孩子結點壓入棧a
    }
}
算法實現如下:
void layer_order(BiNode* _node)
{
    BiNode* st_1[MAX], *st_2[MAX],*p;
    int top_1=-1,top_2=-1;

    if(_node==NULL)
        return;
    top_1++;
    st_1[top_1]=_node;

    while(top_1>-1||top_2>-1)
    {
        while(top_1>-1)
        {
            p=st_1[top_1];
            top_1--;
            cout<<p->data<<" ";
            if(p->rchild!=NULL)
            {
                top_2++;
                st_2[top_2]=p->rchild;
            }
            if(p->lchild!=NULL)
            {
                top_2++;
                st_2[top_2]=p->lchild;
            }
        }
        cout<<endl;
        while(top_2>-1)
        {
            p=st_2[top_2];
            top_2--;
            cout<<p->data<<" ";
            if(p->lchild!=NULL)
            {
                top_1++;
                st_1[top_1]=p->lchild;
            }
            if(p->rchild!=NULL)
            {
                top_1++;
                st_1[top_1]=p->rchild;
            }
        }
        cout<<endl;
    }
}

第二種方法稍難理解些,採用遞歸算法,設h返回p所指結點的高度,其初值爲0.找到指定結點時,返回其層次,否則返回0.lh作爲一箇中間變量在計算搜索層次時使用,其初值爲1.
void level(BiNode* _node,char data,int &h, int lh)
{
    if(_node==NULL)
        h=0;
    else if(_node->data==data)
        h=lh;
    else
    {
        level(_node->lchild,data,h,lh+1);
        if(h==0)
            level(_node->rchild,data,h,lh+1);
    }
}

問題描述5:輸出二叉樹
其實以上的遍歷算法也對二叉樹進行了輸出,但是少了一種輸出方法,在創建的時候,我們曾用過A(B,C)這種類似的格式來創建二叉樹,而今我們也想以這種方式進行輸出,那該怎麼辦呢?
void disp_tree(BiNode* _node)
{
    if(_node!=NULL)
    {
        cout<<_node->data;
        if(_node->lchild==NULL&&_node->rchild==NULL)
            return;
        cout<<"(";
        if(_node->lchild!=NULL)
            disp_tree(_node->lchild);
        cout<<",";
        if(_node->rchild!=NULL)
            disp_tree(_node->rchild);
        cout<<")";
    }
}

問題描述6:如何構建哈夫曼樹
構建哈夫曼樹稍爲簡單
首先創建一個vector,用來保存哈夫曼樹結點的指針。
while(vector的大小!=1)
{
    找出vector中最小的兩個元素,用left right表示;
    將最小的兩個元素從vector中刪除;
    取left right的權重和,新創建一個結點,插入vector;
}
HTNode* create_HTTree(vector<HTNode*>& list_htnode)
{
    int smallest1,smallest2;
    vector<HTNode*>::const_iterator beg;
    HTNode* left,*right;
    while(list_htnode.size()>1)
    {
        smallest1=htnode_smallest(list_htnode);
        left=*(list_htnode.begin()+smallest1);
        list_htnode.erase(list_htnode.begin()+smallest1);
        smallest2=htnode_smallest(list_htnode);
        right=*(list_htnode.begin()+smallest2);
        list_htnode.erase(list_htnode.begin()+smallest2);

        HTNode* temp_root=(HTNode*)malloc(sizeof(HTNode));
               temp_root->weight=left->weight+right->weight;
        temp_root->lchild=left;
        temp_root->rchild=right;
        list_htnode.push_back(temp_root);
    }
    return list_htnode[0];
}

以上算法我都一一實現過,對於二叉樹的知識點涵蓋較全,除了沒有線索二叉樹以及中後序創建二叉樹外。一一實現大約花費了我一個星期的工夫,花了這麼長時間,也真是值了,終於把盲點清掃一空,把二叉樹弄完,下一步開始圖算法的清掃了。

發佈了46 篇原創文章 · 獲贊 79 · 訪問量 25萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章