資料結構-二叉樹的儲存結構與遍歷

IAM四十二發表於2017-10-24

定義

一個有窮的結點集合,可以為空。若不為空,則它是由根結點和稱為其左子樹右子樹的兩個互不相交的二叉樹組成。

  1. 二叉樹的五種基本形態:

tree_state
tree_state

  1. 二叉樹的子樹是有順序之分的,稱為左子樹和右子樹

left_right_tree
left_right_tree

  1. 幾種特殊的二叉樹
    • 斜二叉樹

skew_tree
skew_tree

  • 完美二叉樹(滿二叉樹)

full_tree
full_tree

  • 完全二叉樹
    有n個結點的二叉樹,對樹中結點按從上之下,從左至右的順序進行編號,編號為i(1<=1<=n)結點與滿二叉樹中編號為i結點在二叉樹中位置相同

二叉樹的幾個重要性質:

  1. 在二叉樹的第i層上最多有2 i-1 個節點 。(i>=1)
  2. 二叉樹中如果深度為k,那麼最多有2k-1個節點。(k>=1)
  3. 對任何非空二叉樹T,若n0表示度數為0的節點 n2表示度數為2的節點,那麼n0=n2+1;
  4. 具有n個結點的完全二叉樹的深度為 log2 n + 1

這裡在補充一下樹的其他一些性質和概念:

  1. 結點的度:結點所擁有的子樹的個數稱為結點的度;
  2. 樹的度:樹中各節點的度的最大值;因此,二叉樹的度最大為2;
  3. 結點的層數:規定根節點的層數為1,其餘結點的層數為他的雙親結點層數加1
  4. 輸的深度:樹中所有結點的最大層數。

二叉樹的抽象資料型別(ADT)

對於二叉樹的元素,主要的操作包括:

  1. 判別二叉樹是否為空
  2. 遍歷二叉樹,按特定的順序訪問每個結點
    • 前序遍歷:根節點-->左子樹-->右子樹
    • 中序遍歷:左子樹-->根節點-->右子樹
    • 後序遍歷:左子樹-->右子樹-->根節點
    • 層序遍歷:從上至下,從左至右。
  3. 建立一個二叉樹

二叉樹的儲存結構

順序儲存結構

linear_tree
linear_tree

使用順序儲存結構,對完全二叉樹這種結構是非常合適的。可以按照從上之下,從左至右順序儲存n個結點的完全二叉樹的結點父子關係

linear_tree_array
linear_tree_array

完全二叉樹的這種儲存結構,有以下特點

  • 非根節點(序號i>1)的父節點序號(陣列下標)是 i/2 (取整)。
  • 結點(序號為i)的左孩子結點的序號是2i,如果2i>n,則沒有左孩子;
  • 結點(序號為i)的右孩子結點的序號是2i+1,如果2i+1>n,則沒有右孩子。

一般普通的二叉樹,在其空餘位置補充控制,當做是完全二叉樹,採用陣列結構儲存,將導致儲存空間的浪費。

鏈式儲存結構

二叉樹的鏈式儲存結構中,每一個結點包含三個關鍵屬性:指向左子樹的指標,資料域,指向右子樹的指標;根據這個敘述,我們可以按如下結構定義結點。

link_tree
link_tree

結點定義
/**
 * Created by engineer on 2017/10/23.
 * <p>
 * 二叉樹結點定義
 */

public class TreeNode<T> {

    // 資料域
    private T data;
    // 左子樹
    private TreeNode<T> leftChild;
    // 右子樹
    private TreeNode<T> rightChild;

    public TreeNode(T data) {
        this(null, data, null);
    }

    public TreeNode(TreeNode<T> leftChild, T data, TreeNode<T> rightChild) {
        this.leftChild = leftChild;
        this.data = data;
        this.rightChild = rightChild;
    }

    public T getData() {
        return data;
    }

    public TreeNode<T> getLeftChild() {
        return leftChild;
    }

    public TreeNode<T> getRightChild() {
        return rightChild;
    }
}複製程式碼
二叉樹初始化

我們就以下圖為例,構造一顆二叉樹。

