java實現二叉樹的Node節點定義手撕8種遍歷(一遍過)
用java的思想和程式從最基本的怎麼將一個int型的陣列變成Node樹狀結構說起,再到遞迴前序遍歷,遞迴中序遍歷,遞迴後序遍歷,非遞迴前序遍歷,非遞迴前序遍歷,非遞迴前序遍歷,到最後的廣度優先遍歷和深度優先遍歷
1.Node節點的Java實現
首先在可以看到打上Node這個字串,就可以看到只能的IDEA系統提供的好多提示:
點進去看,卻不是可以直接構成二叉樹的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)採用先序遞迴遍歷右子樹;
這裡用圖來說明:
先序的結果: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)採用中序遍歷右子樹
中序的結果: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)訪問根節點;
後序的結果: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.小結
- 以上的程式碼,全部為我自己成功實現的coding,希望看到這裡的你,已經完全理清二叉樹的遍歷?
- 繼續努力!!!
- 參考文章:https://blog.csdn.net/weixin_42636552/article/details/82973190