LeetCode 无重复字符的最长子串 打败100%的人

简介: LeetCode 无重复字符的最长子串 打败100%的人

😀前言

LeetCode上的“无重复字符的最长子串”问题要求我们找到给定字符串中不包含重复字符的最长子串的长度。这个问题是一个典型的滑动窗口技巧的应用,需要有效地处理字符出现的情况来找到解决方案。

.

在本解决方案中,我们将探讨两种不同的算法实现来解决这个问题。首先,我们将使用基本方法来解决问题,然后进一步优化以获得更好的时间复杂度。

🧑个人简介:大家好,我是尘觉,希望我的文章可以帮助到大家,您的满意是我的动力😉😉


🤔无重复字符的最长子串

力扣官方题解

方法一:滑动窗口
思路和算法


我们先用一个例子考虑如何在较优的时间复杂度内通过本题。


我们不妨以示例一中的字符串 abcabcbb为例,找出从每一个字符开始的,不包含重复字符的最长子串,那么其中最长的那个字符串即为答案。对于示例一中的字符串,我们列举出这些结果,其中括号中表示选中的字符以及最长的字符串:


发现了什么?如果我们依次递增地枚举子串的起始位置,那么子串的结束位置也是递增的!这里的原因在于,假设我们选择字符串中的第 k 个字符作为起始位置,


并且得到了不包含重复字符的最长子串的结束位置为 rk 。那么当我们选择第 k+1k+1 个字符作为起始位置时,首先从 k+1到 rk的字符显然是不重复的,并且由于少了原本的第 k 个字符,我们可以尝试继续增大 rk ,直到右侧出现了重复字符为止。


这样一来,我们就可以使用「滑动窗口」来解决这个问题了:

  • 我们使用两个指针表示字符串中的某个子串(或窗口)的左右边界,其中左指针代表着上文中「枚举子串的起始位置」,而右指针即为上文中的 rk ;
  • 在每一步的操作中,我们会将左指针向右移动一格,表示 我们开始枚举下一个字符作为起始位置,然后我们可以不断地向右移动右指针,但需要保证这两个指针对应的子串中没有重复的字符。在移动结束后,这个子串就对应着 以左指针开始的,不包含重复字符的最长子串。我们记录下这个子串的长度;
  • 在枚举结束后,我们找到的最长的子串的长度即为答案。


判断重复字符


在上面的流程中,我们还需要使用一种数据结构来判断 是否有重复的字符,常用的数据结构为哈希集合


在左指针向右移动的时候,我们从哈希集合中移除一个字符,在右指针向右移动的时候,我们往哈希集合中添加一个字符。

 public int lengthOfLongestSubstring(String s) {
        // 哈希集合,记录每个字符是否出现过
        Set<Character> occ = new HashSet<Character>();
        int n = s.length();
        // 右指针,初始值为 -1,相当于我们在字符串的左边界的左侧,还没有开始移动
        int rk = -1, ans = 0;
        for (int i = 0; i < n; ++i) {
            if (i != 0) {
                // 左指针向右移动一格,移除一个字符
                occ.remove(s.charAt(i - 1));
            }
            while (rk + 1 < n && !occ.contains(s.charAt(rk + 1))) {
                // 不断地移动右指针
                occ.add(s.charAt(rk + 1));
                ++rk;
            }
            // 第 i 到 rk 个字符是一个极长的无重复字符子串
            ans = Math.max(ans, rk - i + 1);
        }
        return ans;
    }

优秀思路

这个打败100%

自己思路


建议对照着代码来看


首先因为字母表最大ASCLL为128所以我们就初始化128个空间


第一步 我们先初始化整个数组 赋值为-1代表没有出现过


其次我们设置 最大个数0 开始位置0


start = Math.max(start, last[index] + 1) 这个意思是 代表着 从0,到 这个字符有没有重复过如果没有就是


0 有的话就从新位置开始计算后面举例子


res = Math.max(res, i - start + 1); 这个意思相当于 之前最大的子串 和 当前 i 位置 和开始计算的位置的个


数 比如 3-5 出现了 5-3+1次 看谁大


最后更新last[index] = i,代表这个字母出现过并且记录字母中的位置


举个例子


比如 acbaa


i=0, res = 0,start = 0;


我们截取第一个字符 a 所以 start = Math.max(start, last[index] + 1); 相当于 start = Math.max(0, 0);所以


开始位置是0 然后


res = Math.max(res, i - start + 1);相当于 res = Math.max(0, 0 - 0 + 1);所以到现在最大子串长度为1


然后 last[index] = i; 相当于last[index] = 0 代表在数组中 a 元素已经出现过并且位置为0


