數據結構-二叉樹的存儲結構與遍歷

定義

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

  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

這裏在補充一下樹的其餘一些性質和概念:node

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

二叉樹的抽象數據類型(ADT)

對於二叉樹的元素,主要的操做包括:數組

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

二叉樹的存儲結構

順序存儲結構

linear_tree
linear_tree

使用順序存儲結構,對徹底二叉樹這種結構是很是合適的。能夠按照從上之下,從左至右順序存儲n個結點的徹底二叉樹的結點父子關係bash

linear_tree_array
linear_tree_array

徹底二叉樹的這種存儲結構,有如下特色數據結構

  • 非根節點(序號i>1)的父節點序號(數組下標)是 i/2 (取整)。
  • 結點(序號爲i)的左孩子結點的序號是2i,若是2i>n,則沒有左孩子;
  • 結點(序號爲i)的右孩子結點的序號是2i+1,若是2i+1>n,則沒有右孩子。

通常普通的二叉樹,在其空餘位置補充控制,當作是徹底二叉樹,採用數組結構存儲,將致使存儲空間的浪費。post

鏈式存儲結構

二叉樹的鏈式存儲結構中,每個結點包含三個關鍵屬性:指向左子樹的指針,數據域,指向右子樹的指針;根據這個敘述,咱們能夠按以下結構定義結點。測試

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;
    }
}複製代碼
二叉樹初始化

咱們就如下圖爲例,構造一顆二叉樹。ui

/** * 構建二叉樹 * * @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;
    }複製代碼

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

二叉樹的遍歷

二叉樹的遍歷是二叉樹最要的操做,也是二叉樹的核心。從二叉樹的定義咱們能夠得知,二叉樹是一種遞歸形式的數據結構,根結點下的左右子樹又分別是二叉樹;所以這使得二叉樹的遍歷離不開遞歸這種思想。spa

很顯然,對於二叉樹的三種遍歷,咱們就能夠藉助其自身的特性,經過遞歸實現。

  • 二叉樹的遞歸遍歷實現
/** * 訪問每一個結點 * * @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複製代碼

嗯,和預期想象的一致。


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

相關文章
相關標籤/搜索