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

解题思路
本题主要还是用到了滑动窗口+双指针的方法
python代码思路
首先构造哈希表,在python中哈希表一般用set或者dict来表示:
-
哈希表 :Python 的
set和dict都是基于哈希表实现的。-
当只需要记录元素是否存在时,用
set。 -
当需要记录元素的位置或其他附加信息时,用
dict(如char -> index)。
-
在"无重复字符的最长子串"中,最简单的做法是用 set 存储当前窗口内的字符。当遇到重复字符时,从 set 中移除左指针指向的字符,直到重复字符被移出。
C++代码思路
在C++中一般是使用unordered_set或者是unordered_map作为哈希表来进行映射的。
- 哈希表 :C++ 标准库提供
std::unordered_set(无序集合)和std::unordered_map(无序映射),底层也是哈希表。
具体思路和上述基本一致,但是还是存在细微差别:
首先遍历字符串s,并且在哈希表中对当前字符的出现次数进行计数,如果当前字符的出现次数大于1的话,证明当前字符在前面已经出现过,因此需要从 unordered_map 中移除左指针指向的字符,直到重复字符被移出。
代码
python
python
class Solution(object):
def lengthOfLongestSubstring(self, s):
"""
:type s: str
:rtype: int
"""
c = set()
left, maxlen = 0, 0
for right in range(len(s)):
while s[right] in c:
c.remove(s[left])
left += 1
c.add(s[right])
maxlen = max(maxlen, right - left + 1)
return maxlen
C++
cpp
class Solution {
public:
int lengthOfLongestSubstring(string s) {
unordered_map<char, int> cnt;
int left = 0;
int n = s.size();
int maxlen = 0;
for(int right = 0;right < n;right++)
{
char c = s[right];
cnt[c]++;
// 说明当前窗口中存在重复元素
while(cnt[c] > 1)
{
cnt[s[left]]--;// 移除窗口左边的字符
left++;// 窗口缩小
}
maxlen = max(maxlen, right - left + 1);
}
return maxlen;
}
};
5.最长回文子串

解题思路
我们用中心扩展法,因为如果是回文字符串的话,那么从中心往两边走的话,逐步比较左右元素是否相同,这样就可以找到最长回文子串。
而关于中心的话:
对于一个长度为n的字符串,回文的中心可以分为以下2类:
- 字符中心 :每个字符本身可以作为一个奇数长度回文的中心,例如
"aba"的中心是'b'。一共有n个这样的中心。
- 间隙中心 :每两个相邻字符之间的位置可以作为一个偶数长度回文的中心,例如
"abba"的中心在'b'和'b'之间。一共有n-1个这样的中心。
所以总的中心数是n+n-1=2n-1。
那么如何一个循环遍历所有中心呢?
我们用一个整数 i 从 0 到 2n-2(共 2n-1 个值)来代表所有中心。
关键是将 i 映射到左右指针 (l, r),使得:
当i为奇数的时候,中心落在2个字符的间隙;(偶数回文串)
当i为偶数时,中心落在1个字符上;(奇数回文串)
python
l = i // 2
r = (i + 1) // 2
当i = 2k(偶数)时,l = k,r = k,此时中心在s[k]上;
当i = 2K +1(奇数)时,l = k,r = k + 1,此时中心落在s[k],s[k+1]之间。
这样一个i就对应了一个中心。
代码
python(注意python字符串区间是左闭右开)
python
class Solution(object):
def longestPalindrome(self, s):
"""
:type s: str
:rtype: str
"""
n = len(s)
ans_left, ans_right = 0, 0
for i in range(2*n - 1):
l, r = i//2, (i+1)//2
while l>=0 and r<n and s[l] == s[r]:
l -= 1
r +=1
if r - l -1 > ans_right - ans_left:
ans_left, ans_right = l+1, r # 左闭右开
return s[ans_left:ans_right]
C++(取子串用的是substr[起始位置,长度])
cpp
class Solution {
public:
string longestPalindrome(string s) {
// 由中间向两边回溯
int n = s.size();
int ans_left = 0;
int ans_right = 0;
for(int i = 0; i < 2*n -1; i++)
{
int l = i / 2;
int r = (i+1)/2;
while(l>=0&&r<n&&s[l]==s[r])
{
l--;
r++;
}
if(r - l - 1 > ans_right - ans_left)
{
ans_left = l+1;
ans_right = r - 1;
}
}
return s.substr(ans_left, ans_right - ans_left + 1);
}
};
11.盛最多水的容器

