LeetCode11.盛水最多的容器:
题目描述:
给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。
找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
返回容器可以储存的最大水量。
说明:你不能倾斜容器。
示例 1:
输入:[1,8,6,2,5,4,8,3,7]
输出:49
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
示例 2:
输入:height = [1,1]
输出:1
提示:
n == height.length
2 <= n <= 10^5
0 <= height[i] <= 10^4
思路:
贪心思想:使用双指针分别从头和尾开始,每次计算比较两个指针的高度,将较小的往中间移一个位置,每次保留最大的装水体积
证明:

代码:
class Solution {
public:
int maxArea(vector<int>& height) {
int res = 0;
for(int i = 0, j = height.size() - 1; i < j; )
{
res = max(res, min(height[i], height[j]) * (j - i));
if(height[i] > height[j]) j--;
else i++;
}
return res;
}
};
LeetCode12.整数转罗马数字:
题目描述:
七个不同的符号代表罗马数字,其值如下:
符号:值
I 1
V 5
X 10
L 50
C 100
D 500
M 1000
罗马数字是通过添加从最高到最低的小数位值的转换而形成的。将小数位值转换为罗马数字有以下规则:
如果该值不是以 4 或 9 开头,请选择可以从输入中减去的最大值的符号,将该符号附加到结果,减去其值,然后将其余部分转换为罗马数字。
如果该值以 4 或 9 开头,使用减法形式,表示从以下符号中减去一个符号,例如 4 是 5 (V) 减 1 (I): IV ,9 是 10 (X) 减 1 (I):IX。仅使用以下减法形式:4 (IV),9 (IX),40 (XL),90 (XC),400 (CD) 和 900 (CM)。
只有 10 的次方(I, X, C, M)最多可以连续附加 3 次以代表 10 的倍数。你不能多次附加 5 (V),50 (L) 或 500 (D)。如果需要将符号附加4次,请使用减法形式。
给定一个整数,将其转换为罗马数字。
示例 1:
输入:num = 3749
输出: "MMMDCCXLIX"
解释:
3000 = MMM 由于 1000 (M) + 1000 (M) + 1000 (M)
700 = DCC 由于 500 (D) + 100 © + 100 ©
40 = XL 由于 50 (L) 减 10 (X)
9 = IX 由于 10 (X) 减 1 (I)
注意:49 不是 50 (L) 减 1 (I) 因为转换是基于小数位
示例 2:
输入:num = 58
输出:"LVIII"
解释:
50 = L
8 = VIII
示例 3:
输入:num = 1994
输出:"MCMXCIV"
解释:
1000 = M
900 = CM
90 = XC
4 = IV
提示:
1 <= num <= 3999
思路: 找规律:

这样的话从高位往下依次减, 只要将圈住的部分用数组存起来,每次找到对应的数值转换成字符串之后,再原有基础上减去当前数值,然后继续往下找能减的部分继续减并转换。
代码:
class Solution {
public:
string intToRoman(int num) {
int values[] = {
1000,
900, 500, 400, 100,
90, 50, 40, 10,
9, 5, 4, 1
};
string reps[] = {
"M",
"CM", "D", "CD", "C",
"XC", "L", "XL", "X",
"IX", "V", "IV", "I"
};
string res;
for(int i = 0; i < 13; i++)
{
while(num >= values[i])
{
num -= values[i];
res += reps[i];
}
}
return res;
}
};
LeetCode13. 罗马数字转整数:
题目描述:
罗马数字包含以下七种字符: I, V, X, L,C,D 和 M。
字符 数值
I 1
V 5
X 10
L 50
C 100
D 500
M 1000
例如, 罗马数字 2 写做 II ,即为两个并列的 1 。12 写做 XII ,即为 X + II 。 27 写做 XXVII, 即为 XX + V + II 。
通常情况下,罗马数字中小的数字在大的数字的右边。但也存在特例,例如 4 不写做 IIII,而是 IV。数字 1 在数字 5 的左边,所表示的数等于大数 5 减小数 1 得到的数值 4 。同样地,数字 9 表示为 IX。这个特殊的规则只适用于以下六种情况:
I 可以放在 V (5) 和 X (10) 的左边,来表示 4 和 9。
X 可以放在 L (50) 和 C (100) 的左边,来表示 40 和 90。
C 可以放在 D (500) 和 M (1000) 的左边,来表示 400 和 900。
给定一个罗马数字,将其转换成整数。
示例 1:
输入: s = "III"
输出: 3
示例 2:
输入: s = "IV"
输出: 4
示例 3:
输入: s = "IX"
输出: 9
示例 4:
输入: s = "LVIII"
输出: 58
解释: L = 50, V= 5, III = 3.
示例 5:
输入: s = "MCMXCIV"
输出: 1994
解释: M = 1000, CM = 900, XC = 90, IV = 4.
提示:
1 <= s.length <= 15
s 仅含字符 ('I', 'V', 'X', 'L', 'C', 'D', 'M')
题目数据保证 s 是一个有效的罗马数字,且表示整数在范围 [1, 3999] 内
题目所给测试用例皆符合罗马数字书写规则,不会出现跨位等情况。
IL 和 IM 这样的例子并不符合题目要求,49 应该写作 XLIX,999 应该写作 CMXCIX 。
关于罗马数字的详尽书写规则,可以参考 罗马数字 - 百度百科。
思路:
找潜在规律可以发现:在I, V,X, L,C, D, M这些基本的罗马数字之间
只要前面的罗马字符代表的数值大于当前代表的数值,就可以直接将当前字符代表的数值加到后面,而 IV, IX, XL, XC, CD, CM这几个罗马数字可以发现,前面的单独字符都比后面的字符所代表的数值小,而后者代表数值减去前者代表数值刚好为它本身代表的数值,所以总结发现,我们只需要判断前后两个字符单独代表的数值大小,就能直接对每个字符进行加减运算
实现方式:将单独的罗马字符以及它表示的数值映射到hash表中,依次遍历罗马数字的每个字符,找到hash表中对应的数值,对比前后两个字符表示的数值,进行加减即可。

