普通樹與二叉樹
樹
樹作為一種常用的數據結構,不可不知。樹采用的是鏈式存儲,在詳細介紹樹之前要先了解幾個基本概念:
根、節點、孩子、雙親、兄弟、分支 就不多BB了,葉子指的是沒有子節點的節點,樹的高度指從根到樹所有葉子節點的最大長度,節點的度為其子節點的數量,節點的深度為節點到根的路徑長度。
二叉樹
二叉表示至多兩向選擇,在許多場景下都有應用。二叉樹的種類實在太多,今天先寬泛地說一下幾種二叉樹。
嚴格二叉樹
嚴格二叉樹是這樣的二叉樹,它的每個節點或者沒有孩子或者有兩個孩子,即沒有一個孩子的節點。
滿二叉樹
滿二叉樹要求每一層的節點數量都達到最大。
完全二叉樹
在一棵完全二叉樹中,除最後一層之外,每一層都有在這一層上可能的最大節點數量。最後一層的節點數量可以小於最大可能數量,但是它們必須是從左到右一個一個挨著排列的。
(實在太簡單,就不上圖了)
二叉樹的遍歷與署名
二叉樹的遍歷
可以大致分為前序遍歷、中序遍歷、後序遍歷與層級遍歷,前三種的區別只在於訪問根節點相對左右子節點的訪問順序,層級遍歷則是按照層級,一層一層逐級遍歷節點。很明顯,前中後序三中遍歷用遞歸實現非常簡單,有興趣用循環實現的同學可以參考我另一篇博客遞歸轉循環的通法。不好意思,盜個圖先,嘻嘻。
二叉樹的署名
數據結構的署名是這一結構及其內容的純文本形式的編碼,這一署名可以離線存儲,並在需要時在內存中重構該數據結構。(說白了,持久化存儲)
既然要持久化二叉樹,那麽其存儲結構必須要唯一標識二叉樹,不能產生歧義,否則存儲的結構無意義。層級遍歷結果首先否定,因為其完全無法提供節點層級與節點的父子關系。再來看三序遍歷,單單一種遍歷同樣無法區分,那麽兩種遍歷結合到一起呢?
假設所有節點存儲的數據內容不同,在此前提下,前後序遍歷無法唯一標識,因為無法提供節點之間的父子關系。而前中與中後則可以,有興趣的同學可以思考一下,不深入討論了。
這裏給出鄙人實現的二叉樹JAVA代碼,請輕拍。
package com.structures.tree; /** * Created by wx on 2017/11/2. * BinaryTree Node class. */ public class BinaryTree<T> { private T data; public BinaryTree<T> left; public BinaryTree<T> right;public BinaryTree<T> parent; // 子類無法繼承父類的構造方法,需要的話,就顯示調用;T申明過了,除了static不用再申明 public BinaryTree(){ T data = null; BinaryTree<T> left = null; BinaryTree<T> right = null; BinaryTree<T> parent = null; } // 初始節點值 public void makeRoot(T data){ if(this.data!=null) throw new TreeViolationException("The tree is not null!"); else this.data = data; } //設定節點值 public void setData(T data){ this.data = data; } public T getData(){ return data; } // 返回整棵樹的根 public BinaryTree<T> root(){ BinaryTree<T> myRoot = this; while(myRoot.parent!=null){ myRoot = myRoot.parent; } return myRoot; } //設定左子樹 public void attachLeft(BinaryTree<T> tree){ if(left!=null) throw new TreeViolationException("Left sub tree already exists!"); left = tree; tree.parent = this; } //設定右子樹 public void attachRight(BinaryTree<T> tree){ if(right!=null) throw new TreeViolationException("Right sub tree already exists!"); right = tree; tree.parent = this; } //刪除左子樹並返回之 public BinaryTree<T> detachLeft(){ BinaryTree<T> subTree = left; subTree.parent = null; left = null; return subTree; } //刪除右子樹並返回之 public BinaryTree<T> detachRight(){ BinaryTree<T> subTree = right; subTree.parent = null; right = null; return subTree; } //返回是否為空樹 public boolean isEmpty(){ return (data==null && left==null && right==null); } //清空二叉樹,並使之與雙親節點斷開 public void clear(){ if(parent!=null){ if(parent.left==this) parent.left = null; else parent.right = null; } parent = left = right = null; data = null; } } class TreeViolationException extends RuntimeException{ TreeViolationException(String info){ super(info); } }
普通樹
普通樹主要是以層次的形式組織起來的,最常見的就是Unix操作系統的文件組成層次,生活中又如企業的管理層級。話不多說,上圖。
樹的理解和構建都很簡單,但是相對二叉樹又有很多一些問題。
問題一:存儲空間問題
普通樹的節點的孩子數量是不確定的,那麽直接構建足夠引用孩子節點空間的節點是行不通的。在不知道一個節點有多少個孩子的情況下,我們對節點要進行過高評估並分配一個最大空間,這會帶來嚴重的空間浪費。假設節點的孩子最多k個,這棵樹有n個節點,這個時候浪費的空間比例為:w = (nk-(n-1)) / (nk) ≈ 1- 1/k 。k=5時,幾乎有80%的浪費!
問題二:普通樹的署名
二叉樹可以通過前中或者中後序遍歷署名,那麽普通樹呢? 首先普通樹就不存在所謂的中序遍歷,而前後序結合又不能唯一標識一棵普通樹。然後,尷尬了!
怎麽解決?
普通樹實際上是以二叉樹的形式存儲,這樣的話,上面兩個問題就迎刃而解了。其與之對應的二叉樹之間的轉化關系為:
- 對於普通樹的節點A,A的第一個孩子變為二叉樹中A的左孩子
- A的右兄弟變為二叉樹中A的右孩子,以此類推
以圖中樹為實例,不妨寫一下其遍歷結果。
原來樹的遍歷:
- 前序遍歷:ABCEFDXG
- 後序遍歷:BEFCDGXA
轉換後二叉樹的遍歷:
- 前序遍歷:ABCEFDXG
- 中序遍歷:FEGXDCBA
- 後序遍歷:BEFCDGXA
呦,怎麽普通樹的前序遍歷和後序遍歷分別與等價二叉樹的前序遍歷和中序遍歷結果相同啊?!
仔細分析一下,等價二叉樹節點left 對應 原來子節點, 等價二叉樹節點right 對應原來的 右兄弟, 等價二叉樹的中序遍歷的遍歷順序與原來的前序相同!
同理 等價二叉樹的中序遍歷 與 原來的後續遍歷的遍歷順序相同。追根究底,就是這樣對應轉換,沒有改變遍歷節點的順序規則。
普通樹與等價二叉樹一一對應,同時使用二叉樹的署名也唯一標識了普通樹,問題二解決!
普通樹與二叉樹