搞定“340. 至多包含 K 个不同字符的最长子串”,看这篇就够了

开篇:算法小白的困惑

作为一名算法爱好者,在刷题的道路上,想必大家都和我一样,有过迷茫与困惑。面对浩如烟海的算法题,常常感觉无从下手。就拿 “340. 至多包含 K 个不同字符的最长子串” 这道题来说,初次看到时,真的是一头雾水。题目要求找出给定字符串 s 中,至多包含 k 个不同字符的最长子串,并返回其长度。这看似简单的描述,实则暗藏玄机,要如何高效地找到这个子串呢?它不仅考验对字符串的处理能力,更涉及到一些巧妙的算法思维,而这正是其魅力与挑战所在。今天,就让我们一起深入探究这道题,看看如何一步步攻克它。

一、问题剖析

让我们先来仔细剖析这道题。题目给定一个字符串 s 和一个整数 k ,要求找出至多包含 k 个不同字符的最长子串,并返回其长度。例如,输入字符串 s = “eceba”,k = 2,那么满足条件的最长子串就是 “ece”,长度为 3 ;再比如输入 s = “aa”,k = 1,最长子串就是 “aa”,长度为 2 。从这些示例可以看出,要在字符串的众多子串中精准定位到符合条件的最长子串并非易事。

这道题的关键在于如何高效地遍历字符串,利用合适的数据结构来记录字符出现的情况,进而通过巧妙的指针移动找到最长子串。而滑动窗口和哈希表的结合,正是解决此类问题的一把利刃,后续我们会详细讲解它们是如何协同作战的。

二、滑动窗口 + 哈希表:解题 “王炸” 组合

(一)滑动窗口基础

滑动窗口,简单来说,就像是一个在字符串上灵活移动的 “窗框”,窗框的两边分别由两个指针 left 和 right 把控。初始时,这两个指针都指向字符串的开头,框住的就是字符串开头的第一个字符,这是初始的窗口。随着算法的推进,right 指针会像一个勤劳的探索者,一步步地向右移动,不断扩大这个窗口,去探索后面的字符;而 left 指针则像是一个谨慎的守护者,当窗口内的情况不符合要求时,它就会适时地向右移动,缩小窗口,确保窗口里的字符始终满足题目的条件。

我们可以通过一个生活中的例子来加深理解。假设你在一个长长的书架前找书,你想找到一段连续的书架区间,使得这个区间内最多只有 k 种不同类别的书。一开始,你的目光聚焦在书架的最左端(相当于 left 和 right 都指向开头),然后慢慢向右扫视(right 移动),一旦发现当前扫视的区间内书的类别超过了 k 种,就把目光的左边界向右挪一挪(left 移动),直到找到满足条件的最长区间。通过这两个指针的默契配合,就能高效地遍历整个字符串,找到我们想要的最长子串。

(二)哈希表的关键角色

哈希表在这个算法中扮演着至关重要的角色,它就像是窗口的 “记忆小助手”。哈希表主要用于记录字符出现的位置或频次,方便我们快速判断窗口内不同字符的数量。以字符串 “eceba” 为例,当 right 指针逐步向右移动时,哈希表可以记录每个字符出现的最新位置。一开始,哈希表为空,当 right 指向第一个 ‘e’ 时,哈希表记录 ‘e’ 的位置(假设索引从 0 开始,这里就是 0);接着 right 指向 ‘c’,哈希表更新为 ‘e’ 的位置 0,‘c’ 的位置 1;再到下一个 ‘e’,哈希表更新 ‘e’ 的位置为 2 。如此一来,我们通过查看哈希表的大小就能知道当前窗口内不同字符的数量,当哈希表大小超过 k 时,就知道需要移动 left 指针来调整窗口了。它能够让我们在 O (1) 的时间复杂度内完成字符的查找、插入和删除操作,大大提高了算法的效率,这在处理大规模数据的时候尤为关键。

三、步步为营:代码实现详解

(一)初始化工作