i=1, 之前的res = 1,start = 0;


我们截取第二个字符 c 所以 start = Math.max(start, last[index] + 1); 相当于 start = Math.max(0, 0);所以


开始位置是0 然后


res = Math.max(res, i - start + 1); 相当于 res = Math.max(1, 1- 0 + 1);所以到现在最大子串长度为2


然后 last[index] = i; 相当于last[index] = 1 代表在数组中 c 元素已经出现过并且位置为1


i=2, 之前的res = 2,start = 0;


我们截取第三个字符 b 所以 start = Math.max(start, last[index] + 1); 相当于 start = Math.max(0, 0);所以


开始位置是0 然后


res = Math.max(res, i - start + 1); 相当于 res = Math.max(2, 2- 0 + 1);所以到现在最大子串长度为3


然后 last[index] = i; 相当于last[index] = 2 代表在数组中 b 元素已经出现过并且位置为2


重点来了


i=3, 之前的res = 3,start = 0;


我们截取第四个字符 a 所以 start = Math.max(start, last[index] + 1); 相当于 start = Math.max(0, 1);所以


开始位置是1 然后


res = Math.max(res, i - start + 1); 相当于 res = Math.max(3, 3- 1 + 1);所以到现在最大子串长度为3


这个就是妙处了直接从他记录的位置取出来 然后因为数组从0开始 所以这里要+1 相当于a【cba】a


然后 last[index] = i; 相当于last[index] = 3 代表在数组中 a 元素已经出现过并且位置为3


i=4, res = 3,start = 1;


我们截取第五个字符 a 所以 start = Math.max(start, last[index] + 1); 相当于 start = Math.max(1, 4);所以


开始位置是4 然后


res = Math.max(res, i - start + 1); 相当于 res = Math.max(3, 4- 4+ 1);所以到现在最大子串长度为3


4- 4+ 1相当于 当前我i的位置和我开始算的位置的长度和上一个最大长度比较


然后 last[index] = i; 相当于last[index] = 4 代表在数组中 a 元素已经出现过并且位置为4


在举一个例子


abcdbef


前面基本就直接跳过了直接从i= 4开始 那么此时 res = 4,start = 0; b的last[index]=1


我们截取第四个字符 b 所以 start = Math.max(start, last[index] + 1);


相当于 start = Math.max(0, 2);所以跟新开始位置开始计算


后面就是 res = Math.max(res, i - start + 1); 相当于 res = Math.max(4, 4- 2 + 1);


所以到现在最大子串长度为4


4- 2 + 1相当于 ab【cdb】e 和之前比的 【abcd】be比较


然后我们跟新b的地址 last[index]为 4


i= 5 那么此时 res = 4,start = 2;


截取第四个字符 e


start = = Math.max(2, 0)=2


res = Math.max(4,5-2+1)=4


5-2+1相当于 ab【cdbe】 和之前比的 【abcd】be比较


然后记录e的位置


i= 6 那么此时 res = 4,start = 2;


截取第五个字符 f


start = = Math.max(2, 0)=2


res = Math.max(4,6-2+1)=5


6-2+1相当于 ab【cdbef】 和之前比的 【abcd】be比较


然后记录 f 的位置

总结

i - start + 1就是统计有多少个 或者结合我上面的思路然后debug一下

    public int lengthOfLongestSubstring(String s) {
        // 记录字符上一次出现的位置
        int[] last = new int[128];
        for(int i = 0; i < 128; i++) {
            last[i] = -1;
        }
        int n = s.length();
        int res = 0;
        int start = 0; // 窗口开始位置
        for(int i =0 ; i < n; i++) {
            int index = s.charAt(i);
            start = Math.max(start, last[index] + 1);
            res   = Math.max(res, i - start + 1);
            last[index] = i;
        }
        return res;
    }

再优化一下

大体逻辑是差不多的这里注意说一下区别


这里没有赋值是利用了java在初始化数组的时候是默认分配0


last[index] = i+1; 代表着这个字符出现的位置 比如 aabcd 第一个a 它的位置是 i+1=1次 到第二个a的时候就是 i=1


所以 start = Math.max(start, last[index])直接是从1开始 然后重新记录a的位置是 i+1=2 以此类推

注意这个是从 1开始的 不是0 包括后面 res也是从1开始 不是0

总结

要注意区分 统计 和 数组位置 以及 从1开始的位置

// 记录字符上一次出现的位置
        int[] last = new int[128];
//        for(int i = 0; i < 128; i++) {
//            last[i] = -1;
//        }
        int n = s.length();
        int res = 0;
        int start = 0; // 窗口开始位置
        for(int i = 0; i < n; i++) {
            int index = s.charAt(i);
            start = Math.max(start, last[index]);
            res   = Math.max(res, i + 1 - start );
            last[index] = i+1;
        }
        return res;

