一、快慢指针
1. 原地修改数组类型
介绍:
原地修改数组,分为删除数组中某些元素,改变数组为特定顺序等等。
方法是在开头放置两个指针,然后一个块一个慢,快的去找元素,把符合条件的元素搬运给慢指针,慢慢的覆盖原数组实现原地修改。(被覆盖的地方时快指针检索过的所以可以放心覆盖)
例题1:删除有序数组中的重复项
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
if(nums.size() == 0){
return 0;
}
int fast = 1, slow = 1;
while(fast < nums.size()){
if(nums[fast] != nums[slow-1]){
nums[slow++] = nums[fast];
}
fast++;
}
return slow;
}
};
例题2: 移除元素
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int fast = 0, slow = 0;
while(fast < nums.size()){
if(nums[fast] != val){
nums[slow++] = nums[fast];
}
fast++;
}
return slow;
}
};
例题3: 移动零
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int cnt = 0;
int slow = 0, fast = 0;
while(fast != nums.size()){
if(nums[fast] != 0){
nums[slow++] = nums[fast];
}
else{
cnt++;
}
fast++;
}
for(int i = 0; i<cnt; i++){
nums[slow+i] = 0;
}
}
};
分析:记录遇到0的次数,最后把0一次性输出到数组末尾即可。
2. 滑动窗口算法
介绍:主要用来解决子数组/子串问题,比如最大/最小子数组,最长/最短的满足条件的字串;思路就是维护一个窗口,然后不断滑动来更新结果。因为left和right指针都不会回退,所以每个元素都只会进入窗口一次,出窗口一次,所以复杂度为O(N)。
例题4:最小覆盖子串
分析:
代码:
曾经写的C代码:滑动窗口+模式选择(通过判断当前窗口是否包含所有字符来决定左指针还是右指针滑动),预计C++能节省很多代码量
#define min(a, b) a<b?a:b
#define SIZE 256
int isOverlapped(int* array, int length){
int ret = 1;
for(int i = 0; i<length; i++){
if(array[i] > 0){
ret = 0;
break;
}
}
return ret;
}
int isInt(char* s, char c){
int ret = 0;
while(*s!=0){
if(c == *s){
ret = 1;
break;
}
s++;
}
return ret;
}
char* minWindow(char* s, char* t) {
char* N = malloc(sizeof(int)*1);
*N = 0;
if(strlen(s) < strlen(t)){
return N;
}
//每记录下一个min,都要跟着记录min_start
int min_start = 0, min = INT_MAX;
//双指针
int start = 0, end = -1;
//模式选择
int mode = 1;
int* t_array = malloc(sizeof(int)*SIZE);
for(int i =0; i< SIZE; i++){
t_array[i] = 0;
}
for(int i =0; i< strlen(t); i++){
t_array[t[i]]++;
}
//业务功能
while(1){
switch(mode){
case 1:
end++;
if(s[end] == 0){
goto exit;
}
if(isInt(t, s[end])){
t_array[s[end]]--;
}
if(isOverlapped(t_array, SIZE)){
if( (end - start +1) < min){
min = end - start + 1;
min_start = start;
}
mode = 0;
}
break;
case 0:
if(isInt(t, s[start++]) ){
t_array[s[start-1]]++;
if(!isOverlapped(t_array, SIZE)){
mode = 1;
}
else{
if( (end - start +1) < min){
min = end - start + 1;
min_start = start;
}
}
}
else{
if( (end - start +1) < min){
min = end - start + 1;
min_start = start;
}
}
break;
}
}
exit:
if(min == INT_MAX){
return N;
}
s[min_start+min] = 0;
return &s[min_start];
}
这次写的C++代码:
class Solution {
public:
unordered_map<char, int> tmap, smap;
int overlapped(){
for(const auto c : tmap){
if(smap[c.first] < c.second){
return 0;
}
}
return 1;
}
string minWindow(string s, string t) {
if(s.size() < t.size()){
return "";
}
for(char c : t){
tmap[c]++;
}
int left = 0, right = -1;
int min = INT_MAX, minleft= 0;
while(right < (int)s.size()){
if(!overlapped()){
right++;
smap[s[right]]++;
}
else{
if(right + 1 - left < min){
min = right + 1 - left;
minleft = left;
}
smap[s[left]]--;
left++;
}
}
min = min==INT_MAX? 0: min;
return s.substr(minleft, min);
}
};
总结:
i)s.size()返回的是size_t类型,是无符号数,和-1直接比较会把-1转化为INT_MAX,所以要先进行强制类型转换!
ii)map的键值对<char, int>的char/int获取方式是first和second
iii)这种题,s < t的情况一定要先判断,保证s >= t。
iv)min为了更新会设置一个很大的数,但是如果没有被更新过的话,就需要手动把它置为0,所以return前要加一句min的赋值。
例题5:无重复字符的最长子串
分析:
巧妙思路:使用左右指针,right每次遇到一个字母,在日记本里面查找left应该移动到哪里能够让窗口满足每个字母只出现一次,然后更新这个日记本(对于新遇到这个字母下一次left需要移动到当前右指针+1的位置)
普通思路:维护一个unordered_map,每次遇到新字母都去查表看看是否出现过。
代码:
巧妙思路
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int ans = 0;
vector<int> diary(128, 0);
int i = 0;
for(int j = 0; j< s.size(); j++){
i = max(diary[s[j]], i);
diary[s[j]] = j+1;
ans = max(ans, j - i +1);
}
return ans;
}
};
普通思路:
总结:
i)巧妙思路很简洁但是很难想,普通思路比较套路化但是比较麻烦。
例题6:找到字符串中所有字母异位词
分析:同例7
代码:
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
vector<int> cnt1(26), cnt2(26);
vector<int> ans;
int m = p.length(), n = s.length();
if(n < m){
return ans;
}
for(int i = 0; i<m; i++){
cnt1[s[i] - 'a']++;
cnt2[p[i] - 'a']++;
}
//pos=0单独判断
if(cnt1 == cnt2){
ans.push_back(0);
}
int pos = 0;
while(pos < s.length() -m){
cnt1[s[pos+m] - 'a']++;
cnt1[s[pos] - 'a']--;
pos++;
if(cnt1 == cnt2){
ans.push_back(pos);
}
}
return ans;
}
};
总结:
i)字符串的字母出现次数用vector<int> cnt(26)来存,用unordered_map存会有点问题。(不知道为什么)
ii)不改省略的别省,比如这里的pos=0的情况判断,我一开始想用一个do-while来合并写,但是怎么写都不对,最后只能把它们分开,虽然看起来比较笨拙,但是不容易出错。
例题7:字符串的排列
分析:这道题只需要关心长度固定的窗口,因为长度不等一定不满足条件,然后因为只要有一个排列相同即可,所以用vector来存储每个字母出现的次数,然后比较这两个vector是否相等即可。
代码:
class Solution {
public:
bool checkInclusion(string s1, string s2) {
int n = s1.length(), m = s2.length();
if(n > m){
return false;
}
vector<int> cnt1(26), cnt2(26);
for(int i = 0; i<n; i++){
cnt1[s1[i] - 'a']++;
cnt2[s2[i] - 'a']++;
}
if(cnt1 == cnt2){
return true;
}
for(int i = n; i<m; i++){
cnt2[s2[i] - 'a']++;
cnt2[s2[i-n] - 'a']--;
if(cnt1 == cnt2){
return true;
}
}
return false;
}
};
总结:
i) c++里面的vector<int> cnt,指定大小是用(26)而不是[26]
ii) vector比较于数组的好处是,可以直接用比较来判断两个数组是否相等,如:cnt1 == cnt2,而数组需要用一个循环来判断,省了很多事情。
iii) 每道题整体框架思路都差不多,但是细节处理需要认真思考,比如这道题,窗口大小必须一致才有可能满足条件,所以就不需要分别动left和right,整个窗口一起滑动即可。
二、左右指针
1. 二分查找
介绍:二分查找真正的坑根本就不是那个细节问题,而是在于到底要给 mid
加一还是减一,while 里到底用 <=
还是 <。
二分查找分三种情况:找一个数、找左边界、找右边界。
需要注意的一点是,right + left 可能会溢出(int)的边界,所以当下标比较大的时候可以用left+(right - left)/2。
难点:
1. while 里面什么时候用 left < right 什么时候用 left <= right ?
答:初始区间为开区间时,用left < right,初始区间为闭区间时,用left <= right。
因为区间一直在收缩,缩到最后,左右边界值相等,这时候区间的开闭就决定了能不能继续判断。
比如[2, 2),这种时候就要退出,即 left < right, [2, 2]即 left <= right。
2. 什么时候用mid = right-1,什么时候用mid = right ?
答:当搜索区间两端都闭的时候用 right - 1, 因为mid已经被搜索过了,所以下一步搜索[left, mid - 1],如果时开区间,就要搜索 (left, right)。
3. 寻找左右边界的技巧
答:当nums[mid] == target的时候,这时候mid可能是左右边界(不知道是不是),让start/end = mid,然后再向左边/右边寻找,方法是:right = mid - 1(更新right,探索左区间)/ left = mid + 1(更新left,探索右区间),while里面的条件时 left <= right。
例题9:二分查找模板
int binarySearch(vector<int>& nums, int target) {
// 一左一右两个指针相向而行
// 二分查找的前提是有序的
int left = 0, right = nums.size()-1;
while(left <= right){
int mid = (right + left)/2;
if(nums[mid] == target){
return mid;
}
else if(nums[mid) < target){
left = mid + 1;
}
else{
right = mid - 1;
}
}
//没有找到
return -1;
}
例题10:二分查找(找一个数)
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[mid] == target){
return mid;
}
else if(nums[mid] > target){
right = mid-1;
}
else{
left = mid+1;
}
}
return -1;
}
};
总结:
i)二分查找用来比较的可以是两边的平均数(类似例题12),也可以是两边中间的数(这道题)。
例题11:在排序数组中查找元素的第一个和最后一个位置(找左右边界)
分析:分两次找,一次找出现的第一个位置,一次找最后一个位置,两次的区别在于当nums[mid] == target的时候,是让start/end等于mid,以及更新right/left,理论支撑是当相等时,这个位置大概率实在一连串相同元素的中间位置,这时候应该往左边找(找第一个位置)还是右边找(找最后一个位置)。
代码:
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
int left = 0, right = nums.size() -1;
int start = -1, end = -1;
int mid;
vector<int> ans;
//找开始
while(left <= right){
mid = (left + right) /2;
if(nums[mid] == target){
start = mid;
right = mid-1;
}
else if(nums[mid] > target){
right = mid -1;
}
else{
left = mid + 1;
}
}
left = 0, right = nums.size() -1;
//找end
while(left <= right){
mid = (left + right) /2;
if(nums[mid] == target){
end = mid;
left = mid+1;
}
else if(nums[mid] > target){
right = mid -1;
}
else{
left = mid+ 1;
}
}
ans.push_back(start);
ans.push_back(end);
return ans;
}
};
总结:
2. 两数之和
例题12:两数之和 II - 输入有序数组
分析:和二分查找的模板类似,左右两头一对双指针,区别在于,二分是跳跃的,这道题是滑动的。
代码:
class Solution {
public:
vector<int> twoSum(vector<int>& numbers, int target) {
int left = 0, right = numbers.size() - 1;
int presentSum;
vector<int> ans;
while(left < right){
presentSum = numbers[left] + numbers[right];
if(target == presentSum){
ans.push_back(left + 1);
ans.push_back(right + 1);
return ans;
}
else if(target > presentSum){
left++;
}
else{
right--;
}
}
return ans;
}
};
总结:
i)vector<int> ans(2),会在vector里面添加两个为0的元素,如果再push_back的话,就是在两个0后面添加元素,是一个值得注意的小细节。
拓展:一个方法团灭 nSum 问题
链接:
3. 反转数组
例题13:反转字符串
分析:很简单的双指针交换
代码:
class Solution {
public:
void reverseString(vector<char>& s) {
auto left = s.begin(), right = s.end() -1;
char tmp;
while(left < right){
tmp = *left;
*left = *right;
*right = tmp;
left++; right--;
}
}
};
总结:
i)注意s.end()得到的是最后一个元素后面一个位置的迭代器,应该-1得到最后一个元素。
4. 回文串判断
分析:难点在于,回文串长度可能是奇数或者偶数,所以需要分类讨论,回文串的关键在于他的中心,奇数长度有一个中心,偶数有两个。
例题14:最长回文子串
代码:
曾经写的C代码:纯暴力做法,从最长的长度开始往下递减,看看有没有这个长度的回文串
int isbacksub(char *s, int length);
char * longestPalindrome(char * s){
int length = 0;
while(s[length] != '\0'){
length++;
}
int sublength = length;
for(int i = sublength; i>0; i--){
for(int j = 0; j+i <= length; j++){
if(isbacksub(&s[j], i)){
s[j+i] = '\0';
return &s[j];
}
}
}
return s;
}
int isbacksub(char *s, int length){
int is = 1;
for(int i = 0; i<length; i++){
if(s[i] != s[length-1-i]){
is = 0;
break;
}
}
return is;
}
这次写的C++代码:(中心拓展)
class Solution {
public:
//返回字符串s中以c1和c2作为中心的回文串
string backtext(string s, int c1, int c2){
int originL = c2 - c1 + 1;
while(c1 >= 0 && c2 <= s.size() && s[c1] == s[c2]){
originL += 2;
c2++; c1--;
}
//出来的时候最长的回文串是从c1+1到c2-1的(包括)
return s.substr(c1+1, originL-2);
}
string longestPalindrome(string s) {
string ans;
string s1, s2, smax;
for(int i = 0; i< s.size(); i++){
s1 = backtext(s, i, i);
s2= backtext(s, i, i+1);
//算出来后比较一下更新结果
smax = s1.size() > s2.size() ?s1 :s2;
ans = smax.size() > ans.size() ?smax: ans;
}
return ans;
}
};
Leecode官方解答(用状态转移方程分析)
总结:无