解题思路
首先,盛最多水的容器,容器的盛水量从题目中得到的是应该是两边的最小值*对应的下标差,
比如min(height[i], height[j]) * (j - i)
取i和j分别为height数组的2端,然后同时向内运动,直接i和j相遇。
而在此过程中,究竟是移动长的,还是短的那部分呢?
移动长的,min(height[i], height[j])可能不变或者更小,但是盛水量一定变小;而移动短的那部分,min(height[i], height[j])可能不变或者更大,盛水量可能变大。
算法流程
初始化:设定i, j分别为是水槽的两端
循环收窄:直到i, j相遇时跳出循环
更新面积值;
选两端中较短的那部分,往中间收窄一格;
返回值:返回面积最大值res
代码
python
python
class Solution(object):
def maxArea(self, height):
"""
:type height: List[int]
:rtype: int
"""
i, j = 0, len(height) - 1
res = 0
while i < j:
if height[i] < height[j]:
res = max(res, height[i] * (j - i))
i += 1
else:
res = max(res, height[j] * (j - i))
j -= 1
return res
C++
cpp
class Solution {
public:
int maxArea(vector<int>& height) {
int i = 0;
int j = height.size() - 1;
int res = 0;
while(i < j)
{
if(height[i] < height[j])
{
res = max(res, height[i]*(j - i));
i++;
}
else
{
res = max(res, height[j]*(j - i));
j--;
}
}
return res;
}
};
15.三数之和

解题思路
题中要求在数组中是否存在三元数能够使得nums[i] + nums[j] + nums[k] == 0,且i != j!= k;
根据题意,我们先给整个数组排序 (按从小到大的顺序排序)
取三个数k ,i , j
初始化:取k遍历整个数组,而i , j分别在数组的两端。
循环判定:
结束条件:i==j
若nums[k] == nums[k-1],跳过相等的数,因为整个组合都是相同的,重复了;
取i = k+1 , j = nums.size() -1;
判断s = nums[k] + nums[i] + nums[j] ?= 0
若大于0,则需要j往中间靠,同时一样要判定nums[j]==nums[j+1],以防重复;
若小于0,则需要i往中间靠,同时一样要判定nums[i] == nums[i-1],以防重复;
若等于0,则将当前的结果存入结果中,i,j同时往中间靠,但也要同时判断nums[j]==nums[j+1]、nums[i] == nums[i-1],以防重复。
返回值:返回存入的列表
代码
C++
cpp
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
// 排序
sort(nums.begin(), nums.end());
vector<vector<int>> res;
if(nums.size() < 3)
return res;
for(int k = 0;k<nums.size()-2;k++)
{
if(nums[k]>0)
break; // j > i > k
if(k>0 && nums[k] == nums[k-1])//跳过nums[k]==nums[k-1]的值,跳过相等的值
continue;
int i = k + 1;
int j = nums.size() - 1;
while(i < j)
{
int s = nums[k] + nums[i] + nums[j];
if(s < 0)
{
i++;
while(i < j && nums[i] == nums[i-1])
{
i++;
}
}
else if(s > 0)
{
j--;
while(i < j && nums[j] == nums[j+1])
{
j--;
}
}
else
{
res.push_back({nums[k],nums[i],nums[j]});
i++;
j--;
while(i < j && nums[i] == nums[i-1]) i++;
while(i < j && nums[j] == nums[j+1]) j--;
}
}
}
return res;
}
};
python
python
class Solution(object):
def threeSum(self, nums):
"""
:type nums: List[int]
:rtype: List[List[int]]
"""
nums.sort()
res, k = [], 0
for k in range(len(nums) -2):
if len(nums)<3:
break
if nums[k] > 0:
break
# 判断是否重复
if k > 0 and nums[k] == nums[k-1]:
continue
i, j= k + 1, len(nums) - 1
while i < j:
s = nums[k] + nums[i] + nums[j]
if s < 0:
i += 1
while i < j and nums[i] == nums[i-1]:
i += 1
elif s > 0:
j -= 1
while i < j and nums[j] == nums[j+1]:
j -= 1
else:
res.append([nums[k], nums[i], nums[j]])
i += 1
j -= 1
while i < j and nums[i] == nums[i-1]: i += 1
while i < j and nums[j] == nums[j+1]: j -= 1
return res
75.颜色分类

