一丶二叉樹的遍歷
1.二叉樹遍歷遞迴寫法與遞迴序
瞭解過二叉樹的朋友,最開始肯定是從二叉樹的遍歷開始的,二叉樹遍歷的遞迴寫法想必大家都有所瞭解。
public static void process(TreeNode node) {
if (node == null) {
return;
}
//如果在這裡列印 代表前序遍歷 ----位置1
process(node.left);
//如果在這裡列印中序遍歷 ----位置2
process(node.right);
//如果在這裡列印 後序遍歷 ---位置3
}
process函式在不同的位置進行列印,就實現了不同的遍歷順序。
我們這裡引入一個概念遞迴序
—— 遞迴函式到達節點的順序
process函式的遞迴序列是什麼呢
- 首先
process(1)
此時方法棧記為A,遍歷節點1(可以理解為A棧的位置1) - 然後
process(1.left)
再開闢一個棧記為B 來到2(可以理解為B棧的位置1) - 接著
process(2.left)
為空 出棧 相當於來到了B棧的位置2 ,再次來到2 - 接著
process(2.right)
為空,出棧,來到B棧位置3,再次來到2 - 接著出棧,來到A棧位置2
- 然後
process(1.right)
再開闢一個棧記為C 來到3(可以理解為C棧的位置1) - 接著
process(3.left)
為空 出棧 相當於來到了C棧的位置2 ,再次來到3 - 接著
process(3.right)
為空,出棧,來到C棧位置3,再次來到3 - 最後出棧,來到A棧的位置3,來到1
遞迴序為 1,2,2,2,1,3,3,3,1
。可以看到每一個節點都將訪問3次。
-
第一次訪問的時候列印
1,2,3
——先序遍歷 -
第二次訪問的時候列印
2,1,3
——中序遍歷 -
第三次訪問的時候列印
2,3,1
——後序遍歷
2.二叉樹遍歷非遞迴寫法
下面講解的二叉樹遍歷非遞迴寫法,都針對下面這棵樹
2.1 先序遍歷
遞迴寫法告訴我們,列印結果應該是1,2,4,5,3
。
對於節點2,我們需要先列印2,然後處理4,然後處理5。棧先進後出,如果我們入棧順序是4,5 那麼會先列印5然後列印4,將無法實現先序遍歷,所有我們需要先入5後入4。
- 當前列印的節點記憶為cur
- 列印
- cur的右節點(如果存在)入棧,然後左節點(如果存在)入棧
- 彈出棧頂進行處理,迴圈往復
程式如下
public static void process1(TreeNode root) {
if (root == null) {
return;
}
Stack<TreeNode> stackMemory = new Stack<>();
stackMemory.push(root);
while (!stackMemory.isEmpty()) {
TreeNode temp = stackMemory.pop();
System.out.println(temp.val);
if (temp.right != null) {
stackMemory.push(temp.right);
}
if (temp.left != null) {
stackMemory.push(temp.left);
}
}
}
2.2 中序遍歷
-
將樹的左邊界放入棧中
這時候棧中的內容是
(棧底)1->2->4(棧頂)
-
然後彈出節點cur進行列印
也就是列印4,如果cur具備右子樹,那麼將右子樹的進行步驟一
-
迴圈往復直到棧為空
為什麼這可以實現左->中->右
列印的中序遍歷
首先假如當前節點是A,那麼列印A的前提是,左子樹列印完畢,在列印A的左子樹的時候,我們會把A左子節點的右樹入棧,這一保證了列印A之前,A的左子樹被處理完畢,然後列印A
列印完A,如果A具備右子樹,右子樹會入棧,然後彈出,保證了列印完A後列印其右子樹,從而實現左->中->右
列印的中序遍歷
public static void process2(TreeNode root) {
if (root == null) {
return;
}
Stack<TreeNode> stackMemory = new Stack<>();
do {
//首先左子樹入棧
//1
while (root!=null){
stackMemory.push(root);
root = root.left;
}
//來到這兒,說明左子樹都入棧了
//彈出
if (!stackMemory.isEmpty()){
root = stackMemory.pop();
System.out.println(root.val);
//賦值為右子樹,右子樹會到1的程式碼位置,如果右子樹,那麼右子樹會進行列印
root = root.right;
}
}while (!stackMemory.isEmpty()||root!=null);
}
2.3 後序遍歷
後續遍歷就是左->右->頭
的順序,那麼只要我以頭->左->右
的順序將節點放入收集棧中,最後從收集棧中彈出的順序,就是左->右->頭
public static void process3(TreeNode r) {
if (r == null) {
return;
}
//輔助棧
Stack<TreeNode> help = new Stack<>();
//收集棧
Stack<TreeNode> collect = new Stack<>();
help.push(r);
while (!help.isEmpty()) {
TreeNode temp = help.pop();
collect.push(temp);
if (temp.left != null) {
help.push(temp.left);
}
if (temp.right != null) {
help.push(temp.right);
}
}
StringBuilder sb = new StringBuilder();
while (!collect.isEmpty()) {
sb.append(collect.pop().val).append(",");
}
System.out.println(sb);
}
3.二叉樹寬度優先遍歷
給你二叉樹的根節點 root
,返回其節點值的 層序遍歷 (也是寬度優先遍歷)即逐層地,從左到右訪問所有節點)。
此樹寬度優先遍歷——[3],[9,20],[15,7]
寬度優先遍歷可以使用佇列實現,最開始將佇列的頭放入到佇列中,然後當佇列不為空的時候,拿出佇列頭cur,加入到結果集合中,然後如果當前cur的左兒子,右兒子中不為null的節點放入到佇列中,迴圈往復
下面以LeetCode102為例子
public List<List<Integer>> levelOrder(TreeNode root) {
//結果集合
List<List<Integer>> res = new ArrayList<>();
if (root == null) {
return res;
}
//佇列
LinkedList<TreeNode> queue = new LinkedList<>();
queue.addLast(root);
//當前層的節點數量為1
int curLevelNum = 1;
while (!queue.isEmpty()) {
//儲存當前層節點的值
List<Integer> curLevelNodeValList = new ArrayList<>(curLevelNum);
//下一層節點的數量
int nextLevelNodeNum = 0;
//遍歷當前層
while (curLevelNum > 0) {
TreeNode temp = queue.removeFirst();
curLevelNodeValList.add(temp.val);
//處理左右兒子,只要不為null 那麼加入並且下一次節點數量加1
if (temp.left != null) {
queue.addLast(temp.left);
nextLevelNodeNum++;
}
if (temp.right != null) {
queue.addLast(temp.right);
nextLevelNodeNum++;
}
//當前層減少
curLevelNum--;
}
//當前層結束了,到下一層
curLevelNum = nextLevelNodeNum;
//儲存結果
res.add(curLevelNodeValList);
}
return res;
}
二丶樹型DP
1.從一道題開始——判斷一顆二叉樹是否是搜尋二叉樹
1.1 中序遍歷解題
可以斷定我們可以使用中序遍歷,然後在中序遍歷的途中判斷節點的值是滿足升序即可
-
遞迴中序遍歷判斷是否二叉搜尋樹
public boolean isValidBST(TreeNode root) { if (root == null) { return true; } //第二個引數記錄之前遍歷遇到節點的最大值 //由於TreeNode 可能節點值為int 最小使用Long最小 return check(root, new AtomicLong(Long.MIN_VALUE)); } private boolean check(TreeNode node, AtomicLong preValue) { if (node == null) { return true; } //左樹是否二叉搜尋樹 boolean isLeftBST = check(node.left, preValue); //左樹不是 那麼返回false if (!isLeftBST) { return false; } //當前節點的值 大於之前遇到的最大值 那麼更改preValue if (node.val > preValue.get()) { preValue.set(node.val); } else { //不滿足升序那麼false return false; } //檢查右樹 return check(node.right, preValue); }
-
非遞迴中序遍歷判斷是否二叉搜尋樹
private boolean check(TreeNode root) { if (root == null) { return true; } //前面節點最大值,最開始為null Integer pre = null; Stack<TreeNode> stack = new Stack<>(); do { while (root != null) { stack.push(root); root = root.left; } if (!stack.isEmpty()) { root = stack.pop(); //滿足升序那麼更新pre if (pre == null || pre < root.val) { pre = root.val; } else { return false; } root = root.right; } } while (!stack.isEmpty() || root != null); return true; }
1.2 引入 —— 樹形DP
如果當前位於root節點,我們可以獲取root左子樹的一些"資訊"
,root右子樹的一些資訊,我們們要如何判斷root為根的樹是否是二叉搜尋樹:
-
root左子樹,右子樹必須都是二叉搜尋樹
-
root的值必須大於
左子樹最大
,必須小於右子樹最小
-
根據1和2 我們可以得到
"資訊"
的結構static class Info { //當前子樹的最小值 Integer min; //當前子樹最大值 Integer max; //當前子樹是否是二叉搜尋樹 boolean isBst; Info(Integer min, Integer max, boolean flag) { this.min = min; this.max = max; this.isBst = flag; } }
接下來的問題是,有了左右子樹的資訊,如何拼湊root自己的資訊?如果不滿足二叉搜尋樹的要求那麼返回isBst為false,否則需要返回root這棵樹的最大,最小——這些資訊可以根據左子樹和右子樹的資訊構造而來。程式碼如下
private Info process(TreeNode node) {
//如果當前節點為null 那麼返回null
//為null 表示是空樹
if (node == null) {
return null;
}
//預設現在是二叉搜尋樹
boolean isBst = true;
//左樹最大,右樹最小 二者是否bst ,從左右子樹拿資訊
Info leftInfo = process(node.left);
Info rightInfo = process(node.right);
//左樹不為null 那麼 維護isBst識別符號
if (leftInfo != null) {
isBst = leftInfo.isBst;
}
//右樹不為null 那麼 維護isBst識別符號
if (rightInfo != null) {
isBst = isBst && rightInfo.isBst;
}
//如果左數 或者右樹 不為二叉搜尋樹 那麼返回
if (!isBst){
return new Info(null,null,isBst);
}
//左右是bst,那麼看是否滿足二叉搜尋樹的條件
//左邊最大 是否小於當前節點
if (leftInfo!=null && leftInfo.max >= node.val){
isBst = false;
}
//右邊最小 是否小於當前節點
if (rightInfo!=null && rightInfo.min <= node.val){
isBst = false;
}
//如果不滿足 那麼返回
if (!isBst){
return new Info(null,null,isBst);
}
//說明node為根的樹是bst
//那麼根據左右子樹的資訊返回node這課樹的資訊
Integer min = node.val;
Integer max = node.val;
if (leftInfo!=null){
min = leftInfo.min;
}
if (rightInfo!=null){
max = rightInfo.max;
}
return new Info(min, max, true);
}
2. 樹型DP題目套路
之所以稱之為樹型DP,是因為這個套路用於解決 樹的問題。那麼為什麼叫DP,這是由於node節點的資訊,來自左右子樹的資訊,類似於動態規劃中的狀態轉移。
2.1樹型DP可以解決什麼問題
怎麼理解:
對於1中判斷是否二叉搜尋樹的問題,S規則就是以node為根的這棵樹是否是二叉搜尋樹
最終整棵樹是否二叉搜尋樹,是依賴於樹中所有節點的——"最終答案一定在其中"
2.2 解題模板
3.題目練習
3.1 二叉樹的最大深度
需要的資訊只有樹的高度,我們可以向左子樹獲取,高度然後獲取右子樹的高度,然後二叉高度取max加上1就是當前節點為根的樹的高度
public int maxDepth(TreeNode root) {
if(root == null){
return 0;
}
int leftH = maxDepth(root.left);
int rightH = maxDepth(root.right);
return Math.max(leftH,rightH)+1;
}
3.2 判斷一顆樹是否二叉平衡樹
- 需要什麼資訊:左右樹的高度,左右樹是否是平衡的
- 怎麼根據左右構造當前樹的資訊:當前高度=max(左右高度)+1 ,當前是否平衡=左平衡右平衡且二者高度差不大於1
/***
* 是否是平衡二叉樹
* @return
*/
public static boolean isAVL(TreeNode root) {
return process(root).getKey();
}
public static Pair<Boolean, Integer> process(TreeNode root) {
//當前節點為null 那麼是平衡二叉樹
if (root == null) {
return new Pair<>(true, 0);
}
//右樹
Pair<Boolean, Integer> rightData = process(root.right);
//左樹
Pair<Boolean, Integer> leftData = process(root.left);
//右樹是否是平衡
boolean rTreeIsAVL = rightData.getKey();
//右樹高度
int rHigh = rightData.getValue();
//左樹是否平衡
boolean lTreeIsAVL = leftData.getKey();
//左樹高度
int lHigh = rightData.getValue();
//當前樹是平衡要求:左樹平衡 右樹平衡 且二者高度差小於1
boolean thisNodeIsAvl = rTreeIsAVL
&& lTreeIsAVL
&& Math.abs(rHigh - lHigh) < 2;
//返回當前樹的結果 高度樹是左右高度最大+1
return new Pair<>(thisNodeIsAvl, Math.max(rHigh, lHigh) + 1);
}
3.3 判斷一棵樹是否滿二叉樹
滿二叉樹 樹的高度h和樹節點數目n具備 n = 2的h次方 -1 的特性
- 需要左右樹的高度,左右樹的節點個數
- 怎麼根據左右構造當前樹的資訊:當前高度=max(左高,右高)+1,當前節點個數=左個數+右個數+1
public static boolean isFullTree(TreeNode root) {
Pair<Integer, Integer> rootRes = process(root);
int height = rootRes.getKey();
int nodeNums = rootRes.getValue();
return nodeNums == Math.pow(2, height)-1;
}
//key 高度 v 節點個數
public static Pair<Integer, Integer> process(TreeNode node) {
if (node == null) {
return new Pair<>(0, 0);
}
Pair<Integer, Integer> rInfo = process(node.right);
Pair<Integer, Integer> lInfo = process(node.left);
int thisNodeHeight = Math.max(rInfo.getKey(), lInfo.getKey()) + 1;
int thisNodeNum = rInfo.getValue() + lInfo.getValue() + 1;
return new Pair<>(thisNodeHeight, thisNodeNum);
}