C++刷题 -- 数组

C++刷题 – 数组


一、二分查找

1.原理

条件:数组为有序数组,数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下标可能不是唯一的;

  • 第一种写法:
    在这里插入图片描述
    在这里插入图片描述
class Solution {
public:
    int search(vector<int>& nums, int target) {
        int left = 0;
        int right = nums.size() - 1; // 定义target在左闭右闭的区间里,[left, right]
        while (left <= right) { // 当left==right,区间[left, right]依然有效,所以用 <=
            int middle = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2
            if (nums[middle] > target) {
                right = middle - 1; // target 在左区间,所以[left, middle - 1]
            } else if (nums[middle] < target) {
                left = middle + 1; // target 在右区间,所以[middle + 1, right]
            } else { // nums[middle] == target
                return middle; // 数组中找到目标值,直接返回下标
            }
        }
        // 未找到目标值
        return -1;
    }
};
  • 第二种写法:
    在这里插入图片描述
    在这里插入图片描述
class Solution {
public:
    int search(vector<int>& nums, int target) {
        int left = 0;
        int right = nums.size(); // 定义target在左闭右开的区间里,即:[left, right)
        while (left < right) { // 因为left == right的时候,在[left, right)是无效的空间,所以使用 <
            int middle = left + ((right - left) >> 1);
            if (nums[middle] > target) {
                right = middle; // target 在左区间,在[left, middle)中
            } else if (nums[middle] < target) {
                left = middle + 1; // target 在右区间,在[middle + 1, right)中
            } else { // nums[middle] == target
                return middle; // 数组中找到目标值,直接返回下标
            }
        }
        // 未找到目标值
        return -1;
    }
};

2.例题

2.1.二分查找

https://leetcode.cn/problems/binary-search/description/

2.2.使用二分查找确定target左右边界

https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/

  • 该题目的要点在于非递减数组(升序,但是有可能有重复数字)中寻找target的左右边界,target可能有多个重复值的情况;
  • 时间复杂度需要是O(log N),表明需要使用二分查找确定左右边界,不能采用遍历方法确定边界;
  • 先确定情况:
    • target不再nums区间内;
    • target在nums区间内,但是nums中没有target;
    • nums中有target;

使用二分查找确定边界的原理:
二分查找在没有查找到target的时候,target是在最终的(left, right)这个区间之内的,可以使用这个原理来分别找出左右边界;

  • 确定右边界
    在这里插入图片描述
    当nums[mid]的值小于等于target的时候,选择更新right_border,right_border需要跟着left更新,因为无论是否找到target,最终left一定会指向nums中大于target的数中最小的那个数,就是target的有边界(开区间);
  • 左边界同理;

特殊情况:

  • nums为{2, 2},target为3,最终左右边界输出值为(-2, 2),之间的差值大于1,应该输出左右边界,但是target却不在nums中;
    解决方案:直接在最外面加一个判断target是否属于nums区间的条件;
class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        vector<int> res;
        
        int tar_l = leftBorder(nums, target);
        int tar_r = rightBorder(nums, target);
        cout << tar_l << "   " << tar_r << endl;

        if(nums.empty() || (target < nums[0] || target > nums[nums.size() - 1]))
        {
            //target不在nums范围
            res.push_back(-1);
            res.push_back(-1);
        }
        else if(tar_r - tar_l > 1)
        {
            //target存在于nums中
            res.push_back(tar_l + 1);
            res.push_back(tar_r - 1);
        }
        else
        {
            //target不存在于nums中
            res.push_back(-1);
            res.push_back(-1);
        }

        return res;
    }

    //寻找右边界 -- target右边的一个位置
    int rightBorder(vector<int>& nums, int target)
    {
        int right_border = -2;
        int left = 0, right = nums.size() - 1;
        //进行二分查找
        while(left <= right)
        {
            int mid = left + ((right - left) / 2);
            if(nums[mid] > target)//不断缩小右边界
            {
                right = mid - 1;
            }
            else
            {
                //缩小左边界
                //且当找到target时,left继续向右,目的是找到最右边target的位置
                left = mid + 1;
                right_border = left;
                //left最终指向的会是nums中大于target的最小数
            }
        } 
        return right_border;
    }
    
    //寻找左边界
    int leftBorder(vector<int>& nums, int target)
    {
        int left_border = -2;
        int left = 0, right = nums.size() - 1;
        //进行二分查找
        while(left <= right)
        {
            int mid = left + ((right - left) / 2);
            if(nums[mid] < target)//不断缩小左边界
            {
                left = mid + 1;
            }
            else
            {
                //缩小右边界
                //且当找到target时,right继续向右,目的是找到最左边target的位置
                right = mid - 1;
                left_border = right;
                //right最终指向的会是nums中小于target的最大数
            }
        } 
        return left_border;
    }

};