解题思路------边界覆盖
题目的要求最后要实现的效果是[0,0,0,0,0,......,1,1,1,1,1,......,2,2,2,2,2]
也就是三个连续区域红色、白色、蓝色。
初始化
开始的时候我们需要维护以下2个变量:
p1:下一个1应该放置的位置(白色区域的边界)
p0:下一个0应该放置的位置(红色区域的边界)
i:当前位置;
开始p1=p0=i = 0,红色、白色区域都是空。
遍历整个数组
取当前元素的值保存,并把当前位置的值设置为蓝色。
将当前值与1和0作比较,若<=1,说明它不应该在这个位置,应该放到前面的红色/白色区域,那么则在p1的位置置1,p1++;
若当前值==0,说明当前值应该放到前面的红色区域去,则在p0的位置置0,p0++。
疑问------为什么当前值为0时,还是要先经过nums[p1++] = 1??
首先p0, p1代表红色白色边界,当当前值为0时,因为p0代表红色区域的边界,那么当前值放到红色区域就会和白色区域的第一个元素重叠,导致白色区域第一个数值被覆盖,使得整体报错;
因此,我们需要让p1先往右边移一位,这样当把0放到p0边界的时候,1的**数值区域已经是右移了一位,原本的1值不会被覆盖,**能够避免上述情况的发生。
代码
python
python
class Solution(object):
def sortColors(self, nums):
"""
:type nums: List[int]
:rtype: None Do not return anything, modify nums in-place instead.
"""
p0, p1 = 0, 0
for i,x in enumerate(nums):
nums[i] = 2
if x<=1:
nums[p1] = 1
p1 += 1
if x==0:
nums[p0] = 0
p0 += 1
C++
cpp
class Solution {
public:
void sortColors(vector<int>& nums) {
int p0 = 0;
int p1 = 0;
for(int i = 0;i<nums.size();i++)
{
int x = nums[i];
nums[i] = 2;
if(x <= 1)
nums[p1++] = 1;
if(x==0)
nums[p0++] = 0;
}
}
};
解题思路------三指针
取左右指针left, right,分别指向数组的开头以及结尾,对于数组中为0的数值和left指针指向值进行交换,对于数组中为2的数值和right指针指向的值进行交换。
三指针法的分区定义
-
[0, left):已经排好的 0 -
[left, i):已经排好的 1 -
[i, right]:待处理的元素 -
(right, n-1]:已经排好的 2
初始:left = 0,i = 0,right = n-1。
为什么与 left 交换后可以 i++,而与 right 交换后 i 不动?
关键在于交换来的元素是否"已知"。
-
与 left 交换 :
left指向的元素来自 已处理过的 1 区域 (或自身)。根据分区定义,
[left, i)区间内的元素都是 1(或者为空)。所以nums[left]的值只能是 1 (如果left < i)或者就是nums[i]本身(如果left == i,此时值为 0)。交换后,
nums[i]变成 1(或保持 0),这个值是已经"就绪"的,不需要再次检查,因此可以放心地i++去处理下一个位置。 -
与 right 交换 :
right指向的元素来自 尚未处理的区域[i, right]。这个区域的值可能是 0、1 或 2,完全未知。交换后,
nums[i]变成了一个未检查过的新值 ,必须留在当前位置重新判断,所以不能i++,只能right--缩小右边的 2 区域。
在三指针法(荷兰国旗问题)中,left 和 i 的初始值都为 0。随着算法进行,left 始终指向第一个不是 0 的位置 (即 1 区域的起点),而 i 指向当前正在检查的元素。它们的关系是:left ≤ i。当 left == i 时,表示 1 区域为空,当前检查的位置正好是 0 区域的边界。
代码
python
python
class Solution(object):
def sortColors(self, nums):
"""
:type nums: List[int]
:rtype: None Do not return anything, modify nums in-place instead.
"""
'''
p0, p1 = 0, 0
for i,x in enumerate(nums):
nums[i] = 2
if x<=1:
nums[p1] = 1
p1 += 1
if x==0:
nums[p0] = 0
p0 += 1
'''
n = len(nums)
left, right = 0, n -1
i = 0
while i <= right:
if nums[i] == 0:
nums[i], nums[left] = nums[left], nums[i]
left += 1
i += 1
elif nums[i] == 2:
nums[i], nums[right] = nums[right], nums[i]
right -= 1
else:
i += 1
C++
cpp
class Solution {
public:
void sortColors(vector<int>& nums) {
/*
int p0 = 0;
int p1 = 0;
for(int i = 0;i<nums.size();i++)
{
int x = nums[i];
nums[i] = 2;
if(x <= 1)
nums[p1++] = 1;
if(x==0)
nums[p0++] = 0;
}
*/
int left = 0;
int right = nums.size() - 1;
int i = 0;
while(i<= right)
{
if(nums[i]==0)
{
swap(nums[i],nums[left]);
left++;
i++;
}
else if(nums[i] == 2)
{
swap(nums[i], nums[right]);
right--;
}
else{
i++;
}
}
}
};
76.最小覆盖子串

