3. 无重复字符的最长子串

3. 无重复字符的最长子串

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

示例 1:

输入: s = "abcabcbb"

输出: 3

解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。注意 "bca" 和 "cab" 也是正确答案。

示例 2:

输入: s = "bbbbb"

输出: 1

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

示例 3:

输入: s = "pwwkew"

输出: 3

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

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

cpp 复制代码
#include <iostream> 
#include <string> 
#include <vector>
#include <string>
#include <unordered_set>
using namespace std;
// 求字串 中的 最长连续字串
int lengthOfLongestSubstring(string s) {
    /*unordered_set(哈希集合):平均 O(1) 查找,更快
    set(红黑树集合):O(log n) 查找,保持元素有序*/
	unordered_set<char> strSet;
	int n = s.size();
	int maxSize = 0;
    //右指针 j 标记子串的结束位置
	int j = -1;

    //每次循环代表以 i 作为新的左边界开始寻找无重复子串
    //左指针 i 标记子串的起始位置
	for (int i = 0; i < n; i++)
	{
        //移动左指针(收缩窗口
        //当左指针向右移动时,将离开窗口的字符从集合中移除
		if (i != 0) {
			strSet.erase(s[i - 1]);
		}
        // 查找以当前i为边界 能找到的非重复最大字串
		while (j + 1 < n && !strSet.count(s[j + 1])) {
			strSet.insert(s[j + 1]);
			j++;
		}
		maxSize = max(maxSize, j - i + 1);
	}
	return maxSize;
}

测试用例

cpp 复制代码
int main() {
    // 测试用例
    struct TestCase {
        string input;
        int expected;
    };

    TestCase testCases[] = {
        // 基础测试
        {"abcabcbb", 3},   // "abc"
        {"bbbbb", 1},      // "b"
        {"pwwkew", 3},     // "wke"

        // 边界情况
        {"", 0},           // 空字符串
        {"a", 1},          // 单字符
        {"abcdef", 6},     // 无重复字符
        {"abba", 2},       // 重复字符在两端

        // 复杂情况
        {"dvdf", 3},       // 重复字符在中间
        {"anviaj", 5},     // "nviaj"
        {"tmmzuxt", 5},    // "mzuxt"
        {"abcdeafgh", 8},  // "bcdeafgh"

        // 包含空格
        {"a b c", 3},      // 包含空格
        {"  ", 1},         // 多个空格

        // 混合字符
        {"aA123@!", 7},    // 大小写字母、数字、符号混合
        {"abc123abc", 6},  // "123abc"
    };

    int passed = 0;
    int total = sizeof(testCases) / sizeof(testCases[0]);

    for (int i = 0; i < total; i++) {
        int result = lengthOfLongestSubstring(testCases[i].input);

        if (result == testCases[i].expected) {
            cout << "✓ Test case " << i + 1 << " passed: \""
                << testCases[i].input << "\" -> " << result << endl;
            passed++;
        }
        else {
            cout << "✗ Test case " << i + 1 << " failed: \""
                << testCases[i].input << "\"" << endl;
            cout << "  Expected: " << testCases[i].expected
                << ", Got: " << result << endl;
        }
    }

    cout << "\n" << passed << "/" << total << " test cases passed." << endl;

    return 0;
}

以字符串 "abcabcbb" 为例:
初始:i=0, rk=-1, occ={}

第1轮:i=0 → rk移动到2 → "abc" → 长度=3

第2轮:i=1 → 移除'a' → rk移动到3 → "bca" → 长度=3

第3轮:i=2 → 移除'b' → rk移动到4 → "cab" → 长度=3

第4轮:i=3 → 移除'c' → rk移动到5 → "abc" → 长度=3

第5轮:i=4 → 移除'a' → rk无法移动(遇到'b'重复) → "bc" → 长度=2

...

最终结果:3

为什么用 unordered_set 而不是 set?

unordered_set(哈希集合):平均 O(1) 查找,更快

set(红黑树集合):O(log n) 查找,保持元素有序

在这个算法中:

我们只需要快速判断字符是否存在,不需要有序性

因此使用 unordered_set 性能更好

unordered_set

