算法 | 一週刷完《劍指Offer》 Day2:第17~26題

寫在前面


Day2:第17~26題

今天的題涉及遞歸及二叉樹較多,需要深入理解其邏輯和原理。

  • T17. 樹的子結構
  • T18. 二叉樹的鏡像
  • T19. 順時針打印矩陣
  • T20. 包含 min 函數的棧
  • T21. 棧的壓入、彈出序列
  • T22. 從上往下打印二叉樹
  • T23. 二叉搜索樹的後序遍歷序列
  • T24. 二叉樹中和爲某一值的路徑
  • T25. 複雜鏈表的複製
  • T26. 二叉搜索樹與雙向鏈表

T17. 樹的子結構

題目描述

輸入兩棵二叉樹A,B,判斷B是不是A的子結構。(ps:我們約定空樹不是任意一個樹的子結構)

解題思路

設置終止條件進行判斷,將B樹與A樹,A樹左子樹,A樹右子樹進行比較,遞歸進行即可。

	public boolean HasSubtree(TreeNode root1, TreeNode root2) {
		if(root1 == null || root2 == null) return false;
	    return isSubtree(root1, root2) //root2與root1比較
	    		|| HasSubtree(root1.left, root2) //root2與root1左子樹比較,遞歸邏輯
	    		|| HasSubtree(root1.right, root2); //root2與root1右比較,遞歸邏輯
	    //以上三種情況任一爲true,即證明root2是root1的子結構
    }
	
	private boolean isSubtree(TreeNode root1, TreeNode root2) {
		//終止判定
		if(root1 == null && root2 == null) return true;//爲null,能執行到此步且相同,爲子結構
	    if(root1 == null) return false;//root1爲null,root2不爲null,不同,不爲子結構
	    if(root2 == null) return true;//root1不爲null,root2爲null,能執行到此步說明相同,爲子結構
	    if(root1.val != root2.val) return false;//root1,root2都不爲null,val不同,不爲子結構
	    
	    //能執行到此步,說明未判定完,繼續對root1,root2的左右子樹分別遞歸此方法進行判斷,均爲true則爲子結構
	    return isSubtree(root1.left, root2.left) && isSubtree(root1.right, root2.right);
	}

T18. 二叉樹的鏡像

題目描述

操作給定的二叉樹,將其變換爲源二叉樹的鏡像。

解題思路

交換每個節點的左右子樹,並對該節點的左右子節點分別進行此操作,遞歸進行即可。

	public void Mirror(TreeNode root) {
        if(root == null) return;
        
        //交換左右子樹
        swap(root);
        //分別對root左右子樹進行交換,遞歸調用此方法即可
        Mirror(root.left);
        Mirror(root.right);
    }
	
	private void swap(TreeNode root) {
		TreeNode tmp = root.left;
		root.left = root.right;
		root.right = tmp;
	}

T19. 順時針打印矩陣

題目描述

輸入一個矩陣,按照從外向裏以順時針的順序依次打印出每一個數字,例如,如果輸入如下4 X 4矩陣: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 則依次打印出數字1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10.

解題思路

順時針繞圈打印即可,從外向裏每次循環繞一圈。
注意:一定細心,出錯基本都是馬虎造成的。

	public ArrayList<Integer> printMatrix(int[][] matrix) {
		ArrayList<Integer> list = new ArrayList<>();
		
		if(matrix == null || matrix[0] == null) return list;
		
		int rowTop = 0, rowBottom = matrix.length - 1;
		int colLeft = 0, colRight = matrix[0].length - 1;
		
		while(colLeft <= colRight && rowTop <= rowBottom) {//跳出條件
			for(int i = colLeft; i <= colRight; i ++) {//添加上邊一行,從左到右
				list.add(matrix[rowTop][i]);
			}
			for(int i = rowTop + 1; i <= rowBottom; i ++) {//添加右邊一列,從上到下,注意去掉已添加的【右上角】。
				list.add(matrix[i][colRight]);
			}
			if(rowTop != rowBottom) {//若相等則已到同一行,無可繼續添加的
				for(int i = colRight - 1; i >= colLeft; i --) {//添加下邊一行,從右到左,注意去掉已添加的【右下角】。
					list.add(matrix[rowBottom][i]);
				}
			}
			if(colLeft != colRight) {//若相等則已到同一列,無可繼續添加的
				for(int i = rowBottom - 1; i > rowTop; i --) {//添加左邊一列,從下到上,注意去掉已添加的【左下角】及【左上角】。
					list.add(matrix[i][colLeft]);
				}
			}
			
			colLeft ++;
			colRight --;
			rowTop ++;
			rowBottom --;
		}
		
		return list;
    }

T20. 包含 min 函數的棧

題目描述

定義棧的數據結構,請在該類型中實現一個能夠得到棧中所含最小元素的min函數(時間複雜度應爲O(1))。

解題思路