代码:
class Solution {
public:
int romanToInt(string s) {
unordered_map<char, int> hash;
hash['I'] = 1, hash['V'] = 5;
hash['X'] = 10, hash['L'] = 50;
hash['C'] = 100, hash['D'] = 500;
hash['M'] = 1000;
int res = 0;
for(int i = 0; i < s.size(); i++)
{
//如果i的后面有字符,并且代表的数值小于后面的字符代表数值
if(i + 1 < s.size() && hash[s[i]] < hash[s[i + 1]])
{
//那么减去代表数值
res -= hash[s[i]];
}
//否则加上代表数值
else res += hash[s[i]];
}
return res;
}
};
纯享版:
class Solution {
public:
int romanToInt(string s) {
unordered_map<char, int> hash;
hash['I'] = 1, hash['V'] = 5;
hash['X'] = 10, hash['L'] = 50;
hash['C'] = 100, hash['D'] = 500;
hash['M'] = 1000;
int ans = 0;
for(int i = 0; i < s.size(); i++)
{
if(i + 1 < s.size() && hash[s[i]] < hash[s[i + 1]])
{
ans -= hash[s[i]];
}
else ans += hash[s[i]];
}
return ans;
}
};
LeetCode14.最长公共前缀:
题目描述:
编写一个函数来查找字符串数组中的最长公共前缀。
如果不存在公共前缀,返回空字符串 ""。
示例 1:
输入:strs = ["flower","flow","flight"]
输出:"fl"
示例 2:
输入:strs = ["dog","racecar","car"]
输出:""
解释:输入不存在公共前缀。
提示:
1 <= strs.length <= 200
0 <= strs[i].length <= 200
strs[i] 仅由小写英文字母组成
思路:
从第一个字符开始依次对比每个字符串的每个字符,全能比对上就将当前字符加入答案子串中
注意:
这里加&符是为了减少时间消耗,否则的话每次循环复制一遍整个数组,时间复杂度会上升
代码:
class Solution {
public:
string longestCommonPrefix(vector<string>& strs) {
string res;
if(strs.empty()) return res;
//从第一个字符开始遍历
for(int i = 0; i <= 200; i++)
{
//如果i大于第一个字符串的长度了,那么后面也就没必要继续了
if(i >= strs[0].size()) return res;
//每次取第一个字符串的第i个字符
char c = strs[0][i];
for(auto& str : strs)
{
//跟每一个字符串进行比对,如果字符串长度小于i那么肯定不用继续了,或者说第i个字符匹配不上
if(str.size() <= i || str[i] != c)
{
return res;
}
}
//能比对上就在答案子串里将当前字符加上
res += c;
}
return res;
}
};
纯享版:
class Solution {
public:
string longestCommonPrefix(vector<string>& strs) {
string res;
if(strs.empty()) return res;
for(int i = 0; i <= 200; i++)
{
if(i >= strs[0].size()) return res;
char c = strs[0][i];
for(auto& str : strs)
{
if(str.size() <= i || str[i] != c)
{
return res;
}
}
res += c;
}
return res;
}
LeetCode15.三数之和:
题目描述:
给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例 1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。
示例 2:
输入:nums = [0,1,1]
输出:[]
解释:唯一可能的三元组和不为 0 。
示例 3:
输入:nums = [0,0,0]
输出:[[0,0,0]]
解释:唯一可能的三元组和为 0 。
提示:
3 <= nums.length <= 3000
-105 <= nums[i] <= 105
思路:
先从小到大排序,然后先固定最小的那个数nums[i],再使用双指针将后面的两个数确定下来,第二个数nums[j]从第一个数nums[i]后面开始,第三个数nums[k]从最后面开始,每次找到能满足nums[i] + nums[j] + nums[k] >= 0最小的nums[k]三个数的组合,过程中会发现当nums[j]往后遍历时,满足条件的nums[k]会往前挪,两者(j ,k)都具有单调性,使用双指针算法可以极大优化时间复杂度,将原本O(n ^2)变为O(2n)时间复杂度。

代码:
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> res;
//先进行排序
sort(nums.begin(), nums.end());
//先将第一个数固定下来
for(int i = 0; i < nums.size(); i++)
{
//避免找到重复的方案, 因为已经排序了,nums[i]也固定了, 后面两个数肯定比nums[i]大
//而且nums[i]的方案后面已经全部考虑了
if(i && nums[i] == nums[i - 1]) continue;
//双指针算法: 可以发现,j 从i开始的话,只会逐渐增大, 而k从后往前开始会逐渐减小,具有单调性,使用双指针算法可以极大优化时间复杂度,将原本O(N ^2)变为O(2n)
//j:表示第二个数, 从i后面开始找
//k: 表示第三个数,从最后面往前找
//j < k : 两个指针不能重叠
for(int j = i + 1, k = nums.size() - 1; j < k; j++)
{
//这里的j和i一样需要排除重复方案
if(j > i + 1 && nums[j] == nums[j - 1]) continue;
//找到三个数相加大于零的最小的k
//如果k的下一个数没有和j重叠,并且三个数之和大于零就往前挪一位,
//因为从小到大排序了,所以越往前挪越接近0
while(j < k - 1 && nums[i] + nums[j] + nums[k - 1] >= 0) k--;
//看三个数之和是否满足条件,满足就将三个数存到容器中
if(nums[i] + nums[j] + nums[k] == 0)
{
res.push_back({nums[i], nums[j], nums[k]});
}
}
}
return res;
}
};
纯享版:
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> res;
sort(nums.begin(), nums.end());
for(int i = 0; i < nums.size(); i++)
{
if(i && nums[i] == nums[i - 1]) continue;
for(int j = i + 1, k = nums.size() - 1; j < k; j++)
{
if(j > i + 1 && nums[j] == nums[j - 1]) continue;
while(j < k - 1 && nums[i] + nums[j] + nums[k - 1] >= 0) k--;
if(nums[i] + nums[j] + nums[k] == 0)
{
res.push_back({nums[i], nums[j], nums[k]});
}
}
}
return res;
}
};
###LeetCode16.最接近的三数之和:
##题目描述:
给你一个长度为 n 的整数数组 nums 和 一个目标值 target。请你从 nums 中选出三个整数,使它们的和与 target 最接近。
返回这三个数的和。
假定每组输入只存在恰好一个解。
示例 1:
输入:nums = [-1,2,1,-4], target = 1
输出:2
解释:与 target 最接近的和是 2 (-1 + 2 + 1 = 2)。
示例 2:
输入:nums = [0,0,0], target = 1
输出:0
解释:与 target 最接近的和是 0(0 + 0 + 0 = 0)。
提示:
3 <= nums.length <= 1000
-1000 <= nums[i] <= 1000
-10^4 <= target <= 10^4
##思路:这里跟LeetCode15题思路一致,不过要注意细节方面处理,要考虑最接近target的和,那么有可能是小于target也有可能是大于target,因此要将三数之和与target的差值进行比较,留下最小差的和,


