前面講到的順序表、棧和隊列都是一對一的線性結(jié)構(gòu),這節(jié)講一對多的線性結(jié)構(gòu)——樹。「一對多」就是指一個元素只能有一個前驅(qū),但可以有多個后繼。
目錄:
- 一、基本概念
- 二、樹的存儲結(jié)構(gòu)
- 1、雙親表示法
- 2、孩子表示法
- 3、孩子兄弟表示法
- 三、二叉樹
- 1、基本概念
- 2、二叉樹的性質(zhì)
- 3、二叉樹的實現(xiàn)
- 4、二叉樹的遍歷
- 四、線索二叉樹
- 五、樹、森林與二叉樹的轉(zhuǎn)換
- 六、總結(jié)
一、基本概念
樹(tree)是n(n>=0)個結(jié)點的有窮集。n=0時稱為空樹。在任意一個非空樹中:(1)每個元素稱為結(jié)點(node);(2)僅有一個特定的結(jié)點被稱為根結(jié)點或樹根(root)。(3)當(dāng)n>1時,其余結(jié)點可分為m(m≥0)個互不相交的集合T1,T2,……Tm,其中每一個集合Ti(1<=i<=m)本身也是一棵樹,被稱作根的子樹(subtree)。
注意:
- n>0時,根節(jié)點是唯一的。
- m>0時,子樹的個數(shù)沒有限制,但它們一定是互不相交的。
結(jié)點擁有的子樹數(shù)被稱為結(jié)點的度(Degree)。度為0的結(jié)點稱為葉節(jié)點(Leaf)或終端結(jié)點,度不為0的結(jié)點稱為分支結(jié)點。除根結(jié)點外,分支結(jié)點也被稱為內(nèi)部結(jié)點。結(jié)點的子樹的根稱為該結(jié)點的孩子(Child),該結(jié)點稱為孩子的雙親或父結(jié)點。同一個雙親的孩子之間互稱為兄弟。樹的度是樹中各個結(jié)點度的最大值。
結(jié)點的層次(Level)從根開始定義起,根為第一層,根的孩子為第二層。雙親在同一層的結(jié)點互為堂兄弟。樹中結(jié)點的最大層次稱為樹的深度(Depth)或高度。如果將樹中結(jié)點的各個子樹看成從左到右是有次序的,不能互換的,則稱該樹為有序樹,否則稱為無序樹。森林是m(m>=0)棵互不相交的樹的集合。
樹的定義:
二、樹的存儲結(jié)構(gòu)
由于樹中每個結(jié)點的孩子可以有多個,所以簡單的順序存儲結(jié)構(gòu)無法滿足樹的實現(xiàn)要求。下面介紹三種常用的表示樹的方法:雙親表示法、孩子表示法和孩子兄弟表示法。
1、雙親表示法
由于樹中每個結(jié)點都僅有一個雙親結(jié)點(根節(jié)點沒有),我們可以使用指向雙親結(jié)點的指針來表示樹中結(jié)點的關(guān)系。這種表示法有點類似于前面介紹的靜態(tài)鏈表的表示方法。具體做法是以一組連續(xù)空間存儲樹的結(jié)點,同時在每個結(jié)點中,設(shè)一個「游標(biāo)」指向其雙親結(jié)點在數(shù)組中的位置。代碼如下:
public class PTree<E> {
private static final int DEFAULT_CAPACITY = 100;
private int size;
private Node[] nodes;
private class Node() {
E data;
int parent;
Node(E data, int parent) {
this.data = data;
this.parent = parent;
}
}
public PTree() {
nodes = new PTree.Node[DEFAULT_CAPACITY];
}
}
由于根結(jié)點沒有雙親結(jié)點,我們約定根節(jié)點的parent域值為-1。樹的雙親表示法如下所示:
這樣的存儲結(jié)構(gòu),我們可以根據(jù)結(jié)點的parent域在O(1)的時間找到其雙親結(jié)點,但是只能通過遍歷整棵樹才能找到它的孩子結(jié)點。一種解決辦法是在結(jié)點結(jié)構(gòu)中增加其孩子結(jié)點的域,但若結(jié)點的孩子結(jié)點很多,結(jié)點結(jié)構(gòu)將會變的很復(fù)雜。
2、孩子表示法
由于樹中每個結(jié)點可能有多個孩子,可以考慮用多重鏈表,即每個結(jié)點有多個指針域,每個指針指向一個孩子結(jié)點,我們把這種方法叫多重鏈表表示法。它有兩種設(shè)計方案:
方案一:指針域的個數(shù)等于樹的度。其結(jié)點結(jié)構(gòu)可以表示為:
class Node() {
E data;
Node child1;
Node child2;
...
Node childn;
}
對于上一節(jié)中的樹,樹的度為3,其實現(xiàn)為:
顯然,當(dāng)樹中各結(jié)點的度相差很大時,這種方法對空間有很大的浪費(fèi)。
方案二,每個結(jié)點指針域的個數(shù)等于該結(jié)點的度,取一個位置來存儲結(jié)點指針的個數(shù)。其結(jié)點結(jié)構(gòu)可以表示為:
class Node() {
E data;
int degree;
Node[] nodes;
Node(int degree) {
this.degree = degree;
nodes = new Node[degree];
}
}
對于上一節(jié)中的樹,這種方法的實現(xiàn)為:
這種方法克服了浪費(fèi)空間的缺點,但由于各結(jié)點結(jié)構(gòu)不同,在運(yùn)算上會帶來時間上的損耗。
為了減少空指針的浪費(fèi),同時又使結(jié)點相同。我們可以將順序存儲結(jié)構(gòu)和鏈?zhǔn)酱鎯Y(jié)構(gòu)相結(jié)合。具體做法是:把每個結(jié)點的孩子結(jié)點以單鏈表的形式鏈接起來,若是葉子結(jié)點則此單鏈表為空。然后將所有鏈表存放進(jìn)一個一維數(shù)組中。這種表示方法被稱為孩子表示法。其結(jié)構(gòu)為:
代碼表示:
public class CTree<E> {
private static final int DEFAULT_CAPACITY = 100;
private int size;
private Node[] nodes;
private class Node() {
E data;
ChildNode firstChild;
}
//鏈表結(jié)點
private class ChildNode() {
int cur; //存放結(jié)點在nodes數(shù)組中的下標(biāo)
ChildNode next;
}
public CTree() {
nodes = new CTree.Node[DEFAULT_CAPACITY];
}
}
這種結(jié)構(gòu)對于查找某個結(jié)點的孩子結(jié)點比較容易,但若想要查找它的雙親或兄弟,則需要遍歷整棵樹,比較麻煩。可以將雙親表示法和孩子表示法相結(jié)合,這種方法被稱為雙親孩子表示法。其結(jié)構(gòu)如下:
其代碼和孩子表示法的基本相同,只需在Node結(jié)點中增加parent域即可。
3、孩子兄弟表示法
任意一棵樹,它的結(jié)點的第一個孩子如果存在則是唯一的,它的右兄弟如果存在也是唯一的。因此,我們可以使用兩個分別指向該結(jié)點的第一個孩子和右兄弟的指針來表示一顆樹。其結(jié)點結(jié)構(gòu)為:
class Node() {
E data;
Node firstChild;
Node rightSib;
}
其結(jié)構(gòu)如下:
這個方法,可以方便的查找到某個結(jié)點的孩子,只需先通過firstChild找到它的第一個孩子,然后通過rightSib找到它的第二個孩子,接著一直下去,直到找到想要的孩子。若要查找某個結(jié)點的雙親和左兄弟,使用這個方法則比較麻煩。
這個方法最大的好處是將一顆復(fù)雜的樹變成了一顆二叉樹。這樣就可以使用二叉樹的一些特性和算法了。
三、二叉樹
1、基本概念
二叉樹(Binary Tree)是每個節(jié)點最多有兩個子樹的樹結(jié)構(gòu)。通常子樹被稱作“左子樹”(left subtree)和“右子樹”(right subtree)。
二叉樹的特點:
- 二叉樹不存在度大于2的結(jié)點。
- 二叉樹的子樹有左右之分,次序不能顛倒。
如下圖中,樹1和樹2是同一棵樹,但它們是不同的二叉樹。
1)、斜樹
所有的結(jié)點都只有左子樹的二叉樹叫左斜樹。所有的結(jié)點都只有右子樹的二叉樹叫右斜樹。這兩者統(tǒng)稱為斜樹。
斜樹每一層只有一個結(jié)點,結(jié)點的個數(shù)與二叉樹的深度相同。其實斜樹就是線性表結(jié)構(gòu)。
2)、滿二叉樹
在一棵二叉樹中,如果所有分支結(jié)點都存在左子樹和右子樹,并且所有葉子都在同一層上,這樣的二叉樹稱為滿二叉樹。
滿二叉樹具有如下特點:
- 葉子只能出現(xiàn)在最下一層
- 非葉子結(jié)點的度一定是2
- 同樣深度的二叉樹中,滿二叉樹的結(jié)點個數(shù)最多,葉子數(shù)最多。
3)、完全二叉樹
若設(shè)二叉樹的高度為h,除第 h 層外,其它各層 (1~h-1) 的結(jié)點數(shù)都達(dá)到最大個數(shù),第h層有葉子結(jié)點,并且葉子結(jié)點都是從左到右依次排布,這就是完全二叉樹。
完全二叉樹的特點:
- 葉子結(jié)點只能出現(xiàn)在最下兩層
- 最下層葉子在左部并且連續(xù)
- 同樣結(jié)點數(shù)的二叉樹,完全二叉樹的深度最小
4)、平衡二叉樹
平衡二叉樹又被稱為AVL樹(區(qū)別于AVL算法),它是一棵二叉排序樹,且具有以下性質(zhì):它是一棵空樹或它的左右兩個子樹的高度差的絕對值不超過1,并且左右兩個子樹都是一棵平衡二叉樹
2、二叉樹的性質(zhì)
在二叉樹的第i層上至多有2i-1個結(jié)點(i>=1)。
深度為k的二叉樹至多有2k-1個結(jié)點(k>=1)。
對任何一棵二叉樹T,如果其終端結(jié)點個數(shù)為n0,度為2的結(jié)點數(shù)為n2,則n0 = n2 + 1。
具有n個結(jié)點的完全二叉樹的深度為「log2n」+ 1(「x」表示不大于x的最大整數(shù))。
-
如果對一棵有n個結(jié)點的完全二叉樹的結(jié)點按層序編號(從第一層到第「log2n」+ 1層,每層從左到右),對任一結(jié)點i(1≤i≤n)有:
- 若i=1,則結(jié)點i是二叉樹的根,無雙親;如i>1,則其雙親是結(jié)點「i/2」。
- 如2i>n,則結(jié)點i無左孩子(結(jié)點i為葉子結(jié)點);否則其左孩子是結(jié)點2i。
- 若2i+1>n,則結(jié)點i無右孩子;否則其右孩子是結(jié)點2i+1。
3、二叉樹的實現(xiàn)
二叉樹是一種特殊的樹,它的存儲結(jié)構(gòu)相對于前面談到的一般樹的存儲結(jié)構(gòu)要簡單一些。
1)、順序存儲
二叉樹的順序存儲結(jié)構(gòu)就是用一維數(shù)組來存儲二叉樹中的結(jié)點。不使用數(shù)組的第一個位置。結(jié)點的存儲位置反映了它們之間的邏輯關(guān)系:位置k的結(jié)點的雙親結(jié)點的位置為「k/2」,它的兩個孩子結(jié)點的位置分別為2k和2k+1。
代碼實現(xiàn):
public class ArrayBinaryTree<E> {
private static final int DEFAULT_DEPTH = 5;
private int size = 0;
private E[] datas;
ArrayBinaryTree() {
this(DEFAULT_DEPTH);
}
@SuppressWarnings("unchecked")
ArrayBinaryTree(int depth) {
datas = (E[]) new Object[(int)Math.pow(2, depth)];
}
public boolean isEmpty() { return size == 0; }
public int size(){ return size; }
public E getRoot() { return datas[1]; }
// 返回指定節(jié)點的父節(jié)點
public E getParent(int index) {
checkIndex(index);
if (index == 1) {
throw new RuntimeException("根節(jié)點不存在父節(jié)點!");
}
return datas[index/2];
}
//獲取右子節(jié)點
public E getRight(int index){
checkIndex(index*2 + 1);
return datas[index * 2 + 1];
}
//獲取左子節(jié)點
public E getLeft(int index){
checkIndex(index*2);
return datas[index * 2];
}
//返回指定數(shù)據(jù)的位置
public int indexOf(E data){
if(data==null){
throw new NullPointerException();
} else {
for(int i=0;i<datas.length;i++) {
if(data.equals(datas[i])) {
return i;
}
}
}
return -1;
}
//順序添加元素
public void add(E element) {
checkIndex(size + 1);
datas[size + 1] = element;
size++;
}
//在指定位置添加元素
public void add(E element, int parent, boolean isLeft) {
if(datas[parent] == null) {
throw new RuntimeException("index["+parent+"] is not Exist!");
}
if(element == null) {
throw new NullPointerException();
}
if(isLeft) {
checkIndex(2*parent);
if(datas[parent*2] != null) {
throw new RuntimeException("index["+parent*2+"] is Exist!");
}
datas[2*parent] = element;
}else {
checkIndex(2*parent + 1);
if(datas[(parent+1)*2]!=null) {
throw new RuntimeException("index["+ parent*2+1 +"] is Exist!");
}
datas[2*parent + 1] = element;
}
size++;
}
//檢查下標(biāo)是否越界
private void checkIndex(int index) {
if(index <= 0 || index >= datas.length) {
throw new IndexOutOfBoundsException();
}
}
public static void main(String[] args) {
char[] data = {'A','B','C','D','E','F','G','H','I','J'};
ArrayBinaryTree<Character> abt = new ArrayBinaryTree<>();
for(int i=0; i<data.length; i++) {
abt.add(data[i]);
}
System.out.print(abt.getParent(abt.indexOf('J')));
}
}
一棵深度為k的右斜樹,只有k個結(jié)點,但卻需要分配2k-1個順序存儲空間。所以順序存儲結(jié)構(gòu)一般只用于完全二叉樹。
2)、鏈?zhǔn)酱鎯?/strong>
二叉樹每個結(jié)點最多有兩個孩子,所以為它設(shè)計一個數(shù)據(jù)域和兩個指針域即可。我們稱這樣的鏈表為二叉鏈表。其結(jié)構(gòu)如下圖:
代碼如下:
import java.util.*;
public class LinkedBinaryTree<E> {
private List<Node> nodeList = null;
private class Node {
Node leftChild;
Node rightChild;
E data;
Node(E data) {
this.data = data;
}
}
public Node getRoot() {
return nodeList.get(0);
}
public void createBinTree(E[] array) {
nodeList = new LinkedList<Node>();
for (int i = 0; i < array.length; i++) {
nodeList.add(new Node(array[i]));
}
// 對前l(fā)asti-1個父節(jié)點按照父節(jié)點與孩子節(jié)點的數(shù)字關(guān)系建立二叉樹
for (int i = 0; i < array.length / 2 - 1; i++) {
nodeList.get(i).leftChild = nodeList.get(i * 2 + 1);
nodeList.get(i).rightChild = nodeList.get(i * 2 + 2);
}
// 最后一個父節(jié)點:因為最后一個父節(jié)點可能沒有右孩子,所以單獨拿出來處理
int lastParent = array.length / 2 - 1;
nodeList.get(lastParent).leftChild = nodeList .get(lastParent * 2 + 1);
// 右孩子,如果數(shù)組的長度為奇數(shù)才建立右孩子
if (array.length % 2 == 1) {
nodeList.get(lastParent).rightChild = nodeList.get(lastParent * 2 + 2);
}
}
public static void main(String[] args) {
Character[] data = {'A','B','C','D','E','F','G','H','I','J'};
LinkedBinaryTree<Character> ldt = new LinkedBinaryTree<>();
ldt.createBinTree(data);
}
}
4、二叉樹的遍歷
二叉樹的遍歷(traversing binary tree)是指從根結(jié)點出發(fā),按照某種次序依次訪問二叉樹中所有結(jié)點,使得每個結(jié)點被訪問一次且僅被訪問一次。
二叉樹的遍歷主要有四種:前序遍歷、中序遍歷、后序遍歷和層序遍歷。
1)、前序遍歷
先訪問根結(jié)點,然后遍歷左子樹,最后遍歷右子樹。
代碼:
//順序存儲
public void preOrderTraverse(int index) {
if (datas[index] == null)
return;
System.out.print(datas[index] + " ");
preOrderTraverse(index*2);
preOrderTraverse(index*2+1);
}
//鏈?zhǔn)酱鎯? public void preOrderTraverse(Node node) {
if (node == null)
return;
System.out.print(node.data + " ");
preOrderTraverse(node.leftChild);
preOrderTraverse(node.rightChild);
}
2)、中序遍歷
先遍歷左子樹,然后遍歷根結(jié)點,最后遍歷右子樹。
//鏈?zhǔn)酱鎯? public void inOrderTraverse(Node node) {
if (node == null)
return;
inOrderTraverse(node.leftChild);
System.out.print(node.data + " ");
inOrderTraverse(node.rightChild);
}
3)、后序遍歷
先遍歷左子樹,然后遍歷右子樹,最后遍歷根結(jié)點。
//鏈?zhǔn)酱鎯? public void postOrderTraverse(Node node) {
if (node == null)
return;
postOrderTraverse(node.leftChild);
postOrderTraverse(node.rightChild);
System.out.print(node.data + " ");
}
4)、層序遍歷
從上到下逐層遍歷,在同一層中,按從左到右的順序遍歷。如上一節(jié)中的二叉樹層序遍歷的結(jié)果為ABCDEFGHIJ。
注意:
- 已知前序遍歷和中序遍歷,可以唯一確定一棵二叉樹。
- 已知后序遍歷和中序遍歷,可以唯一確定一棵二叉樹。
- 已知前序遍歷和后序遍歷,不能確定一棵二叉樹。
如前序遍歷是ABC,后序遍歷是CBA的二叉樹有:
四、線索二叉樹
對于n個結(jié)點的二叉樹,在二叉鏈存儲結(jié)構(gòu)中有n+1個空指針域,利用這些空指針域存放在某種遍歷次序下該結(jié)點的前驅(qū)結(jié)點和后繼結(jié)點的指針,這些指針被稱為線索,加上線索的二叉樹稱為線索二叉樹。
結(jié)點結(jié)構(gòu)如下:
其中:
- lTag為0時,lChild指向該結(jié)點的左孩子,為1時指向該結(jié)點的前驅(qū)
- rTag為0時,rChild指向該結(jié)點的右孩子,為1時指向該結(jié)點的后繼。
線索二叉樹的結(jié)構(gòu)圖為:圖中藍(lán)色虛線為前驅(qū),紅色虛線為后繼
代碼如下:
public class ThreadedBinaryTree<E> {
private TBTreeNode root;
private int size; // 大小
private TBTreeNode pre; // 線索化的時候保存前驅(qū)
class TBTreeNode {
E element;
boolean lTag; //false表示指向孩子結(jié)點,true表示指向前驅(qū)或后繼的線索
boolean rTag;
TBTreeNode lChild;
TBTreeNode rChild;
public TBTreeNode(E element) {
this.element = element;
}
}
public ThreadedBinaryTree(E[] data) {
this.pre = null;
this.size = data.length;
this.root = createTBTree(data, 1);
}
//構(gòu)建二叉樹
public TBTreeNode createTBTree(E[] data, int index) {
if (index > data.length){
return null;
}
TBTreeNode node = new TBTreeNode(data[index - 1]);
TBTreeNode left = createTBTree(data, 2 * index);
TBTreeNode right = createTBTree(data, 2 * index + 1);
node.lChild = left;
node.rChild = right;
return node;
}
/**
* 將二叉樹線索化
*/
public void inThreading(TBTreeNode node) {
if (node != null) {
inThreading(node.lChild); // 線索化左孩子
if (node.lChild == null) { // 左孩子為空
node.lTag = true; // 將左孩子設(shè)置為線索
node.lChild = pre;
}
if (pre != null && pre.rChild == null) { // 右孩子為空
pre.rTag = true;
pre.rChild = node;
}
pre = node;
inThreading(node.rChild); // 線索化右孩子
}
}
/**
* 中序遍歷線索二叉樹
*/
public void inOrderTraverseWithThread(TBTreeNode node) {
while(node != null) {
while(!node.lTag) { //找到中序遍歷的第一個結(jié)點
node = node.lChild;
}
System.out.print(node.element + " ");
while(node.rTag && node.rChild != null) { //若rTag為true,則打印后繼結(jié)點
node = node.rChild;
System.out.print(node.element + " ");
}
node = node.rChild;
}
}
/**
* 中序遍歷,線索化后不能使用
*/
public void inOrderTraverse(TBTreeNode node) {
if(node == null)
return;
inOrderTraverse(node.lChild);
System.out.print(node.element + " ");
inOrderTraverse(node.rChild);
}
public TBTreeNode getRoot() { return root;}
public static void main(String[] args) {
Character[] data = {'A','B','C','D','E','F','G','H','I','J'};
ThreadedBinaryTree<Character> tbt = new ThreadedBinaryTree<>(data);
tbt.inOrderTraverse(tbt.getRoot());
System.out.println();
tbt.inThreading(tbt.getRoot());
tbt.inOrderTraverseWithThread(tbt.getRoot());
}
}
線索二叉樹充分利用了空指針域的空間,提高了遍歷二叉樹的效率。
五、樹、森林與二叉樹的轉(zhuǎn)換
具體內(nèi)容請參考這篇博客 樹、森林與二叉樹的轉(zhuǎn)換,這里就不寫了。
六、總結(jié)
至此樹的知識算是基本總結(jié)玩完了,這一節(jié)開頭講了樹的一些基本概念,重點介紹了樹的三種不同的存儲方法:雙親表示法、孩子表示法和孩子兄弟表示法。由兄弟表示法引入了一種特殊的樹:二叉樹,并詳細(xì)介紹了它的性質(zhì)、不同結(jié)構(gòu)的實現(xiàn)方法和遍歷方法。最后介紹了線索二叉樹的實現(xiàn)方法(感覺這個最難理解)。