在 Java 代码实现中,首先要进行一些初始化操作。定义两个指针 left 和 right ,它们都初始化为 0 ,这表示滑动窗口起始位置在字符串的开头。同时,创建一个 LinkedHashMap ,这里使用 LinkedHashMap 是因为它既能像普通哈希表一样快速存取元素,又能保持元素插入的顺序,方便后续操作,其初始容量设置为 k + 1 ,这是为了应对后续可能出现的 k 个不同字符再加一个新字符的情况。还需要初始化一个变量 max_len ,用于记录最长子串的长度,初始值设为 1 ,这是考虑到字符串中至少有一个字符的情况,后续会不断更新这个最大值。这些初始化操作就像是搭建舞台,为后续精彩的算法表演做好准备。


int left = 0;

int right = 0;

LinkedHashMap<Character, Integer> hashmap = new LinkedHashMap<Character, Integer>(k + 1);

int max_len = 1;

(二)遍历字符串

接下来进入关键的遍历字符串环节。使用一个 while 循环,条件是 right 指针小于字符串的长度 n ,确保能遍历完整个字符串。在循环体内,首先获取当前 right 指针指向的字符 character 。接着,判断这个字符是否已经在哈希表 hashmap 中,如果是,先将其从哈希表中删除,这一步看似奇怪,实则精妙,目的是为了将该字符重新插入哈希表,使其成为哈希表中该字符的最新位置记录,保证后续判断窗口内字符顺序的准确性,然后将字符及其当前位置 right 插入哈希表,并将 right 指针向右移动一位。