##代码:
class Solution {
public:
int threeSumClosest(vector<int>& nums, int target) {
sort(nums.begin(), nums.end());
pair<int, int> res(INT_MAX, INT_MAX);
for(int i = 0; i < nums.size(); i++)
{
for(int j = i + 1, k = nums.size() - 1; j < k; j++)
{
while(k - 1 > j && nums[i] + nums[j] + nums[k - 1] >= target) k--;
//第一种是nums[i] + nums[j] + nums[k] >= target 的情况
int s = nums[i] + nums[j] + nums[k];
//记录最接近target的和,这里有可能会出现所有和都小于target的情况,所以使用abs
res = min(res, make_pair(abs(s - target), s));
//nums[i] + nums[j] + nums[k] < target 的情况
//因为第一种情况将nums[k] 逼到三个数最靠近target的情况,
//如果nums[k] + nums[i] + nums[j] 是最接近大于等于target的,那么nums[k - 1] + nums[i] + nums[j] 肯定是小于target的
if(k - 1 > j)
{
s = nums[i] + nums[j] + nums[k - 1];
res = min(res, make_pair(target - s, s));
}
}
}
return res.second;
}
};
##纯享版:
class Solution {
public:
int threeSumClosest(vector<int>& nums, int target) {
sort(nums.begin(), nums.end());
pair<int, int> res(INT_MAX, INT_MAX);
for(int i = 0; i < nums.size(); i++)
{
for(int j = i + 1, k = nums.size() - 1; j < k; j++)
{
while(j < k - 1 && nums[i] + nums[j] + nums[k - 1] >= target) k--;
int s = nums[i] + nums[j] + nums[k];
res = min(res, make_pair(abs(s - target), s));
if(j < k - 1)
{
int s = nums[i] + nums[j] + nums[k - 1];
res = min(res, make_pair(target - s, s));
}
}
}
return res.second;
}
};
LeetCode17.电话号码的数字组合:
题目描述:
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例 1:
输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
示例 2:
输入:digits = ""
输出:[]
示例 3:
输入:digits = "2"
输出:["a","b","c"]
提示:
0 <= digits.length <= 4
digits[i] 是范围 ['2', '9'] 的一个数字。
思路
根据题目描述,将每位数字对应的字母排列起来,也就是依次找到每位数字对应的字母,分别取一个字符进行排列(排列组合问题),有点类似全排列,用dfs算法:深度优先遍历
##注意:只要是涉及到排列组合问题,只要能找到对应的排列数,先考虑dfs能不能做。
代码:
class Solution {
public:
vector<string> ans;
string strs[10] = {
"", "", "abc", "def",
"ghi", "jkl", "mno",
"pqrs", "tuv", "wxyz"
};
vector<string> letterCombinations(string digits) {
if(digits.empty()) return ans;
dfs(digits, 0, "");
return ans;
}
void dfs(string&digits, int u, string path)
{
//path存方案:表示每次的路径
//u表示当前是第几位
//如果u == digits.size() 说明已经排列好了, 将当前走的路径放入ans里
if(u == digits.size()) ans.push_back(path);
else{
//取出当前位代表的每一个字符, 分别递归下去,直到将所有位全部排完
for(auto c : strs[digits[u] - '0'])
{
dfs(digits, u + 1, path + c);
}
}
}
};
纯享版:
class Solution {
public:
vector<string> res;
string strs[10] = {
"", "", "abc", "def",
"ghi", "jkl", "mno",
"pqrs", "tuv", "wxyz"
};
vector<string> letterCombinations(string digits) {
if(digits.empty()) return res;
dfs(digits, 0, "");
return res;
}
void dfs(string& digits, int u, string path)
{
if(u == digits.size()) res.push_back(path);
else{
for(auto c : strs[digits[u] - '0'])
{
dfs(digits, u + 1, path + c);
}
}
}
};
LeetCode18.四数之和:
题目描述:
给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):
0 <= a, b, c, d < n
a、b、c 和 d 互不相同
nums[a] + nums[b] + nums[c] + nums[d] == target
你可以按 任意顺序 返回答案 。
示例 1:
输入:nums = [1,0,-1,0,-2,2], target = 0
输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]
示例 2:
输入:nums = [2,2,2,2,2], target = 8
输出:[[2,2,2,2]]
提示:
1 <= nums.length <= 200
-10^9 <= nums[i] <= 10^9
-10^9 <= target <= 10^9
思路:
这里思路跟三数之和一模一样,只不过使用三层循环,同样使用双指针优化,时间复杂度为O(n^2 * 2n)
注意:
这里数值范围会超过int的取值范围, 所以要使用long long 进行存储
代码:
class Solution {
public:
typedef long long LL;
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int> > res;
sort(nums.begin(), nums.end());
LL num = target;
for(int i = 0; i < nums.size(); i++)
{
//cout<<i<<endl;
if(i && nums[i] == nums[i - 1]) continue;
for(int j = i + 1; j < nums.size(); j++)
{
//cout<<j<<endl;
if(j > i + 1 && nums[j] == nums[j - 1]) continue;
for(int k = j + 1, u = nums.size() - 1; k < u; k++)
{
if(k > j + 1 && nums[k] == nums[k - 1]) continue;
while(k < u - 1 && (LL)(nums[i] + nums[j]) + (LL)(nums[k] + nums[u - 1]) >= num) u--;
if((LL)(nums[i] + nums[j]) + (LL)(nums[k] + nums[u]) == num)
{
res.push_back({nums[i], nums[j], nums[k], nums[u]});
}
}
}
}
return res;
}
};
纯享版:
LeetCode19.删除链表的倒数第N个节点:
题目描述:
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
示例 1:
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
示例 2:
输入:head = [1], n = 1
输出:[]
示例 3:
输入:head = [1,2], n = 1
输出:[1]
提示:
链表中结点的数目为 sz
1 <= sz <= 30
0 <= Node.val <= 100
1 <= n <= sz
##思路:找到链表的倒数第N+1个数,将指针直接指向倒数第N个数的后面,也就是倒数第N+1 个数的next的next
注意:
一般情况下可以直接跳过倒数第N个数,但有些时候需要删除第N个节点
注释代码:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
//定义虚拟头节点
auto dummy = new ListNode(-1);
//将虚拟头节点连接在原本头节点前
dummy ->next = head;
int num = 0;
//计算链表长度
for(auto p = dummy; p; p = p->next) num++;
auto p = dummy;
//从虚拟头节点开始,跳到倒数 n + 1个数的位置
for(int i = 0; i < num - n - 1; i++) p = p -> next;
//让倒数第n + 1个数直接指向后面的后面, 就能直接跳过倒数第 n个数
p -> next = p -> next -> next;
//返回链表
return dummy -> next;
}
};
纯享版:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
auto dummy = new ListNode(-1);
dummy -> next = head;
int num = 0;
for(auto p = dummy; p; p = p -> next) num++;
auto p = dummy;
for(int i = 0; i < num - n - 1; i++) p = p -> next;
p -> next = p -> next -> next;
return dummy -> next;
}
};
LeetCode20.有效的括号:
题目描述:
给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
每个右括号都有一个对应的相同类型的左括号。
示例 1:
输入:s = "()"
输出:true
示例 2:
输入:s = "()[]{}"
输出:true
示例 3:
输入:s = "(]"
输出:false
示例 4:
输入:s = "([])"
输出:true
提示:
1 <= s.length <= 10^4
s 仅由括号 '()[]{}' 组成
思路:
总思路使用栈,每次将左括号入栈,而右括号则进行匹对,如果每个右括号都能在栈顶找到对应的左括号,每次将匹配的栈顶弹出,最后如果栈中没有元素那么说明是有效的。 这里的算法1是使用取巧方法, 查ASCII码值发现,每对括号的差值都不超过2,如果超过说明是不匹配的,那么直接return false 就行。 算法2则是枚举每种右括号的情况,在栈不为空的情况下进行栈顶比对,匹配上则直接将栈顶弹出
算法1:(取巧)代码:
class Solution {
public:
bool isValid(string s) {
stack<char> stk;
for(auto c : s)
{
//如果是右括号,那么将它存入栈中
if(c == '{' || c == '[' || c == '(') stk.push(c);
else {
//如果是左括号,那么根据当前字符与栈顶元素的ASCII码值是否小于等于2来判断是否匹配
//匹配的话将当前栈顶去除
if(stk.size() && abs(stk.top() - c) <= 2) stk.pop();
else return false;
}
}
//如果全部能够匹配上,那么栈为空
return stk.empty();
}
};
算法2: 枚举情况:
class Solution {
public:
bool isValid(string s) {
stack<int> stk;
for(auto c : s)
{
if(c == '[' || c == '{' || c == '(') stk.push(c);
else{
if(stk.size() && c == ']' && stk.top() == '[') stk.pop();
else if(stk.size() && c == '}' && stk.top() == '{') stk.pop();
else if(stk.size() && c == ')' && stk.top() == '(') stk.pop();
else return false;
}
}
return stk.empty();
}
};
LeetCode21.合并两个有序链表:
题目描述:
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例 1:
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
示例 2:
输入:l1 = [], l2 = []
输出:[]
示例 3:
输入:l1 = [], l2 = [0]
输出:[0]
提示:
两个链表的节点数目范围是 [0, 50]
-100 <= Node.val <= 100
l1 和 l2 均按 非递减顺序 排列
##注释代码:
方法1:
每次将归并的新链表的下一个节点直接指向较小的链表节点
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
//定义虚拟头节点
auto dummy = new ListNode(-1);
//当前尾节点
auto tail = dummy;
//每次将最小值归并到新建链表中
while(l1 && l2)
{
if(l1 -> val > l2 -> val)
{
tail -> next = l2;
tail = tail -> next;
l2 = l2 -> next;
} else {
tail -> next = l1;
tail = tail -> next;
l1 = l1 -> next;
}
}
//这里是将l1和l2剩余部分直接拼接到归并后的链表后面,所以不需要指针再一步一步移动
if(l1) tail -> next = l1;
if(l2) tail -> next = l2;
return dummy -> next;
}
};
写法2:
每次将较小的链表节点值赋予归并的新链表
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
//定义虚拟头节点
auto dummy = new ListNode(-1);
//当前尾节点
auto tail = dummy;
//每次将最小值归并到新建链表中
while(l1 && l2)
{
if(l1 -> val > l2 -> val)
{
tail -> next = new ListNode(l2 -> val);
tail = tail -> next;
l2 = l2 -> next;
} else {
tail -> next = new ListNode(l1 -> val);
tail = tail -> next;
l1 = l1 -> next;
}
}
//这里是将l1和l2剩余部分直接拼接到归并后的链表后面,所以不需要指针再一步一步移动
if(l1) tail -> next = l1;
if(l2) tail -> next = l2;
return dummy -> next;
}
};
纯享版:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
auto dummy = new ListNode(-1);
auto tail = dummy;
while(l1 && l2)
{
if(l1 -> val > l2 -> val)
{
tail -> next = l2;
tail = tail -> next;
l2 = l2 -> next;
}else{
tail -> next = l1;
tail = tail -> next;
l1 = l1 -> next;
}
}
if(l1) tail -> next = l1;
if(l2) tail -> next = l2;
return dummy -> next;
}
};
LeetCode22.括号生成:
题目描述:
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
示例 1:
输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]
示例 2:
输入:n = 1
输出:["()"]
提示:
1 <= n <= 8
思路:
这里重要的有两步,一是最后看左右括号数量是否相等,二是每次要看左括号的数量是否大于等于右括号的数量,因为在限制条件下,只要左括号的数量小于右括号,就可以通过添加右括号去合法化方案,但是如果有一个地方右括号数量比左括号多就无法在后面进行补齐

