1. 程式人生 > 其它 >樹-專題模板

樹-專題模板

技術標籤:

文章目錄

1.基礎概念

一、幾個性質

(1)森林是若干樹的集合。
(2)樹可以沒有結點(稱為空樹)。
(3)樹的層次(深度)是從根節點(從上往下增大)算的;而樹的高度是從底層葉子結點(從下往上增大)算的。
(4)遞迴邊界:地址root為空,即root==NULL(而非*root==NULL),表示這個結點不存在,而括號是指結點存在但內容為NULL(沒有內容)。

(5)完全二叉樹性質
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->lchildroot->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->lchildnow->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(k1),
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即層序遍歷。