从2021年8月中旬开始刷题。
本文将记录我在2021年8月中所刷过的题目,题目量不多,但是每一道保证理解透彻。
力扣26题:删除有序数组中的重复项
题目链接:点击这里

首先看清楚题目,这道题目说了是一个有序的数组,那么重复的元素必定是连续的,就连续这一特点我们可以摒弃平常用的字典计数的方法或者使用set的方法,而采用更加简便的方法。
- 思路1:双指针(时间复杂度O(n),空间复杂度O(1))
假设两个指针p和q,p在左边,q在右边,如果nums[p]==nums[q],即两指针指向的元素值相等了,那么q指针右移一个单位,再次判断俩指针指向的值是否相等,如果还是相等,q指针继续移动;如果不相等,p指针移动一个单位,且nums[p]=nums[q]。
那么何时跳出循环呢?如果q到达了nums最后一个元素,那么说明了所有的元素都遍历了一遍,那么此时p所指向的就是不重复的元素的最后一个。
将上述过程写成伪代码:
1 2 3 4 5 6 7 8 9 10 11
| p=0, q=1, r=0 while q < nums.size(): if nums[p] == nums[q]: q++ else: p++ nums[p] = nums[q] return p
|
可以自行举个例子来将上述过程推演一遍:
nums = 0, 0, 1, 1, 1, 2, 2, 3, 3, 4。
完整代码:
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
| class Solution { public: int removeDuplicates(vector<int>& nums) { if (nums.empty()) return 0; int p = 0, q = 1; while (q < nums.size()) { if (nums[p] == nums[q]) { q++; } else { p++; nums[p] = nums[q]; } } return p + 1; } };
class Solution { public: int removeDuplicates(vector<int>& nums) { if (nums.empty()) return 0; int p = 0, q = 1; for (int q = 1; q < nums.size(); q++) { if (nums[p] != nums[q]) nums[++p] = nums[q]; } return p + 1; } };
|
- 思路2:使用STL(时间复杂度O(n),空间复杂度O(1))
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| class Solution { public: int removeDuplicates(vector<int>& nums) { return distance(nums.begin(), removeDuplicates(nums.begin(), nums.end(), nums.begin())); }
template<typename InIt, typename OutIt> OutIt removeDuplicates(InIt first, InIt last, OutIt output) { while (first != last) { *output++ = *first; first = upper_bound(first, last, *first); }
return output; } };
|
- 思路3:通用解法,将原问题的「最多保留 1 位」修改为「最多保留 k 位」。
假设我们使用的nums=[1,1,1,2,2,3]
我们仍然使用双指针法。这样的思路:
既然需要保留K个相同的数字,我们就用一个变量r来存储当前数字的相同的个数。一开始p指针指向index=0,q指针指向index=1,首先判断nums[p]==nums[q]是否成立,如果是的,我们需要判断计数器r的值是否超出了K的限定范围,如果超出了,那么只用q继续往后走,如果没有超出,我们让与p相距r个位置的数值=nums[q],即:nums[p+r]=nums[q];如果不成立,即此时nums[p]!=nums[q],假设刚刚在遍历数字1,那么此时意味着此时数字1已经遍历完了,所以nums[q]=2,这个时候我们要将p指针指向这个新的数字的开端,即p=q。
这样写有什么问题?
如果p和q之间相差很多呢,比如1, 1, 1, 1, 1, 1,2,此时p指向第三个1,而q指向2,能将p直接指向q的2吗?显然不能!但是想起来我们用了一个计数器r,这就容易想到我们可以使用计数器来实现p指针的新指向:p+=r。然后是一个新的数字了,所以计数器r需要更新为1,新的数字还要移动过来给p:nums[p]=nums[q],这就是一个完整的循环需要做的事情。循环结束的标志?和K为1的时候一样,当q超出nums长度即停止。
综合上述流程的伪代码为:
1 2 3 4 5 6 7 8 9
| k=2, p = 0, q = 1, r = 1 while q < nums.size(): if nums[p] == nums[q]: nums[p + r] = nums[q] r++, q++ else: p += r, r = 1 nums[p] = nums[q] q++
|
使用C++来实现:
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 removeDuplicates(vector<int>& nums) { if (nums.empty()) return 0;
int k = 2, r = 1, p = 0, q = 1;
for (; q < nums.size(); q++) { if (nums[p] == nums[q]) { if (r < k) { nums[p + r] = nums[q]; r++; } } else { p += r; r = 1; nums[p] = nums[q]; } } return p + 1; } };
|
问题来了,将这段C++代码拿去运行发现只能通过部分案例,什么问题?仔细分析代码之后发现,如果在退出循环之前,进入的是else分支,那么p将得到更新:p+=r,如果经过的是if分支,p无法得到更新,直接退出循环!所以这里逻辑需要更改,可以直接在if分支中让每一次p+r吗?不行,因为r是不断增加的。我的解决方式是,每一次都让p更新一下,而不是到了最后一次性加上r,即在每一次遇到与nums[p]相同数字的时候,如果r<k就马上更新p:
1 2 3 4 5 6
| if (nums[p] == nums[q]) { if (r < k) { r++; nums[++p] = nums[q]; } }
|
这样就可以将else分支中的p+=r去掉:
1 2 3 4 5
| else {
r = 1; nums[++p] = nums[q]; }
|
完整的代码为:
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 removeDuplicates(vector<int>& nums) { if (nums.empty()) return 0;
int k = 2, r = 1, p = 0, q = 1;
for (; q < nums.size(); q++) { if (nums[p] == nums[q]) { if (r < k) { r++; nums[++p] = nums[q]; } } else {
r = 1; nums[++p] = nums[q]; } } return p + 1; } };
|
这个搞定之后,可以顺便去把这道题给AC掉:
80. 删除有序数组中的重复项 II
力扣33题.搜索旋转排序数组
题目链接:点击这里

