二叉樹是一種常用的數據結構,熟練掌握二叉樹的各種算法,是必須的。本科時學過數據結構課程,但因當時課程繁多,且對很多概念理論不甚熟悉,數據結構課程也是得過且過,雖言考試順利通過,但有許多盲點,還有很多知識點,雖然知道其基本思路,但一旦說要動手實現,抓耳撓腮半響,還是一堆的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];
}
以上算法我都一一實現過,對於二叉樹的知識點涵蓋較全,除了沒有線索二叉樹以及中後序創建二叉樹外。一一實現大約花費了我一個星期的工夫,花了這麼長時間,也真是值了,終於把盲點清掃一空,把二叉樹弄完,下一步開始圖算法的清掃了。
二叉樹及哈夫曼樹
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.