Java實現二叉樹和遍歷


leetcode刷題需要經常用的二叉樹,發現二叉樹這種可以無限擴展知識點來虐別人的數據結構,很受面試官的青睞,這里記錄一下Java定義二叉樹和遍歷。

一、什么是二叉樹

1 .二叉樹的性質

本身是有序樹,樹中包含的各個節點的度不能超過 2,即只能是 0、1 或者 2

 

 圖 1 二叉樹示意圖

二叉樹具有以下幾個性質:

  1. 二叉樹中,第 i 層最多有 2i-1 個結點。
  2. 如果二叉樹的深度為 K,那么此二叉樹最多有 2K-1 個結點。
  3. 二叉樹中,終端結點數(葉子結點數)為 n0,度為 2 的結點數為 n2,則 n0=n2+1。

性質 3 的計算方法為:對於一個二叉樹來說,除了度為 0 的葉子結點和度為 2 的結點,剩下的就是度為 1 的結點(設為 n1),那么總結點 n=n0+n1+n2
同時,對於每一個結點來說都是由其父結點分支表示的,假設樹中分枝數為 B,那么總結點數 n=B+1。而分枝數是可以通過 n1 和 n2 表示的,即 B=n1+2*n2。所以,n 用另外一種方式表示為 n=n1+2*n2+1。
兩種方式得到的 n 值組成一個方程組,就可以得出 n0=n2+1。

2 .滿二叉樹

如果二叉樹中除了葉子結點,每個結點的度都為 2,則此二叉樹稱為滿二叉樹




圖 2 滿二叉樹示意圖

滿二叉樹除了滿足普通二叉樹的性質,還具有以下性質:

  1. 滿二叉樹中第 i 層的節點數為 2n-1 個。
  2. 深度為 k 的滿二叉樹必有 2k-1 個節點 ,葉子數為 2k-1
  3. 滿二叉樹中不存在度為 1 的節點,每一個分支點中都兩棵深度相同的子樹,且葉子節點都在最底層。
  4. 具有 n 個節點的滿二叉樹的深度為 log2(n+1)。

3 .完全二叉樹

如果二叉樹中除去最后一層節點為滿二叉樹,且最后一層的結點依次從左到右分布,則此二叉樹被稱為完全二叉樹



圖 3 完全二叉樹示意圖

如圖 3a) 所示是一棵完全二叉樹,圖 3b) 由於最后一層的節點沒有按照從左向右分布,因此只能算作是普通的二叉樹。
完全二叉樹除了具有普通二叉樹的性質,它自身也具有一些獨特的性質,比如說,n 個結點的完全二叉樹的深度為 ⌊log2n⌋+1。

⌊log2n⌋ 表示取小於 log2n 的最大整數。例如,⌊log24⌋ = 2,而 ⌊log25⌋ 結果也是 2。

對於任意一個完全二叉樹來說,如果將含有的結點按照層次從左到右依次標號(如圖 3a)),對於任意一個結點 i ,完全二叉樹還有以下幾個結論成立:

    1. 當 i>1 時,父親結點為結點 [i/2] 。(i=1 時,表示的是根結點,無父親結點)
    2. 如果 2*i>n(總結點的個數) ,則結點 i 肯定沒有左孩子(為葉子結點);否則其左孩子是結點 2*i 。
    3. 如果 2*i+1>n ,則結點 i 肯定沒有右孩子;否則右孩子是結點 2*i+1 。

二、二叉樹的存儲結構

二叉樹的存儲結構有兩種,分別為順序存儲和鏈式存儲。

1 .順序存儲

二叉樹的順序存儲,指的是使用順序表(數組)存儲二叉樹。需要注意的是,順序存儲只適用於完全二叉樹。換句話說,只有完全二叉樹才可以使用順序表存儲。因此,如果我們想順序存儲普通二叉樹,需要提前將普通二叉樹轉化為完全二叉樹。滿二叉樹也可以使用順序存儲。要知道,滿二叉樹也是完全二叉樹,因為它滿足完全二叉樹的所有特征。

普通二叉樹轉完全二叉樹的方法很簡單,只需給二叉樹額外添加一些節點,將其"拼湊"成完全二叉樹即可。如圖 1 所示:



圖 1 普通二叉樹的轉化

