代码随想录第二天 | 数组:双指针总结/vector.insert()(leetcode 977);双指针 滑动窗口(leetcode 209,904,76);模拟过程(leetcode 59,54)

1、续双指针

1.1 leetcode 977

第一次代码:(第一天做过)

class Solution {
public:
    vector<int> sortedSquares(vector<int>& nums) {
        vector<int> result;
        int start = 0;
        int end = nums.size() - 1;//两边待排中最大
        while(start <= end) {//有等号把最后一个值包含在内,先按倒序排列(push_back()插入在队尾)
            if(nums[start]*nums[start] <= nums[end]*nums[end]) {
                result.push_back(nums[end]*nums[end]);
                end--;
            }
            else {
                result.push_back(nums[start]*nums[start]);
                start++;
            }
        }
        reverse(result.begin(), result.end());
        return result;
    }
};

其实可以使用insert()(见1.3)解决从后往前插入的问题。

使用暴力排序,每个数平方之后,快排

class Solution {
public:
    vector<int> sortedSquares(vector<int>& A) {
        for (int i = 0; i < A.size(); i++) {
            A[i] *= A[i];
        }
        sort(A.begin(), A.end()); // 快速排序
        return A;
    }
};

这个时间复杂度是 O(n + nlogn)

1.2 双指针总结

双指针指向:
1、一快一慢(一个指向目标位置一个指向元素位置)

2、两头(如本题:数组其实是有序的, 只不过负数平方之后可能成为最大数了。那么数组平方的最大值就在数组的两端,不是最左边就是最右边,不可能是中间。此时可以考虑双指针法了,i指向起始位置,j指向终止位置。)
注意可以使用vector result(A.size(), 0);对result数组初始化这样就可以直接从后面加入了

3、滑动窗口(见 2.1-2.5 leetcode 209)

1.3 vector.insert()

参考博客:vector中insert()的用法详解 C++ vector用法详解

insert() 函数有以下三种用法:
1、在指定位置loc前插入值为val的元素,返回指向这个元素的迭代器
2、在指定位置loc前插入num个值为val的元素
3、在指定位置loc前插入区间[start, end)的所有元素

使用insert()方式插入元素:

insert()  //往vector任意位置插入一个元素,指定位置或者指定区间进行插入,
 //第一个参数是个迭代器,第二个参数是元素。返回值是指向新元素的迭代器

vector<int> vA;
vector<int>::iterator it;

//指定位置插入
//iterator insert(const_iterator _Where, const _Ty& _Val)
//第一个参数是个迭代器位置,第二个参数是元素
it = vA.insert(vA.begin(),2); //往begin()之前插入一个int元素2 (vA={2,1}) 此时*it=2

  
//指定位置插入
//void insert(const_iterator _Where, size_type _Count, const _Ty& _Val) 
//第一个参数是个迭代器位置,第二个参数是要插入的元素个数,第三个参数是元素值
it = vA.insert(vA.end(),2,3);//往end()之前插入2个int元素3 (vA={2,1,3,3}) 此时*it=3


//指定区间插入
//void insert(const_iterator _Where, _Iter _First, _Iter _Last) 
vector<int> vB(3,6);  //vector<类型>标识符(最大容量,初始所有值)
it = vA.insert(vA.end(),vB.begin(),vB.end()); //把vB中所有元素插入到vA的end()之前 (vA={2,1,3,3,6,6,6})
//此时*it=6,指向最后一个元素值为6的元素位置



//删除元素操作:
 
pop_back()  从vector末尾删除一个元素

erase()  从vector任意位置删除一个元素,指定位置或者指定区间进行删除,第一个参数都是个迭代器。返回值是指向删除后的下一个元素的迭代器

 clear()   清除vector中所有元素, size=0, 不会改变原有capacity值

示例:

//创建一个vector,置入字母表的前十个字符 
vector <char> Avector; 
for( int i=0; i < 10; i++ ) 
  Avector.push_back( i + 65 ); 
 
 
