二叉樹的順序儲存實現及遍歷
關於二叉樹的實現,常見的大概有三種實現方法:
- 順序儲存:採用陣列來記錄二叉樹的所有節點
- 二叉連結串列儲存: 每個節點保留一個left,right域,指向左右孩子
- 三叉連結串列儲存: 每個節點保留一個left, right, parent域,指向左右孩子和父親
根據滿二叉樹的特性,第i層的節點數量為2^(i-1),對於一個深度為n的二叉樹,節點數最多為2^n - 1。因此,只需要定義一個長度為2^n-1的陣列即可定義出深度為n的二叉樹。
注意:對於非完全二叉樹,陣列中必然會存在空元素的情況,這樣情況下空間浪費比較嚴重。因此僅建議滿二叉樹使用順序儲存來實現,以便實現緊湊儲存和高效訪問。
順序儲存的實現
- 使用一個數組來儲存所有節點
- 按陣列下標進行儲存,根節點儲存在下標0處,其左孩子儲存於下標2*0+1,右孩子儲存於下標2*0+2 …依次型別。
- 下標為i的節點,左右孩子儲存於下標2*i+1與2*i+2
簡易程式碼:
public class OrderBinaryTree<T> {
// 二叉樹所有節點的順序儲存的陣列
public Object[] datas;
// 二叉樹的規模最大值為2的deep次方-1
public int arraySize;
// 樹的深度
public int deep;
// 預設深度
public static int DEFAULT_DEEP = 8;
public OrderBinaryTree() {
this.deep = DEFAULT_DEEP;
this.arraySize = (int) Math.pow(2, deep) - 1;
this.datas = new Object[arraySize];
}
public OrderBinaryTree(int deep) {
this.deep = deep;
this.arraySize = (int ) Math.pow(2, deep) - 1;
this.datas = new Object[arraySize];
}
public OrderBinaryTree(int deep, T root) {
this(deep);
this.datas[0] = root;
}
/**
* 為指定的節點新增子節點
*
* @param index
* 該節點的索引
* @param data
* 被新增為子節點的索引
* @param left
* 是否為左孩子
* @throws Exception
**/
public void add(int index, T data, Boolean left) throws Exception {
int needSize = left? 2 * index + 1: 2 * index + 2;
if (needSize >= datas.length) {
throw new Exception("陣列越界");
}
if (left) {
datas[2 * index + 1] = data;
} else {
datas[2 * index + 2] = data;
}
}
/**
* 為指定的節點新增子節點
*
* @param index
* 該節點的索引
* @return 子節點的索引
**/
public int getLeft(int index) throws Exception {
if (index >= 0 && datas.length > 2 * index + 1) {
return 2 * index + 1;
}
return -1;
}
/**
* 為指定的節點新增子節點
*
* @param index
* 該節點的索引
* @return 子節點的索引
**/
public int getRight(int index) throws Exception {
if (index >= 0 && datas.length > 2 * index + 2) {
return 2 * index + 2;
}
return -1;
}
}
上述程式碼實現了一個非常簡易的二叉樹,當然二叉樹還有很多操作上述沒有列出來。現在使用上述定義的資料結構來實現一個二叉樹例項:
OrderBinaryTree<String> orderBinaryTree = new OrderBinaryTree<String>(3, "a");
orderBinaryTree.add(0, "b", true);
orderBinaryTree.add(0, "c", false);
orderBinaryTree.add(1, "d", true);
orderBinaryTree.add(1, "e", false);
orderBinaryTree.add(2, "f", true);
orderBinaryTree.add(2, "g", false);
上述定義來一個如下面所示的滿二叉樹:
二叉樹遍歷
遍歷二叉樹指的是按某種規律依次訪問二叉樹的每個節點,對於二叉樹的遍歷就是將一個非線性結構的二叉樹中節點排列在一個線性序列上的過程。
遍歷方法:
- 深度優先遍歷
- 廣度優先遍歷
其中廣度優先遍歷非常簡單,就按層遍歷,訪問第一層,第二層,..對於我們的順序實現的二叉樹來說,底層陣列就是其廣度優先遍歷的結果。現在我們重點介紹深度優先遍歷。
深度優先遍歷
深度優先遍歷分3種(假設L,D,R分別表示左,根,右子樹):
- 先序遍歷: DLR
- 中序遍歷: LDR
- 後序遍歷: LRD
一·先序遍歷
對於先序遍歷,或許你經常會看到如下的遞迴程式碼:
public void travPreRecursion() {
return this.preRecursion(0);
}
private List preRecursion(int index) {
List list = new ArrayList();
list.add(datas[index]);
if (2 * index + 1 < arraySize) {
list.addAll(preRecursion(2 * index + 1));// 左兒子
}
if (2 * index + 2 < arraySize) {
list.addAll(preRecursion(2 * index + 2));// 右兒子
}
return list;
}
筆者認為遞迴演算法不僅效率低而且不利於閱讀者理解,因此筆者將其修改為迴圈版
先沿著最左側通路自頂而下訪問沿路節點,再底而上依次遍歷這些節點的右節點
public List travPreCirculate() {
int index = 0;
//定義一個輔助棧來完成最左側通路自頂而下的節點的右節點
Stack<Integer> stack = new Stack<Integer>();
List list = new ArrayList();
try {
while (true) {
//從index開始最左側通路自頂而下的訪問節點,直到底
//並把每個訪問到的節點儲存在stack
setRightStack(index, stack, list);
//如果一條左子數鏈路中沒有任何一個右子數,則遍歷結束
if (stack.empty()) {
break;
}
// 從來臨時棧中依次pop出節點,並繼續取其右節點
index = stack.pop();
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return list;
}
public void setRightStack(int index, Stack<Integer> stack, List list) {
try {
// 從index開始訪問其左子數,並將其所有的右子樹全部push到臨時棧中
while (index >= 0) {
list.add(datas[index]);
int right = getRight(index);
if (right > 0) {
stack.push(right);
}
index = this.getLeft(index);
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
二·中序遍歷
對於中序遍歷,與先序遍歷類似,你經常會看到如下的遞迴程式碼:
public void travPreRecursion() {
return this.preRecursion(0);
}
private List preRecursion(int index) {
List list = new ArrayList();
if (2 * index + 1 < arraySize) {
list.addAll(preRecursion(2 * index + 1));// 左兒子
}
list.add(datas[index]);
if (2 * index + 2 < arraySize) {
list.addAll(preRecursion(2 * index + 2));// 右兒子
}
return list;
}
當然,筆者還是要將其修改為迴圈模式
沿著最左側鏈路,自底而上依次訪問每個節點的右子樹
public void travInCirculate() {
int index = 0;
Stack<Integer> stack = new Stack<Integer>();
List list = new ArrayList();
while (true) {
//將index節點的左子樹全部push到stack中
setLeftStack(index, stack);
if (stack.isEmpty()) {
break;
}
//pop出左子樹
index = stack.pop();
list.add(datas[index]);
try {
//取該節點的右孩子,並繼續push其所有左子樹到棧中
index = this.getRight(index);
} catch (Exception e) { // TODO Auto-generated catch
block e.printStackTrace();
}
}
}
public void setLeftStack(int index, Stack<Integer> stack) {
try {
// 將root開始的所有右子樹全部push到臨時棧中
while (index >= 0) {
stack.push(index);
index = getLeft(index);
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
三.後序遍歷
後序遍歷的遞迴程式碼:
// 後序遍歷(遞迴)
public void travPostRecursion() {
postRecursion(0);
}
public void postRecursion(int index) {
if (2 * index + 1 < arraySize) {
postRecursion(2 * index + 1);// 左兒子
}
if (2 * index + 2 < arraySize) {
postRecursion(2 * index + 2);// 右兒子
}
System.out.println(datas[index]);
}
與上述一致,改造為迴圈
先序,中序遍歷中,第一位元素一定是某個節點的左孩子。
當後序遍歷不一定,後序遍歷的首位原型應該是左子樹中度最高的一個元素,可能是左孩子也有可能是右孩子
// 後序遍歷(迴圈)
public void travPostCirculate() {
int index = 0;
Stack<Integer> stack = new Stack<Integer>();
List list = new ArrayList();
while (true) {
//從index開始將其左孩子push到棧中
//如果到了最後一個左孩子,該節點存在右孩子,則繼續push
setStackForPost(index, stack);
if (stack.empty()) {
break;
}
index = stack.pop();
list.add(datas[index]);
// 獲取其右兄弟
int parent;
try {
parent = this.parent(index);
int right = this.getRight(parent);
index = index == right ? -1 : right;
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public void setStackForPost(int index, Stack stack) {
try {
// 將root開始的所有右子樹全部push到臨時棧中
while (index >= 0) {
stack.push(index);
int pre = index;
index = getLeft(index);
if (index < 0) {
// 當到達最左子樹時,如果該最左子樹存在右子樹,則遍歷從該右子樹開始
index = this.getRight(pre);
}
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
補充
對於中序編譯,也會看到如下寫法,思路是一樣
// 中序遍歷(迴圈)
public void travInCirculate() {
int index = 0;
Stack<Integer> stack = new Stack<Integer>();
try {
while (true) {
if(index>=0) {
stack.push(index);
index = this.getLeft(index);
}else if(!stack.empty()) {
index = stack.pop();
System.out.println(datas[index]);
index = this.getRight(index);
}else {
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
筆者認為中序遍歷是最重要的,一項最基本的操作將是定位中序遍歷中某個節點的直接後繼節點,在平衡二叉樹的等場景中會使用