圖 1 中,左側是普通二叉樹,右側是轉化后的完全(滿)二叉樹。
解決了二叉樹的轉化問題,接下來學習如何順序存儲完全(滿)二叉樹。
完全二叉樹的順序存儲,僅需從根節點開始,按照層次依次將樹中節點存儲到數組即可。




圖 2 完全二叉樹示意圖

例如,存儲圖 2 所示的完全二叉樹,其存儲狀態如圖 3 所示:


圖 3 完全二叉樹存儲狀態示意圖

同樣,存儲由普通二叉樹轉化來的完全二叉樹也是如此。例如,圖 1 中普通二叉樹的數組存儲狀態如圖 4 所示:



圖 4 普通二叉樹的存儲狀態

由此,我們就實現了完全二叉樹的順序存儲。
不僅如此,從順序表中還原完全二叉樹也很簡單。我們知道,完全二叉樹具有這樣的性質,將樹中節點按照層次並從左到右依次標號(1,2,3,...),若節點 i 有左右孩子,則其左孩子節點為 2*i,右孩子節點為 2*i+1。此性質可用於還原數組中存儲的完全二叉樹,也就是實現由圖 3 到圖 2、由圖 4 到圖 1 的轉變。

2 .鏈式存儲

其實二叉樹並不適合用數組存儲,因為並不是每個二叉樹都是完全二叉樹,普通二叉樹使用順序表存儲或多或多會存在空間浪費的現象。

 



圖 1 普通二叉樹示意圖

如圖 1 所示,此為一棵普通的二叉樹,若將其采用鏈式存儲,則只需從樹的根節點開始,將各個節點及其左右孩子使用鏈表存儲即可。因此,圖 1 對應的鏈式存儲結構如圖 2 所示:




圖 2 二叉樹鏈式存儲結構示意圖

由圖 2 可知,采用鏈式存儲二叉樹時,其節點結構由 3 部分構成(如圖 3 所示):

  • 指向左孩子節點的指針(Lchild);
  • 節點存儲的數據(data);
  • 指向右孩子節點的指針(Rchild);

 



圖 3 二叉樹節點結構
其實,二叉樹的鏈式存儲結構遠不止圖 2 所示的這一種。例如,在某些實際場景中,可能會做 "查找某節點的父節點" 的操作,這時可以在節點結構中再添加一個指針域,用於各個節點指向其父親節點,如圖 4 所示:

 

 圖 4 自定義二叉樹的鏈式存儲結構

這樣的鏈表結構,通常稱為三叉鏈表。

利用圖 4 所示的三叉鏈表,我們可以很輕松地找到各節點的父節點。因此,在解決實際問題時,用合適的鏈表結構存儲二叉樹,可以起到事半功倍的效果。

三、java實現二叉樹

1.Node節點的定義

我們需要自定義一個Node類

package com.hanwl.leetcode;

/**
 * @Author hanwl
 * @Date 2021-03-26 10:30
 * @Version 1.0
 * 定義二叉樹的一個節點node
 */
public class Node {
    private int value;        //節點的值
    private Node node;        //此節點,數據類型為Node
    private Node left;        //此節點的左子節點,數據類型為Node
    private Node right;       //此節點的右子節點,數據類型為Node

    public Node() {
    }

    public Node(int value) {
        this.value = value;
        this.left = null;
        this.right = null;
    }

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }

    public Node getNode() {
        return node;
    }

    public void setNode(Node node) {
        this.node = node;
    }

    public Node getLeft() {
        return left;
    }

    public void setLeft(Node left) {
        this.left = left;
    }

    public Node getRight() {
        return right;
    }

    public void setRight(Node right) {
        this.right = right;
    }
}

2.數組轉換二叉樹

數組存儲方式和樹的存儲方式可以相互轉換,即數組可以轉換成樹,樹也可以轉換成數組

順序二叉樹通常只考慮完全二叉樹

  第n個元素的左子節點為 2 * n + 1

  第n個元素的右子節點為 2 * n + 2

  第n個元素的父節點為 (n-1) / 2

  n : 表示二叉樹中的第幾個元素(按0開始編號,如圖所示)

  對於具有n個節點的完全二叉樹,如果按照從上至下和從左至右的順序對所有節點序號從0開始順序編號,則對於序號為i(0<=i< n)的節點有:
  如果i>0,則序號為i節點的雙親節點的序號為(i-1)/2(/為整除);如果i=0,則序號為i節點為根節點,無雙親節點 如果2i+1<n,則序號為i節點的左孩子節點的序號為2i+1;

  如果2i+1>=n,則序號為i節點無左孩子 如果2i+2<n,則序號為i節點的右孩子節點的序號為2i+2;如果2i+2>=n,則序號為i節點無右孩子

 

        在這里插入圖片描述

 

