「这是我参与11月更文挑战的第 6 天,活动详情查看:2021最后一次更文挑战」
刷算法题,从来不是为了记题,而是练习把实际的问题抽象成具体的数据结构或算法模型,然后利用对应的数据结构或算法模型来进行解题。个人觉得,带着这种思维刷题,不仅能解决面试问题,也能更多的学会在日常工作中思考,如何将实际的场景抽象成相应的算法模型,从而提高代码的质量和性能
最长不含重复字符的子串
题目来源:LeetCode-剑指 Offer 48. 最长不含重复字符的子字符串
题目描述
请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。
示例
示例 1
输入: "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2
输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3
输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
提示:
s.length <= 40000
解题
解法一:滑动窗口
思路
首先我们应该知道,滑动窗口适合求解数组/字符串内区间的最值问题,通常是有两个边界(左边界left和有边界right),窗口的大小就是right-left。当看到本题的时候,应该大致能想到用滑动窗口来解题
- 定义左边界left和右边界right,并且定义一个窗口,这个窗口中记录窗口内出现过的字符(这样做的目的是为了能快速知道,右边界遍历到的字符,是否在窗口内出现过)
- 在right向右滑动的过程中,判断right所在的位置的字符,是否出现在left的右边(left可以理解成最长不含重复子串的开始位置,如果出现在left的左边,就可以不关心)
- 如果出现了,则让左边界移动到该重复字符的下一个位置即可
- 期间取窗口的最大值
代码
func SlideWindow(s string) int {
n := len(s)
if n == 0 {
return 0
}
if n == 1 {
return 1
}
left, right, window, maxLength := 0, 0, make(map[byte]int), 1
for right < n {
var rightChar = s[right]
index, ok := window[s[right]] //是否出现过
if ok && index > left {
left = index
}
if right - left + 1 > maxLength {
maxLength = right - left + 1
}
window[rightChar] = right + 1
right++
}
return maxLength
}
解法二:普通思维解法
思路
这个问题用暴力解法很简单,导致它复杂度比较高的原因就是,每遍历一个字符,需要判断在之前是否已经出现过。如果能记录下来之前出现过的字符,就会简单很多
- 定义一个map,用来记录出现过的字符(它的下标是字符串的每一个字符,值是字符串中每个字符对应的下标)
- start用来保存最长不含重复子串的起始下标;maxLength记录最长不含重复子串的长度
- 当遍历字符串中的某一个字符时,判断它是否已经出现,并且出现的位置在start之前,则更新start为该字符的下一个位置
- 然用当前遍历到的字符的下标i-start+1就是0~i这个子串的最长不含重复子串的长度
代码
func lengthOfLongestSubstring(s string) int {
lastOccured := make(map[byte]int) //用来记录已经遍历过的字符(下标是s的值,它的值是s的下标)
maxLength := 0
start :=0 //记录最长不含重复子串的起始位置
for i, ch := range []byte(s) {
lastId, ok := lastOccured[ch]
if ok && lastId >= start { //如果该字符出现过,并且在start的后边出现的,更新start的位置
start = lastId + 1
}
if i - start + 1 > maxLength {
maxLength = i - start + 1
}
lastOccured[ch] = i
}
return maxLength
}