while (right < n) {

Character character = s.charAt(right);

if (hashmap.containsKey(character))

hashmap.remove(character);

hashmap.put(character, right++);

此时,要检查窗口内的字符情况。如果哈希表的大小等于 k + 1 ,说明窗口内已经有了 k + 1 个不同字符,不符合题目要求,需要调整窗口。通过获取哈希表中第一个元素(即最左边的字符,也就是最早进入窗口的字符)的键值对,将其从哈希表中移除,同时更新 left 指针,使其指向这个被移除字符的下一个位置,这样就缩小了窗口,使得窗口内再次满足至多包含 k 个不同字符的条件。


if (hashmap.size() == k + 1) {

Map.Entry<Character, Integer> leftmost = hashmap.entrySet().iterator().next();

hashmap.remove(leftmost.getKey());

left = leftmost.getValue() + 1;

}

(三)更新最大长度

在每一次移动指针和调整窗口后,都需要更新最长子串的长度。通过比较当前 max_len 和 right - left 的大小,取较大值更新 max_len 。这里 right - left 表示当前窗口的长度,因为 right 是窗口的右边界(不包含),left 是窗口的左边界,它们的差值就是窗口内字符的数量。不断比较更新,就能确保 max_len 始终记录着满足条件的最长子串长度。


max_len = Math.max(max_len, right - left);

就这样,通过不断地遍历、调整窗口和更新最大长度,最终就能找到给定字符串中至多包含 k 个不同字符的最长子串的长度,看似复杂的过程,在代码的一步步执行下,变得清晰而高效。

四、代码示例:多语言实现

(一)Java 实现


public class LeetCode340 {

public int lengthOfLongestSubstringKDistinct(String s, int k) {

int n = s.length();

// 如果字符串为空或者k为0,直接返回0

if (n * k == 0) return 0;

// 滑动窗口左右指针,初始都指向字符串开头

int left = 0;

int right = 0;

// LinkedHashMap用于存储字符及其在滑动窗口中的最右位置,初始容量设为k + 1

LinkedHashMap<Character, Integer> hashmap = new LinkedHashMap<Character, Integer>(k + 1);

int max_len = 1;

while (right < n) {

Character character = s.charAt(right);

// 如果字符已在哈希表中,先移除再插入,保证其位置最新

if (hashmap.containsKey(character))

hashmap.remove(character);

hashmap.put(character, right++);

// 当哈希表中不同字符数量达到k + 1时,需要调整窗口

if (hashmap.size() == k + 1) {

// 移除最左边的字符,即最早进入窗口的字符

Map.Entry<Character, Integer> leftmost = hashmap.entrySet().iterator().next();

hashmap.remove(leftmost.getKey());

// 更新左指针,使其指向被移除字符的下一个位置

left = leftmost.getValue() + 1;

}

// 更新最长子串的长度

max_len = Math.max(max_len, right - left);

}

return max_len;

}

}

在这段 Java 代码中,通过滑动窗口和 LinkedHashMap 的协同工作,高效地解决了问题。LinkedHashMap的特性使得它既能快速操作元素,又能记住元素插入顺序,方便找到最左边的字符。每次移动指针和调整窗口后,及时更新最长子串长度,确保最终得到正确结果。

五、算法复杂度分析

(一)时间复杂度

时间复杂度方面,需要分情况讨论。在最好的情况下,如果字符串本身包含的不同字符数量不超过 k 个,那么只需要一次遍历就能得到结果,因为 right 指针从左到右依次移动,没有遇到需要频繁调整 left 指针的情况,时间复杂度为 O (N) ,这里的 N 是字符串的长度。然而,在最坏的情况下,当输入字符串包含了 n 个不同字符时,每一次当窗口内的不同字符数量达到 k + 1 个,需要调整 left 指针。而调整 left 指针时,为了找到需要移除的最左边字符,可能需要遍历当前窗口内的所有字符,这一步在最坏情况下每次需要花费 O (k) 的时间,由于 right 指针总共要移动 N 次,所以总的时间复杂度就是 O (Nk) 。这种最坏情况的出现,就像是在一个装满各种杂物的大箱子里,要不断地挑选出特定的几种物品,而且每次挑选都要翻遍几乎整个箱子,非常耗时。

(二)空间复杂度

空间复杂度相对较为直观,主要取决于哈希表所占用的空间。在算法执行过程中,哈希表最多存储 k + 1 个不同的字符及其对应的位置信息,因为一旦哈希表中字符数量达到 k + 1 ,就会触发窗口调整,移除最左边的字符,使得哈希表中的元素数量始终控制在 k + 1 以内。所以,空间复杂度为 O (k) ,这里的 k 是题目给定的参数,表示最多允许的不同字符数量。这就好比我们出门背包,最多装 k + 1 件不同的物品,背包的空间占用取决于这个 k 的大小。通过对时间和空间复杂度的分析,能让我们更全面地了解这个算法的性能,在实际应用中根据需求选择最合适的解法。

六、举一反三:类似题目拓展

掌握了这道题的解法后,我们不妨来看看一些与之类似的题目,进一步拓展思维。

比如 “至多包含 K 种字符的子串数量”,这道题与我们今天讲的题目的联系在于,都是围绕字符串中字符的种类和子串的特性展开。但解法上存在差异,在求解子串数量时,需要在滑动窗口移动过程中,更加细致地统计满足条件的子串出现的次数,每当窗口内字符种类符合条件时,子串数量就要相应增加,这涉及到对窗口内字符变化的更精准把控,不像原题目只需关注最长子串长度。

再如 “找到包含所有出现次数不少于 k 次的字符的最长子串”,它同样利用滑动窗口思想,但重点在于先统计字符出现频次,然后在窗口移动时,确保窗口内不仅字符种类受限,还要满足特定字符的出现次数要求,需要多一层频次判断逻辑,相比之下,今天的题目则聚焦于单纯的字符种类数量。

通过接触这些类似题目,大家可以发现,虽然它们各有难点,但核心的滑动窗口和哈希表等基础方法是相通的,只要掌握了这些基础,再遇到新的变形题,也能逐步拆解,找到解题思路,希望大家在算法学习的道路上越走越远,攻克更多难题。

七、总结

至此,我们通过 “滑动窗口 + 哈希表” 的巧妙组合,成功攻克了 “340. 至多包含 K 个不同字符的最长子串” 这道算法难题。滑动窗口如同精准的探测器,在字符串的世界里快速穿梭,寻找符合条件的子串区域;哈希表则像一位可靠的记录员,清晰地记录字符的出现情况,为窗口的调整提供关键依据。

在学习算法的道路上,遇到困难是常有的事,但每一道难题都是成长的阶梯。希望大家通过这道题的学习,不仅掌握了解题方法,更能体会到算法思维的精妙之处。多练习、多思考、多总结,相信大家在面对其他算法挑战时,也能游刃有余,逐步提升自己的编程能力,向着算法高手的目标奋勇前进,去征服更多未知的算法高峰。

<think>嗯,用户想找Python实现包含K个不同字符长子算法。这个问题我之前在LeetCode上见过,好像是第159题和340题的变体。首先得回忆一下滑动窗口方法,这是处理子问题的常用技巧。 首先,我需要确定算法的基本思路。滑动窗口通常用两个指针,left和right,来标记当前窗口的左右边界。然后,用一个哈希表或者字典来记录窗口内各个字符的出现次数。当窗口中的不同字符数超过K时,移动左指针缩小窗口,直到不同字符数回到K或以下。 那具体怎么实现呢?比如,如何统计不同字符的数量?可以用字典,键是字符,值是该字符在窗口中的出现次数。当某个字符的计数从0变为1时,不同字符数加1;当某个字符的计数减到0时,不同字符数减1。这样就能动态维护当前窗口中的字符种类数。 接下来,考虑窗口的扩展和收缩。右指针每次向右移动,将当前字符加入字典,并检查不同字符数是否超过K。如果超过,就需要移动左指针,直到满足条件为止。在这个过程中,记录窗口的大长度。 可能的边界情况包括K为0的情况,这时候应该返回0。另外,字符为空或者长度小于K的情况也需要处理。 然后,编写代码的时候要注意效率。比如,字典的更新和查询操作应该是O(1)时间,这样整个算法的时间复杂度是O(n),因为每个字符多被访问两次(左右指针各一次)。 用户可能需要具体的代码示例,所以需要写出一个函数,接受字符s和整数k作为参数,返回长子的长度。还要考虑测试用例,比如示例中的输入是否正确处理。 另外,用户可能关心优化方法,比如如何减少不必要的计算,或者是否有更高效的数据结构。比如,可以用collections.defaultdict来简化字典的初始化,或者使用变量来跟踪当前不同字符的数量,而不是每次都遍历字典统计键的数量。 后,生成相关问题的时候,可以考虑滑动窗口的其他应用,比如无重复字符长子,或者包含所有字符短子。还有如何处理不同K值的情况,或者算法的时间复杂度分析等。</think>### Python实现至多包含K个不同字符长子算法 #### 算法思路 该问题属于滑动窗口类问题的经典变体,核心是通过维护一个可变窗口,动态调整左右边界以满足「多K个不同字符」的约束条件[^3][^5]。主要步骤包括: 1. 使用哈希表记录窗口内字符出现次数 2. 右指针持续扩展窗口 3.不同字符数超过K时,左指针收缩窗口 4. 始终记录大窗口长度 #### Python实现代码 ```python def length_of_longest_substring_k_distinct(s: str, k: int) -> int: if k == 0 or len(s) == 0: return 0 char_count = {} left = 0 max_len = 0 distinct = 0 for right in range(len(s)): r_char = s[right] if char_count.get(r_char, 0) == 0: distinct += 1 char_count[r_char] = char_count.get(r_char, 0) + 1 while distinct > k: l_char = s[left] char_count[l_char] -= 1 if char_count[l_char] == 0: distinct -= 1 left += 1 max_len = max(max_len, right - left + 1) return max_len ``` #### 复杂度分析 - 时间复杂度:$O(n)$,每个字符多被访问两次(右指针扩展和左指针收缩) - 空间复杂度:$O(k)$,哈希表多存储$k+1$个字符的计数[^1][^5] #### 示例测试 ```python print(length_of_longest_substring_k_distinct("eceba", 2)) # 输出3("ece") print(length_of_longest_substring_k_distinct("aa", 1)) # 输出2 ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值