Kidimos's Studio.

算法题

Word count: 5kReading time: 22 min
2025/07/30
loading

分发糖果

n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。
你需要按照以下要求,给这些孩子分发糖果

  • 每个孩子至少分配到 1 个糖果
  • 相邻两个孩子评分更高的孩子会获得更多的糖果。
    请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目

解题思路:

  1. 初始化变量作用:
    • pre记录上一个数的大小,inc记录递增数列的长度,dec记录递减数列的长度。
    • 默认情况下,将数组作为递增数列看待,所以pre = 1, inc = 1, dec = 0。
  2. 当前元素大于等于上一位元素的时候,为递增数列,此时初始化递减数列的长度 = 0,计算pre的大小,pre的大小有一下几种情况
    • 如果元素大于上一位元素,说明为严格递增,此时元素可以接受的最小赋值为pre+1
    • 如果元素等于上一位元素,当前元素可以不需要比上一位高,可以接受的最小赋值为1
  3. 当前元素小于上一位元素的时候,数列转为递减数列,该数列中的加值dec从1开始递增
    • 每次新增的一位元素可以接受的最小赋值为1,但是dec(递减数列的长度)数列中的每一位都需要+1,所以最后的ret需要加上dec
    • 如果dec超过inc,说明转折点(也就是最高点)的原先高度已经不足以支撑新的递减数列,所以需要额外+1
    • pre需要置为1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public:
int candy(vector<int>& ratings) {
int pre = 1, inc = 1, dec = 0;
int n = ratings.size();
int ret = 1;
for(int i = 1;i < n;i++){
if(ratings[i] >= ratings[i - 1]){
dec = 0;
pre = ratings[i] == ratings[i - 1] ? 1 : pre + 1;
ret += pre;
inc = pre;
}else {
dec++;
if(dec == inc){
dec++;
}
ret += dec;
pre = 1;
}
}
return ret;
}
};

文本左右对齐

给定一个单词数组 words 和一个长度 maxWidth ,重新排版单词,使其成为每行恰好有 maxWidth 个字符,且左右两端对齐的文本。
你应该使用 “贪心算法” 来放置给定的单词;也就是说,尽可能多地往每行中放置单词。必要时可用空格 ‘ ‘ 填充,使得每行恰好有 maxWidth 个字符。
要求尽可能均匀分配单词间的空格数量。如果某一行单词间的空格不能均匀分配,则左侧放置的空格数要多于右侧的空格数
文本的最后一行应为左对齐,且单词之间不插入额外的空格。
注意:

  • 单词是指由非空格字符组成的字符序列。
  • 每个单词的长度大于 0,小于等于 maxWidth。
  • 输入单词数组 words 至少包含一个单词。

解题思路:

  1. 根本没用什么技巧,非常直接的解法
  2. 要求有点绕,归根结底还是笔者太菜了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class Solution {
public:
string MultipleEmpty(int n){
string s = "";
for(int i = 0;i < n;i++){
s += " ";
}
return s;
}

vector<string> fullJustify(vector<string>& words, int maxWidth) {
vector<string> res;
vector<string> DividedString;
int n = words.size();
int curWidth = 0;
for(int i = 0;i < n;i++){
DividedString.push_back(words[i]);
curWidth += words[i].size();
if(i == n - 1 || curWidth + words[i + 1].size() + DividedString.size() > maxWidth ){
int empty = maxWidth - curWidth;
int perEmpty, RemainEmpty;
if(DividedString.size() != 1){
perEmpty = empty / (DividedString.size() - 1);
RemainEmpty = empty % (DividedString.size() - 1);
if(i == n - 1){
perEmpty = 1;
RemainEmpty = 0;
}
}else{
perEmpty = empty;
RemainEmpty = 0;
}
string SingleLine = "";
for(int j = 0;j < DividedString.size();j++){
SingleLine += DividedString[j];
if(j < RemainEmpty) SingleLine += MultipleEmpty(perEmpty + 1);
else if(j >= RemainEmpty && j < DividedString.size() - 1){
SingleLine += MultipleEmpty(perEmpty);
}else if(DividedString.size() == 1){
SingleLine += MultipleEmpty(perEmpty);
}
if(i == n - 1 && j == DividedString.size() - 1 && DividedString.size() != 1){
SingleLine += MultipleEmpty(empty - DividedString.size() + 1);
}
}
res.push_back(SingleLine);
DividedString.clear();
curWidth = 0;
}
}
return res;
}
};