//插入四个C到vector中 
vector <char>::iterator theIterator = Avector.begin(); 
Avector.insert( theIterator, 4, 'C' ); 
 
 
//显示vector的内容 
for( theIterator = Avector.begin(); theIterator != Avector.end(); theIterator++ ) 
  cout < < *theIterator; 

这段代码将显示:CCCCABCDEFGHIJ

2、双指针 滑动窗口

2.1 leetcode 209(Time Limit Exceeded)

第一遍暴力法报错:Time Limit Exceeded

class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
    //思路是由于从某一元素开始的最大序列之和固定,因为元素均为正,从前往后找即可(再设一个队尾指针),超过target就停止,超不过记为-1
        vector<int> re(nums.size(), -1);//存储对应元素开始的符合要求的最大长度
        for(int start = 0; start < nums.size(); start++) {
            int sum = 0;
            for(int end = start; end < nums.size(); end++) {
                sum += nums[end];
                if(sum >= target) {
                    re[start] = end - start + 1;
                    break;
                }
            }
        }
        int min = re.size() + 1;
        for(int i = 0; i < re.size(); i++) {
            cout << re[i] << " ";
            if(re[i] < min && re[i] > 0) {
                min = re[i];
            }
        }
        if(min == re.size() + 1) {
            return 0;
        }
        else {
            return min;//可以简写为return result == re.size()+1? 0 : result;
        }
    }
};

滑动窗口

双指针数组操作中第三个重要的方法:滑动窗口。

所谓滑动窗口,就是不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果。

在暴力解法中,是一个for循环滑动窗口的起始位置,一个for循环为滑动窗口的终止位置,用两个for循环 完成了一个不断搜索区间的过程。

滑动窗口用一个for循环来完成这个操作。

2.2 滑动窗口使用前提

1、需要在遍历可能性时下一个答案的连续的一部分(不能有间隔)为上一个答案中的连续的一部分
2、随着结束位置的移动起始位置一定是停止或者随着向右移动的(不会左右晃)

2.3 滑动窗口思考过程

首先要思考 如果用一个for循环,那么应该表示 滑动窗口的起始位置,还是终止位置

如果只用一个for循环来表示 滑动窗口的起始位置,那么如果考虑遍历剩下的终止位置(第一次错误想法的来源)此时难免再次陷入 暴力解法的怪圈。

其实以开始位置为for唯一循环也可以,见后文2.5。与终止位置为循环索引不同的是,每次定下初始位置后移动终止位置时均为不符合要求的情况,直到符合条件再移动初始位置,出while循环为符合要求的情况,附加的,end也不再多走一步了。当然由于移动的是终止位置所以保证终止位置在出while循环时不越界。

只用一个for循环,那么这个循环的索引,可以是表示 滑动窗口的终止位置。
以题目中的示例来举例,s=7, 数组是 2,3,1,2,4,3,来看一下查找的过程:
终止位置为循环索引动图 代码随想录

滑动窗口,主要确定如下三点:

1、窗口内是什么?
2、如何移动窗口的起始位置
3、如何移动窗口的结束位置
窗口就是 满足其和 ≥ s 的长度最小的 连续 子数组。

2.4 leetcode209:以结束位置为循环索引(for唯一循环)

窗口的起始位置如何移动:如果当前窗口的值大于s了,窗口就要向前移动了(也就是该缩小了)。(因为随着结束位置的移动起始位置一定是停止或者随着向右移动的(不会左右晃))可以通过找个变量记录结果(result),这样不用把结果填入数组一起比大小

窗口的结束位置如何移动:窗口的结束位置就是遍历数组的指针,也就是for循环里的索引。

