目录
元素和问题
15. 三数之和
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
for (int i = 0; i < nums.size(); i++) {
// 排序之后如果第一个元素已经大于零,无解,直接返回
if (nums[i] > 0) {
return result;
}
//去除i指向的不合适的重复元素
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
int left = i + 1;
int right = nums.size() - 1;
while (left < right) {
if (nums[i] + nums[left] + nums[right] > 0) {
right--;
//去除right指向的不合适的重复元素
while (left < right && nums[right] == nums[right + 1]) right--;
}
else if (nums[i] + nums[left] + nums[right] < 0) {
left++;
//去除left指向的不合适的重复元素
while (left < right && nums[left] == nums[left - 1]) left++;
}
else {
result.push_back(vector<int> {nums[i], nums[left], nums[right]});
//去除符合要求的重复元素
while (left < right && nums[right] == nums[right - 1]) right--;
while (left < right && nums[left] == nums[left + 1]) left++;
//同时移动双指针
right--;
left++;
}
}
}
return result;
}
该算法复杂度为O(n^2),固定一个元素后,后续元素采用双指针法进行筛选,在迭代的过程中注意去掉重复元素,并注意去重的时机。
- 对于i元素,由于i-1次循环时已经把i-1后的元素全部考虑过了,所以当nums[i]==nums[i-1]时,应对i进行剔除。【在最外层i循环时,已经把第一个元素为nums[i]的结果全都找齐了!】
- left和right所指向元素的剔除,则是根据是已经确定了该元素满足/不满足条件。
- 总结而言:已经完成了条件判断后,再予以剔除。
加速算法的几个关键在于:提前对数组进行排序、确定循环退出的时机。
18. 四数之和
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> res;
sort(nums.begin(), nums.end());
for (int i = 0; i < nums.size(); i++) {
//在最外层i循环时,已经把第一个元素为nums[i]的结果全都找齐了
if (i > 0 && 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;
}
int left = j + 1;
int right = nums.size() - 1;
while (left < right) {
if (nums[left] + nums[right] > target - (nums[i] + nums[j])) {
right--;
while (left < right && nums[right] == nums[right + 1]) right--;
}
else if (nums[left] + nums[right] < target - (nums[i] + nums[j])) {
left++;
while (left < right && nums[left] == nums[left - 1]) left++;
}
else {
res.push_back(vector<int>{nums[i], nums[j], nums[left], nums[right]});
//剔除满足结果的重复元素
while (left < right && nums[right] == nums[right - 1]) right--;
while (left < right && nums[left] == nums[left + 1]) left++;
right--;
left++;
}
}
}
}
return res;
}
本题和三数之和很类似,在外层嵌套一个循环,时间复杂度变为O(n^3)。但照搬上题暗藏以下陷阱:
- 不能采用上题中if(nums[i] > target) 就return res的方式,因为负数相加结果会越来越小;
- 多个数相加小心溢出,尽量更改代码的写法。例如采用:if (nums[left] + nums[right] > target - (nums[i] + nums[j]))
- 移动双指针的过程中,循环中的判断要始终保证(left <right),尤其是剔除重复元素时的while循环。
1237. 找出给定方程的正整数解
vector<vector<int>> findSolution(CustomFunction& customfunction, int z) {
vector<vector<int>> res;
// 双指针法:从小到大枚举x,从大到小枚举y
for (int x = 1, y = 1000; x <= 1000, y >= 1; x++) {
while (y >= 1 && customfunction.f(x, y) > z) {
y--;
}
if (y >= 1 && customfunction.f(x, y) == z) {
res.push_back({ x,y });
}
}
return res;
}
本题的一种解法是遍历x的同时,对y进行二分查找,从而时间复杂度是O(mlog(n)).
这里使用的双指针法原理为:
链表相关问题
21. 合并两个有序链表
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
// 如果p1或p2为空,直接返回
if (!list1) return list2;
if (!list2) return list1;
// 双指针法,在链表1上进行操作
ListNode* p1 = list1;
ListNode* p2 = list2;
ListNode* dummy_head = new ListNode(0);
ListNode* cur = dummy_head;
while (p1 && p2) {
if (p1->val <= p2->val) {
cur->next = new ListNode(p1->val);
p1 = p1->next;
}
else {
cur->next = new ListNode(p2->val);
p2 = p2->next;
}
cur = cur->next;
}
if (p1) cur->next = p1;
if (p2) cur->next = p2;
return dummy_head->next;
}
86. 分隔链表
ListNode* partition(ListNode* head, int x) {
ListNode* dummy_head_small = new ListNode(0);
ListNode* dummy_head_big = new ListNode(0);
ListNode* p_small = dummy_head_small;
ListNode* p_big = dummy_head_big;
ListNode* p = head;
while (p) {
if (p->val < x) {
p_small->next = new ListNode(p->val);
p_small = p_small->next;
}
else {
p_big->next = new ListNode(p->val);
p_big = p_big->next;
}
p = p->next;
}
p_small->next = dummy_head_big->next;
return dummy_head_small->next;
}
本题和上一题很类似,只不过上一题是合并链表,本题是拆分链表为一大一小。
876. 链表的中间结点
ListNode* middleNode(ListNode* head) {
ListNode* dummy_head = new ListNode(0);
dummy_head->next = head;
ListNode* fast = dummy_head;
ListNode* slow = dummy_head;
while (fast->next && fast->next->next) {
fast = fast->next->next;
slow = slow->next;
}
return slow->next;
}
快指针走两步,慢指针走一步。
160. 相交链表
ListNode* getIntersectionNode(ListNode* headA, ListNode* headB) {
int length_A = 0;
int length_B = 0;
ListNode* p_A = headA;
ListNode* p_B = headB;
while (p_A) {
length_A++;
p_A = p_A->next;
}
while (p_B) {
length_B++;
p_B = p_B->next;
}
int length_delta;
p_A = headA;
p_B = headB;
if (length_A >= length_B) {
length_delta = length_A - length_B;
while (length_delta--) {
p_A = p_A->next;
}
}
else {
length_delta = length_B - length_A;
while (length_delta--) {
p_B = p_B->next;
}
}
while (p_A && p_B) {
if (p_A == p_B) return p_A;
p_A = p_A->next;
p_B = p_B->next;
}
return NULL;
}
将A、B末端对齐,从长度相等的地方开始找。
83. 删除排序链表中的重复元素
ListNode* deleteDuplicates(ListNode* head) {
ListNode* slow = head;
ListNode* fast = head;
while (fast && fast->next) {
fast = fast->next;
if (fast->val == slow->val) {
slow->next = fast->next;
}
else {
slow = fast;
}
}
return head;
}
167. 两数之和 II - 输入有序数组
vector<int> twoSum(vector<int>& numbers, int target) {
int left = 0;
int right = numbers.size() - 1;
while (left < right) {
if (numbers[left] == target - numbers[right]) {
return { left + 1, right + 1 };
}
else if (numbers[left] < target - numbers[right]) {
left++;
}
else {
right--;
}
}
return { 0,0 };
}
接雨水问题
11.盛最多水的容器
int maxArea(vector<int>& height) {
// left、right分别记录[0,left]、[right,end]的最大值
int left = 0;
int right = height.size() - 1;
int res = 0;
while (left < right) {
res = max(res, (right - left) * min(height[left], height[right]));
// 移动较低的指针
if (height[left] < height[right]) left++;
else right--;
}
return res;
}
用 left
和 right
两个指针从两端向中心收缩,一边收缩一边计算 [left, right]
之间的矩形面积,取最大的面积值即是答案。值得注意的是双指针的移动方式:选择较低的一边移动,以期让能装的水增多。
42. 接雨水
int trap(vector<int>& height) {
// left、right分别记录[0,left]、[right,end]的最大值
int left = 0, right = height.size() - 1;
int left_max = 0, right_max = 0;
int res = 0;
while (left < right) {
left_max = max(left_max, height[left]);
right_max = max(right_max, height[right]);
// 接雨水:由较低的边决定
if (left_max < right_max) {
res += left_max - height[left];
left++;
}
else {
res += right_max - height[right];
right--;
}
}
return res;
}
本题在《单调栈》一章中分别利用单调栈和动态规划进行了求解,但相比之下双指针法更加简单易懂,如下图所示(图片来源:labuladong):
left_max
和 right_max
代表的是 height[0..left]
和 height[right..end]
的最高柱子高度。已经知道 l_max < r_max
时,装的水只和较低的 l_max
之差有关。因此至于 r_max
是不是右边最大的并不重要。即height[i]
能够装的水只和较低的 l_max
之差有关。
【双指针法的动态过程可以参考leetcode题解】