串联所有单词的子串

给定一个字符串 s 和一个字符串数组 words。 words 中所有字符串 长度相同。

s 中的 串联子串 是指一个包含 words 中所有字符串以任意顺序排列连接起来的子串。

  • 例如,如果 words = [“ab”,”cd”,”ef”], 那么 “abcdef”, “abefcd”,”cdabef”, “cdefab”,”efabcd”, 和 “efcdab” 都是串联子串。 “acdbef” 不是串联子串,因为他不是任何 words 排列的连接。
    返回所有串联子串在 s 中的开始索引。你可以以 任意顺序 返回答案。

解题思路:

  1. 滑动窗口方算法
  2. 需要注意的条件为:words中的所有字符串长度相同。对于滑动窗口来说,我们每次将窗口往右滑动一格会破坏窗口内单词的连贯性,需要重新对窗口里的所有单词进行归纳和计数,时间复杂度大大增加。
  3. 因为,我们需要根据每个word的长度m,来存储多起点的滑动窗口,从0 ~ m-1 创建m个不同起点的滑动窗口。对于已经创建的滑动窗口,我们只需要每次向右滑动m个格子,删除最前端的word,加入最末端的一个word,即可不破坏窗口内单词的连贯性。
  4. 在创建滑动窗口后和向右移动m位后,我们将滑动窗口与words的哈希表进行对比,可以得到是否满足条件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Solution {
public:
vector<int> findSubstring(string s, vector<string>& words) {
int n = words.size(), m = words[0].size(), len = n * m;
vector<int> ret;
if(s.size() < len) return {};
unordered_map<string, int> key;
for(string& word : words){
key[word]++;
}
vector<unordered_map<string, int>> dWindow(m);
for(int i = 0;i < m && i + len <= s.size();i++){
for(int j = i;j < i + len;j += m){
string str = s.substr(j, m);
dWindow[i][str]++;
}
if(dWindow[i] == key){
ret.push_back(i);
}
}
for(int i = m;i + len <= s.size();i++){
int r = i % m;
string del = s.substr(i - m, m);
string add = s.substr(i + len - m, m);
if(--dWindow[r][del] == 0) dWindow[r].erase(del);
dWindow[r][add]++;

if(dWindow[r] == key){
ret.push_back(i);
}

}
return ret;
}
};

接雨水

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

解题思路:

  1. 最简单的方法:直接使用left和right数组存储每个元素左右的最大值,然后再遍历数组得到每个格子的水量。
  2. 单调栈方法:使用一个单调栈存储遍历过的元素下标,当单调栈不为空且当前元素高度大于单调栈顶部高度时候,不断取出单调栈中的两个元素,得到一格可以接雨水的区域,计算宽高得到雨水量。
  3. 不太会解释,可以查看官方题解。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Solution {
public:
int trap(vector<int>& height) {
// int n = height.size();
// vector<int> left(n, 0), right(n, 0);
// for(int i = 1;i < n;i++){
// left[i] = max(left[i - 1], height[i - 1]);
// }
// for(int i = n - 2;i >= 0;i--){
// right[i] = max(right[i + 1], height[i + 1]);
// }
// int ret = 0;
// for(int i = 0;i < n;i++){
// if(height[i] < min(left[i], right[i])){
// ret += (min(left[i], right[i]) - height[i]);
// }
// }
// return ret;
stack<int> stk;
int n = height.size();
int sum = 0;
for(int i = 0;i < n;i++){
while(!stk.empty() && height[i] > height[stk.top()]){
int p1 = stk.top();
stk.pop();
if(stk.empty()) break;
int p2 = stk.top();
int h = min(height[p2], height[i]) - height[p1];
sum += (h * (i - p2 - 1));
}
stk.push(i);
}
return sum;
}
};

