關於樹結構的非線性表編程在數據結構中能夠說佔據了半壁江山,其中涉及的知識點繁雜,但也是數據結構體現運算優化的核心所在,下面咱們將較爲初步且系統得討論數據結構中一系列有關樹的表示。c++
首先咱們再次明確樹的形式化概念:編程
樹是n個節點的有限集合,這個集合知足如下的條件:數組
1) 有且僅有一個節點沒有前件。數據結構
2) 除根外,其餘的全部節點都有且僅有一個前件。優化
3) 除去根之外,其餘每一個節點都經過惟一的路徑鏈接根上。每一個節點的前件稱爲該節點的父節點,後件稱爲該節點的子節點。spa
這篇文章主要包含以下4個知識點:設計
(1) 用樹的遍歷求解層次性問題。code
(2) 用樹結構支持並查集。blog
(3) 用樹狀數組同濟子樹權和。遞歸
(4) 用四叉樹求解二維空間問題。
首先咱們討論樹在計算機中的表示,這不只僅在處理一些數據結構的題目中很重要,在一些樹形動態規劃的問題上,咱們首先要完成的也是樹的表示。
方法1:雙親表示法。
在高級語言中咱們容易操做和定義的是線性結構,即數組這樣的數據結構,所以咱們考慮非線性結構的程序語言表示的時候,基本原則就是將其向線性數據結構轉化。給出一個樹結構,根據樹的定義咱們知道,除了根節點,每一個節點有且僅有一個父節點。所以咱們定義雙親表示數組f[].其中f[i]表示節點i的父節點的節點序號。
方法2:多重鏈表法
這裏咱們經常用到c++語言stl庫中的vector類。對於給定的一棵樹,咱們利用」vector<int> tree[n]」這樣一條語句,而後定義tree[i].push_back(j)表示j做爲i的一個子節點。通俗來講,這裏咱們將樹結構擴成了一個二維數組,而利用vector類則是爲了更好的節約空間資源。
通常題目中,輸入的樹結構每每是對於給定的n個節點的樹結構,輸入n-1個有序節點對用於表示父子關係,這時候每每用方法2表示樹是比較常見的,而每每在通常題目中,須要多種表示方法並用,由於各個樹結構的表示方式都有各自的優勢(訪問樹自身某方面的信息時比較快)。
用樹的遍歷求解層次性問題:
最近公共祖先問題:
給出一個n節點的樹結構的n-1對有序頂點對<x , y>,表示樹結構中的一條邊且x是父節點,y是子節點。最後輸入一對節點序號<a, b>,編寫程序
最近公共祖先代碼以下:
//poj 1330.
#include<cstdio> #include<cstring> #include<vector> using namespace std; const int N = 10000; vector<int > a[N]; int f[N] , r[N]; void DFS(int u , int dep)//從dep層的u節點處罰,先序遍歷計算每一個節點的層次 { r[u] = dep; for(vector<int>::iterator it = a[u].begin();it != a[u].end();++it){ DFS(*it , dep + 1); } } int main(){ int casenum , num , n , i , x , y; scanf("%d" , &casenum); for(num = 0;num < casenum;num++){ scanf("%d" , &n); for(i = 0;i < n;i++) a[i].clear(); memset(f , -1 , sizeof(f)); for(i = 0;i < n - 1;i++){//n個節點的樹 , n-1條邊 scanf("%d %d" , &x , &y);//樹種的一個邊<x , y>,x是父節點,y是子節點; x--;y--; a[x].push_back(y); f[y] = x; } for(i = 0;f[i] >= 0;i++); DFS(i , 0); scanf("%d %d" , &x , &y); x--;y--; while(x != y){ if(r[x] > r[y]) x = f[x]; else y = f[y]; } printf("%d\n" , x + 1); } }
用樹結構支持並查集:
在通常的數據結構的教材中,集合與圖、樹同樣,都是羣聚類的非線性表,可是集合更加側重於包含關係而忽略一個集合中各個元素之間的先後件關係。在一些問題中,咱們須要將n個元素劃分紅若干組,每一個組視爲一個集合,一般須要涉及集合的合併與查找,所以咱們稱其爲並查集。
並查集須要支持以下的操做:
(1) Make_set(x):加入單個元素x到集合S中。
(2) Join(x,y),把x、y所在的不一樣集合進行合併。
(3) Set_find(x):獲得x所在集合S的表明元。
下面咱們來討論並查集的存儲結構。理論上來講,並查集有鏈結構和樹結構兩種,可是樹結構在完成各個操做時的效率更加優良,咱們便直接介紹樹結構。
咱們用一棵樹結構表示集合S={s1,s2,s3,…,sn}.因爲咱們僅僅是用樹結構來表示集合,所以這棵樹中的邊關係,僅僅是體現了合併操做的先後順序,邊關係不一樣時,所表達的集合是徹底等價的。緊接着,因爲樹結構依然不能直接存儲,咱們還要想辦法將其轉化成線性存儲結構。咱們如何來表徵這樣一個集合的特徵呢?咱們選擇一個集合的表明元(也就是樹結構表示的並查集的根節點),咱們定義set[x]表示元素x所在集合的表明元,而若是x是集合S的表明元,則令set[x] = -1(這種表示方法能夠理解爲並查集 -> 樹結構表示 -> 雙親表示法).那麼基於這樣的定義,咱們可以看到,set[x]與 set[y]的相等關係即可以做爲兩個元素x、y是否在同一集合的指標了。
查找過程:
對於給出的元素x,咱們想要找到x所在集合的表明元,也能夠說成是x所在樹結構的根節點。直接訪問set[x]咱們發現存在這樣一個問題,x的根節點y在某一次合併操做以後合併到了一個更大的集合,本來是根節點的y變成了樹結構中的分支節點,所以咱們須要沿樹結構繼續向上找,咱們會遇到相同的問題,所以須要設計一個遞歸機制。同時爲了之後訪問的方便,咱們採用路徑壓縮的手法,來使得從x出發找到根節點r的路徑通過的全部節點i的set[i]都變成r.
int set_find(int p){ if(set[p] < 0) return p; //找到根節點/集合表明元 return set[p] = set_find(set[p]); }
合併過程:
合併過程就顯得很簡單,假設咱們想要合併元素x、y所在集合,咱們只須要分別找到x、y所在集合的表明元p , q,使set[p] = q便可,固然yekeyi 交換p、q的位置。即從x、y所在集合的表明元中選擇任意一個成爲新的集合的表明元。
void join(int x , y){ p = set_find(p); q = set_find(q) if(p != q) set[q] = p; }