本文的题目均来自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;
}
}