java實現二叉樹的Node節點定義手撕8種遍歷(一遍過)

ElloeStudy發表於2022-04-09

java實現二叉樹的Node節點定義手撕8種遍歷(一遍過)

java的思想和程式從最基本的怎麼將一個int型的陣列變成Node樹狀結構說起,再到遞迴前序遍歷,遞迴中序遍歷,遞迴後序遍歷,非遞迴前序遍歷,非遞迴前序遍歷,非遞迴前序遍歷,到最後的廣度優先遍歷和深度優先遍歷

1.Node節點的Java實現

首先在可以看到打上Node這個字串,就可以看到只能的IDEA系統提供的好多提示:

image-20220409220958942

點進去看,卻不是可以直接構成二叉樹的Node,不是我們需要的東西。這裡舉個例子來看org.w3c.dom
這裡面的Node是一個介面,是解析XML時的文件樹。在官方文件裡面看出:
該 Node 介面是整個文件物件模型的主要資料型別。它表示該文件樹中的單個節點。
當實現 Node 介面的所有物件公開處理子節點的方法時,不是實現 Node 介面的所有物件都有子節點。
  • 所以我們需要自定義一個Node類
package com.elloe.實現二叉樹的Node節點.Node的Java實現;

import java.util.LinkedList;
import java.util.Stack;

/**
 * @author ElloeStudy(Sifa Zhang)
 * @create 2022-04-09 13:04
 * To: 真常應物,真常得性,常清常靜,常清靜矣
 *
 * 自定義Node的節點
 */
public class Node {
    private int value;    // 節點的值
    private 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;
    }

    @Override
    public String toString(){
        return this.value + " ";
    }
}

2.陣列昇華二叉樹

一般拿到的資料是一個int型的陣列,那怎麼將這個陣列變成我們可以直接操作的樹結構呢?

1、陣列元素變Node型別節點
2、給N/2-1個節點設定子節點
3、給最後一個節點設定子節點【有可能只有左節點】

那現在就直接上程式碼:

public static void create(int[] datas, List<Node> list){
  // 將陣列的數裝換為節點Node
  for (int i = 0; i < datas.length; i++) {
    Node node = new Node(datas[i]);
    node.setNode(node);
    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));
  }

  // 單獨處理最後一個節點,list.size()/2 -1 進行設定,避免單孩子情況
  int index = list.size()/2 - 1;
  list.get(index).setLeft(list.get(index * 2 + 1));
  if (list.size()%2 == 1){
    // 如果有奇數個節點,最後一個節點才有右節點
    list.get(index).setRight(list.get(index * 2 + 2));
  }
}

很細緻的加上了很多的註釋啊,所以保證一看就懂。

3.遞迴前序遍歷

具體的原理沒有什麼好講的,知道順序即可

先序遍歷過程:
(1)訪問根節點;
(2)採用先序遞迴遍歷左子樹;
(3)採用先序遞迴遍歷右子樹;

這裡用圖來說明:

image-20220409223224915

先序的結果:1 2 4 8 9 5 3 6 7

程式碼實現:

// 傳入需要遍歷的節點
public void preTraversal(Node node){
  // 當遇到葉節點,停止向下遍歷
  if (node == null){
    return;
  }
  // 相當於點前節點的根節點的值
  System.out.print(node.getValue() + " ");
  // 先從底下依次遍歷左節點
  preTraversal(node.getLeft());
  // 先從底下依次遍歷右節點
  preTraversal(node.getRight());
}

看,說了很簡單的!

4.遞迴中序遍歷

中序遍歷:
(1)採用中序遍歷左子樹;
(2)訪問根節點;
(3)採用中序遍歷右子樹

image-20220409223421778

中序的結果:8 4 9 2 5 1 6 3 7

程式碼實現:

// 中序遍歷(遞迴)
public void MidTraversal(Node node){
  // 判斷當前節點是否為葉子節點,如果為葉子節點,停止遍歷
  if (node == null){
    return;
  }
  // 獲得左節點
  MidTraversal(node.getLeft());
  // 獲得根節點
  System.out.print(node.getValue() + " ");
  // 獲得右節點
  MidTraversal(node.getRight());
}