package com.hanwl.leetcode;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

/**
 * @Author hanwl
 * @Date 2021-03-26 10:36
 * @Version 1.0
    一般拿到的數據是一個int型的數組,那怎么將這個數組變成我們可以直接操作的樹結構呢?
    1、數組元素變Node類型節點
    2、給N/2-1個節點設置子節點
    3、給最后一個節點設置子節點【有可能只有左節點】
 */
public class TreeNode {

    public static void main(String[] args) {
        int[] array = {1, 2, 3, 4, 5, 6, 7};
        List<Node> list = new ArrayList<>();
        TreeNode treeNode = new TreeNode();
        treeNode.create(array,list);

        // nodeList中第0個索引處的值即為根節點
        Node root = list.get(0);
        System.out.println("先序遍歷:");
        preOrderTraverse(root);
        System.out.println();

        System.out.println("中序遍歷:");
        inOrderTraverse(root);
        System.out.println();

        System.out.println("后序遍歷:");
        postOrderTraverse(root);
        System.out.println();

        System.out.println("非遞歸先序遍歷:");
        preOrderTraversalbyLoop(root);
        System.out.println();

        System.out.println("非遞歸中序遍歷:");
        inOrderTraversalbyLoop(root);
        System.out.println();

        System.out.println("非遞歸后序遍歷:");
        postOrderTraversalbyLoop(root);
        System.out.println();

        System.out.println("廣度優先遍歷:");
        levelOrderTraversal(root);
        System.out.println();

        System.out.println("深度優先遍歷:");
        depthTraversal(root);
    }

    //創建二叉樹
    public void create(int[] datas, List<Node> list){
        // 將數組里面的東西變成節點的形式
        for(int i=0;i<datas.length;i++) {
            Node node=new Node(datas[i]);
            list.add(node);
        }

        // 節點關聯成樹
        for(int index=0;index<list.size()/2-1;index++){
            //編號為n的節點他的左子節點編號為2*n 右子節點編號為2*n+1 但是因為list從0開始編號,所以還要+1
            list.get(index).setLeft(list.get(index*2+1));
            //與上同理
            list.get(index).setRight(list.get(index*2+2));
        }

        // 最后一個父節點,因為最后一個父節點可能沒有右孩子,所以單獨拿出來處理 避免單孩子情況
        int lastParentIndex=list.size()/2-1;
        list.get(lastParentIndex).setLeft(list.get(lastParentIndex*2+1));
        if (list.size()%2==1) {
            // 如果有奇數個節點,最后一個父節點才有右子節點
            list.get(lastParentIndex).setRight(list.get(lastParentIndex*2+2));
        }
    }

    /**
     * 先序遍歷
     *
     * 這三種不同的遍歷結構都是一樣的,只是先后順序不一樣而已
     *
     * @param node
     * 遍歷的節點
     */
    public static void preOrderTraverse(Node node) {
        if (node == null)
            return;
        System.out.print(node.getValue() + " ");
        preOrderTraverse(node.getLeft());
        preOrderTraverse(node.getRight());
    }

    /**
     * 中序遍歷
     *
     * 這三種不同的遍歷結構都是一樣的,只是先后順序不一樣而已
     *
     * @param node
     *            遍歷的節點
     */
    public static void inOrderTraverse(Node node) {
        if (node == null)
            return;
        inOrderTraverse(node.getLeft());
        System.out.print(node.getValue() + " ");
        inOrderTraverse(node.getRight());
    }

    /**
     * 后序遍歷
     *
     * 這三種不同的遍歷結構都是一樣的,只是先后順序不一樣而已
     *
     * @param node
     *            遍歷的節點
     */
    public static void postOrderTraverse(Node node) {
        if (node == null)
            return;
        postOrderTraverse(node.getLeft());
        postOrderTraverse(node.getRight());
        System.out.print(node.getValue() + " ");
    }