/**
     * 構建二叉樹
     *
     * @return 樹根
     */
    TreeNode CreateTree() {
        TreeNode<String> nodeH = new TreeNode<>("H");
        TreeNode<String> nodeG = new TreeNode<>("G");

        TreeNode<String> nodeF = new TreeNode<>(nodeH, "F", null);
        TreeNode<String> nodeE = new TreeNode<>(nodeG, "E", null);
        TreeNode<String> nodeD = new TreeNode<>("D");

        TreeNode<String> nodeC = new TreeNode<>(null, "C", nodeF);
        TreeNode<String> nodeB = new TreeNode<>(nodeD, "B", nodeE);
        TreeNode<String> nodeA = new TreeNode<>(nodeB, "A", nodeC);
        return nodeA;
    }複製程式碼

這樣,我們就按上圖所示構建了一顆二叉樹,返回二叉樹的根結點。

二叉樹的遍歷

二叉樹的遍歷是二叉樹最要的操作,也是二叉樹的核心。從二叉樹的定義我們可以得知,二叉樹是一種遞迴形式的資料結構,根結點下的左右子樹又分別是二叉樹;因此這使得二叉樹的遍歷離不開遞迴這種思想。

很顯然,對於二叉樹的三種遍歷,我們就可以藉助其自身的特性,通過遞迴實現。

  • 二叉樹的遞迴遍歷實現
/**
     * 訪問每個結點
     *
     * @param node
     */
    private void visitNode(TreeNode node) {
        System.out.print(node.getData().toString());
        System.out.print(" ");
    }

    /**
     * 前序遍歷-遞迴實現
     *
     * @param node
     */
    void preTraversal(TreeNode node) {
        if (node != null) {
            visitNode(node);
            preTraversal(node.getLeftChild());
            preTraversal(node.getRightChild());
        }
    }

    /**
     * 中序遍歷-遞迴實現
     *
     * @param node
     */
    void traversal(TreeNode node) {
        if (node != null) {
            traversal(node.getLeftChild());
            visitNode(node);
            traversal(node.getRightChild());
        }
    }

    /**
     * 後序遍歷-遞迴實現
     * @param node
     */
    void postTraversal(TreeNode node) {
        if (node != null) {
            postTraversal(node.getLeftChild());
            postTraversal(node.getRightChild());
            visitNode(node);
        }
    }複製程式碼

可以看到,使用遞迴實現二叉樹的遍歷十分簡單,但我們也可以考慮使用非遞迴的形式,使用棧。

嚴格來說,使用棧實現二叉樹的遍歷,其實還是遞迴思想,只不過是我們自己用棧完成了遞迴實現中系統幫我們完成的工作

本質上來說,二叉樹這種遞迴的資料結構,他的遍歷是離不開遞迴思想的,只不過看我們怎麼去理解遞迴的實現了。

  • 二叉樹的非遞迴實現
/**
     * 前序遍歷-迭代實現
     * @param node
     */
    void preTraversalIteration(TreeNode node) {
        // 建立一個棧
        Stack<TreeNode> mStack = new Stack<>();
        while (true) {
            while (node != null) { // 非葉子結點的子樹
                // 前序遍歷,先訪問根結點
                visitNode(node);
                // 將當前結點壓入棧
                mStack.push(node);
                // 對左子樹繼續進行前序遍歷
                node=node.getLeftChild();
            }

            if (mStack.isEmpty()) {
                //所有元素已遍歷完成
                break;
            }
            // 彈出棧頂結點
            node=mStack.pop();
            // 右子樹前序遍歷
            node=node.getRightChild();
        }
    }

    /**
     * 中序遍歷-迭代實現
     * @param node
     */
    void TraversalIteration(TreeNode node) {
        // 建立一個棧
        Stack<TreeNode> mStack = new Stack<>();
        while (true) {
            while (node != null) { // 非葉子結點的子樹
                // 將當前結點壓入棧
                mStack.push(node);
                // 對左子樹繼續進行中序遍歷
                node=node.getLeftChild();
            }

            if (mStack.isEmpty()) {
                //所有元素已遍歷完成
                break;
            }
            // 彈出棧頂結點
            node=mStack.pop();
            // 中序遍歷,訪問根結點
            visitNode(node);
            // 右子樹中序遍歷
            node=node.getRightChild();
        }
    }

    /**
     * 後序遍歷-迭代實現
     * @param node
     */
    void postTraversalIteration(TreeNode node) {
        // 建立一個棧
        Stack<TreeNode> mStack = new Stack<>();
        while (true) {
            if (node != null) {
                //當前結點非空,壓入棧
                mStack.push(node);
                // 左子樹繼續遍歷
                node=node.getLeftChild();
            }else {
                // 左子樹為空

                if(mStack.isEmpty()){
                    return;
                }

                if (mStack.lastElement().getRightChild() == null) {
                    // 棧頂元素右子樹為空,則當前結點為葉子結點,輸出
                    node=mStack.pop();
                    visitNode(node);
                    while (node == mStack.lastElement().getRightChild()) {
                        visitNode(mStack.lastElement());
                        node=mStack.pop();
                        if (mStack.isEmpty()) {
                            break;
                        }
                    }
                }

                if (!mStack.isEmpty()) {
                    node=mStack.lastElement().getRightChild();
                }else {
                    node=null;
                }
            }
        }
    }複製程式碼

