樹的定義
之前一直介紹的是一對一的線性結構,可現實中還有多一對多的情況需要處理,這就是今天要介紹的一對多的資料結構——樹。
樹(Tree):是n(n>=0)個結點的有限集。n=0時稱為空樹。在任意一棵非空樹中:
- 有且僅有一個特定的稱為根(Root)的結點;
- 當n>1時,其餘結點可分為m(m>0)個互不相交的有限集T1、T2、···、Tm,其中每一個集合本身又是一顆樹,並且稱為根的子樹(SubTree),如圖:
樹的定義其實就是我們在說棧的時候提到的遞迴的方法。也就是在樹的定義之中還用到了樹的概念,這是一種比較新的定義方法。下圖的子樹T1和子樹T2就是根結點A的子樹。當然,D、G、H、I組成的樹又是B為根結點的子樹,E、J組成的樹是以C為根結點的子樹。
對於樹的定義還需要強調兩點:
- n>0時根結點是唯一的,不可能存在多個根結點,別和現實中的大樹混在一起,現實中的樹有很多根鬚,那是真實的樹,資料結構中的樹只有一個根結點。
- m>0時,子樹的個數是沒有限制的,但它們一定是互不相交的,像下圖中的兩個結構就不符合樹的定義,因為它們有相交的子樹:
1、結點的分類
樹的結點包含一個資料元素,及若干指向其子樹的分支。結點擁有的子樹數稱為結點的度。度為0的結點稱為葉子結點或終端結點;度不為0的結點稱為非終端結點或分支結點。除了根結點外,分支結點也叫內部結點。樹的度是樹內各結點的度的最大值。
2、結點間關係
**結點的子樹的根稱為該結點的孩子,相應地,該結點稱為孩子的雙親。**同一個雙親的孩子之間互稱兄弟。結點的祖先是從根到該結點所經分支上的所有結點。反之,以某結點為根的子樹中任一結點都稱為該結點的子孫。
3、樹的其他概念
結點的層次從根開始定義起,根為第一層,根的孩子為第二層。若某結點在第l層,則其子樹的根就在第l+1層。其雙親在同一層的結點互稱為堂兄弟。樹中結點的最大層次稱為樹的深度或高度。
如果將樹中結點的各子樹看成從左至右是有次序的,不能互換的,則稱該樹為有序樹,否則稱為無序樹。
森林是m(m>=0)棵互不相交的樹的集合。對樹中的每個結點來說,其子樹集合即為森林。
樹的儲存結構
一說到儲存結構,就必須說到順序儲存和鏈式儲存兩種方式。
樹中某個結點的孩子可以有多個,這就意味著,無論按何種順序將樹中所有結點儲存到陣列中,結點的儲存位置都無法直接反映邏輯關係,所以簡單的順序儲存結構不能滿足樹的實現要求。
今天介紹三種不同表示方法:雙親表示法、孩子表示法和孩子兄弟表示法。
1、雙親表示法
除了根結點外,其餘每個結點,它不一定會有孩子,但一定有且僅有一個雙親。
我們假設以一組連續空間儲存樹的結點,同時在每個結點中,附設一個指示器指示其雙親結點在陣列中的位置。也就是說每個結點除了知道自己是誰以外,還知道它的雙親是在哪裡。
其中data是資料域,儲存結點資料資訊,而parent是指標域,儲存該結點雙親在陣列中的下標。
由於根結點是沒有雙親的,所以我們約定根結點的位置域設定為-1,也就意味著,我們所有的結點都存有它的雙親。下面這個樹可以這樣表示:
這樣的儲存結構,可以根據結點的parent指標很容易找到它的雙親結點,所以時間複雜度是O(1),直到parent為-1時,表示找到了樹結點的根。可是如果要知道孩子結點是什麼,對不起,需要遍歷整個結構。
能不能改進一下呢?當然可以,我們增加一個結點最左邊孩子的域,不妨叫他長子域,這樣可以很容易找到它的孩子。如果沒有孩子,長子域設定為-1:
這樣我們找除了第一個孩子之外,找其他孩子就不太容易了。
另一個場景,如果我們很關注各兄弟之間的關係,雙親表示法無法體現這樣的關係,我們可以增加一個右兄弟域來體現兄弟關係,也就是說,每一個結點,再儲存一個它右兄弟的下標:
但是如果結點的孩子很多,超過了2個,我們既關注結點的雙親,又關注結點和孩子和兄弟,而且對遍歷時間要求較高,那麼就可以擴充此結構有雙親域,長子域和右兄弟域等。
儲存結構的設計是一個非常靈活的過程。一個儲存結構設計得是否合理,取決於基於該儲存結構的運算是否合適、是否方便和時間複雜度等。
2、孩子表示法
換一種考慮方式。由於樹中每個結點可能右多棵子樹,可以考慮用多重連結串列,即每個結點有多個指標域,其中每個指標指向一個子樹的根結點,我們叫之多重連結串列表示法。
不過,樹的每個結點的度是不同的,所以限制有兩個方案解決:
方案一
指標域的個數等於樹的度:
其中data是資料域,child1到childd是指標域,用來指向該結點的孩子結點。
對於上面提到的樹來說,樹的度是3,所以我們的指標域個數是3,實現如下:
這種方法對於樹中各結點的度相差很大時,顯然是浪費空間的,因為很多的結點,它的指標域是空的。不過如果樹的各結點度相差不大時,那就意味著開闢的空間被充分利用了。
方案二
這種方案時每個結點指標域的個數等於該結點的度,我們專用一個位置來儲存結點指標域的個數:
其中data為資料域,degree為度域,也就是儲存該結點的孩子的個數,child1到childd為指標域,指向結點的各個孩子的結點。如下:
這種方法雖然克服了浪費空間的缺點,對空間利用率是提高了,但是由於各個結點的連結串列是不同結構,加上需要維護結點的度的數值,再運算下會帶來時間上的損耗。
接下來介紹下孩子表示法,具體辦法是:把每個結點的孩子排列起來,以單連結串列作儲存結構,則n個結點有n個孩子連結串列,如果是葉子結點則此單連結串列為空。然後n個頭指標又組成一個線性表,採用順序儲存結構,存放進一個一維陣列中,如圖:
為此設計兩種結點結構,一個是孩子連結串列的孩子結點:
其中child是資料域,用來儲存某個結點在表頭陣列中的下標,next是指標域,用來儲存指向某個結點的下一個孩子結點的指標。
另一個是表頭陣列的表頭結點:
data是資料域,儲存結點資料資訊,firstChild是該結點的孩子連結串列的頭指標。
3、孩子兄弟表示法
任意一棵樹,它的結點的第一個孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的。因此,我們設定兩個指標,分別指向該結點的第一個孩子和此結點的右兄弟。
其中data是資料域,firstChild為指標域,儲存該結點第一個孩子結點儲存地址,rightsib是指標域,存放該結點右兄弟結點的儲存位置:
這種表示法,給查詢某個結點的某個孩子帶來了方柏霓,只需要通過firstChild找到此結點的長子,在通過長子結點的rightsib找到它的二弟,接著一直下去,直到找到具體孩子。
其實這種方式最大好處就是將一個普通樹轉成了一棵二叉樹,整理一下上面的圖可以得到:
關於樹的介紹,我們還將繼續,獲取更多精彩內容,關注我的微信公眾號——Android機動車