二叉樹
1.基本概念
二叉樹是每個節點最多有兩個子樹的樹結構,度可能是0,1,2;
完成二叉樹:從左到右依次填滿;
滿二叉樹:除了葉子節點,所有節點都有兩個孩子,並且所有葉子節點在同一層;
2.性質
1.完全二叉樹除了最后一層外,下一層節點個數是上一層兩倍,
如果一顆完全二叉樹的節點總數是n,那么葉子節點個數為n/2(n為偶數)或(n+1)/2(n為奇數);
3.遞歸在二叉樹中的應用
寫遞歸算法的關鍵就是明確函數的定義是什么,然后相信這個定義,利用這個定義推道出最終結果,絕不要跳入遞歸的細節 (人的小腦袋才能堆幾層棧)
寫樹相關的算法,簡單的就是說,去想象一個最小單元,搞清楚當前最小單元中t節點“該做什么,什么時候做”這兩點,然后根據函數定義遞歸調用子節點。
- 該做什么:就是我們的最小單元中的root節點(可能不止一個)想要實現功能,需要得到什么信息,然后能做什么;
- 做點什么能夠提供信息給下面的子樹利用(先序)
- 能從下面的子樹上獲得什么信息然后利用(后序)
- 什么時候做:剛才寫的代碼應該放在前序、中序還是后序的代碼位置上。
把題目的要求細化,搞清楚根節點應該做什么,然后剩下的事情拋給前/中/后序的遍歷框架就行了,難點在於如何通過題目的要求思考出每一個節點需要做什么。
寫完之后自己代入一個最基本的看能不能實現功能,會不會出不來,檢驗一下。
4.遍歷
4.1 概念
- 前序遍歷:根節點 -> 左子樹 -> 右子樹;
- 中序遍歷:左子樹 -> 根節點 -> 右子樹;
- 后序遍歷:左子樹 -> 右子樹 -> 根節點;
4.2 遞歸實現
遞歸的時候不要太在意實現的細節,其本質上是通過棧來實現的,每次在方法里自己調自己,就把新調用的自己壓棧,是一個有去有回的過程。
對於遞歸,關鍵就是要清楚函數的功能,什么時候停下來。
對於前序遍歷,想先打印根節點,再左再右;那就先輸出,再去遞歸調用,傳入當前節點的左子樹,左子樹同樣打印它的根,傳入根的左子樹,知道整個左子樹處理完了,再去處理右子樹;
class BinaryTreeTraverse<T>{
/**
* 前序遍歷(遞歸實現);
*/
public static void preOrderByRecursion(TreeNode node){
if (node == null) return; //遞歸終止條件;
System.out.println(node.value); //獲取根節點的值;
preOrderByRecursion(node.left); //左子樹的根節點;
preOrderByRecursion(node.right);//右子樹的根節點;
}
/**
* 中序遍歷(遞歸實現)
*/
public static void inOrderByRecursion(TreeNode node){
if (node == null) return; //遞歸終止;
inOrderByRecursion(node.left); //先左子樹;
System.out.println(node.value); //再根節點;
inOrderByRecursion(node.right); //再右節點;
}
/**
* 后序遍歷(遞歸實現)
*/
public static void postOrderByRecursion(TreeNode node){
if (node == null) return;
postOrderByRecursion(node.left);
postOrderByRecursion(node.right);
System.out.println(node.value);
}
時間復雜度:0(N),每個節點遍歷N次;
空間復雜度:O(N),遞歸過程中棧的開銷;
4.3 迭代實現
前序遍歷
前序遍歷就是我們來手動實現在遞歸過程中的棧。
想要實現先左再右,那壓棧的時候右先入棧,左再入棧。
如下圖所示;
規則:
壓入根節點;
1.彈出就打印;
2.如有右孩子,壓入右;
3.如有左孩子,壓入左;
重復1.2.3
思考一下這個過程;其實就是相當於兩個孩子來替換棧里的根節點,這就很符合前序的定義,根先走,左子樹干到了最頂部,要是我左子樹還有左孩子,那我也走,兩個孩子來替我,每次都是我先走,我左孩子這邊整個都完事了,再來我右孩子,因為我右孩子被壓在最下面。
public static void preOrder(TreeNode node){
Stack<TreeNode> stack = new Stack<>();
if (node != null){
stack.push(node); //根節點入棧;
}
while (!stack.isEmpty()){
TreeNode top = stack.pop(); //彈出就打印;
System.out.println(top.value);
if (top.right != null) stack.push(top.right); //依次入棧右節點和左節點;
if (top.left != null) stack.push(top.left);
}
}
中序遍歷
規則:
1.整條左邊界依次入棧;
2.條件1執行不了了,彈出就打印;
3.來到彈出節點的右子樹上,繼續執行條件1;(右樹為空,執行2.彈出就打印;右樹有節點,執行1.壓棧;
說明:將整個樹全用左邊界去看,都是先處理了左邊界,將左邊界分解成了左頭,頭先入,左再入,然后弄不動了,彈出,然后看其右節點,再把右節點里的左依次進去;就這樣往返;
如下如所示;
public static void inOrder(TreeNode node){
Stack<TreeNode> stack = new Stack<>();
while (!stack.isEmpty() || node != null){
if (node != null){ //條件1;能往左走就往左走;
stack.push(node);
node = node.left;
}else {
TreeNode top = stack.pop(); //條件2;彈出打印;
System.out.println(top.value);
node = top.right; //條件3;來彈出節點右樹上,繼續1;
}
}
}
后序遍歷
后序遍歷可以用前序遍歷來解決,想一下前序遍歷:根左右,我們先壓右樹再壓左樹。怎么實現根右左呢,可以先壓左樹再壓右樹嘛,然后反過來不就是左右根了嗎?(反過來用棧來實現,棧一個很大的作用就是實現逆序)
public static void postOrder(TreeNode node){
Stack<TreeNode> stackA = new Stack<>();
Stack<TreeNode> stackB = new Stack<>();
if (node != null){
stackA.push(node);
}
while (!stackA.isEmpty()){
TreeNode top = stackA.pop();
stackB.push(top); //棧A彈出的進入棧B;先實現根右左,B倒序實現左右根;
if (top.left != null) stackA.push(top.left);
if (top.right != null) stackA.push(top.right);
}
while (!stackB.isEmpty()){
System.out.println(stackB.pop().value);
}
}
4.4 Morris實現
二叉樹的結構中只有父節點指向孩子節點,孩子節點不能向上指,所以需要棧。
而Morris遍歷的實質就是讓下層節點也能指向上層,怎么辦呢,一個節點有兩個指針,左和右,如果這兩個指針都有指向具體節點了那指定用不上了。
但是二叉樹可是有很多空閑指針啊,比如說所有的葉子節點,它們的指針就都指向null,所以可以利用其right指針指向上層。
這樣把下層往上層建立連接以后,cur指針就可以完整的順着一個鏈條遍歷完整個樹。
因為不用堆棧,所以其空間復雜度變為O(1);
如下圖所示:
cur指針走的順序:1 2 4 2 5 1 3 6 3 7;
核心: 以某個根節點開始,找到其左子樹的最右節點(必然是個葉子節點),然后利用其right指針指向根節點(建立從下到上的連接)
- 到達兩次的是有左子樹的節點;
- 到達一次的是沒有左子樹的節點;
原則:
- 1.如果cur無左孩子,cur向右移動(cur=cur.right)【圖中有4到2】
- 2.如果cur有左孩子,找到cur左子樹上最右的節點,記為mostRightNode;
- 1.如果mostRightNode的right指針指向空,讓其指向cur,cur向左移動(cur=cur.left)
- 2.如果mostRightNode的right指針指向cur,讓其指向空,cur向右移動(cur=cur.right)【圖中由2到5】
(迭代法的中序遍歷我們可以將整個樹全部分成左邊界去看,如上面的圖,其實在morris遍歷里我們可以將整個樹全部分成右邊界來看)
public static void morris(TreeNode node){
if(node == null){
return;
}
TreeNode cur = node;
TreeNode mostRightNode = null; //記錄cur左子樹的最右節點;
while(cur != null){
mostRightNode = cur.left;
if(mostRightNode != null){ //cur有左子樹,就證明有下一層;
//找到cur左子樹的最右節點(找到cur下一層右邊界的最后一個)
while(mostRight.right != null && mostRightNode != cur){
//1.最右節點為空說明到頭了,找到了;
//2.最右節點指向上層說明已經處理過了,來過了;
mostRightNode = mostRightNode.right;
}
//走到這里證明跳出上面循環,無非兩個原因:
//1.右邊沒了;2.右邊指向上層了(之前就處理過了);
if(mostRightNode.right == null){
mostRightNode.right = cur; //建立從最右節點到cur的連接;
cur = cur.left; //處理下一個節點;
continue;
}else{
//能到這里說明已經建立了最右節點到cur的連接;
//也說明cur指的這個節點是第二次到了,斷開連接;
mostRightNode.right = null;
}
}
//cur右移的情況:
//1.cur沒有左子樹了(自然要開始處理右子樹)
//2.cur有左子樹,但是cur左子樹最右節點已經指向cur了(執行完上面else斷開后,cur左邊已經完全處理好了,開始右移。)
cur = cur.right;
}
}
前序遍歷
1.對於cur只達到一次的節點(沒有左子樹),cur達到就打印;
2.對於cur到達兩次的節點(有左子樹),到達第一次時打印;
public static void preOrderMorris(TreeNode node){
if (node == null){
return;
}
TreeNode cur = node;
TreeNode mostRightNode = null;
while (cur != null){
mostRightNode = cur.left;
if (mostRightNode != null){ //到達兩次的節點;
//找到cur左子樹的最右節點;
while (mostRightNode.right != null && mostRightNode.right != cur){
mostRightNode = mostRightNode.right;
}
if (mostRightNode.right == null){
mostRightNode.right = cur; //指向上層cur;
System.out.println(cur.value); //第一次到的時候打印;
cur = cur.left;
continue;
}else{
mostRightNode.right = null; //第二次到時不打印;
}
}else {
System.out.println(cur.value); //只到達一次的節點;
}
cur = cur.right;
}
}
中序遍歷
1.對於cur只達到一次的節點(沒有左子樹),cur達到就打印;
2.對於cur到達兩次的節點(有左子樹),到達第二次時打印;
public static void inOrderMorris(TreeNode node){
if (node == null){
return;
}
TreeNode cur = node;
TreeNode mostRightNode = null;
while (cur != null){
mostRightNode = cur.left;
if (mostRightNode != null){ //有左子樹,到達兩次的節點;
while (mostRightNode.right != null && mostRightNode.right != cur){
mostRightNode = mostRightNode.right;
}
if (mostRightNode == null){
mostRightNode.right = cur; //第一次不打印;
cur = cur.left;
continue;
}else {
System.out.println(cur.value); //第二次遇到時打印;
mostRightNode.right = null;
}
}else {
System.out.println(cur.value); //只到達一次的節點,遇到就打印;
}
cur = cur.right;
}
}
后序遍歷
后序遍歷比前面兩個要復雜一點;
將一個節點的連續右節點當成是一個單鏈表看,如下圖所示:
當我們到達最左側,也就是左邊連線已經創建完畢了。
打印 4
打印 5 2
打印 6
打印 7 3 1
我們將一個節點的連續右節點當成一個單鏈表來看待。
當我們返回上層之后,也就是將連線斷開的時候,打印下層的單鏈表。
比如返回到 2,此時打印 4
比如返回到 1,此時打印 5 2
比如返回到 3,此時打印 6
最后別忘記頭節點那一串,即1 3 7
那么我們只需要將這個單鏈表逆序打印就行了。
這里不應該打印當前層,而是下一層,否則根結點會先與右邊打印。
public static void postOrderMorris(TreeNode node){
if (node == null){
return;
}
TreeNode cur = node;
TreeNode mostRightNode = null;
while (cur != null){
mostRightNode = cur.left;
if (mostRightNode != null){
while (mostRightNode.right != null && mostRightNode.right != cur){
mostRightNode = mostRightNode.right;
}
if (mostRightNode.right == null){
mostRightNode.right = cur;
cur = cur.left;
continue;
}else { //能到這里的都是達到過兩次的,也就是是有左孩子的。
mostRightNode.right = null; //這時候是已經返回上層之后,斷開了連接,所以打印下層的單鏈表;
postMorrisPrint(cur.left);
}
}
cur = cur.right;
}
postMorrisPrint(node); //最后把頭節點那一串右打印一遍;
}
public static void postMorrisPrint(TreeNode node){
TreeNode reverseList = postMorrisReverseList(node); //反轉鏈表;
TreeNode cur = reverseList;
while (cur != null){
System.out.println(cur.value);
cur = cur.right;
}
postMorrisReverseList(reverseList); //最后再還原;
}
public static TreeNode postMorrisReverseList(TreeNode node){
TreeNode cur = node;
TreeNode pre = null;
while (cur != null){
TreeNode next = cur.right;
cur.right = pre;
pre = cur;
cur = next;
}
return pre;
}
4.5 層次遍歷
層次遍歷顧名思義就是一層一層的遍歷。從上到下,從左到右,那需要借助什么結構呢?
可以采用隊列的結構,利用其先進先出的特性,每一層依次入隊,再依次出隊。
對該層節點進行出隊時,將這個節點的左右節點入隊,這樣當一層所有節點出隊完成后,下一層也入隊完成了。
/**
* 層次遍歷
* 借助隊列的結構,每一層依次入隊,再依次出隊;
* 對該層節點進行出隊操作時,需要將該節點的左孩子和右孩子入隊;
*/
public static int layerOrder(TreeNode node){
if (node == null) return 0;
Queue<TreeNode> queue = new LinkedList<>();
queue.add(node);
while (!queue.isEmpty()){
int size = queue.size(); //當前層的節點數量;
for (int i = 0; i < size; i++){
TreeNode front = queue.poll();
System.out.println(front.value);
if (front.left != null) queue.add(front.left);
if (front.right != null) queue.add(front.right);
}
}
}
5.深度
二叉樹的最大深度是根節點到最遠葉子結點的距離;
5.1 最大深度
遞歸實現
1.終止條件:在二叉樹為空的時候,深度為1;
2.縮小范圍,等價關系:給定一個二叉樹,其深度為左子樹的深度和右子樹的深度的最大值+1;
/**
* 求最大深度(遞歸)
* 最大深度是左子樹和右子樹的最大深度的大的那個+1;
*/
public static int maxDepthByRecursion(TreeNode node){
if(node == null) return 0;
int leftDepth = maxDepthByRecursion(node.left);
int rightDepth = maxDepthByRecursion(node.right);
return Math.max(leftDepth,rightDepth)+1;
}
非遞歸實現(層次遍歷)
關鍵點:每遍歷一層,則計數器加+1;直到遍歷完成,得到樹的深度。
采用二叉樹的層次遍歷,來計數總共有多少層,采用隊列的結構,當前層節點出隊,計數器加1,然后把下一層的節點全部入隊,直到隊為空。
/**
* 求最大深度(非遞歸)
* 層次遍歷(BFS)
* 每遍歷一層,則計數器加+1;直到遍歷完成,得到樹的深度。
*/
public static int maxDepth(TreeNode node){
if (node == null) return 0;
Queue<TreeNode> queue = new LinkedList<>();
int level = 0; //層數;
queue.add(node);
while (!queue.isEmpty()){
level++;
int levelNum = queue.size(); //每層的節點數;
for (int i = 0; i < levelNum; i++){
TreeNode front = queue.poll(); //當前層出隊,下一層入隊;
if (front.left != null) queue.add(front.left);
if (front.right != null) queue.add(front.right);
}
}
return level;
}
5.2 最小深度
二叉樹的深度是根節點到最近葉子節點的距離;
遞歸實現
此題不能像最大深度那樣直接求兩顆子樹的最大然后+1,最大深度可以是因為取大值不會影響一棵樹為空的時候。但是取最小就不一樣了,如果一棵樹為空,那最小的應該是不為空的那邊的值,但是還按原來方式就變成了0+1;比如下面這個例子:最小深度應該我2.但是按原來方式寫的話最小深度就會變為1.所以,在處理每一個節點的時候,如果有兩個孩子,那就可以繼續取小+1,如果只有一個孩子,那就只能去遞歸它的孩子。
/**
* 求最小深度(遞歸)
* 注意和求最大深度的區別;
*/
public static int minDepthByRecursion(TreeNode node){
if (node == null) return 0;
if (node.right == null && node.left == null) return 1;
if (node.left == null && node.right != null) return minDepthByRecursion(node.right) + 1;
if (node.right == null && node.left != null) return minDepthByRecursion(node.left) + 1;
return Math.min(minDepthByRecursion(node.left), minDepthByRecursion(node.right))+1;
}
非遞歸實現(層次遍歷)
關鍵點:每遍歷一層,則計數器加+1;在遍歷的過程中,如果出現了沒有葉子節點的節點,那就可以結束了,就是最小深度。
采用二叉樹的層次遍歷,來計數總共有多少層,采用隊列的結構,當前層節點出隊,計數器加1,然后把下一層的節點全部入隊,直到遇到葉子節點或隊為空。
/**
* 求最小深度(非遞歸)
*/
public static int minDepth(TreeNode node){
if (node == null) return 0;
Queue<TreeNode> queue = new LinkedList<>();
int level = 0;
queue.add(node);
while (!queue.isEmpty()){
level++;
int levelnum = queue.size();
for(int i = 0; i < levelnum; i++){
TreeNode front = queue.poll();
if (front.left == null && front.right == null){
return level; //遇到第一個無葉子節點的時候,該節點的深度為最小深度;
}
if (front.right != null){
queue.add(front.right);
}
if (front.left != null){
queue.add(front.left);
}
}
}
return level;
}
6.重構二叉樹
根據二叉樹的前序或后序中的一個再加上中序來還原出整個二叉樹。
注意: 中序是必須有的,因為其可以明確的把左右子樹分開。
先看下3種遍歷的特點(如下圖):
特點
- 1.前序的第一個節點是root,后序的最后一個節點是root。
- 2.每種排序的左右子樹分布都是有規律的。
- 3.每一個子樹又可以看成是一顆全新的樹,仍然遵循上述規律。
6.1 前序+中序
前序的遍歷順序是根左右,中序的遍歷順序是左中右,
遞歸實現
1.前序的第一個節點是root節點,對應能夠找到在中序中的位置。
2.根據中序遍歷的特點,在找到的根前邊序列是左子樹的中序遍歷序,后邊序列是右子樹的中序遍歷。
3.求出左邊序列的個數,比如設為leftSize,那在前序序列中緊跟着根的leftSize個元素是左子樹的前序序列,后邊的為右子樹的前序序列。
4.這樣就又獲得了兩個子樹的前序遍歷和中序遍歷,開始遞歸。
/**
* 根據前序遍歷和中序遍歷構造二叉樹;
*/
public static TreeNode buildTreeByPreOrder(int[] preorder, int[] inorder){
if (preorder == null){
return null;
}
//因為我們要在中序遍歷中尋找某個元素的位置,然后划分左右子樹
//用一個map來存儲元素在中序遍歷中的位置,
Map<Integer,Integer> map = new HashMap<>();
for(int i = 0; i < inorder.length; i++){
map.put(inorder[i], i);
}
return buildTreeByPreOrder(preorder, inorder, 0, preorder.length-1, 0, inorder.length-1,map);
}
//傳入前序和中序,傳入前序的左右邊界,中序的左右邊界;
private static TreeNode buildTreeByPreOrder(int[] preorder, int[] inorder, int preleft, int preright, int inleft, int inright, Map<Integer,Integer> map){
if (preleft > preright) return null;
//獲得整顆樹的根節點:先序中的第一個元素;
TreeNode root = new TreeNode(preorder[preleft]);
//得到此元素在中序中的位置,以此進行划分出左右子樹;
int rootIndex = map.get(root);
//得到左子樹的大小;(注意此時不能直接是rootIndex,inleft不總是從0開始的,想一下建立右子樹的左子樹。
int leftTreeSize = rootIndex - inleft;
//左子樹的中序:inleft不變,inright為rootIndex-1;
//左子樹的前序:preleft為根后一位,即preleft+1,preright為根后leftTreeSize位,即preleft+leftTreeSize;
root.left = buildTreeByPreOrder(preorder,inorder,preleft+1, preleft+leftTreeSize, inleft, rootIndex-1, map);
//右子樹的中序:inleft為rootIndex+1,inright不變;
//右子樹的前序:preleft為左子樹的右邊界+1,即preleft+leftTreeSize+1,preright不變;
root.right = buildTreeByPreOrder(preorder,inorder,preleft+leftTreeSize+1, preright, rootIndex+1, inright,map);
return root;
}
6.2 后序+中序
后序的遍歷順序是左右根,中序的遍歷順序是左根右,
遞歸實現
1.后序的第一個節點是root節點,對應能夠找到在中序中的位置。
2.根據中序遍歷的特點,在找到的根前邊序列是左子樹的中序遍歷,后邊序列是右子樹的中序遍歷。
3.求出左邊序列的個數,比如設為leftSize,那在后序序列中的leftSize個元素是左子樹的后序序列,后邊的為右子樹的后序序列。
4.這樣就又獲得了兩個子樹的后序遍歷和中序遍歷,開始遞歸。
/**
* 根據后序遍歷和中序遍歷構造二叉樹
*/
public static TreeNode buildTreeByPostOrder(int[] postorder, int[] inorder){
if (postorder == null){
return null;
}
//因為我們要在中序遍歷中尋找某個元素的位置,然后划分左右子樹
//用一個map來存儲元素在中序遍歷中的位置,
Map<Integer,Integer> map = new HashMap<>();
for(int i = 0; i < inorder.length; i++){
map.put(inorder[i], i);
}
return buildTreeByPostOrder(inorder, postorder, 0, inorder.length-1, 0, postorder.length-1,map);
}
//傳入后序和中序,傳入后序的左右邊界,中序的左右邊界;
private static TreeNode buildTreeByPostOrder(int[] inorder, int[] postorder, int inleft, int inright, int postleft, int postright, Map<Integer,Integer> map){
if (postleft > postright) return null;
//獲得整顆樹的根節點:后序中的最后個元素;
TreeNode root = new TreeNode(postorder[postleft]);
//得到此元素在中序中的位置,以此進行划分出左右子樹;
int rootIndex = map.get(root.value);
//得到左子樹的大小;(注意此時不能直接是rootIndex,inleft不總是從0開始的,想一下建立右子樹的左子樹。
int leftTreeSize = rootIndex - inleft;
root.left = buildTreeByPostOrder(inorder, postorder, inleft, rootIndex-1,postleft,postleft+leftTreeSize-1,map);
root.right = buildTreeByPostOrder(inorder,postorder,rootIndex+1, inright, postleft+leftTreeSize,postright-1,map);
return root;
}
附錄(全程序)
package xin.utils;
import jdk.internal.dynalink.beans.StaticClass;
import sun.reflect.generics.tree.VoidDescriptor;
import javax.sound.midi.Soundbank;
import java.awt.font.TransformAttribute;
import java.util.*;
public class Tree {
}
/**
* 定義一個二叉樹節點;
*/
class TreeNode<T>{
public T value; //數據;
public TreeNode<T> left; //左子樹;
public TreeNode<T> right;//右子樹;
public TreeNode(){} //空參的;
public TreeNode(T value){ //有參的;
this.value = value;
}
public TreeNode(T value, TreeNode left, TreeNode right){
this.value = value;
this.left = left;
this.right = right;
}
}
class BinaryTreeTraverse<T> {
/**
* 前序遍歷(遞歸實現);
*/
public static void preOrderByRecursion(TreeNode node) {
if (node == null) return; //遞歸終止條件;
System.out.println(node.value); //獲取根節點的值;
preOrderByRecursion(node.left); //左子樹的根節點;
preOrderByRecursion(node.right);//右子樹的根節點;
}
/**
* 前序遍歷(非遞歸實現);
* 本質上就是維持遞歸實現的棧;
* 1.先入棧根節點,輸出根節點的值,再入棧其右節點,左節點;(為了出棧的時候先出左節點,再出右節點);
* 2.出棧左節點,輸出值,再入棧左節點的右節點、左節點;直到遍歷完左子樹;
* 3.出棧右節點,輸出值,再入棧右節點的右節點、左節點;直到遍歷完右子樹;
* 每次都是出棧一個根節點,如果有孩子,就依次入棧其右節點和左節點。
* 規則:
* 壓入根節點;
* 1.彈出就打印;
* 2.如有右孩子,壓入右;
* 3.如有左孩子,壓入左;重復;
* (相當於兩個孩子替換掉了棧里的根節點,這就很符號:根先走了,左子樹干到了最頂部,要是我左子樹還有孩子,ok,我也走,兩個孩子來替我,
* 要是左子樹沒孩子了,我自己出去,我這里就完事了;再去看右子樹就可以了)
*/
public static void preOrder(TreeNode node) {
Stack<TreeNode> stack = new Stack<>();
if (node != null) {
stack.push(node); //根節點入棧;
}
while (!stack.isEmpty()) {
TreeNode top = stack.pop(); //彈出就打印;
System.out.println(top.value);
if (top.right != null) stack.push(top.right); //依次入棧右節點和左節點;
if (top.left != null) stack.push(top.left);
}
}
/**
* 中序遍歷(遞歸實現)
*/
public static void inOrderByRecursion(TreeNode node) {
if (node == null) return; //遞歸終止;
inOrderByRecursion(node.left); //先左子樹;
System.out.println(node.value); //再根節點;
inOrderByRecursion(node.right); //再右節點;
}
/**
* 中序遍歷(非遞歸實現)
* 規則:
* 1.整條左邊界依次壓棧;
* 2.條件1執行不了,彈出就打印;
* 3.來到彈出節點右樹上,繼續執行條件1;(右樹為空,執行2.彈出打印;右樹不為空,執行1;壓棧)
* 說明:將整個樹全用左邊界去看,都是先處理了左邊界,將左邊界分解成了左頭,頭先入,左再入,然后弄不動了,彈出,然后看其右節點,再把右節點里的左依次進去;就這樣往返;
*/
public static void inOrder(TreeNode node) {
Stack<TreeNode> stack = new Stack<>();
while (!stack.isEmpty() || node != null) {
if (node != null) { //條件1;能往左走就往左走;
stack.push(node);
node = node.left;
} else {
TreeNode top = stack.pop(); //條件2;彈出打印;
System.out.println(top.value);
node = top.right; //條件3;來彈出節點右樹上,繼續1;
}
}
}
/**
* 后序遍歷(遞歸實現)
*/
public static void postOrderByRecursion(TreeNode node) {
if (node == null) return;
postOrderByRecursion(node.left);
postOrderByRecursion(node.right);
System.out.println(node.value);
}
/**
* 后序遍歷(非遞歸實現)
* 想一下前序遍歷:根左右;過程是先壓右孩子,再壓左孩子;
* 如果我們想實現根右左:那就把前序里的換成先壓左,再壓右;就處理成了 根右左;
* 然后再從后往前看,就變成了右左根;所以可以再准備一個棧,用來把第一個棧彈出的壓到第二個,那第二個彈出的時候就倒過來了;
* 要記住:棧有實現倒序的功能;
*/
public static void postOrder(TreeNode node) {
Stack<TreeNode> stackA = new Stack<>();
Stack<TreeNode> stackB = new Stack<>();
if (node != null) {
stackA.push(node);
}
while (!stackA.isEmpty()) {
TreeNode top = stackA.pop();
stackB.push(top); //棧A彈出的進入棧B;先實現根右左,B倒序實現左右根;
if (top.left != null) stackA.push(top.left);
if (top.right != null) stackA.push(top.right);
}
while (!stackB.isEmpty()) {
System.out.println(stackB.pop().value);
}
}
/**
* Morris遍歷;
* 二叉樹的結構中只有父節點指向孩子節點,孩子節點不能向上指,所以需要棧。
* 而morris遍歷的實質就是讓下層節點能夠指向上層。怎么辦呢,一個節點有兩個指針,左和右,如果這兩個指針上都有指向具體的節點肯定就不行了。
* 但是二叉樹上有很多空閑指針,比如所有的葉子節點,它們的指針就指向null,所以可以利用其right指針指向上層的節點。
* 這樣連接后,cur這個指針就可以完整的順着一個鏈條遍歷完整個樹。
* 核心:以某個根節點開始,找到它左子樹的最右側節點(必然是個葉子節點),然后利用其right指向根節點(完成向上層的返回)。
* 到達兩次的是有左子樹的節點;
* 到達一次的是沒有左子樹的節點;
*/
public static void morris(TreeNode node) {
if (node == null) {
return;
}
TreeNode cur = node;
TreeNode mostRight = null; //cur左子樹的最右節點;
while (cur != null) {
mostRight = cur.left;
if (mostRight != null) { //cur有左子樹;
//找到左子樹的最右節點;
while (mostRight.right != null && mostRight.right != cur) {
mostRight = mostRight.right; //1.右邊為空了證明到頭了(找到了);2.右邊指向上層了證明之前就處理過了,結束;
}
//走到這里證明跳出上面循環,無非兩個原因:
//1.右邊沒了;2.右邊指向上層了(之前就處理過了);
if (mostRight.right == null) { //右邊走到頭了;
mostRight.right = cur; //左子樹的最右節點指向上層(cur);
cur = cur.left; //cur左移,處理下一個節點;
continue; //此次循環結束,開始下一個cur;
} else { //證明這個mostRight的right指針已經處理過了,即mostRight已經指向了cur;
// 能到這里說明我們已經回到了根節點,並且重復了之前的操作;也說明我們已經完全處理完了此根節點左邊的的樹了,把路斷開;
mostRight.right = null;
}
}
//cur右移的情況:
//1.cur沒有左子樹了(自然要開始處理右子樹)
//2.cur有左子樹,但是cur左子樹最右節點已經指向cur了(執行完上面else斷開后,cur左邊已經完全處理好了,開始右移。)
cur = cur.right;
}
}
/**
* 前序遍歷(Morris實現);
* 1.對於cur只到達一次的節點(沒有左子樹),cur到達就打印;
* 2.對於cur到達兩次的節點,到達第一次時打印;
*/
public static void preOrderMorris(TreeNode node) {
if (node == null) {
return;
}
TreeNode cur = node;
TreeNode mostRightNode = null;
while (cur != null) {
mostRightNode = cur.left;
if (mostRightNode != null) { //到達兩次的節點;
//找到cur左子樹的最右節點;
while (mostRightNode.right != null && mostRightNode.right != cur) {
mostRightNode = mostRightNode.right;
}
if (mostRightNode.right == null) {
mostRightNode.right = cur; //指向上層cur;
System.out.println(cur.value); //第一次到的時候打印;
cur = cur.left;
continue;
} else {
mostRightNode.right = null; //第二次到時不打印;
}
} else {
System.out.println(cur.value); //只到達一次的節點;
}
cur = cur.right;
}
}
/**
* 中序遍歷(Morris實現);
* 1.對於cur只到達一次的節點(沒有左子樹),cur到達就打印;
* 2.對於cur到達兩次的節點,到達第二次時打印;
*/
public static void inOrderMorris(TreeNode node) {
if (node == null) {
return;
}
TreeNode cur = node;
TreeNode mostRightNode = null;
while (cur != null) {
mostRightNode = cur.left;
if (mostRightNode != null) { //有左子樹,到達兩次的節點;
while (mostRightNode.right != null && mostRightNode.right != cur) {
mostRightNode = mostRightNode.right;
}
if (mostRightNode == null) {
mostRightNode.right = cur; //第一次不打印;
cur = cur.left;
continue;
} else {
System.out.println(cur.value); //第二次遇到時打印;
mostRightNode.right = null;
}
} else {
System.out.println(cur.value); //只到達一次的節點,遇到就打印;
}
cur = cur.right;
}
}
/**
* 后序遍歷(morris實現)
* 后序遍歷比前面兩個要復雜一點;
* 將一個節點的連續右節點當做是一個單鏈表來看待,
* 當返回上層后,也就是將建立的連線斷開后,打印下層的單鏈表;
* 單鏈表逆序打印,就和我們做的把單鏈表逆序一樣。
*/
public static void postOrderMorris(TreeNode node) {
if (cur == null) {
return;
}
TreeNode cur = node;
TreeNode mostRightNode = null;
while (node != null) {
mostRightNode = cur.left;
if (mostRightNode != null) {
while (mostRightNode.right != null && mostRightNode.right != cur) {
mostRightNode = mostRightNode.right;
}
if (mostRightNode.right == null) {
mostRightNode.right = cur;
cur = cur.left;
continue;
} else { //能到這里的都是達到過兩次的,也就是是有左孩子的。
mostRightNode.right = null; //這時候是已經返回上層之后,斷開了連接,所以打印下層的單鏈表;
postMorrisPrint(cur.left);
}
}
cur = cur.right;
}
postMorrisPrint(node); //最后把頭節點那一串右打印一遍;
}
public static void postMorrisPrint(TreeNode node) {
TreeNode reverseList = postMorrisReverseList(node); //反轉鏈表;
TreeNode cur = reverseList;
while (cur != null) {
System.out.println(cur.value);
cur = cur.right;
}
postMorrisReverseList(reverseList); //最后再還原;
}
public static TreeNode postMorrisReverseList(TreeNode node) {
TreeNode cur = node;
TreeNode pre = null;
while (cur != null) {
TreeNode next = cur.right;
cur.right = pre;
pre = cur;
cur = next;
}
return pre;
}
/**
* 層次遍歷
* 借助隊列的結構,每一層依次入隊,再依次出隊;
* 對該層節點進行出隊操作時,需要將該節點的左孩子和右孩子入隊;
*/
public static void layerOrder(TreeNode node) {
if (node == null) return;
Queue<TreeNode> queue = new LinkedList<>();
queue.add(node);
while (!queue.isEmpty()) {
int size = queue.size(); //當前層的節點數量;
for (int i = 0; i < size; i++) {
TreeNode front = queue.poll();
System.out.println(front.value);
if (front.left != null) queue.add(front.left);
if (front.right != null) queue.add(front.right);
}
}
}
}
class Depth{
/**
* 求最大深度(遞歸)
* 最大深度是左子樹和右子樹的最大深度的大的那個+1;
*/
public static int maxDepthByRecursion(TreeNode node){
if(node == null) return 0;
int leftDepth = maxDepthByRecursion(node.left);
int rightDepth = maxDepthByRecursion(node.right);
return Math.max(leftDepth,rightDepth)+1;
}
/**
* 求最大深度(非遞歸)
* 層次遍歷(BFS)
* 每遍歷一層,則計數器加+1;直到遍歷完成,得到樹的深度。
*/
public static int maxDepth(TreeNode node){
if (node == null) return 0;
Queue<TreeNode> queue = new LinkedList<>();
int level = 0; //層數;
queue.add(node);
while (!queue.isEmpty()){
level++;
int levelNum = queue.size(); //每層的節點數;
for (int i = 0; i < levelNum; i++){
TreeNode front = queue.poll(); //當前層出隊,下一層入隊;
if (front.left != null) queue.add(front.left);
if (front.right != null) queue.add(front.right);
}
}
return level;
}
/**
* 求最小深度(遞歸)
* 注意和求最大深度的區別;
*/
public static int minDepthByRecursion(TreeNode node){
if (node == null) return 0;
if (node.right == null && node.left == null) return 1;
if (node.left == null && node.right != null) return minDepthByRecursion(node.right) + 1;
if (node.right == null && node.left != null) return minDepthByRecursion(node.left) + 1;
return Math.min(minDepthByRecursion(node.left), minDepthByRecursion(node.right))+1;
}
/**
* 求最小深度(非遞歸)
*/
public static int minDepth(TreeNode node){
if (node == null) return 0;
Queue<TreeNode> queue = new LinkedList<>();
int level = 0;
queue.add(node);
while (!queue.isEmpty()){
level++;
int levelnum = queue.size();
for(int i = 0; i < levelnum; i++){
TreeNode front = queue.poll();
if (front.left == null && front.right == null){
return level; //遇到第一個無葉子節點的時候,該節點的深度為最小深度;
}
if (front.right != null){
queue.add(front.right);
}
if (front.left != null){
queue.add(front.left);
}
}
}
return level;
}
}
class BuildBinaryTree{
/**
* 根據前序遍歷和中序遍歷構造二叉樹;
*/
public static TreeNode buildTreeByPreOrder(int[] preorder, int[] inorder){
if (preorder == null){
return null;
}
//因為我們要在中序遍歷中尋找某個元素的位置,然后划分左右子樹
//用一個map來存儲元素在中序遍歷中的位置,
Map<Integer,Integer> map = new HashMap<>();
for(int i = 0; i < inorder.length; i++){
map.put(inorder[i], i);
}
return buildTreeByPreOrder(preorder, inorder, 0, preorder.length-1, 0, inorder.length-1,map);
}
//傳入前序和中序,傳入前序的左右邊界,中序的左右邊界;
private static TreeNode buildTreeByPreOrder(int[] preorder, int[] inorder, int preleft, int preright, int inleft, int inright, Map<Integer,Integer> map){
if (preleft > preright) return null;
//獲得整顆樹的根節點:先序中的第一個元素;
TreeNode root = new TreeNode(preorder[preleft]);
//得到此元素在中序中的位置,以此進行划分出左右子樹;
int rootIndex = map.get(root.value);
//得到左子樹的大小;(注意此時不能直接是rootIndex,inleft不總是從0開始的,想一下建立右子樹的左子樹。
int leftTreeSize = rootIndex - inleft;
//左子樹的中序:inleft不變,inright為rootIndex-1;
//左子樹的前序:preleft為根后一位,即preleft+1,preright為根后leftTreeSize位,即preleft+leftTreeSize;
root.left = buildTreeByPreOrder(preorder,inorder,preleft+1, preleft+leftTreeSize, inleft, rootIndex-1, map);
//右子樹的中序:inleft為rootIndex+1,inright不變;
//右子樹的前序:preleft為左子樹的右邊界+1,即preleft+leftTreeSize+1,preright不變;
root.right = buildTreeByPreOrder(preorder,inorder,preleft+leftTreeSize+1, preright, rootIndex+1, inright,map);
return root;
}
/**
* 根據后序遍歷和中序遍歷構造二叉樹
*/
public static TreeNode buildTreeByPostOrder(int[] postorder, int[] inorder){
if (postorder == null){
return null;
}
//因為我們要在中序遍歷中尋找某個元素的位置,然后划分左右子樹
//用一個map來存儲元素在中序遍歷中的位置,
Map<Integer,Integer> map = new HashMap<>();
for(int i = 0; i < inorder.length; i++){
map.put(inorder[i], i);
}
return buildTreeByPostOrder(inorder, postorder, 0, inorder.length-1, 0, postorder.length-1,map);
}
//傳入后序和中序,傳入后序的左右邊界,中序的左右邊界;
private static TreeNode buildTreeByPostOrder(int[] inorder, int[] postorder, int inleft, int inright, int postleft, int postright, Map<Integer,Integer> map){
if (postleft > postright) return null;
//獲得整顆樹的根節點:后序中的最后個元素;
TreeNode root = new TreeNode(postorder[postleft]);
//得到此元素在中序中的位置,以此進行划分出左右子樹;
int rootIndex = map.get(root.value);
//得到左子樹的大小;(注意此時不能直接是rootIndex,inleft不總是從0開始的,想一下建立右子樹的左子樹。
int leftTreeSize = rootIndex - inleft;
root.left = buildTreeByPostOrder(inorder, postorder, inleft, rootIndex-1,postleft,postleft+leftTreeSize-1,map);
root.right = buildTreeByPostOrder(inorder,postorder,rootIndex+1, inright, postleft+leftTreeSize,postright-1,map);
return root;
}
}