Note
- 題解彙總:劍指offer題解彙總
- 代碼地址:Github 劍指offer Java實現彙總
- 點擊目錄中的題名鏈接可直接食用題解~
- 有些解法博文中未實現,不代表一定很難,可能只是因爲博主太懶```(Orz)
- 如果博文中有明顯錯誤或者某些題目有更加優雅的解法請指出,謝謝~
目錄
題號 | 題目名稱 |
---|---|
1 | 二維數組中的查找 |
2 | 替換空格 |
3 | 從尾到頭打印鏈表 |
4 | 重建二叉樹 |
5 | 用兩個棧實現隊列 |
6 | 旋轉數組的最小數字 |
7 | 斐波那契數列 |
8 | 跳臺階 |
9 | 變態跳臺階 |
10 | 矩形覆蓋 |
正文
1、二維數組中的查找
題目描述
在一個二維數組中(每個一維數組的長度相同),每一行都按照從左到右遞增的順序排序,每一列都按照從上到下遞增的順序排序。請完成一個函數,輸入這樣的一個二維數組和一個整數,判斷數組中是否含有該整數。
題目分析
解法一: 我們可以利用該二維數組的性質:每一行都按照從左到右遞增的順序排序,每一列都按照從上到下遞增的順序排序。也就是說,對於數組左下角的值 m,m 是該行最小的數,同時也是該列最大的數。我們只需要每次將 m 和目標值 target 進行比較:
1、當 m < target 時,由於 m 是當前列的最大元素,想要得到更大的數只能對列進行右移;
2、當 m > target 時,由於 m 是當前行的最小元素,想要得到更小的數只能對行進行上移;
3、當 m = target 時,找到該值,返回 true。
用某行最小或某列最大與 target 比較,每次可剔除一整行或一整列。故時間複雜度是O(m+n),使用兩層循環暴力破解需要時間複雜度爲O(n²)。
代碼實現
解法一:
public boolean Find(int target, int [][] array) {
if (array == null || array.length == 0) return false;
if (array[0].length == 0) return false;
int row = array.length - 1;
int col = 0;
do {
if (array[row][col] < target) {
col++;
} else if (array[row][col] > target) {
row--;
} else {
return true;
}
} while (row >= 0 && col < array[0].length);
return false;
}
2、替換空格
題目描述
請實現一個函數,將一個字符串中的每個空格替換成“%20”。例如,當字符串爲We Are Happy.則經過替換之後的字符串爲We%20Are%20Happy。
題目分析
解法一: 用Java自帶的替換函數,str.toString().replace(" “,”%20")。
解法二: 在當前字符串上進行替換。
1、先計算替換後的字符串需要多大的空間,並對原字符串空間進行擴容;
2、從後往前替換字符串的話,每個字符串只需要移動一次;
3、如果從前往後,每個字符串需要多次移動,效率較低。
解法三: 開闢一個新的字符串。與解法二基本一致,只是不需要對字符進行移動,而是將需要替換的字符直接添加到新的字符串上,需要整個字符串的額外空間。
代碼實現
解法一:
public String replaceSpace(StringBuffer str) {
if (str == null || str.length() == 0) return "";
return str.toString().replace(" ","%20");
}
解法二:
public static String replaceSpace1(StringBuffer str) {
if (str == null || str.length() == 0) return "";
int count = 0;
for (int i = 0; i < str.length(); i++) {
if (str.charAt(i) == ' ') {
count++;
}
}
int oldLen = str.length();
str.setLength(oldLen + 2 * count);
for (int i = oldLen - 1; i >= 0; i--) {
if (str.charAt(i) != ' ') {
str.setCharAt(i + 2 * count, str.charAt(i));
} else {
str.setCharAt(i + 2 * count, '0');
str.setCharAt(i + 2 * count - 1, '2');
str.setCharAt(i + 2 * count - 2, '%');
count--;
}
}
return str.toString();
}
解法三:
public String replaceSpace(StringBuffer str) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if (c == ' ') {
sb.append("%20");
} else {
sb.append(c);
}
}
return sb.toString();
}
3、從尾到頭打印鏈表
題目描述
輸入一個鏈表,按鏈表從尾到頭的順序返回一個ArrayList。
題目分析
解法一: 依次遍歷鏈表並將每個鏈表元素插入到ArrayList中,最後使用Collection.reverse對ArrayList進行翻轉並返回。
解法二: ArrayList中有個方法是add(index,value),可以指定index位置插入value值。依次遍歷鏈表並將每個鏈表元素插入到list的0位置,最後返回ArrayList即可得到逆序鏈表。
解法三: 利用遞歸,藉助系統的棧將元素壓棧,依次出棧並將元素添加至ArrayList中即可完成逆序。
代碼實現
解法一:
public static ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
ListNode head = listNode;
ArrayList<Integer> list = new ArrayList<>();
while (head != null) {
list.add(head.val);
head = head.next;
}
Collections.reverse(list);
return list;
}
解法二:
public static ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
ArrayList<Integer> list = new ArrayList<>();
ListNode tmp = listNode;
while(tmp != null){
list.add(0, tmp.val);
tmp = tmp.next;
}
return list;
}
解法三:
ArrayList<Integer> list = new ArrayList();
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
if (listNode != null) {
printListFromTailToHead(listNode.next);
list.add(listNode.val);
}
return list;
}
4、重建二叉樹
題目描述
輸入某二叉樹的前序遍歷和中序遍歷的結果,請重建出該二叉樹。假設輸入的前序遍歷和中序遍歷的結果中都不含重複的數字。例如輸入前序遍歷序列{1,2,4,7,3,5,6,8}和中序遍歷序列{4,7,2,1,5,3,8,6},則重建二叉樹並返回。
題目分析
解法一:
根據中序遍歷和前序遍歷可以確定二叉樹,具體過程爲:
- 根據前序序列第一個元素確定根結點; 遍歷中序序列,查找前序序列第一個節點在中序序列所在位置;
- 根據根結點在中序序列中的位置分割出左右兩個子序列,分別代表左子樹和右子樹的中序遍歷結果;
- 根據左右子樹的中序遍歷長度,計算出左右子樹的前序遍歷結果;
- 現已獲得左右子樹的前序和中序遍歷結果,對左右子樹按照1-4步驟進行遞歸重構。
例如對於前序遍歷序列{1,2,4,7,3,5,6,8}和中序遍歷序列{4,7,2,1,5,3,8,6}而言,我們的步驟是:
- 根據前序遍歷第一個元素,確定1爲根節點;
- 遍歷中序序列,查找到第四個元素爲根節點1,因此對於根節點1而言,左子樹的中序遍歷序列爲{4,7,2},右子樹的中序遍歷序列爲{5,3,8,6};
- 根據左子樹的中序遍歷長度爲3,可以知道在前序遍歷序列中,{2,4,7}爲左子樹的前序遍歷序列,{3,5,6,8}爲右子樹的前序遍歷序列;
- 根據左子樹的前序和中序遍歷序列分別爲{2,4,7}和{4,7,2},可以按照1-3步驟遞歸重構出一顆樹,並將它作爲1的左子樹;
- 根據右子樹的前序和中序遍歷序列分別爲{3,5,6,8}和{5,3,8,6},可以按照1-3步驟遞歸重構出一顆樹,並將它作爲1的右子樹;
- 直至前序序列或中序序列長度爲0,說明到達葉子節點。返回空,結束遞歸。
代碼實現
解法一:
public TreeNode reConstructBinaryTree(int [] pre, int [] in) {
if (pre.length == 0 || in.length == 0) {
return null;
}
TreeNode h = new TreeNode(pre[0]);
int index = -1;
for (int i = 0; i < in.length; i++) {
if (in[i] == pre[0]) {
index = i;
}
}
int[] in1 = Arrays.copyOfRange(in, 0, index);
int[] in2 = Arrays.copyOfRange(in, index + 1, in.length);
int[] p1 = Arrays.copyOfRange(pre, 1, in1.length + 1);
int[] p2 = Arrays.copyOfRange(pre, in1.length + 1, pre.length);
h.left = reConstructBinaryTree(p1, in1);
h.right = reConstructBinaryTree(p2, in2);
return h;
}
5、用兩個棧實現隊列
題目描述
用兩個棧來實現一個隊列,完成隊列的Push和Pop操作。 隊列中的元素爲int類型。
題目分析
解法一: 定義兩個棧,棧A和棧B。棧A只負責正常的push;當棧B有元素時則直接pop,當棧B沒有元素時,將棧A中的元素全部、一次性地pop到棧B,然後對棧B進行pop。
代碼實現
解法一:
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()) {
return stack2.pop();
} else {
return -1;
}
}
}
6、旋轉數組的最小數字
題目描述
把一個數組最開始的若干個元素搬到數組的末尾,我們稱之爲數組的旋轉。輸入一個非遞減排序的數組的一個旋轉,輸出旋轉數組的最小元素。例如數組{3,4,5,1,2}爲{1,2,3,4,5}的一個旋轉,該數組的最小值爲1。
NOTE:給出的所有元素都大於0,若數組大小爲0,請返回0。
題目分析
解法一: 一次遍歷。數組在發生旋轉後,以最小元素爲中心的左右兩端數組都是升序的,因此我們可以對數組進行遍歷,當某個元素比它前一個元素小,則說明該元素是數組中的最小元素。
解法二: 二分查找。二分查找用於查找有序的數組中的值,題目所給數組在兩段範圍內有序,我們可以將給定數組分爲兩種情況:
- 其實並沒有旋轉,例如 {1,2,3,4,5},旋轉後也是 {1,2,3,4,5},這樣可以直接使用二分查找;
- 如題所示,旋轉了一部分,例如 {1,2,3,4,5},旋轉後爲 {3,4,5,1,2},需要限定特殊條件後使用二分查找。
當數組如情況 1,有個鮮明的特徵,即數組左邊元素 < 數組右邊元素,這時我們直接返回首元素即可;
當數組如情況 2,此時有三種可能找到最小值:
- 下標爲 n+1 的值小於下標爲 n 的值,則下標爲 n+1 的值肯定是最小元素;
- 下標爲 n 的值小於下標爲 n-1 的值,則下標爲 n 的值肯定是最小元素;
- 由於不斷查找,數組查找範圍內的值已經全爲非降序(退化爲情況1)。
再討論每次二分查找時範圍的變化,由於情況數組的情況 1 能直接找到最小值,需要變化範圍的肯定是情況 2:
- 當下標爲 n 的值大於下標爲 0 的值,從 0 到 n 這一段肯定是升序,由於是情況 2,最小值肯定在後半段;
- 當下標爲 n 的值小於下標爲 0 的值,從 0 到 n 這一段不是升序,最小值肯定在這一段。
代碼實現
解法一: O(n)
public int minNumberInRotateArray(int [] array) {
if (array.length == 0) return 0;
int index = 0;
for (int i = 1; i < array.length; i++) {
if (array[i] < array[i - 1]) {
index = i;
break;
}
}
return array[index];
}
解法二: O(logn)
public int minNumberInRotateArray(int [] array) {
if (array.length == 1) return array[0];
int l = 0;
int r = array.length - 1;
while (l < r) {
int m = l + ((r - l) >> 1);
if (array[l] < array[r]) {
return array[l];
}
if (array[m] > array[m + 1]) {
return array[m + 1];
}
if (array[m] < array[m - 1]) {
return array[m];
}
if (array[m] > array[0]) {
l = m + 1;
} else {
r = m - 1;
}
}
return 0;
}
7、斐波那契數列
題目描述
大家都知道斐波那契數列F(n)=F(n-1)+F(n-2),現在要求輸入一個整數n,請你輸出斐波那契數列的第n項(從0開始,第0項爲0,第1項爲1)。n<=39.
題目分析
解法一: 遞歸。根據F(n)=F(n-1)+F(n-2)遞歸調用即可。
解法二: 動態規劃。可以選擇一個長度爲n的數組用於保存每次計算的值,遍歷數組,根據數組之前的元素得到當前元素,在該情況下空間複雜度爲O(n)。由於在遍歷過程中,實際上只用到n-1和n-2兩個位置的值,因此可以只使用兩個變量對歷史的計算結果進行存儲,將空間複雜度優化到O(1)。
代碼實現
解法一: O(2^n)
public int Fibonacci(int n) {
if (n <= 1) return n;
return Fibonacci(n-1) + Fibonacci(n-2);
}
解法一: O(n)
public int Fibonacci(int n) {
if (n == 0) return 0;
if (n <= 2) return 1;
int pre1 = 1;
int pre2 = 1;
int res = 0;
for (int i = 3; i <= n; i++) {
res = pre1 + pre2;
pre1 = pre2;
pre2 = res;
}
return res;
}
8、跳臺階
題目描述
一隻青蛙一次可以跳上1級臺階,也可以跳上2級。求該青蛙跳上一個n級的臺階總共有多少種跳法(先後次序不同算不同的結果)。
題目分析
解法一: 動態規劃。斐波那契數列的變種,上一題的優化解法需要三個額外變量,該解法只需要兩個額外變量,sum用來存儲當前值,pre用來存儲前一個值,但是空間複雜度仍爲O(1)。
代碼實現
解法一: O(n)
public int JumpFloor(int target) {
if (target < 2) return 1;
int pre = 1;
int sum = 1;
for (int i = 2; i <= target; i++) {
sum = sum + pre;
pre = sum - pre;
}
return sum;
}
9、變態跳臺階
題目描述
一隻青蛙一次可以跳上1級臺階,也可以跳上2級……它也可以跳上n級。求該青蛙跳上一個n級的臺階總共有多少種跳法。
題目分析
解法一: 動態規劃。斐波那契數列的變種。由於青蛙每次可以跳n級臺階,因此青蛙跳上n級臺階的跳法爲f(n)=f(n-1)+f(n-2)+……f(1)+f(0)。經過分析,發現結果呈規律性,f(n)以2的n次冪形式增長,f(0)=1,f(1)=1,f(2)=2…從而可以歸納得到計算表達式:f(n)=2^(n-1)。
代碼實現
解法一: O(1)
public int JumpFloorII(int target) {
return target == 0 ? 1 : (int) Math.pow(2, target - 1);
}
10、矩形覆蓋
題目描述
我們可以用2*1的小矩形橫着或者豎着去覆蓋更大的矩形。請問用n個2*1的小矩形無重疊地覆蓋一個2*n的大矩形,總共有多少種方法?
題目分析
解法一: 動態規劃。斐波那契數列的變種。其實和第9題是一樣的,f(n)=f(n-1)+f(n-2)。因爲每次多加1*2的矩形時,無非就是f(n-1)種然後加上一條豎着的矩陣,或者f(n-2)種然後加上兩條橫着的矩陣。在該解法中使用的是遞歸方法求解,時間複雜度極高。在這僅僅是給出遞歸解的實現,建議使用時間複雜度爲O(n)、空間複雜度爲O(1)的最優解法(參考第7-9題)。
代碼實現
解法一: O(2^n)
public int RectCover(int target) {
if (target == 0) return 0;
if (target == 1) return 1;
if (target == 2) return 2;
return RectCover(target - 1) + RectCover(target - 2);
}