Rick的刷题之路
哈希表
两数之和
题目描述
给定一个整数数组 nums
和一个目标值 target
,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。
示例:
给定 nums = [2, 7, 11, 15], target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]
题目解析
使用查找表来解决该问题。
设置一个 map 容器 record 用来记录元素的值与索引,然后遍历数组 nums。
- 每次遍历时使用临时变量 complement 用来保存目标值与当前值的差值
- 在此次遍历中查找 record ,查看是否有与 complement 一致的值,如果查找成功则返回查找值的索引值与当前变量的值 i
- 如果未找到,则在 record 保存索引值 i与该元素
题目代码
// 时间复杂度:O(n)
// 空间复杂度:O(n)
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> record;
for (int i = 0; i < nums.size(); i++)
{
int complement = target - nums[i];
if (record.find(complement) != record.end())
{
int res[] = { i,record[complement] };
return vector<int>(res, res + 2);
}
record[nums[i]] = i;
}
return {};
}
};
罗马数字转整数
题目描述
罗马数字包含以下七种字符: 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。
给定一个罗马数字,将其转换成整数。
题目解析
题目解析:
这个问题是关于将罗马数字转换为整数的。罗马数字是一种古老的数字表示方法,它使用字母来表示数值。在这个问题中,我们需要编写一个函数,该函数接受一个罗马数字字符串作为输入,并返回相应的整数值。
罗马数字的表示规则如下:
- 每个字母代表一个特定的数值。
- 如果一个较小的数值在较大的数值的左边,那么它表示从较大的数值中减去这个较小的数值。例如,”IV” 表示 4,因为 “I” 代表 1,”V” 代表 5,所以 “IV” 是 5 – 1 = 4。
- 如果一个较小的数值在较大的数值的右边,那么它表示加到较大的数值上。例如,”VI” 表示 6,因为 “V” 代表 5,”I” 代表 1,所以 “VI” 是 5 + 1 = 6。
为了解决这个问题,我们可以使用查找表(在本例中是一个哈希表或字典)来存储每个罗马数字字母对应的整数值。然后,我们遍历输入的罗马数字字符串,并根据上述规则计算整数值。
在提供的代码中,我们创建了一个名为 symbolValues
的哈希表,其中键是罗马数字字母,值是相应的整数值。然后,在 romanToInt
函数中,我们遍历输入的字符串 s
,并使用 symbolValues
来查找每个字符对应的整数值。
在遍历过程中,我们检查当前字符的整数值是否小于下一个字符的整数值。如果是这样,我们将当前字符的整数值从总和中减去;否则,我们将其添加到总和中。这样,我们就能够正确处理罗马数字中的减法情况。
最后,函数返回计算得到的总和,即输入罗马数字字符串对应的整数值。
示例 1:
输入: s = "III"
输出: 3
示例 2:
输入: s = "IV"
输出: 4
题目代码
#include <iostream>
#include <unordered_map>
#include <string>
using namespace std;
class Solution {
private:
unordered_map<char, int> symbolValues = {
{'I', 1},
{'V', 5},
{'X', 10},
{'L', 50},
{'C', 100},
{'D', 500},
{'M', 1000},
};
public:
int romanToInt(string s) {
int length = s.length();
int ans = 0;
for (int i = 0; i < length; i++)
{
int value = symbolValues[s[i]];
if (i < length - 1 && value < symbolValues[s[i + 1]])
ans -= value;
else
ans += value;
}
return ans;
}
};
多数元素
题目描述
给定一个大小为 n
的数组 nums
,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋
的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
题目解析
首先,代码定义了一个 Solution
类,并在其中实现了 majorityElement
函数。这个函数接受一个整数向量 nums
,并返回向量中的众数。
函数内部定义了一个 unordered_map<int, int>
类型的变量 counts
,用于存储每个元素及其出现的次数。unordered_map
是一个哈希表实现,它允许我们以常数平均时间复杂度进行插入、删除和查找操作。
然后,函数定义了两个整数变量 majority
和 cnt
,分别用于存储当前的众数和众数的计数。初始时,这两个变量都被设置为0。
接下来,函数遍历输入向量 nums
中的每个元素。对于每个元素,它首先将其在 counts
中的计数加1。然后,它检查该元素的计数是否大于 cnt
(即当前众数的计数)。如果是,那么它更新 majority
和 cnt
,将当前元素设置为新的众数,并将其计数设置为当前元素的计数。
最后,函数返回 majority
,即向量中的众数。
示例 1:
输入:nums = [3,2,3]
输出:3
示例 2:
输入:nums = [2,2,1,1,1,2,2]
输出:2
题目代码
#include <iostream>
#include <vector>
#include <unordered_map>
using namespace std;
class Solution {
public:
int majorityElement(vector<int>& nums) {
unordered_map<int, int> counts;
int majority = 0, cnt = 0; //majority 用于存储当前的众数,cnt 用于存储众数当前的计数。
for (int num : nums) {
++counts[num];
if (counts[num] > cnt) {
majority = num;
cnt = counts[num];
}
}
return majority;
}
};
存在重复元素
题目描述
给你一个整数数组 nums
。如果任一值在数组中出现 至少两次 ,返回 true
;如果数组中每个元素互不相同,返回 false
。
题目解析
unordered_map
是一个键值对的集合,它存储的是键和与键关联的值。在这种情况下,我们并不关心与键关联的值,我们只需要检查键是否存在。unordered_set
是一个只包含键的集合,它不允许有重复的元素。这完全符合我们的需求:我们只需要检查一个元素是否已经在集合中出现过。
示例 1:
输入:nums = [1,2,3,1]
输出:true
示例 2:
输入:nums = [1,2,3,4]
输出:false
示例 3:
输入:nums = [1,1,1,3,3,4,3,2,4,2]
输出:true
题目代码
#include <iostream>
#include <vector>
#include <unordered_set>
using namespace std;
class Solution {
public:
bool containsDuplicate(vector<int>& nums) {
unordered_set<int> s;
for (int x : nums) {
if (s.find(x) != s.end()) {
return true;
}
s.insert(x);
}
return false;
}
};
存在重复元素Ⅱ
题目描述
给你一个整数数组 nums
和一个整数 k
,判断数组中是否存在两个 不同的索引 i
和 j
,满足 nums[i] == nums[j]
且 abs(i - j) <= k
。如果存在,返回 true
;否则,返回 false
。
题目解析
滑动窗口 + 哈希表
整理题意:是否存在长度不超过的 k+1
窗口,窗口内有相同元素。
我们可以从前往后遍历 nums
,同时使用 Set 记录遍历当前滑窗内出现过的元素。
假设当前遍历的元素为 nums[i]
下标小于等于 k(起始滑窗长度还不足 k+1
):直接往滑窗加数,即将当前元素加入 Set 中;
下标大于 k
:将上一滑窗的左端点元素 nums[i−k−1]
移除,判断当前滑窗的右端点元素 nums[i]
是否存在 Set 中,若存在,返回 True,否则将当前元素 nums[i]
加入 Set 中。
重复上述过程,若整个nums
处理完后仍未找到,返回 False。
题目代码
#include <iostream>
#include <vector>
#include <unordered_map>
using namespace std;
class Solution {
public:
bool containsNearbyDuplicate(vector<int>& nums, int k) {
int num_size = nums.size();
unordered_map<int,bool> set;
for (int i = 0; i < num_size; i++)
{
if (i > k) set[nums[i - k -1]] = false;
if (set[nums[i]]) return true;
set[nums[i]] = true;
}
return false;
}
};
示例 1:
输入:nums = [1,2,3,1], k = 3
输出:true
示例 2:
输入:nums = [1,0,1,1], k = 1
输出:true
示例 3:
输入:nums = [1,2,3,1,2,3], k = 2
输出:false
有效的字母异位词
题目描述
给定两个字符串 *s*
和 *t*
,编写一个函数来判断 *t*
是否是 *s*
的字母异位词。
注意:若 *s*
和 *t*
中每个字符出现的次数都相同,则称 *s*
和 *t*
互为字母异位词。
题目解析
解题思路:
- 首先,检查两个字符串的长度是否相等。如果长度不等,那么它们肯定不是变位词,直接返回false。
- 接下来,使用一个无序映射(unordered_map)
dic
来记录字符串s中每个字符出现的次数。遍历字符串s,对于每个字符c,将其在dic
中对应的值加1。 - 然后,遍历字符串t。对于每个字符c,将其在
dic
中对应的值减1。这一步相当于从s的字符集合中移除t中的字符。 - 最后,检查
dic
中的所有值是否都为0。如果有任何一个值不为0,说明s和t的字符集合不同,因此它们不是变位词,返回false。如果所有值都为0,说明s和t的字符集合完全相同,只是顺序不同,它们是变位词,返回true。
题目代码
class Solution {
public:
bool isAnagram(string s, string t) {
if (s.length() != t.length())
return false;
unordered_map<char, int> dic;
for (char c : s) {
dic[c] += 1;
}
for (char c : t) {
dic[c] -= 1;
}
for (auto kv : dic) {
if (kv.second != 0)
return false;
}
return true;
}
};
示例 1:
输入: s = "anagram", t = "nagaram"
输出: true
示例 2:
输入: s = "rat", t = "car"
输出: false
丢失的数字
题目描述
给定一个包含 [0, n]
中 n
个数的数组 nums
,找出 [0, n]
这个范围内没有出现在数组中的那个数。
题目解析
使用哈希集合,可以将时间复杂度降低到 O(n)
首先遍历数组 nums
,将数组中的每个元素加入哈希集合,然后依次检查从 0
到 n
的每个整数是否在哈希集合中,不在哈希集合中的数字即为丢失的数字。由于哈希集合的每次添加元素和查找元素的时间复杂度都是 O(1)
,因此总时间复杂度是 O(n)
。
题目代码
class Solution {
public:
int missingNumber(vector<int>& nums) {
unordered_set<int> findmissing;
int numsize = nums.size();
for (auto& i : nums)
{
findmissing.insert(i);
}
int missing = -1;
for (int i = 0; i < numsize; i++)
{
if (!findmissing.count(i))
{
missing = i;
break;
}
}
if (missing == -1)
return numsize;
return missing;
}
};
示例 1:
输入:nums = [3,0,1]
输出:2
解释:n = 3,因为有 3 个数字,所以所有的数字都在范围 [0,3] 内。2 是丢失的数字,因为它没有出现在 nums 中。
示例 2:
输入:nums = [0,1]
输出:2
解释:n = 2,因为有 2 个数字,所以所有的数字都在范围 [0,2] 内。2 是丢失的数字,因为它没有出现在 nums 中。
示例 3:
输入:nums = [9,6,4,2,3,5,7,0,1]
输出:8
解释:n = 9,因为有 9 个数字,所以所有的数字都在范围 [0,9] 内。8 是丢失的数字,因为它没有出现在 nums 中。
同构字符串
题目描述
给定两个字符串 s
和 t
,判断它们是否是同构的。
如果 s
中的字符可以按某种映射关系替换得到 t
,那么这两个字符串是同构的。
每个出现的字符都应当映射到另一个字符,同时不改变字符的顺序。不同字符不能映射到同一个字符上,相同字符只能映射到同一个字符上,字符可以映射到自己本身。
题目解析
我们维护两张哈希表,第一张哈希表 s2t
以 s
中字符为键,映射至 t
的字符为值,第二张哈希表 t2s
以 t
中字符为键,映射至 s
的字符为值。从左至右遍历两个字符串的字符,不断更新两张哈希表,如果出现冲突(即当前下标 index
对应的字符 s[index]
已经存在映射且不为 t[index]
或当前下标 index
对应的字符 t[index]
已经存在映射且不为 s[index]
时说明两个字符串无法构成同构,返回 false
。
如果遍历结束没有出现冲突,则表明两个字符串是同构的,返回 true
即可。
题目代码
#include <string>
#include <iostream>
#include <unordered_map>
using namespace std;
class Solution {
public:
bool isIsomorphic(string s, string t) {
unordered_map<char, char> s2t;
unordered_map<char, char> t2s;
int length = s.length();
for (int i = 0; i < length; i++)
{
char x = s[i];
char y = t[i];
while ((s2t.count(x) && s2t[x] != y) || (t2s.count(y) && t2s[y] != x))
{
return false;
}
s2t[x] = y;
t2s[y] = x;
}
return true;
}
};
示例 1:
输入:s = "egg", t = "add"
输出:true
示例 2:
输入:s = "foo", t = "bar"
输出:false
示例 3:
输入:s = "paper", t = "title"
输出:true
赎金信
题目描述
给你两个字符串:ransomNote
和 magazine
,判断 ransomNote
能不能由 magazine
里面的字符构成。
如果可以,返回 true
;否则返回 false
。
magazine
中的每个字符只能在 ransomNote
中使用一次。
题目分析
可以考虑优化的地方,第一个将字符转换为数字,第二个是逆向判断,不必去统计ransomNote的字符出现数量,而是减去magazine中的字符出现次数,这样子可以节省空间
题目代码
#include <vector>
#include <unordered_map>
#include <string>
using namespace std;
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
unordered_map<char, int> rans;
unordered_map<char,int> mag;
for (int i = 0; i < magazine.size(); i++)
{
mag[magazine[i]]++;
}
for (int i = 0; i < ransomNote.size(); i++)
{
if (++rans[ransomNote[i]] > mag[ransomNote[i]])
{
return false;
}
}
return true;
}
};
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
if (ransomNote.size() > magazine.size()) {
return false;
}
vector<int> cnt(26);
for (auto& c : magazine) {
cnt[c - 'a']++;
}
for (auto& c : ransomNote) {
cnt[c - 'a']--;
if (cnt[c - 'a'] < 0) {
return false;
}
}
return true;
}
};
示例 1:
输入:ransomNote = "a", magazine = "b"
输出:false
示例 2:
输入:ransomNote = "aa", magazine = "ab"
输出:false
示例 3:
输入:ransomNote = "aa", magazine = "aab"
输出:true
单词规律
题目描述
给定一种规律 pattern
和一个字符串 s
,判断 s
是否遵循相同的规律。
这里的 遵循 指完全匹配,例如, pattern
里的每个字母和字符串 s
中的每个非空单词之间存在着双向连接的对应规律。
题目分析
见代码中注释
题目代码
#include <iostream>
#include <string>
#include <unordered_map>
#include <sstream>
using namespace std;
class Solution {
public:
bool wordPattern(string pattern, string s) {
s.append(1, ' ');
unordered_map<char, string> word_pattern;
unordered_map<string, char> word_pattern1;
int index = 0;
int num = 0;
string word;
//abc hh ab
int i = 0;
for (auto& ch : s)
{
if (ch == ' ')
{
word = s.substr(index, num);
cout << "index现在是 " << index;
index = index + num + 1; //这里的 +1 就是要把空格的长度也算上
cout << " 变换为后是 " << index << endl;
num = -1; //当出现第一个空白后,与下面的num++抵消 也可以使用continue.
cout << word_pattern.count(pattern[i]) << "相对于的word = " << word << endl;
if ( ((word_pattern.count(pattern[i])) && (word_pattern[pattern[i]] != word)) || ( word_pattern1.count(word) && word_pattern1[word] != pattern[i]))
{
//cout << s << "a" << endl;
return false;
}
word_pattern[pattern[i]] = word;
word_pattern1[word] = pattern[i];
cout << "i的值是 i=" << i << endl;
i++;
}
num++;
}
if (i != pattern.size())
{
return false;
}
return true;
}
};
class Solution2 {
public:
bool wordPattern(string pattern, string str) {
unordered_map<char, string> map;
unordered_map<string, char> rmap;
stringstream ss(str); string s;
for (char c : pattern) {
//!(ss >> s) 这行语句判断是否存在单词的长度比字符串长
//当 ss 中没有更多的单词时,ss >> s 操作会失败,
//但循环仍然会继续尝试处理 pattern 中的下一个字符 "g"。然而,由于 s 仍然是之前最后一个提取的单词 "dog"
//这会导致rmap 的检查失败
if (!(ss >> s) || (map.count(c) == 1 && map[c] != s) || (rmap.count(s) == 1 && rmap[s] != c)) return false;
map[c] = s; rmap[s] = c;
}
//下面这行代码判断是否字符串比单词长
//因为这个方法是遍历单词,所以最后要判断是否字符串比单词长
return (ss >> s) ? false : true;
}
};
int main()
{
string pattern = "abba", s = "dog cat cat dog";
Solution sol;
if (sol.wordPattern(pattern, s))
{
cout << "true" << endl;
}
else
{
cout << "false" << endl;
}
}
示例1:
输入: pattern = "abba", s = "dog cat cat dog"
输出: true
示例 2:
输入:pattern = "abba", s = "dog cat cat fish"
输出: false
示例 3:
输入: pattern = "aaaa", s = "dog cat cat dog"
输出: false
字母异位词分组
题目描述
给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。
字母异位词 是由重新排列源单词的所有字母得到的一个新单词。
题目分析
- 初始化自定义哈希函数:创建一个可以哈希
array<int, 26>
类型的函数arrayHash
。 - 创建哈希表:创建一个
unordered_map
,键是array<int, 26>
,值是vector<string>
。 - 遍历字符串数组:对于每个字符串,计算其字母计数。
- 对于 “eat”:计数数组为
[1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
,表示 ‘a’ 出现了1次,‘e’ 出现了1次,‘t’ 出现了1次。 - 对于 “tea”:计数数组与 “eat” 相同。
- 对于 “tan”:计数数组为
[1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
,与 “eat” 和 “tea” 相同。 - 对于 “ate”:计数数组与 “eat”、“tea” 和 “tan” 相同。
- 对于 “nat”:计数数组为
[1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0]
。 - 对于 “bat”:计数数组为
[1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]
。
- 对于 “eat”:计数数组为
- 填充哈希表:
- 对于 “eat”、“tea”、“ate”:它们共享相同的计数数组,所以它们会被添加到同一个
vector<string>
中。 - 对于 “tan”、“nat”:它们也共享相同的计数数组,所以它们会被添加到同一个
vector<string>
中。 - 对于 “bat”:它有一个独特的计数数组,所以它会被添加到一个新的
vector<string>
中。
- 对于 “eat”、“tea”、“ate”:它们共享相同的计数数组,所以它们会被添加到同一个
- 构建结果数组:遍历哈希表,将每个
vector<string>
添加到结果数组ans
中。
[&](size_t acc, int num) { return (acc << 1) ^ fn(num); }
- 捕获列表:这里使用了
&
,表示以引用的方式捕获外部作用域中的所有变量。在这个例子中,捕获列表为空,因为Lambda表达式没有使用外部作用域中的任何变量。 - 参数列表:
size_t acc, int num
。这个Lambda表达式接受两个参数:acc
是一个size_t
类型的累积值,num
是当前要处理的数组元素,是一个int
类型。 - 返回类型:这里省略了返回类型,因为编译器可以自动推导出返回类型是
size_t
。 - 函数体:
return (acc << 1) ^ fn(num);
。函数体定义了如何将当前元素num
的哈希值累积到累积值acc
上。首先,将acc
左移一位,然后将num
通过fn
(即hash<int>
)进行哈希处理,最后将这两个结果进行异或运算,得到新的累积值并返回。
这里是arrayHash
函数的详细解释:
[fn = hash<int>{}]
:这是一个捕获列表,它捕获了一个默认构造的hash<int>
对象。hash<int>
是C++标准库中的一个模板类,它提供了对int
类型的默认哈希函数。捕获这个对象是为了在Lambda表达式中使用它来哈希数组中的每个整数。(const array<int, 26>& arr) -> size_t
:这是Lambda表达式的参数列表和返回类型。它接受一个常量引用的array<int, 26>
对象,并返回一个size_t
类型的哈希值。accumulate(arr.begin(), arr.end(), 0u, [&](size_t acc, int num) { ... })
:这是C++标准库中的accumulate
函数,它用于对数组中的所有元素进行累积操作。accumulate
函数的参数包括迭代器的起始和结束位置、初始累积值(在这里是0u
,表示一个无符号的0),以及一个二元Lambda表达式,用于定义如何累积每个元素。return (acc << 1) ^ fn(num);
:在accumulate
的Lambda表达式中,每个元素的哈希值是通过将累积值acc
左移一位(<< 1
)然后与当前元素的哈希值(通过调用fn(num)
得到)进行异或运算(^
)来计算的。这种操作将每个元素的哈希值结合到累积值中,生成一个最终的哈希值。
这个自定义哈希函数是为了将array<int, 26>
类型的键转换为size_t
类型的哈希值,这样就可以在unordered_map
中使用这种类型的键了。
题目代码
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
// 自定义对 array<int, 26> 类型的哈希函数
auto arrayHash = [fn = hash<int>{}](const array<int, 26>& arr) -> size_t {
return accumulate(arr.begin(), arr.end(), 0u, [&](size_t acc, int num) {
return (acc << 1) ^ fn(num);
});
};
unordered_map<array<int, 26>, vector<string>, decltype(arrayHash)> mp(0, arrayHash);
for (string& str : strs) {
array<int, 26> counts{};
int length = str.length();
for (int i = 0; i < length; ++i) {
counts[str[i] - 'a']++;
}
mp[counts].emplace_back(str);
}
vector<vector<string>> ans;
for (auto it = mp.begin(); it != mp.end(); ++it) {
ans.emplace_back(it->second);
}
return ans;
}
};
//将字符串排序后加入哈希表中
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
unordered_map<string, vector<string>> m;
for (auto& s : strs) {
string sorted_s = s;
ranges::sort(sorted_s);
m[sorted_s].push_back(s); // sorted_s 相同的字符串分到同一组
}
vector<vector<string>> ans;
ans.reserve(m.size()); // 预分配空间
for (auto& [_, value] : m) {
ans.push_back(value);
}
return ans;
}
};
示例 1:
输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
输出: [["bat"],["nat","tan"],["ate","eat","tea"]]
示例 2:
输入: strs = [""]
输出: [[""]]
示例 3:
输入: strs = ["a"]
输出: [["a"]]
最长连续序列
题目描述
给定一个未排序的整数数组 nums
,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。
请你设计并实现时间复杂度为 O(n)
的算法解决此问题。
题目分析
我的解法是找到一个数,然后向两边寻找,找到以后直接删除
官方的解法是首先判断一个数的前一位是否存在,然后进入循环,开始寻找当前数的后一位是否存在
题目代码
#include <iostream>
#include <vector>
#include <unordered_set>
using namespace std;
class Solution {
public:
int longestConsecutive(vector<int>& nums) {
if (nums.size() == 0)
{
return 0;
}
//unordered_set<int> store;
unordered_set<int> store(nums.begin(), nums.end());
/*for (const int& num : nums) {
store.insert(num);
}*/
int maxlength = 0;
for (int num : nums)
{
if (store.find(num) != store.end())
{
store.erase(num);
int length = 1;
int left = num - 1;
int right = num + 1;
while (store.find(left) != store.end())
{
store.erase(left);
left--;
length++;
}
while (store.find(right) != store.end())
{
store.erase(right);
right++;
length++;
}
maxlength = max(maxlength, length);
}
}
return maxlength;
}
};
class Solution {
public:
int longestConsecutive(vector<int>& nums) {
unordered_set<int> num_set;
for (const int& num : nums) {
num_set.insert(num);
}
int longestStreak = 0;
for (const int& num : num_set) {
if (!num_set.count(num - 1)) { //防止重复,直接寻找开头的元素
int currentNum = num;
int currentStreak = 1;
while (num_set.count(currentNum + 1)) {
currentNum += 1;
currentStreak += 1;
}
longestStreak = max(longestStreak, currentStreak);
}
}
return longestStreak;
}
};
示例 1:
输入:nums = [100,4,200,1,3,2]
输出:4
解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。
示例 2:
输入:nums = [0,3,7,2,5,8,4,6,0,1]
输出:9
随机链表的复制
题目描述
给你一个长度为 n
的链表,每个节点包含一个额外增加的随机指针 random
,该指针可以指向链表中的任何节点或空节点。
构造这个链表的 深拷贝。 深拷贝应该正好由 n
个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next
指针和 random
指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。
例如,如果原链表中有 X
和 Y
两个节点,其中 X.random --> Y
。那么在复制链表中对应的两个节点 x
和 y
,同样有 x.random --> y
。
返回复制链表的头节点。
用一个由 n
个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index]
表示:
val
:一个表示Node.val
的整数。random_index
:随机指针指向的节点索引(范围从0
到n-1
);如果不指向任何节点,则为null
。
你的代码 只 接受原链表的头节点 head
作为传入参数。
题目分析
这里的哈希表秀到我了,代码直观简洁.
题目代码
#include <iostream>
#include <vector>
#include <unordered_map>
using namespace std;
class Node {
public:
int val;
Node* next;
Node* random;
Node(int _val) {
val = _val;
next = NULL;
random = NULL;
}
};
class Solution {
public:
Node* copyRandomList(Node* head) {
if (head == nullptr) return nullptr;
// 创建新节点,同时建立原始节点到复制节点的映射
unordered_map<Node*, Node*> map;
Node* current = head;
while (current != nullptr) {
map[current] = new Node(current->val);
current = current->next;
}
// 设置复制节点的next和random指针
current = head;
while (current != nullptr) {
map[current]->next = map[current->next];
map[current]->random = map[current->random];
current = current->next;
}
return map[head];
}
};
示例 1:
输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]
示例 2:
输入:head = [[1,1],[2,1]]
输出:[[1,1],[2,1]]
示例 3:
输入:head = [[3,null],[3,0],[3,null]]
输出:[[3,null],[3,0],[3,null]]
缺失的第一个正数
题目描述
给你一个未排序的整数数组 nums
,请你找出其中没有出现的最小的正整数。
请你实现时间复杂度为 O(n)
并且只使用常数级别额外空间的解决方案。
题目分析
题目代码
#include <vector>
using namespace std;
class Solution {
public:
int firstMissingPositive(vector<int>& nums) {
int n = nums.size();
for (auto& x : nums)
{
if (x <= 0)
{
x = n + 1;
}
}
for (int i = 0; i < n; i++)
{
int num = abs(nums[i]);
if (num <= n)
{
nums[num - 1] = -abs(nums[num - 1]);
}
}
for (int i = 0; i < n; i++)
{
if (nums[i] > 0)
{
return i + 1;
}
}
return n + 1;
}
};
示例 1:
输入:nums = [1,2,0]
输出:3
解释:范围 [1,2] 中的数字都在数组中。
示例 2:
输入:nums = [3,4,-1,1]
输出:2
解释:1 在数组中,但 2 没有。
示例 3:
输入:nums = [7,8,9,11,12]
输出:1
解释:最小的正数 1 没有出现。
提示:
1 <= nums.length <= 105
-231 <= nums[i] <= 231 - 1
链表
快乐数
题目描述
编写一个算法来判断一个数 n
是不是快乐数。
「快乐数」 定义为:
- 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
- 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
- 如果这个过程 结果为 1,那么这个数就是快乐数。
如果 n
是 快乐数 就返回 true
;不是,则返回 false
。
题目解析:
方法:
使用 “快慢指针” 思想,找出循环:“快指针” 每次走两步,“慢指针” 每次走一步,当二者相等时,即为一个循环周期。此时,判断是不是因为 1 引起的循环,是的话就是快乐数,否则不是快乐数。
注意:此题不建议用集合记录每次的计算结果来判断是否进入循环,因为这个集合可能大到无法存储;另外,也不建议使用递归,同理,如果递归层次较深,会直接导致调用栈崩溃。不要因为这个题目给出的整数是 int 型而投机取巧。
示例 1:
输入:n = 19
输出:true
解释:
12 + 92 = 82
82 + 22 = 68
62 + 82 = 100
12 + 02 + 02 = 1
示例 2:
输入:n = 2
输出:false
题目代码
#include<iostream>
using namespace std;
int getSum(int number)
{
int sum = 0;
while (number > 0)
{
int perNumber = number % 10;
sum += perNumber * perNumber;
number = number / 10;
}
return sum;
}
class Solution {
public:
bool isHappy(int n) {
int slow = n, fast = n;
do {
slow = getSum(slow);
fast = getSum(fast);
fast = getSum(fast);
} while (slow != fast);
return slow == 1;
}
};
合并两个有序数组
题目描述
给你两个按 非递减顺序 排列的整数数组 nums1
和 nums2
,另有两个整数 m
和 n
,分别表示 nums1
和 nums2
中的元素数目。
请你 合并 nums2
到 nums1
中,使合并后的数组同样按 非递减顺序 排列。
注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1
中。为了应对这种情况,nums1
的初始长度为 m + n
,其中前 m
个元素表示应合并的元素,后 n
个元素为 0
,应忽略。nums2
的长度为 n
。
题目解析
方法二中,之所以要使用临时变量,是因为如果直接合并到数组 nums1
中,nums1
中的元素可能会在取出之前被覆盖。那么如何直接避免覆盖 nums1
中的元素呢?观察可知,nums1
的后半部分是空的,可以直接覆盖而不会影响结果。因此可以指针设置为从后向前遍历,每次取两者之中的较大者放进 nums1
的最后面。
严格来说,在此遍历过程中的任意一个时刻,nums1
数组中有m-p1-1
个元素被放入nums1
的后半部,nums2
数组中有n-p2-1
个元素被放入nums1
的后半部,而在指针p1
的后面,nums1
数组有m+n-p1-1
个位置
示例 1:
输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
输出:[1,2,2,3,5,6]
解释:需要合并 [1,2,3] 和 [2,5,6] 。
合并结果是 [1,2,2,3,5,6] ,其中斜体加粗标注的为 nums1 中的元素。
示例 2:
输入:nums1 = [1], m = 1, nums2 = [], n = 0
输出:[1]
解释:需要合并 [1] 和 [] 。
合并结果是 [1] 。
示例 3:
输入:nums1 = [0], m = 0, nums2 = [1], n = 1
输出:[1]
解释:需要合并的数组是 [] 和 [1] 。
合并结果是 [1] 。
注意,因为 m = 0 ,所以 nums1 中没有元素。nums1 中仅存的 0 仅仅是为了确保合并结果可以顺利存放到 nums1 中。
题目代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
//正向双链表
class Solution {
public:
void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
int p1 = 0, p2 = 0;
int sorted[m + n];
int cur;
while (p1 < m || p2 < n) {
if (p1 == m) {
cur = nums2[p2++];
}
else if (p2 == n) {
cur = nums1[p1++];
}
else if (nums1[p1] < nums2[p2]) {
cur = nums1[p1++];
}
else {
cur = nums2[p2++];
}
sorted[p1 + p2 - 1] = cur;
}
for (int i = 0; i != m + n; ++i) {
nums1[i] = sorted[i];
}
}
};
//逆向双链表
class Solution {
public:
void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
int p1 = m - 1, p2 = n - 1;
int tail = m + n - 1;
int cur;
while (p1 >= 0 || p2 >= 0) {
if (p1 == -1) {
cur = nums2[p2--];
}
else if (p2 == -1) {
cur = nums1[p1--];
}
else if (nums1[p1] > nums2[p2]) {
cur = nums1[p1--];
}
else {
cur = nums2[p2--];
}
nums1[tail--] = cur;
}
}
};
移除元素
题目描述
给你一个数组 nums
和一个值 val
,你需要原地移除所有数值等于 val
的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1)
额外空间并原地修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
题目解析
方法一:
由于题目要求删除数组中等于 val
的元素,因此输出数组的长度一定小于等于输入数组的长度,我们可以把输出的数组直接写在输入数组上。可以使用双指针:右指针 right
指向当前将要处理的元素,左指针 left
指向下一个将要赋值的位置。
如果右指针指向的元素不等于 val
,它一定是输出数组的一个元素,我们就将右指针指向的元素复制到左指针位置,然后将左右指针同时右移;
如果右指针指向的元素等于 val
,它不能在输出数组里,此时左指针不动,右指针右移一位。
整个过程保持不变的性质是:区间 [0,left)
中的元素都不等于 val
。当左右指针遍历完输入数组以后,left
的值就是输出数组的长度。
这样的算法在最坏情况下(输入数组中没有元素等于 val
),左右指针各遍历了数组一次。
方法二:
如果要移除的元素恰好在数组的开头,例如序列[1,2,3,4,5]
,当 val
为 1 时,我们需要把每一个元素都左移一位。注意到题目中说:「元素的顺序可以改变」。实际上我们可以直接将最后一个元素 5
移动到序列开头,取代元素 1
,得到序列[5,2,3,4]
,同样满足题目要求。这个优化在序列中 val
元素的数量较少时非常有效。
实现方面,我们依然使用双指针,两个指针初始时分别位于数组的首尾,向中间移动遍历该序列。
算法
如果左指针 left
指向的元素等于 val
,此时将右指针 right
指向的元素复制到左指针 left
的位置,然后右指针 right
左移一位。如果赋值过来的元素恰好也等于 val
,可以继续把右指针 right
指向的元素的值赋值过来(左指针 left
指向的等于 val
的元素的位置继续被覆盖),直到左指针指向的元素的值不等于 val
为止。
当左指针 left
和右指针 right
重合的时候,左右指针遍历完数组中所有的元素。
这样的方法两个指针在最坏的情况下合起来只遍历了数组一次。与方法一不同的是,方法二避免了需要保留的元素的重复赋值操作。
题目代码
#include <iostream>
#include <vector>
using namespace std;
//方法一
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int n = nums.size();
int left = 0;
for (int right = 0; right < n; right++)
{
if (nums[right] != val)
{
nums[left] = nums[right];
left++;
}
}
return left;
}
};
//方法二
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int right = nums.size();
int left = 0;
while (left < right)
{
if (nums[left] == val)
{
nums[left] = nums[right-1];
right--;
}
else
{
left++;
}
}
return left;
}
};
示例 1:
输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2]
解释:函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。
示例 2:
输入:nums = [0,1,2,2,3,0,4,2], val = 2
输出:5, nums = [0,1,3,0,4]
解释:函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素。
删除有序数组中的重复项
题目描述
给你一个 非严格递增排列 的数组 nums
,请你原地删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums
中唯一元素的个数。
考虑 nums
的唯一元素的数量为 k
,你需要做以下事情确保你的题解可以被通过:
- 更改数组
nums
,使nums
的前k
个元素包含唯一元素,并按照它们最初在nums
中出现的顺序排列。nums
的其余元素与nums
的大小不重要。 - 返回
k
。
题目解析
题目使用快慢指针进行判断,由快指针进行元素是否相同的判断,慢指针当元素不同时自增,快指针无论什么情况下都要自增
题目代码
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
int slow = 1, fast = 1;
int n = nums.size();
while (fast < n)
{
if (nums[fast-1] != nums[fast] )
{
nums[slow] = nums[fast];
slow++;
}
fast++;
}
return slow;
}
};
示例 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 。不需要考虑数组中超出新长度后面的元素。
删除有序数组中的重复项Ⅱ
题目描述
给你一个有序数组 nums
,请你原地删除重复出现的元素,使得出现次数超过两次的元素只出现两次 ,返回删除后数组的新长度。
不要使用额外的数组空间,你必须在原地并在使用 O(1) 额外空间的条件下完成。
题目分析
和刚刚那道题目差不多,发现有这么几个关键点,第一个就是一开始快慢指针选取的位置,都选择在需要判断的位置,然后第二点,为什么这次使用[slow-2]
而不是[fast-2]
,答案在于,上面那道题不可以存在两个相同的元素,所以上面写成[slow-1]
替换[fast-1]
也是可以的,所以上面那道题是特殊情况,这道题就不可以使用[fast-2] != [fast]
题目代码
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
int n = nums.size();
if (n <= 2) {
return n;
}
int slow = 2, fast = 2;
while (fast < n) {
if (nums[slow - 2] != nums[fast]) {
nums[slow] = nums[fast];
++slow;
}
++fast;
}
return slow;
}
};
示例 1:
输入:nums = [1,1,1,2,2,3]
输出:5, nums = [1,1,2,2,3]
解释:函数应返回新长度 length = 5, 并且原数组的前五个元素被修改为 1, 1, 2, 2, 3。 不需要考虑数组中超出新长度后面的元素。
示例 2:
输入:nums = [0,0,1,1,1,1,2,3,3]
输出:7, nums = [0,0,1,1,2,3,3]
解释:函数应返回新长度 length = 7, 并且原数组的前七个元素被修改为 0, 0, 1, 1, 2, 3, 3。不需要考虑数组中超出新长度后面的元素。
接雨水
题目描述
给定 n
个非负整数表示每个宽度为 1
的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
题目分析
使用双指针 left
和 right
分别从柱状图的左右两侧开始,同时跟踪左侧的最大高度 leftMax
和右侧的最大高度 rightMax
。
在每一步中,执行以下操作:
- 更新
leftMax
为leftMax
和height[left]
中的较大值。 - 更新
rightMax
为rightMax
和height[right]
中的较大值。 - 判断哪一侧的柱子较低:
- 如果
height[left] < height[right]
,则左侧柱子能接的雨水量为leftMax - height[left]
,将结果加到总雨水量中,并将left
向右移动一位。 - 如果
height[left] >= height[right]
,则右侧柱子能接的雨水量为rightMax - height[right]
,将结果加到总雨水量中,并将right
向左移动一位。
- 如果
- 重复步骤 1-3,直到
left
和right
相遇。 - 返回计算得到的总雨水量。
题目代码
//双指针
class Solution {
public:
int trap(vector<int>& height) {
int ans = 0;
int size = height.size();
int left = 0; int right = size - 1;
int leftmax = 0;
int rightmax = 0;
while (left < right)
{
leftmax = max(leftmax, height[left]);
rightmax = max(rightmax, height[right]);
if (height[left] < height[right])
{
ans += leftmax - height[left];
++left;
}
else
{
ans += rightmax - height[right];
--right;
}
}
return ans;
}
};
class Solution {
public:
int trap(vector<int>& height) {
int ans = 0;
stack<int> st;
for (int i = 0; i < height.size(); i++) {
while (!st.empty() && height[i] >= height[st.top()]) {
int bottom_h = height[st.top()];
st.pop();
if (st.empty()) { //这一步很重要,必须要判断,否则当stk为空,下一步会出错
break;
}
int left = st.top();
int dh = min(height[left], height[i]) - bottom_h; // 面积的高
ans += dh * (i - left - 1);
}
st.push(i);
}
return ans;
}
};
示例 1:
输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。
示例 2:
输入:height = [4,2,0,3,2,5]
输出:9
验证回文串
题目描述
如果在将所有大写字符转换为小写字符、并移除所有非字母数字字符之后,短语正着读和反着读都一样。则可以认为该短语是一个 回文串 。
字母和数字都属于字母数字字符。
给你一个字符串 s
,如果它是 回文串 ,返回 true
;否则,返回 false
。
题目分析
最简单的方法是对字符串 s
进行一次遍历,并将其中的字母和数字字符进行保留,放在另一个字符串 sgood
中。这样我们只需要判断 sgood
是否是一个普通的回文串即可。
判断的方法有两种。第一种是使用语言中的字符串翻转 API 得到 sgood
的逆序字符串 sgood_rev
,只要这两个字符串相同,那么 sgood
就是回文串。
第二种是使用双指针。初始时,左右指针分别指向 sgood
的两侧,随后我们不断地将这两个指针相向移动,每次移动一步,并判断这两个指针指向的字符是否相同。当这两个指针相遇时,就说明 sgood
时回文串。
void isalnum(int c) 检查所传的字符是否是字母和数字。
int tolower(int c) 把给定的字母转换为小写字母。
题目代码
#include <iostream>
#include <string>
using namespace std;
//调用API
class Solution {
public:
bool isPalindrome(string S) {
string str;
for (auto& s : S)
{
if (isalnum(s))
{
str += tolower(s);
}
}
string str_rev(str.rbegin(), str.rend());
if (str == str_rev)
{
return true;
}
else
{
return false;
}
}
};
//双指针
class Solution {
public:
bool isPalindrome(string S) {
string str;
for (auto& s : S)
{
if (isalnum(s))
{
str += tolower(s);
}
}
int size = str.length();
int left = 0, right = size - 1;
while (left < right)
{
if (str[left++] != str[right--])
return false;
}
return true;
}
};
示例 1:
输入: s = "A man, a plan, a canal: Panama"
输出:true
解释:"amanaplanacanalpanama" 是回文串。
示例 2:
输入:s = "race a car"
输出:false
解释:"raceacar" 不是回文串。
示例 3:
输入:s = " "
输出:true
解释:在移除非字母数字字符之后,s 是一个空字符串 "" 。
由于空字符串正着反着读都一样,所以是回文串。
判断子序列
题目描述
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"
是"abcde"
的一个子序列,而"aec"
不是)。
进阶:
如果有大量输入的 S,称作 S1, S2, … , Sk 其中 k >= 10亿,你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码?
题目分析
s
是否是 t
的子序列,因此只要能找到任意一种 s
在 t
中出现的方式,即可认为 s
是 t
的子序列。
而当我们从前往后匹配,可以发现每次贪心地匹配靠前的字符是最优决策。
题目代码
#include <iostream>
#include <string>
#include <vector>
using namespace std;
//双指针
class Solution {
public:
bool isSubsequence(string s, string t) {
int str_first = 0;
int str_second = 0;
if (s == "")
{
return true;
}
int t_size = t.length();
int s_size = s.length();
while (str_second < t_size)
{
if (s[str_first] == t[str_second])
{
str_first++;
if (str_first == s_size)
return true;
}
str_second++;
}
return false;
}
};
示例 1:
输入:s = "abc", t = "ahbgdc"
输出:true
示例 2:
输入:s = "axc", t = "ahbgdc"
输出:false
两数之和Ⅱ-输入有序数组
题目描述
给你一个下标从 1 开始的整数数组 numbers
,该数组已按 非递减顺序排列 ,请你从数组中找出满足相加之和等于目标数 target
的两个数。如果设这两个数分别是 numbers[index1]
和 numbers[index2]
,则 1 <= index1 < index2 <= numbers.length
。
以长度为 2 的整数数组 [index1, index2]
的形式返回这两个整数的下标 index1
和 index2
。
你可以假设每个输入 只对应唯一的答案 ,而且你 不可以 重复使用相同的元素。
你所设计的解决方案必须只使用常量级的额外空间。
题目分析
属于正常的双指针
题目代码
#include <iostream>
#include <vector>
#include <unordered_map>
#include <algorithm>
using namespace std;
class Solution {
public:
vector<int> twoSum(vector<int>& numbers, int target) {
int left = 0;
int right = numbers.size() -1;
while (left < right)
{
int sum = numbers[left] + numbers[right];
if (target == sum)
{
return { left + 1,right + 1 };
}
else
{
if (sum < target)
{
left++;
}
else
{
if (sum > target )
right--;
}
}
}
return {};
}
};
盛水最多的容器
题目描述
给定一个长度为 n
的整数数组 height
。有 n
条垂线,第 i
条线的两个端点是 (i, 0)
和 (i, height[i])
。
找出其中的两条线,使得它们与 x
轴共同构成的容器可以容纳最多的水。
返回容器可以储存的最大水量。
说明:你不能倾斜容器。
题目分析
本题和接雨水的区别在于 接雨水需要考虑每一块能称多少水,而这道题只需要找到 所称最大区域就行了,也就是只需要找到两边
题目代码
class Solution {
public:
int maxArea(vector<int>& height) {
int l = 0, r = height.size() - 1;
int ans = 0;
while (l < r) {
int area = min(height[l], height[r]) * (r - l);
ans = max(ans, area);
if (height[l] <= height[r]) {
++l;
}
else {
--r;
}
}
return ans;
}
};
示例 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
三数之和
题目描述
给你一个整数数组 nums
,判断是否存在三元组 [nums[i], nums[j], nums[k]]
满足 i != j
、i != k
且 j != k
,同时还满足 nums[i] + nums[j] + nums[k] == 0
。请
你返回所有和为 0
且不重复的三元组。
注意:答案中不可以包含重复的三元组。
题目分析
题目代码
//灵神的方法
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
ranges::sort(nums);
vector<vector<int>> ans;
int n = nums.size();
for (int i = 0; i < n - 2; i++) {
int x = nums[i];
if (i && x == nums[i - 1]) continue; // 跳过重复数字
if (x + nums[i + 1] + nums[i + 2] > 0) break; // 优化一 因为我们已经排过序了
if (x + nums[n - 2] + nums[n - 1] < 0) continue; // 优化二
int j = i + 1, k = n - 1;
while (j < k) {
int s = x + nums[j] + nums[k];
if (s > 0) {
k--;
} else if (s < 0) {
j++;
} else {
ans.push_back({x, nums[j], nums[k]});
for (j++; j < k && nums[j] == nums[j - 1]; j++); // 跳过重复数字
for (k--; k > j && nums[k] == nums[k + 1]; k--); // 跳过重复数字
}
}
}
return ans;
}
};
示例 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 。
环形链表
题目描述
给你一个链表的头节点 head
,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos
不作为参数进行传递 。仅仅是为了标识链表的实际情况。
如果链表中存在环 ,则返回 true
。 否则,返回 false
。
题目分析
slow
指针每次前进一步,而 fast
指针每次前进两步。如果链表中存在环,它们最终会相遇。如果 fast
指针到达链表的末尾(即 fast
或 fast->next
为 nullptr
),则表示链表中没有环。
题目代码
class Solution {
public:
bool hasCycle(ListNode* head) {
if (head == nullptr || head->next == nullptr) {
return false;
}
ListNode* slow = head;
ListNode* fast = head->next;
while (slow != fast) {
if (fast == nullptr || fast->next == nullptr) {
return false;
}
slow = slow->next;
fast = fast->next->next;
}
return true;
}
};
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:
输入:head = [1], pos = -1
输出:false
解释:链表中没有环。
两数相加
题目描述
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
题目分析
由于输入的两个链表都是逆序存储数字的位数的,因此两个链表中同一位置的数字可以直接相加。
我们同时遍历两个链表,逐位计算它们的和,并与当前位置的进位值相加。具体而言,如果当前两个链表处相应位置的数字为 n1,n2,进位值为 carry
,则它们的和为 n1+n2+carry其中,答案链表处相应位置的数字为 (n1+n2+carry) mod 10
,而新的进位值为 (n1+n2+carry) / 10
如果两个链表的长度不同,则可以认为长度短的链表的后面有若干个 0 。
此外,如果链表遍历结束后,有 carry>0,还需要在答案链表的后面附加一个节点,节点的值为 carry
。
题目代码
#include <iostream>
#include <vector>
using namespace std;
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* addTwoNumbers(ListNode* l1, ListNode* l2) {
ListNode* head = new ListNode();
ListNode* tail = new ListNode();
//ListNode* head = nullptr, * tail = nullptr;
int carry = 0;
while (l1 || l2)
{
int n1 = l1 ? l1->val : 0;
int n2 = l2 ? l2->val : 0;
int sum = n1 + n2 + carry;
if (head == nullptr)
{
head = tail = new ListNode(sum % 10);
}
else
{
tail->next = new ListNode(sum % 10);
tail = tail->next;
}
carry = sum / 10;
if (l1 != nullptr)
l1 = l1->next;
if (l2 != nullptr)
l2 = l2->next;
}
if (carry > 0)
tail->next = new ListNode(carry);
return head;
}
};
示例 1:
输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807.
示例 2:
输入:l1 = [0], l2 = [0]
输出:[0]
示例 3:
输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]
输出:[8,9,9,9,0,0,0,1]
合并两个有序链表
题目描述
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
题目分析
在这里需要注意,虚拟头节点的学习
题目代码
#include <iostream>
#include <vector>
using namespace std;
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) {
ListNode* preHead = new ListNode(-1);
ListNode* prev = preHead;
while (l1 != nullptr && l2 != nullptr) {
if (l1->val < l2->val) {
prev->next = l1;
l1 = l1->next;
}
else {
prev->next = l2;
l2 = l2->next;
}
prev = prev->next;
}
// 合并后 l1 和 l2 最多只有一个还未被合并完,我们直接将链表末尾指向未合并完的链表即可
prev->next = l1 == nullptr ? l2 : l1;
return preHead->next;
}
};
示例 1:
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
示例 2:
输入:l1 = [], l2 = []
输出:[]
示例 3:
输入:l1 = [], l2 = [0]
输出:[0]
反转链表Ⅱ
题目描述
给你单链表的头指针 head
和两个整数 left
和 right
,其中 left <= right
。请你反转从位置 left
到位置 right
的链表节点,返回 反转后的链表 。
题目分析
一步步模拟一下 这道题很关键
题目代码
#include <iostream>
#include <vector>
using namespace std;
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 {
private:
void reverseLinkedList(ListNode* head) {
// 也可以使用递归反转一个链表
ListNode* pre = nullptr;
ListNode* cur = head;
while (cur != nullptr) {
ListNode* next = cur->next;
cur->next = pre;
pre = cur;
cur = next;
}
}
public:
ListNode* reverseBetween(ListNode* head, int left, int right) {
// 因为头节点有可能发生变化,使用虚拟头节点可以避免复杂的分类讨论
ListNode* dummyNode = new ListNode(-1);
dummyNode->next = head;
ListNode* pre = dummyNode;
// 第 1 步:从虚拟头节点走 left - 1 步,来到 left 节点的前一个节点
// 建议写在 for 循环里,语义清晰
for (int i = 0; i < left - 1; i++) {
pre = pre->next;
}
// 第 2 步:从 pre 再走 right - left + 1 步,来到 right 节点
ListNode* rightNode = pre;
for (int i = 0; i < right - left + 1; i++) {
rightNode = rightNode->next;
}
// 第 3 步:切断出一个子链表(截取链表)
ListNode* leftNode = pre->next;
ListNode* curr = rightNode->next;
// 注意:切断链接
pre->next = nullptr;
rightNode->next = nullptr;
// 第 4 步:同第 206 题,反转链表的子区间
reverseLinkedList(leftNode);
// 第 5 步:接回到原来的链表中
pre->next = rightNode;
leftNode->next = curr;
return dummyNode->next;
}
};
class Solution {
public:
ListNode* reverseBetween(ListNode* head, int left, int right) {
// 设置 dummyNode 是这一类问题的一般做法
ListNode* dummyNode = new ListNode(-1);
dummyNode->next = head;
ListNode* pre = dummyNode;
for (int i = 0; i < left - 1; i++) {
pre = pre->next;
}
ListNode* cur = pre->next;
ListNode* next;
//这个循环将执行 right - left 次,即需要反转的节点数量。
for (int i = 0; i < right - left; i++) {
next = cur->next;
cur->next = next->next;
next->next = pre->next;
pre->next = next;
}
return dummyNode->next;
}
};
示例 1:
输入:head = [1,2,3,4,5], left = 2, right = 4
输出:[1,4,3,2,5]
示例 2:
输入:head = [5], left = 1, right = 1
输出:[5]
K个一组翻转链表
题目描述
给你链表的头节点 head
,每 k
个节点一组进行翻转,请你返回修改后的链表。
k
是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k
的整数倍,那么请将最后剩余的节点保持原有顺序。
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
题目分析
使用迭代的方式去翻转链表
只能说以为会了 实际上没有会
题目代码
#include <iostream>
#include <vector>
#include <tuple>
using namespace std;
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:
// 翻转一个子链表,并且返回新的头与尾
pair<ListNode*, ListNode*> myReverse(ListNode* head, ListNode* tail) {
ListNode* prev = tail->next;
ListNode* cur = head;
while (prev != tail) {
ListNode* nex = cur->next;
cur->next = prev;
prev = cur;
cur = nex;
}
return { tail, head };
}
ListNode* reverseKGroup(ListNode* head, int k) {
ListNode* hair = new ListNode(0);
hair->next = head;
ListNode* pre = hair;
while (head) {
ListNode* tail = pre;
// 查看剩余部分长度是否大于等于 k
for (int i = 0; i < k; ++i) {
tail = tail->next;
if (!tail) {
return hair->next;
}
}
ListNode* nex = tail->next;
// 这里是 C++17 的写法,也可以写成
// pair<ListNode*, ListNode*> result = myReverse(head, tail);
// head = result.first;
// tail = result.second;
tie(head, tail) = myReverse(head, tail);
// 把子链表重新接回原链表
pre->next = head;
tail->next = nex;
pre = tail;
head = tail->next;
}
return hair->next;
}
};
//灵神的
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
int n = 0;
for (ListNode cur = head; cur != null; cur = cur.next) {
n++;
}
//preHead 是翻转区间的上个node
ListNode dummy = new ListNode(0, head), preHead = dummy;
ListNode pre = null, cur = head;
for (; n >= k; n -= k) {
//翻转之后 pre 是 newHead,cur 是下个区间的 head
for (int i = 0; i < k; ++i) {
ListNode next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
//这个时候 preHead.next 已经被翻转,是区间的结尾
ListNode tail = preHead.next;
//cur 是下个区间的 head
tail.next = cur;
//pre是区间的newHead
preHead.next = pre;
//preHead来到此区间的结尾,马上要操作下一区间了
preHead = tail;
}
return dummy.next;
}
}
示例 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个节点
题目描述
给你一个链表,删除链表的倒数第 n
个结点,并且返回链表的头结点。
题目分析
我=我们着重说一下一次遍历的 双指针。慢指针和快指针之间要相差两个节点(也就是相差三个next)
因为最后我们的快指针为nullptr时 ,慢指针刚刚好指向需要被删除节点的前一个。
题目代码
#include <iostream>
#include <stack>
using namespace std;
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) {
ListNode* copyhead = new ListNode(-1);
copyhead->next = head;
int size = 0;
ListNode* cur = copyhead;
while (cur != nullptr)
{
size++;
cur = cur->next;
}
ListNode* pre = copyhead;
for (int i = 0; i < size - n - 1; i++)
{
pre = pre->next;
}
pre->next = pre->next->next;
return copyhead->next;
}
};
//栈
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* copyhead = new ListNode(-1);
copyhead->next = head;
stack<ListNode*> stk;
ListNode* cur = copyhead;
while (cur != nullptr)
{
stk.push(cur);
cur = cur->next;
}
for (int i = 0; i < n; i++)
{
stk.pop();
}
ListNode* pre = stk.top();
pre->next = pre->next->next;
ListNode* ans = copyhead->next;
delete copyhead;
return ans;
}
};
//双指针
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummy = new ListNode(0, head);
ListNode* first = head;
ListNode* second = dummy;
for (int i = 0; i < n; ++i) {
first = first->next;
}
while (first) {
first = first->next;
second = second->next;
}
second->next = second->next->next;
ListNode* ans = dummy->next;
delete dummy;
return ans;
}
};
示例 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]
删除排序链表中重复元素Ⅱ
题目描述
给定一个已排序的链表的头 head
, 删除原始链表中所有重复数字的节点,只留下不同的数字 。返回 已排序的链表 。
题目分析
一开始做理解错题了,以为要删除多余的重复元素,没想到是全部删除,那我们建立完哑节点后,就判断下一个 和下下一个节点的值是否相同,如果相同,那么我们就记录下来其代表的值x
,然后判断cur->next->val
是否与记录下的值x
相同,如果相同,修改cur->next所指向的位置 注意不要修改cur的位置,这样可以将多个重复的值全部删除,直到遇到不重复的值
题目代码
#include <iostream>
#include <unordered_set>
using namespace std;
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* deleteDuplicates(ListNode* head) {
ListNode* dummyNode = new ListNode(-200, head);
ListNode* cur = dummyNode;
while (cur->next && cur->next->next)
{
if (cur->next->val == cur->next->next->val)
{
int x = cur->next->val;
while (cur->next && cur->next->val == x)
{
cur->next = cur->next->next;
}
}
else
{
cur = cur->next;
}
}
return dummyNode->next;
}
};
// -1 1 1 1 1 1 2 3 4
示例 1:
输入:head = [1,2,3,3,4,4,5]
输出:[1,2,5]
示例 2:
输入:head = [1,1,1,2,3]
输出:[2,3]
旋转链表
题目描述
给你一个链表的头节点 head
,旋转链表,将链表每个节点向右移动 k
个位置。
题目分析
我没有想到的点在于 可以记录链表的长度然后去找最后一个节点
题目代码
#include <iostream>
#include <vector>
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* rotateRight(ListNode* head, int k) {
if (k == 0 || head == nullptr || head->next == nullptr) {
return head;
}
int n = 1;
ListNode* iter = head;
while (iter->next != nullptr) {
iter = iter->next;
n++;
}
//1,2,3,4
//3 4 1 2
int add = n - k % n;
if (add == n) {
return head;
}
iter->next = head;
while (add--) {
iter = iter->next;
}
ListNode* ret = iter->next;
iter->next = nullptr;
return ret;
}
};
示例 1:
输入:head = [1,2,3,4,5], k = 2
输出:[4,5,1,2,3]
示例 2:
输入:head = [0,1,2], k = 4
输出:[2,0,1]
分隔链表
题目描述
给你一个链表的头节点 head
和一个特定值 x
,请你对链表进行分隔,使得所有 小于 x
的节点都出现在 大于或等于 x
的节点之前。
你应当 保留 两个分区中每个节点的初始相对位置。
题目分析
构造两个子链,一个记录小于的,一个记录大于等于的,最后将两个链表合在一起
题目代码
#include <iostream>
using namespace std;
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* partition(ListNode* head, int x) {
ListNode* small = new ListNode(0);
ListNode* smallHead = small;
ListNode* large = new ListNode(0);
ListNode* largeHead = large;
while (head != nullptr) {
if (head->val < x) {
small->next = head;
small = small->next;
}
else {
large->next = head;
large = large->next;
}
head = head->next;
}
large->next = nullptr;
small->next = largeHead->next;
return smallHead->next;
}
};
示例 1:
输入:head = [1,4,3,2,5,2], x = 3
输出:[1,2,2,4,3,5]
示例 2:
输入:head = [2,1], x = 2
输出:[1,2]
移动零
题目描述
给定一个数组 nums
,编写一个函数将所有 0
移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
题目分析
题目代码
#include <vector>
using namespace std;
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int n = nums.size();
int l = 0, r = 0;
while (r < n)
{
if (nums[r] != 0)
{
swap(nums[l], nums[r]);
l++;
}
r++;
}
}
};
示例 1:
输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]
示例 2:
输入: nums = [0]
输出: [0]
回文链表
题目描述
给你一个单链表的头节点 head
,请你判断该链表是否为回文链表。如果是,返回 true
;否则,返回 false
。
题目分析
题目代码
#include <iostream>
using namespace std;
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* reverseList(ListNode* head)
{
ListNode* pre = nullptr;
ListNode* cur = head;
ListNode* next;
while (cur)
{
next = cur->next;
cur->next = pre;
pre = cur;
cur = next;
}
return pre;
}
ListNode* FindMid(ListNode* head)
{
ListNode* fast = head;
ListNode* slow = head;
while (fast && fast->next)
{
fast = fast->next->next;
slow = slow->next;
}
return slow;
}
bool isPalindrome(ListNode* head) {
ListNode* mid = FindMid(head);
ListNode* head2 = reverseList(mid);
while (head != mid)
{
if (head->val != head2->val)
{
return false;
}
head = head->next;
head2 = head2->next;
}
return true;
}
};
示例 1:
输入:head = [1,2,2,1]
输出:true
示例 2:
输入:head = [1,2]
输出:false
提示:
- 链表中节点数目在范围
[1, 105]
内 0 <= Node.val <= 9
排序
有效的字母异位词
题目描述
给定两个字符串 *s*
和 *t*
,编写一个函数来判断 *t*
是否是 *s*
的字母异位词。
注意:若 *s*
和 *t*
中每个字符出现的次数都相同,则称 *s*
和 *t*
互为字母异位词。
题目解析
t
是 s
的异位词等价于「两个字符串排序后相等」。因此我们可以对字符串 s
和 t
分别排序,看排序后的字符串是否相等即可判断。此外,如果 s
和 t
的长度不同,t
必然不是 s
的异位词。
题目代码
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
class Solution {
public:
bool isAnagram(string s, string t) {
if (s.length() != t.length()) {
return false;
}
sort(s.begin(), s.end());
sort(t.begin(), t.end());
return s == t;
}
};
示例 1:
输入: s = "anagram", t = "nagaram"
输出: true
示例 2:
输入: s = "rat", t = "car"
输出: false
丢失的数字
题目描述
给定一个包含 [0, n]
中 n
个数的数组 nums
,找出 [0, n]
这个范围内没有出现在数组中的那个数。
题目解析
将数组排序之后,即可根据数组中每个下标处的元素是否和下标相等,得到丢失的数字
题目代码
class Solution {
public:
int missingNumber(vector<int>& nums) {
int numcount = nums.size();
int tag = 0;
sort(nums.begin(),nums.end());
for (auto& i : nums)
{
if (i == tag)
tag++;
else
return tag;
}
return tag;
}
};
示例 1:
输入:nums = [3,0,1]
输出:2
解释:n = 3,因为有 3 个数字,所以所有的数字都在范围 [0,3] 内。2 是丢失的数字,因为它没有出现在 nums 中。
示例 2:
输入:nums = [0,1]
输出:2
解释:n = 2,因为有 2 个数字,所以所有的数字都在范围 [0,2] 内。2 是丢失的数字,因为它没有出现在 nums 中。
示例 3:
输入:nums = [9,6,4,2,3,5,7,0,1]
输出:8
解释:n = 9,因为有 9 个数字,所以所有的数字都在范围 [0,9] 内。8 是丢失的数字,因为它没有出现在 nums 中。
多数元素
题目描述
给定一个大小为 n
的数组 nums
,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋
的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
题目解析
如果将数组 nums
中的所有元素按照单调递增或单调递减的顺序排序,那么下标为 ⌊ n/2 ⌋
的元素(下标从 0 开始)一定是众数。
题目代码
class Solution {
public:
int majorityElement(vector<int>& nums) {
sort(nums.begin(), nums.end());
return nums[nums.size() / 2];
}
};
示例 1:
输入:nums = [3,2,3]
输出:3
示例 2:
输入:nums = [2,2,1,1,1,2,2]
输出:2
H指数
题目描述
给你一个整数数组 citations
,其中 citations[i]
表示研究者的第 i
篇论文被引用的次数。计算并返回该研究者的 h
指数。
根据维基百科上 h 指数的定义:h
代表“高引用次数” ,一名科研人员的 h
指数 是指他(她)至少发表了 h
篇论文,并且 至少 有 h
篇论文被引用次数大于等于 h
。如果 h
有多种可能的值,h
指数 是其中最大的那个。
题目分析
我们可以知道h一定是小于等于论文数量的,所以我们可在论文数量中进行二分查找,最终找到正确的h值
题目代码
class Solution {
public:
int hIndex(vector<int>& citations) {
int left=0,right=citations.size() ;
int mid=0,cnt=0;
while(left<right){
// +1 防止死循环
mid=(left+right+1)>>1;
cnt=0;
for(int i=0;i<citations.size();i++){
if(citations[i]>=mid){
cnt++;
}
}
if(cnt>=mid){
// 要找的答案在 [mid,right] 区间内
left=mid;
}else{
// 要找的答案在 [0,mid) 区间内
right=mid-1;
}
}
return left;
}
};
题目分析
- 初始化:
n
:数组citations
的大小。tot
:用于计算当前引用次数至少为i
的论文数量。counter
:一个大小为n+1
的数组,用于统计每个引用次数对应的论文数量。由于引用次数可能是从0到n
的任意整数,所以数组大小为n+1
。
- 统计引用次数:
- 遍历
citations
数组,对于每个引用次数citations[i]
:- 如果
citations[i]
大于等于n
,那么将其统计到counter[n]
中(因为H-Index不可能大于数组长度n
)。 - 否则,将其统计到
counter[citations[i]]
中。
- 如果
- 遍历
- 寻找H-Index:
- 从
n
开始,向前遍历counter
数组。 - 对于每个
i
(从n
到0
),将counter[i]
累加到tot
中。 - 如果
tot
(即当前引用次数至少为i
的论文数量)大于或等于i
,那么i
就是我们要找的H-Index,直接返回。 - 如果遍历完整个
counter
数组都没有找到满足条件的i
,则返回0
(这通常不会发生,除非citations
数组为空,但按照题目要求,这种情况应该返回0
作为H-Index)。
- 从
题目代码
class Solution {
public:
int hIndex(vector<int>& citations) {
int n = citations.size(), tot = 0;
vector<int> counter(n + 1);
for (int i = 0; i < n; i++) {
if (citations[i] >= n) {
counter[n]++;
}
else {
counter[citations[i]]++;
}
}
for (int i = n; i >= 0; i--) {
tot += counter[i];
if (tot >= i) {
return i;
}
}
return 0;
}
};
示例 1:
输入:citations = [3,0,6,1,5]
输出:3
解释:给定数组表示研究者总共有 5 篇论文,每篇论文相应的被引用了 3, 0, 6, 1, 5 次。
由于研究者有 3 篇论文每篇 至少 被引用了 3 次,其余两篇论文每篇被引用 不多于 3 次,所以她的 h 指数是 3。
示例 2:
输入:citations = [1,3,1]
输出:1
字母异位词分组
题目描述
给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。
字母异位词 是由重新排列源单词的所有字母得到的一个新单词。
题目分析
- 初始状态: 假设
mp
是一个空的unordered_map
,key
是某个字符串的排序版本,str
是原始的字符串。 - 第一次调用: 当
str
是输入列表中的第一个字符串(例如”eat”)时,key
会被排序为”ate”。如果mp
中还没有键”ate”,那么mp
会创建一个新的键”ate”,并关联一个空的vector<string>
。mp["ate"]
=[]
- 执行
emplace_back
:str
(“eat”)会被插入到mp["ate"]
的末尾。mp["ate"]
=["eat"]
- 后续调用: 对于输入列表中的下一个字符串”tea”,其排序后的
key
也是”ate”。由于mp
中已经有了键”ate”,所以”tea”会被添加到与”ate”关联的vector<string>
中。mp["ate"]
=["eat", "tea"]
- 重复过程: 对于所有其他与”ate”排序后相同的字符串(例如”ate”),它们也会被添加到
mp["ate"]
中。mp["ate"]
=["eat", "tea", "ate"]
题目代码
#include <iostream>
#include <vector>
#include <string>
#include <unordered_map>
#include <algorithm>
using namespace std;
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
unordered_map<string, vector<string>> mp;
for (string& str : strs) {
string key = str;
sort(key.begin(), key.end());
mp[key].emplace_back(str);
}
vector<vector<string>> ans;
for (auto it = mp.begin(); it != mp.end(); ++it) {
ans.emplace_back(it->second);
}
return ans;
}
};
示例 1:
输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
输出: [["bat"],["nat","tan"],["ate","eat","tea"]]
示例 2:
输入: strs = [""]
输出: [[""]]
示例 3:
输入: strs = ["a"]
输出: [["a"]]
合并区间
题目描述
以数组 intervals
表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi]
。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。
题目分析
首先,我们将列表中的区间按照左端点升序排序。然后我们将第一个区间加入 merged 数组中,并按顺序依次考虑之后的每个区间:
如果当前区间的左端点在数组 merged 中最后一个区间的右端点之后,那么它们不会重合,我们可以直接将这个区间加入数组 merged 的末尾;
否则,它们重合,我们需要用当前区间的右端点更新数组 merged 中最后一个区间的右端点,将其置为二者的较大值。
题目代码
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
vector<vector<int>> merge(vector<vector<int>>& intervals) {
if (intervals.size() == 0) {
return {};
}
sort(intervals.begin(), intervals.end());
vector<vector<int>> merged;
for (int i = 0; i < intervals.size(); ++i) {
int L = intervals[i][0], R = intervals[i][1];
if (!merged.size() || merged.back()[1] < L) {
merged.push_back({ L, R });
}
else {
merged.back()[1] = max(merged.back()[1], R);
}
}
return merged;
}
};
//灵神的方法
class Solution {
public:
vector<vector<int>> merge(vector<vector<int>>& intervals) {
ranges::sort(intervals); // 按照左端点从小到大排序
vector<vector<int>> ans;
for (auto& p : intervals) {
if (!ans.empty() && p[0] <= ans.back()[1]) { // 可以合并
ans.back()[1] = max(ans.back()[1], p[1]); // 更新右端点最大值
} else { // 不相交,无法合并
ans.emplace_back(p); // 新的合并区间
}
}
return ans;
}
};
示例 1:
输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
示例 2:
输入:intervals = [[1,4],[4,5]]
输出:[[1,5]]
解释:区间 [1,4] 和 [4,5] 可被视为重叠区间。
让所有学生保持开心的分组方法数
题目描述
给你一个下标从 0 开始、长度为 n
的整数数组 nums
,其中 n
是班级中学生的总数。班主任希望能够在让所有学生保持开心的情况下选出一组学生:
如果能够满足下述两个条件之一,则认为第 i
位学生将会保持开心:
- 这位学生被选中,并且被选中的学生人数 严格大于
nums[i]
。 - 这位学生没有被选中,并且被选中的学生人数 严格小于
nums[i]
。
返回能够满足让所有学生保持开心的分组方法的数目。
题目分析
2860. 让所有学生保持开心的分组方法数 – 力扣(LeetCode)
题目代码
class Solution {
public:
int countWays(vector<int>& nums) {
ranges::sort(nums);
int ans = nums[0] > 0;
for (int i = 1; i < nums.size(); i++)
{
ans += nums[i - 1] < i && i < nums[i];
}
return ans + 1;
}
};
示例 1:
输入:nums = [1,1]
输出:2
解释:
有两种可行的方法:
班主任没有选中学生。
班主任选中所有学生形成一组。
如果班主任仅选中一个学生来完成分组,那么两个学生都无法保持开心。因此,仅存在两种可行的方法。
示例 2:
输入:nums = [6,0,3,3,6,7,2,7]
输出:3
解释:
存在三种可行的方法:
班主任选中下标为 1 的学生形成一组。
班主任选中下标为 1、2、3、6 的学生形成一组。
班主任选中所有学生形成一组。
位运算
丢失的数字
题目描述
给定一个包含 [0, n]
中 n
个数的数组 nums
,找出 [0, n]
这个范围内没有出现在数组中的那个数。
题目解析
数组 nums
中有 n
个数,在这 n
个数的后面添加从 0
到 n
的每个整数,则添加了 n+1
个整数,共有 2n+1
个整数。
在 2n+1
个整数中,丢失的数字只在后面 n+1
个整数中出现一次,其余的数字在前面 n
个整数中(即数组中)和后面 n+1
个整数中各出现一次,即其余的数字都出现了两次。
根据出现的次数的奇偶性,可以使用按位异或运算得到丢失的数字。按位异或运算 ⊕
满足交换律和结合律,且对任意整数 x
都满足 x⊕x=0
和 x⊕0=x
。
由于上述 2n+1
个整数中,丢失的数字出现了一次,其余的数字都出现了两次,因此对上述 2n+1
个整数进行按位异或运算,结果即为丢失的数字。
题目代码
class Solution {
public:
int missingNumber(vector<int>& nums) {
int res = 0;
int n = nums.size();
for (int i = 0; i < n; i++) {
res ^= nums[i];
}
for (int i = 0; i <= n; i++) {
res ^= i;
}
return res;
}
};
示例 1:
输入:nums = [3,0,1]
输出:2
解释:n = 3,因为有 3 个数字,所以所有的数字都在范围 [0,3] 内。2 是丢失的数字,因为它没有出现在 nums 中。
示例 2:
输入:nums = [0,1]
输出:2
解释:n = 2,因为有 2 个数字,所以所有的数字都在范围 [0,2] 内。2 是丢失的数字,因为它没有出现在 nums 中。
示例 3:
输入:nums = [9,6,4,2,3,5,7,0,1]
输出:8
解释:n = 9,因为有 9 个数字,所以所有的数字都在范围 [0,9] 内。8 是丢失的数字,因为它没有出现在 nums 中。
设计机械累加器
题目描述
请设计一个机械累加器,计算从 1、2… 一直累加到目标数值 target
的总和。注意这是一个只能进行加法操作的程序,不具备乘除、if-else、switch-case、for 循环、while 循环,及条件判断语句等高级功能。
题目分析
对于A&&B
来说,只要A为false则B不会执行
对于A||B
来说 ,只要A为true则B不会执行
我们可以巧妙的运用这一点
题目代码
//不推荐这个方法,但是可以熟悉static和构造函数
class Temp {
public:
// 构造函数
Temp() {
N++;
sum += N;
}
// 返回值(静态函数)
static unsigned int getSum() {
return sum;
}
// 重置静态变量
static void reset() {
N = 0;
sum = 0;
}
private:
// 静态变量的声明式
static unsigned int N;
static unsigned int sum;
};
// 定义式
unsigned int Temp::N = 0;
unsigned int Temp::sum = 0;
class Solution {
public:
int sumNums(int n) {
Temp::reset();
Temp* p = new Temp[n];
delete[] p;
p = nullptr;
return Temp::getSum();
}
};
示例 1:
输入: target = 5
输出: 15
示例 2:
输入: target = 7
输出: 28
贪心
整数转罗马数字
题目描述
罗马数字包含以下七种字符: 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。
给你一个整数,将其转为罗马数字。
题目解析
贪心法则:我们每次尽量使用最大的数来表示。 比如对于 1994
这个数,如果我们每次尽量用最大的数来表示,依次选 1000
,900
,90
,4
,会得到正确结果 MCMXCIV
。
所以,我们将哈希表按照从大到小的顺序排列,然后遍历哈希表,直到表示完整个输入
题目代码
#include <iostream>
#include <string>
using namespace std;
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++) //这里不使用图里的count了,一遍一遍来就行了
while (num >= values[i])
{
num -= values[i];
res += reps[i];
}
return res;
}
};
示例 1:
输入: num = 3
输出: "III"
示例 2:
输入: num = 4
输出: "IV"
示例 3:
输入: num = 9
输出: "IX"
买卖股票的最佳时机Ⅱ
题目描述
给你一个整数数组 prices
,其中 prices[i]
表示某支股票第 i
天的价格。
在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
返回 你能获得的 最大 利润 。
题目解析
由于这里的股票可以日抛,只需考虑那些处于上涨阶段的股票就行.
题目代码
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
int maxProfit(vector<int>& prices) {
int sum = 0;
int size = prices.size();
for (int i = size - 1; i > 0; i--)
{
sum += max(0, (prices[i] - prices[i - 1]));
}
return sum;
}
};
示例 1:
输入:prices = [7,1,5,3,6,4]
输出:7
解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3 。
总利润为 4 + 3 = 7 。
示例 2:
输入:prices = [1,2,3,4,5]
输出:4
解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。
总利润为 4 。
示例 3:
输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 交易无法获得正利润,所以不参与交易可以获得最大利润,最大利润为 0 。
跳跃游戏
题目描述
给你一个非负整数数组 nums
,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标,如果可以,返回 true
;否则,返回 false
。
题目分析
这样以来,我们依次遍历数组中的每一个位置,并实时维护 最远可以到达的位置。对于当前遍历到的位置 x
,如果它在 最远可以到达的位置 的范围内,那么我们就可以从起点通过若干次跳跃到达该位置,因此我们可以用 x+nums[x]
更新 最远可以到达的位置。
在遍历的过程中,如果 最远可以到达的位置 大于等于数组中的最后一个位置,那就说明最后一个位置可达,我们就可以直接返回 True
作为答案。反之,如果在遍历结束后,最后一个位置仍然不可达,我们就返回 False
作为答案。
题目代码
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
bool canJump(vector<int>& nums) {
int nums_size = nums.size();
int maxstep = 0;
for (int i = 0; i < nums_size - 1; i++)
{
if (i <= maxstep) //首先要判断是否能到达下标位置
{
maxstep = max((nums[i] + i), maxstep);
}
if (maxstep >= nums_size - 1)
return true;
}
return false;
}
};
示例 1:
输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。
示例 2:
输入:nums = [3,2,1,0,4]
输出:false
解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。
跳跃游戏Ⅱ
题目描述
给定一个长度为 n
的 0 索引整数数组 nums
。初始位置为 nums[0]
。
每个元素 nums[i]
表示从索引 i
向前跳转的最大长度。换句话说,如果你在 nums[i]
处,你可以跳转到任意 nums[i + j]
处:
0 <= j <= nums[i]
i + j < n
返回到达 nums[n - 1]
的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]
。
题目分析
从头开始,计算能走到最远的距离 max(maxPos, i + nums[i])
,同时判断是否走到尽头end
题目代码
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
int jump(vector<int>& nums) {
int maxPos = 0, n = nums.size(), end = 0, step = 0;
for (int i = 0; i < n - 1; ++i) {
if (maxPos >= i) {
maxPos = max(maxPos, i + nums[i]);
if (i == end) {
end = maxPos;
++step;
}
}
}
return step;
}
};
示例 1:
输入: nums = [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。
从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。
示例 2:
输入: nums = [2,3,0,1,4]
输出: 2
加油站
题目描述
在一条环路上有 n
个加油站,其中第 i
个加油站有汽油 gas[i]
升。
你有一辆油箱容量无限的的汽车,从第 i
个加油站开往第 i+1
个加油站需要消耗汽油 cost[i]
升。你从其中的一个加油站出发,开始时油箱为空。
给定两个整数数组 gas
和 cost
,如果你可以按顺序绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1
。如果存在解,则 保证 它是 唯一 的。
题目分析
我们首先检查第 0 个加油站,并试图判断能否环绕一周;如果不能,就从第一个无法到达的加油站开始继续检查。
这段代码的目的是确定一个起始点,使得一辆汽车从该点出发能够遍历完一个循环路径(由gas
和cost
两个数组表示),而不需要耗尽汽油。其中,gas[i]
表示在第i
个加油站可以获得的汽油量,而cost[i]
表示汽车从第i
个加油站到第i+1
个加油站(或回到起点)所需的汽油量。
- 双层循环:代码使用了一个外层循环和一个内层循环。外层循环遍历所有可能的起始点,而内层循环则尝试从当前起始点出发,遍历整个路径,并检查汽油是否足够。
- 内层循环:
* `gasall`和`costall`分别用于记录从当前起始点开始到当前加油站为止的累积汽油量和累积消耗量。
* `cnt`用于记录从当前起始点开始已经遍历了多少个加油站。
* 内层循环会尝试从当前起始点开始,一直遍历到路径的末尾或汽油不足为止。
* 如果在遍历过程中发现`gasall`小于`costall`,则说明从当前起始点出发无法遍历完整个路径,因此跳出内层循环。
- 确定起始点:
* 如果内层循环能够遍历完整个路径(即`cnt == n`),那么说明从当前起始点出发可以遍历完整个路径,因此返回该起始点的索引。
* 否则,更新起始点为下一个尚未尝试过的加油站(即`i = i + cnt + 1`)。由于`cnt`表示从当前起始点开始已经遍历了多少个加油站,因此`i + cnt + 1`确保了我们从下一个尚未尝试过的加油站开始。
- 返回值:
* 如果能够找到一个起始点使得汽车可以遍历完整个路径,则返回该起始点的索引。
* 否则,返回-1,表示不存在这样的起始点。
题目代码
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
int n = gas.size();
int i = 0;
while (i < n) {
int sumOfGas = 0, sumOfCost = 0;
int cnt = 0;
while (cnt < n) {
int j = (i + cnt) % n;
sumOfGas += gas[j];
sumOfCost += cost[j];
if (sumOfCost > sumOfGas) {
break;
}
cnt++;
}
if (cnt == n) {
return i;
}
else {
i = i + cnt + 1;
}
}
return -1;
}
};
示例 1:
输入: gas = [1,2,3,4,5], cost = [3,4,5,1,2]
输出: 3
解释:
从 3 号加油站(索引为 3 处)出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油
开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油
开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油
开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油
开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油
开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。
因此,3 可为起始索引。
示例 2:
输入: gas = [2,3,4], cost = [3,4,3]
输出: -1
解释:
你不能从 0 号或 1 号加油站出发,因为没有足够的汽油可以让你行驶到下一个加油站。
我们从 2 号加油站出发,可以获得 4 升汽油。 此时油箱有 = 0 + 4 = 4 升汽油
开往 0 号加油站,此时油箱有 4 - 3 + 2 = 3 升汽油
开往 1 号加油站,此时油箱有 3 - 3 + 3 = 3 升汽油
你无法返回 2 号加油站,因为返程需要消耗 4 升汽油,但是你的油箱只有 3 升汽油。
因此,无论怎样,你都不可能绕环路行驶一周。
分发糖果
题目描述
n
个孩子站成一排。给你一个整数数组 ratings
表示每个孩子的评分。
你需要按照以下要求,给这些孩子分发糖果:
- 每个孩子至少分配到
1
个糖果。 - 相邻两个孩子评分更高的孩子会获得更多的糖果。
请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。
题目分析
我们可以将「相邻的孩子中,评分高的孩子必须获得更多的糖果」这句话拆分为两个规则,分别处理。
左规则:当 ratings[i−1]<ratings[i]
时,i
号学生的糖果数量将比 i−1
号孩子的糖果数量多。
右规则:当 ratings[i]>ratings[i+1]
时,i
号学生的糖果数量将比 i+1
号孩子的糖果数量多。
我们遍历该数组两次,处理出每一个学生分别满足左规则或右规则时,最少需要被分得的糖果数量。每个人最终分得的糖果数量即为这两个数量的最大值。
在实际代码中,我们先计算出左规则 left
数组,在计算右规则的时候只需要用单个变量记录当前位置的右规则,同时计算答案即可。
题目代码
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
int candy(vector<int>& ratings) {
int n = ratings.size();
vector<int> left(n);
for (int i = 0; i < n; i++) {
if (i > 0 && ratings[i] > ratings[i - 1]) {
left[i] = left[i - 1] + 1;
}
else {
left[i] = 1;
}
}
int right = 0, ret = 0;
for (int i = n - 1; i >= 0; i--) {
if (i < n - 1 && ratings[i] > ratings[i + 1]) {
right++;
}
else {
right = 1;
}
ret += max(left[i], right);
}
return ret;
}
};
示例 1:
输入:ratings = [1,0,2]
输出:5
解释:你可以分别给第一个、第二个、第三个孩子分发 2、1、2 颗糖果。
示例 2:
输入:ratings = [1,2,2]
输出:4
解释:你可以分别给第一个、第二个、第三个孩子分发 1、2、1 颗糖果。
第三个孩子只得到 1 颗糖果,这满足题面中的两个条件。
安排工作以达到最大收益
题目描述
你有 n
个工作和 m
个工人。给定三个数组: difficulty
, profit
和 worker
,其中:
difficulty[i]
表示第i
个工作的难度,profit[i]
表示第i
个工作的收益。worker[i]
是第i
个工人的能力,即该工人只能完成难度小于等于worker[i]
的工作。
每个工人 最多 只能安排 一个 工作,但是一个工作可以 完成多次 。
- 举个例子,如果 3 个工人都尝试完成一份报酬为
$1
的同样工作,那么总收益为$3
。如果一个工人不能完成任何工作,他的收益为$0
。
返回 在把工人分配到工作岗位后,我们所能获得的最大利润 。
题目分析
我们首先对工人按照能力大小排序,对工作按照难度排序。
我们使用「双指针」的方法,一个指针指向工人数组,一个指向任务数组,从低难度的任务开始遍历。对于每个工人,我们继续遍历任务,直到难度大于其能力,并把可以完成任务中的最大利润更新到结果中。
最后返回所有工人能得到的利润总和。
题目代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
int maxProfitAssignment(vector<int>& difficulty, vector<int>& profit, vector<int>& worker) {
vector<pair<int, int>> jobs;
int n = profit.size(), res = 0, i = 0, best = 0;
for (int j = 0; j < n; ++j) {
jobs.emplace_back(difficulty[j], profit[j]);
}
sort(jobs.begin(), jobs.end());
sort(worker.begin(), worker.end());
for (int w : worker) {
while (i < n && w >= jobs[i].first) {
best = max(best, jobs[i].second);
i++;
}
res += best;
}
return res;
}
};
示例 1:
输入: difficulty = [2,4,6,8,10], profit = [10,20,30,40,50], worker = [4,5,6,7]
输出: 100
解释: 工人被分配的工作难度是 [4,4,6,6] ,分别获得 [20,20,30,30] 的收益。
示例 2:
输入: difficulty = [85,47,57], profit = [24,66,99], worker = [40,25,25]
输出: 0
分治
多数元素
题目描述
给定一个大小为 n
的数组 nums
,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋
的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
题目解析
如果数 a
是数组 nums
的众数,如果我们将 nums
分成两部分,那么 a
必定是至少一部分的众数。
我们可以使用反证法来证明这个结论。假设 a
既不是左半部分的众数,也不是右半部分的众数,那么 a
出现的次数少于 l / 2 + r / 2
次,其中 l
和 r
分别是左半部分和右半部分的长度。由于 l / 2 + r / 2 <= (l + r) / 2
,说明 a
也不是数组 nums
的众数,因此出现了矛盾。所以这个结论是正确的。
这样以来,我们就可以使用分治法解决这个问题:将数组分成左右两部分,分别求出左半部分的众数 a1
以及右半部分的众数 a2
,随后在 a1
和 a2
中选出正确的众数。
题目代码
class Solution {
int count_in_range(vector<int>& nums, int target, int lo, int hi) {
int count = 0;
for (int i = lo; i <= hi; ++i)
if (nums[i] == target)
++count;
return count;
}
int majority_element_rec(vector<int>& nums, int lo, int hi) {
if (lo == hi)
return nums[lo];
int mid = (lo + hi) / 2;
int left_majority = majority_element_rec(nums, lo, mid);
int right_majority = majority_element_rec(nums, mid + 1, hi);
if (count_in_range(nums, left_majority, lo, hi) > (hi - lo + 1) / 2)
return left_majority;
if (count_in_range(nums, right_majority, lo, hi) > (hi - lo + 1) / 2)
return right_majority;
return -1;
}
public:
int majorityElement(vector<int>& nums) {
return majority_element_rec(nums, 0, nums.size() - 1);
}
};
示例 1:
输入:nums = [3,2,3]
输出:3
模拟过程
调用 majorityElement 函数,传入数组 [3, 2, 3],初始范围是从索引 0 到索引 2。
调用 majority_element_rec 函数,传入数组 [3, 2, 3],范围是从索引 0 到索引 2。
由于 lo != hi,递归继续。计算中间索引 mid = (0 + 2) / 2 = 1。
递归调用 majority_element_rec 在左半部分 [3, 2](范围从 0 到 1)。
再次检查 lo != hi,继续递归。
这次 mid = (0 + 1) / 2 = 0。
递归调用 majority_element_rec 在左半部分 [3](范围从 0 到 0)。
lo == hi,返回 nums[0] = 3 作为左半部分的多数元素。
返回左半部分的多数元素 3,并赋值给 left_majority。
递归调用 majority_element_rec 在右半部分 [3](范围从 2 到 2)。
lo == hi,返回 nums[2] = 3 作为右半部分的多数元素。
返回右半部分的多数元素 3,并赋值给 right_majority。
现在,检查 left_majority 和 right_majority 在整个数组 [3, 2, 3] 中的出现次数。
count_in_range(nums, left_majority, 0, 2) = 2(因为 3 在数组中出现了两次)。
2 > (2 - 0 + 1) / 2 是真的,所以返回 left_majority,即 3。
majorityElement 函数返回 3,这是数组 [3, 2, 3] 中的多数元素。
示例 2:
输入:nums = [2,2,1,1,1,2,2]
输出:2
两数之和Ⅱ-输入有序数组
题目描述
给你一个下标从 1 开始的整数数组 numbers
,该数组已按 非递减顺序排列 ,请你从数组中找出满足相加之和等于目标数 target
的两个数。如果设这两个数分别是 numbers[index1]
和 numbers[index2]
,则 1 <= index1 < index2 <= numbers.length
。
以长度为 2 的整数数组 [index1, index2]
的形式返回这两个整数的下标 index1
和 index2
。
你可以假设每个输入 只对应唯一的答案 ,而且你 不可以 重复使用相同的元素。
你所设计的解决方案必须只使用常量级的额外空间。
题目分析
这道题逻辑很简单,需要学习的是边界的处理
题目代码
#include <iostream>
#include <vector>
#include <unordered_map>
#include <algorithm>
using namespace std;
class Solution {
public:
vector<int> twoSum(vector<int>& numbers, int target) {
int size = numbers.size();
for (int i = 0; i < size - 1; i++)
{
int low = i + 1 ;
int high = size - 1;
while (low <= high) {
int mid = (low + high) >> 1;
if (numbers[mid] == target - numbers[i]) {
return { i + 1, mid + 1 };
}
else if (numbers[mid] > target - numbers[i]) {
high = mid - 1;
}
else {
low = mid + 1;
}
}
}
return {};
}
};
#include <iostream>
#include <vector>
#include <unordered_map>
#include <algorithm>
using namespace std;
class Solution {
public:
vector<int> twoSum(vector<int>& numbers, int target) {
int size = numbers.size();
for (int i = 0; i < size - 1; i++)
{
int low = i ;
int high = size;
while (low < high) {
int mid = (low + high) >> 1;
if (numbers[mid] == target - numbers[i]) {
return { i + 1, mid + 1 };
}
else if (numbers[mid] > target - numbers[i]) {
high = mid;
}
else {
low = mid +1;
}
}
}
return {};
}
};
示例 1:
输入:numbers = [2,7,11,15], target = 9
输出:[1,2]
解释:2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。返回 [1, 2] 。
示例 2:
输入:numbers = [2,3,4], target = 6
输出:[1,3]
解释:2 与 4 之和等于目标数 6 。因此 index1 = 1, index2 = 3 。返回 [1, 3] 。
示例 3:
输入:numbers = [-1,0], target = -1
输出:[1,2]
解释:-1 与 0 之和等于目标数 -1 。因此 index1 = 1, index2 = 2 。返回 [1, 2] 。
Boyer-Moore 投票算法
多数元素
题目描述
给定一个大小为 n
的数组 nums
,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋
的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
题目解析
我们维护一个候选众数 candidate
和它出现的次数 count
。初始时 candidate
可以为任意值,count
为 0;
我们遍历数组 nums
中的所有元素,对于每个元素 x
,在判断 x
之前,如果 count
的值为 0,我们先将 x
的值赋予 candidate
,随后我们判断 x
:
如果 x
与 candidate
相等,那么计数器 count
的值增加 1;
如果 x
与 candidate
不等,那么计数器 count
的值减少 1。
在遍历完成后,candidate
即为整个数组的众数。
题目代码
class Solution {
public:
int majorityElement(vector<int>& nums) {
int count = 0;
int candidate = -1;
for (auto& num : nums)
{
if (candidate == num)
{
count++;
}
else
{
if (--count < 0)
{
candidate = num;
count = 1;
}
}
}
return candidate;
}
};
示例 1:
输入:nums = [3,2,3]
输出:3
示例 2:
输入:nums = [2,2,1,1,1,2,2]
输出:2
数组翻转
轮转数组
题目描述
给定一个整数数组 nums
,将数组中的元素向右轮转 k
个位置,其中 k
是非负数。
题目分析
该方法基于如下的事实:当我们将数组的元素向右移动 k
次后,尾部 k mod n
个元素会移动至数组头部,其余元素向后移动 k mod n
个位置。
该方法为数组的翻转:我们可以先将所有元素翻转,这样尾部的 k mod nk\bmod nkmodn 个元素就被移至数组头部,然后我们再翻转 0,kmodn−1
区间的元素和kmodn,n−1
区间的元素即能得到最后的答案。
我们以 n=7,k=3
为例进行如下展示:
操作 | 结果 |
---|---|
原始数组 | 1 2 3 4 5 6 7 |
翻转所有元素 | 7 6 5 4 3 2 1 |
翻转0,kmodn-1区间的元素(假设k=3) | 5 6 7 4 3 2 1 |
翻转kmodn,n-1区间的元素(假设k=3) | 5 6 7 1 2 3 4 |
题目代码
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
void reverse(vector<int>& nums, int start, int end) {
while (start < end) {
swap(nums[start], nums[end]);
start += 1;
end -= 1;
}
}
void rotate(vector<int>& nums, int k) {
k %= nums.size();
reverse(nums, 0, nums.size() - 1);
reverse(nums, 0, k - 1);
reverse(nums, k, nums.size() - 1);
}
};
示例 1:
输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右轮转 1 步: [7,1,2,3,4,5,6]
向右轮转 2 步: [6,7,1,2,3,4,5]
向右轮转 3 步: [5,6,7,1,2,3,4]
示例 2:
输入:nums = [-1,-100,3,99], k = 2
输出:[3,99,-1,-100]
解释:
向右轮转 1 步: [99,-1,-100,3]
向右轮转 2 步: [3,99,-1,-100]
动态规划
买卖股票的最佳时机
题目描述
给定一个数组 prices
,它的第 i
个元素 prices[i]
表示一支给定股票第 i
天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0
。
题目分析
我们只需要遍历价格数组一遍,记录历史最低点,然后在每一天考虑这么一个问题:如果我是在历史最低点买进的,那么我今天卖出能赚多少钱?当考虑完所有天数之时,我们就得到了最好的答案。
题目代码
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
int maxProfit(vector<int>& prices) {
int minprice = 1e9;
int maxprofit = 0;
for (auto& price : prices)
{
maxprofit = max(maxprofit, price - minprice);
minprice = min(minprice, price);
}
if (maxprofit > 0)
return maxprofit;
else
return 0;
}
};
示例 1:
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
示例 2:
输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 没有交易完成, 所以最大利润为 0。
接雨水
题目描述
给定 n
个非负整数表示每个宽度为 1
的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
题目分析
- 初始化:
* `size`是`height`数组的大小。
* 如果`size`为0,则直接返回0,因为没有柱子可以接雨水。
- 计算左侧最大高度:
* 使用一个`leftmax`数组来存储每个位置左侧的最大高度。
* `leftmax[0]`自然是`height[0]`,因为第一个柱子左侧没有柱子。
* 从`i = 1`开始遍历,`leftmax[i]`是`leftmax[i-1]`和`height[i]`中的较大值。
- 计算右侧最大高度:
* 使用一个`rightmax`数组来存储每个位置右侧的最大高度。
* `rightmax[size-1]`自然是`height[size-1]`,因为最后一个柱子右侧没有柱子。
* 从`i = size-2`开始反向遍历,`rightmax[i]`是`rightmax[i+1]`和`height[i]`中的较大值。
- 计算接到的雨水:
* 遍历`height`数组,对于每个位置`i`,可以接到的雨水是`min(leftmax[i], rightmax[i]) - height[i]`。
* 这是因为雨水的高度取决于左侧和右侧的最大高度中的较小值,但不得超过该位置的实际高度。
* 将所有位置可以接到的雨水加起来,得到最终答案。
而单调栈的做法是存储下标
题目代码
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
int trap(vector<int>& height) {
int size = height.size();
if (size == 0)
return 0;
vector<int> leftmax(size);
leftmax[0] = height[0];
for (int i = 1; i < size; i++)
{
leftmax[i] = max(leftmax[i - 1], height[i]);
}
vector<int> rightmax(size);
rightmax[size - 1] = height[size - 1];
for (int i = size - 2; i >= 0; i--)
{
rightmax[i] = max(rightmax[i + 1], height[i]);
}
int ans = 0;
for (int i = 0; i < size; i++)
{
ans += min(leftmax[i], rightmax[i]) - height[i];
}
return ans;
}
};
class Solution {
public:
int trap(vector<int>& height) {
int n = height.size();
stack<int> stk;
int ans = 0;
for (int right = 0; right < n; right++)
{
while (!stk.empty() && height[right] >= height[stk.top()])
{
int top = stk.top();
stk.pop();
if (stk.empty()) break;
int left = stk.top();
int currentwidth = right - left - 1;
int currentheight = min(height[right], height[left]) - height[top];
ans += currentheight * currentwidth;
}
stk.push(right);
}
return ans;
}
};
示例 1:
输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。
示例 2:
输入:height = [4,2,0,3,2,5]
输出:9
判断子序列
题目描述
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"
是"abcde"
的一个子序列,而"aec"
不是)。
进阶:
如果有大量输入的 S,称作 S1, S2, … , Sk 其中 k >= 10亿,你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码?
题目分析
这里需要的数据就是匹配到某一点时 待匹配的字符在长字符串中 下一次 出现的位置。
所以我们前期多做一点工作,将长字符串研究透彻,假如长字符串的长度为 n
,建立一个 n∗26
大小的矩阵,表示每个位置上26
个字符下一次出现的位置。
对于要匹配的短字符串,遍历每一个字符,不断地寻找该字符在长字符串中的位置,然后将位置更新,寻找下一个字符,相当于在长字符串上“跳跃”。
如果下一个位置为 -1
,表示长字符串再没有该字符了,返回 false 即可。
如果能正常遍历完毕,则表示可行,返回 true
对于 "abc"
在 “ahbgdc” 上匹配的时候,由于长字符串第一个 a 的下一个出现 a 的位置为 -1(不出现),会导致一个 bug。
所以在生成数组时在长字符串前插入一个空字符即可。
题目代码
//动态规划
class Solution {
public:
bool isSubsequence(string s, string t) {
t.insert(t.begin(), ' ');
int len1 = s.size();
int len2 = t.size();
vector<vector<int>> dp(len2, vector<int>(26, 0));
for (char ch = 'a'; ch <= 'z'; ch++)
{
int nextPos = -1; //表示接下来不会出现该字符
for (int i = len2 - 1; i >= 0; i--)
{
dp[i][ch - 'a'] = nextPos;
if (t[i] == ch)
nextPos = i;
}
}
int index = 0;
for (char c : s)
{
index = dp[index][c - 'a'];
if (index == -1)
return false;
}
return true;
}
};
示例 1:
输入:s = "abc", t = "ahbgdc"
输出:true
示例 2:
输入:s = "axc", t = "ahbgdc"
输出:false
数据结构应用题
O(1) 时间插入、删除和获取随机元素
题目描述
实现RandomizedSet
类:
RandomizedSet()
初始化RandomizedSet
对象bool insert(int val)
当元素val
不存在时,向集合中插入该项,并返回true
;否则,返回false
。bool remove(int val)
当元素val
存在时,从集合中移除该项,并返回true
;否则,返回false
。int getRandom()
随机返回现有集合中的一项(测试用例保证调用此方法时集合中至少存在一个元素)。每个元素应该有 相同的概率 被返回。
你必须实现类的所有函数,并满足每个函数的 平均 时间复杂度为 O(1)
。
题目分析
- 构造函数(RandomizedSet):
* `srand((unsigned)time(NULL));`:这是C++中设置随机数种子的常用方法。它使用当前时间(以秒为单位)作为种子,使得每次程序运行时生成的随机数序列都是不同的。
- 插入操作(insert):
* `if (indices.count(val)) { return false; }`:检查值`val`是否已经在集合中(通过检查它是否存在于`indices`这个哈希表中)。如果存在,返回`false`表示插入失败。
* `int index = nums.size();`:获取当前`nums`数组的大小(也就是下一个要插入的位置的索引)。
* `nums.emplace_back(val);`:在`nums`数组的末尾插入值`val`。
* `indices[val] = index;`:在`indices`哈希表中为值`val`记录其索引。
* 返回`true`表示插入成功。
- 删除操作(remove):
* `if (!indices.count(val)) { return false; }`:检查值`val`是否存在于集合中。如果不存在,返回`false`表示删除失败。
* `int index = indices[val];`:获取值`val`在`nums`数组中的索引。
* `int last = nums.back();`:获取`nums`数组的最后一个元素的值。
* `nums[index] = last;`:将`nums`数组的最后一个元素的值复制到要删除的元素的位置(即`val`的位置)。
* `indices[last] = index;`:更新`indices`哈希表,将最后一个元素的值`last`的索引更新为之前`val`的索引。
* `nums.pop_back();`:从`nums`数组中删除最后一个元素(现在它的值已经被复制到`val`的位置了)。
* `indices.erase(val);`:从`indices`哈希表中删除值`val`的索引。
* 返回`true`表示删除成功。
- 随机选择操作(getRandom):
* `int randomIndex = rand()%nums.size();`:生成一个介于0(包含)和`nums.size()`(不包含)之间的随机索引。
* `return nums[randomIndex];`:返回`nums`数组中对应随机索引的元素值。
题目代码
#include <iostream>
#include <vector>
#include <unordered_map>
using namespace std;
class RandomizedSet {
public:
RandomizedSet() {
srand((unsigned)time(NULL));
}
bool insert(int val) {
if (indices.count(val))
return false;
int index = nums.size();
nums.emplace_back(val);
indices[val] = index;
return true;
}
bool remove(int val) {
if (!indices.count(val))
return false;
int index = indices[val];
int last = nums.back();
nums[index] = last;
indices[last] = index;
nums.pop_back();
indices.erase(val);
return true;
}
int getRandom() {
int randomIndex = rand() % nums.size();
return nums[randomIndex];
}
private:
vector<int> nums;
unordered_map<int, int> indices;
};
/**
* Your RandomizedSet object will be instantiated and called as such:
* RandomizedSet* obj = new RandomizedSet();
* bool param_1 = obj->insert(val);
* bool param_2 = obj->remove(val);
* int param_3 = obj->getRandom();
*/
示例:
输入
["RandomizedSet", "insert", "remove", "insert", "getRandom", "remove", "insert", "getRandom"]
[[], [1], [2], [2], [], [1], [2], []]
输出
[null, true, false, true, 2, true, false, 2]
解释
RandomizedSet randomizedSet = new RandomizedSet();
randomizedSet.insert(1); // 向集合中插入 1 。返回 true 表示 1 被成功地插入。
randomizedSet.remove(2); // 返回 false ,表示集合中不存在 2 。
randomizedSet.insert(2); // 向集合中插入 2 。返回 true 。集合现在包含 [1,2] 。
randomizedSet.getRandom(); // getRandom 应随机返回 1 或 2 。
randomizedSet.remove(1); // 从集合中移除 1 ,返回 true 。集合现在包含 [2] 。
randomizedSet.insert(2); // 2 已在集合中,所以返回 false 。
randomizedSet.getRandom(); // 由于 2 是集合中唯一的数字,getRandom 总是返回 2
LRU缓存
题目描述
请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。
实现 LRUCache
类:
LRUCache(int capacity)
以 正整数 作为容量capacity
初始化 LRU 缓存int get(int key)
如果关键字key
存在于缓存中,则返回关键字的值,否则返回-1
。void put(int key, int value)
如果关键字key
已经存在,则变更其数据值value
;如果不存在,则向缓存中插入该组key-value
。如果插入操作导致关键字数量超过capacity
,则应该 逐出 最久未使用的关键字。
函数 get
和 put
必须以 O(1)
的平均时间复杂度运行。
题目分析
首先我们要维护一个哈希表和一个双向链表,哈希表中存放值与结构体指针,然后我们使用头插法去插入未存储的键值,如果插入的值大于容量,则移除尾节点的值。
题目代码
#include <iostream>
#include <vector>
#include <unordered_map>
using namespace std;
struct DLinkedNode
{
int key, value;
DLinkedNode* prev;
DLinkedNode* next;
DLinkedNode() : key(0), value(0), prev(nullptr), next(nullptr) {}
DLinkedNode(int _key, int _value) :key(_key), value(_value), prev(nullptr), next(nullptr) {};
};
class LRUCache {
private:
unordered_map<int, DLinkedNode*> cache;
DLinkedNode* head;
DLinkedNode* tail;
int size;
int capacity;
public:
LRUCache(int _capacity) : capacity(_capacity), size(0) {
head = new DLinkedNode();
tail = new DLinkedNode();
head->next = tail;
tail->prev = head;
}
int get(int key) {
if (!cache.count(key))
{
return -1;
}
DLinkedNode* node = cache[key];
moveToHead(node);
return node->value;
}
void put(int key, int value) {
if (!cache.count(key))
{
DLinkedNode* node = new DLinkedNode(key, value);
cache[key] = node;
addToHead(node);
++size;
if (size > capacity)
{
DLinkedNode* removed = removeTail();
cache.erase(removed->key);
delete removed;
--size;
}
}
else
{
DLinkedNode* node = cache[key];
node->value = value;
moveToHead(node);
}
}
void addToHead(DLinkedNode* node)
{
node->prev = head;
node->next = head->next;
head->next->prev = node;
head->next = node;
}
void removeNode(DLinkedNode* node)
{
node->prev->next = node->next;
node->next->prev = node->prev;
}
void moveToHead(DLinkedNode* node)
{
removeNode(node);
addToHead(node);
}
DLinkedNode* removeTail() {
DLinkedNode* node = tail->prev;
removeNode(node);
return node;
}
};
示例:
输入
["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出
[null, null, null, 1, null, -1, null, -1, 3, 4]
解释
LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
lRUCache.get(1); // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lRUCache.get(2); // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lRUCache.get(1); // 返回 -1 (未找到)
lRUCache.get(3); // 返回 3
lRUCache.get(4); // 返回 4
二叉搜索树迭代器
题目描述
实现一个二叉搜索树迭代器类BSTIterator
,表示一个按中序遍历二叉搜索树(BST)的迭代器:
BSTIterator(TreeNode root)
初始化BSTIterator
类的一个对象。BST 的根节点root
会作为构造函数的一部分给出。指针应初始化为一个不存在于 BST 中的数字,且该数字小于 BST 中的任何元素。boolean hasNext()
如果向指针右侧遍历存在数字,则返回true
;否则返回false
。int next()
将指针向右移动,然后返回指针处的数字。
注意,指针初始化为一个不存在于 BST 中的数字,所以对 next()
的首次调用将返回 BST 中的最小元素。
你可以假设 next()
调用总是有效的,也就是说,当调用 next()
时,BST 的中序遍历中至少存在一个下一个数字。
题目分析
相当于使用中序遍历将值存储起来
使用索引来确认值是否存在
题目代码
#include <iostream>
#include <vector>
using namespace std;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
class BSTIterator {
private:
vector<int> arr;
int idx;
void inorder(TreeNode* root, vector<int>& res) {
if (!root) {
return;
}
inorder(root->left, res);
res.push_back(root->val);
inorder(root->right, res);
}
vector<int> inorderTraversal(TreeNode* root) {
vector<int> res;
inorder(root, res);
return res;
}
public:
BSTIterator(TreeNode* root) : idx(0), arr(inorderTraversal(root)) {}
int next() {
return arr[idx++];
}
bool hasNext() {
return (idx < arr.size());
}
};
/**
* Your BSTIterator object will be instantiated and called as such:
* BSTIterator* obj = new BSTIterator(root);
* int param_1 = obj->next();
* bool param_2 = obj->hasNext();
*/
示例:
输入
["BSTIterator", "next", "next", "hasNext", "next", "hasNext", "next", "hasNext", "next", "hasNext"]
[[[7, 3, 15, null, null, 9, 20]], [], [], [], [], [], [], [], [], []]
输出
[null, 3, 7, true, 9, true, 15, true, 20, false]
解释
BSTIterator bSTIterator = new BSTIterator([7, 3, 15, null, null, 9, 20]);
bSTIterator.next(); // 返回 3
bSTIterator.next(); // 返回 7
bSTIterator.hasNext(); // 返回 True
bSTIterator.next(); // 返回 9
bSTIterator.hasNext(); // 返回 True
bSTIterator.next(); // 返回 15
bSTIterator.hasNext(); // 返回 True
bSTIterator.next(); // 返回 20
bSTIterator.hasNext(); // 返回 False
实现Trie(前缀树)
题目描述
Trie(发音类似 “try”)或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。
请你实现 Trie 类:
Trie()
初始化前缀树对象。void insert(String word)
向前缀树中插入字符串word
。boolean search(String word)
如果字符串word
在前缀树中,返回true
(即,在检索之前已经插入);否则,返回false
。boolean startsWith(String prefix)
如果之前已经插入的字符串word
的前缀之一为prefix
,返回true
;否则,返回false
。
题目分析
题目代码
class Trie {
private:
vector<Trie*> children;
bool isEnd;
Trie* searchPrefix(string prefix) {
Trie* node = this;
for (char ch : prefix) {
ch -= 'a';
if (node->children[ch] == nullptr) {
return nullptr;
}
node = node->children[ch];
}
return node;
}
public:
Trie() : children(26), isEnd(false) {}
void insert(string word) {
Trie* node = this;
for (char ch : word) {
ch -= 'a';
if (node->children[ch] == nullptr) {
node->children[ch] = new Trie();
}
node = node->children[ch];
}
node->isEnd = true;
}
bool search(string word) {
Trie* node = this->searchPrefix(word);
return node != nullptr && node->isEnd;
}
bool startsWith(string prefix) {
return this->searchPrefix(prefix) != nullptr;
}
};
示例:
输入
["Trie", "insert", "search", "search", "startsWith", "insert", "search"]
[[], ["apple"], ["apple"], ["app"], ["app"], ["app"], ["app"]]
输出
[null, null, true, false, true, null, true]
解释
Trie trie = new Trie();
trie.insert("apple");
trie.search("apple"); // 返回 True
trie.search("app"); // 返回 False
trie.startsWith("app"); // 返回 True
trie.insert("app");
trie.search("app"); // 返回 True
数组
除自身以外数组的乘积
题目描述
给你一个整数数组 nums
,返回 数组 answer
,其中 answer[i]
等于 nums
中除 nums[i]
之外其余各元素的乘积 。
题目数据 保证 数组 nums
之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。
请 不要使用除法,且在 O(*n*)
时间复杂度内完成此题。
题目分析
一部分用来存储前缀,一部分用来存储后缀,思路很简单
题目代码
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
vector<int> productExceptSelf(vector<int>& nums) {
int nums_size = nums.size();
vector<int> prefix(nums_size + 1);
vector<int> lastfix(nums_size + 1);
vector<int> answer(nums_size);
prefix[0] = 1;
lastfix[nums_size - 1] = 1;
for(int i=1;i<nums_size;i++)
{
prefix[i] = nums[i - 1] * prefix[i - 1];
lastfix[nums_size - i -1] = nums[nums_size - i ] * lastfix[nums_size - i];
}
for(int i=0;i<nums_size;i++)
{
answer[i] = prefix[i] * lastfix[i];
}
return answer;
}
};
示例 1:
输入: nums = [1,2,3,4]
输出: [24,12,8,6]
示例 2:
输入: nums = [-1,1,0,-3,3]
输出: [0,0,9,0,0]
和为K的子数组
题目描述
给你一个整数数组 nums
和一个整数 k
,请你统计并返回 该数组中和为 k
的子数组的个数 。
子数组是数组中元素的连续非空序列。
题目分析
题目代码
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
int n = nums.size();
vector<int> s(n + 1);
for (int i = 0; i < n; i++) {
s[i + 1] = s[i] + nums[i];
}
int ans = 0;
unordered_map<int, int> cnt;
for (int sj : s) {
// 注意不要直接 += cnt[sj-k],如果 sj-k 不存在,会插入 sj-k
ans += cnt.contains(sj - k) ? cnt[sj - k] : 0;
cnt[sj]++;
}
return ans;
}
};
示例 1:
输入:nums = [1,1,1], k = 2
输出:2
示例 2:
输入:nums = [1,2,3], k = 3
输出:2
区域和检索 – 数组不可变
题目描述
给定一个整数数组 nums
,处理以下类型的多个查询:
- 计算索引
left
和right
(包含left
和right
)之间的nums
元素的 和 ,其中left <= right
实现 NumArray
类:
NumArray(int[] nums)
使用数组nums
初始化对象int sumRange(int i, int j)
返回数组nums
中索引left
和right
之间的元素的 总和 ,包含left
和right
两点(也就是nums[left] + nums[left + 1] + ... + nums[right]
)
题目分析
问:为什么要定义 s[0]=0,这样做有什么好处?
答:如果 left=0,要计算的子数组是一个前缀(从 a[0] 到 a[right]),我们要用 s[right+1] 减去 s[0]。如果不定义 s[0]=0,就必须特判 left=0 的情况了(读者可以试试)。通过定义 s[0]=0,任意子数组(包括前缀)都可以表示为两个前缀和的差。此外,如果 a 是空数组,定义 s[0]=0 的写法是可以兼容这种情况的。
题目代码
#include <vector>
using namespace std;
class NumArray {
private:
vector<int> ans;
public:
NumArray(vector<int>& nums) {
int n = nums.size();
ans.resize(n + 1);
ans[0] = 0;
for (int i = 0; i < n; i++)
{
ans[i + 1] += ans[i] + nums[i];
}
}
int sumRange(int left, int right) {
return ans[right + 1] - ans[left];
}
};
/**
* Your NumArray object will be instantiated and called as such:
* NumArray* obj = new NumArray(nums);
* int param_1 = obj->sumRange(left,right);
*/
示例 1:
输入:
["NumArray", "sumRange", "sumRange", "sumRange"]
[[[-2, 0, 3, -5, 2, -1]], [0, 2], [2, 5], [0, 5]]
输出:
[null, 1, -1, -3]
解释:
NumArray numArray = new NumArray([-2, 0, 3, -5, 2, -1]);
numArray.sumRange(0, 2); // return 1 ((-2) + 0 + 3)
numArray.sumRange(2, 5); // return -1 (3 + (-5) + 2 + (-1))
numArray.sumRange(0, 5); // return -3 ((-2) + 0 + 3 + (-5) + 2 + (-1))
字符串
最后一个单词的长度
题目描述
给你一个字符串 s
,由若干单词组成,单词前后用一些空格字符隔开。返回字符串中 最后一个 单词的长度。
单词 是指仅由字母组成、不包含任何空格字符的最大子字符串。
题目分析
从字符串的末尾进行反向遍历,当存储的str为非空,且当前的s[i]为空,代表着遍历结束,直接输出s的大小
题目代码
#include <iostream>
#include <string>
#include <vector>
using namespace std;
class Solution {
public:
int lengthOfLastWord(string s) {
vector<char> str;
int string_size = s.size();
for (int i = string_size - 1; i >= 0; i--) {
if (s[i] == ' ') {
if (!str.empty()) {
return str.size();
}
}
else {
str.push_back(s[i]);
}
}
return str.size();
}
};
示例 1:
输入:s = "Hello World"
输出:5
解释:最后一个单词是“World”,长度为 5。
示例 2:
输入:s = " fly me to the moon "
输出:4
解释:最后一个单词是“moon”,长度为 4。
示例 3:
输入:s = "luffy is still joyboy"
输出:6
解释:最后一个单词是长度为 6 的“joyboy”。
最长公众前缀
题目描述
编写一个函数来查找字符串数组中的最长公共前缀。
如果不存在公共前缀,返回空字符串 ""
。
题目分析
横向扫描
依次遍历字符串数组中的每个字符串,对于每个遍历到的字符串,更新最长公共前缀,当遍历完所有的字符串以后,即可得到字符串数组中的最长公共前缀。
如果在尚未遍历完所有的字符串时,最长公共前缀已经是空串,则最长公共前缀一定是空串,因此不需要继续遍历剩下的字符串,直接返回空串即可。
纵向扫描
纵向扫描时,从前往后遍历所有字符串的每一列,比较相同列上的字符是否相同,如果相同则继续对下一列进行比较,如果不相同则当前列不再属于公共前缀,当前列之前的部分为最长公共前缀。
题目代码
#include <iostream>
#include <vector>
#include <string>
using namespace std;
//横向扫描
class Solution {
public:
string longestCommonPrefix(vector<string>& strs) {
int strs_size = strs.size();
string long_prefix = strs[0];
if (!strs_size)
return "";
for (int i = 1; i < strs_size; i++)
{
long_prefix = longestCommonPrefix(long_prefix, strs[i]);
if (!long_prefix.size())
break;
}
return long_prefix;
}
string longestCommonPrefix(const string& str1, const string& str2)
{
int length = min(str1.length(), str2.length());
int index = 0;
while (index < length && str1[index] == str2[index])
{
++index;
}
return str1.substr(0, index);
}
};
//纵向扫描
class Solution {
public:
string longestCommonPrefix(vector<string>& strs) {
int size = strs.size();
if (!size)
{
return "";
}
string prefix = strs[0];
for (int charnum = 0; charnum < prefix.size(); charnum++)
{
char a = strs[0][charnum];
for (int strnum = 1; strnum < size; strnum++)
{
if (a != strs[strnum][charnum] || charnum == strs[strnum].size())
return strs[0].substr(0, charnum);
}
}
return strs[0];
}
};
示例 1:
输入:strs = ["flower","flow","flight"]
输出:"fl"
示例 2:
输入:strs = ["dog","racecar","car"]
输出:""
解释:输入不存在公共前缀。
反转字符串中的单词
题目描述
给你一个字符串 s
,请你反转字符串中 单词 的顺序。
单词 是由非空格字符组成的字符串。s
中使用至少一个空格将字符串中的 单词 分隔开。
返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。
注意:输入字符串 s
中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格。
题目分析
- 去除字符串头尾的空格。
- 分割字符串中的单词。
- 反转单词的顺序。
- 将反转后的单词连接成一个新的字符串。
题目代码
#include <iostream>
#include <string>
#include <vector>
#include <sstream>
using namespace std;
class Solution {
public:
string reverseWords(string s) {
stringstream ss(s);
vector<string> words;
string word;
// 使用 stringstream 分割单词
while (ss >> word) {
words.push_back(word);
}
// 反转单词顺序并连接成字符串
string reversed;
for (int i = words.size() - 1; i >= 0; i--) {
reversed += words[i];
if (i != 0) {
reversed += " ";
}
}
return reversed;
}
};
示例 1:
输入:s = "the sky is blue"
输出:"blue is sky the"
示例 2:
输入:s = " hello world "
输出:"world hello"
解释:反转后的字符串中不能存在前导空格和尾随空格。
示例 3:
输入:s = "a good example"
输出:"example good a"
解释:如果两个单词间有多余的空格,反转后的字符串需要将单词间的空格减少到仅有一个。
Z字形变换
题目描述
将一个给定字符串 s
根据给定的行数 numRows
,以从上往下、从左到右进行 Z 字形排列。
比如输入字符串为 "PAYPALISHIRING"
行数为 3
时,排列如下:
P A H N
A P L S I I G
Y I R
之后,你的输出需要从左往右逐行读取,产生出一个新的字符串,比如:"PAHNAPLSIIGYIR"
。
请你实现这个将字符串进行指定行数变换的函数:
string convert(string s, int numRows);
题目分析
当我们在矩阵上填写字符时,会向下填写 r 个字符,然后向右上继续填写 r−2 个字符,最后回到第一行,因此 Z 字形变换的周期 t=r+r−2=2r−2,每个周期会占用矩阵上的 1+r−2=r−1列。
下方的代码更加简单易懂,利用行转向标志控制行的位置
题目代码
#include <iostream>
#include <vector>
#include <string>
using namespace std;
class Solution {
public:
string convert(string s, int numRows) {
if (numRows == 1)
return s;
vector<string> rows(numRows);
//行转向标志
int flag = 1;
//行下标索引
int idxRows = 0;
for (int i = 0; i < s.size(); i++)
{
rows[idxRows].push_back(s[i]);
//更新行下标
idxRows += flag;
if (idxRows == numRows - 1 || idxRows == 0)
{
flag = -flag;
}
}
string res;
for (auto& row : rows)
{
res += row;
}
return res;
}
};
示例 1:
输入:s = "PAYPALISHIRING", numRows = 3
输出:"PAHNAPLSIIGYIR"
示例 2:
输入:s = "PAYPALISHIRING", numRows = 4
输出:"PINALSIGYAHRPI"
解释:
P I N
A L S I G
Y A H R
P I
示例 3:
输入:s = "A", numRows = 1
输出:"A"
找出字符串中第一个匹配项的下标
题目描述
给你两个字符串 haystack
和 needle
,请你在 haystack
字符串中找出 needle
字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle
不是 haystack
的一部分,则返回 -1
。
题目分析
请见KMP算法
题目代码
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
int strStr(string haystack, string needle) {
int m = haystack.size();
int n = needle.size();
if (n == 0) return 0;
haystack.insert(haystack.begin(), ' ');
needle.insert(needle.begin(), ' ');
vector<int> next(n + 1);
for (int i = 2, j = 0; i < n; i++)
{
while (j && needle[i] != needle[j + 1])
{
j = next[j];
}
if (needle[i] == needle[j + 1])
{
j++;
}
next[i] = j;
}
for (int i = 1, j = 0; i < m ; i++)
{
while (j && haystack[i] != needle[j + 1])
{
j = next[j];
}
if (haystack[i] == needle[j + 1])
{
j++;
}
if (j == n) return i - n;
}
return -1;
}
};
示例 1:
输入:haystack = "sadbutsad", needle = "sad"
输出:0
解释:"sad" 在下标 0 和 6 处匹配。
第一个匹配项的下标是 0 ,所以返回 0 。
示例 2:
输入:haystack = "leetcode", needle = "leeto"
输出:-1
解释:"leeto" 没有在 "leetcode" 中出现,所以返回 -1 。
文本左右对齐
题目描述
给定一个单词数组 words
和一个长度 maxWidth
,重新排版单词,使其成为每行恰好有 maxWidth
个字符,且左右两端对齐的文本。
你应该使用 “贪心算法” 来放置给定的单词;也就是说,尽可能多地往每行中放置单词。必要时可用空格 ' '
填充,使得每行恰好有 maxWidth 个字符。
要求尽可能均匀分配单词间的空格数量。如果某一行单词间的空格不能均匀分配,则左侧放置的空格数要多于右侧的空格数。
文本的最后一行应为左对齐,且单词之间不插入额外的空格。
注意:
- 单词是指由非空格字符组成的字符序列。
- 每个单词的长度大于 0,小于等于 maxWidth。
- 输入单词数组
words
至少包含一个单词。
题目描述
根据题干描述的贪心算法,对于每一行,我们首先确定最多可以放置多少单词,这样可以得到该行的空格个数,从而确定该行单词之间的空格个数。
根据题目中填充空格的细节,我们分以下三种情况讨论:
- 当前行是最后一行:单词左对齐,且单词之间应只有一个空格,在行末填充剩余空格;
- 当前行不是最后一行,且只有一个单词:该单词左对齐,在行末填充空格;
- 当前行不是最后一行,且不只一个单词:设当前行单词数为 numWords,空格数为 numSpaces,我们需要将空格均匀分配在单词之间
题目代码
#include <iostream>
#include <vector>
#include <string>
using namespace std;
class Solution {
// blank 返回长度为 n 的由空格组成的字符串
string blank(int n) {
return string(n, ' ');
}
// join 返回用 sep 拼接 [left, right) 范围内的 words 组成的字符串
string join(vector<string>& words, int left, int right, string sep) {
string s = words[left];
for (int i = left + 1; i < right; ++i) {
s += sep + words[i];
}
return s;
}
public:
vector<string> fullJustify(vector<string>& words, int maxWidth) {
vector<string> ans;
int right = 0, n = words.size();
while (true) {
int left = right; // 当前行的第一个单词在 words 的位置
int sumLen = 0; // 统计这一行单词长度之和
// 循环确定当前行可以放多少单词,注意单词之间应至少有一个空格
while (right < n && sumLen + words[right].length() + right - left <= maxWidth) {
sumLen += words[right++].length();
}
// 当前行是最后一行:单词左对齐,且单词之间应只有一个空格,在行末填充剩余空格
if (right == n) {
string s = join(words, left, n, " ");
ans.emplace_back(s + blank(maxWidth - s.length()));
return ans;
}
int numWords = right - left;
int numSpaces = maxWidth - sumLen;
// 当前行只有一个单词:该单词左对齐,在行末填充剩余空格
if (numWords == 1) {
ans.emplace_back(words[left] + blank(numSpaces));
continue;
}
// 当前行不只一个单词
int avgSpaces = numSpaces / (numWords - 1);
int extraSpaces = numSpaces % (numWords - 1);
string s1 = join(words, left, left + extraSpaces + 1, blank(avgSpaces + 1)); // 拼接额外加一个空格的单词
string s2 = join(words, left + extraSpaces + 1, right, blank(avgSpaces)); // 拼接其余单词
ans.emplace_back(s1 + blank(avgSpaces) + s2);
}
}
};
示例 1:
输入: words = ["This", "is", "an", "example", "of", "text", "justification."], maxWidth = 16
输出:
[
"This is an",
"example of text",
"justification. "
]
示例 2:
输入:words = ["What","must","be","acknowledgment","shall","be"], maxWidth = 16
输出:
[
"What must be",
"acknowledgment ",
"shall be "
]
解释: 注意最后一行的格式应为 "shall be " 而不是 "shall be",
因为最后一行应为左对齐,而不是左右两端对齐。
第二行同样为左对齐,这是因为这行只包含一个单词。
示例 3:
输入:words = ["Science","is","what","we","understand","well","enough","to","explain","to","a","computer.","Art","is","everything","else","we","do"],maxWidth = 20
输出:
[
"Science is what we",
"understand well",
"enough to explain to",
"a computer. Art is",
"everything else we",
"do "
]
清除数字
题目描述
给你一个字符串 s
。
你的任务是重复以下操作删除 所有 数字字符:
- 删除 第一个数字字符 以及它左边 最近 的 非数字 字符。
请你返回删除所有数字字符以后剩下的字符串。
题目分析
string竟然有pop_back() 涨知识了
题目代码
class Solution {
public:
string clearDigits(string s) {
string st;
for (char c : s) {
if (isdigit(c)) {
st.pop_back();
} else {
st += c;
}
}
return st;
}
};
示例 1:
输入:s = “abc”
输出:“abc”
解释:
字符串中没有数字。
示例 2:
输入:s = “cb34”
输出:“”
解释:
一开始,我们对 s[2]
执行操作,s
变为 "c4"
。
然后对 s[1]
执行操作,s
变为 ""
。
滑动窗口
谨记: 数组不是单调的话,不要用滑动窗口,考虑用前缀和
长度最小的子数组
题目描述
给定一个含有 n
个正整数的数组和一个正整数 target
。
找出该数组中满足其总和大于等于 target
的长度最小的子数组
[numsl, numsl+1, ..., numsr-1, numsr]
,并返回其长度。如果不存在符合条件的子数组,返回 0
。
题目分析
定义两个指针 start
和 end
分别表示子数组(滑动窗口窗口)的开始位置和结束位置,维护变量 sum 存储子数组中的元素和(即从 nums[start]
到 nums[end]
的元素和)。
初始状态下,start
和 end
都指向下标 0,sum
的值为 0。
每一轮迭代,将 nums[end]
加到 sum
,如果 sum≥target
,则更新子数组的最小长度(此时子数组的长度是 end−start+1
),然后将 nums[start]
从 sum
中减去并将 start
右移,直到 sum<s
,在此过程中同样更新子数组的最小长度。在每一轮迭代的最后,将 end
右移。
题目代码
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int n = nums.size();
if (n == 0)
{
return 0;
}
int ans = INT_MAX;
int start = 0, end = 0;
int sum = 0;
while (end < n)
{
sum += nums[end];
while (sum >= target)
{
ans = min(ans, end - start + 1);
sum -= nums[start];
start++;
}
end++;
}
return ans == INT_MAX ? 0 : ans;
}
};
示例 1:
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
示例 2:
输入:target = 4, nums = [1,4,4]
输出:1
示例 3:
输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0
无重复字符的最长子串
题目描述
给定一个字符串 s
,请你找出其中不含有重复字符的 最长 子串 的长度。
题目分析
当遇到重复的字符时,会清空整个哈希表 sign
并重新填充,这会导致大量的无效操作。而在这个解法中,只会在遇到重复字符时,从哈希集合 occ
中移除左指针 i
所指向的字符,并且右指针 rk
会持续向右移动,直到遇到重复字符为止。
题目代码
class Solution {
public:
int lengthOfLongestSubstring(string s) {
// 哈希集合,记录每个字符是否出现过
unordered_set<char> occ;
int n = s.size();
// 右指针,初始值为 -1,相当于我们在字符串的左边界的左侧,还没有开始移动
int rk = -1, ans = 0;
// 枚举左指针的位置,初始值隐性地表示为 -1
for (int i = 0; i < n; ++i) {
if (i != 0) {
// 左指针向右移动一格,移除一个字符
occ.erase(s[i - 1]);
}
while (rk + 1 < n && !occ.count(s[rk + 1])) {
// 不断地移动右指针
occ.insert(s[rk + 1]);
++rk;
}
// 第 i 到 rk 个字符是一个极长的无重复字符子串
ans = max(ans, rk - i + 1);
}
return ans;
}
};
//灵神写的
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int n = s.length(), ans = 0, left = 0;
unordered_set<char> window; // 维护从下标 left 到下标 right 的字符
for (int right = 0; right < n; right++) {
char c = s[right];
// 如果窗口内已经包含 c,那么再加入一个 c 会导致窗口内有重复元素
// 所以要在加入 c 之前,先移出窗口内的 c
while (window.count(c)) { // 窗口内有 c
window.erase(s[left++]); // 缩小窗口
}
window.insert(c); // 加入 c
ans = max(ans, right - left + 1); // 更新窗口长度最大值
}
return ans;
}
};
示例 1:
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:
输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
串联所有单词的子串
题目描述
给定一个字符串 s
和一个字符串数组 words
。 words
中所有字符串 长度相同。
s
中的 串联子串 是指一个包含 words
中所有字符串以任意顺序排列连接起来的子串。
- 例如,如果
words = ["ab","cd","ef"]
, 那么"abcdef"
,"abefcd"
,"cdabef"
,"cdefab"
,"efabcd"
, 和"efcdab"
都是串联子串。"acdbef"
不是串联子串,因为他不是任何words
排列的连接。
返回所有串联子串在 s
中的开始索引。你可以以 任意顺序 返回答案。
题目分析
首先先判断words是否为空,如果为空则直接返回答案
接下来我们使用滑动窗口的方式来解决此题,由于words数组中各个单词的长度都相同,因此我们可以选择其中一个单词的长度作为滑动的长度.
其中n是字符串s 的长度,m是words中单词的数量,w是单词的长度
首先遍历统计words中单词出现的数量
接着进行两次遍历
外层循环以 单词的长度为界限,内层遍历以 字符串的长度为界限
题目代码
#include <iostream>
#include <string>
#include <vector>
#include <unordered_map>
using namespace std;
class Solution {
public:
vector<int> findSubstring(string s, vector<string>& words) {
vector<int> ans;
if (words.empty())
return ans;
int n = s.length(), m = words.size(), w = words[0].length();
unordered_map<string, int> total;
for (int i = 0; i < words.size(); i++)
{
total[words[i]]++;
}
for (int i = 0; i < w; i++)
{
unordered_map<string, int> window;
int cnt = 0;
for (int j = i; j + w <= n; j += w)
{
if (j - i >= m * w)
{
string word = s.substr(j - m * w, w);
window[word]--;
if (window[word] < total[word])
{
cnt--;
}
}
string word = s.substr(j, w);
window[word]++;
if (window[word] <= total[word])
{
cnt++;
}
if (cnt == m)
{
ans.push_back(j - (m - 1) * w);
}
}
}
return ans;
}
};
示例 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"] 顺序排列的连接。
最小覆盖子串
题目描述
给你一个字符串 s
、一个字符串 t
。返回 s
中涵盖 t
所有字符的最小子串。如果 s
中不存在涵盖 t
所有字符的子串,则返回空字符串 ""
。
注意:
- 对于
t
中重复字符,我们寻找的子字符串中该字符数量必须不少于t
中该字符数量。 - 如果
s
中存在这样的子串,我们保证它是唯一的答案。
题目分析
1.先创建两个哈希表,用来统计两个字符串中字符出现的次数
2.再将要覆盖的字符串t,存入哈希表t_num
3.设置pos和minLen,用来记录子串的初始位置和最小长度(这步很关键,我们可以等循环结束再拷贝子串,而不是循环中,减少了很多时间开销)
4.其次设置count,用来记录s_num中的有效字符个数(这步优化,可以让我们不用每次都–比较两个哈希表,而是比较count是否达到t的有效字符个数)
5.接下来,是正常的滑动窗口四步走
题目代码
#include <iostream>
#include <string>
#include <unordered_map>
using namespace std;
class Solution {
public:
string minWindow(string s, string t) {
int n = s.length();
int m = t.length();
string ans;
if (n < m)
{
return "";
}
unordered_map<char, int> t_num,s_num;
for (auto word : t)
{
t_num[word]++;
}
int pos = 0, minlen = INT_MAX;
int left = 0, right = 0;
int count = 0;
while (right < n)
{
char in = s[right++];
if (++s_num[in] <= t_num[in])
{
++count;
}
while (count == m)
{
if (right - left < minlen)
{
pos = left;
minlen = right - left;
}
char out = s[left++];
if (s_num[out]-- <= t_num[out])
{
--count;
}
}
}
return minlen == INT_MAX ? "" : s.substr(pos, minlen);
}
};
//灵神的方法
class Solution {
bool is_covered(int cnt_s[], int cnt_t[]) {
for (int i = 'A'; i <= 'Z'; i++) {
if (cnt_s[i] < cnt_t[i]) {
return false;
}
}
for (int i = 'a'; i <= 'z'; i++) {
if (cnt_s[i] < cnt_t[i]) {
return false;
}
}
return true;
}
public:
string minWindow(string s, string t) {
int m = s.length();
int ans_left = -1, ans_right = m, left = 0;
int cnt_s[128]{}, cnt_t[128]{};
for (char c : t) {
cnt_t[c]++;
}
for (int right = 0; right < m; right++) { // 移动子串右端点
cnt_s[s[right]]++; // 右端点字母移入子串
while (is_covered(cnt_s, cnt_t)) { // 涵盖
if (right - left < ans_right - ans_left) { // 找到更短的子串
ans_left = left; // 记录此时的左右端点
ans_right = right;
}
cnt_s[s[left++]]--; // 左端点字母移出子串
}
}
return ans_left < 0 ? "" : s.substr(ans_left, ans_right - ans_left + 1);
}
};
示例 1:
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。
示例 2:
输入:s = "a", t = "a"
输出:"a"
解释:整个字符串 s 是最小覆盖子串。
示例 3:
输入: s = "a", t = "aa"
输出: ""
解释: t 中两个字符 'a' 均应包含在 s 的子串中,
因此没有符合条件的子字符串,返回空字符串。
找到字符串中的所有字母异位词
题目描述
给定两个字符串 s
和 p
,找到 s
中所有 p
的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。
题目分析
题目代码
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
int sLen = s.size(), pLen = p.size();
if (sLen < pLen) {
return vector<int>();
}
vector<int> ans;
vector<int> sCount(26);
vector<int> pCount(26);
for (int i = 0; i < pLen; ++i) {
++sCount[s[i] - 'a'];
++pCount[p[i] - 'a'];
}
if (sCount == pCount) {
ans.emplace_back(0);
}
for (int i = 0; i < sLen - pLen; ++i) {
--sCount[s[i] - 'a'];
++sCount[s[i + pLen] - 'a'];
if (sCount == pCount) {
ans.emplace_back(i + 1);
}
}
return ans;
}
};
示例 1:
输入: s = "cbaebabacd", p = "abc"
输出: [0,6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。
示例 2:
输入: s = "abab", p = "ab"
输出: [0,1,2]
解释:
起始索引等于 0 的子串是 "ab", 它是 "ab" 的异位词。
起始索引等于 1 的子串是 "ba", 它是 "ab" 的异位词。
起始索引等于 2 的子串是 "ab", 它是 "ab" 的异位词。
滑动窗口最大值
题目描述
给你一个整数数组 nums
,有一个大小为 k
的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k
个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。
题目分析
题目代码
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
vector<int> ans;
deque<int> q; // 双端队列
for (int i = 0; i < nums.size(); i++) {
// 1. 入
while (!q.empty() && nums[q.back()] <= nums[i]) {
q.pop_back(); // 维护 q 的单调性
}
q.push_back(i); // 入队
// 2. 出
if (i - q.front() >= k) { // 队首已经离开窗口了
q.pop_front();
}
// 3. 记录答案
if (i >= k - 1) {
// 由于队首到队尾单调递减,所以窗口最大值就是队首
ans.push_back(nums[q.front()]);
}
}
return ans;
}
};
示例 1:
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
--------------- -----
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
示例 2:
输入:nums = [1], k = 1
输出:[1]
矩阵
有效的数独
题目描述
请你判断一个 9 x 9
的数独是否有效。只需要 根据以下规则 ,验证已经填入的数字是否有效即可。
- 数字
1-9
在每一行只能出现一次。 - 数字
1-9
在每一列只能出现一次。 - 数字
1-9
在每一个以粗实线分隔的3x3
宫内只能出现一次。(请参考示例图)
注意:
- 一个有效的数独(部分已被填充)不一定是可解的。
- 只需要根据以上规则,验证已经填入的数字是否有效即可。
- 空白格用
'.'
表示。
题目分析
分别计算每一行,每一列,和每一小九宫格中,数字出现的次数
题目代码
#include <vector>
#include <iostream>
using namespace std;
class Solution {
public:
bool isValidSudoku(vector<vector<char>>& board) {
int rows[9][9];
int column[9][9];
int subboxes[3][3][9];
memset(rows, 0, sizeof(rows));
memset(column, 0, sizeof(column));
memset(subboxes, 0, sizeof(subboxes));
for (int i = 0; i < 9; i++)
{
for (int j = 0; j < 9; j++)
{
char c = board[i][j];
if (c != '.')
{
int index= c - '0' - 1;
rows[i][index]++;
column[j][index]++;
subboxes[i / 3][j / 3][index]++;
if (rows[i][index] > 1 || column[j][index] > 1 || subboxes[i / 3][j / 3][index] > 1)
return false;
}
}
}
return true;
}
};
示例 1:
输入:board =
[["5","3",".",".","7",".",".",".","."]
,["6",".",".","1","9","5",".",".","."]
,[".","9","8",".",".",".",".","6","."]
,["8",".",".",".","6",".",".",".","3"]
,["4",".",".","8",".","3",".",".","1"]
,["7",".",".",".","2",".",".",".","6"]
,[".","6",".",".",".",".","2","8","."]
,[".",".",".","4","1","9",".",".","5"]
,[".",".",".",".","8",".",".","7","9"]]
输出:true
示例 2:
输入:board =
[["8","3",".",".","7",".",".",".","."]
,["6",".",".","1","9","5",".",".","."]
,[".","9","8",".",".",".",".","6","."]
,["8",".",".",".","6",".",".",".","3"]
,["4",".",".","8",".","3",".",".","1"]
,["7",".",".",".","2",".",".",".","6"]
,[".","6",".",".",".",".","2","8","."]
,[".",".",".","4","1","9",".",".","5"]
,[".",".",".",".","8",".",".","7","9"]]
输出:false
解释:除了第一行的第一个数字从 5 改为 8 以外,空格内其他数字均与 示例1 相同。 但由于位于左上角的 3x3 宫内有两个 8 存在, 因此这个数独是无效的。
螺旋矩阵
题目描述
给你一个 m
行 n
列的矩阵 matrix
,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。
题目分析
思路很清晰,看代码即可
题目代码
#include <vector>
using namespace std;
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
vector<int> ans;
int up = 0;
int down = matrix.size() - 1;
int left = 0;
int right = matrix[0].size() - 1;
while (1)
{
for (int i = left; i <= right; i++)
{
ans.push_back(matrix[up][i]);
}
if (++up > down) break;
for (int i = up; i <= down; i++)
{
ans.push_back(matrix[i][right]);
}
if (--right < left) break;
for (int i = right; i >= left; i--)
{
ans.push_back(matrix[down][i]);
}
if (--down < up) break;
for (int i = down; i >= up; i--)
{
ans.push_back(matrix[i][left]);
}
if (++left > right) break;
}
return ans;
}
};
示例 1:
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,2,3,6,9,8,7,4,5]
示例 2:
输入:matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]
输出:[1,2,3,4,8,12,11,10,9,5,6,7]
旋转图像
题目描述
给定一个 n × n 的二维矩阵 matrix
表示一个图像。请你将图像顺时针旋转 90 度。
你必须在 原地旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。
题目分析
一个是循环四次,使得通过一次临时变量就可以实现全部的旋转
一个是先水平反转,然后再对角线翻转
题目代码
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
int n = matrix.size();
// 水平翻转
for (int i = 0; i < n / 2; ++i) {
for (int j = 0; j < n; ++j) {
swap(matrix[i][j], matrix[n - i - 1][j]);
}
}
// 主对角线翻转
for (int i = 0; i < n; ++i) {
for (int j = 0; j < i; ++j) {
swap(matrix[i][j], matrix[j][i]);
}
}
}
};
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
int n = matrix.size();
for (int i = 0; i < n / 2; ++i) {
for (int j = 0; j < (n + 1) / 2; ++j) {
int temp = matrix[i][j];
matrix[i][j] = matrix[n - j - 1][i];
matrix[n - j - 1][i] = matrix[n - i - 1][n - j - 1];
matrix[n - i - 1][n - j - 1] = matrix[j][n - i - 1];
matrix[j][n - i - 1] = temp;
}
}
}
};
示例 1:
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[[7,4,1],[8,5,2],[9,6,3]]
示例 2:
输入:matrix = [[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]]
输出:[[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]]
矩阵置零
题目描述
给定一个 *m* x *n*
的矩阵,如果一个元素为 0 ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。
题目分析
方法一 :将为0的元素进行标记,在第二次遍历进行修改
方法二
我们可以用矩阵的第一行和第一列代替方法一中的两个标记数组,以达到 O(1) 的额外空间。但这样会导致原数组的第一行和第一列被修改,无法记录它们是否原本包含 0。因此我们需要额外使用两个标记变量分别记录第一行和第一列是否原本包含 0。
在实际代码中,我们首先预处理出两个标记变量,接着使用其他行与列去处理第一行与第一列,然后反过来使用第一行与第一列去更新其他行与列,最后使用两个标记变量更新第一行与第一列即可。
题目代码
#include <vector>
using namespace std;
class Solution {
public:
void setZeroes(vector<vector<int>>& matrix) {
int m = matrix.size();
int n = matrix[0].size();
vector<int> row(m), col(n);
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (!matrix[i][j]) {
row[i] = col[j] = true;
}
}
}
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (row[i] || col[j]) {
matrix[i][j] = 0;
}
}
}
}
};
class Solution {
public:
void setZeroes(vector<vector<int>>& matrix) {
int m = matrix.size();
int n = matrix[0].size();
int flag_col0 = false, flag_row0 = false;
for (int i = 0; i < m; i++) {
if (!matrix[i][0]) {
flag_col0 = true;
}
}
for (int j = 0; j < n; j++) {
if (!matrix[0][j]) {
flag_row0 = true;
}
}
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
if (!matrix[i][j]) {
matrix[i][0] = matrix[0][j] = 0;
}
}
}
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
if (!matrix[i][0] || !matrix[0][j]) {
matrix[i][j] = 0;
}
}
}
if (flag_col0) {
for (int i = 0; i < m; i++) {
matrix[i][0] = 0;
}
}
if (flag_row0) {
for (int j = 0; j < n; j++) {
matrix[0][j] = 0;
}
}
}
};
示例 1:
输入:matrix = [[1,1,1],[1,0,1],[1,1,1]]
输出:[[1,0,1],[0,0,0],[1,0,1]]
示例 2:
输入:matrix = [[0,1,2,0],[3,4,5,2],[1,3,1,5]]
输出:[[0,0,0,0],[0,4,5,0],[0,3,1,0]]
生命相关
题目描述
根据 百度百科 , 生命游戏 ,简称为 生命 ,是英国数学家约翰·何顿·康威在 1970 年发明的细胞自动机。
给定一个包含 m × n
个格子的面板,每一个格子都可以看成是一个细胞。每个细胞都具有一个初始状态: 1
即为 活细胞 (live),或 0
即为 死细胞 (dead)。每个细胞与其八个相邻位置(水平,垂直,对角线)的细胞都遵循以下四条生存定律:
- 如果活细胞周围八个位置的活细胞数少于两个,则该位置活细胞死亡;
- 如果活细胞周围八个位置有两个或三个活细胞,则该位置活细胞仍然存活;
- 如果活细胞周围八个位置有超过三个活细胞,则该位置活细胞死亡;
- 如果死细胞周围正好有三个活细胞,则该位置死细胞复活;
下一个状态是通过将上述规则同时应用于当前状态下的每个细胞所形成的,其中细胞的出生和死亡是同时发生的。给你 m x n
网格面板 board
的当前状态,返回下一个状态。
题目分析
第一种解法 需要学习的是 int neighbor[3] = {0,1,-1};
通过这个数组的设置可以实现 简易的边界判断
int r = (i + neighbor[a]);
int c = (j + neighbor[b]);
if ((r < row && r >= 0) && (c < column && c >= 0) && (copyBoard[r][c] == 1))
{
live++;
}
解法二
如果细胞之前是活的 然后死了,则标记为 -1
如果细胞之前是死的 然后活了,则标记为 2
题目代码
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
void gameOfLife(vector<vector<int>>& board) {
int row = board.size();
int column = board[0].size();
int neighbor[3] = {0,1,-1};
vector<vector<int>> copyBoard(row, vector<int>(column, 0));
for (int i = 0; i < row; i++)
{
for (int j = 0; j < column; j++)
{
copyBoard[i][j] = board[i][j];
}
}
for (int i = 0; i < row; i++)
{
for (int j = 0; j < column; j++)
{
int live = 0;
for (int a = 0; a < 3; a++)
{
for (int b = 0; b < 3; b++)
{
if (!(neighbor[a] == 0 && neighbor[b] == 0))
{
int r = (i + neighbor[a]);
int c = (j + neighbor[b]);
if ((r < row && r >= 0) && (c < column && c >= 0) && (copyBoard[r][c] == 1))
{
live++;
}
}
}
}
if ((copyBoard[i][j] == 1) && (live < 2 || live > 3))
{
board[i][j] = 0;
}
if ((copyBoard[i][j] == 0) && (live == 3))
{
board[i][j] = 1;
}
}
}
}
};
class Solution {
public:
void gameOfLife(vector<vector<int>>& board) {
int neighbors[3] = {0, 1, -1};
int rows = board.size();
int cols = board[0].size();
// 遍历面板每一个格子里的细胞
for (int row = 0; row < rows; row++) {
for (int col = 0; col < cols; col++) {
// 对于每一个细胞统计其八个相邻位置里的活细胞数量
int liveNeighbors = 0;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (!(neighbors[i] == 0 && neighbors[j] == 0)) {
// 相邻位置的坐标
int r = (row + neighbors[i]);
int c = (col + neighbors[j]);
// 查看相邻的细胞是否是活细胞
if ((r < rows && r >= 0) && (c < cols && c >= 0) && (abs(board[r][c]) == 1)) {
liveNeighbors += 1;
}
}
}
}
// 规则 1 或规则 3
if ((board[row][col] == 1) && (liveNeighbors < 2 || liveNeighbors > 3)) {
// -1 代表这个细胞过去是活的现在死了
board[row][col] = -1;
}
// 规则 4
if (board[row][col] == 0 && liveNeighbors == 3) {
// 2 代表这个细胞过去是死的现在活了
board[row][col] = 2;
}
}
}
// 遍历 board 得到一次更新后的状态
for (int row = 0; row < rows; row++) {
for (int col = 0; col < cols; col++) {
if (board[row][col] > 0) {
board[row][col] = 1;
} else {
board[row][col] = 0;
}
}
}
}
};
示例 1:
输入:board = [[0,1,0],[0,0,1],[1,1,1],[0,0,0]]
输出:[[0,0,0],[1,0,1],[0,1,1],[0,1,0]]
示例 2:
输入:board = [[1,1],[1,0]]
输出:[[1,1],[1,1]]
搜索二维矩阵Ⅱ
题目描述
编写一个高效的算法来搜索 *m* x *n*
矩阵 matrix
中的一个目标值 target
。该矩阵具有以下特性:
- 每行的元素从左到右升序排列。
- 每列的元素从上到下升序排列。
题目分析
题目代码
//灵神的
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int m = matrix.size(), n = matrix[0].size();
int i = 0, j = n - 1; // 从右上角开始
while (i < m && j >= 0) { // 还有剩余元素
if (matrix[i][j] == target) {
return true; // 找到 target
}
if (matrix[i][j] < target) {
i++; // 这一行剩余元素全部小于 target,排除
} else {
j--; // 这一列剩余元素全部大于 target,排除
}
}
return false;
}
};
示例 1:
输入:matrix = [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]], target = 5
输出:true
示例 2:
输入:matrix = [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]], target = 20
输出:false
区间
汇总区间
题目描述
给定一个 无重复元素 的 有序 整数数组 nums
。
返回 恰好覆盖数组中所有数字 的 最小有序 区间范围列表 。也就是说,nums
的每个元素都恰好被某个区间范围所覆盖,并且不存在属于某个范围但不属于 nums
的数字 x
。
列表中的每个区间范围 [a,b]
应该按如下格式输出:
"a->b"
,如果a != b
"a"
,如果a == b
题目分析
这个写法的好处是,无需特判 nums 是否为空,也无需在循环结束后,再补上处理最后一段区间的逻辑。
题目代码
#include <iostream>
#include <vector>
#include <string>
using namespace std;
class Solution {
public:
vector<string> summaryRanges(vector<int>& nums) {
vector<string> ans;
int i = 0;
int n = nums.size();
while (i < n) {
int low = i;
i++;
while (i < n && nums[i] == nums[i - 1] + 1) {
i++;
}
int high = i - 1;
if (low < high) {
ans.push_back(to_string(nums[low]) + "->" + to_string(nums[high]));
}
else {
ans.push_back(to_string(nums[low]));
}
}
return ans;
}
};
示例 1:
输入:nums = [0,1,2,4,5,7]
输出:["0->2","4->5","7"]
解释:区间范围是:
[0,2] --> "0->2"
[4,5] --> "4->5"
[7,7] --> "7"
示例 2:
输入:nums = [0,2,3,4,6,8,9]
输出:["0","2->4","6","8->9"]
解释:区间范围是:
[0,0] --> "0"
[2,4] --> "2->4"
[6,6] --> "6"
[8,9] --> "8->9"
插入区间
题目描述
给你一个 无重叠的 ,按照区间起始端点排序的区间列表 intervals
,其中 intervals[i] = [starti, endi]
表示第 i
个区间的开始和结束,并且 intervals
按照 starti
升序排列。同样给定一个区间 newInterval = [start, end]
表示另一个区间的开始和结束。
在 intervals
中插入区间 newInterval
,使得 intervals
依然按照 starti
升序排列,且区间之间不重叠(如果有必要的话,可以合并区间)。
返回插入之后的 intervals
。
注意 你不需要原地修改 intervals
。你可以创建一个新数组然后返回它。
题目分析
如果 l> right,说明[l,r]与 S 不重叠并且在其右侧,我们可以直接将[l,r]加入答案;
如果 r< left,说明[l,r]与 S 不重叠并且在其左侧,我们可以直接将[l,r]加入答案;
如果上面情况都不满足,说明[l,r]重叠,我们无需将[l,r]加入答案,我们需要合并二者
题目代码
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
vector<vector<int>> insert(vector<vector<int>>& intervals, vector<int>& newInterval) {
int left = newInterval[0];
int right = newInterval[1];
bool placed = false;
vector<vector<int>> ans;
for (const auto& interval : intervals) {
if (interval[0] > right) {
// 在插入区间的右侧且无交集
if (!placed) {
ans.push_back({ left, right });
placed = true;
}
ans.push_back(interval);
}
else if (interval[1] < left) {
// 在插入区间的左侧且无交集
ans.push_back(interval);
}
else {
// 与插入区间有交集,计算它们的并集
left = min(left, interval[0]);
right = max(right, interval[1]);
}
}
if (!placed) {
ans.push_back({ left, right });
}
return ans;
}
};
示例 1:
输入:intervals = [[1,3],[6,9]], newInterval = [2,5]
输出:[[1,5],[6,9]]
示例 2:
输入:intervals = [[1,2],[3,5],[6,7],[8,10],[12,16]], newInterval = [4,8]
输出:[[1,2],[3,10],[12,16]]
解释:这是因为新的区间 [4,8] 与 [3,5],[6,7],[8,10] 重叠。
用最少数量的箭引爆气球
题目描述
有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points
,其中points[i] = [xstart, xend]
表示水平直径在 xstart
和 xend
之间的气球。你不知道气球的确切 y 坐标。
一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x
处射出一支箭,若有一个气球的直径的开始和结束坐标为 x``start
,x``end
, 且满足 xstart ≤ x ≤ x``end
,则该气球会被 引爆 。可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后,可以无限地前进。
给你一个数组 points
,返回引爆所有气球所必须射出的 最小 弓箭数 。
题目分析
标杆
[1.......6]
[2..........8]
[7.........12]
[10.........16]
- 拿当前区间的右端作为标杆。
- 只要 下一个区间的左端<=标杆,则重合,继续寻求与下一个区间重合。
- 直到遇到不重合的区间,弓箭数 +1。
- 拿新区间的右端作为标杆,重复以上步骤。
题目代码
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
int findMinArrowShots(vector<vector<int>>& points) {
if (points.size() == 0)
{
return 0;
}
sort(points.begin(), points.end(), [](const vector<int>& u, const vector<int>& v) {
return u[1] < v[1];
});
int num = 1;
int pos = points[0][1];
for (auto& point : points)
{
if (point[0] > pos)
{
num++;
pos = point[1];
}
}
return num;
}
};
示例 1:
输入:points = [[10,16],[2,8],[1,6],[7,12]]
输出:2
解释:气球可以用2支箭来爆破:
-在x = 6处射出箭,击破气球[2,8]和[1,6]。
-在x = 11处发射箭,击破气球[10,16]和[7,12]。
示例 2:
输入:points = [[1,2],[3,4],[5,6],[7,8]]
输出:4
解释:每个气球需要射出一支箭,总共需要4支箭。
示例 3:
输入:points = [[1,2],[2,3],[3,4],[4,5]]
输出:2
解释:气球可以用2支箭来爆破:
- 在x = 2处发射箭,击破气球[1,2]和[2,3]。
- 在x = 4处射出箭,击破气球[3,4]和[4,5]。
栈
有效的括号
题目描述
给定一个只包括 '('
,')'
,'{'
,'}'
,'['
,']'
的字符串 s
,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号。
题目分析
在这里我们不能使用
unordered_map<char, char> pair = {
{'{','}'},
{'[',']'},
{'(',')'}
};
因为我需要判断闭括号所对应的键值是否和栈顶的开括号相同
题目代码
#include <string>
#include <unordered_map>
#include <stack>
using namespace std;
class Solution {
public:
bool isValid(string s) {
if (s.size() % 2 == 1)
{
return false;
}
unordered_map<char, char> pair = {
{'}', '{'},
{']', '['},
{')', '('}
};
stack<char> stk;
for (auto& S : s)
{
if (pair.count(S))
{
if (stk.empty() || pair[S] != stk.top())
return false;
stk.pop();
}
else
{
stk.push(S);
}
}
return stk.empty();
}
};
示例 1:
输入:s = "()"
输出:true
示例 2:
输入:s = "()[]{}"
输出:true
示例 3:
输入:s = "(]"
输出:false
提示:
1 <= s.length <= 104
s
仅由括号'()[]{}'
组成
简化路径
题目描述
给你一个字符串 path
,表示指向某一文件或目录的 Unix 风格 绝对路径 (以 '/'
开头),请你将其转化为更加简洁的规范路径。
在 Unix 风格的文件系统中,一个点(.
)表示当前目录本身;此外,两个点 (..
) 表示将目录切换到上一级(指向父目录);两者都可以是复杂相对路径的组成部分。任意多个连续的斜杠(即,'//'
)都被视为单个斜杠 '/'
。 对于此问题,任何其他格式的点(例如,'...'
)均被视为文件/目录名称。
请注意,返回的 规范路径 必须遵循下述格式:
- 始终以斜杠
'/'
开头。 - 两个目录名之间必须只有一个斜杠
'/'
。 - 最后一个目录名(如果存在)不能 以
'/'
结尾。 - 此外,路径仅包含从根目录到目标文件或目录的路径上的目录(即,不含
'.'
或'..'
)。
返回简化后得到的 规范路径 。
题目分析
对于「空字符串」以及「一个点」,我们实际上无需对它们进行处理,因为「空字符串」没有任何含义,而「一个点」表示当前目录本身,我们无需切换目录。
对于「两个点」或者「目录名」,我们则可以用一个栈来维护路径中的每一个目录名。当我们遇到「两个点」时,需要将目录切换到上一级,因此只要栈不为空,我们就弹出栈顶的目录。当我们遇到「目录名」时,就把它放入栈。
题目代码
#include <string>
#include <vector>
using namespace std;
class Solution {
public:
string simplifyPath(string path) {
auto splite = [](const string& s, const char delim) -> vector<string> {
vector<string> ans;
string cur;
for (auto& ch : s)
{
if (ch == '/')
{
ans.push_back(move(cur));
cur.clear();
}
else
{
cur += ch;
}
}
ans.push_back(move(cur));
return ans;
};
vector<string> names = splite(path, '/');
vector<string> stack;
for (string& name : names)
{
if (name == "..")
{
if (!stack.empty())
{
stack.pop_back();
}
}
else
{
if (!name.empty() && name != ".")
{
stack.push_back(move(name));
}
}
}
string ans;
if (stack.empty()) {
ans = "/";
}
else {
for (string& name : stack) {
ans += "/" + move(name);
}
}
return ans;
}
};
示例 1:
输入:path = "/home/"
输出:"/home"
解释:注意,最后一个目录名后面没有斜杠。
示例 2:
输入:path = "/../"
输出:"/"
解释:从根目录向上一级是不可行的,因为根目录是你可以到达的最高级。
示例 3:
输入:path = "/home//foo/"
输出:"/home/foo"
解释:在规范路径中,多个连续斜杠需要用一个斜杠替换。
示例 4:
输入:path = "/a/./b/../../c/"
输出:"/c"
最小栈
题目描述
设计一个支持 push
,pop
,top
操作,并能在常数时间内检索到最小元素的栈。
实现 MinStack
类:
MinStack()
初始化堆栈对象。void push(int val)
将元素val推入堆栈。void pop()
删除堆栈顶部的元素。int top()
获取堆栈顶部的元素。int getMin()
获取堆栈中的最小元素。
题目分析
使用额外的空间去实现最小栈
题目代码
#include <iostream>
#include <stack>
using namespace std;
class MinStack {
stack<int> stk;
stack<int> min_stack;
public:
MinStack() {
min_stack.push(INT_MAX);
}
void push(int val) {
stk.push(val);
min_stack.push(min(min_stack.top(), val));
}
void pop() {
stk.pop();
min_stack.pop();
}
int top() {
return stk.top();
}
int getMin() {
return min_stack.top();
}
};
/**
* Your MinStack object will be instantiated and called as such:
* MinStack* obj = new MinStack();
* obj->push(val);
* obj->pop();
* int param_3 = obj->top();
* int param_4 = obj->getMin();
*/
示例 1:
输入:
["MinStack","push","push","push","getMin","pop","top","getMin"]
[[],[-2],[0],[-3],[],[],[],[]]
输出:
[null,null,null,null,-3,null,0,-2]
解释:
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin(); --> 返回 -3.
minStack.pop();
minStack.top(); --> 返回 0.
minStack.getMin(); --> 返回 -2.
逆波兰表达式求值
题目描述
给你一个字符串数组 tokens
,表示一个根据 逆波兰表示法 表示的算术表达式。
请你计算该表达式。返回一个表示表达式值的整数。
注意:
- 有效的算符为
'+'
、'-'
、'*'
和'/'
。 - 每个操作数(运算对象)都可以是一个整数或者另一个表达式。
- 两个整数之间的除法总是 向零截断 。
- 表达式中不含除零运算。
- 输入是一个根据逆波兰表示法表示的算术表达式。
- 答案及所有中间计算结果可以用 32 位 整数表示。
题目分析
最不想中等题的中等题
题目代码
#include <iostream>
#include <vector>
#include <string>
#include <stack>
using namespace std;
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> num;
for (auto& ch : tokens)
{
if (ch == "+" || ch == "-" || ch == "*" || ch == "/")
{
int a = num.top(); num.pop();
int b = num.top(); num.pop();
if (ch == "+") num.push(b + a);
if (ch == "-") num.push(b - a);
if (ch == "*") num.push(b * a);
if (ch == "/") num.push(b / a);
}
else
{
num.push(stoi(ch));
}
}
return num.top();
}
};
示例 1:
输入:tokens = ["2","1","+","3","*"]
输出:9
解释:该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9
示例 2:
输入:tokens = ["4","13","5","/","+"]
输出:6
解释:该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6
示例 3:
输入:tokens = ["10","6","9","3","+","-11","*","/","*","17","+","5","+"]
输出:22
解释:该算式转化为常见的中缀算术表达式为:
((10 * (6 / ((9 + 3) * -11))) + 17) + 5
= ((10 * (6 / (12 * -11))) + 17) + 5
= ((10 * (6 / -132)) + 17) + 5
= ((10 * 0) + 17) + 5
= (0 + 17) + 5
= 17 + 5
= 22
基本计算器
题目描述
给你一个字符串表达式 s
,请你实现一个基本计算器来计算并返回它的值。
注意:不允许使用任何将字符串作为数学表达式计算的内置函数,比如 eval()
。
题目分析
题目代码
#include <string>
#include <stack>
using namespace std;
class Solution {
public:
int calculate(string s) {
stack<int> ops;
ops.push(1);
int sign = 1;
int ret = 0;
int n = s.length();
int i = 0;
while (i < n) {
if (s[i] == ' ') {
i++;
}
else if (s[i] == '+') {
sign = ops.top();
i++;
}
else if (s[i] == '-') {
sign = -ops.top();
i++;
}
else if (s[i] == '(') {
ops.push(sign);
i++;
}
else if (s[i] == ')') {
ops.pop();
i++;
}
else {
long num = 0;
while (i < n && s[i] >= '0' && s[i] <= '9') {
num = num * 10 + s[i] - '0';
i++;
}
ret += sign * num;
}
}
return ret;
}
};
示例 1:
输入:s = "1 + 1"
输出:2
示例 2:
输入:s = " 2-1 + 2 "
输出:3
示例 3:
输入:s = "(1+(4+5+2)-3)+(6+8)"
输出:23
接雨水
题目描述
给定 n
个非负整数表示每个宽度为 1
的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
题目分析
除了计算并存储每个位置两边的最大高度以外,也可以用单调栈计算能接的雨水总量。
维护一个单调栈,单调栈存储的是下标,满足从栈底到栈顶的下标对应的数组 height
中的元素递减。
从左到右遍历数组,遍历到下标 i 时,如果栈内至少有两个元素,记栈顶元素为 top 的下面一个元素是 left,则一定有 height[left]≥height[top]
。如果 height[i]>height[top]
,则得到一个可以接雨水的区域,该区域的宽度是 i−left−1
,高度是 min(height[left],height[i])−height[top]
,根据宽度和高度即可计算得到该区域能接的雨水量。
为了得到 left
,需要将 top
出栈。在对 top
计算能接的雨水量之后,left
变成新的 top
,重复上述操作,直到栈变为空,或者栈顶下标对应的 height
中的元素大于或等于 height[i]
。
在对下标 i 处计算能接的雨水量之后,将 i 入栈,继续遍历后面的下标,计算能接的雨水量。遍历结束之后即可得到能接的雨水总量。
题目代码
//单调栈
class Solution {
public:
int trap(vector<int>& height) {
int h = height.size();
stack<int> stk;
int ans = 0;
for (int right = 0; right < h; right++)
{
while (!stk.empty() && height[right] > height[stk.top()])
{
int top = stk.top();
stk.pop();
if (stk.empty()) break;
int left = stk.top();
int currentwidith = right - left - 1;
int currentheight = min(height[left], height[right]) - height[top];
ans += currentheight * currentwidith;
}
stk.push(right);
}
return ans;
}
};
示例 1:
输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。
示例 2:
输入:height = [4,2,0,3,2,5]
输出:9
二叉树
二叉树的最大深度
题目描述
给定一个二叉树 root
,返回其最大深度。
二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。
题目分析
需要注意我们使用广度优先搜索 使用队列更好
题目代码
#include <iostream>
#include <queue>
using namespace std;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
//深度优先搜索
class Solution {
public:
int maxDepth(TreeNode* root) {
if (root == nullptr)
return 0;
return max(maxDepth(root->left), maxDepth(root->right)) + 1;
}
};
class Solution {
public:
int maxDepth(TreeNode* root) {
queue<TreeNode*> Q;
if (root == nullptr) return 0;
Q.push(root);
int ans = 0;
while (!Q.empty())
{
int size = Q.size();
while (size > 0)
{
TreeNode* node = Q.front();
Q.pop();
if (node->left != nullptr) Q.push(node->left);
if (node->right != nullptr) Q.push(node->right);
size--;
}
ans += 1;
}
return ans;
}
};
示例 1:
输入:root = [3,9,20,null,null,15,7]
输出:3
示例 2:
输入:root = [1,null,2]
输出:2
相同的树
题目描述
给你两棵二叉树的根节点 p
和 q
,编写一个函数来检验这两棵树是否相同。
如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。
题目分析
没什么好分析的 还是要好好理解深度优先搜索
题目代码
#include <iostream>
#include <queue>
using namespace std;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
//广度优先搜索
class Solution {
public:
bool isSameTree(TreeNode* p, TreeNode* q) {
if (p == nullptr && q == nullptr) return true;
if (p == nullptr && q != nullptr) return false;
if (p != nullptr && q == nullptr) return false;
queue<TreeNode*> Q1, Q2;
Q1.push(p);
Q2.push(q);
while (!Q1.empty() && !Q2.empty())
{
int size_Q1 = Q1.size();
int size_Q2 = Q2.size();
while (size_Q1 > 0 && size_Q2>0)
{
TreeNode* node1 = Q1.front(); Q1.pop();
TreeNode* node2 = Q2.front(); Q2.pop();
if (node1->val != node2->val)
{
return false;
}
if (node1->left != nullptr) Q1.push(node1->left);
if (node1->right != nullptr) Q1.push(node1->right);
if (node2->left != nullptr) Q2.push(node2->left);
if (node2->right != nullptr) Q2.push(node2->right);
if (node1->left != nullptr && node2->left == nullptr) return false;
if (node1->left == nullptr && node2->left != nullptr) return false;
if (node1->right != nullptr && node2->right == nullptr) return false;
if (node1->right == nullptr && node2->right != nullptr) return false;
size_Q1--;
size_Q2--;
}
}
if (Q1.empty() && Q2.empty())
return true;
return false;
}
};
//深度优先搜索
class Solution {
public:
bool isSameTree(TreeNode* p, TreeNode* q) {
if (p == nullptr && q == nullptr)
{
return true;
}
else
{
if (p == nullptr || q == nullptr)
{
return false;
}
else
{
if (p->val != q->val)
{
return false;
}
else
{
return isSameTree(p->left, q->left) && isSameTree(p->right, q->right);
}
}
}
}
};
示例 1:
输入:p = [1,2,3], q = [1,2,3]
输出:true
示例 2:
输入:p = [1,2], q = [1,null,2]
输出:false
示例 3:
输入:p = [1,2,1], q = [1,1,2]
输出:false
翻转二叉树
题目描述
给你一棵二叉树的根节点 root
,翻转这棵二叉树,并返回其根节点。
题目分析
这个题目为什么可以使用递归,我们分析一下
这个题目的大问题是翻转左右子树
而拆分为小问题,对于每一个子树也需要翻转子树的左右子树
所以我们可以使用递归
题目代码
#include <iostream>
using namespace std;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
class Solution {
public:
TreeNode* invertTree(TreeNode* root) {
if (root == nullptr) return root;
TreeNode* left = invertTree(root->left);
TreeNode* right = invertTree(root->right);
root->left = right;
root->right = left;
return root;
}
};
示例 1:
输入:root = [4,2,7,1,3,6,9]
输出:[4,7,2,9,6,3,1]
示例 2:
输入:root = [2,1,3]
输出:[2,3,1]
示例 3:
输入:root = []
输出:[]
对称二叉树
题目描述
给你一个二叉树的根节点 root
, 检查它是否轴对称。
题目分析
我觉得这里很巧妙的点就在于check函数,有了这个我们就可以实现值是否相同的判断。
题目代码
#include <iostream>
#include <queue>
using namespace std;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
//深度优先搜索
class Solution {
public:
bool isSymmetric(TreeNode* root) {
return check(root, root);
}
bool check(TreeNode* p, TreeNode* q)
{
if (p == nullptr && q == nullptr)
{
return true;
}
else
{
if (p == nullptr || q == nullptr)
{
return false;
}
else
{
return (p->val == q->val) && (check(p->left, q->right)) && (check(p->right, q->left));
}
}
}
};
class Solution {
public:
bool isSymmetric(TreeNode* root) {
return check(root, root);
}
bool check(TreeNode* u, TreeNode* v)
{
queue<TreeNode*> q;
q.push(u);
q.push(v);
while (!q.empty())
{
u = q.front(); q.pop();
v = q.front(); q.pop();
if (!u && !v) continue;
if ((!u || !v) || (u->val != v->val)) return false;
q.push(u->left);
q.push(v->right);
q.push(u->right);
q.push(v->left);
}
return true;
}
};
示例 1:
输入:root = [1,2,2,3,4,4,3]
输出:true
示例 2:
输入:root = [1,2,2,null,3,null,3]
输出:false
从前序与中序遍历序列构造二叉树
题目描述
给定两个整数数组 preorder
和 inorder
,其中 preorder
是二叉树的先序遍历, inorder
是同一棵树的中序遍历,请构造二叉树并返回其根节点。
题目分析
- 二叉树的遍历:
- 前序遍历:首先访问根节点,然后递归地遍历左子树,最后递归地遍历右子树。
- 中序遍历:首先递归地遍历左子树,然后访问根节点,最后递归地遍历右子树。
- 重建二叉树:
- 由于前序遍历的第一个元素总是树的根节点,我们可以通过这个元素在中序遍历中找到根节点的位置。
- 在中序遍历中,根节点左侧的所有元素构成了左子树,右侧的所有元素构成了右子树。
- 根据左右子树的节点数目,我们可以在前序遍历中划分出左右子树的元素范围。
- 通过递归的方式,我们可以对左右子树重复上述过程,直到所有的节点都被重建。
题目代码
#include <vector>
#include <unordered_map>
using namespace std;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
//递归
class Solution {
private:
unordered_map<int, int> index;
public:
TreeNode* myBuildTree(const vector<int>& preorder, const vector<int>& inorder, int preorder_left, int preorder_right, int inorder_left, int inorder_right) {
if (preorder_left > preorder_right) {
return nullptr;
}
// 前序遍历中的第一个节点就是根节点
int preorder_root = preorder_left;
// 在中序遍历中定位根节点
int inorder_root = index[preorder[preorder_root]];
// 先把根节点建立出来
TreeNode* root = new TreeNode(preorder[preorder_root]);
// 得到左子树中的节点数目
int size_left_subtree = inorder_root - inorder_left;
// 递归地构造左子树,并连接到根节点
// 先序遍历中「从 左边界+1 开始的 size_left_subtree」个元素就对应了中序遍历中「从 左边界 开始到 根节点定位-1」的元素
root->left = myBuildTree(preorder, inorder, preorder_left + 1, preorder_left + size_left_subtree, inorder_left, inorder_root - 1);
// 递归地构造右子树,并连接到根节点
// 先序遍历中「从 左边界+1+左子树节点数目 开始到 右边界」的元素就对应了中序遍历中「从 根节点定位+1 到 右边界」的元素
root->right = myBuildTree(preorder, inorder, preorder_left + size_left_subtree + 1, preorder_right, inorder_root + 1, inorder_right);
return root;
}
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
int n = preorder.size();
// 构造哈希映射,帮助我们快速定位根节点
for (int i = 0; i < n; ++i) {
index[inorder[i]] = i;
}
return myBuildTree(preorder, inorder, 0, n - 1, 0, n - 1);
}
};
class Solution {
public:
TreeNode *buildTree(vector<int> &preorder, vector<int> &inorder) {
int n = preorder.size();
unordered_map<int, int> index;
for (int i = 0; i < n; i++) {
index[inorder[i]] = i;
}
function<TreeNode*(int, int, int, int)> dfs = [&](int pre_l, int pre_r, int in_l, int in_r) -> TreeNode* {
if (pre_l == pre_r) { // 空节点
return nullptr;
}
int left_size = index[preorder[pre_l]] - in_l; // 左子树的大小
TreeNode *left = dfs(pre_l + 1, pre_l + 1 + left_size, in_l, in_l + left_size);
TreeNode *right = dfs(pre_l + 1 + left_size, pre_r, in_l + 1 + left_size, in_r);
return new TreeNode(preorder[pre_l], left, right);
};
return dfs(0, n, 0, n); // 左闭右开区间
}
};
示例 1:
输入: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
输出: [3,9,20,null,null,15,7]
示例 2:
输入: preorder = [-1], inorder = [-1]
输出: [-1]
从中序与后序遍历序列构造二叉树
题目描述
给定两个整数数组 inorder
和 postorder
,其中 inorder
是二叉树的中序遍历, postorder
是同一棵树的后序遍历,请你构造并返回这颗 二叉树 。
题目分析
题目代码
//递归
class Solution {
int post_idx;
unordered_map<int, int> idx_map;
public:
TreeNode* helper(int in_left, int in_right, vector<int>& inorder, vector<int>& postorder) {
// 如果这里没有节点构造二叉树了,就结束
if (in_left > in_right) {
return nullptr;
}
// 选择 post_idx 位置的元素作为当前子树根节点
int root_val = postorder[post_idx];
TreeNode* root = new TreeNode(root_val);
// 根据 root 所在位置分成左右两棵子树
int index = idx_map[root_val];
// 下标减一
post_idx--;
// 构造右子树
root->right = helper(index + 1, in_right, inorder, postorder);
// 构造左子树
root->left = helper(in_left, index - 1, inorder, postorder);
return root;
}
TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
// 从后序遍历的最后一个元素开始
post_idx = (int)postorder.size() - 1;
// 建立(元素,下标)键值对的哈希表
int idx = 0;
for (auto& val : inorder) {
idx_map[val] = idx++;
}
return helper(0, (int)inorder.size() - 1, inorder, postorder);
}
};
class Solution {
public:
TreeNode *buildTree(vector<int> &inorder, vector<int> &postorder) {
int n = inorder.size();
unordered_map<int, int> index;
for (int i = 0; i < n; i++) {
index[inorder[i]] = i;
}
function<TreeNode*(int, int, int, int)> dfs = [&](int in_l, int in_r, int post_l, int post_r) -> TreeNode* {
if (post_l == post_r) { // 空节点
return nullptr;
}
int left_size = index[postorder[post_r - 1]] - in_l; // 左子树的大小
TreeNode *left = dfs(in_l, in_l + left_size, post_l, post_l + left_size);
TreeNode *right = dfs(in_l + left_size + 1, in_r, post_l + left_size, post_r - 1);
return new TreeNode(postorder[post_r - 1], left, right);
};
return dfs(0, n, 0, n); // 左闭右开区间
}
};
示例 1:
输入:inorder = [9,3,15,20,7], postorder = [9,15,7,20,3]
输出:[3,9,20,null,null,15,7]
示例 2:
输入:inorder = [-1], postorder = [-1]
输出:[-1]
从前序与后序遍历序列构造二叉树
题目描述
给定两个整数数组,preorder
和 postorder
,其中 preorder
是一个具有 无重复 值的二叉树的前序遍历,postorder
是同一棵树的后序遍历,重构并返回二叉树。
如果存在多个答案,您可以返回其中 任何 一个。
题目分析
题目代码
#include <iostream>
#include <vector>
#include <unordered_map>
#include <functional>
using namespace std;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
class Solution {
public:
TreeNode* constructFromPrePost(vector<int>& preorder, vector<int>& postorder) {
int n = preorder.size();
vector<int> index(n + 1);
for (int i = 0; i < n; i++) {
index[postorder[i]] = i;
}
function<TreeNode* (int, int, int, int)> dfs = [&](int pre_l, int pre_r, int post_l, int post_r) -> TreeNode* {
if (pre_l == pre_r) { // 空节点
return nullptr;
}
if (pre_l + 1 == pre_r) { // 叶子节点
return new TreeNode(preorder[pre_l]);
}
int left_size = index[preorder[pre_l + 1]] - post_l + 1; // 左子树的大小
TreeNode* left = dfs(pre_l + 1, pre_l + 1 + left_size, post_l, post_l + left_size);
TreeNode* right = dfs(pre_l + 1 + left_size, pre_r, post_l + left_size, post_r - 1);
return new TreeNode(preorder[pre_l], left, right);
};
return dfs(0, n, 0, n); // 左闭右开区间
}
};
示例 1:
输入:preorder = [1,2,4,5,3,6,7], postorder = [4,5,2,6,7,3,1]
输出:[1,2,3,4,5,6,7]
示例 2:
输入: preorder = [1], postorder = [1]
输出: [1]
填充每个节点的下一个右侧节点指针 II
题目描述
给定一个二叉树:
struct Node {
int val;
Node *left;
Node *right;
Node *next;
}
填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL
。
初始状态下,所有 next 指针都被设置为 NULL
。
题目分析
因为它题目给出的链表顺序属于层序遍历,我们可以找到每一层最左边的那个节点,然后当我们再一次进入该层时,所得到的节点就是需要被连接的节点(next)
题目代码
#include <cstddef>
#include <iostream>
#include <vector>
using namespace std;
class Node {
public:
int val;
Node* left;
Node* right;
Node* next;
Node() : val(0), left(NULL), right(NULL), next(NULL) {}
Node(int _val) : val(_val), left(NULL), right(NULL), next(NULL) {}
Node(int _val, Node* _left, Node* _right, Node* _next)
: val(_val), left(_left), right(_right), next(_next) {}
};
class Solution {
public:
vector<Node*> pre;
public:
Node* connect(Node* root) {
dfs(root, 0);
return root;
}
void dfs(Node* node, int depth)
{
if (node == nullptr)
{
return;
}
if (depth == pre.size()) //此时的node相当于一层最左边的节点
{
pre.emplace_back(node);
}
else
{
pre[depth]->next = node; //当node不是这一层最左边的节点时
pre[depth] = node;
}
dfs(node->left, depth + 1);
dfs(node->right, depth + 1);
}
};
示例 1:
输入:root = [1,2,3,4,5,null,7]
输出:[1,#,2,3,#,4,5,7,#]
解释:给定二叉树如图 A 所示,你的函数应该填充它的每个 next 指针,以指向其下一个右侧节点,如图 B 所示。序列化输出按层序遍历顺序(由 next 指针连接),'#' 表示每层的末尾。
示例 2:
输入:root = []
输出:[]
二叉树展开为链表
题目描述
给你二叉树的根结点 root
,请你将它展开为一个单链表:
- 展开后的单链表应该同样使用
TreeNode
,其中right
子指针指向链表中下一个结点,而左子指针始终为null
。 - 展开后的单链表应该与二叉树 先序遍历 顺序相同。
题目分析
具体做法是,对于当前节点,如果其左子节点不为空,则在其左子树中找到最右边的节点,作为前驱节点,将当前节点的右子节点赋给前驱节点的右子节点,然后将当前节点的左子节点赋给当前节点的右子节点,并将当前节点的左子节点设为空。对当前节点处理结束后,继续处理链表中的下一个节点,直到所有节点都处理结束。
题目代码
#include <iostream>
#include <vector>
using namespace std;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
class Solution {
public:
void flatten(TreeNode* root) {
vector<TreeNode*> l;
preorderTraversal(root, l);
int n = l.size();
for (int i = 1; i < n; i++) {
TreeNode* prev = l.at(i - 1), * curr = l.at(i);
prev->left = nullptr;
prev->right = curr;
}
}
void preorderTraversal(TreeNode* root, vector<TreeNode*>& l) {
if (root != NULL) {
l.push_back(root);
preorderTraversal(root->left, l);
preorderTraversal(root->right, l);
}
}
};
class Solution {
public:
void flatten(TreeNode* root) {
TreeNode* cur = root;
while (cur != nullptr)
{
if (cur->left != nullptr)
{
TreeNode* next = cur->left;
TreeNode* predecessor = next;
while (predecessor->right != nullptr)
{
predecessor = predecessor->right;
}
predecessor->right = cur->right;
cur->left = nullptr;
cur->right = next;
}
cur = cur->right;
}
}
};
示例 1:
输入:root = [1,2,5,3,4,null,6]
输出:[1,null,2,null,3,null,4,null,5,null,6]
示例 2:
输入:root = []
输出:[]
示例 3:
输入:root = [0]
输出:[0]
路径总和
题目描述
给你二叉树的根节点 root
和一个表示目标和的整数 targetSum
。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum
。如果存在,返回 true
;否则,返回 false
。
叶子节点 是指没有子节点的节点。
题目分析
一看就会,一做就废
题目代码
#include <iostream>
#include <queue>
using namespace std;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
class Solution {
public:
public:
bool hasPathSum(TreeNode* root, int targetSum) {
if (root == nullptr)
return false;
targetSum -= root->val;
if (root->left == root->right)
return targetSum == 0;
return hasPathSum(root->left, targetSum) || hasPathSum(root->right, targetSum);
}
};
示例 1:
输入:root = [5,4,8,11,null,13,4,7,2,null,null,null,1], targetSum = 22
输出:true
解释:等于目标和的根节点到叶节点路径如上图所示。
示例 2:
输入:root = [1,2,3], targetSum = 5
输出:false
解释:树中存在两条根节点到叶子节点的路径:
(1 --> 2): 和为 3
(1 --> 3): 和为 4
不存在 sum = 5 的根节点到叶子节点的路径。
示例 3:
输入:root = [], targetSum = 0
输出:false
解释:由于树是空的,所以不存在根节点到叶子节点的路径。
求根节点到叶节点数字之和
题目描述
给你一个二叉树的根节点 root
,树中每个节点都存放有一个 0
到 9
之间的数字。
每条从根节点到叶节点的路径都代表一个数字:
- 例如,从根节点到叶节点的路径
1 -> 2 -> 3
表示数字123
。
计算从根节点到叶节点生成的 所有数字之和 。
叶节点 是指没有子节点的节点。
题目分析
功夫不负有心人,根据前辈的经验,这道递归终于做出来啦!
题目代码
#include <iostream>
#include <vector>
using namespace std;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
class Solution {
public:
int sumNumbers(TreeNode* root) {
int sum = 0;
vector<int> num;
auto ans = calculate(root,sum,num);
int size = ans.size();
int Allsum = 0;
for (auto& a : ans)
{
Allsum += a;
}
return Allsum;
}
vector<int> calculate(TreeNode* root, int sum,vector<int> num)
{
if (root == nullptr)
{
return num;
}
sum = sum * 10 + root->val;
if (root->left == root->right)
{
num.emplace_back(sum);
}
num = calculate(root->left, sum, num);
num = calculate(root->right, sum, num);
return num;
}
};
示例 1:
输入:root = [1,2,3]
输出:25
解释:
从根到叶子节点路径 1->2 代表数字 12
从根到叶子节点路径 1->3 代表数字 13
因此,数字总和 = 12 + 13 = 25
示例 2:
输入:root = [4,9,0,5,1]
输出:1026
解释:
从根到叶子节点路径 4->9->5 代表数字 495
从根到叶子节点路径 4->9->1 代表数字 491
从根到叶子节点路径 4->0 代表数字 40
因此,数字总和 = 495 + 491 + 40 = 1026
二叉树中的最大路径和
题目描述
二叉树中的 路径 被定义为一条节点序列,序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。
路径和 是路径中各节点值的总和。
给你一个二叉树的根节点 root
,返回其 最大路径和 。
题目分析
之后看灵神的解析吧,这个感觉还是有点迷糊
题目代码
#include <iostream>
#include <functional>
using namespace std;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
class Solution {
public:
int maxPathSum(TreeNode* root) {
int ans = INT_MIN;
function<int(TreeNode*)> dfs = [&](TreeNode* node) -> int {
if (node == nullptr)
return 0;
int left_val = dfs(node->left);
int right_val = dfs(node->right);
ans = max(ans, left_val + right_val + node->val);
return max(max(left_val, right_val) + node->val, 0); //返回以当前节点为终点的最大路径和
};
dfs(root);
return ans;
}
};
示例 1:
输入:root = [1,2,3]
输出:6
解释:最优路径是 2 -> 1 -> 3 ,路径和为 2 + 1 + 3 = 6
示例 2:
输入:root = [-10,9,20,null,null,15,7]
输出:42
解释:最优路径是 15 -> 20 -> 7 ,路径和为 15 + 20 + 7 = 42
完全二叉树的节点个数
题目描述
给你一棵 完全二叉树 的根节点 root
,求出该树的节点个数。
完全二叉树 的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h
层,则该层包含 1~ 2h
个节点。
题目分析
根据完全二叉树的性质,我们可以清楚的知道:总节点数 = 倒数第二层以上的节点数 + 最后一层的节点数
除最后一层外,这棵树为满二叉树,节点数为:2^depth_prev - 1
,depth_prev
为倒数第二层树的深度
最后一层的节点数的范围是 [1, 2^depth_prev]
;并且依次靠左排列
所以现在的问题就转换为判断最后一层节点数
接下来是如何判断最后一层某个节点是否存在,也就是 is_exist()函数:
给定最后一层某节点的位置索引 index,将他和分界线比大小,就可以判断该节点在左子树还是右子树
题目代码
#include <iostream>
#include <queue>
using namespace std;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
class Solution {
public:
// 求二叉树的深度
int countLevels(TreeNode* root) {
int levels = 0;
while (root) {
root = root->left; levels += 1;
}
return levels;
}
/*
* 功能: 判断最后一层第index个索引是否存在
* root: 二叉树根节点
* index:判断最后一层索引为index的节点是否存在, 索引范围是[1, 2^depth]
* depth:倒数第二层的深度, 这是因为满二叉树最后一层的节点数等于 2^depth
*/
bool is_exist(TreeNode* root, int index, int depth) {
TreeNode* node = root;
while (depth) {
// 最后一层分界线
int mid = ((1 << depth) >> 1);
if (index > mid) {
// 如果在右子树,需要更新索引值
index -= mid;
node = node->right;
}
else {
node = node->left;
}
depth -= 1;
}
return node != nullptr;
}
int countNodes(TreeNode* root) {
// 3. 二分查找
if (root == nullptr) return 0;
// 二叉树深度
int depth = countLevels(root);
// 倒数第二层深度
int depth_prev = depth - 1;
int start = 1, end = (1 << depth_prev), mid = 0;
while (start <= end) {
mid = start + ((end - start) >> 1);
if (is_exist(root, mid, depth_prev)) start = mid + 1;
else end = mid - 1;
}
// start - 1为最后一层节点数
int ret = (1 << depth_prev) - 1 + start - 1;
return ret;
}
};
示例 1:
输入:root = [1,2,3,4,5,6]
输出:6
示例 2:
输入:root = []
输出:0
示例 3:
输入:root = [1]
输出:1
二叉树的最近公共祖先
题目描述
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
题目分析
寻找左子树和右子树,如果同时满足则返回一个祖先节点。
题目代码
#include <cstddef>
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if (root == nullptr || root == p || root == q)
{
return root;
}
TreeNode* left = lowestCommonAncestor(root->left, p, q);
TreeNode* right = lowestCommonAncestor(root->right, p, q);
if (left && right)
{
return root;
}
return left ? left : right;
}
};
示例 1:
输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出:3
解释:节点 5 和节点 1 的最近公共祖先是节点 3 。
示例 2:
输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出:5
解释:节点 5 和节点 4 的最近公共祖先是节点 5 。因为根据定义最近公共祖先节点可以为节点本身。
示例 3:
输入:root = [1,2], p = 1, q = 2
输出:1
二叉树的右视图
题目描述
给定一个二叉树的 根节点 root
,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
题目分析
递归右子树,再递归左子树,当某个深度首次到达时,对应的节点就在右视图中。
题目代码
#include <vector>
#include <algorithm>
using namespace std;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
class Solution {
public:
vector<int> ans;
vector<int> rightSideView(TreeNode* root) {
dfs(root, 0);
return ans;
}
void dfs(TreeNode* node, int depth)
{
if (node == nullptr)
{
return;
}
if (depth == ans.size())
{
ans.emplace_back(node->val);
}
dfs(node->right, depth + 1);
dfs(node->left, depth + 1);
return;
}
};
示例 1:
输入: [1,2,3,null,5,null,4]
输出: [1,3,4]
示例 2:
输入: [1,null,3]
输出: [1,3]
示例 3:
输入: []
输出: []
二叉树的锯齿形层序遍历
题目描述
给你二叉树的根节点 root
,返回其节点值的 锯齿形层序遍历 。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。
题目分析
ranges::reverse(temp);
C++20中新增的翻转功能,这道题需要学习的就是这个
题目代码
#include <vector>
#include <queue>
#include <iostream>
using namespace std;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
class Solution {
public:
vector<vector<int>> ans;
vector<vector<int>> zigzagLevelOrder(TreeNode* root) {
if (root == nullptr)
return ans;
queue<TreeNode*> Q;
Q.push(root);
while (!Q.empty())
{
int size = Q.size();
vector<int> temp;
while (size > 0)
{
TreeNode* node = Q.front();
Q.pop();
temp.emplace_back(node->val);
if (node->left != nullptr) Q.push(node->left);
if (node->right != nullptr) Q.push(node->right);
size--;
}
if (ans.size() % 2) ranges::reverse(temp);
ans.emplace_back(temp);
}
return ans;
}
};
示例 1:
输入:root = [3,9,20,null,null,15,7]
输出:[[3],[20,9],[15,7]]
示例 2:
输入:root = [1]
输出:[[1]]
示例 3:
输入:root = []
输出:[]
二叉搜索树的最小绝对差
题目描述
给你一个二叉搜索树的根节点 root
,返回 树中任意两不同节点值之间的最小差值 。
差值是一个正数,其数值等于两值之差的绝对值。
题目分析
根据二叉搜索树的性质,他们的最小绝对差一定是两个相邻节点,所以我们使用中序遍历。
题目代码
#include <iostream>
using namespace std;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
class Solution {
public:
void dfs(TreeNode* root, int& pre, int& ans)
{
if (root == nullptr)
{
return;
}
dfs(root->left, pre, ans);
if (pre == -1)
{
pre = root->val;
}
else
{
ans = min(ans,root->val- pre);
pre = root->val;
}
dfs(root->right, pre, ans);
}
int getMinimumDifference(TreeNode* root) {
int ans = INT_MAX;
int pre = -1;
dfs(root, pre, ans);
return ans;
}
};
示例 1:
输入:root = [4,2,6,1,3]
输出:1
示例 2:
输入:root = [1,0,48,null,null,12,49]
输出:1
二叉搜索树中第K小的元素
题目描述
给定一个二叉搜索树的根节点 root
,和一个整数 k
,请你设计一个算法查找其中第 k
个最小元素(从 1 开始计数)。
题目分析
这里我们要知道K什么时候开始需要递减,就是到最小节点以后,那么我们一开始就可以递归到最小节点,然后开始进行判断。
题目代码
#include <iostream>
using namespace std;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
class Solution {
public:
int res;
int K;
int kthSmallest(TreeNode* root, int k) {
this->K = k;
res = 1;
dfs(root);
return res;
}
void dfs(TreeNode* root)
{
if (root == nullptr)
{
return;
}
dfs(root->left);
if (K == 0) return;
if (--K == 0) res = root->val;
dfs(root->right);
}
};
示例 1:
输入:root = [3,1,4,null,2], k = 1
输出:1
示例 2:
输入:root = [5,3,6,2,4,null,null,1], k = 3
输出:3
验证二叉搜索树
题目描述
给你一个二叉树的根节点 root
,判断其是否是一个有效的二叉搜索树。
有效 二叉搜索树定义如下:
- 节点的左子树只包含 小于 当前节点的数。
- 节点的右子树只包含 大于 当前节点的数。
- 所有左子树和右子树自身必须也是二叉搜索树。
题目分析
- 前序遍历在某些数据下不需要递归到叶子节点就能返回(比如根节点不满足要求,压根就不会往下递归),而中序遍历和后序遍历至少要递归到一个边界,从这个角度上来说,前序遍历是最快的。
- 中序遍历很好地利用了二叉搜索树的性质,使用到的变量最少。
- 后序遍历的思想是最通用的,即自底向上计算子问题的过程。想要学好动态规划的话,请务必掌握这个思想。
题目代码
//前序遍历
class Solution {
public:
bool isValidBST(TreeNode* root, long left = LONG_MIN, long right = LONG_MAX) {
if (root == nullptr) {
return true;
}
long x = root->val;
return left < x && x < right &&
isValidBST(root->left, left, x) &&
isValidBST(root->right, x, right);
}
};
//中序遍历
class Solution {
long pre = LONG_MIN;
public:
bool isValidBST(TreeNode* root) {
if (root == nullptr) {
return true;
}
if (!isValidBST(root->left) || root->val <= pre) {
return false;
}
pre = root->val;
return isValidBST(root->right);
}
};
//后序遍历
class Solution {
pair<long, long> dfs(TreeNode* node) {
if (node == nullptr) {
return {LONG_MAX, LONG_MIN};
}
auto[l_min, l_max] = dfs(node->left);
auto[r_min, r_max] = dfs(node->right);
long x = node->val;
// 也可以在递归完左子树之后立刻判断,如果发现不是二叉搜索树,就不用递归右子树了
if (x <= l_max || x >= r_min) {
return {LONG_MIN, LONG_MAX};
}
return {min(l_min, x), max(r_max, x)};
}
public:
bool isValidBST(TreeNode* root) {
return dfs(root).second != LONG_MAX;
}
};
示例 1:
输入:root = [2,1,3]
输出:true
示例 2:
输入:root = [5,1,4,null,null,3,6]
输出:false
解释:根节点的值是 5 ,但是右子节点的值是 4 。
图
岛屿数量
题目描述
给你一个由 '1'
(陆地)和 '0'
(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
题目分析
看看代码,很容易理解。
题目代码
#include <vector>
#include <queue>
using namespace std;
//DFS
class Solution {
public:
int dir[4][2] = { 0, 1, 1, 0, -1, 0, 0, -1 }; // 四个方向 下 右 左 上
int numIslands(vector<vector<char>>& grid) {
int x = grid.size();
int y = grid[0].size();
vector<vector<bool>> visited = vector<vector<bool>>(x, vector<bool>(y, false));
int result = 0;
for (int i = 0; i < x; i++)
{
for (int j = 0; j < y; j++)
{
if (!visited[i][j] && grid[i][j] == '1')
{
result++;
dfs(grid, visited, i, j);
}
}
}
return result;
}
void dfs(vector<vector<char>>& grid, vector<vector<bool>>& visited, int x, int y)
{
for (int i = 0; i < 4; i++)
{
int nextx = x + dir[i][0];
int nexty = y + dir[i][1];
if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size())
{
continue;
}
if (!visited[nextx][nexty] && grid[nextx][nexty] == '1')
{
visited[nextx][nexty] = true;
dfs(grid, visited, nextx, nexty);
}
}
}
};
//BFS
class Solution {
private:
int dir[4][2] = { 0, 1, 1, 0, -1, 0, 0, -1 }; // 四个方向
void bfs(vector<vector<char>>& grid, vector<vector<bool>>& visited, int x, int y) {
queue<pair<int, int>> que;
que.push({ x, y });
visited[x][y] = true; // 只要加入队列,立刻标记
while (!que.empty()) {
pair<int, int> cur = que.front(); que.pop();
int curx = cur.first;
int cury = cur.second;
for (int i = 0; i < 4; i++) {
int nextx = curx + dir[i][0];
int nexty = cury + dir[i][1];
if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; // 越界了,直接跳过
if (!visited[nextx][nexty] && grid[nextx][nexty] == '1') {
que.push({ nextx, nexty });
visited[nextx][nexty] = true; // 只要加入队列立刻标记
}
}
}
}
public:
int numIslands(vector<vector<char>>& grid) {
int n = grid.size(), m = grid[0].size();
vector<vector<bool>> visited = vector<vector<bool>>(n, vector<bool>(m, false));
int result = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (!visited[i][j] && grid[i][j] == '1') {
result++; // 遇到没访问过的陆地,+1
bfs(grid, visited, i, j); // 将与其链接的陆地都标记上 true
}
}
}
return result;
}
};
示例 1:
输入:grid = [
["1","1","1","1","0"],
["1","1","0","1","0"],
["1","1","0","0","0"],
["0","0","0","0","0"]
]
输出:1
示例 2:
输入:grid = [
["1","1","0","0","0"],
["1","1","0","0","0"],
["0","0","1","0","0"],
["0","0","0","1","1"]
]
输出:3
被围绕的区域
题目描述
给你一个 m x n
的矩阵 board
,由若干字符 'X'
和 'O'
组成,捕获 所有 被围绕的区域:
- 连接:一个单元格与水平或垂直方向上相邻的单元格连接。
- 区域:连接所有
'0'
的单元格来形成一个区域。 - 围绕:如果您可以用
'X'
单元格 连接这个区域,并且区域中没有任何单元格位于board
边缘,则该区域被'X'
单元格围绕。
题目分析
定义一个 visited 二维数组,单独标记周边的’O’,然后遍历地图的时候同时对 数组board 和 数组visited 进行判断,是否’O’改成’X’。
这样做其实就有点麻烦了,不用额外定义空间了,标记周边的’O’,可以直接改board的数值为其他特殊值。
深搜或者广搜将地图周边的’O’全部改成’A’
再遍历地图,将’O’全部改成’X’(地图中间的’O’改成了’X’),将’A’改回’O’(保留的地图周边的’O’)
题目代码
#include <vector>
using namespace std;
class Solution {
private:
int dir[4][2] = { -1, 0, 0, -1, 1, 0, 0, 1 }; // 保存四个方向
void dfs(vector<vector<char>>& board, int x, int y) {
board[x][y] = 'A';
for (int i = 0; i < 4; i++) { // 向四个方向遍历
int nextx = x + dir[i][0];
int nexty = y + dir[i][1];
// 超过边界
if (nextx < 0 || nextx >= board.size() || nexty < 0 || nexty >= board[0].size()) continue;
// 不符合条件,不继续遍历
if (board[nextx][nexty] == 'X' || board[nextx][nexty] == 'A') continue;
dfs(board, nextx, nexty);
}
return;
}
public:
void solve(vector<vector<char>>& board) {
int n = board.size(), m = board[0].size();
// 步骤一:
// 从左侧边,和右侧边 向中间遍历
for (int i = 0; i < n; i++) {
if (board[i][0] == 'O') dfs(board, i, 0);
if (board[i][m - 1] == 'O') dfs(board, i, m - 1);
}
// 从上边和下边 向中间遍历
for (int j = 0; j < m; j++) {
if (board[0][j] == 'O') dfs(board, 0, j);
if (board[n - 1][j] == 'O') dfs(board, n - 1, j);
}
// 步骤二:
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (board[i][j] == 'O') board[i][j] = 'X';
if (board[i][j] == 'A') board[i][j] = 'O';
}
}
}
};
示例 1:
输入:board = [[“X”,”X”,”X”,”X”],[“X”,”O”,”O”,”X”],[“X”,”X”,”O”,”X”],[“X”,”O”,”X”,”X”]]
输出:[[“X”,”X”,”X”,”X”],[“X”,”X”,”X”,”X”],[“X”,”X”,”X”,”X”],[“X”,”O”,”X”,”X”]]
解释:
在上图中,底部的区域没有被捕获,因为它在 board 的边缘并且不能被围绕。
示例 2:
输入:board = [[“X”]]
输出:[[“X”]]
克隆图
题目描述
给你无向 连通 图中一个节点的引用,请你返回该图的 深拷贝(克隆)。
图中的每个节点都包含它的值 val
(int
) 和其邻居的列表(list[Node]
)。
class Node {
public int val;
public List<Node> neighbors;
}
测试用例格式:
简单起见,每个节点的值都和它的索引相同。例如,第一个节点值为 1(val = 1
),第二个节点值为 2(val = 2
),以此类推。该图在测试用例中使用邻接列表表示。
邻接列表 是用于表示有限图的无序列表的集合。每个列表都描述了图中节点的邻居集。
给定节点将始终是图中的第一个节点(值为 1)。你必须将 给定节点的拷贝 作为对克隆图的引用返回。
题目分析
使用哈希表记录当前节点是否已经被创立,然后使用递归节点的邻接列表。
题目代码
#include <vector>
#include <unordered_map>
using namespace std;
class Node {
public:
int val;
vector<Node*> neighbors;
Node() {
val = 0;
neighbors = vector<Node*>();
}
Node(int _val) {
val = _val;
neighbors = vector<Node*>();
}
Node(int _val, vector<Node*> _neighbors) {
val = _val;
neighbors = _neighbors;
}
};
class Solution {
public:
unordered_map<int,Node*> node_set;
Node* cloneGraph(Node* node) {
if (node == nullptr)
{
return node;
}
if (node_set.count(node->val)) return node_set[node->val];
Node* p = new Node(node->val);
node_set[node->val] = p;
vector<Node*> neighbor = node->neighbors;
for (int i = 0; i < neighbor.size(); i++)
{
p->neighbors.emplace_back(cloneGraph(neighbor[i]));
}
return p;
}
};
示例 1:
输入:adjList = [[2,4],[1,3],[2,4],[1,3]]
输出:[[2,4],[1,3],[2,4],[1,3]]
解释:
图中有 4 个节点。
节点 1 的值是 1,它有两个邻居:节点 2 和 4 。
节点 2 的值是 2,它有两个邻居:节点 1 和 3 。
节点 3 的值是 3,它有两个邻居:节点 2 和 4 。
节点 4 的值是 4,它有两个邻居:节点 1 和 3 。
示例 2:
输入:adjList = [[]]
输出:[[]]
解释:输入包含一个空列表。该图仅仅只有一个值为 1 的节点,它没有任何邻居。
示例 3:
输入:adjList = []
输出:[]
解释:这个图是空的,它不含任何节点。
除法求值
题目描述
给你一个变量对数组 equations
和一个实数值数组 values
作为已知条件,其中 equations[i] = [Ai, Bi]
和 values[i]
共同表示等式 Ai / Bi = values[i]
。每个 Ai
或 Bi
是一个表示单个变量的字符串。
另有一些以数组 queries
表示的问题,其中 queries[j] = [Cj, Dj]
表示第 j
个问题,请你根据已知条件找出 Cj / Dj = ?
的结果作为答案。
返回 所有问题的答案 。如果存在某个无法确定的答案,则用 -1.0
替代这个答案。如果问题中出现了给定的已知条件中没有出现的字符串,也需要用 -1.0
替代这个答案。
注意:输入总是有效的。你可以假设除法运算中不会出现除数为 0 的情况,且不存在任何矛盾的结果。
注意:未在等式列表中出现的变量是未定义的,因此无法确定它们的答案。
题目分析
看代码注释
题目代码
#include <vector>
#include <string>
#include <unordered_map>
#include <queue>
#include <unordered_set>
using namespace std;
// 定义一个解决方案类
class Solution {
public:
// 主函数,接受方程组、值以及查询,返回查询的结果
vector<double> calcEquation(vector<vector<string>>& equations, vector<double>& values, vector<vector<string>>& queries) {
// 创建一个图,用于表示方程组中的关系
// 图是一个字符串到字符串到double的映射,表示节点到节点之间的权重
unordered_map<string, unordered_map<string, double>> graph;
// 方程组的数量
int n = equations.size();
// 构建图,将方程组中的关系添加到图中
for (int i = 0; i < n; i++)
{
string s = equations[i][0]; // 方程的起始节点
string e = equations[i][1]; // 方程的终止节点
double v = values[i]; // 方程的值
graph[s][e] = v; // 添加s到e的边,权重为v
graph[e][s] = 1 / v; // 添加e到s的边,权重为1/v
graph[s][s] = 1.0; // 添加s到s的边,权重为1.0(自身循环)
graph[e][e] = 1.0; // 添加e到e的边,权重为1.0(自身循环)
}
// 创建一个队列,用于宽度优先搜索
queue<pair<string, double>> q;
// 查询的数量
int m = queries.size();
// 结果数组,初始化为-1.0,用于存储查询的结果
vector<double> ans(m, -1.0);
// 遍历所有查询
for (int i = 0; i < m; i++)
{
string qx = queries[i][0], qy = queries[i][1]; // 当前查询的两个节点
// 如果查询中的节点不在图中,则跳过当前查询
if (graph.find(qx) == graph.end() || graph.find(qy) == graph.end()) continue;
// 将起始节点和初始乘积1.0放入队列
q.emplace(qx, 1.0);
// 创建一个已访问集合,用于记录已访问的节点
unordered_set<string> visited{ qx };
// 进行宽度优先搜索
while (!q.empty())
{
string node = q.front().first; // 当前节点
double mul = q.front().second; // 从起始节点到当前节点的乘积
q.pop(); // 弹出当前节点
// 遍历当前节点的所有邻居
for (pair<string, double> neighbor : graph[node])
{
string ngh = neighbor.first; // 邻居节点
double weight = neighbor.second; // 当前节点到邻居节点的权重
// 如果找到了查询的终止节点,则计算结果并退出循环
if (ngh == qy) {
ans[i] = mul * weight;
break;
}
// 如果邻居节点没有被访问过,则将其加入队列
if (visited.find(ngh) == visited.end())
{
visited.emplace(ngh);
q.emplace(ngh, mul * weight);
}
}
}
}
// 返回结果数组
return ans;
}
};
示例 1:
输入:equations = [["a","b"],["b","c"]], values = [2.0,3.0], queries = [["a","c"],["b","a"],["a","e"],["a","a"],["x","x"]]
输出:[6.00000,0.50000,-1.00000,1.00000,-1.00000]
解释:
条件:a / b = 2.0, b / c = 3.0
问题:a / c = ?, b / a = ?, a / e = ?, a / a = ?, x / x = ?
结果:[6.0, 0.5, -1.0, 1.0, -1.0 ]
注意:x 是未定义的 => -1.0
示例 2:
输入:equations = [["a","b"],["b","c"],["bc","cd"]], values = [1.5,2.5,5.0], queries = [["a","c"],["c","b"],["bc","cd"],["cd","bc"]]
输出:[3.75000,0.40000,5.00000,0.20000]
示例 3:
输入:equations = [["a","b"]], values = [0.5], queries = [["a","b"],["b","a"],["a","c"],["x","y"]]
输出:[0.50000,2.00000,-1.00000,-1.00000]
课程表
题目描述
你这个学期必须选修 numCourses
门课程,记为 0
到 numCourses - 1
。
在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites
给出,其中 prerequisites[i] = [ai, bi]
,表示如果要学习课程 ai
则 必须 先学习课程 bi
。
- 例如,先修课程对
[0, 1]
表示:想要学习课程0
,你需要先完成课程1
。
请你判断是否可能完成所有课程的学习?如果可以,返回 true
;否则,返回 false
。
题目分析
这道题我们采用拓扑排序,如果最后没有环,则代表可以成功。
题目代码
#include <vector>
#include <unordered_map>
#include <queue>
using namespace std;
class Solution {
private:
vector<vector<int>> edges;
vector<int> visited;
bool valid = true;
public:
void dfs(int u) {
visited[u] = 1;
for (int v : edges[u]) {
if (visited[v] == 0) {
dfs(v);
if (!valid) {
return;
}
}
else if (visited[v] == 1) {
valid = false;
return;
}
}
visited[u] = 2;
}
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
edges.resize(numCourses);
visited.resize(numCourses);
for (const auto& info : prerequisites) {
edges[info[1]].push_back(info[0]);
}
for (int i = 0; i < numCourses && valid; ++i) {
if (!visited[i]) {
dfs(i);
}
}
return valid;
}
};
//BFS
class Solution {
public:
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
//所有课程的入度
vector<int> ingree(numCourses, 0);
//保存课程和先修课程之间的关系
unordered_map<int, vector<int>> preCourse;
for (int i = 0; i < prerequisites.size(); ++i) {
preCourse[prerequisites[i][0]].push_back(prerequisites[i][1]);
ingree[prerequisites[i][1]]++;
}
queue<int> zero_ingree;
int count = 0;
for (int i = 0; i < numCourses; ++i) {
//所有入度为0的课程入队列
if (!ingree[i]) {
zero_ingree.push(i);
++count;
}
}
while (!zero_ingree.empty()) {
int cur_course = zero_ingree.front();
zero_ingree.pop();
for (int i = 0; i < preCourse[cur_course].size(); ++i) {
//当前入度为0的课程对应的先修课程入度减1
--ingree[preCourse[cur_course][i]];
if (!ingree[preCourse[cur_course][i]]) {
//入度为0的课程入队列
zero_ingree.push(preCourse[cur_course][i]);
++count;
}
}
}
//所有课程的入度减为0,说明课程可以修完
if (count == numCourses) return true;
return false;
}
};
示例 1:
输入:numCourses = 2, prerequisites = [[1,0]]
输出:true
解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。
示例 2:
输入:numCourses = 2, prerequisites = [[1,0],[0,1]]
输出:false
解释:总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0 ;并且学习课程 0 之前,你还应先完成课程 1 。这是不可能的。
课程表Ⅱ
题目描述
现在你总共有 numCourses
门课需要选,记为 0
到 numCourses - 1
。给你一个数组 prerequisites
,其中 prerequisites[i] = [ai, bi]
,表示在选修课程 ai
前 必须 先选修 bi
。
- 例如,想要学习课程
0
,你需要先完成课程1
,我们用一个匹配来表示:[0,1]
。
返回你为了学完所有课程所安排的学习顺序。可能会有多个正确的顺序,你只要返回 任意一种 就可以了。如果不可能完成所有课程,返回 一个空数组 。
题目分析
和课程表的区别就在于,这个将拓扑排序的结果记录下来,这样到时候输出就可以了。
题目代码
#include <vector>
#include <queue>
#include <unordered_map>
using namespace std;
class Solution {
public:
vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
// 存储每个节点的入度
vector<int> inDegree(numCourses, 0);
// 存储有向图
unordered_map<int, vector<int>> graph;
// 存储拓扑排序的结果
vector<int> result;
// 构建图和入度数组
for (auto& edge : prerequisites) {
graph[edge[1]].push_back(edge[0]);
inDegree[edge[0]]++;
}
// 找到所有入度为0的节点,并将它们加入队列
queue<int> q;
for (int i = 0; i < numCourses; ++i) {
if (inDegree[i] == 0) {
q.push(i);
}
}
// 进行拓扑排序
while (!q.empty()) {
int course = q.front();
q.pop();
result.push_back(course);
for (int nextCourse : graph[course]) {
//这里的nextCourse是vector<int>中的所有值
inDegree[nextCourse]--;
if (inDegree[nextCourse] == 0) {
q.push(nextCourse);
}
}
}
// 如果结果长度不等于课程数,说明存在环,无法完成所有课程
if (result.size() != numCourses) {
return {};
}
return result;
}
};
示例 1:
输入:numCourses = 2, prerequisites = [[1,0]]
输出:[0,1]
解释:总共有 2 门课程。要学习课程 1,你需要先完成课程 0。因此,正确的课程顺序为 [0,1] 。
示例 2:
输入:numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]]
输出:[0,2,1,3]
解释:总共有 4 门课程。要学习课程 3,你应该先完成课程 1 和课程 2。并且课程 1 和课程 2 都应该排在课程 0 之后。
因此,一个正确的课程顺序是 [0,1,2,3] 。另一个正确的排序是 [0,2,1,3] 。
示例 3:
输入:numCourses = 1, prerequisites = []
输出:[0]
蛇梯棋
题目描述
给你一个大小为 n x n
的整数矩阵 board
,方格按从 1
到 n2
编号,编号遵循 转行交替方式 ,从左下角开始 (即,从 board[n - 1][0]
开始)每一行交替方向。
玩家从棋盘上的方格 1
(总是在最后一行、第一列)开始出发。
每一回合,玩家需要从当前方格 curr
开始出发,按下述要求前进:
- 选定目标方格
next
,目标方格的编号符合范围[curr + 1, min(curr + 6, n2)]
。 - 传送玩家:如果目标方格
next
处存在蛇或梯子,那么玩家会传送到蛇或梯子的目的地。否则,玩家传送到目标方格next
。 - 当玩家到达编号
n2
的方格时,游戏结束。
r
行 c
列的棋盘,按前述方法编号,棋盘格中可能存在 “蛇” 或 “梯子”;如果 board[r][c] != -1
,那个蛇或梯子的目的地将会是 board[r][c]
。编号为 1
和 n2
的方格上没有蛇或梯子。
注意,玩家在每回合的前进过程中最多只能爬过蛇或梯子一次:就算目的地是另一条蛇或梯子的起点,玩家也 不能 继续移动。
- 举个例子,假设棋盘是
[[-1,4],[-1,3]]
,第一次移动,玩家的目标方格是2
。那么这个玩家将会顺着梯子到达方格3
,但 不能 顺着方格3
上的梯子前往方格4
。
返回达到编号为 n2
的方格所需的最少移动次数,如果不可能,则返回 -1
。
题目分析
909. 蛇梯棋 – 力扣(LeetCode)题解看这一篇
题目代码
#include <vector>
#include <queue>
using namespace std;
class Solution {
public:
int snakesAndLadders(vector<vector<int>>& board) {
int n = board.size(); // 获取方阵的边长
int target = n * n; // 获取方阵尺寸,也是最后要到达目的地
queue<pair<int, int>> Q; // 队列用于BFS,存放待搜索的方格编号和到达该方格时的最少移动数
Q.emplace(1, 0); // 初始{1,0}入队,表示起点1,0次移动
vector<vector<bool>> visited(n, vector<bool>(n)); // 用于BFS过程中标记方格是否搜索过
// BFS
while (!Q.empty()) {
auto node = Q.front();
Q.pop();
int curr = node.first, cnt = node.second; // 获取当前搜索的方格宾浩和到达该方格的最少移动数
cnt++; // 移动数加1
for (int i = curr + 1; i <= min(target, curr + 6); i++) {
// 枚举所有下一个可搜索且未搜索过的方格编号
int r = n - 1 - (i - 1) / n, c = (i - 1) % n; // 根据方格编号获取这个编号的行和列
c += (n - 1 - 2 * c) * ((n - 1 - r) & 1); // 根据行数修正列数
if (visited[r][c])continue; // 跳过搜索过的编号
visited[r][c] = true; // 标记该编号已搜索
int next = board[r][c] == -1 ? i : board[r][c]; // 如果这个编号所在的方格可以转移到其他格子,转移到对应编号;否则就是在当前编号
if (next == target)return cnt; // 到达终点,直接返回最小移动数
Q.emplace(next, cnt); // 加入队列
}
}
return -1; // 退出循环说明没有到达目的地
}
};
示例 1:
输入:board = [[-1,-1,-1,-1,-1,-1],[-1,-1,-1,-1,-1,-1],[-1,-1,-1,-1,-1,-1],[-1,35,-1,-1,13,-1],[-1,-1,-1,-1,-1,-1],[-1,15,-1,-1,-1,-1]]
输出:4
解释:
首先,从方格 1 [第 5 行,第 0 列] 开始。
先决定移动到方格 2 ,并必须爬过梯子移动到到方格 15 。
然后决定移动到方格 17 [第 3 行,第 4 列],必须爬过蛇到方格 13 。
接着决定移动到方格 14 ,且必须通过梯子移动到方格 35 。
最后决定移动到方格 36 , 游戏结束。
可以证明需要至少 4 次移动才能到达最后一个方格,所以答案是 4 。
示例 2:
输入:board = [[-1,-1],[-1,3]]
输出:1
最小基因变化
题目描述
基因序列可以表示为一条由 8 个字符组成的字符串,其中每个字符都是 'A'
、'C'
、'G'
和 'T'
之一。
假设我们需要调查从基因序列 start
变为 end
所发生的基因变化。一次基因变化就意味着这个基因序列中的一个字符发生了变化。
- 例如,
"AACCGGTT" --> "AACCGGTA"
就是一次基因变化。
另有一个基因库 bank
记录了所有有效的基因变化,只有基因库中的基因才是有效的基因序列。(变化后的基因必须位于基因库 bank
中)
给你两个基因序列 start
和 end
,以及一个基因库 bank
,请你找出并返回能够使 start
变化为 end
所需的最少变化次数。如果无法完成此基因变化,返回 -1
。
注意:起始基因序列 start
默认是有效的,但是它并不一定会出现在基因库中。
题目分析
这道题是BFS的模板题,用于查找最短路径问题。
题目代码
#include <string>
#include <queue>
#include <unordered_set>
#include <vector>
using namespace std;
class Solution {
public:
int minMutation(string startGene, string endGene, vector<string>& bank) {
unordered_set<string> string_set(bank.begin(), bank.end());
queue<string> Q;
Q.push(startGene);
unordered_set<string> visited;
visited.emplace(startGene);
int step = 0;
while (!Q.empty())
{
int size = Q.size();
while (size > 0)
{
string cur = Q.front();
Q.pop();
if (cur == endGene)
{
return step;
}
for (char gene : "ACGT")
{
for (int j = 0; j < cur.size(); j++)
{
string next = cur;
next[j] = gene;
if (string_set.count(next) && !visited.count(next))
{
Q.push(next);
visited.emplace(next);
}
}
}
size--;
}
step++;
}
return -1;
}
};
示例 1:
输入:start = "AACCGGTT", end = "AACCGGTA", bank = ["AACCGGTA"]
输出:1
示例 2:
输入:start = "AACCGGTT", end = "AAACGGTA", bank = ["AACCGGTA","AACCGCTA","AAACGGTA"]
输出:2
示例 3:
输入:start = "AAAAACCC", end = "AACCCCCC", bank = ["AAAACCCC","AAACCCCC","AACCCCCC"]
输出:3
字典树
单词搜索Ⅱ
题目描述
给定一个 m x n
二维字符网格 board
和一个单词(字符串)列表 words
, 返回所有二维网格上的单词 。
单词必须按照字母顺序,通过 相邻的单元格 内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母在一个单词中不允许被重复使用。
题目分析
经典的字典树
题目代码
#include <vector>
#include <string>
#include <unordered_map>
#include <iostream>
using namespace std;
/**
* 字典树节点
*/
struct TrieNode {
unordered_map<char, TrieNode*> children; // 子节点列表,存储字符和对应的节点
string str; // 如果是尾节点,存储对应的单词
TrieNode() : str("") {};
};
class Solution {
private:
TrieNode* root; // 根节点
/**
* 将单词word插入字段数root
*/
void insert(string word) {
TrieNode* node = root; // 从根节点开始构造这个word对应的路径节点
for (auto& char_ : word) {
if (node->children.find(char_) == node->children.end()) {
// 字符char_对应的节点不存在,新建一个
node->children.emplace(char_, new TrieNode());
}
// 更新node
node = node->children[char_];
}
node->str = word; // 尾节点记录单词,用于后序查找的时候快速得到
}
/**
* 深度优先搜索的同时,判断当前路径构成的字符串是否为查找单词
* @param board: 二维网格
* @param r: 行号
* @param c: 列号
* @param node:当前字符对应的路径节点
* @param len: 当前路径构成的字符串长度
* @param res:结果集
*/
void dfs_Search(vector<vector<char>>& board, int r, int c, TrieNode* node, int len, vector<string>& res) {
if (len > 10)return; // 字符串长度超过10,返回
char ch = board[r][c]; // 获取当前行列对应的字符
if (node->children.find(ch) == node->children.end())return; // 当前字符对应的节点不存在,即构造的字符串不在words中
TrieNode* last = node; // 记录当前node
node = node->children[ch]; // 更新当前node为当前字符对应得到的节点
if (node->str.size() > 0) {
res.emplace_back(node->str); // 当前节点记录了一个单词,则得到了一个words中的单词
node->str = ""; // 匹配了单词,不重复匹配
};
if (node->children.empty()) {
// 当前节点没有后序字符了,那么这个节点一定是某个单词最后一个字符对应的节点。
// 并且不是其他任何单词的前缀,因此匹配完了之后,可以将这个字符从其父节点的childran列表中删除。
last->children.erase(ch);
return;
}
len++; // 更新长度
board[r][c] = '*'; // 用特殊符号标记当前位置已使用
// 四个方向转递递归
if (r - 1 >= 0 && board[r - 1][c] != '*')dfs_Search(board, r - 1, c, node, len, res);
if (r + 1 < board.size() && board[r + 1][c] != '*')dfs_Search(board, r + 1, c, node, len, res);
if (c - 1 >= 0 && board[r][c - 1] != '*')dfs_Search(board, r, c - 1, node, len, res);
if (c + 1 < board[0].size() && board[r][c + 1] != '*')dfs_Search(board, r, c + 1, node, len, res);
board[r][c] = ch; // 回溯,这个位置处理完了恢复成原来的字符
}
public:
vector<string> findWords(vector<vector<char>>& board, vector<string>& words) {
int m = board.size();
int n = board[0].size();
// 构建words的字典树
root = new TrieNode();
for (auto& word : words) {
if (word.size() > m * n)continue; // 字符串长度超过二维矩阵尺寸,肯定无法构成
insert(word);
}
// 以二维网格的每个位置(i,j)为起点,寻找以其为首字符的所有字符串
vector<string> res;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
dfs_Search(board, i, j, root, 0, res);
}
}
return res;
}
};
示例 1:
输入:board = [["o","a","a","n"],["e","t","a","e"],["i","h","k","r"],["i","f","l","v"]], words = ["oath","pea","eat","rain"]
输出:["eat","oath"]
示例 2:
输入:board = [["a","b"],["c","d"]], words = ["abcb"]
输出:[]
回溯
电话号码的数字组合
题目描述
给定一个仅包含数字 2-9
的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
题目分析
这里的function 和 lambda表达式 需要注意一下
function<void(int)>
表示一个可以接受一个int
类型参数并且没有返回值的函数类型。
这里,[&]
是Lambda表达式的捕获列表,(int i)
是参数列表,省略了返回类型和函数体。以下是各部分的解释:
- 捕获列表
[&]
:&
表示以引用的方式捕获外部作用域中的所有变量。这意味着在Lambda表达式内部可以访问和修改其定义时所在作用域内的所有变量。比如下面代码中的str就是被以引用的方式捕获的。 - 参数列表
(int i)
:这表示Lambda表达式接受一个int
类型的参数i
。
题目代码
#include <iostream>
#include <vector>
#include <string>
#include <functional>
using namespace std;
class Solution {
string MAPPING[10] = { "", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz" };
public:
vector<string> letterCombinations(string digits) {
int n = digits.size();
if (n == 0) return {};
vector<string> ans;
string str(n, 0);
function<void(int)> dfs = [&](int i)
{
if (i == n)
{
ans.emplace_back(str);
return;
}
for (char c : MAPPING[digits[i] - '0'])
{
str[i] = c;
dfs(i + 1);
}
};
dfs(0);
return ans;
}
};
示例 1:
输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
示例 2:
输入:digits = ""
输出:[]
示例 3:
输入:digits = "2"
输出:["a","b","c"]
组合
题目描述
给定两个整数 n
和 k
,返回范围 [1, n]
中所有可能的 k
个数的组合。
你可以按 任何顺序 返回答案。
题目分析
继续积累吧
题目代码
class Solution {
public:
vector<vector<int>> combine(int n, int k) {
vector<vector<int>> ans;
vector<int> path;
function<void(int)> dfs = [&](int i)
{
int d = k - path.size();
if (d == 0)
{
ans.emplace_back(path);
return;
}
for (int j = i; j >= d; j--)
{
path.emplace_back(j);
dfs(j - 1);
path.pop_back();
}
};
dfs(n);
return ans;
}
};
示例 1:
输入:n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
示例 2:
输入:n = 1, k = 1
输出:[[1]]
提示:
1 <= n <= 20
1 <= k <= n
全排列
题目描述
给定一个不含重复数字的数组 nums
,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
题目分析
这里我没有解决的一个问题就是,如何保证已经添加过的数字不会重复添加,解决方式就是使用bool来进行验证
好久没做题了,思路不太清晰
题目代码
#include <iostream>
#include <vector>
#include <functional>
using namespace std;
class Solution {
public:
vector<vector<int>> permute(vector<int>& nums) {
int n = nums.size();
vector<vector<int>> ans;
vector<int> path(n), on_path(n);
function<void(int)> dfs = [&](int i)
{
if (i == n)
{
ans.emplace_back(path);
return;
}
for (int j = 0; j < n; j++)
{
if (!on_path[j])
{
path[i] = nums[j];
on_path[j] = true;
dfs(i + 1);
on_path[j] = false;
}
}
};
dfs(0);
return ans;
}
};
示例 1:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
示例 2:
输入:nums = [0,1]
输出:[[0,1],[1,0]]
示例 3:
输入:nums = [1]
输出:[[1]]
提示:
1 <= nums.length <= 6
-10 <= nums[i] <= 10
nums
中的所有整数 互不相同
组合总和
题目描述
给你一个 无重复元素 的整数数组 candidates
和一个目标整数 target
,找出 candidates
中可以使数字和为目标数 target
的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates
中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target
的不同组合数少于 150
个。
题目分析
这里我们引入了一个新的变量left来进行判断是否刚好为0
主要是学习lambda表达式怎么写
题目代码
#include <vector>
#include <iostream>
#include <functional>
#include <ranges>
#include <algorithm>
using namespace std;
class Solution {
public:
std::vector<std::vector<int>> combinationSum(std::vector<int>& candidates, int target) {
std::ranges::sort(candidates);
std::vector<std::vector<int>> ans;
std::vector<int> path;
function<void(int, int)> dfs = [&](int i, int left)
{
if (left == 0)
{
ans.emplace_back(path);
return;
}
if (left < candidates[i])
{
return;
}
for (int j = i; j < candidates.size(); j++)
{
path.push_back(candidates[j]);
dfs(j, left - candidates[j]);
path.pop_back();
}
};
dfs(0, target);
return ans;
}
};
示例 1:
输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。
示例 2:
输入: candidates = [2,3,5], target = 8
输出: [[2,2,2,2],[2,3,3],[3,5]]
示例 3:
输入: candidates = [2], target = 1
输出: []
####
N 皇后Ⅱ
题目描述
n 皇后问题 研究的是如何将 n
个皇后放置在 n × n
的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n
,返回 n 皇后问题 不同的解决方案的数量。
示例 1:
题目分析
我们要确保每一列 每一列 以及主对角线和副对角线都没有重复的棋子.
题目代码
#include <vector>
#include <functional>
using namespace std;
class Solution {
public:
int totalNQueens(int n) {
int ans = 0;
vector<int> on_path(n), diag1(n * 2 - 1), diag2(n * 2 - 1);
function<void(int)> dfs = [&](int r)
{
if (r == n)
{
ans++;
return;
}
for (int c = 0; c < n; c++)
{
int rc = r - c + n - 1;
if (!on_path[c] && !diag1[r + c] && !diag2[rc]) {
on_path[c] = diag1[r + c] = diag2[rc] = true;
dfs(r + 1);
on_path[c] = diag1[r + c] = diag2[rc] = false; // 恢复现场
}
}
};
dfs(0);
return ans;
}
};
输入:n = 4
输出:2
解释:如上图所示,4 皇后问题存在两个不同的解法。
示例 2:
输入:n = 1
输出:1
括号生成
题目描述
数字 n
代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
题目分析
题目代码
#include <vector>
#include <string>
#include <functional>
using namespace std;
class Solution {
public:
vector<string> generateParenthesis(int n) {
int m = n * 2;
vector<string> ans;
string path(m, 0);
function<void(int, int)> dfs = [&](int i, int open) {
if (i == m) {
ans.emplace_back(path);
return;
}
if (open < n) { // 可以填左括号
path[i] = '(';
dfs(i + 1, open + 1);
}
if (i - open < open) { // 可以填右括号
path[i] = ')';
dfs(i + 1, open);
}
};
dfs(0, 0);
return ans;
}
};
示例 1:
输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]
示例 2:
输入:n = 1
输出:["()"]
提示:
1 <= n <= 8
单词搜索
题目描述
给定一个 m x n
二维字符网格 board
和一个字符串单词 word
。如果 word
存在于网格中,返回 true
;否则,返回 false
。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
题目分析
题目代码
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
bool exist(vector<vector<char>>& board, string word) {
for (int i = 0; i < board.size(); i++) {
for (int j = 0; j < board[0].size(); j++) {
vector<vector<bool>> visited(board.size(), vector<bool>(board[0].size(), false));
if (dfs(board, visited, word, 0, i, j)) return true;
}
}
return false;
}
private:
bool dfs(vector<vector<char>>& board, vector<vector<bool>>& visited, const string& word, int str_idx, int i, int j) {
if (str_idx == word.size()) return true;
// 越界、被访问过、当前位置的字符不是word对应位置的字符
if (i >= board.size() || i < 0 ||
j >= board[0].size() || j < 0 ||
visited[i][j] == true || board[i][j] != word[str_idx]) return false;
visited[i][j] = true;
if (dfs(board, visited, word, str_idx + 1, i + 1, j) ||
dfs(board, visited, word, str_idx + 1, i - 1, j) ||
dfs(board, visited, word, str_idx + 1, i, j + 1) ||
dfs(board, visited, word, str_idx + 1, i, j - 1)) return true;
visited[i][j] = false;
return false;
}
};
示例 1:
输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
输出:true
示例 2:
输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "SEE"
输出:true
示例 3:
输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCB"
输出:false
分治
将有序数组转换为二叉搜索树
题目描述
给你一个整数数组 nums
,其中元素已经按 升序 排列,请你将其转换为一棵 平衡 二叉搜索树。
题目分析
题目代码
#include <vector>
using namespace std;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
class Solution {
private:
/**
* 对范围 [left, right) 的元素生成树
* 选择数组nums中给定范围的 [left, right) 的中点作为根节点,[left, mid)作为左子树,[mid+1, right]作为右子树
* @param nums: 元素数组
* @param left: 左边界
* @param right: 右边界
* @return: 构造的根节点
*/
TreeNode* buildTree(vector<int>& nums, int left, int right) {
if (left >= right)return nullptr; // 范围为空,即没有元素可以构造节点,返回空
int mid = left + ((right - left) >> 1); // 获取范围中点
TreeNode* node = new TreeNode(nums[mid], buildTree(nums, left, mid), buildTree(nums, mid + 1, right)); // 创建根节点并递归生成子树
return node;
}
public:
TreeNode* sortedArrayToBST(vector<int>& nums) {
return buildTree(nums, 0, nums.size()); // 对整个数组范围的元素生成树
}
};
示例 1:
输入:nums = [-10,-3,0,5,9]
输出:[0,-3,9,-10,null,5]
解释:[0,-10,5,null,-3,null,9] 也将被视为正确答案:
示例 2:
输入:nums = [1,3]
输出:[3,1]
解释:[1,null,3] 和 [3,1] 都是高度平衡二叉搜索树。
排序链表
题目描述
给你链表的头结点 head
,请将其按 升序 排列并返回 排序后的链表 。
题目分析
题目代码
#include <vector>
using namespace std;
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* sortList(ListNode* head) {
return mergeSort(head);
}
/**
* 对给定的链表进行归并排序
*/
ListNode* mergeSort(ListNode* head) {
// 如果链表为空或只有一个节点,无需排序直接返回
if (!head || !head->next) {
return head;
}
// 获取链表的中间节点,分别对左右子链表进行排序
ListNode* mid = getMid(head);
ListNode* rightSorted = mergeSort(mid->next); // 排序右子链表
if (mid)mid->next = nullptr; // 断开两段子链表
ListNode* leftSorted = mergeSort(head); // 排序左子链表
return mergeTwoLists(leftSorted, rightSorted); // 两个子链表必然有序,合并两个有序的链表
}
/**
* 获取以head为头节点的链表中间节点
* 如果链表长度为奇数,返回最中间的那个节点
* 如果链表长度为偶数,返回中间靠左的那个节点
*/
ListNode* getMid(ListNode* head) {
if (!head)return head;
ListNode* slow = head, * fast = head->next; // 快慢指针,慢指针初始为
while (fast != nullptr && fast->next != nullptr)
{
fast = fast->next->next; // 快指针每次移动两个节点
slow = slow->next; // 慢指针每次移动一个节点
}
return slow; // 快指针到达链表尾部时,慢指针即指向中间节点
}
/**
* 合并两个有序链表list1和list2
*/
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
ListNode* dummy = new ListNode(); // 伪头节点,用于定位合并链表的头节点
ListNode* node = dummy; // 新链表当前的最后一个节点,初始为伪头节点
// 直到两个链表都遍历完了,合并结束
while (list1 != nullptr || list2 != nullptr) {
int val1 = list1 == nullptr ? 50001 : list1->val; // 如果链表1已经遍历完,val1取最大值,保证链表2的节点被选择到
int val2 = list2 == nullptr ? 50001 : list2->val; // 如果链表2已经遍历完,val2取最大值,保证链表1的节点被选择到
if (val1 < val2) {
// 链表1的节点值更小,加入到合并链表,并更新链表1指向的节点
node->next = list1;
list1 = list1->next;
}
else {
// 链表2的节点值更小,加入到合并链表,并更新链表2指向的节点
node->next = list2;
list2 = list2->next;
}
node = node->next; // 更新合并链表当前的最后一个节点指向
}
return dummy->next; // 伪头节点的下一个节点即为合并链表的头节点
}
};
示例 1:
输入:head = [4,2,1,3]
输出:[1,2,3,4]
示例 2:
输入:head = [-1,5,3,4,0]
输出:[-1,0,3,4,5]
示例 3:
输入:head = []
输出:[]
建立四叉树
题目描述
给你一个 n * n
矩阵 grid
,矩阵由若干 0
和 1
组成。请你用四叉树表示该矩阵 grid
。
你需要返回能表示矩阵 grid
的 四叉树 的根结点。
四叉树数据结构中,每个内部节点只有四个子节点。此外,每个节点都有两个属性:
val
:储存叶子结点所代表的区域的值。1 对应 True,0 对应 False。注意,当isLeaf
为 False 时,你可以把 True 或者 False 赋值给节点,两种值都会被判题机制 接受 。isLeaf
: 当这个节点是一个叶子结点时为 True,如果它有 4 个子节点则为 False 。
class Node {
public boolean val;
public boolean isLeaf;
public Node topLeft;
public Node topRight;
public Node bottomLeft;
public Node bottomRight;
}
我们可以按以下步骤为二维区域构建四叉树:
- 如果当前网格的值相同(即,全为
0
或者全为1
),将isLeaf
设为 True ,将val
设为网格相应的值,并将四个子节点都设为 Null 然后停止。 - 如果当前网格的值不同,将
isLeaf
设为 False, 将val
设为任意值,然后如下图所示,将当前网格划分为四个子网格。 - 使用适当的子网格递归每个子节点。
如果你想了解更多关于四叉树的内容,可以参考 wiki 。
四叉树格式:
你不需要阅读本节来解决这个问题。只有当你想了解输出格式时才会这样做。输出为使用层序遍历后四叉树的序列化形式,其中 null
表示路径终止符,其下面不存在节点。
它与二叉树的序列化非常相似。唯一的区别是节点以列表形式表示 [isLeaf, val]
。
如果 isLeaf
或者 val
的值为 True ,则表示它在列表 [isLeaf, val]
中的值为 1 ;如果 isLeaf
或者 val
的值为 False ,则表示值为 0 。
题目分析
这道题难度在于理解题目意思,其实本身没有那么困难。
题目代码
#include <iostream>
#include <vector>
using namespace std;
class Node {
public:
bool val;
bool isLeaf;
Node* topLeft;
Node* topRight;
Node* bottomLeft;
Node* bottomRight;
Node() {
val = false;
isLeaf = false;
topLeft = NULL;
topRight = NULL;
bottomLeft = NULL;
bottomRight = NULL;
}
Node(bool _val, bool _isLeaf) {
val = _val;
isLeaf = _isLeaf;
topLeft = NULL;
topRight = NULL;
bottomLeft = NULL;
bottomRight = NULL;
}
Node(bool _val, bool _isLeaf, Node* _topLeft, Node* _topRight, Node* _bottomLeft, Node* _bottomRight) {
val = _val;
isLeaf = _isLeaf;
topLeft = _topLeft;
topRight = _topRight;
bottomLeft = _bottomLeft;
bottomRight = _bottomRight;
}
};
class Solution {
public:
Node* construct(vector<vector<int>>& grid) {
int n = grid.size(); // 矩阵尺寸
preSum.resize(n + 1, vector<int>(n + 1)); // 初始化前缀和数组
// 计算二维矩阵前缀和
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
preSum[i + 1][j + 1] = preSum[i][j + 1] + preSum[i + 1][j] - preSum[i][j] + grid[i][j];
}
}
// 构造四叉树
return constructFromArea(grid, 0, 0, n - 1, n - 1);
}
private:
vector<vector<int>> preSum; // 二维数组前缀和
/**
* 生成给定区域[(ltr, ltc), (rtr, rtc)]的子树
* @param grid: 输入矩阵
* @param ltr: 范围矩阵左上角行标
* @param ltc: 范围矩阵左上角列标
* @param rtr: 范围矩阵右下角行标
* @param rtc: 范围矩阵右下角列标
*/
Node* constructFromArea(vector<vector<int>>& grid, int ltr, int ltc, int rbr, int rbc) {
// areaSum[(si, sj), (i,j)] = preSum[i+1][j+1] – preSum[si][j+1] – preSum[i+1][sj] + preSum[si][sj]
int areaSum = preSum[rbr + 1][rbc + 1] - preSum[ltr][rbc + 1] - preSum[rbr + 1][ltc] + preSum[ltr][ltc]; // 计算区域范围和
int areaCnts = (rbr - ltr + 1) * (rbc - ltc + 1); // 计算区域单元格个数
if (areaSum == 0 || areaSum == areaCnts) {
// 如果区域和为0,则该区域全为0;如果区域和等于区域格子数,则区域全为1;这两种情况就是叶子节点
return new Node(areaSum / areaCnts, true);
}
int mr = ltr + (rbr - ltr + 1) / 2; // 中间行的行号
int mc = ltc + (rbc - ltc + 1) / 2; // 中间列的列号
// 将当前区域划分成四个子区域,递归生成子树
return new Node(0, false,
constructFromArea(grid, ltr, ltc, mr - 1, mc - 1),
constructFromArea(grid, ltr, mc, mr - 1, rbc),
constructFromArea(grid, mr, ltc, rbr, mc - 1),
constructFromArea(grid, mr, mc, rbr, rbc)
);
}
};
示例 1:
输入:grid = [[0,1],[1,0]]
输出:[[0,1],[1,0],[1,1],[1,1],[1,0]]
解释:此示例的解释如下:
请注意,在下面四叉树的图示中,0 表示 false,1 表示 True 。
示例 2:
输入:grid = [[1,1,1,1,0,0,0,0],[1,1,1,1,0,0,0,0],[1,1,1,1,1,1,1,1],[1,1,1,1,1,1,1,1],[1,1,1,1,0,0,0,0],[1,1,1,1,0,0,0,0],[1,1,1,1,0,0,0,0],[1,1,1,1,0,0,0,0]]
输出:[[0,1],[1,1],[0,1],[1,1],[1,0],null,null,null,null,[1,0],[1,0],[1,1],[1,1]]
解释:网格中的所有值都不相同。我们将网格划分为四个子网格。
topLeft,bottomLeft 和 bottomRight 均具有相同的值。
topRight 具有不同的值,因此我们将其再分为 4 个子网格,这样每个子网格都具有相同的值。
解释如下图所示:
合并K个升序链表
题目描述
给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。
题目分析
一个巧妙的思路是,把 lists 一分为二(尽量均分),先合并前一半的链表,再合并后一半的链表,然后把这两个链表合并成最终的链表。如何合并前一半的链表呢?我们可以继续一分为二。如此分下去直到只有一个链表,此时无需合并。
题目代码
class Solution {
// 21. 合并两个有序链表
ListNode *mergeTwoLists(ListNode *list1, ListNode *list2) {
ListNode dummy{}; // 用哨兵节点简化代码逻辑
auto cur = &dummy; // cur 指向新链表的末尾
while (list1 && list2) {
if (list1->val < list2->val) {
cur->next = list1; // 把 list1 加到新链表中
list1 = list1->next;
} else { // 注:相等的情况加哪个节点都是可以的
cur->next = list2; // 把 list2 加到新链表中
list2 = list2->next;
}
cur = cur->next;
}
cur->next = list1 ? list1 : list2; // 拼接剩余链表
return dummy.next;
}
// 合并从 lists[i] 到 lists[j-1] 的链表
ListNode *mergeKLists(vector<ListNode *> &lists, int i, int j) {
int m = j - i;
if (m == 0) return nullptr; // 注意输入的 lists 可能是空的
if (m == 1) return lists[i]; // 无需合并,直接返回
auto left = mergeKLists(lists, i, i + m / 2); // 合并左半部分
auto right = mergeKLists(lists, i + m / 2, j); // 合并右半部分
return mergeTwoLists(left, right); // 最后把左半和右半合并
}
public:
ListNode *mergeKLists(vector<ListNode *> &lists) {
return mergeKLists(lists, 0, lists.size());
}
};
示例 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个升序链表
题目描述
给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。
题目分析
合并后的第一个节点 first,一定是某个链表的头节点(因为链表已按升序排列)。
合并后的第二个节点,可能是某个链表的头节点,也可能是 first 的下一个节点。
例如有三个链表 1->2->5, 3->4->6, 4->5->6,找到第一个节点 1 之后,第二个节点不是另一个链表的头节点,而是节点 1 的下一个节点 2。
按照这个过程继续思考,每当我们找到一个节点值最小的节点 x,就把节点 x.next 加入「可能是最小节点」的集合中。
因此,我们需要一个数据结构,它支持:
从数据结构中找到并移除最小节点。
插入节点。
这可以用最小堆实现。初始把所有链表的头节点入堆,然后不断弹出堆中最小节点 x,如果 x.next 不为空就加入堆中。循环直到堆为空。把弹出的节点按顺序拼接起来,就得到了答案。
题目代码
#include <vector>
#include <queue>
using namespace std;
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* mergeKLists(vector<ListNode*>& lists) {
auto cmp = [](const ListNode* a, const ListNode* b) {
return a->val > b->val; // 最小堆
};
priority_queue<ListNode*, vector<ListNode*>, decltype(cmp)> pq;
for (auto head : lists) {
if (head) {
pq.push(head);
}
}
ListNode dummy{}; // 哨兵节点,作为合并后链表头节点的前一个节点
auto cur = &dummy;
while (!pq.empty()) { // 循环直到堆为空
auto node = pq.top(); // 剩余节点中的最小节点
pq.pop();
if (node->next) { // 下一个节点不为空
pq.push(node->next); // 下一个节点有可能是最小节点,入堆
}
cur->next = node; // 合并到新链表中
cur = cur->next; // 准备合并下一个节点
}
return dummy.next; // 哨兵节点的下一个节点就是新链表的头节点
}
};
示例 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 = [[]]
输出:[]
动态规划
最大子数组和
题目描述
给你一个整数数组 nums
,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组
是数组中的一个连续部分。
题目分析
nums[i] 单独组成一个子数组,那么 f[i]=nums[i]。
nums[i] 和前面的子数组拼起来,也就是在以 nums[i−1] 结尾的最大子数组和之后添加 nums[i],那么 f[i]=f[i−1]+nums[i]。
题目代码
#include <vector>
#include <iostream>
#include <ranges>
#include <algorithm>
using namespace std;
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int ans = INT_MIN; // 注意答案可以是负数,不能初始化成 0
int f = 0;
for (int x : nums) {
f = max(f, 0) + x;
ans = max(ans, f);
}
return ans;
}
};
示例 1:
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
示例 2:
输入:nums = [1]
输出:1
示例 3:
输入:nums = [5,4,-1,7,8]
输出:23
提示:
1 <= nums.length <= 105
-104 <= nums[i] <= 104
环形子数组最大和
题目描述
给定一个长度为 n
的环形整数数组 nums
,返回 nums
的非空 子数组 的最大可能和 。
环形数组 意味着数组的末端将会与开头相连呈环状。形式上, nums[i]
的下一个元素是 nums[(i + 1) % n]
, nums[i]
的前一个元素是 nums[(i - 1 + n) % n]
。
子数组 最多只能包含固定缓冲区 nums
中的每个元素一次。形式上,对于子数组 nums[i], nums[i + 1], ..., nums[j]
,不存在 i <= k1, k2 <= j
其中 k1 % n == k2 % n
。
题目分析
题目代码
#include <vector>
using namespace std;
class Solution {
public:
int maxSubarraySumCircular(vector<int>& nums) {
int max_s = INT_MIN; // 最大子数组和,不能为空
int min_s = 0; // 最小子数组和,可以为空
int max_f = 0, min_f = 0, sum = 0;
for (int x : nums) {
// 以 nums[i-1] 结尾的子数组选或不选(取 max)+ x = 以 x 结尾的最大子数组和
max_f = max(max_f, 0) + x;
max_s = max(max_s, max_f);
// 以 nums[i-1] 结尾的子数组选或不选(取 min)+ x = 以 x 结尾的最小子数组和
min_f = min(min_f, 0) + x;
min_s = min(min_s, min_f);
sum += x;
}
return sum == min_s ? max_s : max(max_s, sum - min_s);
}
};
示例 1:
输入:nums = [1,-2,3,-2]
输出:3
解释:从子数组 [3] 得到最大和 3
示例 2:
输入:nums = [5,-3,5]
输出:10
解释:从子数组 [5,5] 得到最大和 5 + 5 = 10
示例 3:
输入:nums = [3,-2,2,-3]
输出:3
解释:从子数组 [3] 和 [3,-2,2] 都可以得到最大和 3
二分查找
搜索插入位置
题目描述
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n)
的算法。
题目分析
注意先减后加,防止溢出
第二最好使用开区间,更好用。
题目代码
#include <vector>
using namespace std;
class Solution {
// lower_bound 返回最小的满足 nums[i] >= target 的 i
// 如果数组为空,或者所有数都 < target,则返回 nums.size()
// 要求 nums 是非递减的,即 nums[i] <= nums[i + 1]
// 闭区间写法
int lower_bound(vector<int>& nums, int target) {
int left = 0, right = (int)nums.size() - 1; // 闭区间 [left, right]
while (left <= right) { // 区间不为空
// 循环不变量:
// nums[left-1] < target
// nums[right+1] >= target
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1; // 范围缩小到 [mid+1, right]
}
else {
right = mid - 1; // 范围缩小到 [left, mid-1]
}
}
return left;
}
// 左闭右开区间写法
int lower_bound2(vector<int>& nums, int target) {
int left = 0, right = nums.size(); // 左闭右开区间 [left, right)
while (left < right) { // 区间不为空
// 循环不变量:
// nums[left-1] < target
// nums[right] >= target
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1; // 范围缩小到 [mid+1, right)
}
else {
right = mid; // 范围缩小到 [left, mid)
}
}
return left;
}
// 开区间写法
int lower_bound3(vector<int>& nums, int target) {
int left = -1, right = nums.size(); // 开区间 (left, right)
while (left + 1 < right) { // 区间不为空
// 循环不变量:
// nums[left] < target
// nums[right] >= target
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid; // 范围缩小到 (mid, right)
}
else {
right = mid; // 范围缩小到 (left, mid)
}
}
return right;
}
public:
int searchInsert(vector<int>& nums, int target) {
return lower_bound3(nums, target); // 选择其中一种写法即可
}
};
示例 1:
输入: nums = [1,3,5,6], target = 5
输出: 2
示例 2:
输入: nums = [1,3,5,6], target = 2
输出: 1
示例 3:
输入: nums = [1,3,5,6], target = 7
输出: 4
搜索二维矩阵
题目描述
给你一个满足下述两条属性的 m x n
整数矩阵:
- 每行中的整数从左到右按非严格递增顺序排列。
- 每行的第一个整数大于前一行的最后一个整数。
给你一个整数 target
,如果 target
在矩阵中,返回 true
;否则,返回 false
。
题目分析
代码实现时,并不需要真的拼成一个长为 mn 的数组 a,而是将 a[i] 转换成矩阵中的行号和列号。例如示例 1,i=9 对应的 a[i]=30,由于矩阵有 n=4 列,所以 a[i] 在 i/n=2 行,在 imodn=1 列。
题目代码
#include <vector>
using namespace std;
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int column = matrix[0].size();
int row = matrix.size();
int left = -1, right = row*column;
while (left + 1 < right)
{
int mid = left + (right - left) / 2;
if (matrix[mid/column][mid%column] == target)
{
return true;
}
if (matrix[mid / column][mid % column] < target)
{
left = mid;
}
else
{
right = mid;
}
}
return false;
}
};
示例 1:
输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 3
输出:true
示例 2:
输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 13
输出:false
寻找峰值
题目描述
峰值元素是指其值严格大于左右相邻值的元素。
给你一个整数数组 nums
,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回 任何一个峰值 所在位置即可。
你可以假设 nums[-1] = nums[n] = -∞
。
你必须实现时间复杂度为 O(log n)
的算法来解决此问题。
题目分析
定理:如果 i<n−1 且 nums[i]<nums[i+1],那么在下标 [i+1,n−1] 中一定存在至少一个峰值。
同理可得,如果 i<n−1 且 nums[i]>nums[i+1],那么在 [0,i] 中一定存在至少一个峰值。
记住这个就行了
题目代码
#include <vector>
using namespace std;
class Solution {
public:
int findPeakElement(vector<int>& nums) {
int left = -1, right = nums.size() - 1;
while (left + 1 < right)
{
int mid = left + (right - left) / 2;
if (nums[mid] > nums[mid + 1])
{
right = mid;
}
else
{
left = mid;
}
}
return right;
}
};
示例 1:
输入:nums = [1,2,3,1]
输出:2
解释:3 是峰值元素,你的函数应该返回其索引 2。
示例 2:
输入:nums = [1,2,1,3,5,6,4]
输出:1 或 5
解释:你的函数可以返回索引 1,其峰值元素为 2;
或者返回索引 5, 其峰值元素为 6。
搜索旋转排列数组
题目描述
整数数组 nums
按升序排列,数组中的值 互不相同 。
在传递给函数之前,nums
在预先未知的某个下标 k
(0 <= k < nums.length
)上进行了 旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]]
(下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7]
在下标 3
处经旋转后可能变为 [4,5,6,7,0,1,2]
。
给你 旋转后 的数组 nums
和一个整数 target
,如果 nums
中存在这个目标值 target
,则返回它的下标,否则返回 -1
。
你必须设计一个时间复杂度为 O(log n)
的算法解决此问题。
题目分析
- 如果 target>nums[n−1],那么 target 一定在第一段 [0,i−1] 中,在 [0,i−1] 中二分查找 target。
- 如果 target≤nums[n−1],那么:
如果 i=0,说明 nums 是递增的,直接在 [0,n−1] 中二分查找 target。
如果 i>0,那么 target 一定在第二段 [i,n−1] 中,在 [i,n−1] 中二分查找 target。
这两种情况可以合并成:在 [i,n−1] 中二分查找 target。
二分的范围可以是 [0,n−2]
。
这是因为,如果 nums[n−1] 是数组最小值,那么 nums 分成两段,第一段 [0,n−2],第二段 [n−1,n−1],且第一段的所有数都大于 nums[n−1]。每次 x 和 nums[n−1] 比大小,一定是 x>nums[n−1]。这意味着每次二分更新的都是 left,那么最终答案自然就是 n−1。
题目代码
#include <vector>
using namespace std;
class Solution {
// 153. 寻找旋转排序数组中的最小值
int findMin(vector<int>& nums) {
int left = -1, right = nums.size() - 1; // 开区间 (-1, n-1)
while (left + 1 < right) { // 开区间不为空
int mid = left + (right - left) / 2;
if (nums[mid] < nums.back()) {
right = mid;
}
else {
left = mid;
}
}
return right;
}
// 有序数组中找 target 的下标
int lower_bound(vector<int>& nums, int left, int right, int target) {
while (left + 1 < right) { // 开区间不为空
// 循环不变量:
// nums[left] < target
// nums[right] >= target
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid; // 范围缩小到 (mid, right)
}
else {
right = mid; // 范围缩小到 (left, mid)
}
}
return nums[right] == target ? right : -1;
}
public:
int search(vector<int>& nums, int target) {
int i = findMin(nums);
if (target > nums.back()) { // target 在第一段
return lower_bound(nums, -1, i, target); // 开区间 (-1, i)
}
// target 在第二段
return lower_bound(nums, i - 1, nums.size(), target); // 开区间 (i-1, n)
}
};
示例 1:
输入:nums = [4,5,6,7,0,1,2], target = 0
输出:4
示例 2:
输入:nums = [4,5,6,7,0,1,2], target = 3
输出:-1
示例 3:
输入:nums = [1], target = 0
输出:-1
在排序数组中查找元素的第一个和最后一个位置
题目描述
给你一个按照非递减顺序排列的整数数组 nums
,和一个目标值 target
。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target
,返回 [-1, -1]
。
你必须设计并实现时间复杂度为 O(log n)
的算法解决此问题。
示例 1:
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
示例 2:
输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]
示例 3:
输入:nums = [], target = 0
输出:[-1,-1]
题目分析
先判断是否存在target,然后通过target加1找到target的后一下标,然后下标减一就行了。
题目代码
#include <vector>
using namespace std;
class Solution {
public:
// 开区间写法
int lower_bound3(vector<int>& nums, int target) {
int left = -1, right = nums.size(); // 开区间 (left, right)
while (left + 1 < right) { // 区间不为空
// 循环不变量:
// nums[left] < target
// nums[right] >= target
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid; // 范围缩小到 (mid, right)
}
else {
right = mid; // 范围缩小到 (left, mid)
}
// 也可以这样写
// (nums[mid] < target ? left : right) = mid;
}
return right;
}
vector<int> searchRange(vector<int>& nums, int target) {
int start = lower_bound3(nums, target); // 使用其中一种写法即可
if (start == nums.size() || nums[start] != target) {
return { -1, -1 }; // nums 中没有 target
}
// 如果 start 存在,那么 end 必定存在
int end = lower_bound3(nums, target + 1) - 1;
return { start, end };
}
};
寻找两个正序数组的中位数
题目描述
给定两个大小分别为 m
和 n
的正序(从小到大)数组 nums1
和 nums2
。请你找出并返回这两个正序数组的 中位数 。
算法的时间复杂度应该为 O(log (m+n))
。
题目分析
看注释吧 这个还得慢慢理解
题目代码
#include <vector>
#include <iostream>
using namespace std;
class Solution {
public:
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int m = nums1.size(); // 取原始数组长度
int n = nums2.size();
nums1.emplace_back(INT_MAX); // 追加哨兵位
nums2.emplace_back(INT_MAX);
int k = (m + n + 1) / 2; // 获取中间数是第几个数(偶数取前一个数)
int idx1 = 0; // 遍历指针,指向当前取得数组的元素
int idx2 = 0;
bool flag = false; // 标记中间数取的是否为nums1的元素
while (true) {
// 数组1元素全部取完,剩下的元 素由数组2提供
if (idx1 == m) {
// [idx2, ?]提供k个元素,即?-idx2+1=k => ? = idx2+k-1
idx2 += k - 1;
break;
}
// 数组2元素全部取完,剩下的元素由数组1提供
if (idx2 == n) {
idx1 += k - 1;
flag = true;
break;
}
// 为了使指针停在中间数的位置,最后一个数单独处理
if (k == 1) {
flag = nums1[idx1] < nums2[idx2]; // 指针无需移动,只需要判断取哪个数组的元素
break;
}
// 每次两个数组各出k/2的元素,然后取数字更小的那一组
int half = k / 2;
int mid1 = min(idx1 + half, m) - 1; // [idx1, mid1] 范围元素,确保mid1不越界
int mid2 = min(idx2 + half, n) - 1;
// mid1更小,那么mid1一定不会是第k个数,因此淘汰[idx1, mid1]往后找;否则淘汰[idx2, mid2]往后找
// k减少对应区间的元素个数 idx-mid + 1
if (nums1[mid1] < nums2[mid2]) {
k -= mid1 - idx1 + 1;
idx1 = mid1 + 1;
}
else {
k -= mid2 - idx2 + 1;
idx2 = mid2 + 1;
}
}
// 总元素个数为奇数,直接返回中间数,根据flag判断中间数是在nums1还是在nums2
if ((m + n) & 1)return (flag ? nums1[idx1] : nums2[idx2]) / 1.0;
// 总元素个数为偶数,不仅要取中间数,还要取中间数的后一位取平均
// 根据flag判断中间数是在nums1还是在nums2,中间数的下一个数要么是中间数同数组的后一位,要么是另一个数组的当前指向元素
if (flag)return (nums1[idx1] + min(nums1[idx1 + 1], nums2[idx2])) / 2.0;
return (nums2[idx2] + min(nums2[idx2 + 1], nums1[idx1])) / 2.0;
}
};
示例 1:
输入:nums1 = [1,3], nums2 = [2]
输出:2.00000
解释:合并数组 = [1,2,3] ,中位数 2
示例 2:
输入:nums1 = [1,2], nums2 = [3,4]
输出:2.50000
解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5
堆排序
数组中的第K个最大元素
题目描述
给定整数数组 nums
和整数 k
,请返回数组中第 **k**
个最大的元素。
请注意,你需要找的是数组排序后的第 k
个最大的元素,而不是第 k
个不同的元素。
你必须设计并实现时间复杂度为 O(n)
的算法解决此问题。
题目分析
我们可以借助一个小顶堆来维护当前堆内元素的最小值,同时保证堆的大小为 k:
遍历数组将元素入堆;
如果当前堆内元素超过 k 了,我们就把堆顶元素去除,即去除当前的最小值。
题目代码
#include <vector>
#include <queue>
using namespace std;
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
priority_queue<int, vector<int>, greater<int>> pq;
for (auto& num : nums)
{
pq.emplace(num);
if (pq.size() > k)
{
pq.pop();
}
}
return pq.top();
}
};
示例 1:
输入: [3,2,1,5,6,4], k = 2
输出: 5
示例 2:
输入: [3,2,3,1,2,4,5,5,6], k = 4
输出: 4
IPO
题目描述
假设 力扣(LeetCode)即将开始 IPO 。为了以更高的价格将股票卖给风险投资公司,力扣 希望在 IPO 之前开展一些项目以增加其资本。 由于资源有限,它只能在 IPO 之前完成最多 k
个不同的项目。帮助 力扣 设计完成最多 k
个不同项目后得到最大总资本的方式。
给你 n
个项目。对于每个项目 i
,它都有一个纯利润 profits[i]
,和启动该项目需要的最小资本 capital[i]
。
最初,你的资本为 w
。当你完成一个项目时,你将获得纯利润,且利润将被添加到你的总资本中。
总而言之,从给定项目中选择 最多 k
个不同项目的列表,以 最大化最终资本 ,并输出最终可获得的最多资本。
答案保证在 32 位有符号整数范围内。
题目分析
题目代码
#include <vector>
#include <queue>
#include <numeric>
using namespace std;
class Solution {
public:
int findMaximizedCapital(int k, int w, vector<int>& profits, vector<int>& capital) {
// 生成索引序列
int n = capital.size();
vector<int> indexes(n);
iota(indexes.begin(), indexes.end(), 0);
// 根据资本值对索引进行升序排序
sort(indexes.begin(), indexes.end(), [&](int i, int j) {
return capital[i] < capital[j];
});
priority_queue<int> pq; // 维护堆内利润值的大顶堆
int i = 0;
while (k-- > 0) {
// 将启动资本小于等于当前资本的项目的利润加入大顶堆
while (i < n && capital[indexes[i]] <= w) {
pq.emplace(profits[indexes[i++]]);
}
if (pq.empty())break; // 没有可以启动的项目,后面启动资本更大的项目也无法启动,退出
w += pq.top(); // 选择启动资本满足条件的项目中利润最大的那个,更新w
pq.pop();
}
return w;
}
};
示例 1:
输入:k = 2, w = 0, profits = [1,2,3], capital = [0,1,1]
输出:4
解释:
由于你的初始资本为 0,你仅可以从 0 号项目开始。
在完成后,你将获得 1 的利润,你的总资本将变为 1。
此时你可以选择开始 1 号或 2 号项目。
由于你最多可以选择两个项目,所以你需要完成 2 号项目以获得最大的资本。
因此,输出最后最大化的资本,为 0 + 1 + 3 = 4。
示例 2:
输入:k = 3, w = 0, profits = [1,2,3], capital = [0,1,2]
输出:6
查找和最小的K对数字
题目描述
给定两个以 非递减顺序排列 的整数数组 nums1
和 nums2
, 以及一个整数 k
。
定义一对值 (u,v)
,其中第一个元素来自 nums1
,第二个元素来自 nums2
。
请找到和最小的 k
个数对 (u1,v1)
, (u2,v2)
… (uk,vk)
。
题目分析
题目代码
#include <vector>
#include <queue>
using namespace std;
class Solution {
public:
vector<vector<int>> kSmallestPairs(vector<int>& nums1, vector<int>& nums2, int k) {
int n = nums1.size(), m = nums2.size();
vector<vector<int>> ans;
priority_queue<tuple<int, int, int>> pq;
for (int i = 0; i < min(n, k); i++) { // 至多 k 个
pq.emplace(-nums1[i] - nums2[0], i, 0); // 取相反数变成小顶堆
}
while (ans.size() < k) {
auto [_, i, j] = pq.top();
pq.pop();
ans.push_back({ nums1[i], nums2[j] });
if (j + 1 < m) {
pq.emplace(-nums1[i] - nums2[j + 1], i, j + 1);
}
}
return ans;
}
};
示例 1:
输入: nums1 = [1,7,11], nums2 = [2,4,6], k = 3
输出: [1,2],[1,4],[1,6]
解释: 返回序列中的前 3 对数:
[1,2],[1,4],[1,6],[7,2],[7,4],[11,2],[7,6],[11,4],[11,6]
示例 2:
输入: nums1 = [1,1,2], nums2 = [1,2,3], k = 2
输出: [1,1],[1,1]
解释: 返回序列中的前 2 对数:
[1,1],[1,1],[1,2],[2,1],[1,2],[2,2],[1,3],[1,3],[2,3]
数据流的中位数
题目描述
中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。
- 例如
arr = [2,3,4]
的中位数是3
。 - 例如
arr = [2,3]
的中位数是(2 + 3) / 2 = 2.5
。
实现 MedianFinder 类:
MedianFinder()
初始化MedianFinder
对象。void addNum(int num)
将数据流中的整数num
添加到数据结构中。double findMedian()
返回到目前为止所有元素的中位数。与实际答案相差10-5
以内的答案将被接受。
题目分析
建立一个 小顶堆 A 和 大顶堆 B ,各保存列表的一半元素,且规定:
- A 保存 较大 的一半,长度为 2N( N 为偶数)或 2N+1( N 为奇数)。
- B 保存 较小 的一半,长度为 2N( N 为偶数)或 2N−1( N 为奇数)。
函数 addNum(num) :
- 当 m=n(即 N 为 偶数):需向 A 添加一个元素。实现方法:将新元素 num 插入至 B ,再将 B 堆顶元素插入至 A 。
- 当 m\=n(即 N 为 奇数):需向 B 添加一个元素。实现方法:将新元素 num 插入至 A ,再将 A 堆顶元素插入至 B 。
题目代码
#include <vector>
#include <queue>
using namespace std;
class MedianFinder {
public:
priority_queue<int, vector<int>, greater<int>> A; // 小顶堆,保存较大的一半
priority_queue<int, vector<int>, less<int>> B; // 大顶堆,保存较小的一半
MedianFinder() { }
void addNum(int num) {
if (A.size() != B.size()) {
A.push(num);
B.push(A.top());
A.pop();
}
else {
B.push(num);
A.push(B.top());
B.pop();
}
}
double findMedian() {
return A.size() != B.size() ? A.top() : (A.top() + B.top()) / 2.0;
}
};
示例 1:
输入
["MedianFinder", "addNum", "addNum", "findMedian", "addNum", "findMedian"]
[[], [1], [2], [], [3], []]
输出
[null, null, null, 1.5, null, 2.0]
解释
MedianFinder medianFinder = new MedianFinder();
medianFinder.addNum(1); // arr = [1]
medianFinder.addNum(2); // arr = [1, 2]
medianFinder.findMedian(); // 返回 1.5 ((1 + 2) / 2)
medianFinder.addNum(3); // arr[1, 2, 3]
medianFinder.findMedian(); // return 2.0
位运算
二进制求和
题目描述
给你两个二进制字符串 a
和 b
,以二进制字符串的形式返回它们的和。
题目分析
由于这道题模拟的是二进制数的加法,因此 s[i] 和 add 可以采用位运算得到。
位异或可以模拟无进位的两个二进制加法,因此 s[i] = a[i] ^ b[i] ^ add;
a[i]、b[i] 和 add 只要至少两个数为 1 即可以产生进位。判断两个数都为 1 可以通过位与操作。三个数至少两个数为 1 可以通过位或两两之间的位与实现。即 add = (a[i] & b[i]) | (a[i] & add) | (b[i] & add)
题目代码
#include <string>
using namespace std;
class Solution {
public:
string addBinary(string a, string b) {
string sum;
int add = 0;
int i = a.size() - 1;
int j = b.size() - 1;
while (i >= 0 || j >= 0)
{
int aDigit = i == -1 ? 0 : a[i--] - '0';
int bDigit = j == -1 ? 0 : b[j--] - '0';
int sDigit = aDigit ^ bDigit ^ add;
add = (aDigit & bDigit) | (aDigit & add) | (bDigit & add); // 位运算计算当前进位位
sum += to_string(sDigit); // 更新当前位
}
if (add == 1)sum += to_string(1); // 已有位加完后,进位位为1,说明产生高位
reverse(sum.begin(), sum.end()); // 逆序反转
return sum;
}
};
示例 1:
输入:a = "11", b = "1"
输出:"100"
示例 2:
输入:a = "1010", b = "1011"
输出:"10101"
颠倒二进制位
题目描述
颠倒给定的 32 位无符号整数的二进制位。
题目分析
获取每一位 i
- 位与1;【获取到最低位的值】
- 右移一位;【去掉最低位,将次低位移到最低位】
更新 31-i
位
- 将上一步得到的位左移到
31 - i
; - 位或结果
res
。
题目代码
#include <cstdint>
class Solution {
public:
uint32_t reverseBits(uint32_t n) {
int idx = 0; // 位索引
uint32_t res = 0; // 反转结果,初始为0表示所有位都为0
// 循环处理到最高位1
while (n > 0) {
int digit = n & 1; // 获取当前最低位
n >>= 1; // 将最低位右移掉
res |= (digit << (31 - idx++)); // 将这个最低位反转到它实际位置上去
}
return res;
}
};
提示:
- 请注意,在某些语言(如 Java)中,没有无符号整数类型。在这种情况下,输入和输出都将被指定为有符号整数类型,并且不应影响您的实现,因为无论整数是有符号的还是无符号的,其内部的二进制表示形式都是相同的。
- 在 Java 中,编译器使用二进制补码记法来表示有符号整数。因此,在 示例 2 中,输入表示有符号整数
-3
,输出表示有符号整数-1073741825
。
示例 1:
输入:n = 00000010100101000001111010011100
输出:964176192 (00111001011110000010100101000000)
解释:输入的二进制串 00000010100101000001111010011100 表示无符号整数 43261596,
因此返回 964176192,其二进制表示形式为 00111001011110000010100101000000。
示例 2:
输入:n = 11111111111111111111111111111101
输出:3221225471 (10111111111111111111111111111111)
解释:输入的二进制串 11111111111111111111111111111101 表示无符号整数 4294967293,
因此返回 3221225471 其二进制表示形式为 10111111111111111111111111111111 。
位1的个数
题目描述
编写一个函数,获取一个正整数的二进制形式并返回其二进制表达式中 设置位 的个数(也被称为汉明重量)。
题目分析
我们可以通过位运算:
- 通过
& 1
:获取最低位的值; - 通过
>> 1
:每次右移一位来更新最低位
这样的时间复杂度为 O(C)
,C
为数字的数据类型的位数。
位运算优化
暴力枚举是固定要枚举所有位的,有没有什么办法加快这个进程呢?
我们可以用 n = n & (n – 1) 代替 n = n >> 1 进行 n 的更新。
每一次 n = n & (n - 1)
都会把当前 n
的最低位 1
移除掉,直到最后所有 1
都被消除,n
变为 0
。
题目代码
class Solution {
public:
int hammingWeight(int n) {
int c = 0;
while (n)
{
n = n & (n - 1);
c++;
}
return c;
}
};
示例 1:
输入:n = 11
输出:3
解释:输入的二进制串 1011 中,共有 3 个设置位。
示例 2:
输入:n = 128
输出:1
解释:输入的二进制串 10000000 中,共有 1 个设置位。
示例 3:
输入:n = 2147483645
输出:30
解释:输入的二进制串 11111111111111111111111111111101 中,共有 30 个设置位。
只出现一次的数字
题目描述
给你一个 非空 整数数组 nums
,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。
题目分析
利用异或运算 a⊕a=0 的性质,我们可以用异或来「消除」所有出现了两次的元素,最后剩下的一定是只出现一次的元素。
题目代码
#include <vector>
using namespace std;
class Solution {
public:
int singleNumber(vector<int>& nums) {
int ans = 0;
for (int x : nums)
{
ans ^= x;
}
return ans;
}
};
示例 1 :
输入:nums = [2,2,1]
输出:1
示例 2 :
输入:nums = [4,1,2,1,2]
输出:4
示例 3 :
输入:nums = [1]
输出:1
只出现一次的数字Ⅱ
题目描述
给你一个整数数组 nums
,除某个元素仅出现 一次 外,其余每个元素都恰出现 三次 。请你找出并返回那个只出现了一次的元素。
你必须设计并实现线性时间复杂度的算法且使用常数级空间来解决此问题。
题目分析
137. 只出现一次的数字 II – 力扣(LeetCode)
题目代码
#include <vector>
using namespace std;
class Solution {
public:
int singleNumber(vector<int>& nums) {
int a = 0, b = 0;
for (int x : nums) {
b = (b ^ x) & ~a;
a = (a ^ x) & ~b;
}
return b;
}
};
示例 1:
输入:nums = [2,2,3,2]
输出:3
示例 2:
输入:nums = [0,1,0,1,0,1,99]
输出:99
数字范围按位与
题目描述
给你两个整数 left
和 right
,表示区间 [left, right]
,返回此区间内所有数字 按位与 的结果(包含 left
、right
端点)。
题目分析
而我们要找范围 [left, right]
元素的位公共前缀,等价于找 left
和 right
的公共前缀;
题目代码
class Solution {
public:
int rangeBitwiseAnd(int left, int right) {
int cnt = 0;
while (left != right)
{
cnt++;
left >>= 1;
right >>= 1;
}
return left << cnt;
}
};
示例 1:
输入:left = 5, right = 7
输出:4
示例 2:
输入:left = 0, right = 0
输出:0
示例 3:
输入:left = 1, right = 2147483647
输出:0
回文数
题目分析
给你一个整数 x
,如果 x
是一个回文整数,返回 true
;否则,返回 false
。
回文数
是指正序(从左向右)和倒序(从右向左)读都是一样的整数。
- 例如,
121
是回文,而123
不是。
题目分析
题目代码
class Solution {
public:
bool isPalindrome(int x) {
// 特殊情况:
// 如上所述,当 x < 0 时,x 不是回文数。
// 同样地,如果数字的最后一位是 0,为了使该数字为回文,
// 则其第一位数字也应该是 0
// 只有 0 满足这一属性
if (x < 0 || (x % 10 == 0 && x != 0)) {
return false;
}
int revertedNumber = 0;
while (x > revertedNumber) {
revertedNumber = revertedNumber * 10 + x % 10;
x /= 10;
}
// 当数字长度为奇数时,我们可以通过 revertedNumber/10 去除处于中位的数字。
// 例如,当输入为 12321 时,在 while 循环的末尾我们可以得到 x = 12,revertedNumber = 123,
// 由于处于中位的数字不影响回文(它总是与自己相等),所以我们可以简单地将其去除。
return x == revertedNumber || x == revertedNumber / 10;
}
};
示例 1:
输入:x = 121
输出:true
示例 2:
输入:x = -121
输出:false
解释:从左向右读, 为 -121 。 从右向左读, 为 121- 。因此它不是一个回文数。
示例 3:
输入:x = 10
输出:false
解释:从右向左读, 为 01 。因此它不是一个回文数。
加一
题目描述
给定一个由 整数 组成的 非空 数组所表示的非负整数,在该数的基础上加一。
最高位数字存放在数组的首位, 数组中每个元素只存储单个数字。
你可以假设除了整数 0 之外,这个整数不会以零开头。
题目分析
题目代码
#include <vector>
using namespace std;
class Solution {
public:
vector<int> plusOne(vector<int>& digits) {
int n = digits.size();
vector<int> res(digits.begin(), digits.end()); // 拷贝一个digits
res[n - 1]++; // 最后一位+1
int add = 0; // 进位位
for (int i = n - 1; i >= 0; i--) {
res[i] += add; // 除了最后一位,其他每一位都需要累加进位位;初始add=0,最后一位累加进位无影响
add = res[i] / 10; // 根据当前位的值,判断是否产生进位
res[i] %= 10; // 取模值
if (add == 0)return res; // 如果没有产生进位,剩下的位不会再变化,直接返回
}
// 最高位产生进位,这个数字一定是9...9 + 1,结果就是10...0
// 多产生了一位高位1,其他位都为0
vector<int> res_(n + 1);
res_[0] = 1;
return res_;
}
};
示例 1:
输入:digits = [1,2,3]
输出:[1,2,4]
解释:输入数组表示数字 123。
示例 2:
输入:digits = [4,3,2,1]
输出:[4,3,2,2]
解释:输入数组表示数字 4321。
示例 3:
输入:digits = [0]
输出:[1]
阶乘后的零
题目描述
给定一个整数 n
,返回 n!
结果中尾随零的数量。
提示 n! = n * (n - 1) * (n - 2) * ... * 3 * 2 * 1
题目分析
这道题要统计整数 n 阶乘的结果中有多少个尾部的 0。一个数如果有尾随的 0,那一定可以表示成 m x 10^n,并且 n 就是尾随 0 的个数,也是这个数可以拆分出来的因子 10 的个数。
因此我们就要看 n 阶乘,即 [1,n] 这 n 个数相乘,能够拆分出多少个因子 10。而因子 10 = 2 x 5,因此就是统计有多少对因子 2 和 5。
而在一段连续的数当中,2 的倍数一定比 5 的倍数多,即因子 2 一定比因子 5 多。因此我们实际要统计的就是 因子5的个数。
题目代码
class Solution {
public:
int trailingZeroes(int n) {
int res = 0; // 统计[1, n]的因子5的个数
// 只要n大于等于5,就一定包含5的倍数
while (n >= 5) {
n /= 5; // 每次循环,n / 5 是在统计n中有多少5^i的倍数,i是循环次数
res += n; // 这些倍数都可以分解一个因子5
}
return res;
}
};
示例 1:
输入:n = 3
输出:0
解释:3! = 6 ,不含尾随 0
示例 2:
输入:n = 5
输出:1
解释:5! = 120 ,有一个尾随 0
示例 3:
输入:n = 0
输出:0
x的平方根
题目描述
给你一个非负整数 x
,计算并返回 x
的 算术平方根 。
由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。
注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5)
或者 x ** 0.5
。
题目分析
我们要搜索 i 肯定属于 [1, x + 1),并且满足随着 i 增大,搜索值 i ^ 2 也是增大的。因此我们就变成了在元素 [1, x+1) 这个有序区间内,找到最后一个满足 i ^ 2 <= x 的值。
而我们最后取到的值是 左边界-1(因为是闭区间)
题目代码
class Solution {
public:
int mySqrt(int x) {
// 1和0的算术平方根为其自身
if(x <= 1)return x;
// 对于大于1的数x,其向下取整的算术平方根都小于等于x/2
// 我们的搜索区间是左闭右开,因此右边界取到x/2+1
int left = 0;
int right = x / 2 + 1;
while(left < right){
int mid = left + ((right - left) >> 1);
if((long long)mid * mid <= x){
// 找到一个满足算数平方根的数,暂存ans = mid
// 向右查找满足条件的更大,left = mid + 1。因此ans = left - 1,我们可以不用暂存ans
left = mid + 1;
}else{
right = mid; // 数值过大,不为算术平方根,向左查找
}
}
return left - 1;
}
};
示例 1:
输入:x = 4
输出:2
示例 2:
输入:x = 8
输出:2
解释:8 的算术平方根是 2.82842..., 由于返回类型是整数,小数部分将被舍去。
Pow(x,n)
题目描述
实现 pow(x, n) ,即计算 x
的整数 n
次幂函数(即,xn
)。
题目分析
题目代码
class Solution {
public:
double myPow(double x, int N) {
double ans = 1;
long long n = N;
if (n < 0)
{
n = -n;
x = 1 / x;
}
while (n)
{
if (n & 1)
{
ans *= x;
}
x *= x;
n >>= 1;
}
return ans;
}
};
示例 1:
输入:x = 2.00000, n = 10
输出:1024.00000
示例 2:
输入:x = 2.10000, n = 3
输出:9.26100
示例 3:
输入:x = 2.00000, n = -2
输出:0.25000
解释:2-2 = 1/22 = 1/4 = 0.25
直线上最多的点数
题目描述
给你一个数组 points
,其中 points[i] = [xi, yi]
表示 X-Y 平面上的一个点。求最多有多少个点在同一条直线上。
题目分析
在计算斜率时,得出分子与分母上的最大公约数,如果这个斜率是第一次出现,则在哈希表中初始化为1,往后再出现的时候,数量加一
题目代码
#include <vector>
#include <unordered_map>
using namespace std;
class Solution {
private:
const int K = 20001; // 生成斜率二元组编号的系数
/**
* 辗转相除法求a和b的最大公约数
*/
int gcd(int a, int b) {
if (a == 1 || b == 1)return 1;
while (b > 0) {
int tmp = a;
a = b;
b = tmp % b;
}
return a;
}
/**
* 获取给定dx和dy的斜率二元组(dx, dy)编号
* 斜率编号 id = dx * 20001 + dy
* 为了正确表示斜率,需要对dx和dy进行约分,即二者同时除以最大公约数
* 斜率区分正负,我们可以根据斜率符号对编号加上正负进行区分
* 特别的,对于dx = 0(dy = 0),无论dy(dx)值为多少,斜率都是一样为0(∞),我们用INT_MAX最大值表示无穷
*/
int getK(int dx, int dy) {
if (dx == 0)return 0; // dx为0 返回斜率编号为0
if (dy == 0)return INT_MAX; // dy为0,返回斜率编号为极大值
int sign = dx * dy > 0 ? 1 : -1; // 获取斜率编号符号
dx = abs(dx);
dy = abs(dy);
int g = gcd(dx, dy); // 将dx和dy取绝对值后取最大公约数
dx /= g;
dy /= g; // 得到斜率分子和分母绝对值
// std::cout << "dx: " << dx << " dy: " << dy << std::endl;
return sign * (dx * K + dy); // 返回绝对值编号
}
public:
int maxPoints(vector<vector<int>>& points) {
int n = points.size();
if (n <= 2)return n; // 不超过两个点的一定共线
int res = 0;
for (int i = 0; i < n; i++) {
// 如果当前最大共线点数超过剩下要枚举的点数,则一定是最大值
if (res >= n - i)
break;
// 获取(x1, y1)所在直线的最大点数
int x1 = points[i][0], y1 = points[i][1];
int maxCnt = 1; // 统计(x1, y1)所在直线的最大点数
unordered_map<int, int> kCnts; // 统计(x1, y1)所在各直线的点数
for (int j = i + 1; j < n; j++) {
int x2 = points[j][0], y2 = points[j][1];
int k = getK(x2 - x1, y2 - y1); // 获取(x1, y1)和(x2, y2)的斜率编号
if (kCnts.find(k) == kCnts.end()) {
kCnts[k] = 1; // 这个斜率首次出现,初始值为1,表示(x1, y1)这个点
}
kCnts[k]++; // 累加1,表示加入(x2, y2)这个点
}
// 统计(x1, y1)在线的最大共线点数
for (auto& kv : kCnts) {
maxCnt = max(maxCnt, kv.second);
}
// 更新结果
res = max(res, maxCnt);
}
return res;
}
};
示例 1:
输入:points = [[1,1],[2,2],[3,3]]
输出:3
示例 2:
输入:points = [[1,1],[3,2],[5,3],[4,1],[2,3],[1,4]]
输出:4
动态规划
爬楼梯
题目描述
假设你正在爬楼梯。需要 n
阶你才能到达楼顶。
每次你可以爬 1
或 2
个台阶。你有多少种不同的方法可以爬到楼顶呢?
题目分析
题目代码
#include <vector>
using namespace std;
class Solution {
public:
vector<int> b;
int dfs(int n)
{
if (n <= 1)
{
return 1;
}
int& res = b[n];
if (res)
{
return res;
}
return res = dfs(n - 2) + dfs(n - 1);
}
int climbStairs(int n) {
b.resize(n + 1);
return dfs(n);
}
};
class Solution {
public:
int climbStairs(int n) {
vector<int> f(n + 1);
f[0] = f[1] = 1;
for (int i = 2; i <= n; i++) {
f[i] = f[i - 1] + f[i - 2];
}
return f[n];
}
};
示例 1:
输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶
示例 2:
输入:n = 3
输出:3
解释:有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶
打家劫舍
题目描述
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
题目分析
题目代码
#include <vector>
using namespace std;
class Solution {
public:
int rob(vector<int>& nums) {
int n = nums.size();
vector<int> f(n + 2);
for (int i = 0; i < n; i++) {
f[i + 2] = max(f[i + 1], f[i] + nums[i]);
}
return f[n + 1];
}
};
示例 1:
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:
输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。
单词拆分
题目描述
给你一个字符串 s
和一个字符串列表 wordDict
作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s
则返回 true
。
注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
题目分析
题目代码
#include <vector>
#include <unordered_set>
#include <string>
using namespace std;
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
int n = s.size();
unordered_set<string> st = { wordDict.begin(),wordDict.end() };
vector<int> memo(n, -1);
auto dfs = [&](auto&& dfs, int i) -> bool {
if (i < 0) return true;
if (memo[i] != -1) return memo[i];
//枚举每一个切分点,不能为空
bool ok = false;
for (int k = i; k >= 0; k--) {
string suffix = s.substr(k, i - k + 1);
ok = ok || (st.contains(suffix) && dfs(dfs, k - 1));
if (ok) return memo[i] = true;
}
return memo[i] = false;
};
return dfs(dfs, n - 1);
}
};
示例 1:
输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接成。
示例 2:
输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以由 "apple" "pen" "apple" 拼接成。
注意,你可以重复使用字典中的单词。
示例 3:
输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
输出: false
零钱兑换
题目描述
给你一个整数数组 coins
,表示不同面额的硬币;以及一个整数 amount
,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1
。
你可以认为每种硬币的数量是无限的。
题目分析
题目代码
#include <vector>
using namespace std;
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
int n = coins.size();
vector<vector<int>> memo(n, vector<int>(amount + 1, -1));
auto dfs = [&](auto&& dfs, int i, int c) -> int {
if (i < 0)
{
return c == 0 ? 0 : INT_MAX / 2;
}
int& res = memo[i][c];
if (res != -1) //之前计算过
{
return res;
}
if (c < coins[i])
{
return res = dfs(dfs, i - 1, c);
}
//递归考虑两种情况:不使用当前硬币和至少使用一个当前硬币。取这两种情况的最小值,并存储在 memo[i][c] 中。
return res = min(dfs(dfs, i - 1, c), dfs(dfs, i, c - coins[i]) + 1);
};
int ans = dfs(dfs, n - 1, amount);
return ans < INT_MAX / 2 ? ans : -1;
}
};
示例 1:
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
示例 2:
输入:coins = [2], amount = 3
输出:-1
示例 3:
输入:coins = [1], amount = 0
输出:0
最长递增子序列
题目描述
给你一个整数数组 nums
,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7]
是数组 [0,3,1,6,2,2,7]
的子序列。
题目分析
题目代码
#include <vector>
#include <iostream>
using namespace std;
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n = nums.size();
vector<int> memo(n);
auto dfs = [&](auto&& dfs, int i) -> int {
int& res = memo[i];
if (res > 0)
{
return res;
}
for (int j = 0; j < i; j++)
{
if (nums[j] < nums[i])
{
res = max(res, dfs(dfs, j));
}
}
return ++res;
};
int ans = 0;
for (int i = 0; i < n; i++)
{
ans = max(ans, dfs(dfs, i));
}
return ans;
}
};
示例 1:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
示例 2:
输入:nums = [0,1,0,3,2,3]
输出:4
示例 3:
输入:nums = [7,7,7,7,7,7,7]
输出:1
三角形最小路径和
题目描述
给定一个三角形 triangle
,找出自顶向下的最小路径和。
每一步只能移动到下一行中相邻的结点上。相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。也就是说,如果正位于当前行的下标 i
,那么下一步可以移动到下一行的下标 i
或 i + 1
。
题目分析
题目代码
#include <vector>
#include <iostream>
using namespace std;
class Solution {
public:
int minimumTotal(vector<vector<int>>& triangle) {
int row = triangle.size();
for (int i = 1; i < row; i++)
{
for (int j = 0; j < i; j++)
{
if (j == 0)
{
triangle[i][j] += triangle[i - 1][j];
}
else
{
if (i == j)
{
triangle[i][j] += triangle[i - 1][j - 1];
}
else
{
triangle[i][j] += min(triangle[i - 1][j], triangle[i-1][j - 1]);
}
}
}
}
// 求返回值:最底层的数组中的最小值
int minSum = triangle[row - 1][0];
for (auto e : triangle[row - 1])
{
if (minSum > e) minSum = e;
}
return minSum;
}
};
class Solution {
public:
int minimumTotal(vector<vector<int>>& triangle) {
int row = triangle.size();
//自下而上遍历
for (int i = row - 2; i >= 0; --i) // 最后一行作初始值 不参与遍历
{
for (int j = 0; j <= i; ++j)
{
triangle[i][j] += min(triangle[i + 1][j], triangle[i + 1][j + 1]);
}
}
return triangle[0][0];
}
};
示例 1:
输入:triangle = [[2],[3,4],[6,5,7],[4,1,8,3]]
输出:11
解释:如下面简图所示:
2
3 4
6 5 7
4 1 8 3
自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。
示例 2:
输入:triangle = [[-10]]
输出:-10
最小路径和
题目描述
给定一个包含非负整数的 *m* x *n*
网格 grid
,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
题目分析
题目代码
#include <vector>
using namespace std;
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
int m = grid.size();
int n = grid[0].size();
vector<vector<int>> dp(m + 1, vector<int>(n + 1, INT_MAX)); // dp[i+1][j+1]表示到达grid[i][j]的最小路径和
dp[0][1] = dp[1][0] = 0; // 特殊处理处理dp[1][1]的左侧和上侧的状态,保证dp[1][1]=grid[0][0]
// 状态转移
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
dp[i + 1][j + 1] = min(dp[i + 1][j], dp[i][j + 1]) + grid[i][j];
}
}
return dp[m][n]; // 最终存储的是到达grid[m-1][n-1]的最小路径和
}
};
示例 1:
输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。
示例 2:
输入:grid = [[1,2,3],[4,5,6]]
输出:12
不同路径Ⅱ
题目描述
一个机器人位于一个 m x n
网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用 1
和 0
来表示。
题目分析
题目代码
#include <vector>
using namespace std;
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
if (obstacleGrid[0][0] == 1)return 0; // 起点就有障碍物 无法转移
int m = obstacleGrid.size();
int n = obstacleGrid[0].size();
vector<int> dp(n + 1, 0); // dp[j+1]表示到达grid[i][j]的不同路径,初始都为0表示不可达
dp[1] = 1; // 到达(0,0)只有一条路径
// 状态转移
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (obstacleGrid[i][j] == 1) {
dp[j + 1] = 0; // 障碍物网格不可达,无状态转移
}
else {
// dp[j+1] 等价于dp[i][j+1] 表示到达(i-1, j)的不同路径
// dp[j] 等价于 dp[i+1][j] 表示到达(i, j-1)的不同路径
dp[j + 1] += dp[j];
}
}
}
return dp[n]; // 最终存储的是到达grid[m-1][n-1]的不同路径
}
};
示例 1:
输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出:2
解释:3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:
1. 向右 -> 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右 -> 向右
示例 2:
输入:obstacleGrid = [[0,1],[0,0]]
输出:1
最长回文子串
题目描述
给你一个字符串 s
,找到 s
中最长的 回文 子串。
题目分析
题目代码
#include <string>
#include <vector>
using namespace std;
class Solution {
public:
string longestPalindrome(string s) {
int n = s.size();
int length = 1; // 最长回文子串的长度
int start = 0; // 最长回文子串的起始位置
vector<vector<bool>> dp(n, vector<bool>(n)); // dp[j][i]表示子串s[j:i]是否为回文串
for (int i = 0; i < n; i++) {
// 以i为终点,往回枚举起点j
for (int j = i; j >= 0; j--) {
if (i == j) {
dp[j][i] = true; // 一个字符,一定为回文串
}
else if (i == j + 1) {
dp[j][i] = (s[i] == s[j]); // 两个字符,取决于二者是否相等
}
else {
dp[j][i] = (s[i] == s[j]) && dp[j + 1][i - 1]; // 两个字符以上,首先端点两个字符要相等,其次[j+1, i-1]也要为回文串
}
// [j,i]为回文串且长度更大,更新
if (dp[j][i] && (i - j + 1) > length) {
length = i - j + 1;
start = j;
}
}
}
return s.substr(start, length); // 截取最长回文子串
}
};
示例 1:
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
示例 2:
输入:s = "cbbd"
输出:"bb"
交错字符串
题目描述
给定三个字符串 s1
、s2
、s3
,请你帮忙验证 s3
是否是由 s1
和 s2
交错 组成的。
两个字符串 s
和 t
交错 的定义与过程如下,其中每个字符串都会被分割成若干 非空
子字符串
:
s = s1 + s2 + ... + sn
t = t1 + t2 + ... + tm
|n - m| <= 1
- 交错 是
s1 + t1 + s2 + t2 + s3 + t3 + ...
或者t1 + s1 + t2 + s2 + t3 + s3 + ...
注意:a + b
意味着字符串 a
和 b
连接。
题目分析
题目代码
#include <string>
#include <vector>
#include <functional>
using namespace std;
class Solution {
public:
bool isInterleave(string s1, string s2, string s3) {
int n = s1.size(), m = s2.size();
if (n + m != s3.size()) return false;
//特判
if (n == 0) return s2 == s3;
if (m == 0) return s1 == s3;
int memo[n][m];
memset(memo, -1, sizeof(memo));
function<bool(int, int)> dfs = [&](int i, int j) -> bool {
//如果s1判断完了,判断剩余的s2是否满足条件
if (i < 0) {
for (int k = 0; k <= j; k++) {
if (s2[k] != s3[k]) return false;
}
return true;
}
//如果s2判断完了,判断剩余的s1是否满足条件
if (j < 0) {
for (int k = 0; k <= i; k++) {
if (s1[k] != s3[k]) return false;
}
return true;
}
if (memo[i][j] != -1) return memo[i][j];
return memo[i][j] = (s1[i] == s3[i + j + 1] && dfs(i - 1, j))
|| (s2[j] == s3[i + j + 1] && dfs(i, j - 1));
};
return dfs(n - 1, m - 1);
}
};
示例 1:
输入:s1 = "aabcc", s2 = "dbbca", s3 = "aadbbcbcac"
输出:true
示例 2:
输入:s1 = "aabcc", s2 = "dbbca", s3 = "aadbbbaccc"
输出:false
示例 3:
输入:s1 = "", s2 = "", s3 = ""
输出:true
编译距离
题目描述
给你两个单词 word1
和 word2
, 请返回将 word1
转换成 word2
所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
题目分析
题目代码
#include <vector>
#include <string>
using namespace std;
class Solution {
public:
int minDistance(string word1, string word2) {
int n = word1.size(), m = word2.size();
vector<vector<int>> memo(n, vector<int>(m, -1));
auto dfs = [&](auto&& dfs, int i, int j) -> int {
if (i < 0)
{
return j + 1;
}
if (j < 0)
{
return i + 1;
}
int& res = memo[i][j];
if (memo[i][j] != -1)
{
return res;
}
if (word1[i] == word2[j])
{
return res = dfs(dfs, i - 1, j - 1);
}
return res = min(min(dfs(dfs, i - 1, j), dfs(dfs, i, j - 1)), dfs(dfs, i - 1, j - 1) ) + 1;
};
return dfs(dfs, n - 1, m - 1);
}
};
示例 1:
输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')
示例 2:
输入:word1 = "intention", word2 = "execution"
输出:5
解释:
intention -> inention (删除 't')
inention -> enention (将 'i' 替换为 'e')
enention -> exention (将 'n' 替换为 'x')
exention -> exection (将 'n' 替换为 'c')
exection -> execution (插入 'u')
买卖股票的最佳时机Ⅲ
题目描述
给定一个数组,它的第 i
个元素是一支给定的股票在第 i
天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
题目分析
看题解吧,本题相当于有四个不同的状态,我们假设在第一天购入,售出两次,所以第一天我们初始化为-prices[0],0
题目代码
#include <vector>
using namespace std;
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
int buy1 = -prices[0];
int sell1 = 0;
int buy2 = -prices[0];
int sell2 = 0;
for (int i = 1; i <= n; i++)
{
buy1 = max(buy1, -prices[i - 1]);
sell1 = max(sell1, buy1 + prices[i - 1]);
buy2 = max(buy2, sell1 - prices[i - 1]);
sell2 = max(sell2, buy2 + prices[i - 1]);
}
return sell2;
}
};
示例 1:
输入:prices = [3,3,5,0,0,3,1,4]
输出:6
解释:在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。
随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3 。
示例 2:
输入:prices = [1,2,3,4,5]
输出:4
解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
示例 3:
输入:prices = [7,6,4,3,1]
输出:0
解释:在这个情况下, 没有交易完成, 所以最大利润为 0。
示例 4:
输入:prices = [1]
输出:0
买卖股票的最佳时机Ⅳ
题目描述
给你一个整数数组 prices
和一个整数 k
,其中 prices[i]
是某支给定的股票在第 i
天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 k
笔交易。也就是说,你最多可以买 k
次,卖 k
次。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
题目分析
题目代码
#include <vector>
#include <array>
using namespace std;
class Solution {
public:
int maxProfit(int k, vector<int>& prices) {
int n = prices.size();
vector<vector<array<int, 2>>> memo(n, vector<array<int, 2>>(k + 1, { -1, -1 })); // -1 表示还没有计算过
auto dfs = [&](auto&& dfs, int i, int j, bool hold) -> int {
if (j < 0) {
return INT_MIN / 2; // 除 2 防止溢出
}
if (i < 0) {
return hold ? INT_MIN / 2 : 0;
}
int& res = memo[i][j][hold]; // 注意这里是引用
if (res != -1) { // 之前计算过
return res;
}
if (hold) {
return res = max(dfs(dfs, i - 1, j, true), dfs(dfs, i - 1, j - 1, false) - prices[i]);
}
return res = max(dfs(dfs, i - 1, j, false), dfs(dfs, i - 1, j, true) + prices[i]);
};
return dfs(dfs, n - 1, k, false);
}
};
示例 1:
输入:k = 2, prices = [2,4,1]
输出:2
解释:在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2 。
示例 2:
输入:k = 2, prices = [3,2,6,5,0,3]
输出:7
解释:在第 2 天 (股票价格 = 2) 的时候买入,在第 3 天 (股票价格 = 6) 的时候卖出, 这笔交易所能获得利润 = 6-2 = 4 。
随后,在第 5 天 (股票价格 = 0) 的时候买入,在第 6 天 (股票价格 = 3) 的时候卖出, 这笔交易所能获得利润 = 3-0 = 3 。
最大正方形
题目描述
在一个由 '0'
和 '1'
组成的二维矩阵内,找到只包含 '1'
的最大正方形,并返回其面积。
题目分析
memo[i][j] = 1 + min(memo[i - 1][j - 1], min(memo[i][j - 1], memo[i - 1][j]));
这里要加一的原因是要考虑,自身可以构建一个1*1的小正方形
题目代码
#include <vector>
using namespace std;
class Solution {
public:
int maximalSquare(vector<vector<char>>& matrix) {
int n = matrix.size(); // i row
int m = matrix[0].size(); // j column
int maxSide = 0;
vector<vector<int>> memo(n, vector<int>(m));
for (int j = 0; j < m; j++)
{
memo[0][j] = matrix[0][j] - '0';
maxSide = max(maxSide, memo[0][j]);
}
for (int i = 0; i < n; i++)
{
memo[i][0] = matrix[i][0] - '0';
maxSide = max(maxSide, memo[i][0]);
}
for (int i = 1; i < n; i++)
{
for (int j = 1; j < m; j++)
{
if (matrix[i][j] == '0') continue;
memo[i][j] = 1 + min(memo[i - 1][j - 1], min(memo[i][j - 1], memo[i - 1][j]));
maxSide = max(maxSide, memo[i][j]);
}
}
return maxSide * maxSide;
}
};
示例 1:
输入:matrix = [["1","0","1","0","0"],["1","0","1","1","1"],["1","1","1","1","1"],["1","0","0","1","0"]]
输出:4
示例 2:
输入:matrix = [["0","1"],["1","0"]]
输出:1
示例 3:
输入:matrix = [["0"]]
输出:0
求出最长好子序列Ⅱ
题目描述
给你一个整数数组 nums
和一个 非负 整数 k
。如果一个整数序列 seq
满足在范围下标范围 [0, seq.length - 2]
中存在 不超过 k
个下标 i
满足 seq[i] != seq[i + 1]
,那么我们称这个整数序列为 好 序列。
请你返回 nums
中 好
子序列
的最长长度
题目分析
3177. 求出最长好子序列 II – 力扣(LeetCode)
题目代码
#include <vector>
#include <unordered_map>
using namespace std;
class Solution {
public:
int maximumLength(vector<int>& nums, int k) {
unordered_map<int, vector<int>> fs;
vector<int> mx(k + 2);
for (int x : nums)
{
auto& f = fs[x];
f.resize(k + 1);
for (int i = k; i >= 0; i--)
{
f[i] = max(f[i], mx[i]) + 1;
mx[i + 1] = max(mx[i + 1], f[i]);
}
}
return mx[k + 1];
}
};
示例 1:
输入:nums = [1,2,1,1,3], k = 2
输出:4
解释:
最长好子序列为 [1,2,1,1,3]
。
示例 2:
输入:nums = [1,2,3,4,5,1], k = 0
输出:2
解释:
最长好子序列为 [1,2,3,4,5,1]
。
提示:
1 <= nums.length <= 5 * 103
1 <= nums[i] <= 109
0 <= k <= min(50, nums.length)
通用模板
字符串句子
模板代码
这类题目可以分为两类,一类是有前置或者后置空格的,另一类是没有前置和后置空格的。
1、如果有前后置空格,那么必须判断临时字符串非空才能输出,否则会输出空串
模板如下:
s += " "; //这里在最后一个字符位置加上空格,这样最后一个字符串就不会遗漏
string temp = ""; //临时字符串
vector<string> res; //存放字符串的数组
for (char ch : s) //遍历字符句子
{
if (ch == ' ') //遇到空格
{
if (!temp.empty()) //临时字符串非空
{
res.push_back(temp);
temp.clear(); //清空临时字符串
}
}
else
temp += ch;
}
2、没有前后置的空格不需要判断空串
s += " ";
string temp = "";
vector<string> res;
for (char ch : s)
{
if (ch == ' ')
{
res.push_back(temp);
temp.clear();
}
else
temp += ch;
}
别学了别学了😭😭
呜呜。°(°¯᷄◠¯᷅°)°。今天电脑出问题了,没法卷了