2.3.x的平方根

https://leetcode.cn/problems/sqrtx/submissions/483798269/

从题目的要求和示例我们可以看出,这其实是一个查找整数的问题,并且这个整数是有范围的。

  • 如果这个整数的平方 恰好等于 输入整数,那么我们就找到了这个整数;
  • 如果这个整数的平方 严格大于 输入整数,那么这个整数肯定不是我们要找的那个数;
  • 如果这个整数的平方 严格小于 输入整数,那么这个整数 可能 是我们要找的那个数(重点理解这句话)。

因此我们可以使用「二分查找」来查找这个整数,不断缩小范围去猜。
方法一:

  • 猜的数平方以后大了就往小了猜;
  • 猜的数平方以后恰恰好等于输入的数就找到了;
  • 猜的数平方以后小了,可能猜的数就是,也可能不是。
class Solution {
public:
    int mySqrt(int x) {
        int min = 0, max = x;
        int res = 0;
        
        while(min <= max)
        {
            int mid = min + (max - min) / 2;
            if((long long)mid * mid <= x)  // 防止乘法溢出
            {
            	// 目标结果的平方小于等于x,寻找出的就是范围内最大的满足平方<=x的数
                min = mid + 1;
                res = mid;
            }
            else
            {
                max = mid - 1;
            }
        }
        return res;
    }
};

方法二:

  • 使用二分查找寻找边界,目标数其实就是左边界
class Solution {
public:
    int mySqrt(int x) {
        int min = 0, max = x;
        int res = 0;
        //寻找左边界
        while(min <= max)
        {
            int mid = min + ((max - min) / 2);
            if((long long)mid * mid < x)
            {
                min = mid + 1;
            }
            else if((long long)mid * mid > x)
            {
                max = mid - 1;
                res = max;
            }
            else
            {
                return mid;
            }
        }
        return res;
    }
};

二、双指针法

1.移除元素

https://leetcode.cn/problems/remove-element/description/

快慢指针法
通过一个快指针和慢指针在一个for循环下完成两个for循环的工作;

  • 快指针:寻找新数组的元素 ,新数组就是不含有目标元素的数组
  • 慢指针:指向更新 新数组下标的位置
    请添加图片描述
class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int fastptr = 0, slowptr = 0;
        for(; fastptr < nums.size(); fastptr++)
        {
            if(nums[fastptr] != val)
            {
                nums[slowptr] = nums[fastptr];
                slowptr++;
            }
        }
        return slowptr;
    }
};

首尾指针法

class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int left = 0, right = nums.size() - 1;
        while(left <= right)
        {
            if(nums[left] == val)
            {
                int tmp = nums[left];
                nums[left] = nums[right];
                nums[right] = tmp;
                right--;
            }
            else
            {
                left++;
            }
        }
        return right + 1;
    }
};

2.有序数组的平方

双指针法

数组其实是有序的, 只不过负数平方之后可能成为最大数了;
那么数组平方的最大值就在数组的两端,不是最左边就是最右边,不可能是中间;
此时可以考虑双指针法了,i指向起始位置,j指向终止位置;
定义一个新数组result,和A数组一样的大小,让k指向result数组终止位置。
请添加图片描述

class Solution {
public:
    vector<int> sortedSquares(vector<int>& A) {
        int k = A.size() - 1;
        vector<int> result(A.size(), 0);
        for (int i = 0, j = A.size() - 1; i <= j;) { // 注意这里要i <= j,因为最后要处理两个元素
            if (A[i] * A[i] < A[j] * A[j])  {
                result[k--] = A[j] * A[j];
                j--;
            }
            else {
                result[k--] = A[i] * A[i];
                i++;
            }
        }
        return result;
    }
};

3.三数之和(重点)

详见链接

4.四数之和

详见链接

5.替换数字

详见链接

6.反转字符串

详见链接

7.翻转字符串里的单词

详见链接

8.删除链表的倒数第 N 个结点

详情见本篇博客

9.链表相交

详见链接

10.环形链表II

详见链接

三、滑动窗口

