1. 两数之和
用的暴力做法——时间复杂度是O(n^2)
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
for(int i = 0; i < nums.size(); i++){
int l = nums[i];
for(int j = i + 1; j < nums.size(); j++){
if(nums[j] == target - l){
return {i,j};
}
}
}
return {-1,-1};
}
};
y总思路:
怎么实现O(n)的时间复杂度?
思路
对于给定的数组区间上,我们从头开始遍历数组,假设我们走到了箭头的位置,我们就去查看该位置前面是否有一个数等于 target - nums[i],这个查看过程我们就可以使用hash表来完成。
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
//声明一个hash表<数值,索引>
unordered_map<int, int> heap;
for(int i = 0; i < nums.size(); i++){
int r = target - nums[i];
//查看 i 前面是否存在满足条件的数
//count()是unordered_map的一个方法判断 Key 中是否存在目标值
if(heap.count(r)) return {heap[r], i};
//不存在则放入
heap[nums[i]] = i;
}
return {};
}
};
2. 两数相加
思想
模拟了手动相加列竖式的过程,分别从两个链表头开始相加,直到两个链表结束并且没有进位。
技巧
可以创建一个虚拟头节点,方便最后结果输出
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
//创建一个node节点,head作为虚拟头节点存放地址
ListNode* head = new ListNode(0);
//cur也指向第一个头节点,但是用于更新后面的节点
ListNode* cur = head;
int t = 0;
//链表不为空,或者t不为0就一直循环
while(l1 || l2 || t) {
if(l1) t += l1->val, l1 = l1->next;
if(l2) t += l2->val, l2 = l2->next;
cur = cur->next = new ListNode(t % 10);
t /= 10;
}
//虚拟头节点派上用处
return head->next;
}
};
//技巧:
ListNode* head = new ListNode(0);
ListNode* cur = head;
//等价于
auto head = new ListNode(0), cur = head;
3. 无重复字符的最长子串
思路
这道题目是使用双指针算法。首先是第一个指针 i 我们枚举表示这个字串的末尾,另一个指针 j 则是从 i 开始不断往前枚举,直到最远位置。[j,i]就表示以 i 结尾的最长字串,然后枚举 i 找出所有 i 中的最长字串长度就是 res。
为什么 i 枚举出来的就是最长字串?
假设我们 i 往后移一位,j’ 新的位置一定位于原来 j 或者 j 的右边。我们可以通过反证法说明,因为[j,i]就表示以 i 结尾的最长字串,如果 j’ 位于原来 j 的左边,说明原来的 j 不是最远的位置,就矛盾了。
所以正是这样的单调性才能得到正确的结果。并且我们通过维护一个hash表来存放[j,i]中的所有元素,当加入下一个元素的时候,如果有重复就将 j 移到重复元素的位置开始。
class Solution {
public:
int lengthOfLongestSubstring(string s) {
unordered_map<char, int> heap;
int res = 0;
for(int i = 0, j = 0; i < s.size(); i++){
//把i字符加入到hash表中
heap[s[i]]++;
//判断是都存在重复,存在重复就移动j指针的位置
while(heap[s[i]] > 1) heap[s[j++]]--;
res = max(res, i - j + 1);
}
return res;
}
};
4. 寻找两个正序数组的中位数
思路
由于时间复杂度是log(m+n),就要思考一种递归的做法来实现。对于给定的两个数组nums1,nums2,我们从中找出第 k 个数是多少,在这里要找的k = (m+n)/2。我们去判断 nums1[k/2−1],nums2[k/2−1]大小关系:
- nums1[k/2−1] > nums2[k/2−1] 就表明中nums2中的 起始点到 k/2−1范围的数都不是第 k 个数,可以删除
所以剩下的数 k - k/2 个数我们就从剩下的区间中去递归找 - nums2[k/2−1] > nums1[k/2−1] 这种情况则是和上面分析的相反,过程分析一致
- nums1[k/2−1] = nums2[k/2−1] 当相同的时候就是表示找到了第 k 个数,取其中之一就是答案
剩下是代码实现的细节和注意事项
class Solution {
public:
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
//首先计算两个数组的长度之和
int tot = nums1.size() + nums2.size();
//对于tot要分奇偶两种情况讨论
if(tot % 2 == 0) {
int left = findkthnumber(nums1, 0, nums2, 0, tot / 2);
int right = findkthnumber(nums1, 0, nums2, 0, tot / 2 + 1);
return (left + right) / 2.0;
} else {
return findkthnumber(nums1, 0, nums2, 0, tot / 2 + 1);
}
}
int findkthnumber(vector<int> nums1, int i, vector<int> nums2, int j, int k) {
//先判断nums1和nums2的大小关系,保证第一个数组是小的
if(nums1.size() - i > nums2.size() - j) return findkthnumber(nums2, j, nums1, i, k);
//判断小数组是否为空
if(nums1.size() == i) return nums2[j + k - 1];
//当k == 1时候选择最小的起始点就是答案
if(k == 1) return min(nums1[i], nums2[j]);
int si = min(i + k / 2, int(nums1.size())), sj = j + k / 2;
if(nums1[si - 1] > nums2[sj - 1]) {
return findkthnumber(nums1, i, nums2, sj, k - (sj - j));
} else {
return findkthnumber(nums1, si, nums2, j, k - (si - i));
}
}
};
这题难点一个是怎么相处log(m+n)的解法,另一个难点就是递归过程中完整的边界情况的考虑
5. 最长回文子串
思想
这道题目解法很多,我们这里使用一个比较容易想的方法
对于给定的字符串,我们可以枚举每一个可能的回文串的中心位置,由于回文串长度可能是奇数,也可能是偶数所以我们都需要考虑,从回文串的中心位置开始,如果左右两边相等,我们就使两个指针分别左右移动。
class Solution {
public:
string longestPalindrome(string s) {
string res;
for(int i = 0; i < s.size(); i++){
//如果是奇数长
int l = i - 1, r = i + 1;
while(l >= 0 && r < s.size() && s[l] == s[r]) l--,r++;
if(res.size() < r - l - 1)res = s.substr(l + 1, r - l - 1);
//如果是偶数长
l = i, r = l + 1;
while(l >= 0 && r < s.size() && s[l] == s[r]) l--,r++;
if(res.size() < r - l - 1)res = s.substr(l + 1, r - l - 1);
}
return res;
}
};
6. Z 字形变换
思想
我们可以假设数字来模拟,可以发现第一行和最后一行都是以第一个数字为起点的等差数列,公差 = 2n - 2;
对于中间的行,我们可以进行分组,每一行的第一组是再竖线上的数列,公差同样满足 2n - 2;第二组数列是在斜线上的,同样满足2n - 2
class Solution {
public:
string convert(string s, int numRows) {
string res;
//对于1行的情况需要特殊判断
if(numRows == 1) return s;
for(int i = 0; i < numRows; i++){
//对于第一行或者最后一行情况
if(i == 0 || i == numRows - 1) {
for(int j = i; j < s.size(); j += 2 * numRows - 2) {
res += s[j];
}
//对于中间行的处理
} else {
for(int k = i, j = 2 * numRows - 2 - k; k < s.size() || j < s.size(); k += 2 * numRows - 2, j += 2 * numRows - 2) {
//只有k ,j都没有超出边界的时候才能加入结果
if(k < s.size()) res += s[k];
if(j < s.size()) res += s[j];
}
}
}
return res;
}
};
7. 整数反转
思想
对于正数例如1234,我们就是分别取出各个数字,算法就是通过循环 1234 % 10 = 4;1234 / 10;
对于负数例如-1234,C++中取模运算有所不同,-1234 % 10 = -4;-1234 / 10;
当我们获得各个数字的以后我们可以每位 * 10 相加恢复成一个整数
注意点:题目要求了不能使用 long long 就需要特殊判断一下溢出的情况
class Solution {
public:
int reverse(int x) {
int r = 0;
while(x) {
if(x > 0 && r > (INT_MAX - x % 10) / 10) return 0;
if(x < 0 && r < (INT_MIN - x % 10) / 10) return 0;
//以上两个if就是用来判断r可能溢出INT的情况
r = r * 10 + x % 10;
x /= 10;
}
return r;
}
};
8. 字符串转换整数 (atoi)
思想
我们一步一步考虑可能出现情况,并将他处理了即可
- 可能会出现前面很多空格的情况,我们就声明一个变量循环取出字符串中的每一个字符,和’ '比较
- 空格后可能出现符号 - 和 + 我们同样需要进行判断,并用变量存储起来
- 符号后面就是数字,数字部分我们需要注意额外的范围判断,并且注意正负范围不是对称的情况
class Solution {
public:
int myAtoi(string s) {
//首先需要判断一下' ',把前面的空格全部删除
int k = 0;
while(s[k] == ' ') k++;
if(k == s.size()) return 0;
//再判断可能遇到的 - 和 +
int f = 1;
if(s[k] == '-') f = -1, k++;
else if(s[k] == '+') k++;
//取出每一个数字
int res = 0;
while(k < s.size() && s[k] >= '0' && s[k] <= '9') {
int x = s[k++] - '0';
//可能存在溢出情况需要特殊判断
if(f > 0 && res > (INT_MAX - x) / 10) return INT_MAX;
if(f < 0 && -res < (INT_MIN + x) / 10) return INT_MIN;
//由于正负区间不是对称的,所以需要特殊判断一下范围更大的负数边界
if(-res * 10 - x == INT_MIN) return INT_MIN;
res = res * 10 + x;
}
return res * f;
}
};
9. 回文数
思想
这题目的方法很多也比较简单,这里主要介绍翻转法和数值法
- 翻转法
rbegin()
表示翻转后的第一个元素,rend()
同理。
class Solution {
public:
bool isPalindrome(int x) {
if(x < 0) return false;
string s = to_string(x);
return s == string(s.rbegin(), s.rend());
}
};
- 数值法
数值法就是和前面整数翻转思想一模一样,并且这道题目没有long long
的限制
class Solution {
public:
bool isPalindrome(int x) {
if(x < 0) return false;
long long res = 0, k = x;
while(x) {
res = res * 10 + x % 10;
x /= 10;
}
return k == res;
}
};
11. 盛最多水的容器
思想
使用双指针算法,i从数组的起点开始,j从数组的结尾开始,每次都先计算一下[i, j]
范围内的结果,和上一次计算结果比较取最大值。随后比较i,j两个位置的大小,小的一侧移动一位。
class Solution {
public:
int maxArea(vector<int>& height) {
int res = 0;
for(int i = 0, j = height.size() - 1; i < j;) {
res = max(res, min(height[i], height[j]) * (j - i));
if(height[i] < height[j]) i++;
else j--;
}
return res;
}
};
12. 整数转罗马数字
思想
我们根据题干和数据得内容判断,这个题目应该可以枚举找出其中的规律
[图片]
其中每一位的情况中有几个特殊的情况比如,900、500、400、90、50、40、9、5、4
给定的整数我们从高位开始从大到小判断整数属于哪个区间,减去对应区间的数值,这样描述比较抽象,我按照2469距离:
class Solution {
public:
string intToRoman(int num) {
int a[] = {
1000,
900, 500, 400, 100,
90, 50, 40, 10,
9, 5, 4, 1
};
string b[] = {
"M",
"CM", "D", "CD", "C",
"XC", "L", "XL", "X",
"IX", "V", "IV", "I"
};
string res;
for(int i = 0; i < 13; i++) {
while(num >= a[i]) {
num -= a[i];
res += b[i];
}
}
return res;
}
};
13. 罗马数字转整数
这道题目就是上一题目的逆运算,需要特殊判断的情况是:前一位小于后一位,例如4、40…
class Solution {
public:
int romanToInt(string s) {
unordered_map<char, int> heap;
heap['I'] = 1, heap['V'] = 5,
heap['X'] = 10, heap['L'] = 50,
heap['C'] = 100, heap['D'] = 500,
heap['M'] = 1000;
int res = 0;
for(int i = 0; i < s.size(); i++) {
if(i + 1 < s.size() && heap[s[i]] < heap[s[i + 1]]) res -= heap[s[i]];
else res += heap[s[i]];
}
return res;
}
};
14. 最长公共前缀
思想
通过暴力做法:第一层循环遍历每一位,第二层循环遍历i位置的每一个字符串是否相等
class Solution {
public:
string longestCommonPrefix(vector<string>& strs) {
string res;
if(strs.empty()) return res;
for(int i = 0; i < strs[0].size(); i++) {
char c = strs[0][i];
for(auto stri : strs) {
if(i > stri.size() || c != stri[i]) return res;
}
res += c;
}
return res;
}
};
启发:增强for循环这里第二层循环就很好用,可以快速取出strs中的每一个字符串
15. 三数之和
思想
这题目和后面一些题目都是类似的方法,都会用到双指针算法,而双指针算法需要明确的第一件事——数组是有序的。因为这个题目里面一共有三个变量,定义 i,j,k三个指针,没有三指针算法,所以需要先固定一个指针i,默认三个指针大小顺序是 i < j < k,可以减少一部分重复,固定了i指针,j从i后面开始遍历,k从尾部倒序遍历,目标是找出满足 nums[i] + nums[j] + nums[k] >= 0
的最小的k ,这样能够简化时间复杂度的运用是排完序以后,如果j增加,那么k一定只能往左走,因为nums[i]是固定的。
重复的问题怎么解决?从i入手,每一次先判断i位置的数值和上个是否相同,相同就跳过。
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> res;
sort(nums.begin(), nums.end());
//第一个指针i遍历整个集合
for(int i = 0; i < nums.size(); i++) {
//i 不等于0 并且 第i个数和i - 1相同的时候是重复的情况
if(i && nums[i] == nums[i - 1]) continue;
//双指针
for(int j = i + 1, k = nums.size() - 1; j < k; j++) {
//去重
if(j > i + 1 && nums[j] == nums[j - 1]) continue;
//试探性查看k - 1是不是最小能取到的值
while(j < k - 1 && nums[i] + nums[j] + nums[k - 1] >= 0) k--;
if(nums[i] + nums[j] + nums[k] == 0)
res.push_back({nums[i], nums[j], nums[k]});
}
}
return res;
}
};
16. 最接近的三数之和
思想
这题和上面的题目解法是类似的,并且这个问题最后只需要返回结果数值,所以可以省去去重的过程,但是需要注意的一个问题是最终结果可能是nums[i] + nums[j] + nums[k]
(大于target)或者nums[i] + nums[j] + nums[k - 1]
(小于target)
class Solution {
public:
int threeSumClosest(vector<int>& nums, int target) {
//第一步先排序
sort(nums.begin(), nums.end());
//pair数组first存放和target的插值,second存放实际数值
pair<int, int> res(INT_MAX, INT_MAX);
for(int i = 0; i < nums.size(); i++) {
for(int j = i + 1, k = nums.size() - 1; j < k; j++) {
while(j < k - 1 && nums[i] + nums[j] + nums[k - 1] >= target) k--;
int s = nums[i] + nums[j] + nums[k];
res = min(res, make_pair(abs(s - target), s));
//这个特判不可少
if(j < k - 1) {
int n = nums[i] + nums[j] + nums[k - 1];
res = min(res, make_pair(target - n, n));
}
}
}
return res.second;
}
};
17. 电话号码的字母组合
思想
需要找出所有的排序方式,这个问题是典型的DFS暴搜问题,这里我们先来回顾一下DFS知识点。DFS中比较重要的两个概念是回溯和剪枝。
例题:给定一个整数n,将数字1~n排成一排,请你按照字典序将所有的排列方法输出。这就是一个最经典的DFS问题,思考DFS问题我们首先需要画出搜索树,这样能够帮助我们理解DFS过程,DFS就是从第一个位置开始一直往下搜索知道没有路可以走,再回溯。
//给定一个整数n,将数字1~n排成一排,将会有很多种排列方法。
//现在,请你按照字典序将所有的排列方法输出。
//经典的DFS问题
#include <iostream>
using namespace std;
const int N = 20;
int n, path[N];//path数组用来存放搜索的路径结果
bool st[N];//st数组用来保存状态,表示是否已经使用过
void dfs(int u) {
//当 u == n 表示搜索到最后+1位,这个路径搜索结束可以输出结果了
if (u == n) {
for (int i = 0; i < n; ++i)
printf("%d", path[i]);
puts("");
return;
}
//没有走到最后一位+1时,就递归搜索
for (int i = 1; i <= n; ++i) {
if (!st[i]) {
path[u] = i;
st[i] = true;
dfs(u + 1);
//递归搜索结束以后需要恢复状态
st[i] = false;
}
}
}
int main() {
scanf("%d", &n);
dfs(0);//从第一个位置开始搜索
return 0;
}
有了这个基础我们再来看电话号码的问题:
- 首先我们需要定义一个数组用来存放电话数字对应的字符
- DFS搜索出全部的结果
class Solution {
public:
vector<string> res;
string path;//path作为某一次路径的记录变量
string phone[10] = {
"", "", "abc", "def",
"ghi", "jkl", "mno",
"pqrs", "tuv", "wxyz"
};
vector<string> letterCombinations(string digits) {
//特判是否为空的情况
if (digits.empty()) return res;
dfs(digits, 0);
return res;
}
void dfs(string& digits, int u) {
//判断u是否已经走到最后的位置
if (u == digits.size()) {
res.push_back(path);
} else {
//else 就是没有搜末尾的情况,需要递归dfs处理
for (auto c : phone[digits[u] - '0']) {
path.push_back(c);
dfs (digits, u + 1);
//搜索完回溯
path.pop_back();
}
}
}
};
在精简,path作为参数传入
class Solution {
public:
vector<string> res;
string phone[10] = {
"", "", "abc", "def",
"ghi", "jkl", "mno",
"pqrs", "tuv", "wxyz"
};
vector<string> letterCombinations(string digits) {
if (digits.empty()) return res;
dfs(digits, 0, "");
return res;
}
void dfs(string& digits, int u, string path) {
if (u == digits.size()) res.push_back(path);
else {
for (auto str : phone[digits[u] - '0'])
//包含了回溯的过程
dfs (digits, u + 1, path + str);
}
}
};
18. 四数之和
和上面做的三数之和完全类似
class Solution {
public:
vector<vector<int>> res;
//暴力做法n^4,可以通过双指针做法实现n^3
vector<vector<int>> fourSum(vector<int>& nums, int target) {
//双指针问题时有序为前提的,所以排序操作不能忘记
sort(nums.begin(), nums.end());
//处理一下超范围情况
if(target == -294967296 || target == 294967296)return {};
for(int i = 0; i < nums.size(); i++) {
if(i && nums[i] == nums[i - 1]) continue;
for(int j = i + 1; j < nums.size(); j ++) {
if(j > i + 1 && nums[j] == nums[j - 1]) continue;
for(int k = j + 1, l = nums.size() - 1; k < l; k++) {
if(k > j + 1 && nums[k] == nums[k - 1]) continue;
while(k < l - 1 && nums[i] + nums[j] >= target - nums[k] - nums[l - 1]) l--;
if(nums[i] + nums[j] == target - nums[k] - nums[l]) res.push_back({nums[i], nums[j], nums[k], nums[l]});
}
}
}
return res;
}
};
19. 删除链表的倒数第 N 个结点
思想
过程就是先遍历一遍链表得到链表的总长度,删除倒数第k个结点,需要知道倒数第k+1个结点是什么。
注意点:遇到头节点可能变化的题目需要使用到一个技巧——使用虚拟头节点
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
//创建一个虚拟结点-1,dummy指针指向这个虚拟节点
//头指针dummy指向虚拟头节点,虚拟头节点的next是原始头节点
ListNode* dummy = new ListNode(-1);
dummy->next = head;
//统计链表的长度
int cnt = 0;
for(ListNode* i = dummy; i; i = i->next) cnt++;
//找出倒数 k + 1 个结点
ListNode* p = dummy;
for(int i = 0; i < cnt - n - 1; i++) p = p->next;
p->next = p->next->next;
return dummy->next;
}
};
20. 有效的括号
思想
分析题目首先想到这个和栈数据结构非常相似,如果是左括号的一种情况那么就选择放入栈中,如果是右括号中的一种情况,就去分析能否和栈顶的元素配对,能配对就弹出原来的栈顶元素,不能配对就说明不满足条件return false
class Solution {
public:
bool isValid(string s) {
//可以先通过奇偶判断
int n = s.size();
if(n % 2 == 1) return false;
unordered_map<char, char> k = {
{')', '('},
{']', '['},
{'}', '{'}
};
stack<char> stack;
for(auto str: s) {
//判断是左括号还是右括号;左括号考虑加入栈,右括号考虑是否配对
if(k.count(str)) {
if(stack.empty() || stack.top() != k[str]) return false;
else stack.pop();
} else {
stack.push(str);
}
}
return stack.empty();
}
};
y总的简化代码
class Solution {
public:
bool isValid(string s) {
stack<char> stack;
for(auto ca: s) {
if(ca == '(' || ca == '[' || ca == '{') stack.push(ca);
else {
//通过ascaii表发现配对的括号差小于2
if(!stack.empty() && abs(stack.top() - ca) <= 2) stack.pop();
else return false;
}
}
return stack.empty();
}
};
21. 合并两个有序链表
思想
这道题目就是归并排序中的核心思想,只不过是在链表的背景之下。同时,这里再次重申一下题目的小技巧,凡是涉及到头节点需要特判或者头节点会变化的情况,我们就创建一个虚拟头节点。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
//创建一个虚拟头节点dummy,同时为了方便我们更新下一个结点,使用tail节点
auto dummy = new ListNode(-1), tail = dummy;
//等价于 ListNode* dummy = new ListNode(-1);
// ListNode* tail = dummy;
//l1 和 l2 都不为空的时候判断大小
while(list1 && list2) {
if(list1->val < list2->val) {
//tail和list1都需要更新
tail = tail->next = list1;
list1 = list1->next;
} else {
tail = tail->next = list2;
list2 = list2->next;
}
}
//当一个指针到达末尾时,只需要把tail下一个节点接到不为空的那个链表后面
if(list1) tail->next = list1;
if(list2) tail->next = list2;
return dummy->next;
}
};
22. 括号生成
思想
对于括号配对问题需要了解两个常用到的结论(前提是只有一种类型括号):
- 左括号的数量小于等于右括号的数量时,才能配对
- 最终左右括号数量相等
对于这个问题需要给出所有的排列方式的题目,我们应该条件反射的想到dfs来解决。
代码中并没有显式的写出回溯的过程,起始回溯隐藏在了if判断中,if的本质含义起始就是可以实现的几种方案,当一种方案可行并且结束以后,返回到if判断,就会考虑下一个if;即通过多个if考虑了所有可能的情况
class Solution {
public:
vector<string> res;
vector<string> generateParenthesis(int n) {
dfs(n, 0, 0, "");
return res;
}
void dfs(int& n, int ln, int rn, string path) {
//当左括号数目和右括号数目都等于n的时候这条搜索路径结束
if(ln == n && rn == n) res.push_back(path);
else {
//if含义,就是dfs时当前可能走的分支情况,本题就只有两种情况
if(ln < n) dfs(n, ln + 1, rn, path + "(");
if(ln > rn && rn < n) dfs(n, ln, rn + 1, path + ")");
}
}
};
24. 两两交换链表中的节点
思想
整个过程如图所示,涉及到头节点变化的题目都可以通过创建虚拟头节点提供便利。交换过程需要考虑先后问题
链表题注意事项:
以下错误原因,链表的下一个节点是否为空都需要有一个判断,否则就会出现这个错误
runtime error: member access within null pointer of type 'ListNode'
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
auto dummy = new ListNode(-1), tail = dummy;
dummy->next = head;
while(tail->next && tail->next->next) {
auto h1 = tail->next, h2 = h1->next;
h1->next = h2->next;
h2->next = h1;
tail->next = h2;
tail = h1;
}
return dummy->next;
}
};
25. K 个一组翻转链表
思想
本题和上一题思路非常相似,变成了翻转k个数。
- 第一步我们还是创建虚拟头节点
- 通过遍历的方式判断后续是否含有k个节点,不足则不进行翻转操作
- 整个翻转的过程我们可以分为三步
a. 第一步完成k个节点内部指针的反向指针(注意翻转变化的顺序)
b. 第二步虚拟头节点下一个指针改变
c. 第三步k个节点的最后一个节点指针改变 - 指针改变需要三个指针,通过下图帮助理解
/**
* 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) {
//注意提前保存后续要使用到的节点
ListNode* dummy = new ListNode(-1);
dummy->next = head;
for(ListNode* h0 = dummy;;) {
//遍历查看是否含有k个节点
ListNode* h00 = h0;
for(int i = 0; i < k && h00; i++) h00 = h00->next;
if(!h00) break;
//含有k个元素,需要通过赋值的两个指针进行链表内部的翻转
auto h1 = h0->next, h11 = h0->next, h2 = h1->next;
//一共k个元素需要进行k - 1次翻转操作
for(int i = 0; i < k - 1; i++) {
auto h3 = h2->next;
h2->next = h1;
h1 = h2, h2 = h3;
}
h0->next = h1;
h11->next = h2;
h0 = h11;
}
return dummy->next;
}
};
26. 删除有序数组中的重复项
思想
这其实就是一个unique函数,我们就去判断一个数和它前一个数是否重复,比较简单直接上代码
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
int cnt = 0;
for(int i = 0; i < nums.size(); i++) {
if(!i || nums[i] != nums[i - 1]) nums[cnt++] = nums[i];
}
return cnt;
}
};
27. 移除元素
思想
和上面题目思想高度类似。通过cnt变量记录应该放置在数组的什么位置,比较简单代码如下:
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int cnt = 0;
for(int i = 0; i < nums.size(); i++) {
if(nums[i] != val)
nums[cnt++] = nums[i];
}
return cnt;
}
};
28. 找出字符串中第一个匹配项的下标
这道题目就是KMP算法的内容,需要回顾一下KMP的知识。
class Solution {
public:
int strStr(string haystack, string needle) {
if(needle.empty()) return 0;
int n = haystack.size(), m = needle.size();
int ne[m];
//预处理needle
ne[0] = -1;
for(int i = 1, j = -1; i < m; i++) {
//如果新加入的字符不能匹配,j一直回退
while(j != -1 && needle[i] != needle[j + 1]) j = ne[j];
//如果新加入的字符能够匹配,最长字串长度增加,继续匹配
if(needle[i] == needle[j + 1]) j++;
ne[i] = j;
}
//KMP处理,和上面处理思想一致
for(int i = 0, j = -1; i < n; i++) {
while(j != -1 && haystack[i] != needle[j + 1]) j = ne[j];
if(haystack[i] == needle[j + 1]) j++;
if(j == m - 1) {
return i - j;
}
}
return -1;
}
};
29. 两数相除
思想
我们以147 / 3 = 49为例,49的二进制表示 = 110001
由此我们可以借助二进制表示,提前存储好所有2^k * divisor的情况,题目条件限制比较多,只能使用int类型,并且考虑到正数范围比负数小,所以我们在处理的时候统一使用负数来处理。在处理过程中需要及时考虑边界条件溢出的情况。
class Solution {
public:
int divide(int x, int y) {
bool flag = (x > 0) ^ (y > 0);
if (x > 0) x = -x;
if (y > 0) y = -y;
vector<pair<int, int>> tmpadd;
for(int i = y, j = -1; i >= x; i += i, j += j) {
tmpadd.emplace_back(i, j);
if(i < (INT_MIN >> 1)) break;
}
int res = 0;
for(int i = tmpadd.size() - 1; i >= 0; i--) {
if(tmpadd[i].first >= x) {
res += tmpadd[i].second;
x -= tmpadd[i].first;
}
}
if(!flag) {
if(res == INT_MIN) return INT_MAX;
res = -res;
}
return res;
}
};
⭐这个专栏将会持续更新LeetCode题解和思路分享,欢迎留言交流!