本系列为笔者的 Leetcode 刷题记录,顺序为 Hot 100 题官方顺序,根据标签命名,记录笔者总结的做题思路,附部分代码解释和疑问解答,01~07为C++语言,08及以后为Java语言。
01 爬楼梯


java
class Solution {
public int climbStairs(int n) {
int a = 0, b = 0, c = 1; //边界条件
for(int i=0; i<n; i++){
a = b;
b = c;
c = a + b; //转移方程
}
return c;
}
}
/**
边界条件:a = 0, b = 0, c = 1;
转移方程:c = a + b;
*/
02 杨辉三角


java
class Solution {
public List<List<Integer>> generate(int numRows) {
List<List<Integer>> c = new ArrayList<>(numRows);
c.add(List.of(1)); //边界条件
//一行一行计算
for(int i=1; i<numRows; i++){
List<Integer> row = new ArrayList<>(i + 1);
row.add(1);
for(int j=1; j<i; j++){
row.add(c.get(i - 1).get(j - 1) + c.get(i - 1).get(j)); //转移方程
}
row.add(1);
c.add(row);
}
return c;
}
}
/**
边界条件:c.add(List.of(1));
转移方程:row.add(c.get(i - 1).get(j - 1) + c.get(i - 1).get(j));
*/
① c.add(List.of(1));
啥意思?
List.of(1)
:这是Java 9引入的一个静态方法,用来快速创建一个不可变(immutable)的列表,这里创建了一个只包含元素1
的列表。
② List<List<Integer>> c = new ArrayList<>(numRows);
中(numRows)
啥意思?
这里的 numRows
作用是给 ArrayList
指定一个初始容量 (initial capacity
),ArrayList
底层是用数组实现的,当你创建一个 ArrayList
时,如果指定了初始容量,ArrayList
会提前申请这么大容量的底层数组,避免插入元素时频繁扩容,提升效率。
03 打家劫舍



java
class Solution {
public int rob(int[] nums) {
//特殊情况判断
if(nums.length == 0){
return 0;
}
int n = nums.length;
int[] dp = new int[n + 1];
dp[0] = 0;
dp[1] = nums[0]; //边界条件
for(int i=2; i<=n; i++){
dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i-1]); //转移方程
}
return dp[n];
}
}
/**
边界条件:dp[0] = 0; dp[1] = nums[0];
转移方程:dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i-1]);
*/
04 完全平方数


java
class Solution {
public int numSquares(int n) {
int[] dp = new int[n + 1];
for(int i=1; i<=n; i++){ //[1, n]
dp[i] = i; //边界条件
for(int j=1; j*j <= i; j++){ //[1, i]
dp[i] = Math.min(dp[i], dp[i - j * j] + 1); //转移方程
}
}
return dp[n];
}
}
/**
边界条件:dp[i] = i;
转移方程:dp[i] = Math.min(dp[i], dp[i - j * j] + 1);
*/
05 零钱兑换


