LeetCode100天Day2-验证回文串与接雨水

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 是一个空字符串 ""。由于空字符串正着反着读都一样,所以是回文串。

解题思路

这道题的解题思路非常直观,主要分为三个步骤:

  1. 预处理阶段

    • 将字符串转换为小写,统一比较标准
    • 移除所有非字母数字字符,只保留有效字符
  2. 双指针验证

    • 使用左右两个指针,分别从字符串两端向中间移动
    • 依次比较对应位置的字符是否相等
    • 遇到不相等的情况立即返回false
  3. 边界情况处理

    • 空字符串的处理
    • 单个字符的情况
    • 全部为非字母数字字符的情况

代码实现分析

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;
    }
}
代码逐行解析:
  1. 字符串规范化处理

    java 复制代码
    s = s.toLowerCase();
    String str = s.replaceAll("[^a-z0-9]","");
    • toLowerCase():将所有字符转换为小写,消除大小写差异
    • replaceAll("[^a-z0-9]",""):使用正则表达式移除所有非字母数字字符
    • [a-z0-9]:匹配小写字母a-z和数字0-9
    • [^...]:表示取反,匹配不在括号内的字符
  2. 双指针设置

    java 复制代码
    int i = 0;                    // 左指针,指向字符串开头
    int j = str.length() - 1;     // 右指针,指向字符串结尾
  3. 核心比较逻辑

    java 复制代码
    while(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)。

扩展思考

  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;
}
  1. 应用场景
  • 密码强度验证
  • 用户名验证
  • 数据清洗和预处理
  • 文本相似度检测

题目二:接雨水

题目描述

给定 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

初次尝试:模拟法

我的第一个想法是模拟雨水填充的物理过程。这个方法很直观,就像真实的水一样,从下往上逐层计算。

思路解析:
  1. 找到最高柱子的高度
  2. 从第0层到最高层,逐层计算可以接多少水
  3. 在每一层,找到左右两个边界,中间部分可以接水
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;
    }
}
代码详细解析:
  1. 边界情况处理
java 复制代码
if (len == 0) return 0;

空数组直接返回0。

  1. 预处理左边最大值
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的最大高度。

  1. 预处理右边最大值
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的最大高度。

  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;
}

总结与思考

今天的学习让我深刻体会到了算法优化的重要性:

解题思路的演进过程:

  1. 从直观到优化

    • 回文串问题:从简单的字符串处理到双指针优化
    • 接雨水问题:从物理模拟到数学抽象
  2. 复杂度分析的重要性

    • 不能只关注算法的正确性
    • 时间复杂度和空间复杂度同样重要
    • 需要根据具体问题选择合适的权衡
  3. 思考模式的转变

    • 从"怎么做"到"怎样做得更好"
    • 从暴力解法寻找优化空间
    • 学会分析不同解法的优劣

关键收获:

  • 双指针技巧:在回文串和接雨水问题中都发挥了重要作用
  • 空间换时间:有时需要牺牲空间复杂度来优化时间复杂度
  • 预处理思想:通过预处理减少重复计算,提高效率
  • 边界处理:良好的边界处理是算法正确性的基础

编程感悟:算法学习不仅仅是掌握具体的解题方法,更重要的是培养分析问题、优化方案的能力。每一次的思考和优化都是编程思维的提升。


参考资源

  1. LeetCode 官方题库
  2. Java String 类文档
  3. 双指针算法详解
  4. 动态规划基础教程
  5. 算法复杂度分析指南

如果你觉得这篇文章对你有帮助,欢迎点赞、收藏和关注!有什么想法也可以在评论区讨论。


标签: #LeetCode #算法 #Java #双指针 #动态规划 #字符串 #数组

相关推荐
YoungHong19922 小时前
面试经典150题[073]:从中序与后序遍历序列构造二叉树(LeetCode 106)
leetcode·面试·职场和发展
清晓粼溪2 小时前
Java登录认证解决方案
java·开发语言
业精于勤的牙2 小时前
浅谈:算法中的斐波那契数(五)
算法·leetcode·职场和发展
液态不合群2 小时前
查找算法详解
java·数据结构·算法
雨中飘荡的记忆2 小时前
观察者模式:从理论到生产实践
java·设计模式
北城以北88882 小时前
SpringBoot--Redis基础知识
java·spring boot·redis·后端·intellij-idea
wniuniu_2 小时前
ceph中的rbd的稀疏写入
java·服务器·数据库
2201_757830872 小时前
条件分页查询
java·开发语言
LYFlied2 小时前
【每日算法】LeetCode 105. 从前序与中序遍历序列构造二叉树
数据结构·算法·leetcode·面试·职场和发展