class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int result = __INT32_MAX__;
        int start = 0;
        int sum = 0;
        for(int end = 0; end < nums.size(); end++) {
            int len = end - start + 1;
            sum += nums[end];//结束指针往后移别忘了加上加入的元素
            while(sum >= target) {

                //while是关键,不断调整start直至最短,如果start不再满足了,多走了一位,因为随之end也会走,所以多走的一位并不会需要start往回走
                result = len >= result ? result : len;
                sum -= nums[start];
                start++;
                len--;
            }
        }
        return result == __INT32_MAX__ ? 0 : result;
    }
};

2.5 leetcode 209:以开始位置为循环索引(for唯一循环)

以开始位置为for循环索引:(麻烦,滑动窗口还是以终止位置为索引

窗口的开始位置如何移动:如果当前窗口的值小于s了,窗口就要向前移动了(也就是该放大了)。(因为随着开始位置的移动结束位置一定是停止或者随着向右移动的(不会左右晃))

class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int result = __INT32_MAX__;
        int end = 0;
        int sum = nums[0];//开始包含第一个,因为start=0不用再减一个所以乘二(start = end = 0)
        int len = 2;//因为end多走了一步,所以len不能在过程中用end-start+1
        //start=0减一所以初始值为2
        for(int start = 0; start < nums.size(); start++) {
            len--;
            if(start > 0) {
                sum -= nums[start-1];//减前一个位置的nums
            }
            while(sum < target && end < nums.size() - 1) {
                end++;
                len++;
                sum += nums[end];//考虑end = nums.size() - 1的情况应该end先加一再加sum
            }//出去除了end超范围其余符合条件
            if(sum >= target) {
                result = result > len ? len : result;
            }
        }
        return result == __INT32_MAX__ ? 0 : result;
    }
};

2.6 leetcode 904:注释为滑动窗口流程

采用上文leetcode 209中的以结束位置为循环索引。因为随着结束位置的右移,开始位置停止或右移,同时对于下一段可能的区间而言是由上一次的区间的连续的子区间组成的。

class Solution {
public:
    int totalFruit(vector<int>& fruits) {
        int cater_num = 0;//记录水果的种类数
        vector<int> cater(fruits.size(), 0);//记录水果的种类以及每种种类对应的数量
        //以结束位置为for循环索引
        int res = 0;//最终结果
        int num = 0;
        int start = 0;
        for(int end = 0; end < fruits.size(); end++) {
            if(cater[fruits[end]] == 0) {//end变更新变量
                cater_num++;
            }
            num++;
            cater[fruits[end]]++;
            while(cater_num > 2) {//start变更新变量,注意限制条件出while循环为符合条件的情况(与leetcode 209以开始位置为循环索引的情况一致)
                cater[fruits[start]]--;
                if(cater[fruits[start]] == 0) {
                    cater_num--;
                }
                start++;
                num--;
            }
            res = num > res ? num : res;
        }
        return res;
    }
};

可以使用哈希表(与上面代码的vector cater一致)存储这个窗口内的数以及出现的次数

我们每次将 right移动一个位置,并将 fruits[right]加入哈希表。如果此时哈希表不满足要求(即哈希表中出现超过两个键值对),那么我们需要不断移动 left,并将 fruits[left]从哈希表中移除,直到哈希表满足要求为止。
需要注意的是,将fruits[left]从哈希表中移除后,如果fruits[left]在哈希表中的出现次数减少为0,需要将对应的键值对从哈希表中移除。

class Solution {
public:
    int totalFruit(vector<int>& fruits) {
        int n = fruits.size();
        unordered_map<int, int> cnt;

        int left = 0, ans = 0;
        for (int right = 0; right < n; ++right) {
            ++cnt[fruits[right]];
            while (cnt.size() > 2) {
                auto it = cnt.find(fruits[left]);
                --it->second;
                if (it->second == 0) {
                    cnt.erase(it);
                }
                ++left;
            }
            ans = max(ans, right - left + 1);
        }
        return ans;
    }
};

2.7 leetcode 76(没思路,其子串就是元素的频数大于等于该子串,头尾指针交替为循环索引)

