算法学习笔记Day3——双指针技巧之数组

一、快慢指针

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官方解答(用状态转移方程分析)

总结:

  • 17
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,我知道了,下面是我的回答: 聚类算法是一种无监督学习算法,它可以将数据集中的数据按照某种规则进行分组,使得同一组内的数据相似度较高,不同组之间的数据相似度较低。聚类算法可以帮助我们对数据进行分类、分析和理解,同时也可以为其他机器学习任务提供数据预处理和特征提取的支持。 聚类算法的基本流程包括:确定聚类算法的目标函数、选择相似度度量方法、选择聚类算法、确定聚类的数量、进行聚类操作以及评估聚类效果。常见的聚类算法包括K-Means算法、层次聚类算法、密度聚类算法等。 K-Means算法是一种基于距离的聚类算法,它的基本思想是将数据集中的数据划分为K个簇,使得同一簇内的数据相似度较高,不同簇之间的数据相似度较低。K-Means算法的优点是计算复杂度较低,容易实现,但是需要预先指定簇的数量和初始聚类中心。 层次聚类算法是一种基于相似度的聚类算法,它的基本思想是不断合并数据集中相似度最高的数据,直到所有数据都被合并为一个簇或达到预先设定的簇的数量。层次聚类算法的优点是不需要预先指定簇的数量和初始聚类中心,但是计算复杂度较高。 密度聚类算法是一种基于密度的聚类算法,它的基本思想是将数据集中的数据划分为若干个密度相连的簇,不同簇之间的密度差距较大。密度聚类算法的优点是可以发现任意形状的簇,但是对于不同密度的簇分割效果不佳。 以上是聚类算法的基础知识,希望能对您有所帮助。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值