最小覆盖子串

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “” 。

解题思路:

  1. 使用滑动窗口
  2. 由于数组里面只有字母,所以可以提前创建两个大小为128的ASCII字符数组,用于比较子串内元素的个数。
  3. 先预设ansLeft为-1, ansRight为n(s的长度),在遍历的过程中,如果得到长度更小且满足条件的子串,则替换。
  4. 遍历:枚举s子串的右端点right, 如果子串涵盖了t,则不断地移动左端点直到子串不包含。在移动的期间,如果满足条件则更新最短子串的左右端点。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Solution {
public:
bool isContain(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;
}

string minWindow(string s, string t) {
int n = s.size();
int cnt_s[128]{};
int cnt_t[128]{};
for(char c : t) {
cnt_t[c]++;
}
int ansLeft = -1, ansRight = n;
int left = 0;
for(int right = 0;right < n;right++){
cnt_s[s[right]]++;
while(isContain(cnt_s, cnt_t)){
if(right - left < ansRight - ansLeft){
ansLeft = left;
ansRight = right;
}
cnt_s[s[left]]--;
left++;
}
}
return ansLeft < 0 ? "" : s.substr(ansLeft, ansRight - ansLeft + 1);
}
};

K个一组翻转链表

给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。

k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
解题思路:

  1. 使用头插法,重要是创建一个虚拟头节点,然后理清楚插入的关系。
  2. 头插法比其他更简单一些:我们只需要确定一个pre节点(也就是每组的前一个节点)以及一个cur节点(每组的第一个节点)即可。
  3. pre保持不变,先取出cur的next节点(tmp节点),然后我们需要将cur等效前进一位,也就是让cur的next指向tmp节点的next。接着我们将tmp插入到链表的头部,也就是pre的下一位。链表中插入一个元素的方法这边不过多赘述。
1
2
3
4
ListNode* tmp = cur->next;
cur->next = tmp->next;
tmp->next = pre->next;
pre->next = tmp;
  1. 完成一个周期后,重要的两个元素:pre和cur需要更新。pre跳到cur,也就是下一组链表的前一个节点,cur直接进一位,变为下一组链表的第一个节点。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* reverseKGroup(ListNode* head, int k) {
int n = 0;
ListNode* node = head;
while(node){
n++;
node = node->next;
}
if(n < k) return head;
ListNode* dummy = new ListNode(-1, head);

ListNode* pre = dummy;
ListNode* cur = head;

for(int i = 0;i < n / k;i++){
for(int j = 0;j < k - 1;j++){
ListNode* tmp = cur->next;
cur->next = tmp->next;
tmp->next = pre->next;
pre->next = tmp;
}
pre = cur;
cur = cur->next;
}
return dummy->next;
}
};

二叉树中的最大路径和

二叉树中的 路径 被定义为一条节点序列,序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。

路径和 是路径中各节点值的总和。

给你一个二叉树的根节点 root ,返回其 最大路径和 。

解题思路:

  1. 递归
  2. 在递归的过程中,我们将得到的结果分为这么两种:第一种是无法进入下一次递归的,也就是折线类型的。第二种是直线类型的,它可以作为下一次递归的量。这两种结果都可能是最后的结果,所以我们都要探讨。第一种我们使用一个外部的变量来存储更新它,第二种我们直接用函数的返回值来存储它。
  3. 对于第一种折线类型,我们使用当前节点的val+递归得到的左右两个子树的返回值(也就是两个子树的直线最大值)来获取。对于第二种直线类型,我们使用val+左右子树的最大值来获取。
  4. 需要注意的点:
    • 在得到左右子树返回值的时候,如果比0小我们则舍去不用,因为此时我们的节点已经是最大的了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* Definition for a binary tree node.
