本文的題目均來自LeetCode的劍指offer題庫
本文的分類參考自書籍《劍指offer》
代碼均採用Java實現,且大多都是最優解
碼這篇文章的目的是方便自己複習看,所以很多代碼是經過優化的,並且幾乎沒有題解,只是提了提思路。如果第一次刷的不建議只看,建議看看思路然後自己去官方站做,如果看不懂可以看很多大佬的題解
點擊問題標題可直達LeetCode中文站刷題!!!
文章目錄
- 基礎知識
- 高質量的代碼
- 解決面試題的思路
- 優化時間和空間效率
- 時間效率
- 面試題39.數組中出現次數超過一半的數字
- 面試題40.最小的k個數
- 面試題41.數據流中的中位數
- 面試題42.連續子數組的最大和
- 面試題43.1~n整數中1出現的次數
- 面試題44.數字序列中某一位的數字
- 面試題45.把數組排成最小的數
- 面試題46.把數字翻譯成字符串
- 面試題47.禮物的最大價值
- 面試題48.最長不含重複字符的子字符串
- 時間效率額空間效率的平衡
- 面試中的各項能力
基礎知識
數據結構
面試題03.數組中重複的數字
空間優先:原地排序法
遍歷數組,將遍歷的數試探放入值正確位置,如果正確位置已經有過正確數了,且不是當前位置,說明發現了重複數了,退出
如果正確位置不是正確值,將正確值交換到正確位置
(93%,100%)
class Solution {
public int findRepeatNumber(int[] nums) {
for (int i = 0; i < nums.length; i++) {
int n = nums[i];
//try
if (n == nums[n] && n != i) return n;
//swap
int tmp = nums[i];
nums[i] = nums[n];
nums[n] = tmp;
}
//not find
return -1;
}
}
時間優先:字典
用兩個字節的boolean 的數組,佔用空間小
class Solution {
public int findRepeatNumber(int[] nums) {
boolean compare[] = new boolean[nums.length];
for (int i = 0; i < nums.length; i++) {
if (compare[nums[i]]) return nums[i];
compare[nums[i]] = true;
}
return -1;
}
}
面試題04.二維數組中的查找
從左下或者右上爲起點的查找:查找狀態樹是BST
(雙100%)
class Solution {
public boolean findNumberIn2DArray(int[][] matrix, int target) {
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) return false;
int m = matrix.length;
int n = matrix[0].length;
int row = 0;
int col = n - 1;
while (row < m && col >= 0) {
if (target == matrix[row][col]) return true;
else if (target > matrix[row][col]) row++;
else col--;
}
return false;
}
}
面試題05.替換空格
使用線程不安全的底層實現爲動態數組的StringBuilder實現字符串重組
(雙100%)
class Solution {
public String replaceSpace(String s) {
StringBuilder bd = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) == ' ') bd.append("%20");
else bd.append(s.charAt(i));
}
return bd.toString();
}
}
面試題06.從尾到頭打印鏈表
翻轉鏈表後順序打印
- 翻轉鏈表,統計鏈表長度
- 初始化結果集數組,遍歷鏈表添加值
(雙100%)
class Solution {
public int[] reversePrint(ListNode head) {
ListNode pre = null;
ListNode cur = head;
int count = 0;//反轉時順便統計鏈表長度,以便初始化數組
while (cur != null) {
ListNode tmp = cur.next;
cur.next = pre;
pre = cur;
cur = tmp;
count++;
}
int[] res = new int[count];//初始化結果集
cur = pre;//將反轉後的頭指針賦值給cur
int index = 0;//數組下標指針
while (cur != null) {
res[index++] = cur.val;
cur = cur.next;
}
return res;
}
}
面試題07.重建二叉樹
模擬生成二叉樹:
- 前序遍歷的節點對應根節點
- 前序遍歷的值在中序遍歷中將待遍歷值分爲兩部分,小於根節點值的在左子樹,大於根節點值的在右子樹
class Solution {
public TreeNode buildTree(int[] preorder, int[] inorder) {
return helper(preorder, 0, preorder.length, inorder, 0, inorder.length);
}
private TreeNode helper(int[] preorder, int p_start, int p_end, int[] inorder, int i_start, int i_end) {
//p前後指針重複說明p遍歷完了
if (p_start == p_end) return null;
//初始化root節點
int root_val = preorder[p_start];
TreeNode root = new TreeNode(root_val);
//root節點再中序遍歷中的位置
int i_root_index = 0;
//查找位置
for (int i = i_start; i < i_end; i++) {
if (root.val == inorder[i]) {
i_root_index = i;
break;
}
}
int leftNum = i_root_index - i_start;
root.left = helper(preorder, p_start + 1, p_start + leftNum + 1, inorder, i_start, i_root_index);
root.right = helper(preorder, p_start + leftNum + 1, p_end, inorder, i_root_index + 1, i_end);
return root;
}
}
面試題09.用兩個棧實現隊列
定義輸入棧和輸出棧
- 如果添加操作,直接放入輸入棧
- 如果移除操作,判斷輸出棧有無值,如果沒有就將輸入棧所有值加入輸出棧再輸出
class CQueue {
Stack<Integer> in;
Stack<Integer> out;
public CQueue() {
in = new Stack<>();
out = new Stack<>();
}
public void appendTail(int value) {
in.add(value);
}
public int deleteHead() {
if (out.isEmpty()) {
while (!in.isEmpty()) {
out.add(in.pop());
}
}
return out.isEmpty() ? -1 : out.pop();
}
}
算法與數據操作
面試題10-I.斐波那契數列
自頂向下:帶緩存的遞歸
(雙100%)
class Solution {
private int cache[];
public int fib(int n) {
cache = new int[n + 1];
return Myfib(n);
}
private int Myfib(int n) {
if (n < 2) return n;
if (cache[n] == 0) {
cache[n] = Myfib(n - 1) + Myfib(n - 2);
}
return cache[n]%1000000007;
}
}
自底向上:動態規劃
(雙100%)
class Solution {
public int fib(int n) {
if (n < 2) return n;
int dp0 = 0;
int dp1 = 1;
int dp2 = 1;
for (int i = 2; i <= n; i++) {
dp2 = (dp1 + dp0)%1000000007;
dp0 = dp1;
dp1 = dp2;
}
return dp2;
}
}
面試題11.旋轉數組的最小數字
二分查找:
- numbers[mid] > numbers[right]:如45612,最小值在右邊,left = mid +1
- numbers[mid] < numbers[right]:如45123,mid位置可能是最小值,right = mid
- numbers[mid] = numbers[right]:如,12222,值都爲2,所以左移一個繼續找
分支條件有點不通用,需要記一下
class Solution {
public int minArray(int[] numbers) {
int left = 0;
int right = numbers.length - 1;
while (left < right) {
int mid = (left + right) >> 1;
if (numbers[mid] > numbers[right]) {
left = mid + 1;
} else if (numbers[mid] < numbers[right]) {
right = mid;
} else {
right--;
}
}
return numbers[left];
}
}
面試題12.矩陣中的路徑
回溯算法+dfs
遍歷數組,找到等於查找字符串第一個字符的且沒有訪問過的位置,dfs查找
class Solution {
public boolean exist(char[][] board, String word) {
if (board == null || board.length == 0) return false;
int m = board.length;
int n = board[0].length;
boolean[][] visited = new boolean[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (!visited[i][j] && dfs(i, j, visited, board, word, 0)) {
return true;
}
}
}
return false;
}
private boolean dfs(int i, int j, boolean[][] visited, char[][] board, String word, int index) {
if (index == word.length()) {
return true;
}
//退出條件
if (i == -1 || j == -1 || i == board.length || j == board[0].length
|| visited[i][j] || board[i][j] != word.charAt(index)) {
return false;
}
visited[i][j] = true;
//嘗試訪問四個方向
if (dfs(i + 1, j, visited, board, word, index + 1)) return true;
if (dfs(i - 1, j, visited, board, word, index + 1)) return true;
if (dfs(i, j + 1, visited, board, word, index + 1)) return true;
if (dfs(i, j - 1, visited, board, word, index + 1)) return true;
//回溯
visited[i][j] = false;
return false;
}
}
面試題13.機器人的運動範圍
回溯算法:
從0,0位置開始向i增長和j增長的方向深度優先遍歷
class Solution {
public int movingCount(int m, int n, int k) {
boolean visited[][] = new boolean[m][n];
return dfs(0, 0, m, n, k, visited);
}
private int dfs(int i, int j, int m, int n, int k, boolean[][] visited) {
if (i == m || j == n || visited[i][j] || getDigist(i) + getDigist(j) > k) {
return 0;
}
visited[i][j] = true;
//從0,0開始,定這兩個方向dfs
return 1 + dfs(i + 1, j, m, n, k, visited) + dfs(i, j + 1, m, n, k, visited);
}
//求數位和
private int getDigist(int num) {
int res = 0;
while (num != 0) {
res += num % 10;
num /= 10;
}
return res;
}
}
面試題14-I.剪繩子
動態規劃:
- 初始化dp1和dp2(處理i<3的情況)
- n長度的繩子的最大乘積是有三種可能
- i<3時就是1
- 將繩子分兩段的乘積(如果長度<=3,則不分的長度比分了長)
- 繩子分兩段,其中一段繼續分(dp[i-j]緩存了最大值的)
class Solution {
public int cuttingRope(int n) {
int[] dp = new int[n + 1];
dp[1] = 1;
dp[2] = 1;
for (int i = 3; i <= n; i++) {
for (int j = 1; j < i; j++) {
dp[i] = Math.max(dp[i], Math.max(dp[i - j] * j, j * (i - j)));
}
}
return dp[n];
}
}
貪心算法:
經推理論證可知:
- 儘可能多的3帶來的乘積最大
- 當
n%3==0
時,最大積爲所有3相乘 - 當
n%3==1
時,13<22,所以將一個3同1換成2和2 - 當
n%3==2
時,最大積爲所有3相乘*2
(雙100%)
class Solution {
public int cuttingRope(int n) {
if (n <= 3) return n - 1;
int x = n / 3;
int y = n % 3;
if (y == 0) return (int) Math.pow(3, x);
if (y == 1) return (int) Math.pow(3, x - 1) * 2 * 2;
return (int) Math.pow(3, x) * 2;
}
}
面試題15.二進制中1的個數
位運算:
關於最後一位1的兩個運算技巧:
- 去掉最後一個1:
n&(n-1)
- 獲取最後一個1:
n&(-n)
當然,通過一個標誌位從最後一個開始移位比較也可以
所以這裏就有多種解法,以n&(n-1)
爲例
public class Solution {
// you need to treat n as an unsigned value
public int hammingWeight(int n) {
int count = 0;
while (n != 0) {
n &= (n - 1);
count++;
}
return count;
}
}
高質量的代碼
代碼完整性
面試題16.數值的整數次方
分治思想:求pow(x,n)可以轉化爲求一半的次方
class Solution {
public double myPow(double x, int n) {
if (n < 0) {
n = -n;
x = 1 / x;
}
return pow(x, n);
}
private double pow(double x, int n) {
if (n == 0) return 1;
double half = pow(x, n / 2);
return (n & 1) == 1 ? half * half * x : half * half;
}
}
面試題17.打印從1到最大的n位數
計算出上限,一直循環添加就可以了
class Solution {
public int[] printNumbers(int n) {
int count = (int) (Math.pow(10, n) - 1);
int res[] = new int[count];
for (int i = 0; i < count; i++) {
res[i] = i + 1;
}
return res;
}
}
面試題18.刪除鏈表的節點
class Solution {
public ListNode deleteNode(ListNode head, int val) {
if (head.val == val) return head.next;
ListNode cur = head;
while (cur.next != null) {
if (cur.next.val != val)cur = cur.next;
else {
cur.next = cur.next.next;
break;
}
}
return head;
}
}
面試題19.正則表達式匹配
動態規劃
- 如果 p.charAt(j) == s.charAt(i) :
dp[i][j] = dp[i-1][j-1]
- 如果 p.charAt(j) == ‘
.
’ :dp[i][j] = dp[i-1][j-1]
- 如果 p.charAt(j) == ‘
*
’:- 如果 p.charAt(j-1) != s.charAt(i) : dp[i][j] = dp[i][j-2] //in this case, a* only counts as empty
- 如果 p.charAt(i-1) == s.charAt(i) or p.charAt(i-1) == ‘.’:
- dp[i][j] = dp[i-1][j] //in this case, a* counts as multiple a
- or dp[i][j] = dp[i][j-1] // in this case, a* counts as single a
- or dp[i][j] = dp[i][j-2] // in this case, a* counts as empty
class Solution {
public boolean isMatch(String s, String p) {
if (s == null || p == null) return false;
int m = s.length();
int n = p.length();
boolean[][] dp = new boolean[m + 1][n + 1];
dp[0][0] = true;
//"" 和p的匹配關係初始化,a*a*a*a*a*這種能夠匹配空串,其他的是都是false。
// 奇數位不管什麼字符都是false,偶數位爲* 時則: dp[0][i] = dp[0][i - 2]
for (int i = 2; i <= n; i += 2) {
if (p.charAt(i - 1) == '*') {
dp[0][i] = dp[0][i - 2];
}
}
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
char sc = s.charAt(i - 1);
char pc = p.charAt(j - 1);
if (sc == pc || pc == '.') {
dp[i][j] = dp[i - 1][j - 1];
} else if (pc == '*') {
if (dp[i][j - 2]) {
dp[i][j] = true;
} else if (sc == p.charAt(j - 2) || p.charAt(j - 2) == '.') {
dp[i][j] = dp[i - 1][j];
}
}
}
}
return dp[m][n];
}
}
面試題20.表示數值的字符串
設置三個標誌:numSeen 是否有數字,dotSeen 是否有’.’,eSeen 是否有e
- 如果是數字,numSeen = true
- 如果是
.
,且之前沒出現過.
和e
,dotSeen = true - 如果是e或者E,且之前沒出現過e,eSeen = true,numSeen 設置爲false,爲了避免123e這種e後面不帶數字的情況
- 如果是+或者-,判斷是否在第一個位置或者前一個位置是不是e或E,如果不滿足返回false
- 其他情況都不滿足,返回false
- 最後返回numSeen ,因爲要判斷e後面有沒有數字,沒數字也是不合法的
class Solution {
public boolean isNumber(String s) {
if (s == null || s.length() == 0) return false;
boolean numSeen = false;//是否是數字
boolean dotSeen = false;//是否是'.'
boolean eSeen = false;//是否是e/E
char[] str = s.trim().toCharArray();
for (int i = 0; i < str.length; i++) {
if (str[i] >= '0' && str[i] <= '9') {
numSeen = true;
} else if (str[i] == '.') {
//.之前不能出現.和e
if (dotSeen || eSeen) {
return false;
}
dotSeen = true;
} else if (str[i] == 'e' || str[i] == 'E') {
//e之前不能出現e
if (eSeen || !numSeen) {
return false;
}
eSeen = true;
numSeen = false;//數字置爲false了,後面要排除123e和123e+的情況
} else if (str[i] == '-' || str[i] == '+') {
//只有在最開始出現和e後馬上出現才行
if (i != 0 && str[i - 1] != 'e' && str[i - 1] != 'E') {
return false;
}
} else {
return false;
}
}
return numSeen;
}
}
面試題21.調整數組順序使奇數位於偶數前面
因爲對偶數的順序沒有要求,所以只需要維護一個奇數的索引oddIndex
如果是奇數就換到奇數索引位置
class Solution {
public int[] exchange(int[] nums) {
int oddIndex = 0;
for (int i = 0; i < nums.length; i++) {
if ((nums[i] & 1) != 0) {
int tmp = nums[i];
nums[i] = nums[oddIndex];
nums[oddIndex] = tmp;
oddIndex++;
}
}
return nums;
}
}
代碼的魯棒性
面試題22.鏈表中倒數第k個節點
雙指針
fast指針先移動k個位置,然後fast和slow同步後移
當fast == null時,slow剛好到倒數第k個
class Solution {
public ListNode getKthFromEnd(ListNode head, int k) {
ListNode fast = head;
ListNode slow = head;
while (k-- > 0) {
fast = fast.next;
}
while (fast != null) {
fast = fast.next;
slow = slow.next;
}
return slow;
}
}
面試題23.鏈表中環的入口節點(環形鏈表II)
這個題題庫居然沒有,看了下書說的其實就是環形鏈表問題,這裏就直接放環形鏈表的鏈接了
快慢指針法(推薦)
思路:
- 先和環形鏈表I一樣判斷有沒有環,有才進行第二步
- 從同一起點開始,fast一次走兩步,slow一次走一步,當他們相遇之後,fast回到原點一次走一步,第二次相遇節點就是環連接點
- 我的理解:
- 假設第二圈發生第一次相遇,第二圈走完了就第二次相遇了
- 第一次相遇時fast走了F+(a+b)+a
- slow走了F+a
- 因爲fast是slow的兩倍速度,所以第二次相遇時應該滿足fast=2*slow
- 設slow還需要走 x遠第二次相遇,可得方程
(F+2a+b+2x)=2(F+a+x)
,,解得F=b - 所以就假設fast以一倍的速度陪slow走完b,另一倍速從F出發,當fast與slow相遇時,相當於fast走過了b+F = 2b = 2倍slow的距離
- 此時滿足第二次相遇的情況
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode fast = head;
ListNode slow = head;
while (true) {
if (fast == null || fast.next == null) return null;
fast = fast.next.next;
slow = slow.next;
if (fast == slow) break;
}
fast = head;
//第二階段,找環接入點
while (fast != slow) {
fast = fast.next;
slow = slow.next;
}
return fast;
}
}
面試題24.反轉鏈表
class Solution {
public ListNode reverseList(ListNode head) {
ListNode pre = null;
ListNode cur = head;
while (cur != null) {
ListNode tmp = cur.next;
cur.next = pre;
pre = cur;
cur = tmp;
}
return pre;
}
}
面試題25.合併兩個排序的鏈表
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode pre = new ListNode(-1);
ListNode cur = pre;
while (l1 != null && l2 != null) {
if (l1.val < l2.val) {
cur.next = l1;
l1 = l1.next;
cur = cur.next;
} else {
cur.next = l2;
l2 = l2.next;
cur = cur.next;
}
}
if (l1 != null) {
cur.next = l1;
} else {
cur.next = l2;
}
return pre.next;
}
}
面試題26.樹的子結構
針對需要以每個節點都爲起點進行遍歷的問題
可採用如下方式遍歷
f(A){
return B ? A(left) ? A(right)
}
class Solution {
public boolean isSubStructure(TreeNode A, TreeNode B) {
if (A == null || B == null) return false;
return helper(A, B) || isSubStructure(A.left, B) || isSubStructure(A.right, B);
}
private boolean helper(TreeNode a, TreeNode b) {
if (b == null) return true;//b空了 說明比較完了 是滿足的
if (a == null) return false;//b還沒空a空了,說明不滿足
if (a.val != b.val) return false;//值不同不滿足
return helper(a.left, b.left) && helper(a.right, b.right);//下探下一層
}
}
解決面試題的思路
畫圖讓抽象問題具體化
面試題27.二叉樹的鏡像
class Solution {
public TreeNode mirrorTree(TreeNode root) {
if (root == null) return null;
return helper(root);
}
private TreeNode helper(TreeNode root) {
if (root == null) return null;
TreeNode left = helper(root.left);
TreeNode right = helper(root.right);
root.left = right;
root.right = left;
return root;
}
}
面試題28.對稱的二叉樹
class Solution {
public boolean isSymmetric(TreeNode root) {
if (root == null) return true;
return helper(root.left, root.right);
}
private boolean helper(TreeNode left, TreeNode right) {
if (left == null && right == null) return true;
if (left == null || right == null) return false;
if (left.val != right.val) return false;
return helper(left.left, right.right) && helper(left.right, right.left);
}
}
面試題29.順時針打印矩陣
巧妙的應用邊界條件,畫一個矩陣模擬
class Solution {
public int[] spiralOrder(int[][] matrix) {
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) return new int[0];
int top = 0;
int bottom = matrix.length - 1;
int left = 0;
int right = matrix[0].length - 1;
int[] res = new int[matrix.length * matrix[0].length];
int index = 0;
while (true) {
for (int i = left; i <= right; i++) res[index++] = matrix[top][i];
if (++top>bottom)break;
for (int i = top; i <= bottom; i++) res[index++] = matrix[i][right];
if (--right<left)break;
for (int i = right; i >= left; i--) res[index++] = matrix[bottom][i];
if (--bottom<top)break;
for (int i = bottom; i >= top; i--) res[index++] = matrix[i][left];
if (++left>right)break;
}
return res;
}
}
舉例讓抽象問題具體化
面試題30.包含min函數的棧
維護兩個棧,一個最小值棧一個值棧
class MinStack {
Stack<Integer> minStack;
Stack<Integer> stack;
/**
* initialize your data structure here.
*/
public MinStack() {
stack = new Stack<>();
minStack = new Stack<>();
}
public void push(int x) {
stack.push(x);
if (minStack.isEmpty() || minStack.peek() >= x)
minStack.push(x);
}
public void pop() {
if (stack.pop().equals(minStack.peek()))
minStack.pop();
}
public int top() {
return stack.peek();
}
public int min() {
return minStack.peek();
}
}
面試題31.棧的壓入、彈出序列
模擬壓棧出棧
class Solution {
public boolean validateStackSequences(int[] pushed, int[] popped) {
if (pushed == null || popped == null || popped.length != pushed.length) return false;
Stack<Integer> stack = new Stack<>();
int index = 0;//popped的下標
int num = popped.length;
for (int e : pushed) {
stack.push(e);
while (index < num && !stack.isEmpty() && stack.peek() == popped[index]) {
stack.pop();
index++;
}
}
//最後一個都出棧了
return index == num;
}
}
面試題32-III.從上到下打印二叉樹III
本考點有三個問題,其實都是層次遍歷的小變形,列舉最複雜的一個
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
if (root == null) return new ArrayList<>();
List<List<Integer>> res = new ArrayList<>();
Deque<TreeNode> deque = new LinkedList<TreeNode>();
deque.offer(root);
while (!deque.isEmpty()) {
int size = deque.size();
LinkedList<Integer> tmp = new LinkedList<>();
for (int i = 0; i < size; i++) {
TreeNode cur = deque.poll();
if (res.size() % 2 == 1) tmp.addFirst(cur.val);
else tmp.add(cur.val);
if (cur.left != null) deque.offer(cur.left);
if (cur.right != null) deque.offer(cur.right);
}
res.add(tmp);
}
return res;
}
}
面試題33.二叉搜索樹的後序遍歷序列
模擬構造一棵二叉樹
class Solution {
public boolean verifyPostorder(int[] postorder) {
if (postorder.length <= 2) return true;
return helper(postorder, 0, postorder.length - 1);
}
private boolean helper(int[] postorder, int start, int end) {
if (start >= end) return true;
int i;//拐點
for (i = start; i < end; i++) {
if (postorder[i] > postorder[end]) break;
}
//大於根節點的部分
for (int j = i; j < end; j++) {
if (postorder[j] < postorder[end]) return false;
}
//遞歸遍歷
return helper(postorder, start, i - 1) && helper(postorder, i, end - 1);
}
}
面試題34.二叉樹中和爲某一值的路徑
回溯算法
class Solution {
List<List<Integer>> res;
public List<List<Integer>> pathSum(TreeNode root, int sum) {
if (root == null) return new ArrayList<>();
res = new ArrayList<>();
dfs(root, sum, new ArrayList<Integer>());
return res;
}
private void dfs(TreeNode root, int sum, ArrayList<Integer> list) {
if (root == null) return;
list.add(root.val);
sum -= root.val;
if (sum == 0 && root.left == null && root.right == null) {
res.add(new ArrayList<>(list));
}
dfs(root.left, sum, list);
dfs(root.right, sum, list);
list.remove(list.size() - 1);
}
}
分解讓複雜問題簡單化
面試題35.複雜鏈表的複製
使用額外O(n)的空間
一次遍歷使用Map暫存單個的節點
二次遍歷將Map中的單節點串起來
class Solution {
public Node copyRandomList(Node head) {
if (head == null) return null;
Map<Node, Node> map = new HashMap<Node, Node>();
Node cur = head;
//將所有節點存入map
while (cur != null) {
Node singleNode = new Node(cur.val);
map.put(cur, singleNode);
cur = cur.next;
}
//將map中的單個的節點串聯起來
cur = head;
while (cur != null) {
if (cur.next != null) {
map.get(cur).next = map.get(cur.next);
}
if (cur.random != null) {
map.get(cur).random = map.get(cur.random);
}
cur = cur.next;
}
return map.get(head);
}
}
不使用額外空間,原地修改
一圖勝千言,圖自
- 一次遍歷在原來的節點後添加一個和原節點值相同的單節點
- 二次遍歷將新節點的random索引添加上
- 三次遍歷拆分出新鏈表
class Solution {
public Node copyRandomList(Node head) {
if (head == null) return null;
Node l1 = head;
Node l2 = null;
//生成所有節點,並插入到原有節點的後邊
while (l1 != null) {
l2 = new Node(l1.val);
l2.next = l1.next;
l1.next = l2;
l1 = l1.next.next;
}
//更新插入節點的random
l1 = head;
while (l1 != null) {
if (l1.random != null) {
l1.next.random = l1.random.next;
}
l1 = l1.next.next;
}
l1 = head;
Node l2_head = l1.next;
//新舊節點分離,生成新鏈表
while (l1 != null) {
l2 = l1.next;
l1.next = l1.next.next;
if (l2.next != null) {
l2.next = l2.next.next;
}
l1 = l1.next;
}
return l2_head;
}
}
面試題36.二叉搜索樹與雙向鏈表
要求原地操作,所以遍歷完了再連接就不現實了
所以在中序遍歷的時候就要把前後關係連接上
兩個暫存節點,一個存要返回的鏈表頭的值,另一個存當前節點的前一個節點的值
- 如果pre節點爲空,說明是第一個節點,賦值爲head節點
- 如果pre節點不爲空,就把當前節點賦值給pre的right,將pre節點賦值給當前節點的left
- 連接完成後還需要首尾相連,將head賦值給當前的right,將head的left賦值給當前
- 因爲首尾相連覆蓋了當前的right,所以在首尾相連之前應該暫存當前的right值,以便中序遍歷使用
class Solution {
Node head = null;//暫存頭結點
Node pre = null;//當前節點的前一個節點
public Node treeToDoublyList(Node root) {
if (root == null) return null;
helper(root);
return head;
}
private void helper(Node root) {
if (root == null) return;
helper(root.left);
//暫存right節點,後面頭尾相接要覆蓋掉
Node right = root.right;
if (pre == null) {
head = root;
} else {
pre.right = root;
root.left = pre;
}
//頭尾相接
root.right = head;
head.left = root;
//前置節點後移
pre = root;
helper(right);
}
}
面試題37.序列化二叉樹
二叉樹層序遍歷,將結果拼裝成字符串序列化
public class Codec {
private String results;//定義一個成員變量儲存序列化結果
// Encodes a tree to a single string.
public String serialize(TreeNode root) {
if (root == null) return "[]";//返回空串
Deque<TreeNode> deque = new LinkedList<TreeNode>();
deque.offer(root);
results = "[" + root.val;
while (!deque.isEmpty()) {
TreeNode cur = deque.poll();
if (cur.left != null) {
deque.offer(cur.left);
results += "," + cur.left.val;
} else {
results += ",null";
}
if (cur.right != null) {
deque.offer(cur.right);
results += "," + cur.right.val;
} else {
results += ",null";
}
}
results += "]";
return results;
}
// Decodes your encoded data to tree.
public TreeNode deserialize(String data) {
if (data.length() == 2) return null;
data = data.substring(1, data.length() - 1);//去首尾括號
String[] vals = data.split(",");
Deque<TreeNode> deque = new LinkedList<TreeNode>();
TreeNode root = new TreeNode(Integer.valueOf(vals[0]));
deque.offer(root);
int i = 1;//數組下標
while (!deque.isEmpty()) {
TreeNode cur = deque.poll();
if (vals[i].equals("null")) {
cur.left = null;
} else {
cur.left = new TreeNode(Integer.valueOf(vals[i]));
deque.offer(cur.left);
}
i++;
if (vals[i].equals("null")) {
cur.right = null;
} else {
cur.right = new TreeNode(Integer.valueOf(vals[i]));
deque.offer(cur.right);
}
i++;
}
return root;
}
}
面試題38.字符串的排列
回溯算法,可能包含重複元素的全排列,排序後去重排列
class Solution {
List<String> list;
public String[] permutation(String s) {
boolean[] visited = new boolean[s.length()];
list = new ArrayList<>();
char[] chars = s.toCharArray();
Arrays.sort(chars);
helper(chars, visited, "");
return list.toArray(new String[list.size()]);
}
private void helper(char[] chars, boolean[] visited, String str) {
if (str.length() == chars.length) {
list.add(str);
return;
}
for (int i = 0; i < chars.length; i++) {
if (visited[i]) continue;
if (i > 0 && chars[i] == chars[i - 1] && !visited[i - 1]) continue;
visited[i] = true;
helper(chars, visited, str + chars[i]);
visited[i] = false;
}
}
}
優化時間和空間效率
時間效率
面試題39.數組中出現次數超過一半的數字
摩爾投票
- 如果計數器爲0,更換投票對象
- 如果等於投票對象,計數器+1;
- 如果不等於投票對象,計數器-1;
(雙100%)
class Solution {
public int majorityElement(int[] nums) {
int count = 0;//計數器
int card = 0;
for (int num : nums) {
if (count == 0) card = num;
count += (num == card) ? 1 : -1;
}
return card;
}
}
面試題40.最小的k個數
最小或者最大k個數最容易想到堆排序
Java的優先隊列默認實現是小頂堆,採用優先隊列實現:
(33%,100%)
class Solution {
public int[] getLeastNumbers(int[] arr, int k) {
if (arr.length < k) return arr;
//優先隊列默認實現是小頂堆
PriorityQueue<Integer> queue = new PriorityQueue<>();
for (int i = 0; i < arr.length; i++) {
queue.offer(arr[i]);
}
int res[] = new int[k];
for (int i = 0; i < k; i++) {
res[i] = queue.poll();
}
return res;
}
}
採用大頂堆實現,如果堆內少於k個時才能入堆,到達k之後移除堆頂的大的元素
判斷較多,理論上更快,實際運行慢了一些
(23%,100%)
class Solution {
public int[] getLeastNumbers(int[] arr, int k) {
if (arr.length < k) return arr;
//優先隊列實現大頂堆
PriorityQueue<Integer> heap = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});
for (int i = 0; i < arr.length; i++) {
if (heap.isEmpty() || heap.size() < k || arr[i] < heap.peek()) {
heap.offer(arr[i]);
}
if (heap.size() > k) {
heap.poll();
}
}
int res[] = new int[k];
for (int i = 0; i < k; i++) {
res[i] = heap.poll();
}
return res;
}
}
一路快速排序實現
如果下標爲k-1的位置被正確找到了,那麼就直接返回
(87%,100%)
class Solution {
public int[] getLeastNumbers(int[] arr, int k) {
if (k == 0 || arr.length == 0) return new int[0];
//最後一個參數是要查找的數組下標
return quickSearch(arr, 0, arr.length - 1, k - 1);
}
private int[] quickSearch(int[] arr, int left, int right, int k) {
int j = partition(arr, left, right);
if (j == k) {//j位置剛好是k,則j和j之前的都小於j後面的
return Arrays.copyOf(arr, j + 1);
}
return j > k ? quickSearch(arr, left, j - 1, k)
: quickSearch(arr, j + 1, right, k);
}
private int partition(int[] arr, int left, int right) {
int v = arr[left];
int j = left;
for (int i = left; i <= right; i++) {
if (arr[i] < v) {
j++;
swap(arr, i, j);
}
}
swap(arr, left, j);
return j;
}
private void swap(int[] arr, int left, int right) {
int tmp = arr[left];
arr[left] = arr[right];
arr[right] = tmp;
}
}
快速排序再升級,將partition變成二路快速排序
(99.6%,100%)
private int partition(int[] arr, int l, int r) {
// 隨機在arr[l...r]的範圍中, 選擇一個數值作爲標定點pivot
swap(arr, l, (int) (Math.random() * (r - l + 1)) + l);
//暫存arr[l]
int v = arr[l];
//j爲分割位
int i = l + 1, j = r;
while (true) {
//從前面開始。小於v的不變
while (i <= r && arr[i] < v) {
i++;
}
//從後面開始。大於v不變
while (j >= l + 1 && arr[j] > v) {
j--;
}
if (i > j) {
break;
}
//到這裏了: arr[i]>=v arr[j]<=v
//如果等於v也要換,保證了兩邊相等的數目平衡
swap(arr, i, j);
i++;
j--;
}
swap(arr, l, j);
return j;
}
面試題41.數據流中的中位數
使用優先隊列實現
構建一個大頂堆和一個小頂堆
- 大頂堆存放較小的一半的數
- 大頂堆存放較大的一半的數
- 如果個數是偶數,返回大頂堆頂部的較小一半的最大值和小頂堆中較大一半的最小值的平均值
- 如果格式是奇數返回大頂堆的堆頂元素(存放時默認單數的存大頂堆)
(執行時間有點迷 77.7%,100%)
class MedianFinder {
private PriorityQueue<Integer> maxHeap;
private PriorityQueue<Integer> minHeap;
/**
* initialize your data structure here.
*/
public MedianFinder() {
//大頂堆,
maxHeap = new PriorityQueue<>(Collections.reverseOrder());
//小頂堆
minHeap = new PriorityQueue<>();
}
public void addNum(int num) {
maxHeap.offer(num);
minHeap.offer(maxHeap.poll());
//保證大頂堆的個數>=小頂堆的個數
if (minHeap.size() > maxHeap.size()) {
maxHeap.offer(minHeap.poll());
}
}
public double findMedian() {
if (maxHeap.size() == minHeap.size()) {
return (maxHeap.peek() + minHeap.peek()) * 0.5;
}
return maxHeap.peek();
}
}
面試題42.連續子數組的最大和
動態規劃
(98.7%,100%)
class Solution {
public int maxSubArray(int[] nums) {
int dp[] = new int[nums.length];
dp[0] = nums[0];
int res = dp[0];
for (int i = 1; i < nums.length; i++) {
dp[i] = Math.max(nums[i], dp[i - 1] + nums[i]);
res = Math.max(dp[i], res);
}
return res;
}
}
壓縮一下dp數組到一個變量
(98.7%,100%)
class Solution {
public int maxSubArray(int[] nums) {
int preMax = nums[0];
int res = preMax;
for (int i = 1; i < nums.length; i++) {
preMax = Math.max(nums[i], preMax + nums[i]);
res = Math.max(preMax, res);
}
return res;
}
}
面試題43.1~n整數中1出現的次數
遞歸
定義變量:
- high-最高位數字
- pow-數字所在的量級,如1234的pow是1000
- last-去除掉最高位後的值
以兩種類型距離說明遞歸情況
- 求1234的
hight = 1
pow = 1000
last = 234
包含最高位擁有1的個數爲last+1
(1000-1234的千位)
不包含最高位擁有1的個數爲f(pow-1)
(0-999)+f(last)
(1000-1234的非千位)
所以最終的個數爲:f(pow-1)+last+1+f(last)
- 求4321的
high = 4
pow = 1000
last = 321
最高位的1的個數爲pow
(1000-1999的最高位)
不包含最高位的個數爲high*f(pow-1)
(x000-x999的非千位個數)+f(last)
(4000-4321的非千位個數)
所以最終個數爲:pow+high*f(pow-1)+f(last)
(雙100%)
class Solution {
public int countDigitOne(int n) {
if (n <= 0) return 0;
String s = String.valueOf(n);
int high = s.charAt(0) - '0';//最高位
int pow = (int) Math.pow(10, s.length() - 1);//位數級
int last = n - high * pow;
if (high == 1) {
return last + 1 + countDigitOne(last) + high * countDigitOne(pow - 1);
} else {
return pow + countDigitOne(last) + high * countDigitOne(pow - 1);
}
}
}
面試題44.數字序列中某一位的數字
通過觀察可以發現
- 1位數字佔用了10個位置(0-9)
- 2位數字佔用了180個位置(10-99)*2
- 3位數字佔用了2700個位置(100-999)*2
從這裏我們可以得出規律:n位數佔用的位置長度爲(Math.pow(10, n- 1) * 9 * n);
通過計算n在哪個範圍內,就能得出n對應位置是幾位數,以215爲例
power = 2時 ,包含了10+180 = 190的位置,190<215,所以power++
power = 3時,包含了190+2700 = 2890>215,所以確定了power = 3
所以真實對應的數字應該是100+(215-190)/3 = 108
然後計算(215-190)/3 = 1,所以去字符串108的1下標值0,返回0
(雙100%)
class Solution {
public int findNthDigit(int n) {
if (n < 10) return n;
int count = 0;
int power = 1;
while (true) {
count = helper(power);
//如果滿足小於count,說明n對應的是power位的數字
if (n < count) break;
n -= count;
power++;
}
//計算得到這個數字
int resNum = (int) (Math.pow(10, power - 1) + n / power);
//判斷n在這個數字的第幾位就返回幾
return String.valueOf(resNum).charAt(n % power) - '0';
}
//求n位數字組成的串所佔的範圍(1位數字不超過10,2位數字不超過180,3位數字不超過2700,四位數字不超過36000)
private int helper(int power) {
if (power == 1) return 10;
return (int) (Math.pow(10, power - 1) * 9 * power);
}
}
面試題45.把數組排成最小的數
要點:
比較a和b的大小可以比較‘a’+'b'
和‘b’+'a'
的大小,轉換成字符串拼接等等長就可以進行判斷了
具體的排序算法可以自己實現也可以使用系統的快排
Java中可以實現Comparator接口,然後調用String的compareTo方法逐個比較字符
(97.6%,100%)
class Solution {
public String minNumber(int[] nums) {
String[] strs = new String[nums.length];
for (int i = 0; i < nums.length; i++) {
strs[i] = String.valueOf(nums[i]);
}
Arrays.sort(strs, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return (o1 + o2).compareTo(o2 + o1);
}
});
StringBuilder bd = new StringBuilder();
for (int i = 0; i < strs.length; i++) {
bd.append(strs[i]);
}
return bd.toString();
}
}
面試題46.把數字翻譯成字符串
動態規劃,dp[i] = dp[i-1]+dp[i-2]
任何情況下,dp[i] = dp[i+1]
當i位和前一位組合成兩位數還滿足規則時。dp[i] = dp[i-1]+dp[i-2]
class Solution {
public int translateNum(int num) {
char[] sc = String.valueOf(num).toCharArray();
int n = sc.length;
int[] dp = new int[n + 1];
dp[0] = 1;
for (int i = 1; i <= n; i++) {
//劃分爲1位
dp[i] += dp[i - 1];
if (i > 1) {
int a = (sc[i - 2] - '0') * 10 + (sc[i - 1] - '0');
if (a >= 10 && a <= 25) {//可劃分爲兩位
dp[i] += dp[i - 2];
}
}
}
return dp[n];
}
}
面試題47.禮物的最大價值
動態規劃
(98.3%,100%)
class Solution {
public int maxValue(int[][] grid) {
if (grid == null || grid.length == 0 || grid[0].length == 0) return 0;
int m = grid.length;
int n = grid[0].length;
int dp[] = new int[n + 1];
for (int i = 1; i <= n; i++) {
dp[i] = dp[i - 1] + grid[0][i - 1];
}
for (int i = 1; i < m; i++) {
for (int j = 0; j < n; j++) {
dp[j + 1] = grid[i][j] + Math.max(dp[j], dp[j + 1]);
}
}
return dp[n];
}
}
面試題48.最長不含重複字符的子字符串
滑動窗口
用一個set來維護窗口
(60%,100%)
class Solution {
public int lengthOfLongestSubstring(String s) {
int n = s.length();
Set<Character> set = new HashSet<>();
int resMax = 0;//最大結果
int left = 0;//左窗界
int right = 0;//右窗界
while (left < n && right < n) {
//如果不包含,就加入set,右窗口++
if (!set.contains(s.charAt(right))) {
set.add(s.charAt(right++));
resMax = Math.max(resMax, right - left);
} else {//如果包含了,set移除左窗口值,左窗口右移
set.remove(s.charAt(left++));
}
}
return resMax;
}
}
優化的滑動窗口
用長度爲128的數組來記錄某個元素的下一個位置
每次遍歷更新左指針的位置,如果遇到了重複的值就變爲重複值原來的後一個位置
(99%,100%)
class Solution {
public int lengthOfLongestSubstring(String s) {
int n = s.length();
int[] map = new int[128];
int resMax = 0;//最大結果
for (int left = 0, right = 0; right < n; right++) {
left = Math.max(map[s.charAt(right)], left);//更新left
resMax = Math.max(resMax, right - left + 1);
map[s.charAt(right)] = right + 1;//存入right的後一個位置
}
return resMax;
}
}
時間效率額空間效率的平衡
面試題49.醜數
動態規劃dp[i] = min(dp[p2] * 2, dp[p3] * 3, dp[p5] * 5);
(89%,100%)
class Solution {
public int nthUglyNumber(int n) {
int p2 = 0;
int p3 = 0;
int p5 = 0;
int dp[] = new int[n];
dp[0] = 1;
for (int i = 1; i < n; i++) {
dp[i] = Math.min(dp[p2] * 2, Math.min(dp[p3] * 3, dp[p5] * 5));
if (dp[i] == dp[p2] * 2) p2++;
if (dp[i] == dp[p3] * 3) p3++;
if (dp[i] == dp[p5] * 5) p5++;
}
return dp[n - 1];
}
}
面試題50.第一個只出現一次的字符
用一個128長度的數組模擬hash表,將所有字符的個數存入hash表
第二次按照字符串字符順序再次遍歷,如果字符在hash表中值爲1,返回字符
class Solution {
public char firstUniqChar(String s) {
if (s.length() == 0) return ' ';
int[] compare = new int[128];
for (int i = 0; i < s.length(); i++) {
compare[s.charAt(i)]++;
}
for (int i = 0; i < s.length(); i++) {
if (compare[s.charAt(i)] == 1) return s.charAt(i);
}
return ' ';
}
}
面試題51.數組中的逆序對
歸併排序,在排序過程中統計
(72%,100%)
class Solution {
public int reversePairs(int[] nums) {
return mergeSort(nums, 0, nums.length - 1);
}
private int mergeSort(int[] nums, int left, int right) {
if (left >= right) return 0;
int mid = (left + right) >> 1;
return mergeSort(nums, left, mid) + mergeSort(nums, mid + 1, right) + merge(nums, left, mid, right);
}
private int merge(int[] nums, int left, int mid, int right) {
int i = left;
int j = mid + 1;
int k = 0;
int count = 0;
int res[] = new int[right - left + 1];
while (i <= mid && j <= right) {
if (nums[i] > nums[j]) count += mid - i + 1;//如果j位置小於i位置,那麼j位置小於i位置後所有的左半邊的數
res[k++] = nums[i] <= nums[j] ? nums[i++] : nums[j++];
}
while (i <= mid) res[k++] = nums[i++];
while (j <= right) res[k++] = nums[j++];
for (int l = 0; l < res.length; l++) {
nums[left + l] = res[l];
}
return count;
}
}
面試題52.兩個鏈表的第一個公共節點
兩條路徑交替遍歷兩條鏈表,同時遍歷完成的節點就是交點
如果兩個鏈表沒有交點,那麼會在第二次互換鏈表頭時相同,node1==node2==null
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode node1 = headA;
ListNode node2 = headB;
while (node1 != node2) {
node1 = node1 == null ? headB : node1.next;
node2 = node2 == null ? headA : node2.next;
}
return node1;
}
}
面試中的各項能力
知識遷移能力
面試題53-I.在排序數組中查找數字I
二分查找,找到後左右逐個推進找到所有的結果
(雙100%)
class Solution {
public int search(int[] nums, int target) {
if (nums == null || nums.length == 0) return 0;
int left = 0;
int right = nums.length - 1;
while (left <= right) {
int mid = (left + right) >> 1;
if (nums[mid] == target) {
int i = mid - 1;
int j = mid + 1;
while (i >= 0 && nums[i] == target) i--;
while (j < nums.length && nums[j] == target) j++;
return j - i - 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return 0;
}
}
面試題53-II.0~n-1中缺失的數字
運用位運算
異或如下內容:
- n = nums.length;
- 0-n的下標
- nums[0]-num[n-1]的值
異或的內容除去待查找值都出現了兩次,剩下的就是待查找的值
(雙100%)
class Solution {
public int missingNumber(int[] nums) {
int len = nums.length;
int res = 0;
for (int i = 0; i < len; i++) {
res ^= i ^ nums[i];
}
return res ^ len;
}
}
面試題54.二叉搜索樹的第k大節點
利用二叉搜索樹中序遍歷是遞增的特點,將left
和right
的遍歷位置交換後就是遞減的形式,然後計數第幾個就是第幾大了
class Solution {
private int count = 0;
private int res;
public int kthLargest(TreeNode root, int k) {
search(root, k);
return res;
}
private void search(TreeNode root, int k) {
if (root == null) return;
search(root.right, k);
count++;
if (count == k)
res = root.val;
search(root.left, k);
}
}
面試題55-I.二叉樹的深度
子問題:求當前節點的高度等於左右子節點高度的較大值+1
class Solution {
public int maxDepth(TreeNode root) {
return root == null ? 0 : Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;
}
}
面試題55-II.平衡二叉樹
子問題:每一個節點的子樹高度差小於2
class Solution {
int flag = 0;
public boolean isBalanced(TreeNode root) {
helper(root);
return flag == 0;
}
private int helper(TreeNode root) {
if (root == null) return 0;
int left = helper(root.left);
int right = helper(root.right);
if (Math.abs(left - right) > 1) flag = 1;
return 1 + Math.max(left, right);
}
}
面試題56-I.數組中數字出現的次數
當有兩個只出現一次的數時,通過一遍異或運算不能得出最終值,但是可以得到這兩個只出現一次的值的異或值
獲取異或值的最尾部的1的位置,以該位置有無1將整個數組的數分成兩部分
每部分就只有一個數出現一次了,就可以一次遍歷異或找出
(雙100%)
class Solution {
public int[] singleNumbers(int[] nums) {
int tmp = 0;
int res[] = new int[2];
for (int num : nums) {
tmp ^= num;
}
int flag = tmp & -tmp;//確定1的位置
for (int num : nums) {
if ((num & flag) == 0) res[0] ^= num;
else res[1] ^= num;
}
return res;
}
}
面試題56-II.數組中數字出現的次數II
對於上一題使用的位運算遇到都是計數就沒辦法了
每個數字都是32位的,分別統計每一位所有數的和,如果只出現一次的值某一位爲0,則統計值%3==0
,如果只出現一次的值某一位爲1,則統計值%3==1
,餘數可以得出只出現一次的值
(50%,100%)更快的解法用狀態機,O(32) 沒有這個答案直觀好理解,已經是除狀態機最優了
class Solution {
public int singleNumber(int[] nums) {
int res = 0;
for (int i = 0; i < 32; i++) {
int count = 0;
for (int n : nums) {
if ((n & (1 << i)) != 0) count++;
}
if (count % 3 == 1) res |= (1 << i);
}
return res;
}
}
面試題57.和爲s的兩個數字
因爲數據是有序的,所以使用雙指針,從兩端逼近
(99%,100%)
class Solution {
public int[] twoSum(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while (left < right) {
int count = nums[left] + nums[right];
if (count == target)
return new int[]{nums[left], nums[right]};
else if (count > target)
right--;
else
left++;
}
return new int[0];
}
}
面試題57-II.和爲s的連續正數序列
滑動窗口,如果和小於target,右窗扣右移,加上右一個的值,如果和大於target,左窗口右移,減去之前左窗口的值沒如果和等於target,將窗口的值存入數組
(73%,100%)
class Solution {
public int[][] findContinuousSequence(int target) {
List<int[]> res = new ArrayList<>();
int left = 1;
int right = 1;
int sum = 0;
while (left <= target / 2) {
if (sum < target) {
sum += right++;
} else if (sum > target) {
sum -= left++;
} else {
int arr[] = new int[right - left];
for (int i = left; i < right; i++) {
arr[i - left] = i;
}
res.add(arr);
sum -= left++;
}
}
return res.toArray(new int[res.size()][]);
}
}
面試題58-I.翻轉單詞順序
用split以空格將字符串分割,然後倒序拼接,需要判斷分割的結果是不是空串
(99%,100%)
class Solution {
public String reverseWords(String s) {
if (s.trim().equals("")) return "";
String[] s1 = s.trim().split(" ");
StringBuilder res = new StringBuilder();
for (int i = s1.length - 1; i >= 0; i--) {
if (!s1[i].equals("")) res.append(s1[i] + " ");
}
return new String(res.substring(0, res.length() - 1));
}
}
分割時可使用正則表達式去除多個空串的情況,但是效率較低
(24%,100%)
class Solution {
public String reverseWords(String s) {
String[] s1 = s.trim().split(" +");
StringBuilder res = new StringBuilder();
for (int i = s1.length - 1; i >= 0; i--) {
res.append(s1[i] + " ");
}
return new String(res.substring(0, res.length() - 1));
}
}
面試題58-II.左旋轉字符串
原地算法:三次翻轉
(29%,100%)
class Solution {
public String reverseLeftWords(String s, int n) {
char[] arr = s.toCharArray();
reserve(arr, 0, arr.length - 1);
reserve(arr, 0, arr.length - n - 1);
reserve(arr, arr.length - n, arr.length - 1);
return new String(arr);
}
private void reserve(char[] arr, int start, int end) {
while (start < end) {
char tmp = arr[start];
arr[start] = arr[end];
arr[end] = tmp;
start++;
end--;
}
}
}
字符串切割後拼接
(雙100%)
class Solution {
public String reverseLeftWords(String s, int n) {
return s.substring(n, s.length()) + s.substring(0, n);
}
}
面試題59-I.滑動窗口的最大值
滑動窗口
用雙端隊列維護一個從隊首到隊尾從大到小的隊列
- 每次遍歷將新位置的下標加入隊尾,如果隊尾小於新下標元素,隊尾出隊直到符合才入隊
- 每次遍歷檢查隊首是否有效,沒效了就出隊
- 如果運動到k了,就將隊首元素加入到結果集數組
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
if (nums == null || nums.length == 0) return new int[0];
//定下雙端隊列
LinkedList<Integer> queue = new LinkedList<>();
//定義結果集
int[] res = new int[nums.length - k + 1];
//遍歷數組
for (int i = 0; i < nums.length; i++) {
//判斷當前隊列中隊首元素是否有效
if (!queue.isEmpty() && queue.peekFirst() <= i - k) {
queue.poll();
}
//保證隊列是由大到小的,所以如果隊尾元素小於num[i],需要彈出
while (!queue.isEmpty() && nums[queue.peekLast()] <= nums[i]) {
queue.pollLast();
}
//添加當前值對應數組下標
queue.addLast(i);
//當窗口長度爲K時,保存當前窗口最大值
if (i + 1 >= k) {
res[i + 1 - k] = nums[queue.peekFirst()];
}
}
return res;
}
}
面試題59-II.隊列的最大值
和上一個滑動窗口最大值的思想一致,都是維護一個單調隊列
class MaxQueue {
private Deque<Integer> queue;
private Deque<Integer> help;
public MaxQueue() {
queue = new LinkedList<>();
help = new LinkedList<>();
}
public int max_value() {
return queue.isEmpty() ? -1 : help.peekFirst();
}
public void push_back(int value) {
queue.offerLast(value);
//維護單調隊列
while (!help.isEmpty() && value > help.peekLast()) {
help.pollLast();
}
help.offerLast(value);
}
public int pop_front() {
if (queue.isEmpty()) return -1;
int val = queue.pollFirst();
//維護單調隊列
if (help.peekFirst() == val) {
help.pollFirst();
}
return val;
}
}
抽象建模能力
面試題60.n個骰子的點數
找狀態轉移方程也就是找各個階段之間的轉化關係,同樣我們還是隻需分析最後一個階段,分析它的狀態是如何得到的。
最後一個階段也就是投擲完 n 枚骰子後的這個階段,我們用 dp[n][j]
來表示最後一個階段點數 j 出現的次數。
單看第 n 枚骰子,它的點數可能爲 1 , 2, 3, … , 6,因此投擲完 n 枚骰子後點數 j 出現的次數,可以由投擲完 n-1 枚骰子後,對應點數 j-1, j-2, j-3, … , j-6 出現的次數之和轉化過來。
for (第n枚骰子的點數 i = 1; i <= 6; i ++) {
dp[n][j] += dp[n-1][j - i]
}
總的次數爲6^n次,n次投骰子產生的結果最多5*n+1種,最後用dp統計的次數除總數得概率
class Solution {
public double[] twoSum(int n) {
int[][] dp = new int[n + 1][6 * n + 1];
//初始化
for (int j = 1; j <= 6; j++) {
dp[1][j] = 1;
}
for (int i = 2; i <= n; i++) {
for (int j = i; j <= i * 6; j++) {
for (int cur = 1; cur <= 6; cur++) {
if (j - cur <= 0) break;
dp[i][j] += dp[i - 1][j - cur];
}
}
}
//計算總數
int all = (int) Math.pow(6, n);
//n個骰子只有5*n+1中結果
double[] res = new double[5 * n + 1];
//計算每個位置的概率
for (int i = n; i <= 6 * n; i++) {
res[i - n] = dp[n][i] * 1.0 / all;
}
return res;
}
}
面試題61.撲克牌中的順子
先排序
因爲0是萬能的,統計0的個數
從非0開始遍歷
- 如果
nums[i+1]==num[i]
,則一定不是順子 - 如果
nums[i+1]-num[i]》1
,則需要用0來補位
最後判斷0的個數非負則是順子
class Solution {
public boolean isStraight(int[] nums) {
Arrays.sort(nums);
int count = 0;
//統計0個數,通過移動下標,第一個非0 的下標就是0的數目
while (count < nums.length && nums[count] == 0) {
count++;
}
for (int i = count; i < nums.length - 1; i++) {
if (nums[i + 1] == nums[i]) return false;//兩種非0牌相同,一定不是順子
if (nums[i + 1] - nums[i] > 1) {//如果差>1,用0來補位
count -= nums[i + 1] - nums[i] - 1;
}
}
//剩餘爲0的牌大於等於0
return count >= 0;
}
}
面試題62.圓圈中最後剩下的數字
約瑟夫環問題
約瑟夫環公式推導
class Solution {
public int lastRemaining(int n, int m) {
int last = 0;
for (int i = 2; i <= n; i++) {
last = (last + m) % i;
}
return last;
}
}
面試題63.股票的最大利潤
買賣股票一共有6個題,系統複習跳轉動態規劃的文章去看吧動態規劃專欄
以購買的三種狀態dp(5ms)
class Solution {
public int maxProfit(int[] prices) {
if (prices == null || prices.length < 2) return 0;
int dp[][] = new int[prices.length][3];
dp[0][0] = 0;//沒交易
dp[0][1] = -prices[0];//買入
dp[0][2] = 0;//賣出了
for (int i = 1; i < prices.length; i++) {
dp[i][0] = dp[i - 1][0];
dp[i][1] = Math.max(dp[i - 1][0] - prices[i], dp[i - 1][1]);
dp[i][2] = Math.max(dp[i - 1][1] + prices[i], dp[i - 1][2]);
}
return dp[prices.length - 1][2];
}
}
以是否持有dp(6ms)
class Solution {
public int maxProfit(int[] prices) {
if (prices == null || prices.length < 2) return 0;
int dp[][] = new int[prices.length][2];
dp[0][0] = 0;//不持有
dp[0][1] = -prices[0];//持有
for (int i = 1; i < prices.length; i++) {
dp[i][0] = Math.max(dp[i - 1][1] + prices[i], dp[i - 1][0]);
dp[i][1] = Math.max(- prices[i], dp[i - 1][1]);
}
return dp[prices.length - 1][0];
}
}
貪心(1ms)
class Solution {
public int maxProfit(int[] prices) {
if (prices == null || prices.length < 2) return 0;
int min = prices[0];
int resMax = 0;
for (int i = 1; i < prices.length; i++) {
if (prices[i] < min) min = prices[i];
else {
resMax = Math.max(resMax, prices[i] - min);
}
}
return resMax;
}
}
發散思維能力
面試題64.求1+2+…+n
如果沒有限制,那就是一個遞歸,遞歸終止條件時if n==0
沒有if這些做判斷終止條件,可以使用短路與
或者短路或
實現
短路與:如果n<=0
,此時(n > 0) && ((n += sumNums(n - 1)) > 0);
執行到(n > 0) ==fasle
就不會執行之後的數據了,然後開始返回
class Solution {
public int sumNums(int n) {
boolean b = (n > 0) && ((n += sumNums(n - 1)) > 0);
return n;
}
}
短路或,同理
class Solution {
public int sumNums(int n) {
boolean b = (n <= 0) || ((n += sumNums(n - 1)) > 0);
return n;
}
}
面試題65.不用加減乘除做加法
兩個小要點:
- 不進位加法:
a^b
- 進位:
(a&b)<<1
不進位加法a^b
進位數計算(a&b)<<1
舉例:計算12+7
12:1100
7:0111
12^7 = 1011
(12&7)<<1 = 1000
第一輪循環得到以上兩個結果,遞歸調用,求1011+1000
1011^1000 = 0011
(1011&1000)<<1 = 10011
第二輪循環得以上兩個結果,遞歸調用,求0011+10011
00011^10011 = 10000
(00011&10011)<<1 = 00011
第二輪循環得以上兩個結果,遞歸調用,求10000+00011
10000^00011 = 10011
(10000&00011)<<1 = 0
return 10011 獲得結果
迭代:
public class Solution {
public int getSum(int a, int b) {
while (b != 0) {
//不進位加法
int tmp = a ^ b;
//計算進位值
int carry = (a & b) << 1;
a = tmp;
b = carry;
}
return a;
}
}
遞歸
class Solution {
public int add(int a, int b) {
//同位a^b
//進位a&b<<1
return b == 0 ? a : add(a ^ b, (a & b) << 1);
}
}
面試題66.構建乘積數組
題意:
- 求不包含某個位置的數的其他數的積
120 = 2*3*4*5;
60 = 1*3*4*5;
40 = 1*2*4*5;
30 = 1*2*3*5;
24 = 1*2*3*4;
定義一個數組b存放結果,先一次遍歷,從1位置開始,存入i位置左邊的所有數的積
反過來從n-2位置開始,累積i右邊所有數的積
class Solution {
public int[] constructArr(int[] a) {
if (a == null || a.length == 0) return a;
int n = a.length;
int[] b = new int[n];
b[0] = 1;//初始化第一個爲1
//計算i位置左邊的數之積
int tmp = 1;
for (int i = 1; i < n; i++) {
tmp *= a[i - 1];
b[i] = tmp;
}
//計算i位置右邊的數之積
tmp = 1;
for (int i = n - 2; i >= 0; i--) {
tmp *= a[i + 1];
b[i] *= tmp;
}
return b;
}
}
面試題67.把字符串轉換成整數
邏輯判斷順序:
- 跳過首部空格
- 判斷符號(只能用if而不是while,避免多個符號),標記正負數
- 去首部0
- 檢查不是數字就返回0
- 數字進隊列或者數組
- 檢查數字是否大於10位或者10位的首位大於2,按要求返回正負最大值
- 對數字進行重組,如果數字加到10位小於0,或者負數-1小於0,說明越界了返回正負最大值
- 按符號返回值
用數組暫存
class Solution {
public int strToInt(String str) {
int index = 0;//下標
int flag = 0;//表示正負數 0表示正數,1表示負數
int len = str.length();//str長度
//去空格
while (index < len && str.charAt(index) == ' ') index++;
//去符號
if (index < len && (str.charAt(index) == '+' || str.charAt(index) == '-')) {
if (str.charAt(index) == '-') flag = 1;
index++;
}
//去首部0
while (index < len && str.charAt(index) == '0') index++;
//判斷如果非數字就返回了
if(index < len && str.charAt(index) < '1' && str.charAt(index) > '9') return 0;
//數組替代雙端隊列
int[] nums = new int[10];
int nums_index = 0;
//添加入隊列
while (index < len && str.charAt(index) >= '0' && str.charAt(index) <= '9') {
if (nums_index == 10 || nums_index == 9 && nums[0] > 2) {
return flag == 0 ? Integer.MAX_VALUE : Integer.MIN_VALUE;
}
nums[nums_index++] = str.charAt(index++) - '0';
}
int res = 0;
//實現數的重組
for (int i = nums_index - 1; i >= 0; i--) {
Integer cur = nums[i];
//每一位轉化爲對應整數
cur *= (int) Math.pow(10, nums_index - 1 - i);
res += cur;
if (flag == 0 && res < 0) {
return Integer.MAX_VALUE;
} else if (cur != 0 && flag == 1 && res - 1 < 0) {
return Integer.MIN_VALUE;
}
}
return flag == 0 ? res : -res;
}
}
用雙端隊列暫存
class Solution {
public int strToInt(String str) {
int index = 0;//下標
int flag = 0;//表示正負數 0表示正數,1表示負數
int len = str.length();//str長度
//去空格
while (index < len && str.charAt(index) == ' ') index++;
//去符號
if (index < len && (str.charAt(index) == '+' || str.charAt(index) == '-')) {
if (str.charAt(index) == '-') flag = 1;
index++;
}
//去首部0
while (index < len && str.charAt(index) == '0') index++;
//判斷如果非數字就返回了
while (index < len && str.charAt(index) < '1' && str.charAt(index) > '9') return 0;
//用雙端隊列暫存數字,方便轉數,並且方便判斷最高位值,不滿足可以提前退出
Deque<Integer> queue = new LinkedList<>();
//添加入隊列
while (index < len && str.charAt(index) >= '0' && str.charAt(index) <= '9') {
queue.push(str.charAt(index++) - '0');
}
//不符合條件的情況1(如果位數超過10位或者10位的第一位大於2,則已經越界了,按越界規則返回)
if (queue.size() > 10 || queue.size() == 10 && queue.peekLast() > 2) {
return flag == 0 ? Integer.MAX_VALUE : Integer.MIN_VALUE;
}
int res = 0;
int size = queue.size();
//實現數的重組
for (int i = 0; i < size; i++) {
Integer cur = queue.pop();
//每一位轉化爲對應整數
cur *= (int) Math.pow(10, i);
res += cur;
if (flag == 0 && res < 0) {
return Integer.MAX_VALUE;
} else if (cur != 0 && flag == 1 && res - 1 < 0) {
return Integer.MIN_VALUE;
}
}
return flag == 0 ? res : -res;
}
}
面試題68-I.二叉搜索樹的最近公共祖先
二叉搜索樹滿足左子樹的所有節點值小於根節點值,右子樹所有節點值大於根節點值
所以可以將p,q的值分成三類來遞歸
- p,q的值都大於根節點值,祖先節點一定在右子樹,在右子樹中查找
- p,q的值都小於根節點值,祖先節點一定在左子樹,在左子樹中查找
- 其他情況,如一個在左子樹,另一個在右子樹,或者有一個就是根節點,另一個在任意子樹。這些情況根節點都滿足祖先節點條件
(雙100%)
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root.val < p.val && root.val < q.val) return lowestCommonAncestor(root.right, p, q);
else if (root.val > p.val && root.val > q.val) return lowestCommonAncestor(root.left, p, q);
return root;
}
}
面試題68-II.二叉樹的最近公共祖先
遞歸查找,如果找到節點或者下探到空節點開始返回
如果左右子樹都不爲空,找到祖先節點
(雙100%)
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
//遞歸終止條件,下探發現目標節點或者到達空節點了開始出棧
if (root == null || root == p || root == q) return root;
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
//如果左右兩個節點都不爲空,說明root節點就是祖先節點
if (left != null && right != null) return root;
//當前節點不是祖先節點,返回查找結果
return left == null ? right : left;
}
}
不帶剪枝的遞歸,思路更加簡單,統計每顆子樹符合的節點有多少個 ,滿足就暫存
(30%,100%)
class Solution {
TreeNode res;
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
helper(root, p, q);
return res;
}
//尋找子樹有幾個節點符合
private int helper(TreeNode root, TreeNode p, TreeNode q) {
if (root == null) return 0;
int left = helper(root.left, p, q);
int right = helper(root.right, p, q);
int cur = left + right;
if (root == p || root == q) cur++;//如果當前節點滿足,計數+1
//如果找到了祖先節點,暫存,並將計數器置爲-1
if (cur == 2) {
res = root;
cur = -1;
}
return cur;
}
}