要解决 "最长无重复字符子串" 问题,我们可以通过滑动窗口算法 结合哈希表高效求解。以下是详细的解题步骤,包括问题分析、算法设计、具体实现和边界处理:
一、问题分析
题目 :给定一个字符串 s
,请找出其中不含有重复字符的最长子串的长度。示例:
- 输入
s = "abcabcbb"
,输出3
(最长子串为"abc"
、"bca"
等,长度均为 3)。 - 输入
s = "bbbbb"
,输出1
(唯一无重复子串为单个"b"
)。 - 输入
s = ""
,输出0
(空字符串无有效子串)。
二、核心算法:滑动窗口 + 哈希表
1. 滑动窗口的基本思想
滑动窗口是一种通过双指针 (左指针 left
和右指针 right
)界定 "当前子串范围" 的技术。两个指针之间的区域称为 "窗口",通过移动指针动态调整窗口大小,以寻找满足条件(无重复字符)的最长子串。
- 右指针
right
:负责 "扩展窗口",不断向右移动以纳入新字符,尝试扩大子串长度。 - 左指针
left
:负责 "收缩窗口",当右指针遇到重复字符时,左指针向右移动,直到窗口内不再包含重复字符,保证窗口的有效性。
2. 哈希表的作用
为了快速判断 "右指针新纳入的字符是否已在当前窗口中",我们使用哈希表(本题中用大小为 128 的数组,对应 ASCII 字符集)记录字符在窗口中的存在状态:
- 数组索引 = 字符的 ASCII 值(如
'a'
的 ASCII 值为 97,对应索引 97)。 - 数组值 = 1 表示字符 "在当前窗口中",0 表示 "不在窗口中"。
- 优势:判断字符是否重复的时间复杂度为 O(1),比遍历子串检查(O (n))高效得多。
三、详细解题步骤
以示例 s = "abcabcbb"
为例,逐步演示算法执行过程:
步骤 1:初始化
- 左指针
left
和右指针right
均指向字符串起始位置(left = right = 0
,对应字符'a'
)。 - 哈希表
hash[128]
初始化全为 0(表示所有字符初始状态为 "不在窗口中")。 - 变量
max_len
记录最长子串长度,初始为 0。
步骤 2:扩展右指针,纳入新字符
循环执行以下操作,直到 right
到达字符串末尾(*right == '\0'
):
① 检查新字符是否重复
右指针当前指向的字符为 s[right]
,通过哈希表判断:
- 若
hash[s[right]] == 0
:字符未重复,可直接纳入窗口。 - 若
hash[s[right]] == 1
:字符已在窗口中,需先收缩左指针。
② 收缩左指针(若有重复)
当新字符重复时,左指针 left
向右移动,同时将移出窗口的字符从哈希表中移除(标记为 0),直到重复字符被移出窗口:
- 例如,当
right
指向第 3 个字符'a'
(索引 3)时,哈希表中hash['a'] = 1
(因为left
仍在索引 0,'a'
已在窗口中)。 - 此时左指针右移(
left = 1
),并将s[0] = 'a'
从哈希表中移除(hash['a'] = 0
),窗口中不再有重复的'a'
。
③ 纳入新字符并更新最长长度
将右指针指向的字符纳入窗口(hash[s[right]] = 1
),计算当前窗口长度(right - left + 1
),若大于 max_len
则更新 max_len
。
④ 右指针右移,继续扩展
重复上述步骤,直到 right
遍历完所有字符。
四、示例执行过程(s = "abcabcbb"
)
步骤 | right 指向 |
字符 | 是否重复 | 左指针调整 | 窗口范围 | 当前长度 | max_len |
---|---|---|---|---|---|---|---|
1 | 0('a' ) |
'a' | 否 | 无 | [0,0] | 1 | 1 |
2 | 1('b' ) |
'b' | 否 | 无 | [0,1] | 2 | 2 |
3 | 2('c' ) |
'c' | 否 | 无 | [0,2] | 3 | 3 |
4 | 3('a' ) |
'a' | 是(窗口有 'a') | left→1 | [1,3] | 3 | 3 |
5 | 4('b' ) |
'b' | 是(窗口有 'b') | left→2 | [2,4] | 3 | 3 |
6 | 5('c' ) |
'c' | 是(窗口有 'c') | left→3 | [3,5] | 3 | 3 |
7 | 6('b' ) |
'b' | 是(窗口有 'b') | left→4 | [4,6] | 3→2(left→5 后) | 3 |
8 | 7('b' ) |
'b' | 是(窗口有 'b') | left→6 | [6,7] | 2→1(left→7 后) | 3 |
最终 max_len = 3
,与预期结果一致。
五、代码实现(修正后)
cpp
int lengthOfLongestSubstring(char* s) {
int hash[128]={0};
int max = 0;
char *left = s;
char *right = s; //左右指针初始位置均在S第一个字符
while(*right!='\0'){
while(hash[*right]) //直至重复元素对的前者被剔除
{
hash[*left]=0; //哈希表中删除左元素
left++;
}
hash[*right] = 1; //添加该右元素
if(right-left+1>max)
max = right-left+1;
right++; //右移
}
return max;
}
六、关键细节与边界处理
- 空字符串 :若
s
为空(*s == '\0'
),循环不执行,直接返回0
,避免指针越界。 - 单字符字符串 :如
s = "a"
,右指针处理'a'
后,窗口长度为 1,max_len = 1
。 - 全重复字符 :如
s = "aaaaa"
,每次右指针移动都会触发左指针收缩(始终与右指针相邻),窗口长度恒为 1,max_len = 1
。 - 字符类型转换 :用
(unsigned char)*right
确保 ASCII 值为 0~255 的字符正确映射到数组索引(避免负数索引)。
七、时间与空间复杂度
- 时间复杂度 :O (n),其中
n
是字符串长度。每个字符最多被left
和right
各访问一次,总操作次数为 O (n)。 - 空间复杂度:O (1),哈希表大小固定为 128(ASCII 字符集),与输入规模无关。
通过滑动窗口动态调整和哈希表快速判断,该算法高效解决了 "最长无重复字符子串" 问题,兼顾了时间和空间效率。
作者:牢笼
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。