😄总结

基本实现:

  1. 初始化大小为128的数组last(表示ASCII字符)来跟踪每个字符上次出现的索引位置。将last中的所有元素初始化为-1,表示尚未见过任何字符。
  2. 从左到右遍历输入字符串s。
  3. 对于s中索引为i的字符,计算其ASCII值并将其存储在index变量中。
  4. 更新start变量,使其成为当前值和last[index] + 1中的较大者。这一步确保start表示当前不包含重复字符的子串的起始位置。
  5. 更新res变量,使其成为当前值和i + 1 - start中的较大者。这一计算找到了当前不包含重复字符的子串的长度,并与先前的最大长度进行比较。
  6. 更新last[index]为i + 1,记录字符在字符串中的最近位置。
  7. 重复步骤3-6,直到处理完整个字符串。
  8. 最终的res值将表示输入字符串s中不包含重复字符的最长子串的长度。

优化实现:

  1. 优化版本的解决方案消除了不必要的last数组初始化,并相应地调整了计算。还利用了Java默认使用零初始化数组的特性。
  2. 通过进行这些优化,代码变得更加简洁,并且在内存使用和代码简洁性方面都得到了改善。
  3. 这两种实现都提供了正确的答案,但优化版本在内存使用和代码简洁性方面都具有更高的效率。

这些实现为解决LeetCode上的“无重复字符的最长子串”问题提供了清晰而高效的方法,展示了滑动窗口技巧和字符出现情况跟踪的应用。

文章到这里就结束了,如果有什么疑问的地方请指出,诸佬们一起来评论区一起讨论😁

希望能和诸佬们一起努力,今后我们一起观看感谢您的阅读🍻

如果帮助到您不妨3连支持一下,创造不易您们的支持是我的动力🤞


目录
相关文章
|
3月前
|
存储 算法
Leetcode第三题(无重复字符的最长子串)
这篇文章介绍了解决LeetCode第三题“无重复字符的最长子串”的算法,使用滑动窗口技术来找出给定字符串中最长的不含重复字符的子串,并提供了详细的代码实现和解释。
125 0
Leetcode第三题(无重复字符的最长子串)
|
5月前
|
算法
LeetCode第3题无重复字符的最长子串
该文章介绍了 LeetCode 第 3 题无重复字符的最长子串的解法,通过使用 HashSet 记录不重复的子元素,以每个字符开头遍历字符串,遇到重复字符则重新计算,最终找到最长子串,同时提到可以考虑使用 HashMap 降低复杂度。
LeetCode第3题无重复字符的最长子串
|
7月前
|
存储 算法 数据可视化
深入解析力扣161题:相隔为 1 的编辑距离(逐字符比较与动态规划详解)
深入解析力扣161题:相隔为 1 的编辑距离(逐字符比较与动态规划详解)
|
7月前
|
存储 算法 数据可视化
深入解析力扣157题:用Read4高效读取N个字符(多种解法与详细图解)
深入解析力扣157题:用Read4高效读取N个字符(多种解法与详细图解)
|
6月前
|
索引
821.字符的最短距离-力扣(LeetCode)
821.字符的最短距离-力扣(LeetCode)
48 0
|
7月前
|
存储 算法 程序员
力扣经典150题第三十一题:无重复字符的最长子串
力扣经典150题第三十一题:无重复字符的最长子串
41 0
|
7月前
【LeetCode刷题】滑动窗口思想解决问题:长度最小的子数组、无重复字符的最长子串
【LeetCode刷题】滑动窗口思想解决问题:长度最小的子数组、无重复字符的最长子串
|
7月前
|
存储 算法 数据可视化
深入解析力扣159题:至多包含两个不同字符的最长子串(滑动窗口法详细图解)
深入解析力扣159题:至多包含两个不同字符的最长子串(滑动窗口法详细图解)
|
7月前
|
存储 算法 数据挖掘
LeetCode 第三题:无重复字符的最长子串 详解 【3/1000】
LeetCode 第三题:无重复字符的最长子串 详解 【3/1000】
|
4月前
|
Unix Shell Linux
LeetCode刷题 Shell编程四则 | 194. 转置文件 192. 统计词频 193. 有效电话号码 195. 第十行
本文提供了几个Linux shell脚本编程问题的解决方案,包括转置文件内容、统计词频、验证有效电话号码和提取文件的第十行,每个问题都给出了至少一种实现方法。
LeetCode刷题 Shell编程四则 | 194. 转置文件 192. 统计词频 193. 有效电话号码 195. 第十行