可以看到,雖說是非遞迴實現,但本質上還是依靠棧先進後出的特性,實現了遞迴訪問每個結點的操作,無非就是在前、中、後三種順序下,訪問結點的時機不同而已。這裡,前序和中序遍歷的實現其實很容易理解,後續遍歷的實現很考究對棧的使用理解

  • 層序遍歷

最後,再來說一說層序遍歷。顧名思義,層序遍歷就是從上到下按層,從左至右依次訪問每個結點。這種遍歷非常用規律,就是從根節點下一層開始,優先訪問每一層所有的雙親結點,然後依次訪問每個結點的左右兒子。也就是說,從上到下,先遇見到結點先訪問,後遇到的結點後訪問,這典型的就是佇列的思想,因此我們可以使用佇列實現二叉樹的層序遍歷。

/**
     * 層序遍歷
     * @param node
     */
    void levelTraversal(TreeNode node) {
        //建立佇列
        Queue<TreeNode> mNodeQueue = new LinkedList<>();
        // 根結點加入佇列
        mNodeQueue.add(node);

        TreeNode temp;

        while (!mNodeQueue.isEmpty()) {
            //元素出佇列
            temp=mNodeQueue.poll();
            //輸出
            visitNode(temp);
            if (temp.getLeftChild() != null) {
                // 左子樹入佇列
                mNodeQueue.add(temp.getLeftChild());
            }

            if (temp.getRightChild() != null) {
                //右子樹入佇列
                mNodeQueue.add(temp.getRightChild());
            }
        }
    }複製程式碼

測試二叉樹的實現

最後,用一個測試類測試一下我們對二叉樹的實現。

/**
 * Created by engineer on 2017/10/24.
 */

public class BinaryTreeTest {
    public static void main(String[] args) {
        BinaryTree mBinaryTree = new BinaryTree();

        TreeNode root = mBinaryTree.CreateTree();

        System.out.print("前序遍歷-遞迴實現:");
        mBinaryTree.preTraversal(root);
        System.out.print("\n中序遍歷-遞迴實現:");
        mBinaryTree.traversal(root);
        System.out.print("\n後序遍歷-遞迴實現:");
        mBinaryTree.postTraversal(root);
        System.out.println();
        System.out.print("\n前序遍歷-迭代實現:");
        mBinaryTree.preTraversalIteration(root);
        System.out.print("\n中序遍歷-迭代實現:");
        mBinaryTree.TraversalIteration(root);
        System.out.print("\n後序遍歷-迭代實現:");
        mBinaryTree.postTraversalIteration(root);
        System.out.println();
        System.out.print("\n層序遍歷:");
        mBinaryTree.levelTraversal(root);

    }
}複製程式碼

得到輸出:

前序遍歷-遞迴實現:A B D E G C F H 
中序遍歷-遞迴實現:D B G E A C H F 
後序遍歷-遞迴實現:D G E B H F C A 

前序遍歷-迭代實現:A B D E G C F H 
中序遍歷-迭代實現:D B G E A C H F 
後序遍歷-迭代實現:D G E B H F C A 

層序遍歷:A B C D E F G H複製程式碼

嗯,和預期想象的一致。


好了,二叉樹的儲存結構和遍歷就到這裡了。

相關文章