第2️⃣章
03 | 数组中重复的数字
问题描述
在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。
本题特点
- 数组长度与数组内数组内数字范围宽度
解题思路
- 把数字i放到num[i]上
- 如果num[i]上已经有i了,那么就是重复的数字了
代码
class Solution {
public int findRepeatNumber(int[] nums) {
int result = -999;
for (int i = 0; i < nums.length; i++) {
if (nums[i] == i) { // 已经在自己的位置上
continue;
}
// 换到换到自己的位置上,并且将换过来的数也换到自己的位置上,
while (true) {
if (nums[i] == nums[nums[i]]) { // 发现自己位置上已经是对的人了。说明重复了
return nums[i];
} else {
swap(nums, i, nums[i]);
}
if (nums[i] == i) { // i位置上是对的人了
break;
}
}
}
return result;
}
private void swap(int[] nums, int x, int y) {
int temp = nums[x];
nums[x] = nums[y];
nums[y] = temp;
}
}
其他方法分析
方法 | 时间复杂度 | 空间复杂度 | 注 |
---|---|---|---|
题解方法 | O(n) | O(1) | |
哈希表 | O(n) | O(n) | |
先排序后遍历数组 | O(nlogn) | O(1) | |
二分法 | O(nlogn) | O(1) | 不会修改原数组 |
若果不允许修改数组呢?(二分法,详情见书,时间复杂度O(nlogn))
04 | 二维数组中的查找
问题描述
在一个 n * m 的二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
解题思路
- 数组右上角(或者左下角)的元素很特殊,该元素的行中它是最大的,列中它是最小的
- 所以可以将target与它比较
- 若等于,则返回true
- 若martix[右上] < target,则martix[右上]左侧比它还要小的一行就不用考虑了
- 若martix[右上] > target,则martix[右上]下侧比它还要大的一列就不用考虑了
- 一行/一列被去掉后会出现新的martix[右上],继续判断直至没有元素可以比较
代码
class Solution {
public boolean findNumberIn2DArray(int[][] matrix, int target) {
// 空数组
if (matrix.length == 0) {
return false;
}
int x = 0;
int y = matrix[0].length - 1;
while (x != matrix.length && y != -1) {
int cur = matrix[x][y];
if (target == cur) {
return true;
} else if (target > cur) {
x++;
} else {
y--;
}
}
return false;
}
}
05 | 替换空格
问题描述
请实现一个函数,把字符串 s 中的每个空格替换成"%20"。
原题为c题目,在java中这题没有背景
解题思路
- 第一次遍历计算空格数量
- 第二次从尾到头移动数组
06 | 从尾到头打印链表
问题描述
输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。
解题思路(3种)
- 栈栈
- 递归
- 翻转链表
代码
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
//反转链表:快,但是会更改链表结构
public int[] reversePrint(ListNode head) {
ListNode cur = head, prev = null, next;
int count = 0;
while (cur != null) {
count++;
next = cur.next;
cur.next = prev;
prev = cur;
cur = next;
}
int[] ans = new int[count];
cur = prev;
for (int i = 0; cur != null; i++) {
ans[i] = cur.val;
cur = cur.next;
}
return ans;
}
//栈
// Stack<Integer> stack = new Stack<>();
// public int[] reversePrint(ListNode head) {
// while (head != null) {
// stack.push(head.val);
// head = head.next;
// }
// int[] ans = new int[stack.size()];
// for(int i = 0; i < ans.length; i++){
// ans[i] = stack.pop();
// }
// return ans;
// }
//递归:很巧妙但是很慢
// ArrayList<Integer> ans = new ArrayList<>();
// public int[] reversePrint(ListNode cur) {
// if (cur != null){
// reversePrint(cur.next);
// ans.add(cur.val);
// }
// int[] arr = new int[ans.size()];
// for (int i = 0; i < arr.length; i++) {
// arr[i] = ans.get(i);
// }
// return arr;
// }
}
07 | 重建二叉树
问题描述
输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。
解题思路
- 通过手工画图重建,想到肯能是递归
- 前序遍历:根左右;中序遍历:左根右,可以得出
- 前序的第一个节点为根节点
- 中序中根以左的是根的左子树的中序;根以右的是根的右子树的中序
- 前序中除根以外的,“左”“右”混合在一起,单凭前序无法区分,但是可以通过统计中序中根以左的个数来区分前序中的“左”“右”
- 至此已经可以通过一个前序和中序得出:1.根 2.左子树和右子树的前序和中序
- 开始递归
代码
class Solution {
HashMap<Integer, Integer> indexMap = new HashMap<>();
public TreeNode buildTree(int[] preorder, int[] inorder) {
// 建立<节点val,中序数组下标>的哈希索引
for (int i = 0; i < inorder.length; i++) {
indexMap.put(inorder[i], i);
}
TreeNode result = myBuildTree(preorder, inorder, 0 , preorder.length-1, 0, inorder.length-1);
return result;
}
// 方法参数分别为:
// preOrder、inOrder:完整的前序中序数组
// preOrderLeft、preOrderRight:前序数组的左右届
// inOrderLeft、inOrderRight:中序数组的左右届
private TreeNode myBuildTree (int[] preOrder, int[] inOrder, int preOrderLeft, int preOrderRight, int inOrderLeft, int inOrderRight) {
// 这个判断条件还没有搞明白,2021年6月28日
if (preOrderLeft > preOrderRight) {
return null;
}
// 通过前序的第一个值拿到根节点的值
int rootVal = preOrder[preOrderLeft];
// 通过该值拿到根节点在中序中的位置
int rootIndex = indexMap.get(rootVal);
// 中序中根以左全部为该根的左子树,计算节点个数,以从得到先序中右子树开始的位置
int len = rootIndex - inOrderLeft;
// 说起来比较麻烦..找个不是特别简单的前序和中序,h
TreeNode root = new TreeNode(rootVal);
root.left = myBuildTree(preOrder, inOrder, preOrderLeft + 1, preOrderLeft + len, inOrderLeft, rootIndex - 1);
root.right = myBuildTree(preOrder, inOrder, preOrderLeft + len + 1, preOrderRight, rootIndex + 1, inOrderRight);
return root;
}
}
09 | 用两个栈实现队列
问题描述
用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail 和 deleteHead ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead 操作返回 -1 )
解题思路
- 可以想象两个区域,排队区和进入区
- 入队的元素先进入排队区,出队的元素从进入区选择
- 当即将进如区为空时,将排队区的元素全部依次进入进入区(即先进去排队区的,先出进入区(将栈倒置即可))
代码
class CQueue {
Stack<Integer> s1 = new Stack<>();
Stack<Integer> s2 = new Stack<>();
public CQueue() {
}
public void appendTail(int value) {
s1.push(value);
}
public int deleteHead() {
if (s1.empty() && s2.empty()) {
return -1;
}
if (!s2.empty()) {
return s2.pop();
}
while (!s1.empty()) {
s2.push(s1.pop());
}
return s2.pop();
}
}
相关题目-225. 用两个队列实现栈
10 | 斐波那契数列/青蛙跳台阶
问题描述
一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。
解题思路
- 可以把不同的问题化解成相同的问题
- 跳台阶注意初始条件即可:f(1)= 1,f(2) = 2
代码
class Solution {
public int fib(int n) {
if (n == 0) return 0;
if (n == 1) return 1;
int fibNM2 = 0;
int fibNM1 = 1;
int fibN = -99;
for (int i = 2; i <= n; i++) {
fibN = (fibNM1 + fibNM2) % 1000000007;
fibNM2 = fibNM1;
fibNM1 = fibN;
}
return fibN;
}
// int[] fib = new int[101];
// public int fib(int n) {
// if (n == 0) return 0;
// if (n == 1) return 1;
// if (fib[n] != 0) return fib[n];
// int ans = (fib(n-1) + fib(n-2)) % 1000000007;
// fib[n] = ans;
// return ans;
// }
}
11 | 旋转数组的最小数字
题目描述
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如,数组 [3,4,5,1,2] 为 [1,2,3,4,5] 的一个旋转,该数组的最小值为1。
解题思路
-
三种情况
-
数组中只有一个元素,直接返回该元素
-
数组最开始的 0或数组长度的倍数 个元素搬到数组的末尾,也就是还是原数组
- 如果第一个
numbers[0] < numbers[numbers.length - 1]
就是这种情况,直接返回numbers[0]
- 如果第一个
-
正常的经过旋转与原数组不同的数组,这样的数组有一个特征:以最小值为分界点,分为两个单调递增的数组
- 左侧数组 >= 右侧数组,所以比q大的mid位于左侧数组,比q小的mid位于右侧数组
- 通过二分法一次排除掉一半的元素,直至
p + 1 = q
,此时numbers[q]
为最小值 - 其中特例:当p、q、mid相等时,无法判断mid处于哪个数组,采用普通方法解决
-
代码
class Solution {
public int minArray(int[] numbers) {
int p = 0;
int q = numbers.length - 1;
int mid;
// 数组中只有一个元素
if (p == q) {
return numbers[p];
}
// 未经旋转的数组,或旋转后与原数组相同,单调递增
if(numbers[p] < numbers[q]) {
return numbers[p];
}
// p、q会慢慢靠近分界点的,pq挨着时,numbers[q]为最小值
while (q - p != 1) {
mid = (p + q) / 2;
// p q mid相等时,无法判断mid处于哪个数组,采用普通方法解决
if (numbers[p] == numbers[q] && numbers[q] == numbers[mid]) {
return min(numbers, p, q);
}
// 二分法
// 左侧数组 >= 右侧数组,所以比q大的mid位于左侧数组,比q小的mid位于右侧数组
if (numbers[mid] >= numbers[p]) {
p = mid;
} else if (numbers[mid] <= numbers[q]) {
q = mid;
}
}
return numbers[q];
}
private int min(int[] numbers, int p, int q) {
int last = numbers[q];
int cur;
for (int i = p; i <= q; i++) {
cur = numbers[i];
if (cur < last) {
return cur;
}
last = cur;
}
// 都相等的情况
return last;
}
}
12 | 矩阵中的路径
问题描述
给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
例如,在下面的 3×4 的矩阵中包含单词 “ABCCED”(单词中的字母已标出)。
解题思路
- 深搜,失败就回溯
代码
class Solution {
boolean[][] visited;
int m;
int n;
public boolean exist(char[][] board, String word) {
m = board.length;
n = board[0].length;
visited = new boolean[m][n];
// 要找的是第几个字符
int num;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
// 每一个格子都可能是起点
num = 0; // 重置num
if (isOk(board, word, i, j, num, "init")) {
return true;
}
}
}
return false;
}
private boolean isOk(char[][] board, String word, int p, int q, int num, String mod) {
if (mod == "up") {
p--;
} else if (mod == "down") {
p++;
} else if (mod == "left") {
q--;
} else if (mod == "right") {
q++;
}
// 越界、不是要找的字母、已访问
if (p < 0 || p > m - 1 || q < 0 || q > n - 1 || board[p][q] != word.charAt(num) || visited[p][q] == true) {
return false;
} else { // 如果这个格子ok,那么需要看下他周围下一个字符
// 找下一个字符
num++;
// 如果找完了
if (num == word.length()) {
return true;
}
// 标记当前格子是ok的
visited[p][q] = true;
// 周围是否有ok的格子
boolean isOk = isOk(board,word,p,q,num,"up") || isOk(board,word,p,q,num,"down")
|| isOk(board,word,p,q,num,"left") || isOk(board,word,p,q,num,"right");
// 有两种情况会执行下面这句:1.周围4个格子都不符合要求,那么当前格子也就不符合要求了,回溯;2.成功,一路true返回,这是不执行这就也没事
visited[p][q] = false;
return isOk;
}
}
}
13. | 机器人的运动范围
问题描述
地上有一个m行n列的方格,从坐标 [0,0] 到坐标 [m-1,n-1] 。一个机器人从坐标 [0, 0] 的格子开始移动,它每次可以向左、右、上、下移动一格(不能移动到方格外),也不能进入行坐标和列坐标的数位之和大于k的格子。例如,当k为18时,机器人能够进入方格 [35, 37] ,因为3+5+3+7=18。但它不能进入方格 [35, 38],因为3+5+3+8=19。请问该机器人能够到达多少个格子?
解题思路
- dfs
- 去过的地方用
visited[][]
标记 - 根据本题的神秘规律,只需向下和向右探索即可得到所有解
代码
class Solution {
int count = 0;
int m, n, k;
boolean[][] visited;
public int movingCount(int m, int n, int k) {
this.m = m;
this.n = n;
this.k = k;
this.visited = new boolean[m][n];
// dfs
canMoveTo(0, 0);
return count;
}
private void canMoveTo(int p, int q) {
if (p > m - 1 || q > n - 1 || visited[p][q] == true || sum(p) + sum(q) > k) {
return;
} else {
visited[p][q] = true;
count++;
// 只向右和向下
canMoveTo(p + 1, q);
canMoveTo(p, q + 1);
}
}
private int sum (int num) {
int sum = 0;
sum += num / 10;
sum += num % 10;
return sum;
}
}
14| 剪绳子|剪绳子 II
问题描述
给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]…k[m - 1] 。请问 k[0]k[1]…*k[m - 1] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。
Ⅱ:答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
解题思路
- 动态规划
- 自下而上
- 贪心
代码
class Solution {
public int cuttingRope(int n) {
if (n == 2) {
return 1;
}
if (n == 3) {
return 2;
}
int[] products = new int[n + 1];
int max;
products[2] = 2;
products[3] = 3;
for (int i = 4; i <= n; i++) {
max = 0;
for (int j = 2; j <= i / 2; j++) {
max = Math.max(max, products[j] * products[i - j]);
}
products[i] = max;
}
return products[n];
}
}
15 | 二进制中1的个数
问题描述
请实现一个函数,输入一个整数,输出该数二进制表示中 1 的个数。例如,把 9表示成二进制是 1001,有 2 位是 1。因此,如果输入 9,则该函数输出 2。
解题思路
- 方法1:
- 循环检测法:将n与1、100、1000…(二进制)做&运算,若不等于0,可以证明该位为1
- 方法2:
- 将二进制数 -1 即可将二进制数最低位的 1 置位 0 ,比该 1 位数还低的 0 置位 1,如1100 - 1 = 1011
- 将 -1 后的数与原数作 & 操作,即可将原数中最低位的 1 去除
代码
public class Solution {
// 循环检测二进制位
public int hammingWeight(int n) {
int count = 0;
for (int i = 0; i < 32; i++) {
if ((n & 1 << i) != 0) {
count++;
}
}
return count;
}
}
public class Solution {
// 技巧:n & (n - 1)可以去掉最右边的0
public int hammingWeight(int n) {
int count = 0;
while (n != 0) {
count ++;
n = n & (n - 1);
}
return count;
}
}
第3️⃣章
16 | 数值的整数次方
问题描述
实现 pow(x, n) ,即计算 x 的 n 次幂函数(即,xn)。不得使用库函数,同时不需要考虑大数问题。
解题思路
快速幂
代码
class Solution {
public double myPow(double x, int n) {
double result;
// 1的任何次幂还是1
if (Double.compare(x, 1.0) == 0) return x;
// 任何数的1次幂还是本身
if (n == 1) return x;
// 任何数的0次幂是1
if (n == 0) return 1;
int oldn = n;
// 指数小于1就转化一下
if (n < 0) {
x = 1/x;
// 特殊情况:对Integer.MIN_VALUE取负取绝对值都还是Integer.MIN_VALUE
// Integer.MAX_VALUE == 2147483647
// Integer.MIN_VALUE == -2147483648
// 后面需要补一次乘法
if (n == Integer.MIN_VALUE) {
n = Integer.MAX_VALUE;
} else {
n = -n;
}
}
result = fastPow(x, n);
if (oldn == Integer.MIN_VALUE) {
result *= x;
}
return result;
}
// 快速幂
private double fastPow(double x, int n) {
if (n == 1) {
return x;
}
double result;
result = fastPow(x, n >> 1);
result = result * result;
if((n & 1) == 1) {
result *= x;
}
return result;
}
}
22 | 链表中倒数第k个节点
题目描述
输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。例如,一个链表有6个节点,从头节点开始,它们的值依次是1、2、3、4、5、6。这个链表的倒数第3个节点是值为4的节点。
解题思路
- 双指针
- 倒数第k个节点,与倒数第1个节点相差k-1步
- 设置双指针初始为起点,让j先走k-1步,此时i与j的距离与倒k节点与倒1节点距离相等
- 将i,j同时后移直至j抵达链表末尾即可
代码
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode getKthFromEnd(ListNode head, int k) {
ListNode i = head,j = head;
for (int m = 0; m < k-1; m++) {
j = j.next;
}
while (j.next != null) {
i = i.next;
j = j.next;
}
return i;
}
}
24 | 反转链表
问题描述
定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。
解题思路
太简单
代码
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode reverseList(ListNode head) {
ListNode cur = head, next, prev = null;
while (cur != null) {
next = cur.next;
cur.next = prev;
prev = cur;
cur = next;
}
return prev;
}
}
第4️⃣章
30 | 包含min函数的栈
问题描述
定义栈的数据结构,请在该类型中实现一个能够得到栈的最小元素的 min 函数在该栈中,调用 min、push 及 pop 的时间复杂度都是 O(1)。
解题思路
- 栈顶元素为最小元素时,若进行出栈操作,则需要对栈内所有元素进行比较,复杂度:O(N)
- 使用辅助栈来存储各个时刻栈内的最小元素
代码
class MinStack {
Stack<Integer> stack = new Stack<>();
Stack<Integer> auxStack = new Stack<>();
/** initialize your data structure here. */
public MinStack() {
}
public void push(int x) {
stack.push(x);
if (auxStack.empty() || x < auxStack.peek()) {
auxStack.push(x);
} else {
auxStack.push(auxStack.peek());
}
}
public void pop() {
stack.pop();
auxStack.pop();
}
public int top() {
return stack.peek();
}
public int getMin() {
return auxStack.peek();
}
}
第5️⃣章
39 | 数组中出现次数超过一半的数字
题目描述
数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。你可以假设数组是非空的,并且给定的数组总是存在多数元素。
解题思路
- 摩尔投票法
- 如果两个数不相同就让他们抵消,会分为两种情况(其中包含众数,与其中不包含众数),但无论哪种情况,数组中剩余元素的众数 与 原先众数还是一样的。
- 可以拓展为:数组中连续的2*k个元素,若其中有一半(k个)元素是相同的,则他们可以和另一半(k个)抵消。
- xxxx
代码
//摩尔投票法
class Solution {
public int majorityElement(int[] nums) {
int count = 0;
int ans = 0;
for (int i = 0; i < nums.length; i++) {
if (count == 0) {
ans = nums[i];
}
if (nums[i] == ans) {
count++;
} else {
count--;
}
}
return ans;
}
}
52 | 两个链表的第一个公共节点
问题描述
输入两个链表,找出它们的第一个公共节点。
解题思路
- 则设A链表与B链表不相交的部分为a和b,相交的部分(若有)为c
- 若相交则 a + c + b == b + c + a,若不相交则 a + b == b + a
- 可见若将A的尾与B的头连,B的尾与A的头连,则两链表会在某一时刻相交(或都为null)
代码
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode a = headA, b = headB;
while (a != b) {
if (a == null) {
a = headB;
} else {
a = a.next;
}
if (b == null) {
b = headA;
} else {
b = b.next;
}
}
if (a == null) {
return null;
} else {
return a;
}
}
}
第6️⃣章
57 | 和为s的两个数字
题目描述
输入一个递增排序的数组和一个数字s,在数组中查找两个数,使得它们的和正好是s。如果有多对数字的和等于s,则输出任意一对即可。
解题思路
- 少了就加一点,多了就减一点
代码
class Solution {
public int[] twoSum(int[] nums, int target) {
int i = 0, j = nums.length - 1;
int sum = 0;
while (true) {
sum = nums[i] + nums[j];
if (sum == target) {
break;
} else if (sum < target){
i++;
} else {
j--;
}
}
return new int[]{nums[i],nums[j]};
}
}
57-II | 和为s的连续正数序列
题目描述
输入一个正整数 target ,输出所有和为 target 的连续正整数序列(至少含有两个数)。
序列内的数字由小到大排列,不同序列按照首个数字从小到大排列。
解题思路
- 少了就加一点,多了就减一点
代码
class Solution {
public int[][] findContinuousSequence(int target) {
int i = 1,j = 2;
int sum = 3;
ArrayList<int[]> ans = new ArrayList<>();
while (i <= target/2)
{
if (sum > target) {
sum -= i;
i++;
} else if (sum < target){
j++;
sum += j;
}
if (sum == target) {
int[] temp = new int[j-i+1];
for (int k = i; k <= j; k++){
temp[k-i] = k;
}
ans.add(temp);
sum -= i;
i++;
}
}
return ans.toArray(new int[ans.size()][]);
}
}