數(shù)據(jù)結(jié)構(gòu)和算法--二叉樹的實(shí)現(xiàn)
幾種二叉樹
1、二叉樹
和普通的樹相比,二叉樹有如下特點(diǎn):
- 每個(gè)結(jié)點(diǎn)最多只有兩棵子樹,注意是最多。這意味著任意結(jié)點(diǎn)的度小于等于2。
- 子樹有左右之分;某個(gè)結(jié)點(diǎn)只有一個(gè)孩子時(shí),它位于左邊和右邊組成的是不同的樹。如下圖,左圖是作為根結(jié)點(diǎn)的左孩子,右圖是作為根結(jié)點(diǎn)的右孩子,這是兩棵樹,他們結(jié)構(gòu)不同。
順便一提,具有n個(gè)結(jié)點(diǎn)的二叉樹,共有種形態(tài),C即組合。像上圖就是
。這個(gè)知識(shí)點(diǎn)是卡特蘭數(shù),類似的問(wèn)題還有可能的進(jìn)出棧順序,括號(hào)化問(wèn)題等。
2、斜樹
好了繼續(xù),還有更特殊的二叉樹,如果一棵二叉樹只有左孩子,則稱該樹為左斜樹,類似的如果只有右孩子,就稱為右斜樹,他們統(tǒng)稱為斜樹。仔細(xì)一看,這時(shí)候樹就演化成了鏈表!易知,樹的深度就是樹的結(jié)點(diǎn)個(gè)數(shù)。
3、滿二叉樹
如果二叉樹中所有的非葉子結(jié)點(diǎn)都有左孩子和右孩子,而且葉子結(jié)點(diǎn)位于同一層。我們稱這樣的樹為滿二叉樹。如下
看起來(lái)十分美觀。滿二叉樹有一些特點(diǎn):
- 葉子結(jié)點(diǎn)只能在最后一層
- 非葉子結(jié)點(diǎn)的度一定是2
- 在同樣深度的二叉樹中,滿二叉樹的結(jié)點(diǎn)數(shù)最多(因?yàn)槊總€(gè)結(jié)點(diǎn)都有兩個(gè)孩子);擁有的葉子結(jié)點(diǎn)的個(gè)數(shù)也最多(因?yàn)槊總€(gè)結(jié)點(diǎn)都有兩個(gè)孩子,一直到最后一層葉子結(jié)點(diǎn),必然是最多的);在相同結(jié)點(diǎn)數(shù)的樹中,滿二叉樹的深度最小。
4、完全二叉樹
這個(gè)怎么說(shuō),如果對(duì)每個(gè)結(jié)點(diǎn)按照層序編號(hào),然后按照從上到下,從左到右依次編號(hào),編號(hào)時(shí)不跳過(guò)空結(jié)點(diǎn)。二叉樹可能有的結(jié)點(diǎn)只有一個(gè)孩子,不是最后一層也可能出現(xiàn)葉子結(jié)點(diǎn),但是我們編號(hào)的時(shí)候不跳過(guò),始終按照左孩子 -> 右孩子的順序去編號(hào),如果發(fā)現(xiàn)某個(gè)編號(hào)處位置空缺,這棵樹就不是完全二叉樹。舉幾個(gè)例子
樹1的結(jié)點(diǎn)5,按編號(hào)順序它的左孩子應(yīng)該編號(hào)10,右孩子11,但它沒有左孩子,位置10就空缺了,所以不是完全二叉樹;樹2中第二層的結(jié)點(diǎn)3的兩個(gè)孩子應(yīng)分別編號(hào)6和7的,然后是下一層的8和9,但是由于結(jié)點(diǎn)3沒有孩子,所以造成位置6、7空缺,也不是完全二叉樹。樹3也是不是完全二叉樹,結(jié)點(diǎn)5的孩子應(yīng)編號(hào)為10和11,然后是結(jié)點(diǎn)6的孩子編號(hào)為12,但是結(jié)點(diǎn)5沒有孩子,造成了位置10、11空缺。
總結(jié)完全二叉樹的特點(diǎn):
- 首先滿二叉樹一定是完全二叉樹,完全二叉樹不一定是滿二叉樹
- 某結(jié)點(diǎn)的度如果為1,則它只有左孩子
- 葉子結(jié)點(diǎn)只能出現(xiàn)在最后兩層(考慮樹2)
- 相同結(jié)點(diǎn)的樹中,完全二叉樹的深度最小
二叉樹的性質(zhì)
- 二叉樹的第i層至多有
個(gè)結(jié)點(diǎn),這個(gè)看圖很容易得出結(jié)論
- 深度為
的二叉樹最多有
個(gè)結(jié)點(diǎn);擁有最多結(jié)點(diǎn)的是滿二叉樹,根據(jù)第一條其實(shí)就是
- 任意一棵二叉樹,如果葉子結(jié)點(diǎn)數(shù)為
,度為2的結(jié)點(diǎn)數(shù)為
,則
。設(shè)
為二叉樹的總結(jié)點(diǎn)數(shù),那么樹分支(即連線)條數(shù)為
,這個(gè)值的由來(lái)可以從下往上看,除了根結(jié)點(diǎn)外,每個(gè)結(jié)點(diǎn)都有一條指向父結(jié)點(diǎn)的連線,所以是
。另外用
表示度為1的結(jié)點(diǎn),則
;計(jì)算分支條數(shù)還可以從上到下,葉子結(jié)點(diǎn)沒有孩子,擁有一個(gè)孩子的結(jié)點(diǎn)可引出一條連線,擁有兩個(gè)孩子的結(jié)點(diǎn)棵引出兩條連線。所以
分支的條數(shù)
=,將
代入得證。這個(gè)結(jié)論是說(shuō),二叉樹葉子結(jié)點(diǎn)一定比的個(gè)數(shù)比度為2的結(jié)點(diǎn)個(gè)數(shù)多一個(gè)。
- 有
個(gè)結(jié)點(diǎn)的完全二叉樹的深度為
,其中
表示向下取整。因?yàn)橥耆鏄漕~結(jié)點(diǎn)數(shù)肯定不大于滿二叉樹的結(jié)點(diǎn)數(shù)
個(gè),但是也肯定大于
個(gè)(最少的時(shí)候,第k層就1個(gè)結(jié)點(diǎn),但是上面的
層都是滿的),也就是
,又n為正整數(shù),該不等式等價(jià)于
,兩邊取對(duì)數(shù)得到
- 按照層序編號(hào),根結(jié)點(diǎn)編號(hào)為1,對(duì)于任意一個(gè)編號(hào)為
的結(jié)點(diǎn),編號(hào)
為其左孩子,
為其右孩子;相反,對(duì)于任意一個(gè)編號(hào)為
的結(jié)點(diǎn),其父結(jié)點(diǎn)編號(hào)為
- 如果
則結(jié)點(diǎn)
無(wú)孩子;如果
,結(jié)點(diǎn)
只有左孩子沒有右孩子。
二叉樹的存儲(chǔ)結(jié)構(gòu)及實(shí)現(xiàn)
二叉樹每個(gè)結(jié)點(diǎn)最多兩個(gè)孩子,自然想到設(shè)置兩個(gè)指針域。其實(shí)就是二叉鏈表,回憶孩子兄弟表示法——一個(gè)指針指向左孩子,另一個(gè)指針指向其兄弟,現(xiàn)在將這個(gè)指向兄弟的指針指向右孩子就可以實(shí)現(xiàn)二叉樹了。
package Chap4;
import java.math.BigInteger;
import java.util.LinkedList;
import java.util.Queue;
/**
* 二叉樹
*/
public class BinaryTree<Item> {
public static class Node<T> {
private T data;
private Node<T> lchild;
private Node<T> rchild;
public Node(T data) {
this.data = data;
}
public T getData() {
return data;
}
public Node<T> getLchild() {
return lchild;
}
public Node<T> getRchild() {
return rchild;
}
@Override
public String toString() {
String lchildInfo = lchild == null ? null : lchild.getData().toString();
String rchildInfo = rchild == null ? null : rchild.getData().toString();
return "Node{" +
"data=" + data +
", lchild=" + lchildInfo +
", rchild=" + rchildInfo +
'}';
}
}
private Node<Item> root;
private int nodesNum;
public void setRoot(Item data) {
root = new Node<>(data);
nodesNum++;
}
public void addLeftChild(Item data, Node<Item> parent) {
parent.lchild = new Node<>(data);
nodesNum++;
}
public void addRightChild(Item data, Node<Item> parent) {
parent.rchild = new Node<>(data);
nodesNum++;
}
public Node<Item> parentTo(Node<Item> node) {
return parentTo(root, node);
}
public Node<Item> parentTo(Node<Item> currentNode, Node<Item> node) {
if (currentNode == null) {
return null;
}
if (node.equals(currentNode.lchild) || node.equals(currentNode.rchild)) {
return currentNode;
}
// 如果當(dāng)前結(jié)點(diǎn)沒找到,遞歸查找其左右子樹
Node<Item> p;
if ((p = parentTo(currentNode.lchild, node)) != null) {
return p;
// 如果左子樹中沒找到,返回右子樹查找結(jié)果
} else {
return parentTo(currentNode.rchild, node);
}
}
public Node<Item> root() {
return root;
}
public int degreeForNode(Node<Item> node) {
if (node.lchild != null && node.rchild != null) {
return 2;
} else if (node.lchild != null || node.rchild != null) {
return 1;
} else {
return 0;
}
}
public int degree() {
// 無(wú)非三種情況
// 1. 只有一個(gè)根結(jié)點(diǎn),度為0
// 2. 斜樹,度為1
// 3.其余情況度是2
if (root.lchild == null && root.rchild == null) {
return 0;
// 斜樹的結(jié)點(diǎn)數(shù)等于其深度,包括了只有根結(jié)點(diǎn)的情況,所以上面的條件要先判斷
} else if (nodesNum == depth()) {
return 1;
} else {
return 2;
}
}
public int depthForSubTree(Node<Item> node) {
if (node == null) {
return 0;
}
// 從上到下遞歸,從下到上返回深度,下面就是返回某結(jié)點(diǎn)兩個(gè)孩子中深度最大的那個(gè),加1繼續(xù)返回到上一層
int lDepth = depthForSubTree(node.lchild);
int rDepth = depthForSubTree(node.rchild);
return lDepth > rDepth ? lDepth + 1 : rDepth + 1;
}
public int depth() {
return depthForSubTree(root);
}
public int nodesNum() {
return nodesNum;
}
/**
* 前序遍歷--遞歸
*/
public void preOrder(Node<Item> node) {
if (node == null) {
return;
}
System.out.print(node.getData() + " ");
preOrder(node.lchild);
preOrder(node.rchild);
}
/**
* 前序遍歷--非遞歸
*/
public void preOrder2(Node<Item> root) {
// 用棧保存已經(jīng)訪問(wèn)過(guò)的結(jié)點(diǎn),便于返回到父結(jié)點(diǎn)
LinkedList<Node<Item>> stack = new LinkedList<>();
// 當(dāng)前結(jié)點(diǎn)不為空,或者為空但有可以返回的父結(jié)點(diǎn)(可以進(jìn)行pop操作)都可以進(jìn)入循環(huán)
while (root != null || !stack.isEmpty()) {
// 只要當(dāng)前結(jié)點(diǎn),就打印,同時(shí)入棧
while (root != null) {
stack.push(root);
System.out.print(root.getData() + " ");
root = root.lchild;
}
// 上面while終止說(shuō)明當(dāng)前結(jié)點(diǎn)為空;返回到父結(jié)點(diǎn)并處理它的右子樹。由于要執(zhí)行pop操作,先判空
if (!stack.isEmpty()) {
// 返回到父結(jié)點(diǎn)。由于左孩子為空返回時(shí)已經(jīng)彈出過(guò)父結(jié)點(diǎn)了,所以若是由于右孩子為空返回,會(huì)一次性返回到多層
root = stack.pop();
// 開始右子樹的大循環(huán)(第一個(gè)while)
root = root.rchild;
}
}
}
/**
* 中序遍歷--遞歸
*/
public void inOrder(Node<Item> node) {
if (node == null) {
return;
}
inOrder(node.lchild);
System.out.print(node.getData() + " ");
inOrder(node.rchild);
}
/**
* 中序遍歷--非遞歸
*/
public void inOrder2(Node<Item> root) {
LinkedList<Node<Item>> stack = new LinkedList<>();
while (root != null || !stack.isEmpty()) {
while (root != null) {
stack.push(root);
root = root.lchild;
}
// 和前序遍歷唯一不同的是,前序遍歷是入棧時(shí)打印,中序遍歷是出棧時(shí)返回到父結(jié)點(diǎn)才打印
// 和前序遍歷一樣,由于左孩子為空返回時(shí)已經(jīng)彈出過(guò)父結(jié)點(diǎn)了,所以若是由于右孩子為空返回,會(huì)一次性返回多層
root = stack.pop();
System.out.print(root.getData() + " ");
root = root.rchild;
}
}
/**
* 后序遍歷--遞歸
*/
public void postOrder(Node<Item> node) {
if (node == null) {
return;
}
postOrder(node.lchild);
postOrder(node.rchild);
System.out.print(node.getData() + " ");
}
/**
* 后序遍歷--非遞歸
*/
public void postOrder2(Node<Item> root) {
LinkedList<Node<Item>> stack = new LinkedList<>();
// 存放結(jié)點(diǎn)被訪問(wèn)的信息,1表示只訪問(wèn)過(guò)左孩子,2表示右孩子也訪問(wèn)過(guò)了(此時(shí)可以打印了)
LinkedList<Integer> visitedState = new LinkedList<>();
while (root != null || !stack.isEmpty()) {
while (root != null) {
stack.push(root);
root = root.lchild;
// 上句訪問(wèn)過(guò)左孩子了,放入1
visitedState.push(1);
}
// 這個(gè)while和下面的if不可交換執(zhí)行順序,否則變成了中序遍歷
// 用while而不是if是因?yàn)椋航Y(jié)點(diǎn)已經(jīng)訪問(wèn)過(guò)它的兩個(gè)孩子了,先不打印而處于等待狀態(tài)。隨即判斷若它的右孩子不為空,則仍會(huì)被push進(jìn)去,待右孩子處理完后按照遞歸思想應(yīng)該返回到等待中父結(jié)點(diǎn),由于父結(jié)點(diǎn)訪問(wèn)狀態(tài)已經(jīng)是2,直接打印
while (!stack.isEmpty() && visitedState.peek() == 2) {
visitedState.pop();
// 這里不能root = stack.pop()然后在打印root,因?yàn)槿绻@樣的話,最后一個(gè)元素彈出賦值給root,而這個(gè)root不為空,一直while循環(huán)不會(huì)跳出
System.out.print(stack.pop().getData() + " ");
}
if (!stack.isEmpty()) {
// 注意先取出來(lái)而不刪除,等到訪問(wèn)狀態(tài)為2才能刪除
root = stack.peek();
root = root.rchild;
// 上句訪問(wèn)過(guò)右孩子了,應(yīng)該更新訪問(wèn)狀態(tài)到2
visitedState.pop(); // 彈出1,壓入2
visitedState.push(2);
}
}
}
/**
* 層序遍歷
*/
public void levelOrder(Node root) {
if (root == null) {
return;
}
Queue<Node> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
Node node = queue.poll();
System.out.print(node.data+" ");
if (node.lchild != null) queue.offer(node.lchild);
if (node.rchild != null) queue.offer(node.rchild);
}
}
public void preOrder() {
preOrder(root);
}
public void inOrder() {
inOrder(root);
}
public void postOrder() {
postOrder(root);
}
public boolean isEmpty() {
return nodesNum == 0;
}
// 實(shí)際上是刪除以該結(jié)點(diǎn)為根結(jié)點(diǎn)的子樹,后序遍歷
public void deleteSubTree(Node<Item> node) {
if (node == null) {
return;
}
// 結(jié)點(diǎn)信息被清空了,但是結(jié)點(diǎn)本身不是null,對(duì)data進(jìn)行判斷,如果data已經(jīng)為空就不自減了
if (node.data != null) {
nodesNum--;
}
deleteSubTree(node.lchild);
deleteSubTree(node.rchild);
// 刪除根結(jié)點(diǎn)結(jié)點(diǎn)信息
node.lchild = null;
node.rchild = null;
node.data = null;
}
public void clear() {
deleteSubTree(root);
// root.lchild和root.rchild雖然為空了但是root還不為空
root = null;
}
// 根據(jù)卡特蘭數(shù)遞推公式 h(n)=h(n-1)*(4*n-2)/(n+1)
// 已知 h(1) = 1;
// 無(wú)窮數(shù)列,越到后面數(shù)字越大,使用BigInteger
public static BigInteger numOfTreeShape(int n) {
BigInteger a = BigInteger.ONE;
for (int i = 2; i <= n; i++) {
a = a.multiply(BigInteger.valueOf(4 * i - 2)).divide(BigInteger.valueOf(i + 1));
}
return a;
}
public static void main(String[] args) {
BinaryTree<String> tree = new BinaryTree<>();
tree.setRoot("A");
Node<String> root = tree.root();
tree.addLeftChild("B", root);
tree.addRightChild("C", root);
tree.addLeftChild("D", root.getLchild());
tree.addLeftChild("E", root.getRchild());
tree.addRightChild("F", root.getRchild());
tree.addLeftChild("G", root.getLchild().getLchild());
tree.addRightChild("H", root.getLchild().getLchild());
tree.addRightChild("I", root.getRchild().getLchild());
System.out.println("前序遍歷如下");
tree.preOrder();
System.out.println("\n中序遍歷如下");
tree.inOrder();
System.out.println("\n后序遍歷如下");
tree.postOrder();
System.out.println("\n非遞歸后序遍歷:");
tree.postOrder2(tree.root());
System.out.println("\n層序遍歷:");
tree.levelOrder(tree.root());
System.out.println();
System.out.println(root.getRchild().getLchild().getData() + "的父結(jié)點(diǎn)是" + tree.parentTo(root.getRchild().getLchild()).getData());
System.out.println("樹的深度是" + tree.depth());
System.out.println("樹的度是" + tree.degree());
System.out.println("樹的結(jié)點(diǎn)數(shù)是" + tree.nodesNum());
System.out.println("結(jié)點(diǎn)數(shù)為" + tree.nodesNum() + "的二叉樹,共有" + numOfTreeShape(tree.nodesNum()) + "種不同的形態(tài)");
// 刪除左子樹
tree.deleteSubTree(root.getLchild());
System.out.println("還剩" + tree.nodesNum() + "個(gè)結(jié)點(diǎn)");
// 刪除右結(jié)點(diǎn)的左子樹
tree.deleteSubTree(root.getRchild().getLchild());
System.out.println("還剩" + tree.nodesNum() + "個(gè)結(jié)點(diǎn)");
// 清空樹
tree.clear();
System.out.println(tree.isEmpty());
}
}
/* Outputs:
前序遍歷如下
A B D G H C E I F
中序遍歷如下
G D H B A E I C F
后序遍歷如下
G H D B I E F C A
非遞歸后序遍歷:
G H D B I E F C A
層序遍歷:
A B C D E F G H I
E的父結(jié)點(diǎn)是C
樹的深度是4
樹的度是2
樹的結(jié)點(diǎn)數(shù)是9
結(jié)點(diǎn)數(shù)為9的二叉樹,共有4862種不同的形態(tài)
還剩5個(gè)結(jié)點(diǎn)
還剩3個(gè)結(jié)點(diǎn)
true
*/
因?yàn)槊總€(gè)結(jié)點(diǎn)的兩個(gè)指針域就是其左右孩子,所以我們把獲取孩子結(jié)點(diǎn)的實(shí)現(xiàn)放到了Node類中。獲取某個(gè)結(jié)點(diǎn)的父結(jié)點(diǎn)比較麻煩,我這里使用了遞歸的方式,如果當(dāng)前結(jié)點(diǎn)為空,就返回null,不為空就判斷其左右孩子中是否有一個(gè)域所求結(jié)點(diǎn)相同,若是就返回當(dāng)前結(jié)點(diǎn)。若不是,遞歸查找左右子樹。很麻煩,而且遞歸算法復(fù)雜度不敢恭維。基于此考慮,可以給Node類新增一個(gè)parent的指針域。求樹的度,我們換了種方法考慮問(wèn)題,以前的實(shí)現(xiàn)中都遍歷了所有結(jié)點(diǎn),從中選出孩子結(jié)點(diǎn)最多的那個(gè)。基于二叉樹的種種性質(zhì),樹的度無(wú)非就三種情況:
- 度為0。一種情況,只有一個(gè)根結(jié)點(diǎn)時(shí)。
- 度為1。當(dāng)二叉樹為斜樹時(shí)候,即結(jié)點(diǎn)數(shù)等于樹的深度時(shí)。
- 度為2。除以上兩種情況的其他情況。
既然要求樹的深度,這里接著說(shuō)。求樹的深度也用了遞歸算法——從上往下遞歸,從最后一層往根結(jié)點(diǎn)返回。如果某個(gè)結(jié)點(diǎn)為空,當(dāng)然深度返回0;否則遞歸查找其左右子樹,直到最后一層,開始返回。返回當(dāng)前結(jié)點(diǎn)左右子樹的深度值較大者并加上1,這里加上1的意義是因?yàn)楹瘮?shù)返回對(duì)應(yīng)著返回到上一層中的父結(jié)點(diǎn)了,深度自然增加1。
再說(shuō)刪除子樹的方法,實(shí)際上利用了后序遍歷刪除了子樹所有結(jié)點(diǎn)的信息。因?yàn)閯h除子樹總結(jié)點(diǎn)數(shù)nodesNum也必須減少。這里由于結(jié)點(diǎn)本身還不是null(從代碼看出只是其lChild,rChild,data被置空),所以判斷其data是否為空來(lái)遞減nodesNum是個(gè)明智的選擇。清空整棵樹的話,調(diào)用刪除子樹的方法,傳入root作為參數(shù)就好了。我們知道最后處理完畢后只是root的信息被置空了,為了達(dá)到真正意義上的空樹,手動(dòng)將root置null就好了。
重點(diǎn)說(shuō)幾個(gè)方法。先說(shuō)求二叉樹的不同形態(tài)的數(shù)目。前面提到過(guò)這是卡特蘭數(shù)的應(yīng)用。由于卡特蘭數(shù)是無(wú)窮序列,越到后面數(shù)值越大,所以得用Java的大整數(shù)BigInteger
實(shí)現(xiàn)。卡特蘭數(shù)列的遞推公式為且已知
,有了這些信息就可以用編程的手段快速計(jì)算出卡特蘭數(shù)了。
public static BigInteger numOfTreeShape(int n) {
BigInteger a = BigInteger.ONE;
// i從2開始因?yàn)閔(1)已知從h(2)開始計(jì)算
for (int i = 2; i <= n; i++) {
a = a.multiply(BigInteger.valueOf(4 * i - 2)).divide(BigInteger.valueOf(i + 1));
}
return a;
}
然后最后說(shuō)說(shuō)二叉樹的遍歷。
二叉樹的遍歷
分為前序遍歷、中序遍歷、后序遍歷三種。
前序遍歷
前序遍歷操作結(jié)點(diǎn)的順序是根結(jié)點(diǎn) -> 左子樹-> 右子樹。前序遍歷的代碼如下
public void preOrder(Node<Item> node) {
if (node == null) {
return;
}
System.out.print(node.getData() + " ");
preOrder(node.lchild);
preOrder(node.rchild);
}
因?yàn)榇蛴〉炔僮髟谶f歸查找左右子樹之前,所以看代碼就可以說(shuō)出這是前序遍歷。分析代碼,判空是遞歸終止的條件,從根結(jié)點(diǎn)開始,不為空就打印。所以前序遍歷最先打印的必然是根結(jié)點(diǎn),然后先查找左子樹,遇到不為空的就打印出來(lái),直到葉子結(jié)點(diǎn),其孩子為空,返回上一層中的父結(jié)點(diǎn),開始執(zhí)行右子樹的遍歷。就這樣不斷執(zhí)行,直到所有的結(jié)點(diǎn)都被訪問(wèn)過(guò),且只會(huì)訪問(wèn)一次。總結(jié)一下,前序遍歷就是當(dāng)前結(jié)點(diǎn)只要不為空就打印;為空,就返回到父結(jié)點(diǎn),繼續(xù)開始處理父結(jié)點(diǎn)的右子樹。了解了這些規(guī)律,下圖為何是這樣的訪問(wèn)順序應(yīng)該也就清楚了。
A不為空打印之,然后遞歸左子樹,打印B、D、G,G的左子樹為空,返回父結(jié)點(diǎn)G,處理其右子樹,也為空,繼續(xù)返回到G的父結(jié)點(diǎn)D,開始處理D的右子樹,打印H,然后不斷返回到A,開始處理右子樹右子樹....最終打印順序是A B D G H C E I F
中序遍歷
public void inOrder(Node<Item> node) {
if (node == null) {
return;
}
inOrder(node.lchild);
System.out.print(node.getData() + " ");
inOrder(node.rchild);
}
只要將打印或其他操作放到遞歸左右子樹之間,也就是中序遍歷了。中序遍歷操作結(jié)點(diǎn)的順序是先左子樹-> 根結(jié)點(diǎn) -> 右子樹。具體來(lái)說(shuō)先沿著樹的左孩子深入,當(dāng)某個(gè)結(jié)點(diǎn)的左孩子不存在時(shí)開始返回,打印該結(jié)點(diǎn),之后繼續(xù)處理這個(gè)結(jié)點(diǎn)的右子樹。由于先是深入到左子樹,直到其沒有左孩子,所以最先打印一般不是根結(jié)點(diǎn)了。還是結(jié)合圖來(lái)理解。
從根結(jié)點(diǎn)A開始一直深入左子樹直到G,由于G沒有左孩子,開始返回并打印父結(jié)點(diǎn)G,然后處理G的右孩子,沒有繼續(xù)返回到G的父結(jié)點(diǎn)D,打印之,處理D的右子樹,然后打印H,返回多次回到A,然后開始處理右子樹...最終打印順序是G D H B A E I C F
后序遍歷
public void postOrder(Node<Item> node) {
if (node == null) {
return;
}
postOrder(node.lchild);
postOrder(node.rchild);
System.out.print(node.getData() + " ");
}
后序遍歷操作結(jié)點(diǎn)的順序是先左子樹-> 右子樹 -> 根結(jié)點(diǎn)。從代碼中可以看出,后序遍歷是當(dāng)左右子樹都訪問(wèn)過(guò)之后才打印,(可以是遇到葉子結(jié)點(diǎn),由于它沒有孩子所以打印會(huì)得到執(zhí)行;也可以是該結(jié)點(diǎn)的左右子樹不為空但是已經(jīng)訪問(wèn)過(guò)了,函數(shù)即將返回時(shí)執(zhí)行打印操作。)所以這種遍歷的規(guī)律是:遞歸直到遇到左子樹的葉子結(jié)點(diǎn),打印該葉子結(jié)點(diǎn),返回到父結(jié)點(diǎn),開始處理右子樹該結(jié)點(diǎn)的右子樹,左右子樹都處理了后返回到父結(jié)點(diǎn)并打印父結(jié)點(diǎn)。最后遍歷的一定是樹的根結(jié)點(diǎn)。
如圖沿著左子樹深入,直到遇到葉子結(jié)點(diǎn)G,打印之,然后返回到父結(jié)點(diǎn)D,處理其右子樹H,H又是葉子結(jié)點(diǎn),打印之,返回父結(jié)點(diǎn)D,此時(shí)D的左右子樹都處理過(guò)了所以打印D,繼續(xù)返回到B,接著處理B的右子樹,為空,打印B,返回到結(jié)點(diǎn)A處理A的右子樹...最終打印順序?yàn)?code>G H D B I E F C A
二叉樹遍歷的非遞歸實(shí)現(xiàn)
如果三種遍歷的思想都已經(jīng)理解透徹,可以嘗試用非遞歸的方式重寫。三種遍歷的實(shí)現(xiàn)都會(huì)使用到棧(Stack),而LinkedList就具備棧的功能。每訪問(wèn)一個(gè)結(jié)點(diǎn),就將其壓入棧。
1、前序遍歷
每訪問(wèn)一個(gè)結(jié)點(diǎn),若不為空,存入棧,并立即打印。然后不斷深入左子樹,直到為空,此時(shí)返回到父結(jié)點(diǎn)(對(duì)應(yīng)的棧操作是出棧),接著處理它的右子樹。
/**
* 前序遍歷--非遞歸
*/
public void preOrder2(Node<Item> root) {
// 用棧保存已經(jīng)訪問(wèn)過(guò)的結(jié)點(diǎn),便于返回到父結(jié)點(diǎn)
LinkedList<Node<Item>> stack = new LinkedList<>();
// 當(dāng)前結(jié)點(diǎn)不為空,或者為空但有可以返回的父結(jié)點(diǎn)(可以進(jìn)行pop操作)都可以進(jìn)入循環(huán)
while (root != null || !stack.isEmpty()) {
// 只要當(dāng)前結(jié)點(diǎn),就打印,同時(shí)入棧
while (root != null) {
stack.push(root);
System.out.print(root.getData()+" ");
root = root.lchild;
}
// 上面while終止說(shuō)明當(dāng)前結(jié)點(diǎn)為空;返回到父結(jié)點(diǎn)并處理它的右子樹。由于root和stack總有一個(gè)不為空,因此在循環(huán)里不會(huì)有stack為空
// 返回到父結(jié)點(diǎn)。由于左孩子為空返回時(shí)已經(jīng)彈出過(guò)父結(jié)點(diǎn)了,所以若是由于右孩子為空返回,會(huì)一次性返回多層
root = stack.pop();
// 開始右子樹的大循環(huán)(第一個(gè)while)
root = root.rchild;
}
}
2、中序遍歷
/**
* 中序遍歷--非遞歸
*/
public void inOrder2(Node<Item> root) {
LinkedList<Node<Item>> stack = new LinkedList<>();
while (root != null || !stack.isEmpty()) {
while (root != null) {
stack.push(root);
root = root.lchild;
}
// 和前序遍歷唯一不同的是,前序遍歷是入棧時(shí)打印,中序遍歷是出棧時(shí)返回到父結(jié)點(diǎn)才打印
// 和前序遍歷一樣,由于左孩子為空返回時(shí)已經(jīng)彈出過(guò)父結(jié)點(diǎn)了,所以若是由于右孩子為空返回,會(huì)一次性返回多層
root = stack.pop();
System.out.print(root.getData()+" ");
root = root.rchild;
}
}
中序遍歷和前序遍歷的非遞歸實(shí)現(xiàn)難度是一樣的,進(jìn)出棧的時(shí)機(jī)也是一樣的,關(guān)鍵是何時(shí)打印的問(wèn)題。前序遍歷由于每訪問(wèn)一個(gè)結(jié)點(diǎn)就立即打印,所以在進(jìn)棧時(shí)就被打印。而中序遍歷是當(dāng)某個(gè)結(jié)點(diǎn)沒有左孩子時(shí),返回到父結(jié)點(diǎn)(對(duì)應(yīng)的棧操作是出棧)并打印,接著處理右子樹...
3、后序遍歷
后序遍歷的實(shí)現(xiàn)稍微麻煩一點(diǎn),因?yàn)楸仨毊?dāng)某結(jié)點(diǎn)的兩個(gè)孩子都訪問(wèn)過(guò)之后才可以打印,我們實(shí)在不能判斷到底是因?yàn)槟辰Y(jié)點(diǎn)的孩子結(jié)點(diǎn)都為空被打印的,還是該結(jié)點(diǎn)已經(jīng)訪問(wèn)過(guò)它的非空左右子樹后被打印的。所以判空的方法肯定是不行了,考慮用另外一個(gè)棧記錄結(jié)點(diǎn)訪問(wèn)其孩子的信息。當(dāng)某結(jié)點(diǎn)訪問(wèn)過(guò)其左孩子后,結(jié)點(diǎn)訪問(wèn)信息的棧存入1(和存入結(jié)點(diǎn)同步進(jìn)行),當(dāng)該結(jié)點(diǎn)訪問(wèn)過(guò)其右孩子后,將值更新為2。若某結(jié)點(diǎn)的訪問(wèn)信息值為2,這時(shí)才將結(jié)點(diǎn)出棧并打印(同時(shí)該結(jié)點(diǎn)的訪問(wèn)信息出棧)。
/**
* 后序遍歷--非遞歸
*/
public void postOrder2(Node<Item> root) {
LinkedList<Node<Item>> stack = new LinkedList<>();
// 存放結(jié)點(diǎn)被訪問(wèn)的信息,1表示只訪問(wèn)過(guò)左孩子,2表示右孩子也訪問(wèn)過(guò)了(此時(shí)可以打印了)
LinkedList<Integer> visitedState = new LinkedList<>();
while (root != null || !stack.isEmpty()) {
while (root != null) {
stack.push(root);
root = root.lchild;
// 上句訪問(wèn)過(guò)左孩子了,放入1
visitedState.push(1);
}
// 這個(gè)while和下面的if不可交換執(zhí)行順序,否則變成了中序遍歷
// 用while而不是if是因?yàn)椋航Y(jié)點(diǎn)已經(jīng)訪問(wèn)過(guò)它的兩個(gè)孩子了,先不打印而處于等待狀態(tài)。隨即判斷若它的右孩子不為空,則仍會(huì)被push進(jìn)去,待右孩子處理完后按照遞歸思想應(yīng)該返回到等待中父結(jié)點(diǎn),由于父結(jié)點(diǎn)訪問(wèn)狀態(tài)已經(jīng)是2,直接打印
while (!stack.isEmpty() && visitedState.peek() == 2) {
visitedState.pop();
// 這里不能root = stack.pop()然后在打印root,因?yàn)槿绻@樣的話,最后一個(gè)元素彈出賦值給root,而這個(gè)root不為空,一直while循環(huán)不會(huì)跳出
System.out.print(stack.pop().getData()+" ");
}
if (!stack.isEmpty()) {
// 注意先取出來(lái)而不刪除,等到訪問(wèn)狀態(tài)為2才能刪除
root = stack.peek();
root = root.rchild;
// 上句訪問(wèn)過(guò)右孩子了,應(yīng)該更新訪問(wèn)狀態(tài)到2
visitedState.pop(); // 彈出1,壓入2
visitedState.push(2);
}
}
}
一定要注意的是,判斷結(jié)點(diǎn)訪問(wèn)信息的while語(yǔ)句和將訪問(wèn)信息更新為2的if語(yǔ)句不可交換順序。否則就變成了中序遍歷了,有興趣的可以試試,這是因?yàn)閯倝喝肓?,馬上就更新為2,然后判斷結(jié)點(diǎn)訪問(wèn)信息的while語(yǔ)句肯定能進(jìn)入(那么這個(gè)記錄結(jié)點(diǎn)訪問(wèn)信息做的是無(wú)用功),出棧接著會(huì)打印出來(lái),這流程和中序遍歷是一模一樣的。然后再說(shuō)判斷結(jié)點(diǎn)訪問(wèn)信息的while為什么不能用if?當(dāng)某個(gè)結(jié)點(diǎn)的左右孩子都已經(jīng)訪問(wèn),我們不是立即打印的,而是讓其處于“已準(zhǔn)備好”的等待狀態(tài),接著判斷該結(jié)點(diǎn)的右孩子如果不為空,這個(gè)孩子結(jié)點(diǎn)也是要入棧的。待右結(jié)點(diǎn)處理完畢并打印,按照后序遍歷遞歸的思想,應(yīng)該返回上一層函數(shù)中,而剛好上一層的父結(jié)點(diǎn)已經(jīng)準(zhǔn)備好,所以直接打印。
4、層序遍歷
二叉樹的前序、中序、后序其實(shí)都相當(dāng)與深度優(yōu)先遍歷DFS,所以用遞歸和棧都能實(shí)現(xiàn)。層序遍歷相當(dāng)于廣度優(yōu)先BFS,故可以用隊(duì)列實(shí)現(xiàn)。從根結(jié)點(diǎn)開始,一層一層的從左往右打印(或其他操作)每個(gè)結(jié)點(diǎn),比如下面的層序遍歷結(jié)果為:ABCDEFGHI
public void levelOrder(Node root) {
if (root == null) {
return;
}
Queue<Node> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
Node node = queue.poll();
System.out.print(node.data+" ");
if (node.lchild != null) queue.offer(node.lchild);
if (node.rchild != null) queue.offer(node.rchild);
}
}
先將根結(jié)點(diǎn)入列、出列打印它,之后是根結(jié)點(diǎn)的左右子結(jié)點(diǎn)入列,按照隊(duì)列的順序出列并依次將其左右子結(jié)點(diǎn)入列......結(jié)點(diǎn)為null時(shí)候不加入隊(duì)列。由此可得到層序遍歷的序列。
由遍歷次序的確定一棵二叉樹
已知前序遍歷序列和中序遍歷序列或者已知中序序列序列和后序遍歷序列是可以確定一棵二叉樹的,這就是說(shuō)推導(dǎo)出的二叉樹有唯一形態(tài)。
已知前序和中序
比如前序遍歷序列為ABCDEF,中序遍歷序列是CBAEDF。問(wèn)中序遍歷序列?
由于二叉樹形態(tài)唯一,中序遍歷只有一種結(jié)果。現(xiàn)在來(lái)分析:前序中,A為根結(jié)點(diǎn)。于是中序中,C、B為左子樹,E、D、F為右子樹。回到前序中,A下一個(gè)是B,B肯定就是左孩子了,C是B的孩子但不確定是左孩子還是右孩子;再看中序,先打印的C說(shuō)明C是B的左孩子。然后看右子樹DEF,前序中先打印D說(shuō)明D是A的右孩子,接著打印了E說(shuō)明E是D的左孩子。F可能是D的右孩子也可能是E的某一個(gè)孩子。再看中序,E之后是D這就說(shuō)明了E沒有孩子,只能F是D的右孩子了。完畢。由此畫出二叉樹就能得到后序遍歷序列。
已知中序和后序
比如中序序列ABCDEFG,后序序列BDCAFGE,求前序序列?
先看后序確定根結(jié)點(diǎn)為E,則在中序中,ABCD為左子樹,F(xiàn)G為右子樹。由后序BDCA的順序,知到A是根結(jié)點(diǎn)E的左孩子,后序中的FG可以看出G是根結(jié)點(diǎn)E的右孩子。中序中先打印A說(shuō)明A沒有左孩子,BCD位于A的右側(cè);結(jié)合后序中BDCA,說(shuō)明C是A的右孩子,則中序中ABCD的打印順序,知道B是C的左孩子,D是C的右孩子。接著來(lái)到根結(jié)點(diǎn)的右子樹FG,因?yàn)橹行蛑邢却蛴,說(shuō)明F是G的左孩子。完畢。由此畫出二叉樹就能得到前序遍歷序列。
上面的兩個(gè)推導(dǎo)過(guò)程看得人很暈,最好是自己在紙上畫畫,比想象中要簡(jiǎn)單!
那么能否根據(jù)前序遍歷序列和后序遍歷序列確定一棵二叉樹呢?不能。
必須知道中序序列才能確定一棵唯一的二叉樹,因?yàn)橹行虮闅v序列可以區(qū)分出左右子樹。(根結(jié)點(diǎn)左邊的是左子樹,右邊的是右子樹),所以只根據(jù)前序遍歷序列和后序遍歷序列,可能得到多個(gè)形態(tài)的二叉樹,它們前序、后序遍歷出來(lái)是的結(jié)果相同。
by @sunhaiyu
2017.9.12