代码:
class Solution {
public:
vector<string> res;
vector<string> generateParenthesis(int n) {
dfs(n, 0, 0, "");
return res;
}
void dfs(int& n, int lc, int rc, string path)
{
if(lc == n && rc == n) res.push_back(path);
else{
if(rc < n && lc > rc) dfs(n, lc, rc + 1, path + ')');
if(lc < n) dfs(n, lc + 1, rc, path + '(');
}
}
};
LeetCode23.合并K个排序链表:
题目描述:
给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。
示例 1:
输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:
1-\>4-\>5, 1-\>3-\>4, 2-\>6
将它们合并到一个有序链表中得到。
1->1->2->3->4->4->5->6
示例 2:
输入:lists = []
输出:[]
示例 3:
输入:lists = [[]]
输出:[]
提示:
k == lists.length
0 <= k <= 10^4
0 <= lists[i].length <= 500
-10^4 <= lists[i][j] <= 10^4
lists[i] 按 升序 排列
lists[i].length 的总和不超过 10^4
思路:
跟 LeetCode21.合并两个有序链表 一样,每次将最小的节点值归并到新的链表中,正常做法是枚举对比每个链表的节点值,依次将最小的归并到新的链表中,这样的话找最小值的时间复杂度是k * n的,但是这里使用优先队列(小根堆)维护最小值,每次插入和删除的时间复杂度为log(k)的,那么n个链表就是n* log(k)
注意:
这里涉及的优先队列(默认大根堆)进行重载成小根堆不懂的参考 Acwing839.模拟堆 和 关于优先队列大小根堆重载操作符的说明
注释代码:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
//对STL容器的比较运算符"()"进行重载
//因为STL容器在比较的时候用的是结构体的小括号运算符
struct Cmp{
//这里一般来说默认是大根堆,默认小的在下面
//大根堆的排序判断是传入less,也就是比较父子节点的值,如果父节点的值小于子节点那么进行交换
//每次将大的值移到上面,所以最后形成大根堆
//我们要使用小根堆每次维护最小的节点值,所以要对它的默认判断进行重载
//重载为greater,每次比较父子节点的值,将小的传到上面,形成小根堆
bool operator() (ListNode* a, ListNode* b)
{
return a -> val > b -> val;
}
};
ListNode* mergeKLists(vector<ListNode*>& lists) {
priority_queue<ListNode*, vector<ListNode*>, Cmp> heap;
auto dummy = new ListNode(-1);
auto tail = dummy;
//防止传入空链表
//这里传入的是每个链表头节点的地址
for(auto l : lists) if(l) heap.push(l);
while(heap.size())
{
//每次取出并去除小根堆的堆顶元素
auto t = heap.top();
heap.pop();
//让当前新链表的尾节点往后移动
tail -> next = t;
tail = tail -> next;
//如果之前堆顶元素的后面还有元素(也就是将堆顶元素所在的链表的下一个节点放入到堆中,重新排序,找出最小元素的链表的头节点)
if(t -> next) heap.push(t -> next);
}
return dummy -> next;
}
};
纯享版:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
struct Cmp
{
bool operator() (ListNode* a, ListNode* b)
{
return a -> val > b -> val;
}
};
ListNode* mergeKLists(vector<ListNode*>& lists) {
priority_queue<ListNode*, vector<ListNode*>, Cmp> h;
auto dummy = new ListNode(-1);
auto tail = dummy;
for(auto l : lists) if(l) h.push(l);
while(h.size())
{
auto temp = h.top();
h.pop();
tail -> next = temp;
tail = tail -> next;
if(temp -> next) h.push(temp -> next);
}
return dummy -> next;
}
};
LeetCode24.两两交换链表中的节点:
题目描述:
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
示例 1:
输入:head = [1,2,3,4]
输出:[2,1,4,3]
示例 2:
输入:head = []
输出:[]
示例 3:
输入:head = [1]
输出:[1]
提示:
链表中节点的数目在范围 [0, 100] 内
0 <= Node.val <= 100
思路:
链表问题惯例定义虚拟头节点,然后接在链表的前面,设立三个指针,维护虚拟头节点和交换的两个节点,虚拟头节点每次移动到要交换的两个节点前面,只要虚拟头节点后面两个节点都存在就进行交换并进行指针移动,这里特别注意的是三个指针之间的移动关系,防止丢失
代码:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
auto dummy = new ListNode(-1);
dummy -> next = head;
for(auto p = dummy; p -> next && p -> next -> next;)
{
auto a = p -> next, b = a -> next;
p -> next = b;
a -> next = b -> next;
b -> next = a;
p = a;
}
return dummy -> next;
}
};
纯享版:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
auto dummy = new ListNode(-1);
dummy -> next = head;
auto p = dummy;
while(p -> next && p -> next -> next)
{
auto a = p -> next, b = a -> next;
a -> next = b -> next;
p -> next = b;
b -> next = a;
p = a;
}
return dummy -> next;
}
};
###LeetCode25.K个一组翻转链表:
##题目描述:
给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。
k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
示例 1:

输入:head = [1,2,3,4,5], k = 2
输出:[2,1,4,3,5]
示例 2:
输入:head = [1,2,3,4,5], k = 3
输出:[3,2,1,4,5]
提示:
链表中的节点数目为 n
1 <= k <= n <= 5000
0 <= Node.val <= 1000
##思路:题目看起来比较复杂,涉及翻转又要循环,那么为了将复杂事情简单化,思路一般为分步:
##第一步: 每次循环看当前位置的后面够不够k个节点,不足之间跳出
##第二步: 对于k个节点内部进行k-1条链表边进行循环翻转,每两个点之间翻转,依次迭代
##第三步: 将翻转后的头尾重新进行节点衔接

注意:
处理链表问题时,一定要时刻注意存取下一个节点的位置,否则的话无法衔接到正确的位置
注释代码:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* reverseKGroup(ListNode* head, int k) {
auto dummy = new ListNode(-1);
dummy -> next = head;
for(auto p = dummy;;)
{
auto q = p;
//先判断p后面够不够k个元素: p往后面跳k步后的元素是否为空
//保证q不是空节点才往后跳
for(int i = 0; i < k && q; i++) q = q -> next;
if(!q) break;
//处理内部翻转
//k个节点总共k-1条边
auto a = p -> next, b = a -> next;
for(int i = 0; i < k - 1; i++)
{
auto c = b -> next;
b -> next = a; //内部反转,将后面的指针指向前面
//指针错位(迭代)
a = b, b = c;
}
//每次的头尾连接
auto c = p -> next;
p -> next = a;
c -> next = b;
//将p指针移动到下一次的前一个位置
p = c;
}
return dummy -> next;
}
};
纯享版:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* reverseKGroup(ListNode* head, int k) {
auto dummy = new ListNode(-1);
dummy -> next = head;
for(auto p = dummy;;)
{
auto temp = p;
for(int i = 0; i < k && temp; i++) temp = temp -> next;
if(!temp) break;
auto a = p -> next, b = a -> next;
for(int i = 0; i < k - 1; i++)
{
auto c = b -> next;
b -> next = a;
a = b, b = c;
}
auto c = p -> next;
p -> next = a;
c -> next = b;
p = c;
}
return dummy -> next;
}
};
LeetCode26.删除排序数组中的重复项:
题目描述:
给你一个 非严格递增排列 的数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums 中唯一元素的个数。
考虑 nums 的唯一元素的数量为 k ,你需要做以下事情确保你的题解可以被通过:
更改数组 nums ,使 nums 的前 k 个元素包含唯一元素,并按照它们最初在 nums 中出现的顺序排列。nums 的其余元素与 nums 的大小不重要。
返回 k 。
判题标准:
系统会用下面的代码来测试你的题解:
int[] nums = [...]; // 输入数组
int[] expectedNums = [...]; // 长度正确的期望答案
int k = removeDuplicates(nums); // 调用
assert k == expectedNums.length;
for (int i = 0; i < k; i++) {
assert nums[i] == expectedNums[i];
}
如果所有断言都通过,那么您的题解将被 通过。
示例 1:
输入:nums = [1,1,2]
输出:2, nums = [1,2,_]
解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。
示例 2:
输入:nums = [0,0,1,1,1,2,2,3,3,4]
输出:5, nums = [0,1,2,3,4]
解释:函数应该返回新的长度 5 , 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4 。不需要考虑数组中超出新长度后面的元素。
提示:
1 <= nums.length <= 3 * 10^4
-10^4 <= nums[i] <= 10^4
nums 已按 非严格递增 排列
思路:

代码:
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
int k = 0;
//k维护每个第一次出现的数组元素
for(int i = 0 ; i < nums.size(); i++)
{
//如果i 不为0且当前元素跟上一个元素不一样
if(!i || nums[i] != nums[i - 1])
{
//将第k个位置替换为当前元素,并且往后移动一位
nums[k++] = nums[i];
}
}
//最后k的大小就是数组中第一次出现的所有元素个数
return k;
}
};
纯享版:
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
int k = 0;
for(int i = 0; i < nums.size(); i++)
{
if(!i || nums[i] != nums[i - 1])
{
nums[k++] = nums[i];
}
}
return k;
}
};
LeetCode27.移除元素:
题目描述:
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素。元素的顺序可能发生改变。然后返回 nums 中与 val 不同的元素的数量。
假设 nums 中不等于 val 的元素数量为 k,要通过此题,您需要执行以下操作:
更改 nums 数组,使 nums 的前 k 个元素包含不等于 val 的元素。nums 的其余元素和 nums 的大小并不重要。
返回 k。
用户评测:
评测机将使用以下代码测试您的解决方案:
int[] nums = [...]; // 输入数组
int val = ...; // 要移除的值
int[] expectedNums = [...]; // 长度正确的预期答案。
// 它以不等于 val 的值排序。
int k = removeElement(nums, val); // 调用你的实现
assert k == expectedNums.length;
sort(nums, 0, k); // 排序 nums 的前 k 个元素
for (int i = 0; i < actualLength; i++) {
assert nums[i] == expectedNums[i];
}
如果所有的断言都通过,你的解决方案将会 通过。
示例 1:
输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2,, ]
解释:你的函数函数应该返回 k = 2, 并且 nums 中的前两个元素均为 2。
你在返回的 k 个元素之外留下了什么并不重要(因此它们并不计入评测)。
示例 2:
输入:nums = [0,1,2,2,3,0,4,2], val = 2
输出:5, nums = [0,1,4,0,3,, ,_]
解释:你的函数应该返回 k = 5,并且 nums 中的前五个元素为 0,0,1,3,4。
注意这五个元素可以任意顺序返回。
你在返回的 k 个元素之外留下了什么并不重要(因此它们并不计入评测)。
提示:
0 <= nums.length <= 100
0 <= nums[i] <= 50
0 <= val <= 100
思路:
跟 LeetCode26.删除有序数组中的重复项 思路一样
代码:
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int k = 0;
for(int i = 0; i < nums.size(); i++)
{
if(nums[i] != val)
{
nums[k++] = nums[i];
}
}
return k;
}
};
LeetCode28.实现strStr():
题目描述:
给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1 。
示例 1:
输入:haystack = "sadbutsad", needle = "sad"
输出:0
解释:"sad" 在下标 0 和 6 处匹配。
第一个匹配项的下标是 0 ,所以返回 0 。
示例 2:
输入:haystack = "leetcode", needle = "leeto"
输出:-1
解释:"leeto" 没有在 "leetcode" 中出现,所以返回 -1 。
提示:
1 <= haystack.length, needle.length <= 10^4
haystack 和 needle 仅由小写英文字符组成
算法1:
思路:暴力枚举O(NM)
直接按照定义,从长串的每一个字符开始暴力匹配子串
##时间复杂度: 两重循环,O(nm)
class Solution {
public:
int strStr(string s, string p) {
int n = s.size(), m = p.size();
for(int i = 0; i < n - m + 1; i++)
{
bool st = true;
for(int j = 0; j < m; j++)
{
if(s[i + j] != p[j])
{
st = false;
break;
}
}
if(st) return i;
}
return -1;
}
};
算法2:
思路:
创建子串的next数组,当前匹配不上时,下一个可以直接开始比较的位置(前缀和后缀相等的最大长度)
KMP:
时间复杂度: O(n + m)
class Solution {
public:
int strStr(string s, string p) {
if(p.empty()) return 0;
int n = s.size(), m = p.size();
s = ' ' + s, p = ' ' + p;
vector<int> next(m + 1);
//求next数组
//其实是自身进行匹配的过程
//一直循环往复
for(int i = 2, j = 0; i <= m; i++)
{
while(j && p[i] != p[j + 1]) j = next[j];
//如果此时j的后面一位(前缀:j + 1)跟它原本相同的后缀的后面一位相同,说明此时可以扩展一位
if(p[i] == p[j + 1]) j++;
//将最长的前缀和后缀长度放入next数组
next[i] = j;
}
//KMP匹配过程
for(int i = 1, j = 0; i <= n; i++)
{
//如果不匹配了, 将j跳转到next[j]的位置
//也就是p[1~j]中前缀和后缀相同的最大长度的位置,节省前面next[j]个字符的匹配
//或者是j退无可退的情况下,从头开始匹配
while(j && s[i] != p[j + 1]) j = next[j];
//当后面的一个字符能继续匹配,j往后移动
if(s[i] == p[j + 1]) j++;
//如果能完全匹配 m个字符,那么返回第一个匹配字符的下标
//这里 返回i- m 是因为原本是 i - m + 1, 但是由于把字符串增加了一位,所以变成i -m
if(j == m) return i - m;
}
return -1;
}
};
纯享版:
class Solution {
public:
int strStr(string s, string p) {
int n = s.size(), m = p.size();
if(m == 0) return 0;
s = ' ' + s, p = ' ' + p;
vector<int> next(m + 1);
for(int i = 2, j = 0; i <= m; i++)
{
while(j && p[i] != p[j + 1]) j = next[j];
if(p[i] == p[j + 1]) j++;
next[i] = j;
}
for(int i = 1, j = 0; i <= n; i++)
{
while(j && s[i] != p[j + 1]) j = next[j];
if(s[i] == p[j + 1]) j++;
if(j == m) return i - m;
}
return -1;
}
};
LeetCode29.两数相除:
题目描述:
给你两个整数,被除数 dividend 和除数 divisor。将两数相除,要求 不使用 乘法、除法和取余运算。
整数除法应该向零截断,也就是截去(truncate)其小数部分。例如,8.345 将被截断为 8 ,-2.7335 将被截断至 -2 。
返回被除数 dividend 除以除数 divisor 得到的 商 。
注意:假设我们的环境只能存储 32 位 有符号整数,其数值范围是 [−2^31, 2^31 − 1] 。本题中,如果商 严格大于 2^31 − 1 ,则返回 2^31 − 1 ;如果商 严格小于 -2^31 ,则返回 -2^31 。
示例 1:
输入: dividend = 10, divisor = 3
输出: 3
解释: 10/3 = 3.33333... ,向零截断后得到 3 。
示例 2:
输入: dividend = 7, divisor = -3
输出: -2
解释: 7/-3 = -2.33333... ,向零截断后得到 -2 。
提示:
-2^31 <= dividend, divisor <= 2^31 - 1
divisor != 0
思路:
正常暴力做的话就是每次被除数减去除数,但是当被除数为2^31 -1, 除数为1时,需要执行231-1(大约1019)次,肯定超时,所以这里使用类似于快速幂的思想,将商转换成二进制表示,从高往低判断商的当前位是否为1,如果为1就减去当前位,继续判断,这样只要减不超过30次,就能很快确定商的值
注释代码:
class Solution {
public:
int divide(int x, int y) {
typedef long long LL;
//exp存指数项
vector<LL> exp;
//确定符号, false 表示结果是正数
bool is_minus = false;
//如果x, y符号不相同,那么,令符号标记为true
if(x < 0 && y > 0 || x > 0 && y < 0) is_minus = true;
//取 x, y 的绝对值,方便计算
LL a = abs((LL)x), b = abs((LL) y);
//将b的指数项依次存入exp中
for(LL i = b; i <= a; i = i + i) exp.push_back(i);
LL res = 0;
for(int i = exp.size() - 1; i >= 0; i--)
{
if(a >= exp[i])
{
//说明商的当前位上应该是1
a -= exp[i];
//答案加上当前商
res += 1ll << i;
}
}
//最后加上符号
if(is_minus) res = -res;
if(res > INT_MAX ||res < INT_MIN) return INT_MAX;
return res;
}
};
纯享版:
class Solution {
public:
int divide(int x, int y) {
typedef long long LL;
vector<LL> exp;
bool is_minus = false;
if(x > 0 && y < 0 || x < 0 && y > 0) is_minus = true;
LL a = abs((LL)x), b = abs((LL)y);
for(LL i = b; i <= a; i = i + i) exp.push_back(i);
LL res = 0;
for(int i = exp.size() - 1; i >= 0; i--)
{
if(a >= exp[i])
{
a -= exp[i];
res += 1ll << i;
}
}
if(is_minus) res = -res;
if(res > INT_MAX || res < INT_MIN) return INT_MAX;
return res;
}
};
LeetCode30.串联所有单词的子串:
题目描述:
给定一个字符串 s 和一个字符串数组 words。 words 中所有字符串 长度相同。
s 中的 串联子串 是指一个包含 words 中所有字符串以任意顺序排列连接起来的子串。
例如,如果 words = ["ab","cd","ef"], 那么 "abcdef", "abefcd","cdabef", "cdefab","efabcd", 和 "efcdab" 都是串联子串。 "acdbef" 不是串联子串,因为他不是任何 words 排列的连接。
返回所有串联子串在 s 中的开始索引。你可以以 任意顺序 返回答案。
示例 1:
输入:s = "barfoothefoobarman", words = ["foo","bar"]
输出:[0,9]
解释:因为 words.length == 2 同时 words[i].length == 3,连接的子字符串的长度必须为 6。
子串 "barfoo" 开始位置是 0。它是 words 中以 ["bar","foo"] 顺序排列的连接。
子串 "foobar" 开始位置是 9。它是 words 中以 ["foo","bar"] 顺序排列的连接。
输出顺序无关紧要。返回 [9,0] 也是可以的。
示例 2:
输入:s = "wordgoodgoodgoodbestword", words = ["word","good","best","word"]
输出:[]
解释:因为 words.length == 4 并且 words[i].length == 4,所以串联子串的长度必须为 16。
s 中没有子串长度为 16 并且等于 words 的任何顺序排列的连接。
所以我们返回一个空数组。
示例 3:
输入:s = "barfoofoobarthefoobarman", words = ["bar","foo","the"]
输出:[6,9,12]
解释:因为 words.length == 3 并且 words[i].length == 3,所以串联子串的长度必须为 9。
子串 "foobarthe" 开始位置是 6。它是 words 中以 ["foo","bar","the"] 顺序排列的连接。
子串 "barthefoo" 开始位置是 9。它是 words 中以 ["bar","the","foo"] 顺序排列的连接。
子串 "thefoobar" 开始位置是 12。它是 words 中以 ["the","foo","bar"] 顺序排列的连接。
提示:
1 <= s.length <= 10^4
1 <= words.length <= 5000
1 <= words[i].length <= 30
words[i] 和 s 由小写英文字母组成
思路:
这里的思路是将s字符串按照words的单词长度w进行区间划分,从0 ~ w-1为起止位置,每次划分w的长度,这样做相当于每一个槽位刚好是单个单词,可以直接将s字符串的所有单词通过巧妙的方式列举出来,然后对于words的单词匹配则使用滑动窗口进行匹配,同时使用cnt记录有效单词的个数,这里的cnt主要作用是记录在当前窗口维护的有效单词,如果有效单词个数等于words单词个数,那么说明这是一种匹配方案
注意:
这里的cnt是有效单词的个数,进入滑动窗口时会根据窗口中的次数跟tot中words单词出现的次数进行判断

