【力扣算法日记】无重复字符的最长子串

最近刷了很多算法题,这些解题过程也拓展了自己的思路,是个适合记录的素材。所以决定在继技术知识点详解的【一文系列 】之后,开启新坑------【力扣算法系列】,来记录力扣刷题过程。

分享题目不确定,目前打算只分享我认为值得分享的题目,或者你们有什么想要我分享的题目,我愿意去研究透彻之后出分析详解!分享的过程也是自我学习的过程!

那么今天,我们分享第一道题目:无重复字符的最长子串

题目

无重复字符的最长子串

给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。

来源:https://leetcode.cn/problems/longest-substring-without-repeating-characters/description/

示例 1:

输入: s = "abcabcbb"

输出: 3

解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

示例 2:

输入: s = "bbbbb"

输出: 1

解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

示例 3:

输入: s = "pwwkew"

输出: 3

解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。

请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。

提示:

0 <= s.length <= 5 * 104

s 由英文字母、数字、符号和空格组成


解题

暴力法

这是最容易想到的方法,当然也是最废的方法。不过我们在解题的时候,可以先尝试暴力解法,然后再一步步着手优化,或想出更好的方法,这也是一种解题思路。在这道题目中,暴力解法就是逐个检查所有的子字符串,看它是否不含有重复的字符。

java 复制代码
public static int lengthOfLongestSubstring(String s) {
    char[] chars = s.toCharArray();
    Map<Character, Integer> subChars = new HashMap<Character, Integer>(chars.length);
    int maxSize = 0;
    int repeatIndex = -1;
    int i = 0;
    while (i < chars.length) {
        for (int j = i; j < chars.length; j++) {
            char c = chars[j];
            if (subChars.containsKey(c)) {
                repeatIndex = subChars.get(c);
                break;
            }
            subChars.put(c, j);
        }
        if (repeatIndex != -1) {
            // 找到重复元素,从第一个重复元素的下一个下标开始遍历
            i = repeatIndex + 1;
            repeatIndex = -1;
        } else {
            i++;
        }
        maxSize = Math.max(maxSize, subChars.size());
        subChars.clear();
        float middleSize = chars.length / 2f;
        if (maxSize >= middleSize && i >= middleSize) {
            // 最大子串长度超过一半了,可以不用继续遍历了
            break;
        }
    }
    return maxSize;
}

滑动窗口

滑动窗口是字符串类问题的最常用的方法。在这道题目中,我们按区间进行字符串搜索,遇到重复字符,就从左区间开始删除,直到删除掉重复字符为止。

在这道题目中,滑动窗口其实相当于就是一个队列,比如例题中的 abcabcbb:

  1. 进入这个队列(窗口)为 abc,暂时满足题目要求

  2. 当再进入 a,队列变成了 abca

  3. 这时候不满足要求,所以,我们要移动这个队列,把队列的左边的元素移出就行了,一直到满足题目要求。

一直维持这样的队列,找出队列出现最长的长度时候,我们就求得最终答案。

java 复制代码
public int lengthOfLongestSubstring(String s) {
    int len = s.length();
    // 最大长度
    int maxLength = 0;
    // 左指针
    int left = 0;
    // 右指针
    int right = 0;
    // 子串队列
    Set<Character> subChars = new HashSet<>();
    while (left < len && right < len) {
        if (subChars.contains(s.charAt(right))) {
            // 子串重复,字符串从队首开始移除
            subChars.remove(s.charAt(left));
            // 移动左指针
            left++;
        } else {
            // 子串不重复,字符入队尾
            subChars.add(s.charAt(right));
            right++;
            // 记录最长不重复长度
            maxLength = Math.max(maxLength, right - left);
        }
    }
    return maxLength;
}

优化的滑动窗口

在滑动窗口时,上述的方法最多需要执行 2n 个步骤。事实上,它可以被进一步优化为仅需要 n 个步骤。我们可以定义字符到索引的映射,而不是使用集合来判断一个字符是否存在。 当我们找到重复的字符时,我们可以立即跳过该窗口。

java 复制代码
public static int lengthOfLongestSubstring(String s) {
    int len = s.length();
    // 最大长度
    int maxLength = 0;
    // 使用Map结构缓存字符和索引的映射
    Map<Character, Integer> map = new HashMap<>();
    // 滑动窗口
    for (int right = 0, left = 0; right < len; right++) {
        char c = s.charAt(right);
        if (map.containsKey(c)) {
            // 找到重复子串,左指针跳到最后的重复位置
            left = Math.max(map.get(c), left);
        }
        int curLength = right - left + 1;
        // 取最大长度
        maxLength = Math.max(maxLength, curLength);
        // 缓存字符和下标映射
        map.put(c, right + 1);
    }
    return maxLength;
}

优化的滑动窗口2

针对上述优化方式,我们还可以进一步优化,不过这个方法相对有些投机取巧,各位看看就行

java 复制代码
public static int lengthOfLongestSubstring(String s) {
//      Java(假设字符集为 ASCII 128)
//      以前我们没有对字符串 s 所使用的字符集进行假设。
//      当我们知道该字符集比较小的时侯,我们可以用一个整数数组作为直接访问表来替换 Map。
    int len = s.length();
    // 最大长度
    int maxLength = 0;
    // 使用128位数组缓存下标
    int[] index = new int[128];
    // 滑动窗口
    for (int right = 0, left = 0; right < len; right++) {
        char c = s.charAt(right);
        // 取index对应字符的缓存下标
        // 如果没有,则是默认值0
        // 若一旦有非零值,则代表有相同字符缓存到index数组中,则下标可跳到最后重复为止,再计算出不重复长度
        left = Math.max(index[c], left);
        int curLength = right - left + 1;
        // 取最大长度
        maxLength = Math.max(maxLength, curLength);
        // 缓存字符和下标映射
        index[c] = right + 1;
    }
    return maxLength;
}

这道题的解法中,暴力解法一定是不可取的,其实能写出滑动窗口解法就差不多了,优化方式都是加分项了。当然了,解题方法一定不止于此,大家可以继续深究!


更多技术干货,欢迎关注我!

相关推荐
AI软著研究员2 小时前
程序员必看:软著不是“面子工程”,是代码的“法律保险”
算法
FunnySaltyFish2 小时前
什么?Compose 把 GapBuffer 换成了 LinkBuffer?
算法·kotlin·android jetpack
颜酱3 小时前
理解二叉树最近公共祖先(LCA):从基础到变种解析
javascript·后端·算法
地平线开发者19 小时前
SparseDrive 模型导出与性能优化实战
算法·自动驾驶
董董灿是个攻城狮19 小时前
大模型连载2:初步认识 tokenizer 的过程
算法
地平线开发者20 小时前
地平线 VP 接口工程实践(一):hbVPRoiResize 接口功能、使用约束与典型问题总结
算法·自动驾驶
罗西的思考20 小时前
AI Agent框架探秘:拆解 OpenHands(10)--- Runtime
人工智能·算法·机器学习
HXhlx1 天前
CART决策树基本原理
算法·机器学习
Wect1 天前
LeetCode 210. 课程表 II 题解:Kahn算法+DFS 双解法精讲
前端·算法·typescript
颜酱1 天前
单调队列:滑动窗口极值问题的最优解(通用模板版)
javascript·后端·算法