解题思路------滑动窗口
由题意可知,我们需要的是在母串S中找到包含子串t中每一个字符的最小子串,如果没有则返回""。
这道题明显应该使用滑动窗口来解决,具体思路如下:
首先统计子串t中每个字符的出现次数,用unordered_map need来表示,以及用valid来记录t中每种字符的种类,用start以及len来表示最小子串的起点以及长度;
遍历s,如果当前字符是需要的字符,则更新当前窗口中该字符的值,并判断当前字符的出现次数和need中是否一致,若一致,则valid++。
当valid的值等于t中的字符种类的时候,这个时候我们已经找到了满足条件的子串,但并非是最优解,因此需要我们不断更新迭代判断。
若valid值等于t中字符的个数,则 更新最小子串:起始点,最小长度。
然后不断缩小这个满足条件的次优解子串,判断每一次缩小窗口时弹出的元素的值是否是子串计数need中的值?若是,如果窗口中该数的值和need中相等,那么由于该数的滑出,valid必须要--,因此此时的窗口不满足条件了。
最后返回滑动之后得到的最优解。
deepseek的总结:
-
初始化 :
left=0, right=0, valid=0,窗口为空。 -
外层 while(右指针移动):
-
取字符
c = s[right],右移right。 -
如果
c是需要的字符,增加window[c],并检查是否恰好达到需求(window[c] == need[c]),若是则valid++。
-
-
内层 while(当窗口可行时,尝试收缩左边界):
-
记录当前窗口长度(
right - left),如果比minLen小,则更新start和minLen。 -
准备移出左边界字符
d = s[left],左移left。 -
如果
d是需要的字符,且移出前window[d] == need[d](即移出后就不够了),则valid--,然后window[d]--。 -
继续收缩,直到窗口不再可行(
valid < need.size())。
-
-
重复 ,直到
right到达末尾。 -
返回
s.substr(start, minLen)。
代码
C++------窗口:左闭右开
这里需要注意一个地方,就是在C++中我们的代码是:
while(right < s.size())
{
char c = s[right++];
right指向的永远是下一个,最后是s.size()对应的部分,所以C++中窗口是[left, right),是左闭右开
所以对应的窗口长度是right - left。
python------窗口:左闭右闭
python代码则是如下:
for right,c in enumerate(s):
right永远是当前数组的元素,不会出现越界情况。因此对应的窗口是[left, right],是左闭右闭。
对应的窗口长度是right - left + 1。
python
python
class Solution(object):
def minWindow(self, s, t):
from collections import defaultdict
"""
:type s: str
:type t: str
:rtype: str
"""
left, right = 0, 0
start = 0
minlen = float('inf')
valid = 0
need = defaultdict(int)
window = defaultdict(int)
# 统计t中每个字符的出现的数量
for ch in t:
need[ch]+=1
# 滑动窗口
for right,c in enumerate(s):
if c in need:
window[c] += 1
if window[c] == need[c]:
valid += 1
# <==============注意python是左闭右开的区间========================>
# 窗口中亦含有t中所有元素,缩小左边界,优化,以得到最优解
# right - left +1: 当前窗口的长度
while valid== len(need):
if right - left +1 < minlen:
start = left
minlen = right - left + 1
d = s[left]
left += 1
# 当d是t中的元素的时候再做下面的判断、处理
if d in need:
if window[d]==need[d]:
valid -= 1
window[d] -= 1
return "" if minlen==float('inf') else s[start:start+minlen]
C++
cpp
class Solution {
public:
string minWindow(string s, string t) {
unordered_map<char, int> need, window;
int valid = 0; //字符串t中的字符种类数目
int start = 0;
int minLen = INT_MAX;
int left = 0;
int right = 0;
for(char c : t)
need[c]++;
//寻找满足条件的最小覆盖字串
while(right < s.size())
{
char c = s[right++];
//若当前字符是need中需要的,则进行下一步
//当前字符的数目和need中次数一致,则有效种类数目+1
if(need.count(c))
{
window[c]++;
if(window[c] == need[c])
valid++;
}
// 更新得到最优解
while(valid==need.size())
{
if(right - left < minLen)
{
start = left;
minLen = right - start;
}
char d = s[left];
left++;
//移出前满足,移除后便不满足,因此valid需要-1
if(need.count(d))
{
if(window[d] == need[d])
valid--;
window[d]--;
}
}
}
return minLen==INT_MAX? "": s.substr(start, minLen);
}
};
283.移动零

解题思路
题目中要求必须在不复制数组的情况下原地对数组进行操作,那就是要求空间复杂度时O(1),此处我们使用一次遍历:
这里参考了快速排序的思路:快速排序是选取一个中间点x,把小于等于x的结果放到x的左边,把大于x的结果放到x的右边。
那么这道题,我们选取0作为这个中间点,不等于0的数放到数组的左边,那么余下的数放到0的右边。
我们取i, j这两个指针,遍历nums中不是0的数,将nums[i]的值和nums[j]的值做交换。
代码
python
python
class Solution(object):
def moveZeroes(self, nums):
"""
:type nums: List[int]
:rtype: None Do not return anything, modify nums in-place instead.
"""
if nums is None:
return
j = 0
for i in range(len(nums)):
if nums[i] != 0:
t = nums[i]
nums[i] = nums[j]
nums[j] = t
j += 1
C++
cpp
class Solution {
public:
void moveZeroes(vector<int>& nums) {
if(nums.empty())
return;
// 两个指针i, j
int j = 0;
for(int i =0; i < nums.size();i++){
// 把nums[i]中不等于0的值放到前面,之后的就都是0
if(nums[i] != 0)
{
int t = nums[i];
nums[i] = nums[j];
nums[j++] = t;
}
}
}
};
参考
287.寻找重复数

解题思路
本题其实还是使用双指针法,但是在这个地方,需要理清以下情况:
对于任意的数组,按照当前下标--->当前下标所指向的值,这样可以构成序列。
题目中nums含有n+1个数,下标从0~n,数值是[1, n],其中只有一个整数出现2次或者更多,其余正数均只出现一次。
如果nums中出现了不同下标对应的值相同 的情况,那么这个nums所对应的序列中就会出现环。
重复的数就是这个环的入口。
举例说明一下:
nums = [1, 3, 4, 2 , 2]
下标 值
0 1
1 3
2 4
3 2
4 2
形成的序列就是:
0 -> 1 -> 3 -> 2 -> 4
| |
4 <- 2
最后一个徘徊在2-4-2-4-2......这个环当中,其中环的入口就是2,也就是nums中的重复的数。
那么如何得到这个重复的数呢?
首先,我们取快慢指针fast, slow;
初始化:快慢指针都初始化为0,然后快指针每一次往前走2步,而慢指针每一次往前走1步。
如果有重复元素的话,那么快慢指针最终一定会在环内某个部分相遇,假设快慢指针最终在据环的入口处c相遇:
假设环外的元素长度是a ,环的长度是b,那么
快指针走过的距离:a+m*b + c
慢指针走过的距离:a+n*b + c
其中n一定小于m。
由上述2个式子可以得到a+m*b + c = 2(a+n*b + c) ====> a+c = (m-n)*b ==>a+c是环的整数倍长度
那如何得到这个重复元素呢?
我们让一个元素从起点开始走(pre1),让另一个元素从slow和fast指针相遇的地方开始走(pre2)。
当pre1走过的距离是a,到达环的入口处时,那么pre2走过的距离是c+a, 而c+a正好是换的长度的整数倍,也就是说此时pre2也正好走到了环的入口处,所以这俩一定会在环的入口处相遇,最终pre1指向的值就是nums中的重复元素。
代码
python
python
class Solution(object):
def findDuplicate(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
# 快慢指针都从起点开始出发,快指针一次走2步,慢指针一次走1步
fast, slow = 0, 0
fast = nums[nums[fast]]
slow = nums[slow]
while fast != slow:
fast = nums[nums[fast]]
slow = nums[slow]
# 这时快慢指针相等,也即找了重复元素
pre1, pre2 = 0, slow
while pre1 != pre2:
pre1 = nums[pre1]
pre2 = nums[pre2]
# 找到了重复元素pre1 = nums[pre1]
return pre1
C++
cpp
class Solution {
public:
int findDuplicate(vector<int>& nums) {
int slow = 0;
int fast = 0;
slow = nums[slow];
fast = nums[nums[fast]];
while(slow != fast)
{
slow = nums[slow];
fast = nums[nums[fast]];
}
int pre1 = 0;
int pre2 = slow;
while(pre1 != pre2)
{
pre1 = nums[pre1];
pre2 = nums[pre2];
}
return pre1;
}
};
参考
647.回文子串

解题思路
本题思路和最长回文子串的思路一致,我们使用的是中心扩展法,其中这里的中心有2*n-1个,为什么有这么多中心,可以看我上面的最长回文子串的讲解,里面有,在这个地方我们只需要在每一个判断是回文串的时候,计数就行。
代码
python
python
class Solution(object):
def countSubstrings(self, s):
"""
:type s: str
:rtype: int
"""
n = len(s)
count = 0
# 中心扩展法
for i in range(2*n-1):
left = i // 2
right = (i+1)//2
while left >= 0 and right < n and s[left]==s[right]:
count += 1
left -= 1
right += 1
return count
C++
cpp
class Solution {
public:
int countSubstrings(string s) {
int left = 0;
int right = 0;
int count = 0;
// 中心扩展法
for(int i = 0;i < (2*(s.size()) - 1);i++)
{
left = i / 2;
right = (i + 1) / 2;
while(left>=0 && right<s.size()&&s[right]==s[left])
{
left--;
right++;
count++;
}
}
return count;
}
};