注释代码:
class Solution {
public:
vector<int> findSubstring(string s, vector<string>& words) {
vector<int>res;
if(words.empty()) return res;
int n = s.size(), m = words.size(), w = words[0].size();
//单词池的每个单词出现的次数
unordered_map<string, int> tot;
//将每个单词对应出现的次数+1
for(auto&word : words) tot[word]++;
//从0 ~w-1 开始按w固定长度划分整个区间
for(int i = 0; i < w; i++)
{
//当前窗口维护的单词
unordered_map<string, int> wd;
//cnt : 确定有多少个单词是给定tot集合中的
//cnt 维护的是有效的单词,如果没有在tot集合中出现过,或者出现次数大于tot集合次数都是被排除在外的
int cnt = 0;
//将整个字符串按固定长度w划分,起始位置从0 ~ w,每次一个槽位刚好对应一个单词
for(int j = i; j + w <= n; j += w)
{
//窗口大小满足m个单词的话
if(j >= i + m * w)
{
//将最前面放入的单词找出来
auto word = s.substr(j - m * w, w);
//每次窗口移动到删除最前面放入的单词
wd[word]--;
//如果删除该单词之后,小于tot中对应出现的次数
//说明删除的是有效单词,从cnt中减去一个有效单词
//这里的wd无论出现哪个单词,对应出现的次数都是1,最坏的情况都是1
//但是,对于tot来说,如果不是有效的单词,那么它出现的次数为0
//所以这里只要删除当前单词使得wd的次数小于tot中的次数了,说明删除的是一个有效单词
if(wd[word] < tot[word]) cnt--;
}
//通过槽位找到下一个单词
auto word = s.substr(j, w);
//将该单词加入到到当前窗口
wd[word]++;
//如果该单词加入之后,满足tot中应该出现的次数,说明是有效单词
if(wd[word] <= tot[word]) cnt++;
//通过这种方式,每次将有效单词计入cnt,当cnt == words中的单词数量,说明能够匹配到相同的字符串
//将当前的起始位置存入答案
if(cnt == m) res.push_back(j - (m -1) * w);
}
}
return res;
}
};
纯享版:
class Solution {
public:
vector<int> findSubstring(string s, vector<string>& words) {
vector<int> res;
if(words.empty()) return res;
int n = s.size(), m = words.size(), w = words[0].size();
unordered_map<string, int> tot;
for(auto word : words) tot[word]++;
for(int i = 0; i < w; i++)
{
unordered_map<string, int> wd;
int cnt = 0;
for(int j = i; j + w <= n; j += w)
{
if(j >= i + m * w)
{
auto word = s.substr(j - m * w,w);
wd[word]--;
if(wd[word] < tot[word]) cnt--;
}
auto word = s.substr(j, w);
wd[word]++;
if(wd[word] <= tot[word]) cnt++;
if(cnt == m) res.push_back(j - (m - 1) * w);
}
}
return res;
}
};