定義兩個棧,stack存入棧的數,minStack存該數入棧後棧內的最小數min。
注意:兩個棧大小是相同的,同步入棧及出棧。即哪怕入棧的數不是最小的,也把那個最小的再入一次minStack。

	private Stack<Integer> stack = new Stack<>();
	private int min = Integer.MAX_VALUE;
	//minStack用於存儲任一元素入棧時,當前棧內的最小值,與stack是同步入棧出棧的,即兩個棧內元素數目相同
	private Stack<Integer> minStack = new Stack<>();
	
	public void push(int node) {
        stack.push(node);
        if(min > node) {
        	min = node;
        }
    	minStack.push(min);
    }
    
    public void pop() {
        stack.pop();
        minStack.pop();
        min = minStack.peek();
    }
    
    public int top() {
        return stack.peek();
    }
    
    public int min() {
        return min;
    }

T21. 棧的壓入、彈出序列

題目描述

輸入兩個整數序列,第一個序列表示棧的壓入順序,請判斷第二個序列是否可能爲該棧的彈出順序。假設壓入棧的所有數字均不相等。例如序列1,2,3,4,5是某棧的壓入順序,序列4,5,3,2,1是該壓棧序列對應的一個彈出序列,但4,3,5,1,2就不可能是該壓棧序列的彈出序列。(注意:這兩個序列的長度是相等的)

解題思路

建個棧,模擬出入棧順序即可。
注意:每次入棧後,要對出棧順序的數組進行檢測,循環把該出棧的出了,再進行下次入棧。

	public boolean IsPopOrder(int[] pushA, int[] popA) {//pushA和popA長度相同
		//建個棧,模擬入棧出棧操作即可
		Stack<Integer> stack = new Stack<>();
		int popIndex = 0;
		
		for(int pushIndex = 0; pushIndex < pushA.length; pushIndex ++) {
			stack.push(pushA[pushIndex]);//按pushA順序入棧
	    	  
			while(popIndex < popA.length && popA[popIndex] == stack.peek()) {//相同說明可出棧,即模擬popA順序進行出棧操作
				stack.pop();
				popIndex ++;
			}
		}

		//若棧空,說明pushA入棧能按popA順序出棧
		return stack.isEmpty();
    }

T22. 從上往下打印二叉樹

題目描述

從上往下打印出二叉樹的每個節點,同層節點從左至右打印。

解題思路

二叉樹層輸出,使用**廣度優先搜索(BFS)**即可。
使用隊列實現,邊出隊邊輸出,同時將其左右子節點壓入隊列。

	public ArrayList<Integer> PrintFromTopToBottom(TreeNode root) {
		ArrayList<Integer> list = new ArrayList<>();
		
		if(root == null) return list;
		
		//使用隊列實現,不斷按層壓入及輸出
		Queue<TreeNode> queue = new LinkedList<>();
		queue.add(root);
		
		while(!queue.isEmpty()) {
			TreeNode tmp = queue.poll();
			//先左後右按順序壓入子節點
			if(tmp.left != null) queue.add(tmp.left);
			if(tmp.right != null) queue.add(tmp.right);
			
			list.add(tmp.val);
		}
		
		return list;
    }

T23. 二叉搜索樹的後序遍歷序列

題目描述

輸入一個整數數組,判斷該數組是不是某二叉搜索樹的後序遍歷的結果。如果是則輸出Yes,否則輸出No。假設輸入的數組的任意兩個數字都互不相同。

解題思路

二叉搜索樹: 若它的左子樹不空,則左子樹上所有結點的值均小於它的根結點的值;若它的右子樹不空,則右子樹上所有結點的值均大於它的根結點的值;它的左、右子樹也分別爲二叉搜索樹(二叉排序樹)。
後序遍歷:左 -> 右 -> 根。
因此,後序遍歷最後一個數爲根節點。通過根節點可把後序遍歷分爲兩部分前半部分爲小於根節點的左子樹後半部分爲大於根節點的右子樹。然後根據此原理,遞歸對左右子樹分別用此方法進行驗證即可。

	public boolean VerifySquenceOfBST(int [] sequence) {
		if(sequence == null || sequence.length == 0) return false;
		
		return verify(sequence, 0, sequence.length - 1);
    }
	
	private boolean verify(int[] sequence, int first, int last) {
    	//終止條件
		if(first >= last) return true;
		
		int rootValue = sequence[last];//後序遍歷的根節點爲最後一個
		int index = first;
		
		while(sequence[index] <= rootValue && index < last) {//比根節點小的爲左子樹,大的爲右子樹
			index ++;
		}
		//此時sequence[index]是第一個比根節點大的值
		//可將sequence[0]~sequence[index-1]認爲是左子樹,sequence[index]~sequence[last-1]認爲是右子樹
		for(int i = index; i < last; i ++) {
			if(sequence[i] < rootValue) {//若右子樹中存在比根節點小的,則不是二叉搜索樹
				return false;
			}
		}
		
		//此時分別對根節點的左右子樹進行迭代判斷,全部爲true則是後序遍歷
		return verify(sequence, first, index - 1)
				&& verify(sequence, index, last - 1);//last爲根節點
    }

T24. 二叉樹中和爲某一值的路徑

題目描述

