二叉樹的常見方法及三種遍歷方式 Java 實現
讀完本文你將瞭解到:
樹的分類有很多種,但基本都是 二叉樹 的衍生,今天來學習下二叉樹。
什麼是二叉樹 Binary Tree
先來個定義:
二叉樹是有限個節點的集合,這個集合可以是空集,也可以是一個根節點和至多兩個子二叉樹組成的集合,其中一顆樹叫做根的左子樹,另一棵叫做根的右子樹。
簡單地說,二叉樹是每個節點至多有兩個子樹的樹,下面的家譜就是一個形象的二叉樹:
二叉樹的定義是一個遞迴的定義,其中值得注意的是左右子樹的概念,因為有左、右之分,下面兩棵樹並不是同樣的二叉樹:
兩種特殊的二叉樹
有兩種特殊的二叉樹:
- 滿二叉樹
- 完全二叉樹
滿二叉樹
在上文 樹及 Java 實現
如果一棵樹的高度為 k,且擁有 2^k-1 個節點,則稱之為 滿二叉樹。
什麼意思呢?
就是說,每個節點要麼必須有兩棵子樹,要麼沒有子樹。
完全二叉樹
完全二叉樹是一種特殊的二叉樹,滿足以下要求:
- 所有葉子節點都出現在 k 或者 k-1 層,而且從 1 到 k-1 層必須達到最大節點數;
- 第 k 層可是不是慢的,但是第 k 層的所有節點必須集中在最左邊。
簡單地說,
就是葉子節點都必須在最後一層或者倒數第二層,而且必須在左邊。任何一個節點都不能沒有左子樹卻有右子樹。
滿二叉樹 和 完全二叉樹 的對比圖
來一張圖對比下兩者:
二叉樹的實現
二叉樹的實現比普通樹簡單,因為它最多隻有兩個節點嘛。
用 遞迴節點實現法/左右連結串列示法 表示一個二叉樹節點
public class BinaryTreeNode { /* * 一個二叉樹包括 資料、左右孩子 三部分 */ private int mData; private BinaryTreeNode mLeftChild; private BinaryTreeNode mRightChild; public BinaryTreeNode(int data, BinaryTreeNode leftChild, BinaryTreeNode rightChild) { mData = data; mLeftChild = leftChild; mRightChild = rightChild; } public int getData() { return mData; } public void setData(int data) { mData = data; } public BinaryTreeNode getLeftChild() { return mLeftChild; } public void setLeftChild(BinaryTreeNode leftChild) { mLeftChild = leftChild; } public BinaryTreeNode getRightChild() { return mRightChild; } public void setRightChild(BinaryTreeNode rightChild) { mRightChild = rightChild; } }
用這種實現方式表示的節點建立的樹,結構如右圖所示:
用 陣列下標表示法 表示一個節點
public class BinaryTreeArrayNode {
/**
* 陣列實現,儲存的不是 左右子樹的引用,而是陣列下標
*/
private int mData;
private int mLeftChild;
private int mRightChild;
public int getData() {
return mData;
}
public void setData(int data) {
mData = data;
}
public int getLeftChild() {
return mLeftChild;
}
public void setLeftChild(int leftChild) {
mLeftChild = leftChild;
}
public int getRightChild() {
return mRightChild;
}
public void setRightChild(int rightChild) {
mRightChild = rightChild;
}
}
一般使用左右連結串列示的節點來構造二叉樹。
二叉樹的主要方法
有了節點後接下來開始構造一個二叉樹,二叉樹的主要方法有:
- 建立
- 新增元素
- 刪除元素
- 清空
- 遍歷
- 獲得樹的高度
- 獲得樹的節點數
- 返回某個節點的父親節點
- …
1.二叉樹的建立
建立一個二叉樹很簡單,只需要有一個 二叉根節點,然後提供設定根節點的方法即可:
public class BinaryTree {
private BinaryTreeNode mRoot; //根節點
public BinaryTree() {
}
public BinaryTree(BinaryTreeNode root) {
mRoot = root;
}
public BinaryTreeNode getRoot() {
return mRoot;
}
public void setRoot(BinaryTreeNode root) {
mRoot = root;
}
}
2.二叉樹的新增元素
由於二叉樹有左右子樹之分,所以新增元素時也分為兩種情況:新增為左子樹還是右子樹:
public void insertAsLeftChild(BinaryTreeNode child){
checkTreeEmpty();
mRoot.setLeftChild(child);
}
public void insertAsRightChild(BinaryTreeNode child){
checkTreeEmpty();
mRoot.setRightChild(child);
}
private void checkTreeEmpty() {
if (mRoot == null){
throw new IllegalStateException("Can't insert to a null tree! Did you forget set value for root?");
}
}
在每次插入前都會檢查 根節點是否為空,如果是就丟擲異常(跟 Android 原始碼學的嘿嘿)。
3.二叉樹的刪除元素
刪除某個元素很簡單,只需要把自己設為 null。
但是為了避免浪費無用的記憶體,方便 GC 及時回收,我們還需要遍歷這個元素的左右子樹,挨個設為空:
public void deleteNode(BinaryTreeNode node){
checkTreeEmpty();
if (node == null){ //遞迴出口
return;
}
deleteNode(node.getLeftChild());
deleteNode(node.getRightChild());
node = null;
}
4.二叉樹的清空
二叉樹的清空其實就是特殊的刪除元素–刪除根節點,因此很簡單:
public void clear(){
if (mRoot != null){
deleteNode(mRoot);
}
}
5.獲得二叉樹的高度
二叉樹中,樹的高度是 各個節點度的最大值。
因此獲得樹的高度需要遞迴獲取所有節點的高度,然後取最大值。
/**
* 獲取樹的高度 ,特殊的獲得節點高度
* @return
*/
public int getTreeHeight(){
return getHeight(mRoot);
}
/**
* 獲得指定節點的度
* @param node
* @return
*/
public int getHeight(BinaryTreeNode node){
if (node == null){ //遞迴出口
return 0;
}
int leftChildHeight = getHeight(node.getLeftChild());
int rightChildHeight = getHeight(node.getRightChild());
int max = Math.max(leftChildHeight, rightChildHeight);
return max + 1; //加上自己本身
}
6.獲得二叉樹的節點數
獲得二叉樹的節點數,需要遍歷所有子樹,然後加上總和。
public int getSize(){
return getChildSize(mRoot);
}
/**
* 獲得指定節點的子節點個數
* @param node
* @return
*/
public int getChildSize(BinaryTreeNode node){
if (node == null){
return 0;
}
int leftChildSize = getChildSize(node.getLeftChild());
int rightChildSize = getChildSize(node.getRightChild());
return leftChildSize + rightChildSize + 1;
}
7.獲得某個節點的父親節點
由於我們使用左右子樹表示的節點,不含有父親節點引用,因此有時候可能也需要一個方法,返回二叉樹中,指定節點的父親節點。
需要從頂向下遍歷各個子樹,若該子樹的根節點的孩子就是目標節點,返回該節點,否則遞迴遍歷它的左右子樹:
/**
* 獲得指定節點的父親節點
* @param node
* @return
*/
public BinaryTreeNode getParent(BinaryTreeNode node) {
if (mRoot == null || mRoot == node) { //如果是空樹,或者這個節點就是根節點,返回空
return null;
} else {
return getParent(mRoot, node); //否則遞迴查詢 父親節點
}
}
/**
* 遞迴對比 節點的孩子節點 與 指定節點 是否一致
*
* @param subTree 子二叉樹根節點
* @param node 指定節點
* @return
*/
public BinaryTreeNode getParent(BinaryTreeNode subTree, BinaryTreeNode node) {
if (subTree == null) { //如果子樹為空,則沒有父親節點,遞迴出口 1
return null;
}
//正好這個根節點的左右孩子之一與目標節點一致
if (subTree.getLeftChild() == node || subTree.getRightChild() == node) { //遞迴出口 2
return subTree;
}
//需要遍歷這個節點的左右子樹
BinaryTreeNode parent;
if ((parent = getParent(subTree.getLeftChild(), node)) != null) { //左子樹節點就是指定節點,返回
return parent;
} else {
return getParent(subTree.getRightChild(), node); //從右子樹找找看
}
}
二叉樹的遍歷
二叉樹的遍歷單獨介紹,是因為太重要了!以前考試就老考這個。
前面的那些操作可以發現,二叉樹的遞迴資料結構使得很多操作都可以使用遞迴進行。
而二叉樹的遍歷其實也是個 遞迴遍歷的過程,使得每個節點被訪問且僅訪問一次。
根據不同的場景中,根節點、左右子樹遍歷的順序,二叉樹的遍歷分為三種:
- 先序遍歷
- 中序遍歷
- 後序遍歷
這裡先序、中序、後序指的是 根節點相對左右子樹的遍歷順序。
先序遍歷
即根節點在左右子樹之前遍歷:
- 先訪問根節點
- 再先序遍歷左子樹
- 再先序遍歷右子樹
- 退出
程式碼:
/**
* 先序遍歷
* @param node
*/
public void iterateFirstOrder(BinaryTreeNode node){
if (node == null){
return;
}
operate(node);
iterateFirstOrder(node.getLeftChild());
iterateFirstOrder(node.getRightChild());
}
/**
* 模擬操作
* @param node
*/
public void operate(BinaryTreeNode node){
if (node == null){
return;
}
System.out.println(node.getData());
}
中序遍歷
遍歷順序:
- 先中序遍歷左子樹
- 再訪問根節點
- 再中序遍歷右子樹
- 退出
程式碼:
/**
* 中序遍歷
* @param node
*/
public void iterateMediumOrder(BinaryTreeNode node){
if (node == null){
return;
}
iterateMediumOrder(node.getLeftChild());
operate(node);
iterateMediumOrder(node.getRightChild());
}
後序遍歷
即根節點在左右子樹之後遍歷:
- 先後序遍歷左子樹
- 再後序遍歷右子樹
- 最後訪問根節點
- 退出
程式碼:
/**
* 後序遍歷
* @param node
*/
public void iterateLastOrder(BinaryTreeNode node){
if (node == null){
return;
}
iterateLastOrder(node.getLeftChild());
iterateLastOrder(node.getRightChild());
operate(node);
}
遍歷小結
可以看到,三種遍歷方式的區別就在於遞迴的先後。
以上圖為例,三種遍歷結果:
先序遍歷:
1 2 4 5 7 3 6
中序遍歷:
4 2 7 5 1 3 6
後序遍歷:
4 7 5 2 6 3 1
總結
這篇文章介紹了 資料結構中的二叉樹的基本概念,常用操作以及三種遍歷方式。
其中三種遍歷方式一般在面試中可能會考察,給你兩種遍歷結果,讓你畫出實際的二叉樹結構。只要掌握三種遍歷方式的區別,即可解答。
一道筆試題
二叉樹遍歷
題目描述:
給定一棵二叉樹的前序遍歷和中序遍歷,求其後序遍歷(提示:給定前序遍歷與中序遍歷能夠唯一確定後序遍歷)。
輸入:
兩個字串,其長度n均小於等於26。
第一行為前序遍歷,第二行為中序遍歷。
二叉樹中的結點名稱以大寫字母表示:A,B,C….最多26個結點。
輸出:
輸入樣例可能有多組,對於每組測試樣例,
輸出一行,為後序遍歷的字串。
樣例輸入:
FDXEAG
XDEFAG
樣例輸出是多少呢?