    /**
     * 非遞歸前序遍歷
     * 基本的原理就是當循環中的p不為空時,就讀取p的值,並不斷更新p為其左子節點,即不斷讀取左子節點,
     * 直到一個枝節到達最后的子節點,再繼續返回上一層進行取值
     *
     * 我這里使用了棧這個數據結構,用來保存不到遍歷過但是沒有遍歷完全的父節點,之后再進行回滾。
     * */
    public static void preOrderTraversalbyLoop(Node node){
        Stack<Node> stack = new Stack();
        Node p = node;
        while(p!=null || !stack.isEmpty()){
            while(p!=null){
                //當p不為空時,就讀取p的值,並不斷更新p為其左子節點,即不斷讀取左子節點
                System.out.print(p.getValue()+" ");
                stack.push(p); //將p入棧
                p = p.getLeft();
            }
            if(!stack.isEmpty()){
                p = stack.pop();
                p = p.getRight();
            }
        }
    }

    /**
     *非遞歸中序遍歷
     * 基本原理就是當循環中的p不為空時,就讀取p的值,並不斷更新p為其左子節點,但是切記這個時候不能進行輸出,必須不斷讀取左子節點,
     * 直到一個枝節到達最后的子節點,然后每次從棧中拿出一個元素,就進行輸出,再繼續返回上一層進行取值
     * */
    public static void inOrderTraversalbyLoop(Node node){
        Stack<Node> stack = new Stack();
        Node p = node;
        while(p!=null || !stack.isEmpty()){
            while(p!=null){
                stack.push(p);
                p = p.getLeft();
            }
            if(!stack.isEmpty()){
                p = stack.pop();
                System.out.print(p.getValue()+" ");
                p = p.getRight();
            }
        }
    }

    /**
     * 非遞歸后序遍歷
     * */
    public static void postOrderTraversalbyLoop(Node node){
        Stack<Node> stack = new Stack<Node>();
        Node p = node,    prev = node;
        while(p!=null || !stack.isEmpty()){
            while(p!=null){
                stack.push(p);
                p = p.getLeft();
            }
            if(!stack.isEmpty()){
                Node temp = stack.peek().getRight();
                //只是拿出來棧頂這個值,並沒有進行刪除
                if(temp == null||temp == prev){
                    //節點沒有右子節點或者到達根節點【考慮到最后一種情況】
                    p = stack.pop();
                    System.out.print(p.getValue()+" ");
                    prev = p;
                    p = null;
                }
                else{
                    p = temp;
                }
            }
        }
    }

    // 廣度優先遍歷 參數node為根節點
    public static void levelOrderTraversal(Node node){
        if(node==null){
            System.out.print("empty tree");
            return;
        }
        ArrayDeque<Node> deque = new ArrayDeque<Node>();
        deque.add(node);
        while(!deque.isEmpty()){
            Node rnode = deque.remove();
            System.out.print(rnode.getValue()+"  ");
            if(rnode.getLeft()!=null){
                deque.add(rnode.getLeft());
            }
            if(rnode.getRight()!=null){
                deque.add(rnode.getRight());
            }
        }
    }

    // 深度優先遍歷 參數node為根節點
    public static void depthTraversal(Node node){
        if(node==null){
            System.out.print("empty tree");
            return;
        }
        Stack<Node> stack = new Stack<Node>();
        stack.push(node);
        while(!stack.isEmpty()){
            Node rnode = stack.pop();
            System.out.print(rnode.getValue()+"  ");
            if(rnode.getLeft()!=null){
                stack.add(rnode.getLeft());
            }
            if(rnode.getRight()!=null){
                stack.add(rnode.getRight());
            }
        }
    }
}

3.深度優先和廣度優先遍歷詳細步驟

深度優先遍歷:
深度優先遍歷(Depth First Search),簡稱DFS,其原則是,沿着一條路徑一直找到最深的那個節點,當沒有子節點的時候,返回上一級節點,尋找其另外的子節點,繼續向下遍歷,沒有就向上返回一級,直到所有的節點都被遍歷到,每個節點只能訪問一次。

上圖中的深度優先遍歷結果是 {1,2,4,8,9,5,10,3,6,7 },遍歷過程是首先訪問1,然后是1的左節點2,然后是2的左節點4,再是4的左節點8,8沒有子節點了,返回遍歷8的父節點的4的另一個子節點9,9沒有節點,再向上返回。(注意:這里的返回上一次並不會去重新遍歷一遍)。

