目录
第一题:20. 有效的括号
题目
给定一个只包括 '('
,')'
,'{'
,'}'
,'['
,']'
的字符串 s
,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号。
示例 1:
输入:s = "()" 输出:true
示例 2:
输入:s = "()[]{}" 输出:true
示例 3:
输入:s = "(]" 输出:false
提示:
1 <= s.length <= 104
s
仅由括号'()[]{}'
组成
解题思路
题目要求我们验证一个字符串中的括号是否有效。有效字符串需满足:左括号必须用相同类型的右括号闭合。左括号必须以正确的顺序闭合。每个右括号都有一个对应的相同类型的左括号。例如,字符串 "()" 、"()[]{}"和"([{}])"是有效的,但 "(]" 、"([)]"和"[]{}))" 是无效的。
1.使用栈的数据结构
栈是一种先进后出的数据结构,非常适合用于匹配括号的场景。当遇到左括号时,将其压入栈中;当遇到右括号时,检查栈顶元素是否与之匹配。
2.遍历字符串
从字符串的开头到结尾遍历每个字符。
3.处理左括号
如果当前字符是左括号('('、'[' 或 '{'),则将其压入栈中。
4.处理右括号
如果当前字符是右括号(')'、']' 或 '}'),需要检查栈是否为空以及栈顶的左括号是否与当前右括号匹配。
如果栈为空,说明没有匹配的左括号,因此返回 false。
如果栈不为空,但栈顶的左括号与当前右括号不匹配,也返回 false。
如果匹配,则将栈顶的左括号弹出。
5.检查栈是否为空
遍历完整个字符串后,如果栈为空,说明所有左括号都找到了匹配的右括号,因此返回 true。
如果栈不为空,说明还有未匹配的左括号,因此返回 false。
代码实现
// 判断括号是否不匹配
int notMatch(char par, char* stack, int stackTop) {
switch(par) {
case ']': // 如果是右方括号
return stack[stackTop - 1] != '['; // 判断栈顶元素是否为左方括号,如果不是则返回1表示不匹配
case ')': // 如果是右圆括号
return stack[stackTop - 1] != '('; // 判断栈顶元素是否为左圆括号,如果不是则返回1表示不匹配
case '}': // 如果是右大括号
return stack[stackTop - 1] != '{'; // 判断栈顶元素是否为左大括号,如果不是则返回1表示不匹配
}
return 0; // 如果par不是右括号,则返回0表示匹配
}
// 判断字符串中的括号是否有效
bool isValid(char* s) {
int strLen = strlen(s); // 获取字符串的长度
char stack[5000]; // 创建一个栈,用于存放左括号
int stackTop = 0; // 栈顶指针,初始化为0
// 遍历字符串中的每个字符
for(int i = 0; i < strLen; i++) {
char tempChar = s[i]; // 取出当前字符
// 如果当前字符是左括号,则压入栈中
if(tempChar == '(' || tempChar == '[' || tempChar == '{')
stack[stackTop++] = tempChar;
// 如果当前字符是右括号
else if(stackTop == 0 || notMatch(tempChar, stack, stackTop)) {
// 如果栈为空或当前右括号与栈顶左括号不匹配,返回false
return false;
}
// 当前字符与栈顶元素为一对括号,将栈顶元素出栈
else
stackTop--;
}
// 如果遍历完字符串后栈为空,表示所有括号都匹配,返回true;否则返回false
return stackTop == 0;
}
第二题:2810. 故障键盘
题目
你的笔记本键盘存在故障,每当你在上面输入字符 'i'
时,它会反转你所写的字符串。而输入其他字符则可以正常工作。
给你一个下标从 0 开始的字符串 s
,请你用故障键盘依次输入每个字符。
返回最终笔记本屏幕上输出的字符串。
示例 1:
输入:s = "string" 输出:"rtsng" 解释: 输入第 1 个字符后,屏幕上的文本是:"s" 。 输入第 2 个字符后,屏幕上的文本是:"st" 。 输入第 3 个字符后,屏幕上的文本是:"str" 。 因为第 4 个字符是 'i' ,屏幕上的文本被反转,变成 "rts" 。 输入第 5 个字符后,屏幕上的文本是:"rtsn" 。 输入第 6 个字符后,屏幕上的文本是: "rtsng" 。 因此,返回 "rtsng" 。
示例 2:
输入:s = "poiinter" 输出:"ponter" 解释: 输入第 1 个字符后,屏幕上的文本是:"p" 。 输入第 2 个字符后,屏幕上的文本是:"po" 。 因为第 3 个字符是 'i' ,屏幕上的文本被反转,变成 "op" 。 因为第 4 个字符是 'i' ,屏幕上的文本被反转,变成 "po" 。 输入第 5 个字符后,屏幕上的文本是:"pon" 。 输入第 6 个字符后,屏幕上的文本是:"pont" 。 输入第 7 个字符后,屏幕上的文本是:"ponte" 。 输入第 8 个字符后,屏幕上的文本是:"ponter" 。 因此,返回 "ponter" 。
提示:
1 <= s.length <= 100
s
由小写英文字母组成s[0] != 'i'
解题思路
1.函数定义和变量初始化:
reverse函数是用来反转字符串s中从start到end位置的字符的。它使用了两个指针start和end,通过交换两个指针所指向的字符来实现反转。
finalString函数用来处理输入字符串s,并返回最终的结果字符串。在函数内部,首先获取输入字符串的长度len,然后初始化一个用于记录结果字符串中已存储字符数的变量n。
使用malloc函数为结果字符串result分配足够的内存空间,大小为len + 1(包括字符串结束符'\0')。
2.遍历输入字符串s:
使用一个循环来遍历输入字符串s的每个字符。在每次循环中,检查当前字符是否是'i'。
3.处理字符'i':
如果当前字符是'i',需要调用reverse函数来反转result中从字符串开始到当前'i'字符之前(不包括'i')的所有字符。注意,这里使用n - 1作为reverse函数的end参数,因为n是下一个字符应该存放的位置,所以n - 1是当前'i'字符之前的位置。
4.处理非'i'字符:
如果当前字符不是'i',将其添加到结果字符串result中,并更新n的值,以便在下次循环中知道下一个字符应该存放在哪里。
5.添加字符串结束符并返回结果:
在遍历完输入字符串s后,在结果字符串result的末尾添加字符串结束符'\0',以标识字符串的结束。最后,返回处理后的结果字符串result。
代码实现
void reverse(char* s, int start, int end) {
// 定义两个指针,一个指向字符串开始位置,一个指向字符串的end位置
for (; start < end; start++, end--) { // 当start小于end时,循环继续
char t = s[start]; // 临时存储start指针指向的字符
s[start] = s[end]; // 将end指针指向的字符放到start指针的位置
s[end] = t; // 将临时存储的字符放到end指针的位置
}
}
char* finalString(char* s) {
int len = strlen(s); // 获取输入字符串的长度
int n = 0; // n用于记录结果字符串中当前已存储的字符数
char* result = (char*)malloc((len + 1) * sizeof(char)); // 分配足够的空间来存储结果字符串
for (int i = 0; i < len; i++) { // 遍历输入字符串s的每个字符
if (s[i] == 'i') { // 如果遇到字符'i'
reverse(result, 0, n - 1); // 反转结果字符串result中i之前部分
} else {
result[n] = s[i]; // 将当前字符添加到结果字符串result中
n++; // 更新已存储的字符数
}
}
result[n] = '\0'; // 在结果字符串result的末尾添加字符串结束符
return result; // 返回处理后的结果字符串
}
第三题:15. 三数之和
题目
给你一个整数数组 nums
,判断是否存在三元组 [nums[i], nums[j], nums[k]]
满足 i != j
、i != k
且 j != k
,同时还满足 nums[i] + nums[j] + nums[k] == 0
。请
你返回所有和为 0
且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例 1:
输入:nums = [-1,0,1,2,-1,-4] 输出:[[-1,-1,2],[-1,0,1]] 解释: nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。 nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。 nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。 不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。 注意,输出的顺序和三元组的顺序并不重要。
示例 2:
输入:nums = [0,1,1] 输出:[] 解释:唯一可能的三元组和不为 0 。
示例 3:
输入:nums = [0,0,0] 输出:[[0,0,0]] 解释:唯一可能的三元组和为 0 。
提示:
3 <= nums.length <= 3000
-105 <= nums[i] <= 105
解题思路
先对数组进行排序,然后使用双指针法来寻找满足条件的三元组。
1.排序
首先对数组 nums 进行排序。排序的目的是为了后续能够利用双指针法有效地找到和为0的三元组,并且方便跳过重复的元素。
2.遍历数组
在排序后的数组中,固定一个元素,然后使用双指针法在剩余的元素中寻找两个数,使得它们的和与固定元素的和为0。在遍历过程中,需要注意跳过重复的元素以避免重复的三元组。
3.双指针法
对于每个 nums[i],我们设置两个指针 left 和 right,分别指向 i 的下一个位置和数组的末尾。这两个指针用于找到与 nums[i] 相加和为0的两个数。
如果三个数的和小于0,说明 left 指针所指的数太小,我们需要将 left 向右移动以增大和。
如果三个数的和大于0,说明 right 指针所指的数太大,我们需要将 right 向左移动以减小和。
如果三个数的和恰好等于0,那么我们找到了一个符合要求的三元组,将其添加到结果中,并移动 left 和 right 指针以跳过重复的元素。
4.跳过重复元素
在遍历数组和使用双指针法的过程中,需要特别注意跳过重复的元素。因为如果数组中有重复的元素,那么这些重复的元素可能会生成重复的三元组。为了避免这种情况,在遍历数组时,如果当前元素与前一个元素相同,就跳过它。同样,在移动 left 和 right 指针时,如果它们所指的元素与前一个元素相同,也跳过它们。
5、处理结果
当我们找到一个符合要求的三元组时,需要将其存储起来。由于题目要求返回的是一个二维数组,需要为每个三元组分配内存,并将其添加到结果数组中。同时,还需要记录每个三元组的长度,通过 returnColumnSizes 来完成。
6、返回结果
最后,我们返回结果数组、结果数组的长度以及每个三元组的长度。
代码实现
/**
* Return an array of arrays of size *returnSize.
* The sizes of the arrays are returned as *returnColumnSizes array.
* Note: Both returned array and *columnSizes array must be malloced, assume
* caller calls free().
*/
int cmp(const void* a, const void* b) {
// 比较函数,用于 qsort 排序
return (*(int*)a - *(int*)b);
}
int** threeSum(int* nums, int numsSize, int* returnSize,
int** returnColumnSizes) {
if (numsSize < 3) {
// 如果数组元素少于3个,则无法构成三元组,直接返回空
*returnSize = 0;
return NULL;
}
// 先对数组进行排序
qsort(nums, numsSize, sizeof(int), cmp);
// 初始化结果数组和对应的大小数组
int** result = (int**)malloc(sizeof(int*) * numsSize * numsSize);
*returnColumnSizes = (int*)malloc(sizeof(int) * numsSize * numsSize);
*returnSize = 0;
for (int i = 0; i < numsSize - 2; i++) {
// 跳过重复元素
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
int left = i + 1;
int right = numsSize - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum == 0) {
// 当找到和为0的三元组时,分配内存并存储
result[*returnSize] = (int*)malloc(sizeof(int) * 3);
result[*returnSize][0] = nums[i];
result[*returnSize][1] = nums[left];
result[*returnSize][2] = nums[right];
(*returnColumnSizes)[*returnSize] = 3;
(*returnSize)++;
// 跳过重复元素,防止生成重复的三元组
while (left < right && nums[left] == nums[left + 1]) {
left++;
}
while (left < right && nums[right] == nums[right - 1]) {
right--;
}
// 移动指针继续寻找
left++;
right--;
} else if (sum < 0) {
// 和太小,左指针右移增大和
left++;
} else {
// 和太大,右指针左移减小和
right--;
}
}
}
return result;
}
第四题:21. 合并两个有序链表
题目
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例 1:
输入:l1 = [1,2,4], l2 = [1,3,4] 输出:[1,1,2,3,4,4]
示例 2:
输入:l1 = [], l2 = [] 输出:[]
示例 3:
输入:l1 = [], l2 = [0] 输出:[0]
提示:
- 两个链表的节点数目范围是
[0, 50]
-100 <= Node.val <= 100
l1
和l2
均按 非递减顺序 排列
解题思路
1.创建哑节点:
在合并两个链表时,为了方便处理边界情况(例如,当其中一个链表为空时),通常会引入一个哑节点(dummy node)。哑节点本身不存储任何有意义的数据,它的主要作用是作为合并后链表的起始点,从而简化链表头部的操作。
2.初始化指针:
创建一个current指针,并将其指向哑节点。这个指针将用于遍历合并后的链表,并在合适的位置插入来自list1和list2的节点。
3.比较并合并:
使用while循环遍历list1和list2,直到其中一个链表为空。在每次循环中,比较list1和list2当前节点的值:
如果list1的当前节点值小于list2的当前节点值,则将list1的当前节点连接到current之后,并将list1的指针向前移动一位。
否则,将list2的当前节点连接到current之后,并将list2的指针向前移动一位。
不论哪种情况,都将current指针向前移动一位,以便在下一次循环中连接下一个节点。
4.处理剩余部分:
当while循环结束时,至少有一个链表已经为空。此时,将另一个链表(如果有剩余节点)的剩余部分直接连接到current之后。这是因为在前面的循环中,已经确保了已连接的节点是按升序排列的,所以直接将剩余部分连接到末尾不会破坏顺序。
5.返回结果:
最后,返回哑节点的下一个节点作为合并后链表的头节点。哑节点本身不包含在合并后的链表中,它只是一个辅助工具。
代码实现
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
// 创建一个哑节点,用于保存合并后链表的头节点
struct ListNode* dummy = (struct ListNode*)malloc(sizeof(struct ListNode));
// 创建一个指针,用于遍历合并后的链表
struct ListNode* current = dummy;
// 当两个链表都不为空时,进行合并操作
while (list1 != NULL && list2 != NULL) {
// 比较两个链表当前节点的值,将较小的节点添加到合并后的链表中
if (list1->val < list2->val) {
current->next = list1;
list1 = list1->next;
} else {
current->next = list2;
list2 = list2->next;
}
// 移动 current 指针到下一个节点
current = current->next;
}
// 当其中一个链表为空时,将另一个链表的剩余部分添加到合并后的链表中
if (list1 != NULL) {
current->next = list1;
} else {
current->next = list2;
}
// 返回合并后的链表头节点
return dummy->next;
}
第五题:14. 最长公共前缀
题目
编写一个函数来查找字符串数组中的最长公共前缀。
如果不存在公共前缀,返回空字符串 ""
。
示例 1:
输入:strs = ["flower","flow","flight"] 输出:"fl"
示例 2:
输入:strs = ["dog","racecar","car"] 输出:"" 解释:输入不存在公共前缀。
提示:
1 <= strs.length <= 200
0 <= strs[i].length <= 200
strs[i]
仅由小写英文字母组成
解题思路
1.边界情况处理
如果输入的字符串数组strs为空(即strsSize为0),那么自然没有公共前缀,直接返回空字符串。
2.初始化遍历
我们选择数组中的第一个字符串strs[0]作为基准字符串,然后遍历它的每个字符。因为要找的是公共前缀,所以只需要找到一个前缀,使得所有字符串都包含这个前缀即可。
3.字符比较
对于strs[0]中的每个字符,使用内层循环遍历剩余的字符串strs[1]到strs[strsSize-1]。在内层循环中,比较当前位置上的字符是否相同。如果找到了一个位置,使得某个字符串的字符与strs[0]中的字符不同,那么就找到了最长公共前缀的终点。
4.截断并返回
一旦发现最长公共前缀的终点,就将strs[0]的当前字符设置为字符串结束符'\0',以截断它。然后返回截断后的strs[0],它就是最长公共前缀。
5.正常结束
如果内外层循环都正常结束,说明遍历完了strs[0]的所有字符,并且没有找到与strs[0]中字符不同的位置。这意味着strs[0]本身就是最长公共前缀(可能是因为所有字符串都相同,或者有些字符串本身就是strs[0]的前缀)。在这种情况下,直接返回strs[0]。
代码实现
char* longestCommonPrefix(char** strs, int strsSize) {
// 如果字符串数组为空,则没有公共前缀,返回空字符串
if (strsSize == 0) {
return "";
}
// 外层循环:遍历第一个字符串的每个字符
for (int i = 0; i < strlen(strs[0]); i++) {
// 内层循环:遍历剩余的字符串,与第一个字符串的当前字符进行比较
for (int j = 1; j < strsSize; j++) {
// 如果某个字符串的当前字符与第一个字符串的当前字符不同
if (strs[0][i] != strs[j][i]) {
// 将第一个字符串的当前字符设置为字符串结束符,即截断第一个字符串
strs[0][i] = '\0';
// 返回截断后的第一个字符串,它就是最长公共前缀
return strs[0];
}
}
}
// 如果循环正常结束,说明第一个字符串就是最长公共前缀(所有字符串都相同或为空)
return strs[0];
}
第六题:LCR 086. 分割回文串
题目
给定一个字符串 s
,请将 s
分割成一些子串,使每个子串都是 回文串 ,返回 s 所有可能的分割方案。
回文串 是正着读和反着读都一样的字符串。
示例 1:
输入:s = "google" 输出:[["g","o","o","g","l","e"],["g","oo","g","l","e"],["goog","l","e"]]
示例 2:
输入:s = "aab" 输出:[["a","a","b"],["aa","b"]]
示例 3:
输入:s = "a" 输出:[["a"]]
提示:
1 <= s.length <= 16
s
仅由小写英文字母组成
解题思路
采用回溯法来遍历所有可能的子串组合,并检查每个组合是否都是回文串。
1.初始化:
定义全局变量S存储输入的字符串,LEN存储字符串的长度。定义全局变量Path用于存储当前正在构建的回文子串组合的边界索引,Path_Index表示当前Path中的索引数量。定义全局变量Result用于存储最终的回文子串组合结果,定义全局变量RSIZE和RCOLSIZES用于返回结果数组的大小和每个子串组合的长度。
2.辅助函数isPalindrome:
定义一个辅助函数isPalindrome来判断一个字符串的子串(从索引l到r)是否是回文串。使用双指针方法,一个指针从l开始,另一个指针从r开始,逐步向中间移动,比较对应的字符是否相等。如果所有字符都相等,则返回true,否则返回false。
3.回溯函数Back_Track:
这是一个递归函数,用于遍历所有可能的子串组合。从字符串的起始位置offset开始,尝试每一个可能的结束位置i(从offset到LEN-1)。对于每个结束位置i,检查从offset到i的子串是否是回文串(使用isPalindrome函数)。如果是回文串,则将结束位置i添加到Path中,并递归调用Back_Track(i + 1)来继续寻找下一个回文子串。在递归返回之前,需要将Path_Index减1,以撤销对当前结束位置i的选择,进行下一个可能的选择。当offset等于LEN时,说明已经找到了一个完整的回文子串组合,此时调用Duplicate函数将当前组合复制到Result中,并更新结果数组的大小和每个子串组合的长度。
4.复制函数Duplicate:
这个函数用于将当前Path中存储的回文子串组合复制到Result中。根据Path_Index分配足够的空间来存储子串组合的指针数组。遍历Path,计算每个子串的长度和起始位置,并将子串内容复制到新的字符数组中。每个子串的末尾添加字符串结束符\0。
5.主函数partition:
初始化全局变量和动态分配Path的空间。调用Back_Track(0)开始回溯过程,从字符串的起始位置开始寻找回文子串组合。返回Result指针,即最终的回文子串组合结果。
代码实现
/**
* Return an array of arrays of size *returnSize.
* The sizes of the arrays are returned as *returnColumnSizes array.
* Note: Both returned array and *columnSizes array must be malloced, assume caller calls free().
*/
// 判断给定字符串s从l到r的部分是否为回文串
bool isPalindrome(char* s, int l, int r) {
while(l <= r) {
if(s[l] != s[r]) {
return false; // 如果字符不相等,则不是回文串
}
l++;
r--;
}
return true; // 字符都相等,是回文串
}
// 全局变量,用于存储输入的字符串、结果大小、结果列大小、当前字符串长度、路径数组等
char* S = NULL;
int* RSIZE = NULL;
int RCOLSIZES[32768];
int LEN = 0;
int* Path = NULL;
int Path_Index = 0;
char** Result[32768];
// 复制当前路径所代表的字符串到结果数组中
void Duplicate(char*** des) {
*des = (char**)malloc(sizeof(char*) * Path_Index); // 分配结果数组的空间
for(int i = 0; i < Path_Index; i++) {
int sublen = Path[i] - (i ? Path[i - 1] : -1); // 计算当前子串长度
int start = i ? (Path[i - 1] + 1) : 0; // 计算子串起始位置
(*des)[i] = (char*)malloc(sizeof(char) * (sublen + 1)); // 分配子串空间
for(int j = 0; j < sublen; j++) {
(*des)[i][j] = S[start + j]; // 复制子串到结果数组
}
(*des)[i][sublen] = '\0'; // 添加字符串结束符
}
}
// 回溯函数,用于查找字符串s的所有回文子串组合
void Back_Track(int offset) {
if(offset == LEN) { // 遍历到字符串末尾
Duplicate(Result + *RSIZE); // 复制当前路径到结果数组
RCOLSIZES[*RSIZE] = Path_Index; // 记录当前路径长度
(*RSIZE)++; // 结果数组大小加1
return;
}
for(int i = offset; i < LEN; i++) { // 从offset开始遍历到字符串末尾
if(isPalindrome(S, offset, i)) { // 判断从offset到i的子串是否为回文串
Path[Path_Index] = i; // 将i添加到路径中
Path_Index++; // 路径长度加1
Back_Track(i + 1); // 继续向下回溯
Path_Index--; // 回溯,路径长度减1
}
else {
continue; // 如果不是回文串,跳过当前循环,继续下一个循环
}
}
}
// 主函数,返回字符串s的所有回文子串组合
char*** partition(char* s, int* returnSize, int** returnColumnSizes) {
S = s; // 赋值全局变量S
RSIZE = returnSize; // 赋值全局变量RSIZE
*returnColumnSizes = RCOLSIZES; // 赋值全局变量RCOLSIZES
*RSIZE = 0; // 初始化结果数组大小为0
LEN = strlen(s); // 获取字符串长度
Path = (int*)malloc(sizeof(int) * LEN); // 分配路径数组空间
Path_Index = 0; // 初始化路径长度为0
Back_Track(0); // 从字符串开头开始回溯
return Result; // 返回结果数组
}