1.长度最小的子数组

https://leetcode.cn/problems/minimum-size-subarray-sum/description/
所谓滑动窗口,就是不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果

  • 首先要思考 如果用一个for循环,那么应该表示 滑动窗口的起始位置,还是终止位置。
  • 如果只用一个for循环来表示 滑动窗口的起始位置,那么如何遍历剩下的终止位置?
  • 所以 只用一个for循环,那么这个循环的索引,一定是表示 滑动窗口的终止位置。

滑动窗口的起始位置如何移动呢?
这里还是以题目中的示例来举例,s=7, 数组是 2,3,1,2,4,3,来看一下查找的过程:
请添加图片描述
在本题中实现滑动窗口,主要确定如下三点:

  • 窗口内是什么?
  • 如何移动窗口的起始位置?
  • 如何移动窗口的结束位置?

窗口就是 满足其和 ≥ s 的长度最小的连续子数组。
窗口的起始位置如何移动:如果当前窗口的值大于s了,窗口就要向前移动了(也就是该缩小了)。
窗口的结束位置如何移动:窗口的结束位置就是遍历数组的指针,也就是for循环里的索引。
解题的关键在于 窗口的起始位置如何移动,如图所示:
请添加图片描述
可以发现滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将O(n^2)暴力解法降为O(n)。

class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int res = INT_MAX;
        int begin = 0, end = 0; // 滑动窗口起始指针
        int subLen = 0; // 每次满足条件的子数组的大小 -- 滑动窗口长度
        int sum = 0; // 子数组元素之和
        for(; end < nums.size(); end++) // 遍历数组元素,使用end遍历
        {
            sum += nums[end]; // 更新子数组的和
            //若满足条件,则开始滑动begin
            while(sum >= target)
            {
                subLen = end - begin + 1; //更新滑动窗口长度
                res = subLen < res ? subLen : res; // 更新结果
                sum -= nums[begin++]; // 滑动begin
            }
        }
        return res == INT_MAX ? 0 : res;
    }
};
  • 时间复杂度:O(n) – 不要以为for里放一个while就以为是O(n^2)啊, 主要是看每一个元素被操作的次数,每个元素在滑动窗后进来操作一次,出去操作一次,每个元素都是被操作两次,所以时间复杂度是 2 × n 也就是O(n)。
  • 空间复杂度:O(1)

2.水果成篮

https://leetcode.cn/problems/fruit-into-baskets/description/

class Solution {
public:
    int totalFruit(vector<int>& fruits) {
        int maxFruits = 0;
        int sum = 0; // 滑动窗口内的水果数量
        int begin =0, end = 1; // 滑动窗口的首位指针
        if(fruits.size() < 3) // 果树数量小于3时的特殊情况
        {
            return fruits.size();
        }
        while(fruits[end] == fruits[begin]) // 先找到不同的两颗果树
        {
            end++;
            if(end == fruits.size()) // 排除全部果树相同的特殊情况
            {
                return fruits.size();
            }
        }
        int firstFruit = fruits[begin]; // 记录两种水果
        int secondFruit = fruits[end];

        for(; end < fruits.size(); end++)
        {
            // 出现了第三种果树
            if(fruits[end] != firstFruit && fruits[end] != secondFruit)
            {            
                //将begin移动到第三种果树之前的连续的最初的第二种果树的位置
                begin = end - 1;
                while(fruits[begin] == fruits[begin - 1])
                {
                    begin--;
                }
                //更新果树种类
                firstFruit = fruits[begin];
                secondFruit = fruits[end];
            }
            //计算滑动窗口内果树的数量
            sum = end - begin + 1;
            maxFruits = sum > maxFruits ? sum : maxFruits;
        }
        return maxFruits;
    }
};

3.最小覆盖字串

https://leetcode.cn/problems/minimum-window-substring/submissions/485496158/

  • 题目条件是:无序且有重复元素的字符串,因此可以使用哈希表来对目标字符串进行统计计数,对查找的子串也进行统计计数,一旦子串中的字符种类及个数与目标字符串一致,就是满足条件的子串,可以开始缩小滑动窗口
class Solution {
public:
    bool check()
    {
        for (const auto& c : targetStr)
        {
            if (cnt.find(c.first) != cnt.end())
            {
                // cnt中有c字符
                if (cnt[c.first] < c.second)
                {
                    return false;
                }
            }
            else
            {
                // cnt中没有c字符
                return false;
            }
        }
        return true;
    }


