本文的题目均来自LeetCode的剑指offer题库
本文的分类参考自书籍《剑指offer》
代码均采用Java实现,且大多都是最优解
码这篇文章的目的是方便自己复习看,所以很多代码是经过优化的,并且几乎没有题解,只是提了提思路。如果第一次刷的不建议只看,建议看看思路然后自己去官方站做,如果看不懂可以看很多大佬的题解
点击问题标题可直达LeetCode中文站刷题!!!
基础知识
数据结构
面试题03.数组中重复的数字
空间优先:原地排序法
遍历数组,将遍历的数试探放入值正确位置,如果正确位置已经有过正确数了,且不是当前位置,说明发现了重复数了,退出
如果正确位置不是正确值,将正确值交换到正确位置
(93%,100%)
class Solution {
public int findRepeatNumber(int[] nums) {
int i = 0;
while (i < nums.length){
if (nums[i] == i){
i++;
continue;
}
int x = nums[i];
if (nums[x] == x){
return x;
}
int temp = nums[i];
nums[i] = nums[x];
nums[x] = temp;
}
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;
//去掉首位空格
s = s.trim();
boolean numFlag = fal