1. 程式人生 > >二叉樹的常見方法及三種遍歷方式 Java 實現

二叉樹的常見方法及三種遍歷方式 Java 實現

讀完本文你將瞭解到:

樹的分類有很多種,但基本都是 二叉樹 的衍生,今天來學習下二叉樹。

這裡寫圖片描述

什麼是二叉樹 Binary Tree

先來個定義:

二叉樹是有限個節點的集合,這個集合可以是空集,也可以是一個根節點和至多兩個子二叉樹組成的集合,其中一顆樹叫做根的左子樹,另一棵叫做根的右子樹。

簡單地說,二叉樹是每個節點至多有兩個子樹的樹,下面的家譜就是一個形象的二叉樹:

這裡寫圖片描述

二叉樹的定義是一個遞迴的定義,其中值得注意的是左右子樹的概念,因為有左、右之分,下面兩棵樹並不是同樣的二叉樹:

shixinzhang

兩種特殊的二叉樹

有兩種特殊的二叉樹:

  • 滿二叉樹
  • 完全二叉樹

滿二叉樹

在上文 樹及 Java 實現

 中我們介紹了 樹的高度 的定義,而這裡 滿二叉樹 的定義是:

如果一棵樹的高度為 k,且擁有 2^k-1 個節點,則稱之為 滿二叉樹。

什麼意思呢?

就是說,每個節點要麼必須有兩棵子樹,要麼沒有子樹。

完全二叉樹

完全二叉樹是一種特殊的二叉樹,滿足以下要求:

  1. 所有葉子節點都出現在 k 或者 k-1 層,而且從 1 到 k-1 層必須達到最大節點數;
  2. 第 k 層可是不是慢的,但是第 k 層的所有節點必須集中在最左邊。

簡單地說, 
就是葉子節點都必須在最後一層或者倒數第二層,而且必須在左邊。任何一個節點都不能沒有左子樹卻有右子樹。

滿二叉樹 和 完全二叉樹 的對比圖

來一張圖對比下兩者:

shixinzhang

二叉樹的實現

二叉樹的實現比普通樹簡單,因為它最多隻有兩個節點嘛。

用 遞迴節點實現法/左右連結串列示法 表示一個二叉樹節點

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;
    }
}

用這種實現方式表示的節點建立的樹,結構如右圖所示:

shixinzhang

用 陣列下標表示法 表示一個節點

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);
}

遍歷小結

可以看到,三種遍歷方式的區別就在於遞迴的先後。

shixinzhang

以上圖為例,三種遍歷結果:

先序遍歷: 
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

樣例輸出是多少呢?