这是我在开始编程时希望能够阅读到的文章。编程是关于解决问题的,我将深入介绍20种问题解决技巧,包括代码示例、大O分析和挑战,以便你能够掌握它们。
在本文中,我概述了为下一次编程面试做准备的高级策略以及需要避免的常见错误。在这里,我将深入介绍20种必须掌握的问题解决技巧,它们对我在工作中也很有帮助,甚至给了我一个正在进行的副项目的想法。此外,最后一部分包括了一个逐步学习(而不是死记硬背)任何算法或数据结构的指南,其中包含2个示例。
我将这些技巧分为以下几类:
- 基于指针的技巧
- 基于递归的技巧
- 排序和搜索
- 扩展基本数据结构
- 其他杂项技巧
两数之和
给定一个已经按升序排列的整数数组,找到两个数使它们相加等于一个特定的目标数。函数twoSum应该返回这两个数的索引,其中index1必须小于index2。
注意:
- 返回的答案(index1和index2)不是从零开始的。
- 你可以假设每个输入都有且仅有一个解决方案,并且你不可以重复使用相同的元素。
示例:
输入:
numbers = [2,7,11,15],target = 9
输出:
[1,2]
说明:
2和7的和为9。因此index1 = 1,index2 = 2。
解决方案
由于数组a是排序的,我们知道:
- 最大的和等于最后两个元素的和
- 最小的和等于前两个元素的和
- 对于任何索引i在[0,a.size() - 1) => a[i + 1] >= a[i]
有了这个,我们可以设计以下算法:
- 我们保持两个指针:l,从数组的第一个元素开始,r从最后一个元素开始。
- 如果a[l] + a[r]的和小于我们的目标,我们将l增加一(将加法中的最小操作数更改为l+1处的另一个等于或大于它的数);如果它大于目标,我们将r减少一(将最大操作数更改为r-1处的另一个等于或小于它的数)。
- 我们这样做,直到a[l] + a[r]等于我们的目标,或者l和r指向相同的元素(因为我们不能重复使用相同的元素),或者交叉,表示没有解决方案。
以下是一个简单的C++实现:
vector<int> twoSum(const vector<int>& a, int target) {
int l = 0, r = a.size() - 1;
vector<int> sol;
while(l < r) {
const int sum = a[l] + a[r];
if(target == sum){
sol.push_back(l + 1);
sol.push_back(r + 1);
break;
} else if (target > sum) {
++l;
} else {
--r;
}
}
return sol;
}
时间复杂度为O(N),因为我们可能需要遍历数组的N个元素来找到解决方案。
空间复杂度为O(1),因为我们只需要两个指针,不管数组包含多少元素。
从数组中删除重复项
给定一个排序数组nums,原地删除重复项,使每个元素只出现一次,并返回新的长度。
不要为另一个数组分配额外的空间,必须通过修改输入数组来实现,使用O(1)的额外内存。
示例1:
nums = [1,1,2],
输出 = 2
示例2:
nums = [0,0,1,1,1,2,2,3,3,4],
输出 = 5
返回长度之外的值无关紧要。
解决方案
数组已经排序,并且我们希望将重复项移动到数组的末尾,这听起来很像基于某个条件进行分组。你会如何使用两个指针来解决这个问题?
- 你需要一个指针来遍历数组,i。
- 还需要一个指针n,用于定义不包含重复项的区域:[0,n]。
逻辑如下。如果索引i处的元素的值(除了i = 0的情况)和i-1处的元素的值相同:
- 相同,我们不做任何操作 - 这个重复项将被下一个唯一元素覆盖。
- 不同:我们将a[i]添加到不包含重复项的数组部分 - 由n界定,并将n增加一。
int removeDuplicates(vector<int>& nums) {
if(nums.empty())
return 0;
int n = 0;
for(int i = 0; i < nums.size(); ++i){
if(i == 0 || nums[i] != nums[i - 1]){
nums[n++] = nums[i];
}
}
return n;
}
此问题具有线性时间复杂度和常数空间复杂度
排序颜色
给定一个包含红色、白色或蓝色的对象的数组,对它们进行原地排序,使得相同颜色的对象相邻,并且颜色的顺序为红色、白色和蓝色。这里,我们使用整数0、1和2分别表示红色、白色和蓝色。注意:你不能使用库函数的排序功能来解决这个问题。
示例:
输入:
[2,0,2,1,1,0]
输出:
[0,0,1,1,2,2]
解决方案
这次的分组是:
- 小于1
- 等于1
- 大于1
我们可以通过3个指针来实现。
这个实现有点棘手,所以请确保进行充分的测试。
void sortColors(vector<int>& nums) {
int smaller = 0, eq = 0, larger = nums.size() - 1;
while(eq <= larger){
if(nums[eq] == 0){
swap(nums[smaller], nums[eq]);
++smaller; ++eq;
} else if (nums[eq] == 2) {
swap(nums[larger], nums[eq]);
--larger;
} else {
eq++;
}
}
}
由于需要遍历数组进行排序,时间复杂度为O(N)。空间复杂度为O(1)。
有趣的是,这是Dijkstra所描述的荷兰国旗问题的一个实例。
从链表末尾删除第n个节点
给定一个链表和一个数字n,编写一个函数,返回链表倒数第n个节点的值。
解决方案
这是两指针技术最常见的变体之一:引入一个偏移量,使得其中一个指针达到特定条件时,另一个指针处于你感兴趣的位置。
在这种情况下,如果我们将其中一个指针f移动n次,然后同时开始逐个节点向前移动两个指针,当f到达链表末尾时,另一个指针s将指向我们要删除的节点的前一个节点。
确保定义n = 1的含义(最后一个元素还是倒数第二个元素的前一个元素?),并避免一位错误。
时间和空间复杂度与前面的问题相同。
找到未知大小链表的中间节点
给定一个非空的单链表,头节点为head,返回链表的中间节点。如果存在两个中间节点,则返回第二个中间节点。
示例1:
输入:
[1,2,3,4,5]
输出:
链表中的节点3
解决这个问题可以通过两次遍历来实现:第一次遍历计算链表的长度L,第二次遍历只需前进L/2个节点即可找到链表中间的元素。这种方法的时间复杂度为线性,空间复杂度为常数。
那么,如何使用两个指针在一次遍历中找到中间元素呢?
如果其中一个指针f的移动速度是另一个指针s的两倍,当f到达末尾时,s将在链表的中间。
以下是我在C++中的解决方案。在测试代码时,请考虑边界情况(空列表、奇数和偶数大小的列表等)。
ListNode* middleNode(ListNode* head) {
ListNode* slow = head;
ListNode* fast = head;
while (fast != nullptr && fast->next != nullptr) {
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
查找重复数字
给定一个包含n + 1个整数的数组nums,每个整数都在1和n(包括1和n)之间,证明至少存在一个重复数字。假设只有一个重复数字,找到这个重复数字。
示例1:
输入:
[1,3,4,2,2]
输出:
2
这与前面的问题/解决方案相同,只是针对数组而不是链表。
int findDuplicate(const vector<int>& nums) {
int slow = nums[0], fast = slow;
do {
slow = nums[slow];
fast = nums[nums[fast]];
} while(slow != fast);
slow = nums[0];
while(slow != fast){
slow = nums[slow];
fast = nums[fast];
}
return slow;
}
滑动窗口
滑动窗口技术简化了找到满足某种条件的连续数据块的任务:
- 最长子数组…
- 包含某个要素的最短子字符串…
- 等等
你可以将其视为两个指针技术的另一种变体,其中根据某种条件分别更新指针。以下是这类问题的基本步骤,以伪代码的形式呈现:
创建两个指针l和r
创建变量以跟踪结果(res)
在满足条件A之前进行迭代:
基于条件B:
更新l、r或两者
更新res
返回res