java
class Solution {
public int coinChange(int[] coins, int amount) {
int max = amount + 1;
int[] dp = new int[amount + 1];
Arrays.fill(dp, max);
dp[0] = 0; //边界条件
for(int i=1; i<=amount; i++){ //遍历dp数组
for(int j=0; j<coins.length; j++){ //遍历coins数组
if(coins[j] <= i){
dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1); //转移方程
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
}
/**
边界条件:dp[0] = 0;
转移方程:dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
*/
① Arrays.fill(dp, max);
啥意思?
dp
是一个数组,max
是一个变量,表示你想赋给数组每个元素的值,Arrays.fill
是 Java 标准库中的一个静态方法,用来快速初始化或重置数组的值。
② return dp[amount] > amount ? -1 : dp[amount];
为啥就这么确定能找出正确组合,万一是1、2、5
块的零钱,要求凑13
,凑出来5、5、5
块,返回-1
了怎么办?
dp[0] = 0
,其他dp[i]
初始化为一个较大值(通常是amount+1
或者类似的无穷大),对每一个硬币coin
,遍历i
从coin
到amount
,执行:
dp[i] = min(dp[i], dp[i - coin] + 1);
如果dp[i - coin]
不可达(即是初始化的无穷大),则dp[i]
不会被更新。
06 字符串拼接


java
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
Set<String> wordDictSet = new HashSet(wordDict);
boolean[] dp = new boolean[s.length() + 1];
dp[0] = true; //边界条件
for(int i=1; i<=s.length(); i++){ //外层循环:i表示当前考虑字符串的前i个字符
for(int j=0; j<i; j++){ //内层循环:j表示字符串拆分点
if(dp[j] && wordDictSet.contains(s.substring(j, i))){ //转移方程
dp[i] = true;
break;
}
}
}
return dp[s.length()];
}
}
/**
边界条件:dp[0] = true;
转移方程:dp[j] && wordDictSet.contains(s.substring(j, i))
*/
07 最长递增子序列


java
class Solution {
public int lengthOfLIS(int[] nums) {
if(nums.length == 0){
return 0;
}
int ans = 1;
int[] dp = new int[nums.length];
dp[0] = 1;
for(int i=1; i<nums.length; i++){
dp[i] = 1;
for(int j=0; j<i; j++){
if(nums[i] > nums[j]){
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
ans = Math.max(ans, dp[i]);
}
return ans;
}
}
/**
边界条件:dp[0] = 1;
转移方程:dp[i] = Math.max(dp[i], dp[j] + 1);
*/
for(int j=0; j<i; j++)
为什么不是j
从i
到0
,j--
,不然感觉好奇怪,感觉不是"连续"的递增子序列?
题目求的是"最长递增子序列(LIS)",这里的子序列(subsequence)不是必须连续的。也就是说,元素的索引不必连续,只要保持递增顺序即可。
08 乘积最大子序列


java
class Solution {
public int maxProduct(int[] nums) {
int max = Integer.MIN_VALUE;
int imax = 1, imin = 1;
for(int i=0; i<nums.length; i++){
if(nums[i] < 0){
int temp = imax;
imax = imin;
imin = temp;
}
imax = Math.max(nums[i], imax * nums[i]);
imin = Math.min(nums[i], imin * nums[i]);
max = Math.max(max, imax);
}
return max;
}
}
09 分割等和子集

java
class Solution {
public boolean canPartition(int[] nums) {
int n = nums.length;
if(n < 2){
return false;
}
int sum = 0, maxNum = 0;
for(int num : nums){
sum += num;
maxNum = Math.max(maxNum, num);
}
if(sum % 2 != 0){
return false;
}
int target = sum / 2;
if(maxNum > target){
return false;
}
//dp[i][j]
//i 遍历元素
//j 遍历目标值
boolean[][] dp = new boolean[n][target + 1];
for(int i=0; i<n; i++){
dp[i][0] = true;
}
dp[0][nums[0]] = true;
for(int i=1; i<n; i++){ //出错:i=1
for(int j=0; j<=target; j++){
if(j >= nums[i]){
dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i]];
}else{
dp[i][j] = dp[i-1][j];
}
}
}
return dp[n-1][target];
}
}
10 最长有效括号


方法一:动态规划(简单)
java
class Solution {
public int longestValidParentheses(String s) {
int ans = 0;
int[] dp = new int[s.length()];
for(int i=1; i<s.length(); i++){
if(s.charAt(i) == ')'){
if(s.charAt(i-1) == '('){
dp[i] = (i>=2 ? dp[i-2] : 0) + 2;
}else if(i-dp[i-1] > 0 && s.charAt(i-dp[i-1]-1) == '('){
dp[i] = dp[i-1] + (i-dp[i-1] >= 2 ? dp[i-dp[i-1]-2] : 0) + 2;
}
ans = Math.max(ans, dp[i]);
}
}
return ans;
}
}
方法二:栈

java
class Solution {
public int longestValidParentheses(String s) {
int ans = 0;
Deque<Integer> stack = new LinkedList<>(); //记录括号下标
stack.push(-1); //初始化
for(int i=0; i<s.length(); i++){
if(s.charAt(i) == '('){
stack.push(i);
}else{
stack.pop();
if(stack.isEmpty()){
stack.push(i); //记录最后一个未被匹配的右括号下标
}else{
ans = Math.max(ans, i - stack.peek()); //可能是左括号、可能是右括号
}
}
}
return ans;
}
}