輸入一顆二叉樹的跟節點和一個整數,打印出二叉樹中結點值的和爲輸入整數的所有路徑。路徑定義爲從樹的根結點開始往下一直到葉結點所經過的結點形成一條路徑。(注意: 在返回值的list中,數組長度大的數組靠前)

解題思路

深度優先搜索(DFS),通過減法的思想不斷用target減去當前節點值,能減爲0則是一條路徑。
注意:路徑要求最後到達葉子節點
迭代過程中需把當前值在path中移除以保證路徑正確,相當於回退到上一步的路徑。(詳見代碼)

	private ArrayList<ArrayList<Integer>> result = new ArrayList<>();
	
	public ArrayList<ArrayList<Integer>> FindPath(TreeNode root, int target) {
        if(root == null) return result;
        
        ArrayList<Integer> path = new ArrayList<>();
        findPathDFS(root, target, path);
        
		return result;
    }
	
	private void findPathDFS(TreeNode node, int target, ArrayList<Integer> path) {
		if(node == null) return;
		
		path.add(node.val);
		target -= node.val;//減法的思想,目標值能減爲0則是一條路徑
		if(target == 0 && node.left == null && node.right == null) {//已經到達葉子節點且targe正好減完
			result.add(new ArrayList<Integer>(path));
		} else if(target > 0) {//若>0則繼續對其左右子節點進行迭代判斷
			findPathDFS(node.left, target, path);
			findPathDFS(node.right, target, path);
		}
		
		path.remove(path.size() - 1);//此步重要,迭代過程中需把當前值在path中移除以保證路徑正確,相當於回退到上一步的路徑
	}

T25. 複雜鏈表的複製

題目描述

輸入一個複雜鏈表(每個節點中有節點值,以及兩個指針,一個指向下一個節點,另一個特殊指針指向任意一個節點),返回結果爲複製後複雜鏈表的head。(注意,輸出結果中請不要返回參數中的節點引用,否則判題程序會直接返回空)

解題思路

step1:在每個節點的後面(或者說每個節點與下一個節點中間)插入新節點。該新節點爲克隆節點,這麼做是爲了連接random節點。
step2:連接random節點。
step3:拆分鏈表,下邊爲原鏈表,上邊爲clone鏈表。
詳見下圖:

	public RandomListNode Clone(RandomListNode pHead) {
		if(pHead == null) return null;
		
		//step1:在每個節點的後面(或者說每個節點與下一個節點中間)插入【新節點】
		//該新節點爲克隆節點,這麼做是爲了連接random節點
		RandomListNode tmp = pHead;
		while(tmp != null) {
			RandomListNode cloneNode = new RandomListNode(tmp.label);
			
			//插入clone節點
			cloneNode.next = tmp.next;
			tmp.next = cloneNode;
			//移到原鏈表的下一個節點
			tmp = cloneNode.next;
		}
		
		//step2:連接random節點
		tmp = pHead;
		while(tmp != null) {
			RandomListNode cloneNode = tmp.next;
			if(tmp.random != null) {
				cloneNode.random = tmp.random.next;//tmp.random是原鏈表的節點,tmp.random.next纔是那個節點的clone節點
			}
			tmp = cloneNode.next;
		}
		
		//step3:拆分鏈表(詳見圖片)
		tmp = pHead;
		RandomListNode cloneHead = tmp.next;
		while(tmp.next != null) {
			RandomListNode node = tmp.next;
			tmp.next = node.next;
			tmp = node;
		}
		
		return cloneHead;
    }

T26. 二叉搜索樹與雙向鏈表

題目描述

輸入一棵二叉搜索樹,將該二叉搜索樹轉換成一個排序的雙向鏈表。要求不能創建任何新的結點,只能調整樹中結點指針的指向。

解題思路

由於二叉搜索樹左子節點 < 根節點 < 右子節點的性質,題目實質上是二叉搜索樹中序遍歷,改節點的指針。left代表雙向鏈表的prev指針,right代表next指針。

	private TreeNode pre = null;//用於記錄上一個節點
	private TreeNode head = null;
	
	public TreeNode Convert(TreeNode pRootOfTree) {
		if(pRootOfTree == null) return null;
		
		inOrder(pRootOfTree);
		
		return head;
    }
	
	private void inOrder(TreeNode node) {
		if(node == null) return;
		
		//實質上是中序遍歷,改節點的指針。left代表雙向鏈表的prev指針,right代表next
		//左
		inOrder(node.left);
		
		//根
		//改指針的指向(只需與上一個節點相連即可)
		node.left = pre;//連上一個
		if(pre != null) {//如果上一個不爲null,連此時這個
			pre.right = node;
		}
		pre = node;//將pre移向此時這個節點,爲下一次迭代做準備
		
		if(head == null) head = node;//只在第一次找到最小節點時作爲頭結點
		
		//右
		inOrder(node.right);
	}

項目地址https://github.com/JohnnyJYWu/offer-Java

上一篇算法 | 一週刷完《劍指Offer》 Day1:第1~16題
下一篇:算法 | 一週刷完《劍指Offer》 Day3:第27~38題

希望這篇文章對你有幫助~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章