小标题以 _
开头的题目和解法代表独立想到思路及编码完成,其他是对题解的学习。
VsCode 搭建的 Java 环境中 sourcePath 配置比较麻烦,可以用
java main.java
运行(JDK 11 以后)
Go 的数据结构:LeetCode 支持 https://godoc.org/github.com/emirpasic/gods 第三方库。
go get github.com/emirpasic/gods
动态规划(简单)
20. 斐波那契数列
_解法1:迭代
// go
func fib(n int) int {
first, second := 0, 1
for i := 0; i < n; i++ {
second = first + second
first = second - first
first %= 1000000007
}
return first
}
// go 平行赋值
func fib2(n int) int {
a, b := 0, 1
for i := 0; i < n; i++ {
b, a = a+b, b%1000000007
}
return a
}
解法2:记忆化递归
参考我的博客:【恋上数据结构】递归
// 记忆化递归
func fib(n int) int {
if n <= 1 {
return n
}
arr := make([]int, n+1)
arr[1], arr[2] = 1, 1
return helper(arr, n)
}
func helper(arr []int, n int) int {
if arr[n] == 0 {
arr[n] = helper(arr, n-1) + helper(arr, n-2)
}
return arr[n] % 1000000007
}
解法3:动态规划
题解:面试题10- I. 斐波那契数列(动态规划,清晰图解)
// go
// 更好理解的动态规划
func fib(n int) int {
if n <= 1 {
return n
}
dp := make([]int, )
dp[0], dp[1] = 0, 1
for i := 2; i <= n; i++ {
// 递推方程:f(n) = f(n-1) + f(n-2)
dp[i] = (dp[i-1] + dp[i-2]) % 1000000007
}
return dp[n]
}
// java
// 优化过的动态规划
class Solution {
public int fib(int n) {
int a = 0, b = 1;
for (int i = 0; i < n; i++) {
int sum = (a + b) % 1000000007;
a = b;
b = sum;
}
return a;
}
}
21. 青蛙跳台阶问题
_解法1:动态规划
// go
func numWays(n int) int {
if n <= 1 {
return 1
}
dp := make([]int, n+1)
dp[0], dp[1] = 1, 1
for i := 2; i <= n; i++ {
dp[i] = (dp[i-1] + dp[i-2]) % 1000000007
}
return dp[n]
}
题解:面试题10- II. 青蛙跳台阶问题(动态规划,清晰图解)
func numWays1(n int) int {
a, b := 1, 1
for i := 0; i < n; i++ {
sum := (a + b) % 1000000007
a = b
b = sum
}
return a
}
func numWays2(n int) int {
if n <= 1 {
return 1
}
a, b := 1, 2
for i := 2; i < n; i++ {
b = (a + b)
a = b - a
b %= 1000000007
}
return b
}
22. 股票的最大利润*
此题的动规写法与下面一题【连续子数组的最大和】比较着看
_解法1:暴力
// go
func maxProfit(prices []int) int {
max := 0
for i := len(prices) - 1; i >= 0; i-- {
for j := 0; j < i; j++ {
tmp := prices[i] - prices[j]
if tmp > max {
max = tmp
}
}
}
return max
}
解法2:动态规划
// java
class Solution {
// dp(n) 代表第n天的最大利润
// dp(n) = Math.max(dp(n - 1), prices[n] - min)
// 第n天最大利润等于 第n-1天最大利润 跟 第n天股票价格和股票历史最低价的差值 相比
public int maxProfit(int[] prices) {
int len = prices.length;
if (len < 2) return 0;
int[] dp = new int[len];
dp[0] = 0;
int min = prices[0];
for (int i = 1; i < len; i++) {
min = Math.min(min, prices[i-1]);
// 状态转移方程
dp[i] = Math.max(dp[i-1], prices[i] - min);
}
return dp[len - 1];
}
}
有个细节,循环中找最小值时,这样更快:
for (int i = 1; i < len; i++) { if (prices[i] < min) min = prices[i]; dp[i] = Math.max(dp[i-1], prices[i] - min); }
像这种无需记录每次的状态值,可以使用一个变量取代 dp 数组,直接记录结果:
// java
class Solution {
/**
* 动态规划优化 - 使用一个变量即可记录结果
*/
public int maxProfit1(int[] prices) {
if (prices.length < 2) return 0;
int min = prices[0], profit = 0;
for (int i = 0; i < prices.length; i++) {
if (prices[i-1] < min)
min = prices[i-1];
profit = Math.max(profit, prices[i] - min);
}
return profit;
}
}
解法3:迭代
思路:记录一个 max,从后往前遍历,如果比 max 大就更新 max,否则计算利润。
// java
class Solution {
public int maxProfit(int[] prices) {
if (prices.length < 2) return 0;
int max = 0, res = 0;
for (int i = prices.length - 1; i >= 0; i--) {
if (prices[i] > max)
max = prices[i];
else
res = Math.max(res, max - prices[i]);
}
return res;
}
}
动态规划(中等)
23. 连续子数组的最大和*
此题的动规写法与上面一题【股票的最大利润】比较着看
[4, 1, -6, 2]
的情况有助于理解动态规划的思想。
解法1:动态规划
标准的动态规划思想:
// java
// 使用if语句写状态转移方程
class Solution {
public int maxSubArray(int[] nums) {
// dp[i]的含义:以nums[i]结尾的连续子数组的最大和
int[] dp = new int[nums.length];
dp[0] = nums[0];
for (int i = 1; i < nums.length; i++) {
if (dp[i-1] > 0) dp[i] = dp[i-1] + nums[i];
else dp[i] = nums[i];
max = Math.max(max, dp[i]);
}
// 求dp数组的max
int max = dp[0];
for (int i = 0; i < dp.length; i++) {
if (dp[i] > max) max = dp[i];
}
return max;
return max;
}
}
// java
// 使用Math.max写状态转移方程
class Solution {
public int maxSubArray(int[] nums) {
// dp[i]的含义: 以nums[i]结尾的连续子数组的最大和
int[] dp = new int[nums.length];
dp[0] = nums[0];
for (int i = 1; i < nums.length; i++) {
dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]);
}
// 求dp数组的max
int max = dp[0];
for (int i = 0; i < dp.length; i++) {
if (dp[i] > max) max = dp[i];
}
return max;
}
}
代码优化:将求最大值的循环给优化掉
// java
class Solution {
public int maxSubArray(int[] nums) {
// dp[i]的含义: 以nums[i]结尾的连续子数组的最大和
int[] dp = new int[nums.length];
int max = dp[0] = nums[0];
for (int i = 1; i < nums.length; i++) {
dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]);
max = Math.max(max, dp[i]);
}
return max;
}
}
// go
func maxSubArray(nums []int) int {
var dp = make([]int, len(nums))
dp[0] = nums[0]
max := dp[0]
for i := 1; i < len(nums); i++ {
if dp[i-1] > 0 {
dp[i] = dp[i-1] + nums[i]
} else {
dp[i] = nums[i]
}
max = int(math.Max(float64(max), float64(dp[i])))
}
return max
}
24. 礼物的最大价值*
_解法1:递归
普通递归:(超时)
// java
class Solution {
public int maxValue(int[][] grid) {
if (grid.length == 1 && grid[0].length == 1)
return grid[0][0];
return helper(grid, grid.length - 1, grid[0].length - 1);
}
int helper(int[][] grid, int x, int y) {
if (x < 0 || y < 0)
return 0;
if (x == 0) {
int res = 0;
for (int i = 0; i <= y; i++)
res += grid[0][i];
return res;
}
if (y == 0) {
int res = 0;
for (int i = 0; i <= x; i++)
res += grid[i][0];
return res;
}
return Math.max(helper(grid, x - 1, y), helper(grid, x, y - 1)) + grid[x][y];
}
}
记忆化搜索:利用额外的存储空间来存储递归时的中间值
// java
class Solution {
public int maxValue(int[][] grid) {
if (grid.length == 1 && grid[0].length == 1)
return grid[0][0];
// 存储递归结果
int[][] tmp = new int[grid.length][grid[0].length];
return helper(grid, grid.length - 1, grid[0].length - 1, tmp);
}
int helper(int[][] grid, int x, int y, int[][]tmp) {
if (tmp[x][y] != 0)
return tmp[x][y];
if (x < 0 || y < 0)
return 0;
if (x == 0) {
int res = 0;
for (int i = 0; i <= y; i++)
res += grid[0][i];
tmp[x][y] = res; // 存储递归结果
return res;
}
if (y == 0) {
int res = 0;
for (int i = 0; i <= x; i++)
res += grid[i][0];
tmp[x][y] = res; // 存储递归结果1
return res;
}
tmp[x][y] = Math.max(helper(grid, x - 1, y, tmp), helper(grid, x, y - 1, tmp))
+ grid[x][y];
return tmp[x][y];
}
}
解法2:动态规划
基础的动态规划思路:
// java
class Solution {
public int maxValue(int[][] grid) {
int m = grid.length, n = grid[0].length;
// dp[i][j] 表示从 grid[0][0] 到 grid[i-1][j-1] 时的最大价
int[][] dp = new int[m][n];
dp[0][0] = grid[0][0];
// 初始化dp最左边一列,从上到下累加
for (int i = 1; i < m; i++)
dp[i][0] = dp[i - 1][0] + grid[i][0];
// 初始化dp最上边一行,从左到右累加
for (int i = 1; i < n; i++)
dp[0][i] = dp[0][i - 1] + grid[0][i];
// 递推公式的计算
for (int i = 1; i < m; i++)
for (int j = 1; j < n; j++)
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
return dp[m - 1][n - 1];
}
}
优化:原地修改,空间复杂度优化为 O(1)。
class Solution {
public int maxValue(int[][] grid) {
int m = grid.length, n = grid[0].length;
// 初始化第一列
for (int j = 1; j < n; j++)
grid[0][j] += grid[0][j - 1];
// 初始化第一行
for (int i = 1; i < m; i++)
grid[i][0] += grid[i - 1][0];
// 递推公式的计算
for (int i = 1; i < m; i++)
for (int j = 1; j < n; j++)
grid[i][j] += Math.max(grid[i][j - 1], grid[i - 1][j]);
return grid[m - 1][n - 1];
}
}
简化代码的技巧:多开一行一列的空间,经常会使代码更简洁
class Solution {
public int maxValue(int[][] grid) {
int m = grid.length, n = grid[0].length;
int[][] dp = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++)
for (int j = 1; j <= n; j++)
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]) + grid[i - 1][j - 1];
return dp[m][n];
}
}
25. 把数字翻译成字符串
题解1:递归
思路:这题是 [青蛙跳台阶问题] 的抽象升级版。
// go
func translateNum(num int) int {
if num < 10 {
return 1
}
if num%100 >= 10 && num%100 <= 25 {
return translateNum(num/100) + translateNum(num/10)
} else {
return translateNum(num / 10)
}
}
题解2:动态规划
做动态规划的时候还是要尝试以递归的思路去思考。
func translateNum(num int) int {
// dp[i] - 以Xi结尾的数字的翻译方案
numStr := strconv.Itoa(num)
var dp = make([]int, len(numStr)+1)
dp[0], dp[1] = 1, 1
for i := 2; i <= len(numStr); i++ {
// 最后两位组成的数字
tmp := int(numStr[i-2]-'0')*10 + int(numStr[i-1]-'0')
if tmp >= 10 && tmp <= 25 {
dp[i] = dp[i-1] + dp[i-2]
} else {
dp[i] = dp[i-1]
}
}
return dp[len(numStr)]
}
26. 最长不含重复字符的子字符串
_题解1:动态规划
// go
func lengthOfLongestSubstring(s string) int {
if s == "" {
return 0
}
// dp[i] 代表以 s[i]结尾的最长不含重复字符的子字符串
var dp = make([]string, len(s))
dp[0] = s[:1]
max := len(dp[0])
for i := 1; i < len(s); i++ {
idx := strings.IndexByte(dp[i-1], s[i])
if idx == -1 {
dp[i] = dp[i-1] + string(s[i])
} else {
dp[i] = string(dp[i-1][idx+1:]) + string(s[i])
}
max = maxInt(max, len(dp[i]))
}
return max
}
func maxInt(a int, b int) int {
if a < b {
return b
}
return a
}
题解2:滑动窗口
class Solution {
public int lengthOfLongestSubstring(String s) {
int res = 0;
Set<Character> set = new HashSet<>();
for (int l = 0, r = 0; r < s.length(); r++) {
char c = s.charAt(r);
// 让左指针移动到满足条件为止
while (set.contains(c))
set.remove(s.charAt(l++));
set.add(c);
res = Math.max(res, r - l + 1);
}
return res;
}
}
双指针(简单)
27. 删除链表的节点
_解法1:迭代 + 双指针
// java
class Solution {
public ListNode deleteNode(ListNode head, int val) {
if (head.val == val) return head.next;
// cur 进行遍历, pre 保存遍历的前一位置
ListNode cur = head, pre = head;
while (cur != null) {
if (cur.val == val) {
pre.next = cur.next;
break;
}
pre = cur; // 保存遍历的前一位置
cur = cur.next;
}
return head;
}
}
_解法2:递归
评论区看到有人用递归做,立马试了试,一下就写出来了!看来刷了这么多题还是有效果的!
// java
class Solution {
public ListNode deleteNode(ListNode head, int val) {
if (head.val == val) return head.next;
ListNode node = deleteNode(head.next, val);
head.next = node;
return head;
}
}
28. 链表中倒数第 k 个节点
_解法1:暴力迭代
思路:遍历两次,第一次遍历获取链表长度,第二次遍历 n - k 次
class Solution {
public ListNode getKthFromEnd(ListNode head, int k) {
int n = 0;
ListNode cur = head;
while (cur != null) {
cur = cur.next;
n++;
}
cur = head;
while (n > k) {
cur = cur.next;
n--;
}
return cur;
}
}
_解法2:哈希表
思路:利用哈希表存储遍历的索引对应的链表头节点,遍历完获取到长度 n 后直接根据 n -k 取
class Solution {
public ListNode getKthFromEnd(ListNode head, int k) {
Map<Integer, ListNode> tmp = new HashMap<>();
ListNode cur = head;
int n = 0;
while (cur != null) {
tmp.put(n++, cur);
cur = cur.next;
}
return tmp.get(n - k);
}
}
解法3:快慢指针
题解:面试题22. 链表中倒数第 k 个节点(双指针,清晰图解)
思路:提前设置 fast 指针比 slow 指针先走 k 步,当 fast 走完,slow 就是倒数第 k 个节点
class Solution {
public ListNode getKthFromEnd(ListNode head, int k) {
ListNode slow = head, fast = head;
while (k-- > 0)
fast = fast.next;
while (fast != null) {
slow = slow.next;
fast = fast.next;
}
return slow;
}
}
29. 合并两个排序的链表
_解法1:伪头节点
题解:面试题25. 合并两个排序的链表(伪头节点,清晰图解)
思路:指针轮流遍历
// java
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode cur = new ListNode(0), resNode = cur;
while (l1 != null && l2 != null) {
if (l1.val >= l2.val) {
cur.next = new ListNode(l2.val);
l2 = l2.next;
} else {
cur.next = new ListNode(l1.val);
l1 = l1.next;
}
cur = cur.next;
}
cur.next = (l1 == null) ? l2 : l1;
return resNode.next;
}
}
题解中的写法:每次不用 new ListNode
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode dum = new ListNode(0), cur = dum;
while (l1 != null && l2 != null) {
if (l1.val < l2.val) {
cur.next = l1;
l1 = l1.next;
} else {
cur.next = l2;
l2 = l2.next;
}
cur = cur.next;
}
cur.next = (l1 != null) ? l1 : l2;
return dum.next;
}
}
解法2:递归
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if (l1 == null) return l2;
if (l2 == null) return l1;
if (l1.val <= l2.val) {
l1.next = mergeTwoLists(l1.next, l2);
return l1;
} else {
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
}
}
30. 两个链表的第一个公共节点
_解法1:哈希 / Set
思路:哈希 / Set 的思路是一样的,都是遍历第一个链表后把所有节点存起来,在遍历第二个链表的时候,尝试从 Map / Set 中去取,能取到则是公共节点。
class Solution {
/**
* 哈希
*/
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
Map<Integer, ListNode> map = new HashMap<>();
ListNode cur = headA;
while (cur != null) {
map.put(cur.val, cur);
cur = cur.next;
}
cur = headB;
while (cur != null) {
if (map.get(cur.val) == cur)
return cur;
cur = cur.next;
}
return null;
}
/**
* set
*/
public ListNode getIntersectionNode1(ListNode headA, ListNode headB) {
Set<ListNode> set = new HashSet<>();
ListNode cur = headA;
while (cur != null) {
set.add(cur);
cur = cur.next;
}
cur = headB;
while (cur != null) {
if (set.contains(cur))
return cur;
cur = cur.next;
}
return null;
}
}
解法2:双指针 + 交叉 ❤️
题解:图解 双指针法,浪漫相遇
class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if (headA == null || headB == null) return null;
ListNode A = headA, B = headB;
while (A != B) {
A = (A != null) ? A.next : headB;
B = (B != null) ? B.next : headA;
}
return A;
}
}