LeetCode 100天挑战 Day 2:验证回文串与接雨水
目录
前言
欢迎来到我的LeetCode算法100天挑战专栏第二天!今天的两道题目都非常有代表性:一道是考察字符串处理的回文串验证,另一道是经典的数组问题------接雨水。这两道题分别从不同角度训练我们的算法思维。
📊 今日题目统计
| 题目 | 难度 | 知识点 | 通过率 |
|---|---|---|---|
| 验证回文串 | 简单 | 双指针、字符串 | 47.3% |
| 接雨水 | 困难 | 数组、双指针、动态规划 | 32.8% |
题目一:验证回文串
题目描述
如果将所有大写字符转换为小写字符、并移除所有非字母数字字符之后,短语正着读和反着读都一样,则可以认为该短语是一个回文串。字母和数字都属于字母数字字符。
给你一个字符串 s,如果它是回文串,返回 true;否则,返回 false。
示例 1:
输入: s = "A man, a plan, a canal: Panama"
输出:true
解释:"amanaplanacanalpanama" 是回文串。
示例 2:
输入:s = "race a car"
输出:false
解释:"raceacar" 不是回文串。
示例 3:
输入:s = " "
输出:true
解释:在移除非字母数字字符之后,s 是一个空字符串 ""。由于空字符串正着反着读都一样,所以是回文串。
解题思路
这道题的解题思路非常直观,主要分为三个步骤:
-
预处理阶段:
- 将字符串转换为小写,统一比较标准
- 移除所有非字母数字字符,只保留有效字符
-
双指针验证:
- 使用左右两个指针,分别从字符串两端向中间移动
- 依次比较对应位置的字符是否相等
- 遇到不相等的情况立即返回false
-
边界情况处理:
- 空字符串的处理
- 单个字符的情况
- 全部为非字母数字字符的情况
代码实现分析
java
class Solution {
public boolean isPalindrome(String s) {
// 步骤1:转换为小写
s = s.toLowerCase();
// 步骤2:移除非字母数字字符
String str = s.replaceAll("[^a-z0-9]","");
// 步骤3:双指针验证
int i = 0;
int j = str.length() - 1;
while(i < j){
if(str.charAt(i) != str.charAt(j)){
return false;
}
i++;
j--;
}
return true;
}
}
代码逐行解析:
-
字符串规范化处理:
javas = s.toLowerCase(); String str = s.replaceAll("[^a-z0-9]","");toLowerCase():将所有字符转换为小写,消除大小写差异replaceAll("[^a-z0-9]",""):使用正则表达式移除所有非字母数字字符[a-z0-9]:匹配小写字母a-z和数字0-9[^...]:表示取反,匹配不在括号内的字符
-
双指针设置:
javaint i = 0; // 左指针,指向字符串开头 int j = str.length() - 1; // 右指针,指向字符串结尾 -
核心比较逻辑:
javawhile(i < j){ if(str.charAt(i) != str.charAt(j)){ return false; } i++; j--; }while(i < j):当左右指针未相遇时继续比较str.charAt(i) != str.charAt(j):比较对应位置的字符i++和j--:指针向中间移动
复杂度分析
| 指标 | 分析 | 结果 |
|---|---|---|
| 时间复杂度 | 字符串处理O(n) + 双指针比较O(n/2) | O(n) |
| 空间复杂度 | 创建了新的字符串str | O(n) |
💡 优化思考:如果不使用额外空间,可以在双指针遍历时直接跳过非字母数字字符,将空间复杂度优化到O(1)。
扩展思考
- 原地验证优化方案:
java
public boolean isPalindromeOptimized(String s) {
int left = 0, right = s.length() - 1;
while (left < right) {
// 跳过非字母数字字符
while (left < right && !Character.isLetterOrDigit(s.charAt(left))) {
left++;
}
while (left < right && !Character.isLetterOrDigit(s.charAt(right))) {
right--;
}
// 比较字符(忽略大小写)
if (Character.toLowerCase(s.charAt(left)) !=
Character.toLowerCase(s.charAt(right))) {
return false;
}
left++;
right--;
}
return true;
}
- 应用场景:
- 密码强度验证
- 用户名验证
- 数据清洗和预处理
- 文本相似度检测
题目二:接雨水
题目描述
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
示例 1:
输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。
示例 2:
输入:height = [4,2,0,3,2,5]
输出:9
初次尝试:模拟法
我的第一个想法是模拟雨水填充的物理过程。这个方法很直观,就像真实的水一样,从下往上逐层计算。
思路解析:
- 找到最高柱子的高度
- 从第0层到最高层,逐层计算可以接多少水
- 在每一层,找到左右两个边界,中间部分可以接水
java
class Solution {
public int trap(int[] height) {
int len = height.length;
// 找到最高柱子
int hight = height[0];
for(int i = 1; i < len; i++){
if(hight < height[i]){
hight = height[i];
}
}
// 逐层计算雨水量
int ans = 0;
for(int j = 0; j < hight; j++){
int temp = 0;
int index = 0;
boolean flag = false;
for(int i = 0; i < len; i++){
if((height[i] > j) && flag){
temp += i - index - 1; // 计算两个边界之间的水量
}
if(height[i] > j){
flag = true;
index = i; // 更新右边界
}
}
ans += temp;
}
return ans;
}
}
问题分析:空间与时间的权衡
提交后发现时间复杂度过高。让我分析一下问题:
时间复杂度分析:
- 找最高柱子:O(n)
- 逐层计算:O(maxHeight × n)
- 总复杂度:O(maxHeight × n)
当柱子高度很大时(比如height = [100000, 1, 100000]),时间复杂度会急剧上升。
优化方案:双指针法
既然逐层计算效率低,我们可以换一种思路:对于每个位置,计算它能接多少水。
核心思想:
对于位置i,它能接的水量取决于:
- 左边最高柱子:
leftMax[i] - 右边最高柱子:
rightMax[i] - 水量 =
min(leftMax[i], rightMax[i]) - height[i]
最终实现:动态规划优化
java
class Solution {
public int trap(int[] height) {
int len = height.length;
if (len == 0) return 0;
int ans = 0;
int[] leftMax = new int[len];
int[] rightMax = new int[len];
// 计算每个位置左边的最大高度
leftMax[0] = height[0];
for (int i = 1; i < len; i++) {
leftMax[i] = Math.max(leftMax[i-1], height[i]);
}
// 计算每个位置右边的最大高度
rightMax[len-1] = height[len-1];
for (int i = len-2; i >= 0; i--) {
rightMax[i] = Math.max(rightMax[i+1], height[i]);
}
// 计算总雨水量
for (int i = 0; i < len; i++) {
int minHeight = Math.min(leftMax[i], rightMax[i]);
ans += minHeight - height[i];
}
return ans;
}
}
代码详细解析:
- 边界情况处理:
java
if (len == 0) return 0;
空数组直接返回0。
- 预处理左边最大值:
java
leftMax[0] = height[0];
for (int i = 1; i < len; i++) {
leftMax[i] = Math.max(leftMax[i-1], height[i]);
}
leftMax[i]表示从位置0到i的最大高度。
- 预处理右边最大值:
java
rightMax[len-1] = height[len-1];
for (int i = len-2; i >= 0; i--) {
rightMax[i] = Math.max(rightMax[i+1], height[i]);
}
rightMax[i]表示从位置i到len-1的最大高度。
- 计算雨水量:
java
for (int i = 0; i < len; i++) {
int minHeight = Math.min(leftMax[i], rightMax[i]);
ans += minHeight - height[i];
}
对每个位置,取左右最大值中的较小值,减去当前高度。
复杂度分析对比
| 方法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
|---|---|---|---|---|
| 模拟法 | O(maxHeight × n) | O(1) | 空间效率高 | 时间复杂度不稳定 |
| 动态规划 | O(n) | O(n) | 时间复杂度稳定 | 需要额外空间 |
| 双指针优化 | O(n) | O(1) | 完美平衡 | 实现相对复杂 |
进一步优化的双指针方案:
java
public int trapOptimized(int[] height) {
int left = 0, right = height.length - 1;
int leftMax = 0, rightMax = 0;
int ans = 0;
while (left < right) {
if (height[left] < height[right]) {
if (height[left] >= leftMax) {
leftMax = height[left];
} else {
ans += leftMax - height[left];
}
left++;
} else {
if (height[right] >= rightMax) {
rightMax = height[right];
} else {
ans += rightMax - height[right];
}
right--;
}
}
return ans;
}
总结与思考
今天的学习让我深刻体会到了算法优化的重要性:
解题思路的演进过程:
-
从直观到优化:
- 回文串问题:从简单的字符串处理到双指针优化
- 接雨水问题:从物理模拟到数学抽象
-
复杂度分析的重要性:
- 不能只关注算法的正确性
- 时间复杂度和空间复杂度同样重要
- 需要根据具体问题选择合适的权衡
-
思考模式的转变:
- 从"怎么做"到"怎样做得更好"
- 从暴力解法寻找优化空间
- 学会分析不同解法的优劣
关键收获:
- 双指针技巧:在回文串和接雨水问题中都发挥了重要作用
- 空间换时间:有时需要牺牲空间复杂度来优化时间复杂度
- 预处理思想:通过预处理减少重复计算,提高效率
- 边界处理:良好的边界处理是算法正确性的基础
✨ 编程感悟:算法学习不仅仅是掌握具体的解题方法,更重要的是培养分析问题、优化方案的能力。每一次的思考和优化都是编程思维的提升。
参考资源
如果你觉得这篇文章对你有帮助,欢迎点赞、收藏和关注!有什么想法也可以在评论区讨论。
标签: #LeetCode #算法 #Java #双指针 #动态规划 #字符串 #数组