5.遞迴後序遍歷

後序遍歷:
(1)採用後序遞迴遍歷左子樹;
(2)採用後序遞迴遍歷右子樹;
(3)訪問根節點;

image-20220409223810449

後序的結果:8 9 4 5 2 6 7 3 1

程式碼實現:

// 後序遍歷(遞迴)
public void afterTraversal(Node node){
  if (node == null){
    return;
  }
  afterTraversal(node.getLeft());
  afterTraversal(node.getRight());
  System.out.print(node.getValue() + " ");
}

其實程式碼和思想一樣,只是輸出的位置和遞迴呼叫的位置不同而已。

個人覺得懂得非遞迴的原理和程式碼比懂遞迴更有意思,當你能手撕非遞迴二叉樹遍歷的時候,

面試官問你原理,還能不知道嗎?

那接下來的三個模組就是非遞迴的三種遍歷

拭目以待

6.非遞迴前序遍歷

我這裡使用了棧這個資料結構,用來儲存不到遍歷過但是沒有遍歷完全的父節點
之後再進行回滾。

基本的原理就是當迴圈中的present不為空時,就讀取present的值,並不斷更新present為其左子節點,

即不斷讀取左子節點,直到一個枝節到達最後的子節點,再繼續返回上一層進行取值

程式碼:

// 非遞迴前序遍歷
public void  beforeTraversalByLoop(Node node){
  // 建立棧儲存遍歷的節點,但又沒有遍歷完全的節點(即這個節點還沒有操作完,臨時儲存一下)
  Stack<Node> stack = new Stack<>();
  Node present = node;  // 當前的節點
  while (present != null || !stack.isEmpty()){
    // 當前的節點不為null 且 棧不為空

    while (present != null){
      // 當 當前的節點不為null時,讀取present的值,
      // 並不斷更新present為其左子節點(不斷讀取左節點的值)

      // 讀取根節點
      System.out.print(present.getValue() + " ");
      stack.push(present); // 將present壓入棧(此時這個節點還沒有操作好,臨時儲存)
      present = present.getLeft(); // 讀取當前節點的左節點
    }

    if (!stack.isEmpty()){
      // 當棧不為空時
      present = stack.pop(); // 將臨時儲存的數取出
      present = present.getRight();  // 操作臨時儲存的節點的右節點(此時左節點已經全部讀取好了)
    }
  }
}

先序的結果:1 2 4 8 9 5 3 6 7

7.非遞迴中序遍歷

同原理

就是當迴圈中的present不為空時,就讀取present的值,並不斷更新present為其左子節點,

但是切記這個時候不能進行輸出,必須不斷讀取左子節點,直到一個枝節到達最後的子節點,

然後每次從棧中拿出一個元素,就進行輸出,再繼續返回上一層進行取值。

程式碼實現:

// 非遞迴中序遍歷
public void traversalMidByLoop(Node node) {
    // 建立棧儲存遍歷的節點,但又沒有遍歷完全的節點(即這個節點還沒有操作完,臨時儲存一下)
    Stack<Node> stack = new Stack<>();
    Node present = node; // 當前操作的節點
    while (present != null || !stack.isEmpty()) {
        // 當前的節點不為null 且 棧不為空

        // 獲取左節點
        while (present != null) {
            stack.push(present);// 將present壓入棧(此時這個節點還沒有操作好,臨時儲存)
            present = present.getLeft();// 讀取當前節點的左節點
        }

        if (!stack.isEmpty()) {
            present = stack.pop();
            // 獲取根節點
            System.out.print(present.getValue() + " ");
            present = present.getRight(); // 獲取右節點
        }
    }
}

8.非遞迴後序遍歷

後序遍歷相比前面的前序遍歷和中序遍歷在程式設計這裡會難一點,不過理解了思想,看程式碼還是沒有什麼問題的

程式碼實現:

// 非遞迴後序遍歷
public void traversalAfterByLoop(Node node){
    // 存放還沒有完成操作的節點,臨時儲存
    Stack<Node> stack = new Stack<>();
    Node present = node; // 當前的操作節點
    Node prev = node;  // 先前的根節點(一個標誌flag)

    while (present != null || !stack.isEmpty()){
        // 當前的節點不為null 且 棧不為空

        while(present != null){
            // 如果當前的節點不為空

            stack.push(present); // 將當前這個節點臨時儲存
            present = present.getLeft(); // 遍歷獲取其左節點
        }

        if (!stack.isEmpty()){
            // 拿出棧頂的值,並沒有進行刪除
            Node temp = stack.peek().getRight(); // 獲取棧頂節點的右節點

            // 節點沒有右節點或者到達根節點【考慮到了最後一種情況】
            if (temp == null || temp == prev){
                present = stack.pop();
                // 獲取根節點
                System.out.print(present.getValue() + " ");

                prev = present;   // 將當前的節點作為 根節點的標誌(flag)
                present = null;  // 將當前節點 設為空
            }else{
                // 節點有右節點 或者 沒有到達根節點
                present = temp; // 將這個右節點設定為當前節點
            }
        }
    }
}

最後就可以放大招了,來看看廣度優先遍歷和深度優先遍歷吧

9.廣度優先遍歷

在廣度優先遍歷裡面我用到了佇列,不明白的小夥伴可以看我的上一篇!

// 廣度優先遍歷
public void bfs(Node root){
    if (root == null) {
        return ;
    }

    LinkedList<Node> queue = new LinkedList<>();
    queue.offer(root); // 將根節點存入佇列

    //當佇列裡有值時,每次取出隊首的node列印,列印之後判斷node是否有子節點,
    // 若有,則將子節點加入佇列
    while (queue.size() > 0){
        Node node = queue.peek(); // 檢視佇列的頭部節點,不會刪除節點
        queue.poll(); // 取出(移除)對首的節點並列印
        System.out.print(node.getValue() + " ");
        if (node.getLeft() != null){
            // 如果有左節點,則將其存入佇列
            queue.offer(node.getLeft());
        }
        if (node.getRight() != null){
            // 如果有右節點,則將其存入對列
            queue.offer(node.getRight());
        }
    }
}

10.深度優先遍歷

在深度優先遍歷裡面我用到了棧,不明白的小夥伴可以看我的上一篇!

// 深度優先遍歷
public void dfs(Node root) {
    if (root == null){
        return;
    }

    Stack<Node> stack = new Stack<>();
    stack.push(root); // 將根節點壓入棧裡面

    while (!stack.isEmpty()){
        Node node = stack.pop(); // 彈出棧頂的節點
        System.out.print(node.getValue() + " ");

        // 深度優先遍歷,先遍歷左邊在右邊,所以先將右邊壓入再將左邊壓入
        if (node.getRight() != null){
            stack.push(node.getRight());
        }
        if (node.getLeft() != null){
            stack.push(node.getLeft());
        }
    }
}

11.測試用例(貼心吧?)

public static void main(String[] args) {
    int[] ints = new int[9];

    for (int i = 0; i < ints.length; i++) {
        ints[i] = i + 1;
    }
    List<Node> nodes = new ArrayList<>();
    // 陣列建立二叉樹
    BinaryFromArray.create(ints,nodes);
    for (Node node : nodes){
        System.out.print(node.getValue() + " ");
        System.out.print(node.getNode() + " ");
        System.out.print(node.getLeft() + " ");
        System.out.println(node.getRight());
    }

    // 先序遍歷(遞迴),從當前陣列的第一個node節點開始
    nodes.get(0).preTraversal(nodes.get(0));
    // 中序遍歷(遞迴),從當前陣列的第一個node節點開始
    System.out.print("\r\n"); // \r\n    換行
    nodes.get(0).MidTraversal(nodes.get(0));
    // 後序遍歷(遞迴),從當前陣列的第一個node節點開始
    System.out.print("\r\n"); // \r\n    換行
    nodes.get(0).afterTraversal(nodes.get(0));
}
public static void main(String[] args) {
    int[] ints = new int[9];

    for (int i = 0; i < ints.length; i++) {
        ints[i] = i + 1;
    }
    List<Node> nodes = new ArrayList<>();
    // 陣列建立二叉樹
    BinaryFromArray.create(ints,nodes);
    for (Node node : nodes){
        System.out.print(node.getValue() + " ");
        System.out.print(node.getNode() + " ");
        System.out.print(node.getLeft() + " ");
        System.out.println(node.getRight());
    }

    // 先序遍歷,從當前陣列的第一個node節點開始
   nodes.get(0).beforeTraversalByLoop(nodes.get(0));
    // 中序遍歷,從當前陣列的第一個node節點開始
    System.out.println(" ");
   nodes.get(0).traversalMidByLoop(nodes.get(0));
    // 後序遍歷,從當前陣列的第一個node節點開始
    System.out.println(" ");
    nodes.get(0).traversalAfterByLoop(nodes.get(0));
}
public static void main(String[] args) {
    int[] ints = new int[9];

    for (int i = 0; i < ints.length; i++) {
        ints[i] = i + 1;
    }
    List<Node> nodes = new ArrayList<>();
    // 陣列建立二叉樹
    BinaryFromArray.create(ints,nodes);
    for (Node node : nodes){
        System.out.print(node.getValue() + " ");
        System.out.print(node.getNode() + " ");
        System.out.print(node.getLeft() + " ");
        System.out.println(node.getRight());
    }

    // 廣度優先遍歷
   nodes.get(0).bfs(nodes.get(0));
    // 深度優先遍歷
    System.out.println();
    nodes.get(0).dfs(nodes.get(0));
}

