二叉樹的定義####
二叉樹是n(n>=0)個具有相同類型的元素的有限集合,當n=0時稱為空二叉樹,當n>0時,數據元素被分為一個稱為根(Root)的數據元素及兩棵分別為左子樹和右子樹的數據元素的集合,左、右子樹互不相交,且左、右子樹都為二叉樹。在二叉樹中,一個元素也稱為一個節點。
二叉樹的子樹是有序的,即若將其左、右子樹顛倒,就將成為另一棵不同的二叉樹。即使樹中的節點只有一棵子樹,也要明確指出其是左子樹還是右子樹。由于左、右子樹的有序以及二叉樹可以為空,因此,二叉樹具有以下五種基本形態,即空二叉樹、僅有根節點的二叉樹、右子樹為空的二叉樹、左子樹為空的二叉樹、左右子樹均為非空的二叉樹,如下圖所示:
二叉樹的基本概念####
- 節點的度:節點所擁有子樹的個數稱為該節點的度。
- 葉子:度為0的節點稱為葉子。
- 孩子:節點子樹的根,稱為該節點的孩子。二叉樹中,孩子有左右之分,分別為左孩子和右孩子。
- 雙親:孩子節點的上層節點都稱為該節點的雙親。
- 以某節點的根的子樹中的除根節點之外的任意一個節點都稱為該節點的子孫。
- 祖先:節點的祖先是從根到該節點所經分支上的所有節點。
- 節點的層次:從根節點起,跟為第一層,他的孩子為第二層,孩子的孩子為第三層,依次類推,即某個節點的層次為L層,那么他的孩子節點的層數為L+1層。
- 兄弟:同一雙親的孩子互為兄弟。
- 堂兄弟:其雙親在同一層的節點互為堂兄弟。
- 二叉樹的度:二叉樹中最大的節點度稱為二叉樹的度。
- 二叉樹的深度:二叉樹中節點的最大層次書被稱為二叉樹的深度。
- 滿二叉樹:在一棵二叉樹中,所有分支節點都存在左子樹和右子樹,并且所有葉子節點都在同一層上,這樣的二叉樹被稱為滿二叉樹。
- 完全二叉樹:對深度為k的滿二叉樹中的節點從上至下、從左至右從1開始編號。對一棵具有n個節點、深度為k的二叉樹,采用的同樣的辦法對樹中的節點從上至下,從左至右從1開始連續編號,如果編號為i(i<=n)的節點與滿二叉樹中編號為i的節點在同一位置,則稱此二叉樹為完全二叉樹。對于一棵完全二叉樹,其葉子節點只可能出現在最下層和倒數第二層,而最下層的葉子集中在樹的最左部。
二叉樹的性質####
- 一棵非空二叉樹的第i層最多有2^(i-1)個節點。
- 深度為k的二叉樹至多有2^k-1個節點。
- 對任何一棵二叉樹T,如果葉子節點數為n0,度為2的節點數為n2,那么n0=n2+1。
- 具有n個節點的安全二叉樹的深度k=log2N+1(簡書不支持公式。。。這里解釋一下,k=以2為底N的對數向下取整+1)。
- 對一棵具有n個節點的完全二叉樹的從上至下,從左至右從1開始連續編號,那么對任意一個節點i有:
如果i=1,則節點i是二叉樹的根,無雙親;如果i>1,則其雙親是i/2向下取整。
如果2i>n,則節點無左孩子,為葉子節點;如果2i<n,則其左孩子是2i。
如果2i+1>n,則節點i無右孩子;如果2i+1<=n,則其右孩子為2i+1。
二叉樹的存儲結構####
順序存儲######
完全二叉樹的順序存儲
我們先看以下完全二叉樹的順序存儲。要存儲一棵完全二叉樹,不僅需要二叉樹的節點數據,還需要存儲它的結構信息,即雙親和孩子關系信息。從上面的性質5可以看出,完全二叉樹中節點i的左孩子為2i,右孩子為2i+1,雙親為i/2向下取整。因此,如果將完全二叉樹從上至下,從左至右從1開始編號,把編號為i的節點放在線性存儲單元的第i個位子,那么其左孩子存儲在2i位子,右孩子存儲在2i+1位子,則孩子和雙親的關系就體現出來了。
一般二叉樹的順序存儲
對于一般二叉樹而言,如果把它的節點按照從上至下、從左至右從1開始連續編號存儲在一維的存儲單元,那么無法反應其節點間的雙親孩子關系,即不能反映二叉樹的結構關系,怎么辦呢?可以把一般的二叉樹補成一棵完全二叉樹,這樣,它就可以按照完全二叉樹的順序存儲方式存儲了,只是新補上去的節點只占位子,不存放節點數據。如下圖所示:
對于一般二叉樹,需要增加一些節點才能存儲此二叉樹的節點數據和結構關系,這樣可能會造成存儲空間的浪費,例如,對于深度為3的右偏斜二叉樹,需要額外增加4個存儲單位。當二叉樹的深度更深時,則需要更多的節點,例如當深度為100的右偏斜二叉樹,需要2^100-101個額外空間,為了采用順序存儲方式來存儲此二叉樹,把全世界所有計算機的存儲空間加起來也不夠!因此很有必要采用其他形式的存儲方式。
鏈式存儲######
鏈表可以用來表示一維的線性結構,也可以用來表示非線性的二叉樹結構。二叉樹的鏈式存儲結構常有二叉鏈表存儲、三叉鏈表存儲及線索鏈表。二叉鏈表中有兩個指針域,分別指向其左、右孩子;三叉鏈表中除了指向其左、右孩子的指針域外,還有指向其雙親的指針域;線索鏈表是為了反映節點的前驅、后繼而將二叉鏈表中空指針指向其前驅或后繼而形成的鏈式存儲結構。
二叉鏈表存儲
鏈表中每一個節點包含3個域:數據域、左孩子指針域、右孩子指針域。左、右孩子指針域分別指向左孩子和右孩子的存儲地址。
二叉鏈表存儲代碼實現######
public class BinaryTreeNode<T> {
private T data;
private BinaryTreeNode<T> leftChild;
private BinaryTreeNode<T> rightChild;
public BinaryTreeNode(T data) {
this(data, null, null);
}
public BinaryTreeNode(T data, BinaryTreeNode<T> leftChild, BinaryTreeNode<T> rightChild) {
this.leftChild = leftChild;
this.rightChild = rightChild;
this.data = data;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public BinaryTreeNode<T> getLeftChild() {
return leftChild;
}
public void setLeftChild(BinaryTreeNode<T> leftChild) {
this.leftChild = leftChild;
}
public BinaryTreeNode<T> getRightChild() {
return rightChild;
}
public void setRightChild(BinaryTreeNode<T> rightChild) {
this.rightChild = rightChild;
}
}
三叉鏈表存儲
在三叉鏈表中,除根節點的parent域為空外,其余節點的parent域都不為空,指向其雙親。因此在三叉鏈表中,查找孩子和雙親都是很快捷的,但是增加了一些額外空間開銷。
三叉鏈表存儲實現#####
public class BinaryTreeNode3<T> {
private T data;
private BinaryTreeNode3<T> leftChild;
private BinaryTreeNode3<T> parent;
private BinaryTreeNode3<T> rightChild;
// 葉子節點構造器
public BinaryTreeNode3(T data, BinaryTreeNode3<T> parent) {
this(data, null, parent, null);
}
// 一般節點構造器
public BinaryTreeNode3(T data, BinaryTreeNode3<T> leftChild, BinaryTreeNode3<T> parent,
BinaryTreeNode3<T> rightChild) {
this.leftChild = leftChild;
this.parent = parent;
this.rightChild = rightChild;
this.data = data;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public BinaryTreeNode3<T> getLeftChild() {
return leftChild;
}
public void setLeftChild(BinaryTreeNode3<T> leftChild) {
this.leftChild = leftChild;
}
public BinaryTreeNode3<T> getParent() {
return parent;
}
public void setParent(BinaryTreeNode3<T> parent) {
this.parent = parent;
}
public BinaryTreeNode3<T> getRightChild() {
return rightChild;
}
public void setRightChild(BinaryTreeNode3<T> rightChild) {
this.rightChild = rightChild;
}
}
二叉樹的遍歷####
先序、中序、后續的遞歸遍歷######
例子
代碼
public class BinaryTree<T> {
public BinaryTree() {
// ...
}
// 訪問數據
public void visitData(BinaryTreeNode<T> node) {
System.out.print(node.getData() + " ");
}
// 前序遍歷
public void preOrder(BinaryTreeNode<T> node) {
if (null != node) {
visitData(node);
preOrder(node.getLeftChild());
preOrder(node.getRightChild());
}
}
// 中序遍歷
public void inOrder(BinaryTreeNode<T> node) {
if (null != node) {
inOrder(node.getLeftChild());
visitData(node);
inOrder(node.getRightChild());
}
}
// 后續遍歷
public void postOrder(BinaryTreeNode<T> node) {
if (null != node) {
postOrder(node.getLeftChild());
postOrder(node.getRightChild());
visitData(node);
}
}
public static void main(String[] args) {
BinaryTreeNode<String> g = new BinaryTreeNode<String>("g");
BinaryTreeNode<String> c = new BinaryTreeNode<String>("c");
BinaryTreeNode<String> d = new BinaryTreeNode<String>("d");
BinaryTreeNode<String> b = new BinaryTreeNode<String>("b", c, d);
BinaryTreeNode<String> f = new BinaryTreeNode<String>("f", g, null);
BinaryTreeNode<String> e = new BinaryTreeNode<String>("e", null, f);
BinaryTreeNode<String> a = new BinaryTreeNode<String>("a", b, e);
BinaryTree<String> binaryTree = new BinaryTree<String>();
System.out.print("preOder:");
binaryTree.preOrder(a);
System.out.println();
System.out.print("inOder:");
binaryTree.inOrder(a);
System.out.println();
System.out.print("postOder:");
binaryTree.postOrder(a);
System.out.println();
}
}
執行結果
非遞歸遍歷的思想######
當非遞歸遍歷時,我們需要從根節點開始,從左到右深入到每一個葉子。當深入到一個葉子節點時,需要返回到其父節點,然后去深入其他分支。可以看出深入和返回是一對相反的操作,所以可以用到數據結構 棧來保存深入節點時樹的結構關系。至于該遍歷是先序、中序或是后序,這只取決于其訪問
非遞歸的中序遍歷######
沿左子樹深入時,深入一個節點入棧一個節點,沿左分支無法繼續深入時(某個節點無左孩子),出棧,出站時同時訪問節點數據,然后從該節點的右子樹繼續沿左子樹深入,這樣一直下去,最后從根節點的右子樹返回時結束。
// 非遞歸的中序遍歷
public void nrInOrder(BinaryTreeNode<T> root) {
// 聲明一個棧
Stack<BinaryTreeNode<T>> stack = new Stack<BinaryTreeNode<T>>();
// 當前節點
BinaryTreeNode<T> p = root;
// 當節點和棧不同時為空時
while (!(p == null && stack.isEmpty())) {
// 遍歷p節點下的所有左孩子
while (p != null) {
// 將左孩子壓棧
stack.push(p);
p = p.getLeftChild();
}
if (stack.isEmpty()) {
return;
} else {
p = stack.pop();// 出棧
visitData(p);// 出棧時訪問數據
p = p.getRightChild();// 指向右孩子
}
}
}
非遞歸的層次遍歷######
從根節點開始,根節點入隊,訪問其數據,然后根節點的左右孩子入隊,根節點出隊。此時相當于第一層遍歷完畢。第二層數據已經入隊。然后當前隊首元素出隊,訪問數據,加入當前元素的左右孩子,依次類推直到隊列為空。
public void levelOrder(BinaryTreeNode<T> root) {
// 聲明一個隊列
Queue<BinaryTreeNode<T>> queue = new LinkedList<BinaryTreeNode<T>>();
// 如果二叉樹為空,直接返回
if (null == root) {
return;
}
// 根節點入隊
queue.add(root);
// 臨時變量,用來保存上一次出隊的元素
BinaryTreeNode<T> temp;
// 遍歷隊列,直到隊列為空
while (!queue.isEmpty()) {
// 出隊
temp = queue.remove();
// 訪問剛才出隊元素的數據
visitData(temp);
// 若剛才出隊的元素(節點)有左孩子,則左孩子入隊
if (null != temp.getLeftChild()) {
queue.add(temp.getLeftChild());
}
// 若剛才出隊的元素(節點)有左孩子,則左孩子入隊
if (null != temp.getRightChild()) {
queue.add(temp.getRightChild());
}
}
}