这道题如果不去考虑时间复杂度的限制将是非常容易的遍历问题,但是本题的进阶方案是:设计一个时间复杂度为O(logn)的解决方案。
想到时间复杂度为O(logn)很容易思考到二分查找法,但是本题只是两部分有序的,所以直接用原本的二分查找行不通,需要进行修改,思路如下:
假设输入的nums=4,5,6,0,1,2,3。
初始left=0,right=nums.size(),每一次循环中,取mid=(left+right)/2,将序列最左端的值和target比较,有两种情况需要重点考虑:1. num[left]>target,2. num[left]<target,等于的话直接结束程序。
如果 num[left]>target,nums[mid]有3种情况,一种是比target大,一种是比它小,最后就是相等。
- 如果nums[mid]>target,还有两种情况,一种是nums[mid]在nums的最小值的左边,那么此时有:nums[mid]<target<nums[right],所以更新left=mid;一种是在nums的最小值的右边,这种情况下,nums[mid]和nums[right]均大于target,所以需要更新left=left,right=mid。
- 如果nums[mid]<target,只有一种情况,更新为:left=mid,right=right。
- 如果nums[mid]==target,找到目标值。
如果 num[left]<target,mid同样有3中情况,即nums[mid]>target,nums[mid]<target,nums[mid]==target。
- 如果nums[mid]>target,只有一种情况,即:nums[left]<target<nums[mid],更新为:left=left,right=mid
- 如果nums[mid]<target,有两种情况,一种是nums[mid]在nums最小值的左边,那么此时有:nums[left]<nums[mid]<target,这种情况下,需要更新:left=mid,right=right;另一种是nums[mid]在nums最小值的右边,此时有:nums[left]<target<nums[mid]<nums[right],所以更新left=left,right=mid。
- 如果nums[mid]==target,找到目标值。
将上述思路使用代码实现如下:
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
| class Solution { public: int search(vector<int>& nums, int target) { int left = 0, right = nums.size() - 1; int mid; while (left <= right) { mid = (left + right) / 2; if (nums[left] > target) { if (nums[mid] > target) { if (nums[mid] >= nums[left]) left = mid; else right = mid; } else if (nums[mid] < target) left = mid; else return mid; } else if (nums[left] < target) { if (nums[mid] > target) right = mid; else if(nums[mid] < target) { if (nums[mid] >= nums[left]) left = mid; else right = mid; } else return mid; } else return left; } return -1; } };
|
这段代码并不能通过所有的实例,准确的说,很多实例都无法通过,一旦进入类似这样的情况:[2,3],left=0,right=1,target=4,这种情况下,取mid=(0+1)/2,取整,得到mid=0,就会一直循环下去。
其实在二分法中,仅仅使用左右区间索引值之和的一半很容易出现这种循环,需要走出循环,一定需要通过加减来变化索引的值
比如我知道了nums[mid]<target,就取left=mid+1,而不是left=mid。通过加减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 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| class Solution { public: int search(vector<int>& nums, int target) { int left = 0, right = nums.size() - 1; int mid; while (left <= right) { mid = (left + right) / 2; if (nums[left] > target) { if (nums[mid] > target) { if (nums[mid] >= nums[left]) left = mid + 1; else right = mid - 1; } else if (nums[mid] < target) left = mid + 1; else return mid; } else if (nums[left] < target) { if (nums[mid] > target) right = mid - 1; else if(nums[mid] < target) { if (nums[mid] >= nums[left]) left = mid + 1; else right = mid - 1; } else return mid; } else return left; } return -1; } };
|