在算法實現的過程中,我們采用了棧(Stack)這種數據結構,它的特點是,最先壓入棧的數據最后取出。

算法步驟:

1、首先將根節點1壓入棧中【1】

2、將1節點彈出,找到1的兩個子節點3,2,首先壓入3節點,再壓入2節點(后壓入左節點的話,會先取出左節點,這樣就保證了先遍歷左節點),2節點再棧的頂部,最先出來【2,3】

3、彈出2節點,將2節點的兩個子節點5,4壓入【4,5,3】

4、彈出4節點,將4的子節點9,8壓入【8,9,5,3】

5,彈出8,8沒有子節點,不壓入【9,5,3】

6、彈出9,9沒有子節點,不壓入【5,3】

7、彈出5,5有一個節點,壓入10,【10,3】

8、彈出10,10沒有節點,不壓入【3】

9、彈出3,壓入3的子節點7,6【6,7】

10,彈出6,沒有子節點【7】

11、彈出7,沒有子節點,棧為空【】,算法結束

我們來看一看節點的出棧順序【1,2,4,8,9,5,10,3,6,7】,剛好就是我們深度遍歷的順序。下面看Java代碼

/**
     * 二叉樹的深度優先遍歷
     * @param root
     * @return
     */
    public List<Integer> dfs(Node root){
        Stack<Node> stack=new Stack<Node>();
        List<Integer> list=new LinkedList<Integer>();
        if(root==null)
            return list;
        //壓入根節點
        stack.push(root);
        //然后就循環取出和壓入節點,直到棧為空,結束循環
        while (!stack.isEmpty()){
            Node t=stack.pop();
            if(t.getRight()!=null)
                stack.push(t.getRight());
            if(t.getLeft()!=null)
                stack.push(t.getLeft());
            list.add(t.getValue());
        }
        return  list;
    }

廣度優先遍歷:
廣度優先遍歷(Breadth First Search),簡稱BFS;廣度優先遍歷的原則就是對每一層的節點依次訪問,一層訪問結束后,進入下一層,直到最后一個節點,同樣的,每個節點都只訪問一次。

上圖中,廣度優先遍歷的結果是【1,2,3,4,5,6,7,8,9,10】,遍歷順序就是從上到下,從左到右。

在算法實現過程中,我們使用的隊列(Queue)這種數據結構,這種結構的特點是先進先出,在java中Queue是一個借口,而LinkedList實現了這個接口的所有功能。

算法過程:

1、節點1,插入隊列【1】

2、取出節點1,插入1的子節點2,3 ,節點2在隊列的前端【2,3】

3、取出節點2,插入2的子節點4,5,節點3在隊列的最前端【3,4,5】

4、取出節點3,插入3的子節點6,7,節點4在隊列的最前端【4,5,6,7】

5、取出節點4,插入3的子節點8,9,節點5在隊列的最前端【5,6,7,8,9】

6、取出節點5,插入5的子節點10,節點6在隊列的最前端【6,7,8,9,10】

7、取出節點6,沒有子節點,不插入,節點7在隊列的最前端【7,8,9,10】

8、取出節點7,沒有子節點,不插入,節點8在隊列的最前端【8,9,10】

9、取出節點8,沒有子節點,不插入,節點9在隊列的最前端【9,10】

10、取出節點9,沒有子節點,不插入,節點10在隊列的最前端【10】

11、取出節點10,隊列為空,算法結束

我們看一下節點出隊的順序【1,2,3,4,5,6,7,8,9,10】,就是廣度優先遍歷的順序,下面看java代碼

/**
     * 二叉樹的廣度優先遍歷
     * @param root
     * @return
     */
    public List<Integer> bfs(Node root) {
        Queue<Node> queue = new LinkedList<Node>();
        List<Integer> list=new LinkedList<Integer>();
        if(root==null)
            return list;
        queue.add(root);
        while (!queue.isEmpty()){
            Node t=queue.remove();
            if(t.getLeft()!=null)
                queue.add(t.getLeft());
            if(t.getRight()!=null)
                queue.add(t.getRight());
            list.add(t.getValue());
        }
        return list;
    }

 

參考地址:

https://blog.csdn.net/weixin_42636552/article/details/82973190

https://blog.csdn.net/XTAOTWO/article/details/83625586

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM