樹-專題模板
技術標籤:樹
文章目錄
1.基礎概念
一、幾個性質
(1)森林是若干樹的集合。
(2)樹可以沒有結點(稱為空樹)。
(3)樹的層次(深度)是從根節點(從上往下增大)算的;而樹的高度是從底層葉子結點(從下往上增大)算的。
(4)遞迴邊界:地址root為空,即root==NULL
(而非*root==NULL),表示這個結點不存在,而括號是指結點存在但內容為NULL(沒有內容)。
1.當根結點編號為1時,則編號為
x
的結點的左孩子的編號為2x
,右孩子為2x+1
。2.陣列大小設為結點上限個數+1。
3.判斷某結點是否為葉結點:
該結點(記下標為root)的左子結點的編號
root*2
大於結點總個數n(不需要判斷右子結點)。4.判斷某結點是否為空結點的標誌:
該結點下標
root
大於結點總個數n。(6)靜態二叉連結串列的各個操作程式碼,其實就是結點的左右指標域用int型代替,用來 表示左右子樹的根結點在陣列中的下標。
區別——建立一個大小為結點上限個數的node
型陣列,所有動態生成的結點都直接使用陣列中的結點,所有對指標的操作都改為對陣列下標的訪問。
二、二叉樹的儲存結構
二叉連結串列定義
struct node{
typename data;
node* lchild;
node* rchild;
};
若用靜態二叉連結串列,則結點的左右指標用int型——表示左右子樹的根結點在陣列的下標(1、2、3…)
struct node{
typename data;
int lchild;
int rchild;
}Node[maxn];//結點陣列,maxn為結點上限個數
新建結點(如往二叉樹中插入結點時)
//生成一個新結點,v為結點權值
node* newNode(int v){
node* Node=new node;//申請一個node型變數的地址空間
Node->data=v;
Node->lchild=Node->rchild=NULL;//初始狀態下沒有左右孩子
return Node;//返回新建結點的地址
}
同理靜態二叉連結串列插入結點:
int index=0;
int newNode(int v){//分配一個Node陣列中的結點給新的結點,index為其下標
Node[index].data=v;
Node[index].lchild=-1;//以-1或maxn表示空,因為陣列範圍是0~maxn-1
Node[index].rchild=-1;
return index++;
}
2.二叉樹基本操作
(1)結點的查詢:
void search(node* root,int x,int newdata){
if(root==NULL){
return;//空樹,死衚衕(遞迴邊界)
}
if(root->data==x){
root->data=newdata;
}
search(root->lchild,x,newdata);
search(root->rchild,x,newdata);
}
同理,靜態二叉連結串列的寫法:
//查詢,root為根結點在陣列中的下標
void search(int root,int x,int newdata){
if(root==-1){//用-1代替NULL
return;//空樹,死衚衕(遞迴邊界)
}
if(Node[root].data==x){
Node[root].data=newdata;
}
search(Node[root].lchild,x,newdata);
search(Node[root].rchild,x,newdata);
}
(2)結點的插入:
//insert函式將在二叉樹中插入一個數據域為x的新節點
//注意根結點root要使用引用,否則插入不會成功
void insert(node* &root,int x){
if(root==NULL){
root=newNode(x);
return
}
if(由二叉樹的性質,x應該插在左子樹){
insert(root->lchild,x);
}else{
insert(root->rchild,x);
}
}
上面程式碼注意根結點指標root要用引用&——如果不用引用,root=new node
對root的修改就無法作用到原變數(即上一層的root->lchild
和root->rchild
)上去,即不能把新結點接到二叉樹上。
加引用——如果函式中需要新建結點(對二叉樹的結構修改)就要加引用;如果只是修改當前已有結點的內容,或僅遍歷樹就不用加引用。
同理,靜態二叉連結串列的插入寫法:
//插入,root為根結點在陣列中的下標
void insert(int &root,int x){//記得加引用
if(root==-1){
root=newNode(x);
return;
}
if(由二叉樹的性質,x應該插在左子樹){
insert(Node[root].lchild,x);
}else{
insert(Node[root].rchild,x);
}
}
(3)二叉樹的建立
node* Create(int data[],int n){
node* root=NULL;//新建空根結點root
for(int i=0;i<n;i++){
insert(root,data[i]);
}
return root;
}
同理,靜態二叉連結串列的建樹寫法:
int Create(int data[],int n){
int root=-1;//新建空根結點root
for(int i=0;i<n;i++){
insert(root,data[i]);
}
return root;//返回二叉樹的根結點下標
}
3.遍歷
(1)層序遍歷:
void LayerOrder(node* root){
queue<node*> q;//注意佇列裡是存地址
q.push(root);//將根結點地址入隊
while(!q.empty()){
node* now=q.front();//取出隊首元素
q.pop();
printf("%d ",now->data);//訪問隊首元素
if(now->lchild!=NULL)
q.push(now->lchild);//左子樹非空則加入佇列
if(now->rchild!=NULL)
q.push(now->rchild);
}
}
注意佇列中的元素是node*型,而非node。
——因為佇列中儲存的知識原元素的一個副本,如果佇列中直接存放node型,當需要修改隊首元素時,就無法對原元素進行修改(只是修改了佇列中的副本)。
層序遍歷的靜態二叉連結串列寫法:
void LayerOrder(int root){
queue<int> q;//此處佇列裡存放結點下標
q.push(root);//將根結點下標入隊
while(!q.empty()){
int now=q.front();//取出隊首元素
q.pop();
printf("%d ",Node[now].data);//訪問隊首元素
if(Node[now].lchild!=-1)
q.push(Node[now].lchild);//左子樹非空則加入佇列
if(Node[now].rchild!=-1)
q.push(Node[now].rchild);
}
}
(2)統計layer
在上面層序遍歷的基礎上新增一個layer
的變數。
struct node{
typename data;
node* lchild;
node* rchild;
int layer;//層次
};
1.在根結點入隊前先令根結點的layer為1,表示根結點是第一層(或者是層號是0,根據題意)。
2.在now->lchild
和now->rchild
入隊前,把它們的層號都記為當前結點now的層號加1。
void LayerOrder(node* root){
queue<node*> q;//注意佇列裡是存地址
root->layer=1;//根結點的層號為1
q.push(root);//將根結點地址入隊
while(!q.empty()){
node* now=q.front();//取出隊首元素
q.pop();
printf("%d ",now->data);//訪問隊首元素
if(now->lchild!=NULL)
now->lchild->layer=now->layer+1;
q.push(now->lchild);//左子樹非空則加入佇列
if(now->rchild!=NULL)
now->rchild->layer=now->layer+1;
q.push(now->rchild);
}
}
(3)由先中建樹
已知先序遍歷序列和中序遍歷序列,重建二叉樹。
先序序列:
r
o
o
t
=
p
r
e
L
root=preL
root=preL,
l
e
f
t
=
p
r
e
(
p
r
e
L
+
1
)
.
.
.
.
.
.
p
r
e
(
p
r
e
L
+
n
u
m
L
e
f
t
)
left=pre(preL+1)......pre(preL+numLeft)
left=pre(preL+1)......pre(preL+numLeft),
r
i
g
h
t
=
p
r
e
(
p
r
e
L
+
n
u
m
L
e
f
t
+
1
)
.
.
.
.
.
.
p
r
e
(
p
r
e
R
)
right=pre(preL+numLeft+1)......pre(preR)
right=pre(preL+numLeft+1)......pre(preR)
中序序列:
l
e
f
t
=
i
n
(
i
n
L
)
、
i
n
(
i
n
L
+
1
)
、
.
.
.
i
n
(
k
−
1
)
left=in(inL)、in(inL+1)、...in(k-1)
left=in(inL)、in(inL+1)、...in(k−1),
r
o
o
t
=
i
n
(
k
)
root=in(k)
root=in(k),
r
i
g
h
t
=
i
n
(
k
+
1
)
.
.
.
.
.
i
n
(
i
n
R
)
right=in(k+1).....in(inR)
right=in(k+1).....in(inR)
node* create(int preL,int preR,int inL,int inR){
if(preL>preR){
return NULL;//先序序列長度小於等於0時,直接返回
}
node* root=new node;
root->data=pre[preL];//先序遍歷的第一個結點即根結點
int k;
for(k=inL;k<=inR;k++){
if(in[k]==pre[preL]){//在中序序列中找到根結點的位置,即確定k
break;
}
}
int numLeft=k-inL;//左子樹的結點個數
//左子樹的先序區間為[pre+1,pre+numLeft],中序區間為[inL,k-1]
//返回左子樹的根結點地址,賦值給root的左指標
root->lchild=create(preL+1,pre+numLeft,inL,k-1);
//右子樹的先序區間為[pre+numLeft+1,preR],中序區間為[k+1,inR]
//返回右子樹的根結點地址,賦值給root的右指標
root->rchild=create(pre+numLeft+1,preR,k+1,inR);
return root;//返回根結點地址
}
注意numLeft=k-inL即左子樹的結點個數,不用加1。
結論:中序可與先序、後序、層序序列中的任意一個來構建唯一的二叉樹(一定要有中序,因為先序、後序、層序均是提供根結點,即作用相同,必須由中序序列來區分出左右子樹。)
(4)靜態先序
上面已經給出了靜態二叉連結串列的各個操作程式碼,其實就是用int型 代替 結點的左右指標域,用來表示左右子樹的根結點在陣列中的下標。
區別——建立一個大小為結點上限個數的node
型陣列,所有動態生成的結點都直接使用陣列中的結點,所有對指標的操作都改為對陣列下標的訪問。
void preorder(int root){
if(root==-1)
return;
printf("%d\n",Node[root].data);
preorder(Node[root].lchild);
preorder(Node[root].rchild);
}
4.樹
(1)樹的靜態寫法
struct node{
typename data;
int child[maxn];//指標域,存放所有子結點的下標
}Node[maxn];//結點陣列,maxn為結點上限個數
由於無法預知子結點個數,所以可以使用變長陣列vector定義child陣列,即vector<int> child
存放所有子結點的下標。
與二叉樹的靜態實現類似,當需要新建一個結點時,就按順序從陣列中取出一個下標:
int index=0;
int newNode(int v){
Node[index].data=v;
Node[index].child.clear();//清空子結點
return index++;//返回結點下標,並令index自增
}
不過一般上機考試涉及樹(非二叉樹)的考察,會給出結點的編號(且結點的編號一定是0、1、…N-1,N為結點個數)——不需要newNode函式,因為題目中給定的編號可以直接作為Node陣列的下標使用。
V0:Node[0].child[0]=1,Node[0].child=2,Node[0].child[2]=3;
V1:Node[1].child[0]=4,Node[1].child[1]=5;
如果題目不涉及結點的資料域(只需要樹的結構),可以將一開始樹的結構體可以簡化成vector陣列(去掉結構體元素data
),即vector<int> child[maxn]
——child[0]
、…child[maxn-1]
都是一個vector,存放了各結點的所有子結點下標,這種寫法其實就是圖的鄰接表表示法在樹中的應用。
(2)先根遍歷
樹的先根遍歷:先訪問根結點,再訪問所有子樹。如按(1)的圖先根遍歷序列:V0 V1 V4 V5 V2 V3 V6。
void PreOrder(int root){
printf("%d ",Node[root].data);//訪問當前結點
for(int i=0;i<Node[root].child.size();i++){
PreOrder(Node[root].child[i]);//遞迴訪問結點root的所有子結點
}
}
雖然沒寫明遞迴邊界,但其實遞迴邊界即for迴圈的i小於×這個判斷。
(3)樹層次遍歷
思路和二叉樹一毛一樣,就是while迴圈內的去除當前結點的所有結點(不只是2個)入隊。
void LayerOrder(int root){
queue<int> q;//此處佇列裡存放結點下標
q.push(root);//將根結點下標入隊
while(!q.empty()){
int now=q.front();//取出隊首元素
q.pop();
printf("%d ",Node[now].data);//訪問隊首元素
for(int i=0;i<Node[front].child.size();i++){
Q.push(Node[front].child[i]);//將當前結點的所有子結點入隊
}
}
}
(4)層次遍歷layer
不止有左右結點,所以就在二叉樹版本上將結構體元素改為child陣列:
struct node{
int layer;
int data;
vector<int> child;
}
同理在根結點入隊前,對根結點的層號賦初值;對當前結點的所有子結點入隊前,對他們的層號加1。
void LayerOrder(int root){
queue<int> q;//此處佇列裡存放結點下標
q.push(root);//將根結點下標入隊
Node[root].layer=0;//記根結點層號為0
while(!q.empty()){
int now=q.front();//取出隊首元素
q.pop();
printf("%d ",Node[now].data);//訪問隊首元素
for(int i=0;i<Node[front].child.size();i++){
int child=Node[front].child[i];//當前結點的第i個子結點的編號
//子結點層號為當前結點層號加1
Node[child].layer=Node[front].layer+1;
q.push(child);//將當前結點的所有子結點入隊
}
}
}
注意上面的這兩句:
int child=Node[front].child[i]
Node[child].layer=Node[front].layer+1
搞清楚child為當前結點的孩子結點陣列下標編號即可,然後將當前結點的子結點入隊前要【提前】將子結點的層數+1。
(5)小結:
(1)對樹的DFS遍歷就是該樹的先根遍歷的過程。
(2)可以用DFS做的題,可以把一些狀態作為樹的結點,問題就會轉換為對樹進行先根遍歷的問題。
(3)如果想要得到樹的某些資訊,可用DFS獲得結果,如求解葉子結點的帶權路徑和(從根結點到葉子結點的路徑上的結點點權之和)時就可以把到達死衚衕作為一條路徑結束的判斷。
(4)樹的BFS即層序遍歷。