短的看了更长的就不用看了,右边界移动的时候左边界固定,左边界移动的时候右边界固定
先向右移动让字符串区间包含所有字符,再移动左边界直至不满足是子串为止;再向右移动,向左移动…

题解链接

3、模拟过程(找规律)

3.1 leetcode 59

第一遍代码(有点混乱思路):

class Solution {
public:
    vector<vector<int>> generateMatrix(int n) {
    //找规律填充,每一行填充都要与loop/loop_num有关(可以改变行数),每一行左闭右开
        vector<vector<int>> re(n, vector<int>(n, 0));//注意二维数组初始化方法
        int loop_num = n;//每一圈每一条边长度
        int loop = 1;//第几圈,可以与元素放置的位置产生联系
        int num = 1;//待放入的元素
        while(loop_num>1) {//1/2=0,如果是loop_num=1,特别是奇数的时候最后一圈为1不能再放入循环,因为num++会覆盖掉之前的
            loop_num--;//因为左闭右开,实际走的时候需要-1
            for(int i = loop - 1; i < loop_num + loop - 1; i++) {
                re[loop - 1][i] = num++;
            }
            for(int j = loop - 1; j < loop_num + loop - 1; j++) {
                re[j][loop_num + loop - 1] = num++;//注意利用正方形边长相等性质简化坐标的书写
            }
            for(int i = loop_num + loop - 1; i > loop - 1; i--) {
                re[loop_num + loop - 1][i] = num++;
            }
            for(int j = loop_num + loop - 1; j > loop-1; j--) {
                re[j][loop - 1] = num++;
            }
            loop_num = (loop_num+1)-2;//边长为差为2的等差数列
            cout << loop_num;
            loop++;
        }
        if(n%2 == 1) {
            re[loop-1][loop-1] = num;
        }
        return re;
    }
};

模拟顺时针画矩阵的过程:
1、填充上行从左到右
2、填充右列从上到下
3、填充下行从右到左
4、填充左列从下到上
由外向内一圈一圈这么画下去,画一条边都要坚持一致的左闭右开,或者左开右闭的原则,这样这一圈才能按照统一的规则画下来。

第一次代码中矩阵的赋值没有条理,可以与双指针一样,研究填充每一条边开头和结尾。每一圈结尾-1(这里使用边的长度,因为正方形边长相等,这样每次统一-1即可),开始(分成行开始和列开始startx/starty(其实可以共用,但是代码随想录这样更清晰))+1,无论是行还是列,这样再确认一个圈数每圈初始行列位置就行了。中间单独的一个值还是单独给。

class Solution {
public:
    vector<vector<int>> generateMatrix(int n) {
        vector<vector<int>> res(n, vector<int>(n, 0)); // 使用vector定义一个二维数组
        int startx = 0, starty = 0; // 定义每循环一个圈的起始位置
        int loop = n / 2; // 每个圈循环几次,例如n为奇数3,那么loop = 1 只是循环一圈,矩阵中间的值需要单独处理
        int mid = n / 2; // 矩阵中间的位置,例如:n为3, 中间的位置就是(1,1),n为5,中间位置为(2, 2)
        int count = 1; // 用来给矩阵中每一个空格赋值
        int offset = 1; // 需要控制每一条边遍历的长度,每次循环右边界收缩一位
        int i,j;
        while (loop --) {
            i = startx;
            j = starty;

            // 下面开始的四个for就是模拟转了一圈
            // 模拟填充上行从左到右(左闭右开)
            for (j = starty; j < n - offset; j++) {
                res[startx][j] = count++;
            }
            // 模拟填充右列从上到下(左闭右开)
            for (i = startx; i < n - offset; i++) {
                res[i][j] = count++;
            }
            // 模拟填充下行从右到左(左闭右开)
            for (; j > starty; j--) {
                res[i][j] = count++;
            }
            // 模拟填充左列从下到上(左闭右开)
            for (; i > startx; i--) {
                res[i][j] = count++;
            }

            // 第二圈开始的时候,起始位置要各自加1, 例如:第一圈起始位置是(0, 0),第二圈起始位置是(1, 1)
            startx++;
            starty++;

            // offset 控制每一圈里每一条边遍历的长度
            offset += 1;
        }

        // 如果n为奇数的话,需要单独给矩阵最中间的位置赋值
        if (n % 2) {
            res[mid][mid] = count;
        }
        return res;
    }
};

