我的技術博客:www.zhenganwen.topjava
如下題目按照牛客網在線編程排序,全部代碼示例代碼均已經過牛客網OJ。node
在一個二維數組中(每一個一維數組的長度相同),每一行都按照從左到右遞增的順序排序,每一列都按照從上到下遞增的順序排序。請完成一個函數,輸入這樣的一個二維數組和一個整數,判斷數組中是否含有該整數。面試
public boolean Find(int target, int [][] arr) {
}
複製代碼
暴力方法是遍歷一遍二維數組,找到target
就返回true
,時間複雜度爲O(M * N)
(對於M行N列的二維數組)。編程
由題可知輸入的數據樣本具備高度規律性(單獨一行的數據來看是有序的,單獨一列的數據來看也是有序的),所以考慮可否有一個比較基準在一次的比較中根據有序性淘汰沒必要再進行遍歷比較的數。有序、查找,由此不難聯想到二分查找,咱們能夠借鑑二分查找的思路,每次選出一個數做爲比較基準進而淘汰掉一些沒必要比較的數。二分是選取數組的中位數做爲比較基準的,所以可以保證每次都淘汰掉二分之一的數,那麼此題中有沒有這種特性的數呢?咱們不妨舉例觀察一下:數組
不難發現上圖中對角線上的數是其所在行和所在列造成的序列的中位數,不妨選取右上角的數做爲比較基準,若是不相等,那麼咱們能夠淘汰掉全部它左邊的數或者它全部下面的,好比對於target = 6
,由於(0,3)
位置上的4 < 6
,所以(0,3)
位置及其同一行的左邊的全部數都小於6所以能夠直接淘汰掉,淘汰掉以後問題就變爲了從剩下的三行中找target
,這與原始問題是類似的,也就是說每一次都選取右上角的數據爲比較基準而後淘汰掉一行或一列,直到某一輪被選取的數就是target
或者已經淘汰得只剩下一個數的時候就必定能得出結果了,所以時間複雜度爲被淘汰掉的行數和列數之和,即O(M + N)
,通過分析後不難寫出以下代碼:緩存
public boolean Find(int target, int [][] arr) {
//input check
if(arr == null || arr.length == 0 || arr[0] == null || arr[0].length == 0){
return false;
}
int i = 0, j = arr[0].length - 1;
while(i != arr.length - 1 && j != 0){
if(target > arr[i][j]){
i++;
}else if(target < arr[i][j]){
j--;
}else{
return true;
}
}
return target == arr[i][j];
}
複製代碼
值得注意的是每次選取的數都是第一行最後一個數,所以前提是第一行有數,那麼就對應着輸入檢查的arr[0] == null || arr[0].length == 0
,這點比較容易忽略。數據結構
總結:通過分析其實不難發現,此題是在一維有序數組使用二分查找元素的一個變種,咱們應該充分利用數據自己的規律性來尋找解題思路。app
請實現一個函數,將一個字符串中的每一個空格替換成「%20」。例如,當字符串爲We Are Happy.則通過替換以後的字符串爲We%20Are%20Happy。dom
public String replaceSpace(StringBuffer str) {
}
複製代碼
此題考查的是字符串這個數據結構的數組實現(對應的還有鏈表實現)的相關操做。函數
若是可使用API
,那麼能夠很容易地寫出以下代碼:
public String replaceSpace(StringBuffer str) {
//input check
//null pointer
if(str == null){
return null;
}
//empty str or not exist blank
if(str.length() == 0 || str.indexOf(" ") == -1){
return str.toString();
}
for(int i = 0 ; i < str.length() ; i++){
if(str.charAt(i) == ' '){
str.replace(i, i + 1, "%20");
}
}
return str.toString();
}
複製代碼
可是若是面試官告訴咱們不準使用封裝好的替換函數,那麼目的就是在考查咱們對字符串數組實現方式的相關操做。因爲是連續空間存儲,所以須要在建立實例時指定大小,因爲每一個空格都使用%20
替換,所以替換以後的字符串應該比原串多出空格數 * 2
個長度,實現以下:
public String replaceSpace(StringBuffer str) {
//input check
//null pointer
if(str == null){
return null;
}
//empty str or not exist blank
if(str.length() == 0 || str.indexOf(" ") == -1){
return str.toString();
}
char[] source = str.toString().toCharArray();
int blankCount = 0;
for(int i = 0 ; i < source.length ; i++){
blankCount = (source[i] == ' ') ? blankCount + 1 : blankCount;
}
char[] dest = new char[source.length + blankCount * 2];
for(int i = source.length - 1, j = dest.length - 1 ; i >=0 && j >=0 ; i--, j--){
if(source[i] == ' '){
dest[j--] = '0';
dest[j--] = '2';
dest[j] = '%';
continue;
}else{
dest[j] = source[i];
}
}
return new String(dest);
}
複製代碼
若是還要求不能有額外空間,那咱們就要考慮如何複用輸入的字符串,若是咱們從前日後遇到空格就將空格及其以後的兩個位置替換爲%20
,勢必會覆蓋空格以後的兩個字符,好比hello world
會被替換成hello%20rld
,所以咱們須要在長度被擴展後的新串中從後往前肯定每一個索引上的字符。好比使用一個originalIndex
指向原串中的最後一個字符索引,使用newIndex
指向新串的最後一個索引,每次將originalIndex
上的字符複製到newIndex
上而且兩個指針前移,若是originalIndex
上的字符是空格,則將newIndex
依次填充0,2,%
,而後二者再前移,直到二者都到首索引位置。
public String replaceSpace(StringBuffer str) {
//input check
//null pointer
if(str == null){
return null;
}
//empty str or not exist blank
if(str.length() == 0 || str.indexOf(" ") == -1){
return str.toString();
}
int blankCount = 0;
for(int i = 0 ; i < str.length() ; i++){
blankCount = (str.charAt(i) == ' ') ? blankCount + 1 : blankCount;
}
int originalIndex = str.length() - 1, newIndex = str.length() - 1 + blankCount * 2;
str.setLength(newIndex + 1); //須要從新設置一下字符串的長度,不然會報越界錯誤
while(originalIndex >= 0 && newIndex >= 0){
if(str.charAt(originalIndex) == ' '){
str.setCharAt(newIndex--, '0');
str.setCharAt(newIndex--, '2');
str.setCharAt(newIndex, '%');
}else{
str.setCharAt(newIndex, str.charAt(originalIndex));
}
originalIndex--;
newIndex--;
}
return str.toString();
}
複製代碼
總結:要把思惟打開,對於數組的操做咱們習慣性的以
for(int i = 0 ; i < arr.length ; i++)
的形式從頭至尾來操做數組,可是不要忽略了從尾到頭遍歷也有它的獨到之處。
輸入一個鏈表,反轉鏈表後,輸出新鏈表的表頭。
public ListNode ReverseList(ListNode head) {
}
複製代碼
此題的難點在於沒法經過一個單鏈表結點獲取其前驅結點,所以咱們不只要在反轉指針以前保存當前結點的前驅結點,還要保存當前結點的後繼結點,並在下一次反轉以前更新這兩個指針。
/* public class ListNode { int val; ListNode next = null; ListNode(int val) { this.val = val; } }*/
public ListNode ReverseList(ListNode head) {
if(head == null || head.next == null){
return head;
}
ListNode pre = null, p = head, next;
while(p != null){
next = p.next;
p.next = pre;
pre = p;
p = next;
}
return pre;
}
複製代碼
輸入一個鏈表,按鏈表值從尾到頭的順序返回一個ArrayList。
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
}
複製代碼
此題的難點在於單鏈表只有指向後繼結點的指針,所以咱們沒法經過當前結點獲取前驅結點,所以不要妄想先遍歷一遍鏈表找到尾結點而後再依次從後往前打印。
因爲咱們一般是從頭至尾遍歷鏈表的,而題目要求從尾到頭打印結點,這與前進後出的邏輯是相符的,所以你可使用一個棧來保存遍歷時走過的結點,再經過後進先出的特性實現從尾到頭打印結點,可是咱們也能夠利用遞歸來幫咱們壓棧,因爲遞歸簡潔不易出錯,所以面試中能用遞歸儘可能用遞歸:只要當前結點不爲空,就遞歸遍歷後繼結點,當後繼結點爲空時,遞歸結束,在遞歸回溯時將「當前結點」依次添加到集合中
/** * public class ListNode { * int val; * ListNode next = null; * * ListNode(int val) { * this.val = val; * } * } * */
import java.util.ArrayList;
public class Solution {
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
ArrayList<Integer> res = new ArrayList();
//input check
if(listNode == null){
return res;
}
recursively(res, listNode);
return res;
}
public void recursively(ArrayList<Integer> res, ListNode node){
//base case
if(node == null){
return;
}
//node not null
recursively(res, node.next);
res.add(node.val);
return;
}
}
複製代碼
還有一種方法就是將鏈表指針都反轉,這樣將反轉後的鏈表從頭至尾打印就是結果了。須要注意的是咱們不該該在訪問用戶數據時更改存儲數據的結構,所以最後要記得反轉回來:
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
ArrayList<Integer> res = new ArrayList();
//input check
if(listNode == null){
return res;
}
return unrecursively(listNode);
}
public ArrayList<Integer> unrecursively(ListNode node){
ArrayList<Integer> res = new ArrayList<Integer>();
ListNode newHead = reverse(node);
ListNode p = newHead;
while(p != null){
res.add(p.val);
p = p.next;
}
reverse(newHead);
return res;
}
public ListNode reverse(ListNode node){
ListNode pre = null, cur = node, next;
while(cur != null){
//save predecessor
next = cur.next;
//reverse pointer
cur.next = pre;
//move to next
pre = cur;
cur = next;
}
//cur is null
return pre;
}
複製代碼
總結:面試時能用遞歸就用遞歸,固然了若是面試官就是要考查你的指針功底那你也能
just so so
不是
輸入某二叉樹的前序遍歷和中序遍歷的結果,請重建出該二叉樹。假設輸入的前序遍歷和中序遍歷的結果中都不含重複的數字。例如輸入前序遍歷序列{1,2,4,7,3,5,6,8}和中序遍歷序列{4,2,7,1,5,3,8,6},則重建二叉樹並返回。
public TreeNode reConstructBinaryTree(int [] pre,int [] in) {
}
複製代碼
先序序列的特色是第一個數就是根結點然後是左子樹的先序序列和右子樹的先序序列,而中序序列的特色是先是左子樹的中序序列,而後是根結點,最後是右子樹的中序序列。所以咱們能夠經過先序序列獲得根結點,而後經過在中序序列中查找根結點的索引從而獲得左子樹和右子樹的結點數。而後能夠將兩序列都一分爲三,對於其中的根結點可以直接重建,而後根據對應子序列分別遞歸重建根結點的左子樹和右子樹。這是一個典型的將複雜問題劃分紅子問題分步解決的過程。
遞歸體的定義,如上圖先序序列的左子樹序列是2,3,4
對應下標1,2,3
,而中序序列的左子樹序列是3,2,4
對應下標0,1,2
,所以遞歸體接收的參數除了保存兩個序列的數組以外,還須要指明須要遞歸重建的子序列分別在兩個數組中的索引範圍:TreeNode rebuild(int[] pre, int i, int j, int[] in, int m, int n)
。而後遞歸體根據pre
的i~j
索引範圍造成的先序序列和in
的m~n
索引範圍造成的中序序列重建一棵樹並返回根結點。
首先根結點就是先序序列的第一個數,即pre[i]
,所以TreeNode root = new TreeNode(pre[i])
能夠直接肯定,而後經過在in
的m~n
中查找出pre[i]
的索引index
能夠求得左子樹結點數leftNodes = index - m
,右子樹結點數rightNodes = n - index
,若是左(右)子樹結點數爲0則代表左(右)子樹爲null
,不然經過root.left = rebuild(pre, i' ,j' ,in ,m' ,n')
來重建左(右)子樹便可。
這個題的難點也就在這裏,即i',j',m',n'
的值的肯定,筆者曾在此困惑許久,建議經過leftNodes,rightNodes
和i,j,m,n
來肯定:(這個時候了前往不要在腦子裏面想這些下標對應關係!!必定要在紙上畫,確保準確性和歸納性)
因而容易得出以下代碼:
if(leftNodes == 0){
root.left = null;
}else{
root.left = rebuild(pre, i + 1, i + leftNodes, in, m, m + leftNodes - 1);
}
if(rightNodes == 0){
root.right = null;
}else{
root.right = rebuild(pre, i + leftNodes + 1, j, in, n - rightNodes + 1, n);
}
複製代碼
筆者曾以中序序列的根節點索引來肯定i',j',m',n'
的對應關係寫出以下錯誤代碼:
if(leftNodes == 0){
root.left = null;
}else{
root.left = rebuild(pre, i + 1, index, in, m, index - 1);
}
if(rightNodes == 0){
root.right = null;
}else{
root.right = rebuild(pre, index + 1, j, in, index + 1, n);
}
複製代碼
這種對應關係乍一看沒錯,可是不具備歸納性(即囊括全部狀況),好比對序列2,3,4
、3,2,4
重建時:
你看這種狀況,上述錯誤代碼還適用嗎?緣由就在於index
是在in
的m~n
中選取的,與數組in
是綁定的,和pre
沒有直接的關係,所以若是用index
來表示i',j'
天然是不合理的。
此題的正確完整代碼以下:
/** * Definition for binary tree * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode(int x) { val = x; } * } */
public class Solution {
public TreeNode reConstructBinaryTree(int [] pre,int [] in) {
if(pre == null || in == null || pre.length == 0 || in.length == 0 || pre.length != in.length){
return null;
}
return rebuild(pre, 0, pre.length - 1, in, 0, in.length - 1);
}
public TreeNode rebuild(int[] pre, int i, int j, int[] in, int m, int n){
int rootVal = pre[i], index = findIndex(rootVal, in, m, n);
if(index < 0){
return null;
}
int leftNodes = index - m, rightNodes = n - index;
TreeNode root = new TreeNode(rootVal);
if(leftNodes == 0){
root.left = null;
}else{
root.left = rebuild(pre, i + 1, i + leftNodes, in, m, m + leftNodes - 1);
}
if(rightNodes == 0){
root.right = null;
}else{
root.right = rebuild(pre, i + leftNodes + 1, j, in, n - rightNodes + 1, n);
}
return root;
}
public int findIndex(int target, int arr[], int from, int to){
for(int i = from ; i <= to ; i++){
if(arr[i] == target){
return i;
}
}
return -1;
}
}
複製代碼
總結:
- 對於複雜問題,必定要劃分紅若干子問題,逐一求解。好比二叉樹問題,咱們一般將其劃分紅頭結點、左子樹、右子樹。
- 對於遞歸過程的參數對應關係,儘可能使用和數據樣本自己沒有直接關係的變量來表示。好比此題應該選取
leftNodes
和rightNodes
來計算i',j',m',n'
而不該該使用頭結點在中序序列的下標index
(它和in
是綁定的,那麼可能對pre
就不適用了)。
用兩個棧來實現一個隊列,完成隊列的Push和Pop操做。 隊列中的元素爲int類型。
Stack<Integer> stack1 = new Stack<Integer>();
Stack<Integer> stack2 = new Stack<Integer>();
public void push(int node) {
}
public int pop() {
}
複製代碼
這道題只要記住如下幾點便可:
stack1
)只能用來存,另外一個棧(如stack2
)只能用來取stack2
是否爲空,若是不空直接stack2.pop()
,不然將stack1
中的元素所有倒入stack2
,若是倒入以後stack2
仍爲空則須要拋異常,不然stack2.pop()
。代碼示例以下:
import java.util.Stack;
public class Solution {
Stack<Integer> stack1 = new Stack<Integer>();
Stack<Integer> stack2 = new Stack<Integer>();
public void push(int node) {
stack1.push(node);
}
public int pop() {
if(stack2.empty()){
while(!stack1.empty()){
stack2.push(stack1.pop());
}
}
if(stack2.empty()){
throw new IllegalStateException("no more element!");
}
return stack2.pop();
}
}
複製代碼
總結:只要取元素的棧不爲空,取元素時直接彈出其棧頂元素便可,只有當其爲空時才考慮將存元素的棧倒入進來,而且要一次性倒完。
把一個數組最開始的若干個元素搬到數組的末尾,咱們稱之爲數組的旋轉。 輸入一個非減排序的數組的一個旋轉,輸出旋轉數組的最小元素。 例如數組{3,4,5,1,2}爲{1,2,3,4,5}的一個旋轉,該數組的最小值爲1。 NOTE:給出的全部元素都大於0,若數組大小爲0,請返回0。
public int minNumberInRotateArray(int [] arr) {
}
複製代碼
此題需先認真審題:
由此不可貴出以下input check
:
public int minNumberInRotateArray(int [] arr) {
//input check
if(arr == null || arr.length == 0){
return 0;
}
//if only one element or no rotate
if(arr.length == 1 || arr[0] < arr[arr.length - 1]){
return arr[0];
}
//TODO
}
複製代碼
上述的arr[0] < arr[arr.length - 1]
不能寫成arr[0] <= arr[arr.length - 1]
,好比可能會有[1,2,3,3,4] -> [3,4,1,2,3]
的狀況,這時你不能返回arr[0]=3
。
若是走到了程序中的TODO
,就能夠考慮廣泛狀況下的推敲,數組能夠被分紅兩部分:大於等於arr[0]
的左半部分和小於等於arr[arr.length - 1]
右半部分,咱們不妨藉助兩個指針從數組的頭、尾向中間靠近,這樣就能利用二分的思想快速移動指針從而淘汰一些不在考慮範圍以內的數。
如圖,咱們不能直接經過arr[mid]
和arr[l]
(或arr[r]
)的比較(arr[mid] >= arr[l]
)來決定移動l
仍是r
到mid
上,由於數組可能存在若干相同且相鄰的數,所以咱們還須要加上一個限制條件:arr[l + 1] >= arr[l] && arr[mid] >= arr[l]
(對於r
來講則是arr[r - 1] <= arr[r] && arr[mid] <= arr[r]
),即當左半部分(右半部分)不止一個數時,咱們纔可能去移動l
(r
)指針。完整代碼以下:
import java.util.ArrayList;
public class Solution {
public int minNumberInRotateArray(int [] arr) {
//input check
if(arr == null || arr.length == 0){
return 0;
}
//if only one element or no rotate
if(arr.length == 1 || arr[0] < arr[arr.length - 1]){
return arr[0];
}
//has rotate, left part is big than right part
int l = 0, r = arr.length - 1, mid;
//l~r has more than 3 elements
while(r > l && r - l != 1){
//r-l >= 2 -> mid > l
mid = l + ((r - l) >> 1);
if(arr[l + 1] >= arr[l] && arr[mid] >= arr[l]){
l = mid;
}else{
r = mid;
}
}
return arr[r];
}
}
複製代碼
總結:審題時要充分考慮數據樣本的極端狀況,以寫出魯棒性較強的代碼。
你們都知道斐波那契數列,如今要求輸入一個整數n,請你輸出斐波那契數列的第n項(從0開始,第0項爲0)。n<=39
public int Fibonacci(int n) {
}
複製代碼
對於公式f(n) = f(n-1) + f(n-2)
,明顯就是一個遞歸調用,所以根據f(0) = 0
和f(1) = 1
咱們不難寫出以下代碼:
public int Fibonacci(int n) {
if(n == 0 || n == 1){
return n;
}
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
複製代碼
在上述遞歸過程當中,你會發現有不少計算過程是重複的:
動態規劃就在使用遞歸調用自上而下分析過程當中發現有不少重複計算的子過程,因而採用自下而上的方式將每一個子狀態緩存下來,這樣對於上層而言只有當須要的子過程結果不在緩存中時纔會計算一次,所以每一個子過程都只會被計算一次。
public int Fibonacci(int n) {
if(n == 0 || n == 1){
return n;
}
//n1 -> f(n-1), n2 -> f(n-2)
int n1 = 1, n2 = 0;
//從f(2)開始算起
int N = 2, res = 0;
while(N++ <= n){
//每次計算後更新緩存,固然你也可使用一個一維數組保存每次的計算結果,只額外空間複雜度就變爲O(n)了
res = n1 + n2;
n2 = n1;
n1 = res;
}
return res;
}
複製代碼
上述代碼不少人都能寫出來,只是沒有意識到這就是動態規劃。
總結:當你自上而下分析遞歸時發現有不少子過程被重複計算,那麼就應該考慮可否經過自下而上將每一個子過程的計算結果緩存下來。
一隻青蛙一次能夠跳上1級臺階,也能夠跳上2級。求該青蛙跳上一個n級的臺階總共有多少種跳法(前後次序不一樣算不一樣的結果)。
public int JumpFloor(int target) {
}
複製代碼
將複雜問題分解:複雜問題就是不斷地將target
減1或減2(對應跳一級和跳兩級臺階)直到target
變爲1或2(對應只剩下一層或兩層臺階)時咱們可以很容易地得出結果。所以對於當前的青蛙而言,它可以選擇的就是跳一級或跳二級,剩下的臺階有多少種跳法交給子過程來解決:
public int JumpFloor(int target) {
//input check
if(target <= 0){
return 0;
}
//base case
if(target == 1){
return 1;
}
if(target == 2){
return 2;
}
return JumpFloor(target - 1) + JumpFloor(target - 2);
}
複製代碼
你會發現這其實就是一個斐波那契數列,只不過是從f(1) = 1,f(2) = 2
開始的斐波那契數列罷了。天然你也應該可以寫出動態規劃版本。
一隻青蛙一次能夠跳上1級臺階,也能夠跳上2級……它也能夠跳上n級。求該青蛙跳上一個n級的臺階總共有多少種跳法。
本質上仍是分解,只不過上一個是分解成兩步,而這個是分解成n步:
public int JumpFloorII(int target) {
if(target <= 0){
return 0;
}
//base case,當target=0時表示某個分解分支跳完了全部臺階,這個分支就是一種跳法
if(target == 0){
return 1;
}
//本過程要收集的跳法的總數
int res = 0;
for(int i = 1 ; i <= target ; i++){
//本次選擇,選擇跳i階臺階,剩下的臺階交給子過程,每一個選擇就表明一個分解分支
res += JumpFloorII(target - i);
}
return res;
}
複製代碼
這個動態規劃就有一點難度了,首先咱們要肯定緩存目標,斐波那契數列中因爲f(n)
只依賴於f(n-1)
和f(n-2)
所以咱們僅用兩個緩存變量實現了動態規劃,可是這裏f(n)
依賴的是f(0),f(1),f(2),...,f(n-1)
,所以咱們須要經過長度量級爲n
的表緩存前n
個狀態(int arr[] = new int[target + 1]
,arr[target]
表示f(n)
)。而後根據遞歸版本(一般是base case
)肯定哪些狀態的值是能夠直接肯定的,好比由if(target == 0){ return 1 }
可知arr[0] = 1
,從f(N = 1)
開始的全部狀態都須要依賴以前(f(n < N)
)的全部狀態:
int res = 0;
for(int i = 1 ; i <= target ; i++){
res += JumpFloorII(target - i);
}
return res
複製代碼
所以咱們能夠據此自下而上計算出每一個子狀態的值:
public int JumpFloorII(int target) {
if(target <= 0){
return 0;
}
int arr[] = new int[target + 1];
arr[0] = 1;
for(int i = 1 ; i < arr.length ; i++){
for(int j = 0 ; j < i ; j++){
arr[i] += arr[j];
}
}
return arr[target];
}
複製代碼
但這仍不是最優解,由於觀察循環體你會發現,每次f(n)
的計算都要從f(0)
累加到f(n-1)
,咱們徹底能夠將這個累加值緩存起來preSum
,每計算出一次f(N)
以後都將緩存更新爲preSum += f(N)
。如此獲得最優解:
public int JumpFloorII(int target) {
if(target <= 0){
return 0;
}
int arr[] = new int[target + 1];
arr[0] = 1;
int preSum = arr[0];
for(int i = 1 ; i < arr.length ; i++){
arr[i] = preSum;
preSum += arr[i];
}
return arr[target];
}
複製代碼
咱們能夠用2*1
的小矩形橫着或者豎着去覆蓋更大的矩形。請問用n個2*1
的小矩形無重疊地覆蓋一個2*n
的大矩形,總共有多少種方法?
public int RectCover(int target) {
}
複製代碼
有了以前的歷練,咱們能很快的寫出遞歸版本:先豎着放一個或者先橫着放兩個,剩下的交給遞歸處理:
//target 大矩形的邊長,也是剩餘小矩形的個數
public int RectCover(int target) {
if(target <= 0){
return 0;
}
if(target == 1 || target == 2){
return target;
}
return RectCover(target - 1) + RectCover(target - 2);
}
複製代碼
這仍然是個以f(1)=1,f(2)=2
開頭的斐波那契數列:
//target 大矩形的邊長,也是剩餘小矩形的個數
public int RectCover(int target) {
if(target <= 0){
return 0;
}
if(target == 1 || target == 2){
return target;
}
//n_1->f(n-1), n_2->f(n-2),從f(N=3)開始算起
int n_1 = 2, n_2 = 1, N = 3, res = 0;
while(N++ <= target){
res = n_1 + n_2;
n_2 = n_1;
n_1 = res;
}
return res;
}
複製代碼
輸入一個整數,輸出該數二進制表示中1的個數。其中負數用補碼錶示。
public int NumberOf1(int n) {
}
複製代碼
題目已經給咱們下降了難度:負數用補碼(取反加1)表示代表輸入的參數爲均爲正數,咱們只需統計其二進制表示中1的個數、運算時只考慮無符號移位便可。
典型的判斷某個二進制位上是否爲1的方法是將該二進制數右移至該二進制位爲最低位而後與1相與&
,因爲1的二進制表示中只有最低位爲1其他位均爲0,所以相與後的結果與該二進制位上的數相同。據此不難寫出以下代碼:
public int NumberOf1(int n) {
int count = 0;
for(int i = 0 ; i < 32 ; i++){
count += ((n >> i) & 1);
}
return count;
}
複製代碼
固然了,還有一種比較秀的解法就是利用n = n & (n - 1)
將n
的二進制位中爲1的最低位置爲0(只要n
不爲0就說明含有二進位制爲1的位,如此這樣的操做能作多少次就說明有多少個二進制位爲1的位):
public int NumberOf1(int n) {
int count = 0;
while(n != 0){
count++;
n &= (n - 1);
}
return count;
}
複製代碼
給定一個double類型的浮點數base和int類型的整數exponent。求base的exponent次方。
public double Power(double base, int exponent) {
}
複製代碼
這是一道充滿危險色彩的題,求職者可能會心裏竊喜不假思索的寫出以下代碼:
public double Power(double base, int exponent) {
double res = 1;
for(int i = 1 ; i <= exponent ; i++){
res *= base;
}
return res;
}
複製代碼
可是你有沒有想過底數base
和冪exponent
都是可正、可負、可爲0的。若是冪爲負數,那麼底數就不能爲0,不然應該拋出算術異常:
//是不是負數
boolean minus = false;
//若是存在分母
if(exponent < 0){
minus = true;
exponent = -exponent;
if(base == 0){
throw new ArithmeticException("/ by zero");
}
}
複製代碼
若是冪爲0,那麼根據任何不爲0的數的0次方爲1,0的0次方未定義,應該有以下判斷:
//若是指數爲0
if(exponent == 0){
if(base != 0){
return 1;
}else{
throw new ArithmeticException("0^0 is undefined");
}
}
複製代碼
剩下的就是計算乘方結果,可是不要忘了若是冪爲負須要將結果取倒數:
//指數不爲0且分母也不爲0,正常計算並返回整數或分數
double res = 1;
for(int i = 1 ; i <= exponent ; i++){
res *= base;
}
if(minus){
return 1/res;
}else{
return res;
}
複製代碼
也許你還能夠錦上添花爲冪乘方的計算引入二分計算(當冪爲偶數時2^n = 2^(n/2) * 2^(n/2)
):
public double binaryPower(double base, int exp){
if(exp == 1){
return base;
}
double res = 1;
res *= (binaryPower(base, exp/2) * binaryPower(base, exp/2));
return exp % 2 == 0 ? res : res * base;
}
複製代碼
輸入一個整數數組,實現一個函數來調整該數組中數字的順序,使得全部的奇數位於數組的前半部分,全部的偶數位於數組的後半部分,並保證奇數和奇數,偶數和偶數之間的相對位置不變。
public void reOrderArray(int [] arr) {
}
複製代碼
讀題以後發現這個跟快排的partition
思路很像,都是選取一個比較基準將數組分紅兩部分,固然你也能夠以arr[i] % 2 == 0
爲基準將奇數放前半部分,將偶數放有半部分,可是雖然只需O(n)
的時間複雜度但不能保證調整後奇數之間、偶數之間的相對位置:
public void reOrderArray(int [] arr) {
if(arr == null || arr.length == 0){
return;
}
int odd = -1;
for(int i = 0 ; i < arr.length ; i++){
if(arr[i] % 2 == 1){
swap(arr, ++odd, i);
}
}
}
public void swap(int[] arr, int i, int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
複製代碼
涉及到排序穩定性,咱們天然可以想到插入排序,從數組的第二個元素開始向後依次肯定每一個元素應處的位置,肯定的邏輯是:將該數與前一個數比較,若是比前一個數小則與前一個數交換位置並在交換位置後繼續與前一個數比較直到前一個數小於等於該數或者已達數組首部中止。
此題不過是將比較的邏輯由數值的大小改成:當前的數是不是奇數而且前一個數是偶數,是則遞歸向前交換位置。代碼示例以下:
public void reOrderArray(int [] arr) {
if(arr == null || arr.length == 0){
return;
}
int odd = -1;
for(int i = 1 ; i < arr.length ; i++){
for(int j = i ; j >= 1 ; j--){
if(arr[j] % 2 == 1 && arr[j - 1] % 2 == 0){
swap(arr, j, j - 1);
}
}
}
}
複製代碼
輸入一個鏈表,輸出該鏈表中倒數第k個結點。
public ListNode FindKthToTail(ListNode head,int k) {
}
複製代碼
倒數,這又是一個從尾到頭的遍歷邏輯,而鏈表對從尾到頭遍歷是敏感的,前面咱們有經過壓棧/遞歸、反轉鏈表的方式實現這個遍歷邏輯,天然對於此題一樣適用,可是那樣未免太麻煩了,咱們能夠經過兩個間距爲(k-1)個結點的鏈表指針來達到此目的。
public ListNode FindKthToTail(ListNode head,int k) {
//input check
if(head == null || k <= 0){
return null;
}
ListNode tmp = new ListNode(0);
tmp.next = head;
ListNode p1 = tmp, p2 = tmp;
while(k > 0 && p1.next != null){
p1 = p1.next;
k--;
}
//length < k
if(k != 0){
return null;
}
while(p1 != null){
p1 = p1.next;
p2 = p2.next;
}
tmp = null; //help gc
return p2;
}
複製代碼
這裏使用了一個技巧,就是建立一個臨時結點tmp
做爲兩個指針的初始指向,以模擬p1
先走k
步以後,p2
纔開始走,沒走時停留在初始位置的邏輯,有利於幫咱們梳理指針在對應位置上的意義,這樣當p1
走到頭時(p1=null
),p2
就是倒數第k
個結點。
這裏還有一個坑就是,筆者層試圖爲了簡化代碼將上述的9 ~ 12
行寫成以下偷懶模式而致使排錯許久:
while(k-- > 0 && p1.next != null){
p1 = p1.next;
}
複製代碼
緣由是將k--
寫在while()
中,不管判斷是否經過都會執行k = k - 1
,所以代碼老是會在if(k != 0)
處返回null
,但願讀者不要和筆者同樣粗心。
總結:當遇到複雜的指針操做時,咱們不妨試圖多引入幾個指針或者臨時結點,以方便梳理咱們的思路,增強代碼的邏輯化,這些空間複雜度
O(1)
的操做一般也不會影響性能。
輸入兩個單調遞增的鏈表,輸出兩個鏈表合成後的鏈表,固然咱們須要合成後的鏈表知足單調不減規則。
public ListNode Merge(ListNode list1,ListNode list2) {
}
複製代碼
public ListNode Merge(ListNode list1,ListNode list2) {
if(list1 == null || list2 == null){
return list1 == null ? list2 : list1;
}
ListNode newHead = list1.val < list2.val ? list1 : list2;
ListNode p1 = (newHead == list1) ? list1.next : list1;
ListNode p2 = (newHead == list2) ? list2.next : list2;
ListNode p = newHead;
while(p1 != null && p2 != null){
if(p1.val <= p2.val){
p.next = p1;
p1 = p1.next;
}else{
p.next = p2;
p2 = p2.next;
}
p = p.next;
}
while(p1 != null){
p.next = p1;
p = p.next;
p1 = p1.next;
}
while(p2 != null){
p.next = p2;
p = p.next;
p2 = p2.next;
}
return newHead;
}
複製代碼
輸入兩棵二叉樹A,B,判斷B是否是A的子結構。(ps:咱們約定空樹不是任意一個樹的子結構)
/** public class TreeNode { int val = 0; TreeNode left = null; TreeNode right = null; public TreeNode(int val) { this.val = val; } }*/
public boolean HasSubtree(TreeNode root1,TreeNode root2) {
if(root1 == null || root2 == null){
return false;
}
return process(root1, root2);
}
複製代碼
這是一道典型的分解求解的複雜問題。典型的二叉樹分解:遍歷頭結點、遍歷左子樹、遍歷右子樹。首先按照root1
和root2
的值是否相等劃分爲兩種狀況:
root2.left
也是roo1.left
的子結構(遞歸)、root2.right
也是root1.right
的子結構(遞歸),那麼可返回true
。root2
爲root1.left
的子結構或者root2
爲root1.right
的子結構時,才能返回true
據上述兩點很容易得出以下遞歸邏輯:
if(root1.val == root2.val){
if(process(root1.left, root2.left) && process(root1.right, root2.right)){
return true;
}
}
return process(root1.left, root2) || process(root1.right, root2);
複製代碼
接下來肯定遞歸的終止條件,若是某個子過程root2=null
那麼說明在自上而下的比較過程當中root2
的結點已被羅列比較完了,這時不管root1
是否爲null
,該子過程都應該返回true
:
if(root2 == null){
return true;
}
複製代碼
可是若是root2 != null
而root1 = null
,則應返回false
if(root1 == null && root2 != null){
return false;
}
複製代碼
完整代碼以下:
public class Solution {
public boolean HasSubtree(TreeNode root1,TreeNode root2) {
if(root1 == null || root2 == null){
return false;
}
return process(root1, root2);
}
public boolean process(TreeNode root1, TreeNode root2){
if(root2 == null){
return true;
}
if(root1 == null && root2 != null){
return false;
}
if(root1.val == root2.val){
if(process(root1.left, root2.left) && process(root1.right, root2.right)){
return true;
}
}
return process(root1.left, root2) || process(root1.right, root2);
}
}
複製代碼
操做給定的二叉樹,將其變換爲源二叉樹的鏡像。
public void Mirror(TreeNode root) {
}
複製代碼
由圖可知獲取二叉樹的鏡像就是將原樹的每一個結點的左右孩子交換一下位置(這個規律必定要會找),也就是說咱們只需遍歷每一個結點並交換left,right
的引用指向就能夠了,而咱們有成熟的先序遍歷:
public void Mirror(TreeNode root) {
if(root == null){
return;
}
TreeNode tmp = root.left;
root.left = root.right;
root.right = tmp;
Mirror(root.left);
Mirror(root.right);
}
複製代碼
輸入一個矩陣,按照從外向裏以順時針的順序依次打印出每個數字,例如,若是輸入以下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) {
}
複製代碼
只要分析清楚了打印思路(左上角和右下角便可肯定一條打印軌跡)後,此題主要考查條件控制的把握。只要給我一個左上角的點(i,j)
和右下角的點(m,n)
,就能夠將這一圈的打印分解爲四步:
可是若是左上角和右下角的點在一行或一列上那就不必分解,直接打印改行或該列便可,打印的邏輯以下:
public void printEdge(int[][] matrix, int i, int j, int m, int n, ArrayList<Integer> res){
if(i == m && j == n){
res.add(matrix[i][j]);
return;
}
if(i == m || j == n){
//only one while will be execute
while(i < m){
res.add(matrix[i++][j]);
}
while(j < n){
res.add(matrix[i][j++]);
}
res.add(matrix[m][n]);
return;
}
int p = i, q = j;
while(q < n){
res.add(matrix[p][q++]);
}
//q == n
while(p < m){
res.add(matrix[p++][q]);
}
//p == m
while(q > j){
res.add(matrix[p][q--]);
}
//q == j
while(p > i){
res.add(matrix[p--][q]);
}
//p == i
}
複製代碼
接着咱們將每一個圈的左上角和右下角傳入該函數便可:
public ArrayList<Integer> printMatrix(int [][] matrix) {
ArrayList<Integer> res = new ArrayList<Integer>();
if(matrix == null || matrix.length == 0 || matrix[0] == null || matrix[0].length == 0){
return res;
}
int i = 0, j = 0, m = matrix.length - 1, n = matrix[0].length - 1;
while(i <= m && j <= n){
printEdge(matrix, i++, j++, m--, n--, res);
}
return res;
}
複製代碼
定義棧的數據結構,請在該類型中實現一個可以獲得棧中所含最小元素的min函數(時間複雜度應爲O(1))。
public class Solution {
public void push(int node) {
}
public void pop() {
}
public int top() {
}
public int min() {
}
}
複製代碼
最直接的思路是使用一個變量保存棧中現有元素的最小值,但這隻對只存不取的棧有效,當彈出的值不是最小值時還沒什麼影響,但當彈出最小值後咱們就沒法獲取當前棧中的最小值。解決思路是使用一個最小值棧,棧頂老是保存當前棧中的最小值,每次數據棧存入數據時最小值棧就要相應的將存入後的最小值壓入棧頂:
private Stack<Integer> dataStack = new Stack();
private Stack<Integer> minStack = new Stack();
public void push(int node) {
dataStack.push(node);
if(!minStack.empty() && minStack.peek() < node){
minStack.push(minStack.peek());
}else{
minStack.push(node);
}
}
public void pop() {
if(!dataStack.empty()){
dataStack.pop();
minStack.pop();
}
}
public int top() {
if(!dataStack.empty()){
return dataStack.peek();
}
throw new IllegalStateException("stack is empty");
}
public int min() {
if(!dataStack.empty()){
return minStack.peek();
}
throw new IllegalStateException("stack is empty");
}
複製代碼
輸入兩個整數序列,第一個序列表示棧的壓入順序,請判斷第二個序列是否可能爲該棧的彈出順序。假設壓入棧的全部數字均不相等。例如序列1,2,3,4,5是某棧的壓入順序,序列4,5,3,2,1是該壓棧序列對應的一個彈出序列,但4,3,5,1,2就不多是該壓棧序列的彈出序列。(注意:這兩個序列的長度是相等的)
public boolean IsPopOrder(int [] arr1,int [] arr2) {
}
複製代碼
可使用兩個指針i,j
,初始時i
指向壓入序列的第一個,j
指向彈出序列的第一個,試圖將壓入序列按照順序壓入棧中:
arr1[i] != arr2[j]
,那麼將arr1[i]
壓入棧中並後移i
(表示arr1[i]
還沒到該它彈出的時刻)i
以後發現arr1[i] == arr2[j]
,那麼說明此刻的arr1[i]
被壓入後應該被當即彈出纔會產生給定的彈出序列,因而不壓入arr1[i]
(表示壓入並彈出了)並後移i
,j
也要後移(表示彈出序列的arr2[j]
記錄已產生,接着產生或許的彈出記錄便可)。i
,所以循環的終止條件是i
到達arr1.length
,此時若棧中還有元素,那麼從棧頂到棧底造成的序列必須與arr2
中j
以後的序列相同才能返回true
。public boolean IsPopOrder(int [] arr1,int [] arr2) {
//input check
if(arr1 == null || arr2 == null || arr1.length != arr2.length || arr1.length == 0){
return false;
}
Stack<Integer> stack = new Stack();
int length = arr1.length;
int i = 0, j = 0;
while(i < length && j < length){
if(arr1[i] != arr2[j]){
stack.push(arr1[i++]);
}else{
i++;
j++;
}
}
while(j < length){
if(arr2[j] != stack.peek()){
return false;
}else{
stack.pop();
j++;
}
}
return stack.empty() && j == length;
}
複製代碼
從上往下打印出二叉樹的每一個節點,同層節點從左至右打印。
public ArrayList<Integer> PrintFromTopToBottom(TreeNode root) {
}
複製代碼
使用一個隊列來保存當前遍歷結點的孩子結點,首先將根節點加入隊列中,而後進行隊列非空循環:
public ArrayList<Integer> PrintFromTopToBottom(TreeNode root) {
ArrayList<Integer> res = new ArrayList<Integer>();
if(root == null){
return res;
}
LinkedList<TreeNode> queue = new LinkedList();
queue.addLast(root);
while(queue.size() > 0){
TreeNode node = queue.pollFirst();
res.add(node.val);
if(node.left != null){
queue.addLast(node.left);
}
if(node.right != null){
queue.addLast(node.right);
}
}
return res;
}
複製代碼
輸入一個整數數組,判斷該數組是否是某二叉搜索樹的後序遍歷的結果。若是是則輸出Yes,不然輸出No。假設輸入的數組的任意兩個數字都互不相同。
public boolean VerifySquenceOfBST(int [] sequence) {
}
複製代碼
對於二叉樹的後序序列,咱們可以肯定最後一個數就是根結點,還能肯定的是前一半部分是左子樹的後序序列,後一部分是右子樹的後序序列。
遇到這種複雜問題,咱們仍能採用三步走戰略(根結點、左子樹、右子樹):
據此咱們須要定義一個遞歸體,該遞歸體須要收集的信息以下:下層須要向我返回其最大值、最小值、以及是不是BST
class Info{
boolean isBST;
int max;
int min;
Info(boolean isBST, int max, int min){
this.isBST = isBST;
this.max = max;
this.min = min;
}
}
複製代碼
遞歸體的定義以下:
public Info process(int[] arr, int start, int end){
if(start < 0 || end > arr.length - 1 || start > end){
throw new IllegalArgumentException("invalid input");
}
//base case : only one node
if(start == end){
return new Info(true, arr[end], arr[end]);
}
int root = arr[end];
Info left, right;
//not exist left child
if(arr[start] > root){
right = process(arr, start, end - 1);
return new Info(root < right.min && right.isBST,
Math.max(root, right.max), Math.min(root, right.min));
}
//not exist right child
if(arr[end - 1] < root){
left = process(arr, start, end - 1);
return new Info(root > left.max && left.isBST,
Math.max(root, left.max), Math.min(root, left.min));
}
int l = 0, r = end - 1;
while(r > l && r - l != 1){
int mid = l + ((r - l) >> 1);
if(arr[mid] > root){
r = mid;
}else{
l = mid;
}
}
left = process(arr, start, l);
right = process(arr, r, end - 1);
return new Info(left.isBST && right.isBST && root > left.max && root < right.min,
right.max, left.min);
}
複製代碼
總結:二叉樹相關的信息收集問題分步走:
- 分析當前狀態須要收集的信息
- 根據下層傳來的信息加工出當前狀態的信息
- 肯定遞歸終止條件
輸入一顆二叉樹的跟節點和一個整數,打印出二叉樹中結點值的和爲輸入整數的全部路徑。路徑定義爲從樹的根結點開始往下一直到葉結點所通過的結點造成一條路徑。(注意: 在返回值的list中,數組長度大的數組靠前)
public ArrayList<ArrayList<Integer>> FindPath(TreeNode root,int target) {
}
複製代碼
審題可知,咱們須要有一個自上而下從根結點到每一個葉子結點的遍歷思路,而先序遍歷恰好能夠拿來用,咱們只需在來到當前結點時將當前結點值加入到棧中,在離開當前結點時再將棧中保存的當前結點的值彈出便可使用棧模擬保存自上而下通過的結點,從而實如今來到每一個葉子結點時只需判斷棧中數值之和是否爲target
便可。
public ArrayList<ArrayList<Integer>> FindPath(TreeNode root,int target) {
ArrayList<ArrayList<Integer>> res = new ArrayList();
if(root == null){
return res;
}
Stack<Integer> stack = new Stack<Integer>();
preOrder(root, stack, 0, target, res);
return res;
}
public void preOrder(TreeNode root, Stack<Integer> stack, int sum, int target, ArrayList<ArrayList<Integer>> res){
if(root == null){
return;
}
stack.push(root.val);
sum += root.val;
//leaf node
if(root.left == null && root.right == null && sum == target){
ArrayList<Integer> one = new ArrayList();
one.addAll(stack);
res.add(one);
}
preOrder(root.left, stack, sum, target, res);
preOrder(root.right, stack, sum, target, res);
sum -= stack.pop();
}
複製代碼
輸入一個複雜鏈表(每一個節點中有節點值,以及兩個指針,一個指向下一個節點,另外一個特殊指針指向任意一個節點),返回結果爲複製後複雜鏈表的head。(注意,輸出結果中請不要返回參數中的節點引用,不然判題程序會直接返回空)
/* public class RandomListNode { int label; RandomListNode next = null; RandomListNode random = null; RandomListNode(int label) { this.label = label; } } */
public class Solution {
public RandomListNode Clone(RandomListNode pHead) {
}
}
複製代碼
此題主要的難點在於random
指針的處理。
能夠將鏈表中的結點都複製一份,用一個哈希表來保存,key
是源結點,value
就是副本結點,而後遍歷key
取出每一個對應的value
將副本結點的next
指針和random
指針設置好:
public RandomListNode Clone(RandomListNode pHead){
if(pHead == null){
return null;
}
HashMap<RandomListNode, RandomListNode> map = new HashMap();
RandomListNode p = pHead;
//copy
while(p != null){
RandomListNode cp = new RandomListNode(p.label);
map.put(p, cp);
p = p.next;
}
//link
p = pHead;
while(p != null){
RandomListNode cp = map.get(p);
cp.next = (p.next == null) ? null : map.get(p.next);
cp.random = (p.random == null) ? null : map.get(p.random);
p = p.next;
}
return map.get(pHead);
}
複製代碼
首先將每一個結點複製一份並插入到對應結點以後,而後遍歷鏈表將副本結點的random
指針設置好,最後將源結點和副本結點分離成兩個鏈表
public RandomListNode Clone(RandomListNode pHead){
if(pHead == null){
return null;
}
RandomListNode p = pHead;
while(p != null){
RandomListNode cp = new RandomListNode(p.label);
cp.next = p.next;
p.next = cp;
p = p.next.next;
}
//more than two node
//link random pointer
p = pHead;
RandomListNode cp;
while(p != null){
cp = p.next;
cp.random = (p.random == null) ? null : p.random.next;
p = p.next.next;
}
//split source and copy
p = pHead;
RandomListNode newHead = p.next;
//p != null -> p.next != null
while(p != null){
cp = p.next;
p.next = p.next.next;
p = p.next;
cp.next = (p == null) ? null : p.next;
}
return newHead;
}
複製代碼