leetcode刷題需要經常用的二叉樹,發現二叉樹這種可以無限擴展知識點來虐別人的數據結構,很受面試官的青睞,這里記錄一下Java定義二叉樹和遍歷。
一、什么是二叉樹
1 .二叉樹的性質
本身是有序樹,樹中包含的各個節點的度不能超過 2,即只能是 0、1 或者 2

圖 1 二叉樹示意圖
二叉樹具有以下幾個性質:
- 二叉樹中,第 i 層最多有 2i-1 個結點。
- 如果二叉樹的深度為 K,那么此二叉樹最多有 2K-1 個結點。
- 二叉樹中,終端結點數(葉子結點數)為 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 滿二叉樹示意圖
滿二叉樹除了滿足普通二叉樹的性質,還具有以下性質:
- 滿二叉樹中第 i 層的節點數為 2n-1 個。
- 深度為 k 的滿二叉樹必有 2k-1 個節點 ,葉子數為 2k-1。
- 滿二叉樹中不存在度為 1 的節點,每一個分支點中都兩棵深度相同的子樹,且葉子節點都在最底層。
- 具有 n 個節點的滿二叉樹的深度為 log2(n+1)。
3 .完全二叉樹
如果二叉樹中除去最后一層節點為滿二叉樹,且最后一層的結點依次從左到右分布,則此二叉樹被稱為完全二叉樹
圖 3 完全二叉樹示意圖
如圖 3a) 所示是一棵完全二叉樹,圖 3b) 由於最后一層的節點沒有按照從左向右分布,因此只能算作是普通的二叉樹。
完全二叉樹除了具有普通二叉樹的性質,它自身也具有一些獨特的性質,比如說,n 個結點的完全二叉樹的深度為 ⌊log2n⌋+1。
⌊log2n⌋ 表示取小於 log2n 的最大整數。例如,⌊log24⌋ = 2,而 ⌊log25⌋ 結果也是 2。
對於任意一個完全二叉樹來說,如果將含有的結點按照層次從左到右依次標號(如圖 3a)),對於任意一個結點 i ,完全二叉樹還有以下幾個結論成立:
- 當 i>1 時,父親結點為結點 [i/2] 。(i=1 時,表示的是根結點,無父親結點)
- 如果 2*i>n(總結點的個數) ,則結點 i 肯定沒有左孩子(為葉子結點);否則其左孩子是結點 2*i 。
- 如果 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 二叉樹節點結構
圖 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