3.2 leetcode 54(提交未通过)

对于二维vector变量vv,vv.size()的值vector的行数,vv[i].size()的值是vector的列数
第一次代码提交失败,用3.1的套路,[[7],[9],[6]]不符合要求,最后一条边会填2个最终结果[7,9,6,9]而非[7,9,6]

class Solution {
public:
    vector<int> spiralOrder(vector<vector<int>>& matrix) {
        int m = matrix.size();
        int n = matrix[0].size();
        vector<int> re;
        int loop = m/2;//圈数,当然去掉了中间的一行
        int ini = 0;//行列每圈开始的位置
        int cout1 = 1;//每圈队尾剪掉的个数(每次往里-1,队尾就是n-cout)
        //左闭右开
        while(loop--) {
            for(int j = ini; j < n - cout1; j++) {
                re.push_back(matrix[ini][j]);
            }
            for(int i = ini; i < m - cout1; i++) {
                re.push_back(matrix[i][n-cout1]);
            }
            for(int j = n - cout1; j > ini; j--) {
                re.push_back(matrix[m-cout1][j]);
            }
            for(int i = m - cout1; i > ini; i--) {//没办法这么搞了,[[7],[9],[6]]不符合要求
                re.push_back(matrix[i][ini]);
            }
            ini++;
            cout1++;
        }
        if(m % 2 == 1) {//需要补中间一行
            for(int j = ini; j < n - cout1 - ini + 1 + ini; j++) {//这一行元素个数n - cout1 - ini + 1(头尾相减+1)
                re.push_back(matrix[ini][j]);
            }
        }
        return re;
    }
};

其实需要改用左闭右闭的思路(先把某行全部遍历完,再把某列(去掉第一个元素了)遍历完),每一条边循环完后要重新设定边界,若上边界大于下边界或者左边界大于右边界,则遍历遍历完成
题解链接
解题思路:
这里的方法不需要记录已经走过的路径,所以执行用时和内存消耗都相对较小

1、首先设定上下左右边界
2、其次向右移动到最右,此时第一行因为已经使用过了,可以将其从图中删去,体现在代码中就是重新定义上边界
3、判断若重新定义后,上下边界交错,表明螺旋矩阵遍历结束,跳出循环,返回答案
4、若上下边界不交错,则遍历还未结束,接着向下向左向上移动,操作过程与第一,二步同理
5、不断循环以上步骤,直到某两条边界交错,跳出循环,返回答案

对第一次代码进行修改,通过

class Solution {
public:
    vector<int> spiralOrder(vector<vector<int>>& matrix) {
        //先定义上下左右边界,遍历完(左闭右闭)就把那一行/一列去了,直到没有遍历的元素了就结束
        int left = 0;
        int right = matrix[0].size() - 1;
        int up = 0;
        int down = matrix.size() - 1;
        vector<int> re;
        while(true) {
            for(int j = left; j <= right; j++) {
                re.push_back(matrix[up][j]);
            }
            up++;//一行完全遍历完
            if(up > down) break;
            for(int i = up; i <= down; i++) {
                re.push_back(matrix[i][right]);
            }
            right--;
            if(left > right) break;
            for(int j = right; j >= left; j--) {
                re.push_back(matrix[down][j]);
            }
            down--;
            if(up > down) break;
            for(int i = down; i >= up; i--) {
                re.push_back(matrix[i][left]);
            }
            left++;
            if(left > right) break;
        }
        return re;
    }
};

你可能感兴趣的:(leetcode,c++,leetcode,算法,c++,数据结构)