刷了有快三百道 LeetCode 了,聽說找實習必刷《劍指Offer》,於是每天花一點時間把這套題速刷一遍。
如果有和我一樣的菜鳥,咱們可以一起組隊刷題,相互監督打卡哦!!幹就完了!!!奧利給!!!!!
文章目錄
劍指Offer.03 數組中重複的數字 簡單
思路一:HashTable 簡單直白,遍歷數組,用 HashSet 保存遇到過的元素,遇到重複就返回。
//時間複雜度:O(n) 空間複雜度:O(n)
public int findRepeatNumber(int[] nums) {
Set<Integer> set = new HashSet<>();
for (int num : nums) {
if (set.contains(num)) {
return num;
} else {
set.add(num);
}
}
return -1;
}
思路二:抽屜定理 本文數組元素都小於數組長度,一個蘿蔔一個坑,將元素都放回自己的位置,期間遇到重複的元素就返回。類似於 LeetCode 41 題——缺失的第一個正數 。
//時間複雜度:O(n) 空間複雜度:O(1)
public int findRepeatNumber(int[] nums) {
for (int i = 0; i < nums.length; i++) {
while (nums[i] != i) {
if (nums[i] == nums[nums[i]]) return nums[i];
int temp = nums[i];
nums[i] = nums[temp];
nums[temp] = temp;
}
}
return -1;
}
劍指Offer.04 二維數組中的查找 簡單
思路一:雙指針法,每次排除一行或一列 從左下角開始遍歷 i 表示行座標、j 表示列座標,如果遇到等於 target 則返回 true,如果遍歷元素大於 target ,因爲每一行都是增序所以 i-- 排除當前行,如果遍歷元素小於 target ,因爲每一列也都是從上到下增序的,所以 j++ 排除當前列。
//時間複雜度:O(m+n) m行,n列 空間複雜度:O(1)
public boolean findNumberIn2DArray(int[][] matrix, int target) {
if (matrix.length == 0) return false;
int m = matrix.length, n = matrix[0].length;
int i = m - 1, j = 0;
while (i >= 0 && j < n) {
if (matrix[i][j] < target) {
j++;
} else if (matrix[i][j] > target) {
i--;
} else if (matrix[i][j] == target) {
return true;
}
}
return false;
}
劍指Offer.05 替換空格 簡單
思路一:直接遍歷替換 沒什麼好說的遍歷就完了!
//時間複雜度:O(n) 空間複雜度:O(n)
public String replaceSpace(String s) {
StringBuilder res = new StringBuilder();
for (char c : s.toCharArray()) {
if (c != ' ') {
res.append(c);
}else {
res.append("%20");
}
}
return res.toString();
}
劍指Offer.06 從尾到頭打印鏈表 簡單
思路一:利用棧 逆序打印,那就是遍歷節點入棧,再依次出棧,利用棧先進後出的特點。
//時間複雜度:O(n) 空間複雜度:O(n)
public int[] reversePrint(ListNode head) {
Deque<ListNode> stack = new LinkedList<>();
while (head != null) {
stack.push(head);
head = head.next;
}
int[] res = new int[stack.size()];
for (int i = 0; i < res.length; i++) {
res[i] = stack.pop().val;
}
return res;
}
思路二:遞歸,利用方法調用棧 可以不用手動創建一個棧,直接利用遞歸壓棧,觸底返回就好了。
//時間複雜度:O(n) 空間複雜度:O(n)
ArrayList<Integer> list = new ArrayList<>();
public int[] reversePrint(ListNode head) {
recusion(head);
int[] res = new int[list.size()];
for (int i = 0; i < res.length; i++) {
res[i] = list.get(i);
}
return res;
}
private void recusion(ListNode head) {
if (head == null) return;
recusion(head.next);
list.add(head.val);
}
劍指Offer.07 重建二叉樹 中等
思路一:先序找根,中序找到根的左右子樹區間
回想了一下學校老師上課講的如何根據兩個遍歷序列還原出二叉樹的:
- 根據前序序列的第一個字符確定樹的根,示例中的3。
- 知道了3這個根,根據中序序列確定左右子樹[9]是左子樹、[15,20,7]是右子樹。
- 根據左子樹前序序列第一個字符確定樹的根:9。9的左右子樹爲null,左子樹完畢。
- 根據右子樹前序序列第一個字符確定樹的根:20。
- 知道了20這個根,根據中序序列確定左右子樹[15]是左子樹、[7]是右子樹。
- 根據左子樹前序序列第一個字符確定樹的根:15。15的左右子樹爲null,左子樹完畢。
- 根據右子樹前序序列第一個字符確定樹的根:7。7的左右子樹爲null,左子樹完畢。
這個過程就是在利用:前序序列[根左右]根在開頭確定根、中序遍歷[左根右]確定根的左右子樹。
實現的過程其實就是:先根據前序序列確定根,再根據中序序列確定根的左子樹or右子樹,再將左子樹or右子樹對應的前序序列區間和中序序列區間重複前兩步。。。。
明顯是個遞歸結構,遞歸出口是:前序或中序序列的區間爲空。
可以用一個 Map 保存每個中序序列元素的下標,方便直接找到根節點在中序序列的位置,從而計算出左右子樹的區間。如果不用 Map 每次都需要遍歷一下中序序列的區間,才能找到根節點的位置。
//時間複雜度:O(n) 空間複雜度:O(n)
HashMap<Integer, Integer> map = new HashMap<>();
public TreeNode buildTree(int[] preorder, int[] inorder) {
if (preorder.length != inorder.length) return null;
for (int i = 0; i < inorder.length; i++) {
map.put(inorder[i], i);
}
return build(preorder, 0, preorder.length - 1, inorder, 0, inorder.length - 1);
}
private TreeNode build(int[] preorder, int pstart, int pend, int[] inorder, int istart, int iend) {
if (pstart > pend || istart > iend) return null;
if (pstart == pend) return new TreeNode(preorder[pstart]);
TreeNode root = new TreeNode(preorder[pstart]);
int rootIdx = map.get(preorder[pstart]);
int leftOffset = rootIdx - istart, rightOffset = iend - rootIdx;
root.left = build(preorder, pstart + 1, pstart + leftOffset, inorder, istart, rootIdx - 1);
root.right = build(preorder, pend - rightOffset + 1, pend, inorder, rootIdx + 1, iend);
return root;
}
劍指Offer.09 用兩個棧實現隊列 簡單
思路一:保持主棧的順序 每次入棧前都先將主棧(A)中的所有元素都出棧再入棧到輔助棧(B)中,然後新元素就能放到 A棧 的底部,新元素入棧之後再將 B棧 中的元素都出棧再入棧回 A棧。這樣就能保證 A棧 中新入棧的元素都在棧底,最早的元素在棧頂。
public class CQueue {
Deque<Integer> stackA, stackB;
int size;
public CQueue() {
this.stackA = new LinkedList<>();
this.stackB = new LinkedList<>();
this.size = 0;
}
//時間複雜度:O(n)
public void appendTail(int value) {
while (!stackA.isEmpty()) {
stackB.push(stackA.pop());
}
stackA.push(value);
while (!stackB.isEmpty()) {
stackA.push(stackB.pop());
}
size++;
}
//時間複雜度:O(1)
public int deleteHead() {
if (size == 0) {
return -1;
}
size--;
return stackA.pop();
}
}
劍指Offer.10_I 斐波那契數列 簡單
思路一:動態規劃 學習 DP 的經典入門例題,一般會從暴力遞歸到記憶化遞歸最終到 DP,因爲本文只是一個菜鳥的速通日記,介於篇幅就不展開了,直接給出空間壓縮後的 DP 解法,下一題給出 DP 思考過程。
//時間複雜度:O(n) 空間複雜度:O(1)
public int fib(int n) {
int a = 0, b = 1, sum = 0;
for (int i = 1; i <= n; i++) {
sum = (a + b) % 1000000007;
a = b;
b = sum;
}
return a;
}
劍指Offer.10_II 青蛙跳臺階 簡單
思路一:動態規劃 和斐波契數列一樣,區別是斐波那契數列是從 0,1,1,2,3。。。開始的,而本題的數列是從 1,1,2,3。。。開始的。
dp數組含義:dp[i]表示i階有多少中走法。
初始化:dp[0]=1、dp[1]=1,即 0 階 和 1階都只有一種走法。
狀態轉移:dp[i]=dp[i-1]+dp[i-2],第i階是從i-1階一次走一步上來和i-2階一次走兩步上來這兩情況組成的。
//時間複雜度:O(n) 空間複雜度:O(n)
public int climbStairs(int n) {
int[] dp=new int[n+1];
dp[0]=dp[1]=1;
for (int i = 2; i <= n; i++) {
dp[i]=dp[i-1]+dp[i-2];
}
return dp[n];
}
因爲計算 dp[i] 的時候,只考慮 dp[i-1] 和 dp[i-2] 所以用兩個變量記錄這兩個值即可。
//時間複雜度:O(n) 空間複雜度:O(n)
public int numWays(int n) {
int a = 1, b = 1, sum = 0;
for (int i = 0; i < n; i++) {
sum = (a + b) % 1000000007;
a = b;
b = sum;
}
return a;
}
劍指Offer.11 旋轉數組的最小數字 簡單
思路一:二分法 重點在於如何判斷 mid 的條件?
用 nums[mid] 和 nums[right] 進行比較 ,如果 nums[mid] > nums[right]
,說明 mid 的左邊部分一定是一個嚴格升序區間,那麼最小值一定在 mid 右邊的部分;反之 nums[mid] < nums[right]
說明 mid 的右邊是一個嚴格升序全進,最小值一定在 mid 的左邊部分;如果 nums[mid] = nums[right]
,說明發生重複,right-- 跳過這個重複的值,繼續判斷。
搜索的時候,搜索區間不斷地向存在最小值的那一邊收縮,搜索區間選擇左閉右閉區間,搜索停止條件是搜索區間內只剩下一個元素。
//最壞時間複雜度:O(n) 元素全部相同,退化到 O(n)
//平均時間複雜度:O(logn)
public int minArray(int[] numbers) {
int left = 0, right = numbers.length - 1;
//left == right 時停止搜索,此時左閉右閉區間內剩餘一個元素
while (left < right) {
int mid = left + (right - left) / 2;
if (numbers[mid] == numbers[right]) {
//跳過重複
right--;
} else if (numbers[mid] > numbers[right]) {
//此時 mid 一定不是最小值,所以從搜索區間內刪除
left = mid + 1;
} else if (numbers[mid] < numbers[right]) {
right = mid;
}
}
return numbers[left];
}
劍指Offer.12 矩陣中的路徑 中等
思路一:深搜回溯 遍歷board找到等於word第一個字符的元素。然後以這個元素爲開始,向四個方向上分別進行遞歸深搜,如果某個方向上成功匹配整個word則返回true,如果某個元素四個方向都不能匹配成功word的對應字符則返回false。
因爲字符不能重複使用,所以需要一個標記數組對每個字符的使用情況進行標記。也可以採用NO.695島嶼最大面積中的"沉島思想",將用過的字符直接修改爲’.’。
//是否成功搜索word
boolean finished = false;
public boolean exist(char[][] board, String word) {
//遍歷
for (int i = 0; i < board.length; i++) {
for (int j = 0; j < board[0].length; j++) {
//如果等於word第一個字符的元素,以i j 開始進行四個方向深搜
if (board[i][j] == word.charAt(0) && dfs(board, word, i, j, 0)) {
return true;
}
}
}
return false;
}
private boolean dfs(char[][] board, String word, int i, int j, int curr) {
//匹配成功的長度等於word長度,成功搜索
if (curr == word.length()) {
finished = true;
return true;
}
//如果到的邊界或者字符不匹配
if (i < 0 || i >= board.length || j < 0 || j >= board[0].length ||
board[i][j] != word.charAt(curr)) {
return false;
}
//當前未完成匹配,繼續四個方向深搜
if (!finished) {
char c = board[i][j];//保存當前字符,回溯的時候重新填寫回去
board[i][j] = '.';//沉島
boolean down = dfs(board, word, i + 1, j, curr + 1);//向下搜
boolean right = dfs(board, word, i, j + 1, curr + 1);//向右搜
boolean up = dfs(board, word, i - 1, j, curr + 1);//向上搜
boolean left = dfs(board, word, i, j - 1, curr + 1);//向左搜
board[i][j] = c;//回溯重新將字符填回去
return down || right || up || left;
} else {
return true;//已完成
}
}
時間複雜度:O(mn*3^K) 最壞情況:有m*n個起點,每個起點都要遍歷一個 word長度k的方案,除去上一個字符,還有 3 個方向
空間複雜度:O(k)遞歸調用棧不超過 k 層深度
本人菜鳥,有錯誤請告知,感激不盡!