    string minWindow(string s, string t) {
        //先将目标字符串中所有的字符插入哈希表,同时统计每个字符的个数
        for (const auto& c : t)
        {
            targetStr[c]++;
        }

        int begin = 0, end = 0; // 滑动窗口首尾指针
        int minLen = INT_MAX; // 最终的最小子串长度
        int subLen = 0; // 满足条件的子串
        int minBegin = 0, minEnd = 0; // 最小子串的首尾指针

        for (; end < s.size(); end++)
        {
            //将当前end指向的字符插入到cnt中
            cnt[s[end]]++;
            //判断当前滑动窗口内的子串是否满足要求
            while (check())
            {
                // 满足要求,更新subStr
                subLen = end - begin + 1;
                if(subLen < minLen)
                {
                    minLen = subLen;
                    minBegin = begin;
                    minEnd = end;
                }
                // 开始收缩begin
                cnt[s[begin]]--;
                begin++;
            }
        }
        return minLen == INT_MAX ? string() : s.substr(minBegin, minEnd - minBegin + 1);
    }

public:
    unordered_map<char, int> targetStr;
    unordered_map<char, int> cnt;
};

四、螺旋矩阵

https://leetcode.cn/problems/spiral-matrix-ii/
关键点在于坚持循环不变量原则

  • 所有行列填充都是左闭右开
  • 填充上行从左到右
  • 填充右列从上到下
  • 填充下行从右到左
  • 填充左列从下到上
  • 若n为奇数,则中心点须特殊处理
  • 行填充的时候,列是不变的,反之同理
  • 一共需填充n / 2圈,每圈的流程都是类似的
    请添加图片描述
class Solution {
public:
    vector<vector<int>> generateMatrix(int n) {
        int loopCount = n / 2; // 一共循环几圈,若n为奇数,需要对中心值特殊处理
        int count = 1; // 需要填充的数字
        vector<vector<int>> res(n);
        for(int i = 0; i < n; i++)
        {
            res[i].resize(n);
        }

        for(int i = 0; i < loopCount; i++)
        {
            // 每一圈都需要对四条边进行填充,都是左闭右开进行填充
            //上行:[i][i] - [i][n - 2 - i]
            for(int j = i; j < n - 1 - i; j++)
            {
                res[i][j] = count++;
            }

            //右列:[i][n - 1 - i] - [n - 2 - i][n - 1 - i]
            for(int j = i; j < n - 1 - i; j++)
            {
                res[j][n - 1 - i] = count++;
            }

            //下行:[n - 1 - i][n - 1 - i] - [n - 1 - i][1 + i]
            for(int j = n - 1 - i; j > i; j--)
            {
                res[n - 1 - i][j] = count++;
            }

            //左列:[n - 1 - i][i] - [1 + i][i]
            for(int j = n - 1 - i; j > i; j--)
            {
                res[j][i] = count++;
            }
        }
        if(n % 2 != 0)
        {
            res[n / 2][n / 2] = n * n;
        }

        return res;
    }
};
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C++中,algorithm和numeric库中有几个常用的模板函数可以用于刷题,包括swap,reverse,sort,unique和accumulate。 1. swap(): 这个函数可以交换两个变量的值。它的模板定义如下: ```cpp template <class T> void swap(T& a, T& b) { T c(a); a = b; b = c; } ``` 通过调用swap()函数,可以交换两个变量的值。 2. reverse(): 这个函数可以反转容器中元素的顺序。例如,可以使用reverse()函数反转一个vector容器中的元素。 3. sort(): 这个函数可以对容器中的元素进行排序。可以通过提供一个比较函数来指定排序的规则。比如,可以使用sort()函数对一个vector容器中的元素进行升序排序或者降序排序。 4. unique(): 这个函数可以去除容器中的重复元素,并返回指向新的逻辑尾部的迭代器。需要注意的是,在调用unique()函数之前,需要先对容器进行排序。 5. accumulate(): 这个函数可以对容器中的元素进行累加操作,并返回累加结果。accumulate()函数接受三个参数:容器的起始迭代器、容器的结束迭代器和一个初始值。可以使用accumulate()函数计算数组或者vector容器中元素的和。 以上是C++中一些常用的算法函数,可以在刷题过程中使用它们来简化代码并提高效率。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [C++ LeetCode刷题常用函数](https://blog.csdn.net/qq_40876059/article/details/126245632)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值