12.全部程式碼(完整版)[前面成功的小夥伴可以直接跳過]

package com.elloe.實現二叉樹的Node節點.Node的Java實現;

import java.util.LinkedList;
import java.util.List;
import java.util.Stack;

/**
 * @author ElloeStudy(Sifa Zhang)
 * @create 2022-04-09 13:04
 * To: 真常應物,真常得性,常清常靜,常清靜矣
 *
 * 自定義Node的節點
 */
public class Node {
    private int value;    // 節點的值
    private 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;
    }

    @Override
    public String toString(){
        return this.value + " ";
    }

    // 構建二叉樹
    public static void create(int[] datas, List<Node> list){
        // 將陣列的數裝換為節點Node
        for (int i = 0; i < datas.length; i++) {
            Node node = new Node(datas[i]);
            node.setNode(node);
            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));
        }

        // 單獨處理最後一個節點,list.size()/2 -1 進行設定,避免單孩子情況
        int index = list.size()/2 - 1;
        list.get(index).setLeft(list.get(index * 2 + 1));
        if (list.size()%2 == 1){
            // 如果有奇數個節點,最後一個節點才有右節點
            list.get(index).setRight(list.get(index * 2 + 2));
        }
    }

    // 先序遍歷(遞迴)
    // 傳入需要遍歷的節點
    public void preTraversal(Node node){
        // 當遇到葉節點,停止向下遍歷
        if (node == null){
            return;
        }
        // 相當於點前節點的根節點的值
        System.out.print(node.getValue() + " ");
        // 先從底下依次遍歷左節點
        preTraversal(node.getLeft());
        // 先從底下依次遍歷右節點
        preTraversal(node.getRight());
    }


    // 中序遍歷(遞迴)
    public void MidTraversal(Node node){
        // 判斷當前節點是否為葉子節點,如果為葉子節點,停止遍歷
        if (node == null){
            return;
        }
        // 獲得左節點
        MidTraversal(node.getLeft());
        // 獲得根節點
        System.out.print(node.getValue() + " ");
        // 獲得右節點
        MidTraversal(node.getRight());
    }

    // 後序遍歷(遞迴)
    public void afterTraversal(Node node){
        if (node == null){
            return;
        }
        afterTraversal(node.getLeft());
        afterTraversal(node.getRight());
        System.out.print(node.getValue() + " ");
    }


    // 非遞迴前序遍歷
    public void  beforeTraversalByLoop(Node node){
        // 建立棧儲存遍歷的節點,但又沒有遍歷完全的節點(即這個節點還沒有操作完,臨時儲存一下)
        Stack<Node> stack = new Stack<>();
        Node present = node;  // 當前的節點
        while (present != null || !stack.isEmpty()){
            // 當前的節點不為null 且 棧不為空

            while (present != null){
                // 當 當前的節點不為null時,讀取present的值,
                // 並不斷更新present為其左子節點(不斷讀取左節點的值)

                // 讀取根節點
                System.out.print(present.getValue() + " ");
                stack.push(present); // 將present壓入棧(此時這個節點還沒有操作好,臨時儲存)
                present = present.getLeft(); // 讀取當前節點的左節點
            }

            if (!stack.isEmpty()){
                // 當棧不為空時
                present = stack.pop(); // 將臨時儲存的數取出
                present = present.getRight();  // 操作臨時儲存的節點的右節點(此時左節點已經全部讀取好了)
            }
        }
    }

    // 非遞迴中序遍歷
    public void traversalMidByLoop(Node node) {
        // 建立棧儲存遍歷的節點,但又沒有遍歷完全的節點(即這個節點還沒有操作完,臨時儲存一下)
        Stack<Node> stack = new Stack<>();
        Node present = node; // 當前操作的節點
        while (present != null || !stack.isEmpty()) {
            // 當前的節點不為null 且 棧不為空

            // 獲取左節點
            while (present != null) {
                stack.push(present);// 將present壓入棧(此時這個節點還沒有操作好,臨時儲存)
                present = present.getLeft();// 讀取當前節點的左節點
            }

            if (!stack.isEmpty()) {
                present = stack.pop();
                // 獲取根節點
                System.out.print(present.getValue() + " ");
                present = present.getRight(); // 獲取右節點
            }
        }
    }

    // 非遞迴後序遍歷
    public void traversalAfterByLoop(Node node){
        // 存放還沒有完成操作的節點,臨時儲存
        Stack<Node> stack = new Stack<>();
        Node present = node; // 當前的操作節點
        Node prev = node;  // 先前的根節點(一個標誌flag)

        while (present != null || !stack.isEmpty()){
            // 當前的節點不為null 且 棧不為空

            while(present != null){
                // 如果當前的節點不為空

                stack.push(present); // 將當前這個節點臨時儲存
                present = present.getLeft(); // 遍歷獲取其左節點
            }

            if (!stack.isEmpty()){
                // 拿出棧頂的值,並沒有進行刪除
                Node temp = stack.peek().getRight(); // 獲取棧頂節點的右節點

                // 節點沒有右節點或者到達根節點【考慮到了最後一種情況】
                if (temp == null || temp == prev){
                    present = stack.pop();
                    // 獲取根節點
                    System.out.print(present.getValue() + " ");

                    prev = present;   // 將當前的節點作為 根節點的標誌(flag)
                    present = null;  // 將當前節點 設為空
                }else{
                    // 節點有右節點 或者 沒有到達根節點
                    present = temp; // 將這個右節點設定為當前節點
                }
            }
        }
    }


    // 廣度優先遍歷
    public void bfs(Node root){
        if (root == null) {
            return ;
        }

        LinkedList<Node> queue = new LinkedList<>();
        queue.offer(root); // 將根節點存入佇列

        //當佇列裡有值時,每次取出隊首的node列印,列印之後判斷node是否有子節點,
        // 若有,則將子節點加入佇列
        while (queue.size() > 0){
            Node node = queue.peek(); // 檢視佇列的頭部節點,不會刪除節點
            queue.poll(); // 取出(移除)對首的節點並列印
            System.out.print(node.getValue() + " ");
            if (node.getLeft() != null){
                // 如果有左節點,則將其存入佇列
                queue.offer(node.getLeft());
            }
            if (node.getRight() != null){
                // 如果有右節點,則將其存入對列
                queue.offer(node.getRight());
            }
        }
    }


    // 深度優先遍歷
    public void dfs(Node root) {
        if (root == null){
            return;
        }

        Stack<Node> stack = new Stack<>();
        stack.push(root); // 將根節點壓入棧裡面

        while (!stack.isEmpty()){
            Node node = stack.pop(); // 彈出棧頂的節點
            System.out.print(node.getValue() + " ");

            // 深度優先遍歷,先遍歷左邊在右邊,所以先將右邊壓入再將左邊壓入
            if (node.getRight() != null){
                stack.push(node.getRight());
            }
            if (node.getLeft() != null){
                stack.push(node.getLeft());
            }
        }
    }
}

13.小結

相關文章