unordered_set occ;
// 插入元素

occ.insert('a');
// 检查元素是否存在

if (occ.count('a')) {

cout << "'a' 存在" << endl;

}
// 移除元素

occ.erase('a');
// 清空集合

occ.clear();
// 获取集合大小

int size = occ.size();
// 遍历集合(顺序不确定)

for (char c : occ) {

cout << c << " ";

}

为什么只移动一个?

cpp 复制代码
for (int i = 0; i < n; ++i) {
    if (i != 0) {
        occ.erase(s[i - 1]);  // 关键:i从0到1,移除s[0]; i从1到2,移除s[1]...
    }
    // ... 扩展右边界
}

查找的是以当前i为边界 能找到的非重复最大字串

外层 for 循环的 i 就是左指针

每次循环 i++,左指针就自动向右移动一位

occ.erase(s[i - 1]) 正是移除刚刚离开窗口的左边界字符

假设字符串 s = "abcde",当前窗口是 "bcd":

为什么不多移动几个?

因为左指针每次只移动一位是最优的:

如果窗口中有重复字符,一定是右边界遇到了重复

此时需要收缩左边界直到重复字符被移除

每次移一位就能保证找到第一个无重复的窗口起点

字符串: "abcabcbb" 过程:

i=0: 窗口扩张到 "abc" (rk=2)

occ = {'a','b','c'},

ans=3
i=1: 移除 s[0]='a', occ={'b','c'}

右边界可扩展:

s[3]='a'不在occ中 → 加入

occ={'b','c','a'},

窗口="bca" (rk=3),

ans=3
i=2: 移除 s[1]='b', occ={'c','a'}

右边界可扩展:

s[4]='b'不在occ中 → 加入

occ={'c','a','b'},

窗口="cab" (rk=4),

ans=3
i=3: 移除 s[2]='c', occ={'a','b'}

右边界可扩展:

s[5]='c'不在occ中 → 加入

occ={'a','b','c'},

窗口="abc" (rk=5),

ans=3
i=4: 移除 s[3]='a', occ={'b','c'}

右边界 s[6]='b'

已在occ中 → 停止扩展

窗口="bc" (rk=5),

ans=3

为什么先移除再扩展?

假设字符串 s = "abca",

当前状态:

第1轮结束:i=0, rk=2, 窗口="abc", occ={'a','b','c'}

现在进入第2轮(i=1): 情况A:先移除再扩展(正确)

  1. 移除 s[0]='a' → occ={'b','c'}
  2. 尝试扩展:s[3]='a'不在occ中 → 可以加入
  3. 结果:窗口="bca", rk=3, 长度=3

情况B:先扩展再移除(错误)

  1. 尝试扩展:s[3]='a'在occ中(因为occ={'a','b','c'})→ 无法扩展
  2. 移除 s[0]='a' → occ={'b','c'}
  3. 结果:窗口="bc", rk=2, 长度=2 ← 错失了更长的子串!
相关推荐
Morwit4 小时前
【力扣hot100】 312. 戳气球(区间dp)
c++·算法·leetcode
CoovallyAIHub4 小时前
摄像头如何“看懂”你的手势?手势识别实现新人机交互
深度学习·算法·计算机视觉
Q741_1474 小时前
C++ 栈 模拟 力扣 394. 字符串解码 每日一题 题解
c++·算法·leetcode·模拟·
阿闽ooo4 小时前
桥接模式实战:用万能遥控器控制多品牌电视
c++·设计模式·桥接模式
AI科技星4 小时前
张祥前统一场论:空间位移条数概念深度解析
数据结构·人工智能·经验分享·算法·计算机视觉
Wuliwuliii4 小时前
闵可夫斯基和、需存储的最小状态集
c++·算法·动态规划·闵可夫斯基和
CoovallyAIHub4 小时前
颠覆认知!遥感船舶检测“越深越好”是误区?LiM-YOLO证明“少即是多”
深度学习·算法·计算机视觉
byzh_rc4 小时前
[算法设计与分析-从入门到入土] 贪心算法
算法·动态规划
Felven4 小时前
C. Contrast Value
c语言·开发语言·算法