leetcode二叉樹相關操作的總結
做了這麼多關於二叉樹的題,發現一些題目的本質來來回回就是那些操作,所以覺得需要進行總結,可以讓之後的思路更明晰。
二叉樹的操作,最普遍的就是遞迴,和前中後序遍歷,層序遍歷。前中後序遍歷可以用遞迴也可以用自己的棧代替遞迴棧。層序遍歷用一個佇列輔助,每一層都是上一層所有節點的孩子。在這個過程中也可以記錄每一層包含的節點及個數。
遞迴的解決問題的話,有三個要素:首先你要確定他們之間有遞迴關係,大規模的問題由小規模的問題通過一定的轉換關係解決。遞迴不能無限遞迴下去,所以必須有個結束的條件。對於樹來說一般是到達葉子節點;再有就是遞迴可以有一些退出遞迴的條件。
遞迴的解法一般都可以用非遞迴的方式代替。因為遞迴相當於系統呼叫棧進行了臨時儲存。那麼我們就可以自己設定一個棧來實現系統棧的功能代替之。只是一些時候會比較麻煩。
如果是對整個樹操作,其實就相當於對樹的節點進行遍歷,那麼你的操作無非就對應了前序中序後序方式。對於這些方式其實訪問節點的順序也就確定下來了。visit(root.left) root.val visit(root.right)順序就是二叉搜尋樹的節點值得從小到大的順序。visit(root.right) root.val visit(root.left)就是反過來從大到小的順序。
後序遍歷大體是自底向上的,對於每一個子樹來說,根的左子樹自底向上,然後根的右子樹自底向上。訪問了該子樹根以後就往上走。後序遍歷訪問每個節點時,要麼它的右孩子為null,要麼它的右孩子是剛剛訪問的上一個節點。
先把最左邊的一列按順序入棧 然後開始迴圈 如果棧不為空,出棧,如果該節點的右孩子為空,或上一個訪問的節點,那麼就訪問該節點。如果不是,就需要先後序訪問它的右孩子,先把它再壓入棧,然後r->r.right,然後再迴圈做孩子入棧 之後再遞迴迴圈
先序遍歷相當於把最左邊的一列先按順序訪問後入棧,然後出棧r->r.right找到他們的右孩子,右孩子不入棧,之後再用同樣的方式訪問它的右子樹。
中序遍歷類似於先序遍歷,只是它把最左邊的一列按順序入棧,然後出棧訪問,再r->r.right找到他們的右孩子,右孩子不入棧,之後再用同樣的方式訪問它的右子樹。找到右孩子進入下一次迴圈,相當於對右孩子進行相同的中序遍歷,下次迴圈,如果節點為空,沒有右孩子,相當於上一個子樹已經訪問完。就出棧進入上一層,如果棧為空,說明這是最後一個右子樹被訪問完了。
關於二叉樹非遞迴遍歷方式點選開啟連結寫的比較詳細
1.判斷兩個二叉樹是否相同
二叉樹是由根和左右兩個子二叉樹構成,所以判斷兩個二叉樹是否相同就看他們根值是否相同,左子樹和右子樹是否同時相同。判斷左右子樹是否相同就又是同樣的邏輯了。所以這就是典型的遞迴判斷問題。遞迴的邊界就是判斷到葉子節點。葉子節點的特徵就是root.left==null&&root.right==null;退出遞迴的條件就是兩個樹的根值不相同。程式碼如下:
public boolean isSameTree(TreeNode p, TreeNode q) {
if(p==null&&q==null)return true;
if((p==null)||(q==null))return false;
if(p.val==q.val){
return isSameTree(p.left,q.left)&&isSameTree(p.right,q.right);
}
return false;
}
非遞迴程式碼如下:你需要把兩個節點同時壓棧,因為操作時需要對應。java的話就用兩個棧實現同時棧操作
public boolean isSameTree(TreeNode p, TreeNode q) {
Stack<TreeNode> stack_p = new Stack <> ();
Stack<TreeNode> stack_q = new Stack <> ();
if (p != null) stack_p.push( p ) ;
if (q != null) stack_q.push( q ) ;
while (!stack_p.isEmpty() && !stack_q.isEmpty()) {
TreeNode pn = stack_p.pop() ;
TreeNode qn = stack_q.pop() ;
if (pn.val != qn.val) return false ;
if (pn.right != null) stack_p.push(pn.right) ;
if (qn.right != null) stack_q.push(qn.right) ;
if (stack_p.size() != stack_q.size()) return false ;
if (pn.left != null) stack_p.push(pn.left) ;
if (qn.left != null) stack_q.push(qn.left) ;
if (stack_p.size() != stack_q.size()) return false ;
}
return stack_p.size() == stack_q.size() ;
}
2.判斷一個二叉樹是否對稱
一個二叉樹是對稱的就要滿足如下性質:它的左右子樹是鏡面對稱,即中心對稱。判斷兩個個樹是否為鏡面對稱,則需要滿足:根相同,左邊的左子樹跟右邊的右子樹鏡面對稱左邊的右子樹跟右邊的左子樹鏡面對稱。即以根垂直的線為軸線,軸對稱。
public boolean isSymmetric(TreeNode root) {
if(root == null)return true;
return isMirror(root.left,root.right);
}
public boolean isMirror(TreeNode p,TreeNode q){
if(p==null&&q==null)return true;
if(p==null||q==null)return false;
if(p.val!=q.val)return false;
return isMirror(p.left,q.right)&&isMirror(p.right,q.left);
}
非遞迴方式:
public boolean isSymmetric(TreeNode root) {
if(root==null) return true;
Stack<TreeNode> stack = new Stack<TreeNode>();
TreeNode left, right;
if(root.left!=null){
if(root.right==null) return false;
stack.push(root.left);
stack.push(root.right);
}
else if(root.right!=null){
return false;
}
while(!stack.empty()){
if(stack.size()%2!=0) return false;
right = stack.pop();
left = stack.pop();
if(right.val!=left.val) return false;
if(left.left!=null){
if(right.right==null) return false;
stack.push(left.left);
stack.push(right.right);
}
else if(right.right!=null){
return false;
}
if(left.right!=null){
if(right.left==null) return false;
stack.push(left.right);
stack.push(right.left);
}
else if(right.left!=null){
return false;
}
}
return true;
}
3.求二叉樹的最大深度(即深度)
該樹的深度就是max(左子樹,右子樹)+1;遞迴即可,邊界是葉子節點。
public int maxDepth(TreeNode root) {
if(root==null){
return 0;
}
return 1+Math.max(maxDepth(root.left),maxDepth(root.right));
}
非遞迴方式,dfs和bfs
dfs深度優先,你需要知道每一次棧操作時對應的深度,所以就用額外一個棧進行同步棧操作記錄
public int maxDepth(TreeNode root) {
if(root == null) {
return 0;
}
Stack<TreeNode> stack = new Stack<>();
Stack<Integer> value = new Stack<>();
stack.push(root);
value.push(1);
int max = 0;
while(!stack.isEmpty()) {
TreeNode node = stack.pop();
int temp = value.pop();
max = Math.max(temp, max);
if(node.left != null) {
stack.push(node.left);
value.push(temp+1);
}
if(node.right != null) {
stack.push(node.right);
value.push(temp+1);
}
}
return max;
}
bfspublic int maxDepth(TreeNode root) {
if(root == null) {
return 0;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
int count = 0;
while(!queue.isEmpty()) {
int size = queue.size();
while(size-- > 0) {
TreeNode node = queue.poll();
if(node.left != null) {
queue.offer(node.left);
}
if(node.right != null) {
queue.offer(node.right);
}
}
count++;
}
return count;
}
4.將一個有序陣列轉換為二叉搜尋樹BST
二叉搜尋樹的左子樹中所有值比根小,右子樹中所有值比根大。所以首先我們找到這個陣列的中間值mid,它就是根。然後陣列的左半部分構成左子樹,右半部分構成右子樹。如此遞迴下去,遞迴的邊界就是當陣列被分沒的時候
public TreeNode sortedArrayToBST(int[] nums) {
return sortArray(nums,0,nums.length-1);
}
public TreeNode sortArray(int[] nums,int s,int e){
if(s > e)return null;
int mid = s + (e-s)/2;
TreeNode t = new TreeNode(nums[mid]);
t.left = sortArray(nums,s,mid-1);
t.right = sortArray(nums,mid+1,e);
return t;
}
非遞迴 壓入的節點要跟所屬陣列的位置匹配上,所以用另外兩個棧記錄對應陣列的首尾位置
public TreeNode sortedArrayToBST(int[] nums) {
int len = nums.length;
if ( len == 0 ) { return null; }
TreeNode head = new TreeNode(0);
Stack<TreeNode> nodeStack = new Stack<TreeNode>() ;//要被對應序號建立的節點壓入此棧
nodeStack.push(head);
Stack<Integer> leftIndexStack = new Stack<Integer>();
leftIndexStack.push(0);
Stack<Integer> rightIndexStack = new Stack<Integer>();
rightIndexStack.push(len-1);
while ( !nodeStack.isEmpty() ) {
TreeNode currNode = nodeStack.pop();
int left = leftIndexStack.pop();
int right = rightIndexStack.pop();
int mid = left + (right-left)/2; // avoid overflow
currNode.val = nums[mid];
if ( left <= mid-1 ) {
currNode.left = new TreeNode(0);
nodeStack.push(currNode.left);
leftIndexStack.push(left);
rightIndexStack.push(mid-1);
}
if ( mid+1 <= right ) {
currNode.right = new TreeNode(0);
nodeStack.push(currNode.right);
leftIndexStack.push(mid+1);
rightIndexStack.push(right);
}
}
return head;
}
5.將有序連結串列轉為二叉搜尋樹
首先類似陣列轉換二叉樹的思路,我們需要每次都找到連結串列的首節點,中間節點和尾節點。如何快捷的找到呢
while(fast!=null&&fast.next!=null){
fast = fast.next.next;
slow = slow.next;
}
fast每次跳兩跳,slow每次跳一跳,所以當fast跳到最後的時候,slow剛好走了一半,也就是到達中間點,由於連結串列節點個數單雙情況,fast最終指向最後一個節點或者null;由於需要遞迴的建立之後的節點,還需要mid-1,mid+1的位置,所以用pre記錄slow的前一個節點,然後進行遞迴(head,pre) slow (slow.next,fast),但是這裡注意,因為你的判斷條件在每一輪的fast.next上,所以在遞迴之前應該把連結串列斷開。遞迴的結束邊界是當最後一個節點創造樹節點以後,fast=head,slow=head,fast.next==null;所以之後pre==null,所以head==null,再下一次遞迴head==null
結束returnpublic TreeNode sortedListToBST(ListNode head) {
ListNode tail = head;
if(head==null) return null;
while(tail.next!=null){
tail = tail.next;
}
return go(head,tail);
}
public TreeNode go(ListNode head,ListNode tail){
if(head==null)return null;
if(head==tail)return new TreeNode(head.val);
ListNode fast = head;
ListNode slow = head;
ListNode pre = null;
while(fast!=null&&fast.next!=null){
fast = fast.next.next;
pre = slow;
slow = slow.next;
}
if(pre!=null){
pre.next = null;
}else{
head = null;
}
TreeNode t = new TreeNode(slow.val);
t.left = go(head,pre);
t.right = go(slow.next,fast);
return t;
}
上述的建立過程相當於前序遍歷建立節點。
如果我們知道一個樹的總節點數n,已知它是高度平衡的那麼這顆樹是不是形狀已經被確定下來了.用(0,n)(0,n/2)(n/2+1,n)...進行建立的陣列形狀就是最終的形狀,只是需要再填入每個節點的值。那麼我們用中序遍歷
建立節點得順序就相當於中序遍歷那個樹的順序,而剛好中序遍歷搜尋二叉樹的數值順序就是升序1,2,3,4,5,6剛好符合連結串列的順序,那就用對應連結串列節點值建立樹節點吧
private ListNode node;
public TreeNode sortedListToBST(ListNode head) {
if(head == null){
return null;
}
int size = 0;
ListNode runner = head;
node = head;
while(runner != null){
runner = runner.next;
size ++;
}
return inorder(0, size - 1);
}
public TreeNode inorder(int start, int end){
if(start > end){
return null;
}
int mid = start + (end - start) / 2;
TreeNode left = inorder(start, mid - 1);
TreeNode treenode = new TreeNode(node.val);
treenode.left = left;
node = node.next;
TreeNode right = inorder(mid + 1, end);
treenode.right = right;
return treenode;
}
6.遞迴的方式實現層序遍歷
public List<List<Integer>> levelOrderBottom(TreeNode root) {
List<List<Integer>> res = new ArrayList();
levelGo(res,root,0);
return res;
}
public void levelGo(List<List<Integer>> list,TreeNode t,int level){
if(t==null) return;
if(list.size()<=level){
list.add(new ArrayList<Integer>());
}
levelGo(list,t.left,level+1);
levelGo(list,t.right,level+1);
list.get(level).add(t.val);
}
層次反向遍歷(從葉子層到根層)public List<List<Integer>> levelOrderBottom(TreeNode root) {
List<List<Integer>> res = new ArrayList();
levelGo(res,root,1);
return res;
}
public void levelGo(List<List<Integer>> list,TreeNode t,int level){
if(t==null) return;
if(list.size()<level){
list.add(0,new ArrayList<Integer>());
}
levelGo(list,t.left,level+1);
levelGo(list,t.right,level+1);
list.get(list.size()-level).add(t.val);
}
都是在遞迴的過程中傳遞一個該節點所在的層數,根據其所在的層數放到對應相同位置上。相當於通過函式引數將引數中節點的資訊也壓棧,不管遞迴到了那一層,我都能通過函式引數獲得當前節點對應在整個樹中的層數。
7.判斷一棵樹是否平衡
這裡平衡的定義是每個節點的兩顆子樹的高度差不超過1.
這個題一看直接就可以遍歷樹然後求每個節點的左右子樹的高度,做判斷。如果用一個遞迴的話:
int depth (TreeNode root) {
if (root == null) return 0;
return max (depth(root.left), depth (root.right)) + 1;
}
bool isBalanced (TreeNode root) {
if (root == NULL) return true;
int left=depth(root.left);
int right=depth(root.right);
return abs(left - right) <= 1 && isBalanced(root.left) && isBalanced(root.right);
}
這相當於前序遍歷,那麼分析一下可以發現,我基本上是從上向下遍歷樹的節點,而且每次都要重新計算一下下邊節點的高度,越靠下的節點被計算的次數越多,趨於O(n方)了。這樣的話我們可以通過自底向上bottom-up後序遍歷來減少節點高度的重複計算。同時如果底下已經有子樹不平衡了,那麼就可以直接結束了,終止繼續遞迴,沒必要再計算了。所以定義一個函式,後序遍歷樹節點,如果左右子樹滿足平衡,就返回該節點的高度給其父節點,如果不平衡,直接返回一個負數(因為樹的高度是非負數,這樣可以剛好的區分),在獲得左節點高度後,遞迴呼叫判斷右節點之前就進行判斷,終止遞迴。同樣右子樹判斷之後,立馬驗證右子樹是否滿足平衡。滿足再繼續遞迴。
public boolean isBalanced(TreeNode root) {
if(root == null) return true;
return ban(root)!= -1;
}
public int ban(TreeNode t){
if(t == null)return 0;
int tl = ban(t.left);
if(tl == -1)return -1;
int tr = ban(t.right);
if(tr == -1)return -1;
if(tl - tr>1||tl-tr<-1) return -1;
return Math.max(tl,tr)+1;
}
8.找二叉樹最長路徑
兩個節點之間的連線路長為1,找出最長路徑
對於每個節點,它對應的最大路徑為左子樹的深度加右子樹的深度加2,用一個值記錄每個節點對應的最長路徑中的較大者該節點的深度就是左右子樹深度較大值加1