(摘錄加總結------)
一、樹的概念
(1)樹是一種非線性的數據結構,是由n(n>=1)個有限節點組成的有層次關系的集合,在樹中有許多節點,每一個節點最多只有一個父節點,並且可能會有0個或者更多個子節點,沒有父節點的那個稱為根節點,除了根節點外,每個節點又可分為多個不相交的子樹。
(2)樹的相關概念術語:
1)節點<node>
樹中每個元素都叫節點
2)根節點或樹根<root>
樹頂端的節點稱之為根節點,也叫樹根
3)子樹<subTree>
除根節點之外,其他節點可以分為多個樹的集合,叫做子樹,在上圖中,K這個節點可以稱之為一顆子樹,而H、K、L三個節點組合起來也可以叫做一顆子樹
4)節點的度
一個節點直接含有的子樹的個數,稱之為節點的度。比如上圖中B節點的度為3,C節點的度是2,I、J、K、L節點的度是0
5)葉子節點、葉節點、終端節點
度為0的節點叫做葉子節點,也叫葉節點、終端節點,其實就是沒有子節點的節點,或者說沒有子樹的節點
6)雙親節點、父節點
父節點就是一個節點上頭的那個節點,如果一個節點包含若干子節點,那么這個節點就是這些子節點的父節點,也叫雙親節點
7)兄弟節點
擁有相同父節點的節點互稱為兄弟節點
8)樹的度
一棵樹中最大節點的度稱之為樹的度,即樹中哪個節點的子節點最多,那么這個節點的度也就是樹的度
9)節點的層次
從根這一層開始,根算1層,根的子節點算2層,一直到最下面一層
10)樹的高度、深度(這里屬於樹的層次結構)
樹的深度是從根節點開始、自頂向下逐層累加(根節點的高度是1)助記:深度從上到下
樹的高度是從葉節點開始、自底向上逐層累加(葉節點的高度是1)助記:高度由下向上
雖然樹的高度和深度一樣,但是具體到某個節點上,其高度和深度通常是不一樣的。
11)堂兄弟節點
堂兄弟節點是同一層,父節點不同,或者說雙親節點在同一層的節點稱之為堂兄弟節點
712)節點的祖先
從根節點到某一節點一路順下來的除了該節點的所有節點都是該節點的祖先節點
13)節點的子孫
以某節點為根的子樹中,任何一個節點都是其子孫,也就是說這個節點下面與這個節點有關的節點都是這個節點的子孫
14)森林
由m棵不相交的樹組成的集合,叫做森林,對比樹中每個節點的子樹的集合其實就構成了森林。
(3)常見的樹的分類:
常接觸到的樹有:二叉樹、平衡二叉樹、二叉查找樹、B樹、B+樹、B*樹、哈夫曼樹、紅黑樹、trie樹等。
(4)樹的存儲結構的實現
public class TreeNode1{ private int data; private int parent; public int getData(){ return date; } public void setData(int data){ this.data = data; } public int getParent(){ return parent; } public void setParent(int parent){ this.parent = parent; } }
上面的這個實現是利用了雙親表示法,約定根節點的位置域為-1,這樣的存儲結構,我們可以根據結點的 parent 指針很容易找到它的雙親結點,所用的時間復雜度為 0(1) ,直到 parent 為-1 時,表示找到了樹結點的根。可如果我們要知道結點的孩子是什么,對不起,請遍歷整個結構才行。
另外我們可以使用孩子表示法的多重鏈表表示方法來表示,但是考慮到表示的准確性和資源浪費程度,考慮這樣子表示的方法是比較可以的:
將一個樹里面所有的節點按照順序存儲結構形式將其放入到一個數組里面,然后每一個節點對其子節點按照鏈表的形式進行拓展,即這個數組里面存儲的節點的指針域是存儲的該節點的孩子鏈表的頭指針,如果有N個節點的話就應該有N個線性表,比如索引為3的D節點,它有3個子節點,所以以D這個頭指針發散出來的表達了6,7,8即G,H,J的子節點之間的邏輯關系的鏈表表示如圖所示,並且子節點為零的節點它的指針域就會為空。
這樣的結構對於我們要查找某個結點的某個孩子,或者找某個結點的兄弟,只需要查找這個節點的孩子單鏈表即可,對於遍歷整棵樹也是很方便的。
若是對於這個結構進行進一步改進的話,將雙親關系表示進去:(雙親孩子表示法)
(孩子兄弟表示法):任意一棵樹,它的節點的第一個孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的。因此我們設置兩個指針,分別指向該節點的第一個孩子和此節點的右兄弟。但是如果是對於上圖所示的樹來看的話,對於節點D,它有三個子節點:G,H,I,其中H是G的右兄弟,I是G的右兄弟,同時對於I來說沒有它的再對應的右兄弟了,所以它的兩個指針域應該為空。
對於每一個節點類的編寫就應該包含:data,firstChild,rightBro這三個屬性。如果想要找到某個節點的雙親,可以考慮再在此基礎上添加一個parent指針域來解決快速朝招雙親的問題。這個表示法的最大好處就是把一顆復雜的樹變成了一棵二叉樹。
二、二叉樹
對於在某個階段都是兩種結果的情形,比如開和關、0和1、真和假、上和下、對與錯、正面與反面,都適合用樹狀結構來建模,這種樹是一種很特殊的樹狀結構,稱之為二叉樹。對於上圖中的樹由於D有三個節點,所以它不是二叉樹。
(1)二叉樹的特點:
二叉樹中不存在子樹或者有一棵子樹都是可以的。左子樹和右子樹是有順序的,次序不能任意顛倒。即使樹中某節點只有一棵子樹也要區分它是左子樹還是右子樹。由於要區分左子樹和右子樹比如三個節點的樹:
有五種不同的形態:
(2)特殊的二叉樹
斜樹:線性表就是一種樹的極其特殊的表現形式。比如上面的樹2和樹5。
滿二叉樹:非葉子節點的度一定是2。
完全二叉樹:滿二叉樹一定是完全二叉樹,但是完全二叉樹不一定是滿二叉樹。(按層次編號一一對應)相當於是滿二叉樹的某些位置去掉之后的樹,形式上還是滿足滿二叉樹的形式的。
(3)二叉樹的性質(這些性質基本上都是歸納法出來的)
性質1:在二叉樹的第i層上至多有2^(i-1)個節點(i>=1)。
性質2:深度為k的二叉樹至多有2^k-1個節點(k>=1)。最多就是滿二叉樹的情況。
性質3:對任何一棵二叉樹T,如果其終端節點數(葉子節點數)為n0,度為2的節點數為n2,則n0=n2+1。(除了葉子節點外只有度為1或2的節點數了)
性質4:具有n個節點的完全二叉樹的深度為【log2n】+1(【x】表示不大於x的最大整數)。由於深度為k的滿二叉樹的節點數n一定是2^k-1,這個深度就是該二叉樹的度,所以反推可以得到滿二叉樹的度k=log2(n+1)。
性質5:如果對一棵有n個節點的完全二叉樹(其深度為【log2n】+1)的節點按層序編號(從第一層到第【log2n】+1層,每層從左到右),對任一節點i(1<=i<=n)有:
(4)二叉樹順序存儲結構
二叉樹的特殊性導致使用順序存儲結構也可以實現。使用一維數組存儲二叉樹中的節點,並且節點的存儲位置也就是數組的下表要能體現節點之間的邏輯關系,比如雙親與孩子的關系,左右與兄弟的關系。
對於一般的樹盡管層序編號不能反映邏輯關系,但是可以將其按完全二叉樹編號,只不過把不存在的節點設置為^,但是可能會存在存儲空間的浪費,所以一般只用於完全二叉樹。
(5)二叉樹連式存儲結構
也稱為二叉鏈表,為其設計一個數據域和兩個指針域是比較自然的想法。二叉樹的每個節點最多有兩個孩子。
三、二叉樹的遍歷
二叉樹的遍歷:指從根節點出發,按照某種次序一次訪問二叉樹中所有的節點。使得每個節點被訪問一次且僅被訪問一次。這存在一個遍歷次序的不確定性問題,因為樹的節點之間不存在唯一的前驅和后繼關系。
二叉樹遍歷方法:
(1)前序遍歷
若二叉樹為空,則空操作返回,然后訪問順序是訪問根節點,然后遍歷左子樹和右子樹。
public void PreOrder(BinaryTreeNode node){ if(node!=null){ System.out.println(node.getData()); //先訪問根節點 PreOrder(node.getLeftChirld()); //先根遍歷左子樹 PreOrder(node.getRightChirld()); //先根遍歷右子樹 } }
(2)中序遍歷
若樹為空,則空操作返回,遍歷順序從根節點開始但是不是先訪問根節點,而是從左子樹的終端節點開始,遍歷左子樹,然后訪問根節點,再中序遍歷右子樹。
public void InOrder(BinaryTreeNode node){ if(node!=null){ InOrder(node.getLeftChirld()); //中根遍歷左子樹 System.out.println(node); //訪問根節點 InOrder(node.getRightChirld()); //中根遍歷右子樹 } }
(3)后序遍歷
若樹為空,則空操作返回,遍歷順序從左到右先葉子后節點的方式便利訪問左右子樹,最后是訪問根節點。
public void PostOrder(BinaryTreeNode node){ if(node!=null){ PostOrder(node.getLeftChirld()); //后根遍歷左子樹 PostOrder(node.getRightChirld()); //后根遍歷右子樹 System.out.println(node); //訪問根節點 } } }
(4)層序遍歷
若樹為空,則空操作返回,遍歷順序從樹的第一層,從根節點開始訪問,從上而下逐層遍歷,在同一層中,按照從左到右的順序對節點逐個訪問。
這四種遍歷方法都是在把樹中的節點變成某種意義的現行序列。
二叉樹的創建:二叉樹的左右子節點仍然是一棵二叉樹。
(1)表示方法:左右鏈表表示法
public class BinaryTreeNode { private int data; //數據 private BinaryTreeNode leftChirld; //左孩子 private BinaryTreeNode rightChirld; //右孩子 public int getData() { return data; } public void setData(int data) { this.data = data; } public BinaryTreeNode getLeftChirld() { return leftChirld; } public void setLeftChirld(BinaryTreeNode leftChirld) { this.leftChirld = leftChirld; } public BinaryTreeNode getRightChirld() { return rightChirld; } public void setRightChirld(BinaryTreeNode rightChirld) { this.rightChirld = rightChirld; } }
(2)創建二叉樹:操作二叉樹,初始化根節點或者初始化一個空二叉樹。
public class BinaryTree { private BinaryTreeNode root; //初始化二叉樹 public BinaryTree(){} public BinaryTree(BinaryTreeNode root){ this.root = root; } public void setRoot(BinaryTreeNode root){ this.root = root; } public BinaryTreeNode getRoot(){ return root; } }
(3)二叉樹的清空:清空大致分為兩步,首先清空某個節點為根節點的子樹,遞歸地刪除每個節點。接着再刪除根節點。
/** * 二叉樹的清空: * 首先提供一個清空以某個節點為根節點的子樹的方法,既遞歸地刪除每個節點; * 接着提供一個刪除樹的方法,直接通過第一種方法刪除到根節點即可 */ //清除某個子樹的所有節點 public void clear(BinaryTreeNode node){ if(node!=null){ clear(node.getLeftChirld()); clear(node.getRightChirld()); node = null; //刪除節點 } } //清空樹 public void clear(){ clear(root); }
(4)判斷樹是否為空:只需判斷根節點是否為空即可。
//判斷二叉樹是否為空 public boolean isEmpty(){ return root == null; }
(5)求樹的高度:思路,首先需要一種獲取以某個節點為子樹的高度方法,使用遞歸實現。如果一個節點為空,那么這個節點肯定是一顆空樹,高度為0;如果不為空,則遍歷地比較它的左右子樹高度,高的一個為這顆子樹的最大高度,然后加上自身的高度即可。
/** * 求二叉樹的高度: * 首先要一種獲取以某個節點為子樹的高度的方法,使用遞歸調用。 * 如果一個節點為空,那么這個節點肯定是一顆空樹,高度為0; * 如果不為空,那么我們要遍歷地比較它的左子樹高度和右子樹高度, * 高的一個為這個子樹的最大高度,然后加上自己本身的高度就是了 * 獲取二叉樹的高度,只需要調用第一種方法,即傳入根節點 */ //獲取二叉樹的高度 public int heigh(){ return heigh(root); } //獲取以某節點為子樹的高度 public int heigh(BinaryTreeNode node){ if(node==null){ return 0; //遞歸結束,空子樹高度為0 }else{ //遞歸獲取左子樹高度 int l = heigh(node.getLeftChirld()); //遞歸獲取右子樹高度 int r = heigh(node.getRightChirld()); //高度應該算更高的一邊,(+1是因為要算上自身這一層) return l>r? (l+1):(r+1); } }
(6)求二叉樹的節點數:思路,獲取二叉樹節點數,需要獲取以某個節點為根的子樹的節點數實現。
如果節點為空,則個數肯定為0;如果不為空,則算上這個節點之后,繼續遞歸計算所有子樹的節點數,全部相加即可
/** * 獲取二叉樹的節點數 */ public int size(){ return size(root); } /** * 求二叉樹的節點數: * 求節點數時,我們看看獲取某個節點為子樹的節點數的實現。 * 首先節點為空,則個數肯定為0; * 如果不為空,那就算上這個節點之后繼續遞歸所有左右子樹的子節點數, * 全部相加就是以所給節點為根的子樹的節點數 * 如果求二叉樹的節點數,則輸入根節點即可 */ public int size(BinaryTreeNode node){ if(node==null){ return 0; //如果節點為空,則返回節點數為0 }else{ //計算本節點 所以要+1 //遞歸獲取左子樹節點數和右子樹節點數,最終相加 return 1+size(node.getLeftChirld())+size(node.getRightChirld()); } }
(7)返回某節點的父親節點:思路,首先,同樣需要通過一種方法來獲取某個節點在某個子樹中的父節點,這里使用遞歸實現,接着通過這種方法獲取這個節點在二叉樹中的父節點事實上,以現有的這種二叉樹的形式,我們並沒有辦法直接獲取一個節點的父節點, 這里只能通過從根節點遍歷來比較獲取。
//node節點在subTree子樹中的父節點 public BinaryTreeNode getParent(BinaryTreeNode subTree,BinaryTreeNode node){ if(subTree==null){ return null; //如果是空子樹,則沒有父節點 } if(subTree.getLeftChirld()==node || subTree.getRightChirld() == node){ return subTree; //如果子樹的根節點的左右孩子之一是待查節點,則返回子樹的根節點 } BinaryTreeNode parent = null; if(getParent(subTree.getLeftChirld(),node)!=null){ parent = getParent(subTree.getLeftChirld(),node); return parent; }else{ //遞歸左右子樹 return getParent(subTree.getRightChirld(),node); } } //查找node節點在二叉樹中的父節點 public BinaryTreeNode getParent(BinaryTreeNode node){ return (root==null||root==node)? null:getParent(root,node); }
(8)二叉樹的插入:二叉樹的插入分析:
* 分兩種情況:插入某個節點的左子節點;插入某個節點的右子節點 * 值得指出的是,當這個節點本身有子節點時,這樣的插入也會覆蓋原來在這個位置上的節點。 * 另外,雖然插入的是子節點,但是子節點也可以代表一顆子樹。 * 因為但從這個節點來看並不知道這個節點是否有左右子樹存在,所以雖然插入的是一個節點,但有可能 * 插入可很多節點(插入的是一顆子樹) //給某個節點插入左節點 public void insertLeft(BinaryTreeNode parent,BinaryTreeNode newnode){ parent.setLeftChirld(newnode); } //給某個節點插入右節點 public void insertRitht(BinaryTreeNode parent,BinaryTreeNode newnode){ parent.setRightChirld(newnode); }