* 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 ret = INT_MIN;

int getMaxSum(TreeNode* node){
if(node == nullptr) return 0;

int leftGain = max(getMaxSum(node->left), 0);
int rightGain = max(getMaxSum(node->right), 0);

int NewSum = node->val + leftGain + rightGain;
ret = max(ret, NewSum);

return node->val + max(leftGain, rightGain);
}

int maxPathSum(TreeNode* root) {
getMaxSum(root);
return ret;
}
};

合并K个升序链表

给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。

解题思路:

  1. 用分治的方法进行合并
  2. 将 k 个链表配对并将同一对中的链表合并;
  3. 不断合并得到最终结果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* merge(ListNode* node1, ListNode* node2){
ListNode* newHead = new ListNode(-1);
ListNode* pre = newHead;
while(node1 != nullptr && node2 != nullptr){
if(node1->val <= node2->val){
pre->next = node1;
node1 = node1->next;
}else{
pre->next = node2;
node2 = node2->next;
}
pre = pre->next;
}
if(node1 != nullptr){
pre->next = node1;
}else if(node2 != nullptr){
pre->next = node2;
}
return newHead->next;
}

ListNode* mergeSort(vector<ListNode*>& lists, int left, int right){
if(left > right) return nullptr;
if(left == right) return lists[left];
int mid = left + (right - left) / 2;
ListNode* leftNode = mergeSort(lists, left, mid);
ListNode* rightNode = mergeSort(lists, mid + 1, right);
return merge(leftNode, rightNode);
}

ListNode* mergeKLists(vector<ListNode*>& lists) {

int n = lists.size();
return mergeSort(lists, 0, n - 1);
}
};

最长连续序列

给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。
请你设计并实现时间复杂度为 O(n) 的算法解决此问题。

解题思路:

  1. 重要的事这个需要实现时间复杂度为On的要求,如果我们按照最普通的思路,先排序再找最长的连续序列,那么时间复杂度就会超过On,具体的时间复杂度由使用的排序算法决定。
  2. 我们可以使用一个unordered_set来解决这个问题,首先,我们遍历整个数据,将所有的数据装载进set里面,顺便去重。
  3. 紧接着,我们遍历这个set取出元素,对于每个已经装进set的元素来说,我们只需要确认它是不是一段连续序列的开头就可以了,具体的方法为:我们遍历set取出num,然后在set里面查找是否有num-1这个元素的存在,如果有的话,说明我们现在遍历的这个num并不是连续序列的开头,我们可以直接忽略,如果是的话,进入下一步。
  4. 如果我们取出来的元素是连续序列的开头,我们可以在set里面查找这个元素的后续是否存在,也就是不断地将该元素+1(变量current),并在set中查找。最后得到一段连续序列以及它的长度,将该长度与现有的最长连续序列长度进行对比,更新结果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public:
int longestConsecutive(vector<int>& nums) {
unordered_set<int> numsSet;
for(int num : nums){
numsSet.insert(num);
}
int longStreak = 0;

for(const int& num : numsSet){
if(!numsSet.count(num - 1)){
int current = num;
int currentStreak = 1;
while(numsSet.count(current + 1)){
current += 1;
currentStreak += 1;
}
longStreak = max(longStreak, currentStreak);
}
}

return longStreak;
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
public int longestConsecutive(int[] nums) {
Set<Integer> num_set = new HashSet<Integer>();
for(int num : nums){
num_set.add(num);
}

int longestStreak = 0;
for(int num : num_set){
if(!num_set.contains(num - 1)){
int current = num;
int currentStreak = 1;
while(num_set.contains(current + 1)){
current += 1;
currentStreak += 1;
}

longestStreak = Math.max(longestStreak, currentStreak);
}
}
return longestStreak;
}
}

最长公共子序列

给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,”ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

解题思路:

  1. 这道题需要使用动态规划的方法来解决。
  2. 我们需要得知,n长度的text1和m长度的text2之间,最多有多长的公共子序列,因此,可以将问题分解为text1[0:i]与text2[0:j]之间的公共子序列。
  3. 因此使用动态规划,转移状态方程式为:dp[i][j] = dp[i - 1][j- 1]以及dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int n = text1.size(), m = text2.size();
vector<vector<int>> dp(n + 1, vector<int>(m + 1));
for(int i = 1;i <= n;i++){
char c1 = text1.at(i - 1);
for(int j = 1;j <= m;j++){
char c2 = text2.at(j - 1);
if(c1 == c2){
dp[i][j] = dp[i - 1][j - 1] + 1;
}else{
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[n][m];
}
};

动态规划

打家劫舍II

解题思路:

  1. 因为首尾相连,所以我们直接当作两个序列来处理,一个包含头一个包含尾
  2. 然后使用动态规划,dp[i - start] = max(dp[i - start - 1], dp[i - start - 2] + nums[i]);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
int robRange(vector<int>& nums, int start, int end){
vector<int> dp(end - start + 1, 0);
dp[0] = nums[start];
dp[1] = max(nums[start + 1], nums[start]);
for(int i = start + 2;i <= end;i++){
dp[i - start] = max(dp[i - start - 1], dp[i - start - 2] + nums[i]);
}
return dp[end - start];
}

int rob(vector<int>& nums) {
int n = nums.size();
if(n == 1) return nums[0];
else if(n == 2) return max(nums[0], nums[1]);

return max(robRange(nums, 0, n - 2), robRange(nums, 1, n-1));
}
};

最长回文子串

给你一个字符串 s,找到 s 中最长的 回文 子串。

解题思路:

  1. 同样是使用动态规划的方法来解决。
  2. 使用一个二维数组来表示字符串中,start与end之间的字符串是否是一个回文子串,如果是的话则标记为true。
  3. 首先我们需要列举出可能的回文子串的长度,从2到n遍历。
  4. 转移状态方程式为:dp[left][left + L - 1] = dp[left + 1][left + L - 2];
  5. 对于首尾两个字符相等的情况,只需要分成两种情况,一种是L只有2,那么我们可以直接设置为true,一种是扣除首尾后dp数组对应元素仍然为true,此时为回文子串。
  6. 每次遍历中更新最大回文子串。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Solution {
public:
string longestPalindrome(string s) {
int n = s.size();
int maxStart = 0;
int maxEnd = 0;
int maxLen = 1;
vector<vector<bool>> dp(n, vector<bool>(n, false));
for(int i = 0;i < n;i++){
dp[i][i] = true;
}

for(int l = 2;l <= n;l++){
for(int left = 0;left <= n - l;left++){
if(s[left] == s[left + l - 1]){
if(l == 2) dp[left][left + l - 1] = true;
else {
dp[left][left + l - 1] = dp[left + 1][left + l - 2];
}
}
if(dp[left][left + l - 1] && l > maxLen){
maxStart = left;
maxEnd = left + l - 1;
maxLen = l;
}
}
}
return s.substr(maxStart, maxLen);
}
};
CATALOG
  1. 1. 分发糖果
  2. 2. 文本左右对齐
  3. 3. 串联所有单词的子串
  4. 4. 接雨水
  5. 5. 最小覆盖子串
  6. 6. K个一组翻转链表
  7. 7. 二叉树中的最大路径和
  8. 8. 合并K个升序链表
  9. 9. 最长连续序列
  10. 10. 最长公共子序列
  • 动态规划
    1. 1. 打家劫舍II
    2. 2. 最长回文子串