文章目录
数组
数组是存放在连续内存空间上的相同类型数据的集合。数组下标都是从0开始的。数组地址连续,数组元素只能覆盖,不能删除
一、二分查找(关键词:有序数组)
1.1(704)二分查找
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4
这道题目的前提是数组为有序数组,同时题目还强调数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下标可能不是唯一的,这些都是使用二分法的前提条件,当大家看到题目描述满足如上条件的时候,可要想一想是不是可以用二分法了。
二分法:
定义 target 是在一个在左闭右闭的区间里,也就是[left, right] (这个很重要非常重要)。
while (left <= right) 要使用 <= ,因为left == right是有意义的,所以使用 <=
if (nums[middle] > target) right 要赋值为 middle - 1,因为当前这个nums[middle]一定不是target,那么接下来要查找的左区间结束下标位置就是 middle - 1
二分法查找用while循环,定义三个变量,left=0;right=nums.size()-1;mid=left+(right-left)/2;注意:mid的定义!
class Solution {
public:
int search(vector<int>& nums, int target) {
int left=0;int right=nums.size()-1;
while(left<=right)//左闭右闭
{
int mid=left+(right-left)/2;
if(nums[mid]>target)
{
right=mid-1;
}
else if(nums[mid]<target)
{
left=mid+1;
}
else{
return mid;
}
}
return -1;
}
};
1.2(35)二分查找,并把元素插入位置
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
你可以假设数组中无重复元素
考虑四种情况:
1、比第一个数还小
2、比最后一个数还大
// if(target<nums[0]) return 0;
// if(target>nums[right]) return right+1;(这两行可不要,情况四涵盖了这两种情况)
3、等于数组里的一个数(普通的二分查找)
while(left<=right)
{
int mid=left+(right-left)/2;
if(nums[mid]<target)
{
left=mid+1;
}
else if(nums[mid]>target)
{
right=mid-1;
}
else{
return mid;
}
}
4、插入数组中
当数组中只剩下一个元素时,m=l=r,如果t大于最后一个元素值,l=mid+1,此时left就是该插入的位置;t<最后一个元素值,r=mid-1,它插在最后一个元素值前面,最后一个元素值的位置后移,它应该插入的位置就是原先left的位置;故返回left;
return left;
总体代码:
由于情况四涵盖情况一二,可屏蔽掉
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int left=0;int right=nums.size()-1;
// if(target<nums[0]) return 0;
// if(target>nums[right]) return right+1;(这两行可不要)
while(left<=right)
{
int mid=left+(right-left)/2;
if(nums[mid]<target)
{
left=mid+1;
}
else if(nums[mid]>target)
{
right=mid-1;
}
else{
return mid;
}
}
return left;
}
};
1.2(34)二分查找,在排序数组中,查找元素所在的第一个位置和最后一个位置
给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
c++初学者,提供一种比较容易理解的写法。
本题是 “704.二分查找” 的变形题,二分查找的一般写法是:
int getRightBorder(vector<int> nums, int target)
{
int left = 0;
int right = nums.size() - 1;
int rightBorder = -3;
while(left <= right){
int middle = left + (right - left) / 2;
if(nums[middle] < target)
left = middle + 1;
else if(nums[middle] > target)
right = middle - 1;
else{
// 此时 nums[middle] == target
return middle;
}
}
return -1;
}
这道题的思路是利用二分法分别找到target的左右边界的下标,例如:
对于 nums = {5, 7, 7, 8, 8, 10} , target = 8 这个示例 , 左右边界分别是 7 和 10,对应的下标是 2 和 5. 注意,边界是不包含target的。
先解决如何寻找右边界下标的问题。对此,我们只需在传统二分写法的基础上进行修改即可!会了右边界,左边界的寻找方法同理。
首先,简单地将该问题分为 数组中存在target 和 数组中不存在target 这两种情况。
A. 数组中存在target
当找到target时,传统的二分查找算法会立刻返回middle,如下:
else{
// 此时 nums[middle] == target
return middle;
}
但是,本题和传统二分查找的区别在于,数组中可能存在多个target,因此在找到target后还要继续查找是否有其他target存在?那些
target又在哪里呢?target的边界在哪里?为了解决这几个问题,我们将上述代码修改成
else{
// 此时 nums[middle] == target
left = middle + 1;
rightBorder = left;
}
这段代码的意思是,当我们发现一个target后,不要停止,继续将left右移(之所以右移是因为我们要找右边界,查找区间[left,right]
要不断向右缩小),并且因为已经知道middle处是target了,所以自然将left移到middle的右侧。反复这个过程,直到 left > right,
**此时,left就是右边界**,同时程序也从while循环中退出了,为了记录这个边界值,我还在循环中加入了第二句话,即
**rightBorder = left;**,这样,记录的rightBorder之不断更新,直到推出循环时记录的才是最终的右边界下标。
这样,如果target存在于数组中,我们就能找到其右边界了。
B. 数组中不存在target
继续上面的思考,若数组中不存在target,即nums[middle]和target不可能是相等关系,那么程序在while循环中自然永远进不到上
述 else 的代码块,只会会进入下面两个代码块之一:
if(nums[middle] < target)
left = middle + 1;
else if(nums[middle] > target)
right = middle - 1;
**这部分代码是不用改的**,可以发现这两种情况下,**没有对rightBorde的赋值,不像上面的 else 语句中不断更新rightBorder的
值**。那么,我们不妨在进入循环前先给rightBorder一个不可能是下标的值,像 -3,-99 等,如下:
int rightBorder = -3;
while(left <= right){
......
}
这样,程序退出循环后,若发现rightBorder的值还是-3或-99,那么说明程序只是进入了上述 if 和 else if 这两种形况,也就说明数组中没有target;相反,若是发现rightBorder更新成一个正常的下标值,那就说明程序曾经进入过 else 语句块,也就说明数组中有
target,并且此时rightBorder记录的就是右边界值.
综上所述,我们就得到了寻找右边界的函数:
int getRightBorder(vector<int> nums, int target)
{
int left = 0;
int right = nums.size() - 1;
int rightBorder = -3;
while(left <= right){
int middle = left + (right - left) / 2;
if(nums[middle] < target)
left = middle + 1;
else if(nums[middle] > target)
right = middle - 1;
else{
//exsit
//return middle;
left = middle + 1;
rightBorder = left;
}
}
return rightBorder;
}
调用 getRightBorder 时,要看看找到的右边界是正常的下标值还是-3,若是-3,那么说明数组中压根没target,那么最终结果是
{-1,-1};若是正常的下标值,那么rightBorder就是右边界值,但是最终结果要减1,因为我们之前说过了,这个右边界是不包含
target的,减去1后的下标才是最后一个target所在位置!
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
int leftBorder = getLeftBorder(nums, target); // target前一个位置,不包含target
int rightBorder = getRightBorder(nums, target); // target后一个位置,不包含target
if(leftBorder == -3 | rightBorder == -3)
return {-1, -1};
else
return {leftBorder + 1, rightBorder - 1};
}
private:
int getRightBorder(vector<int> nums, int target)
{
int left = 0;
int right = nums.size() - 1;
int rightBorder = -3;
while(left <= right){
int middle = left + (right - left) / 2;
if(nums[middle] < target)
left = middle + 1;
else if(nums[middle] > target)
right = middle - 1;
else{
//exsit
//return middle;
left = middle + 1;
rightBorder = left;
}
}
return rightBorder;
}
int getLeftBorder(vector<int> nums, int target)
{
int left = 0;
int right = nums.size() - 1;
int leftBorder = -3;
while(left <= right){
int middle = left + (right - left) / 2;
if(nums[middle] < target)
left = middle + 1;
else if(nums[middle] > target)
right = middle - 1;
else{
//exsit
//return middle;
right = middle - 1;
leftBorder = right;
}
}
return leftBorder;
}
};
1.4(69)二分查找,查找算术平方根
给你一个非负整数 x ,计算并返回 x 的 算术平方根 。
由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。
注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5 。
示例 1:
输入:x = 4
输出:2
两个点需要注意的:
1、mm可能需要longlong类型
2、如果输入为4,输出恰好为2,但如果输入是8,怎么解决
因为midmid小于x,可能是输出ans,但mid*mid大于x,肯定不可能是输出
int left=1;int right=x;int ans=0;
while(left<=right)//左闭右闭
{
long mid=left+(right-left)/2;
long long n=mid*mid;
if(n>x)
{
right=mid-1;
}
else if(n<x)
{
left=mid+1;
ans=mid;//因为不知道mid+1的平方是否还小于x,所以输出得为mid
}
else{
return mid;
}
}
return ans;
}
};
1.4总结:
1、什么时候用二分法?
一个有序数组,查找某元素,或是在此查找基础上插入数组中
注意对while循环的利用
2、查找时要分情况讨论,在不在数组中?在数组中的话,属不属于数组元素?不属于数组元素应该返回啥?返回left/right要画个例子看一看。
二、数组移除元素(双指针法)
2.1数组移除所有值为val的元素,并返回数组新长度
给定 nums = [3,2,2,3], val = 3,
函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。
你不需要考虑数组中超出新长度后面的元素。
1、暴力解法(不断覆盖n2时间复杂度)
代码如下:
// 时间复杂度:O(n^2)
// 空间复杂度:O(1)
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int size = nums.size();
for (int i = 0; i < size; i++) {
if (nums[i] == val) { // 发现需要移除的元素,就将数组集体向前移动一位
for (int j = i + 1; j < size; j++) {
nums[j - 1] = nums[j];
}
i--; // 因为下表i以后的数值都向前移动了一位,所以i也向前移动一位
size--; // 此时数组的大小-1
}
}
return size;
}
};
2、双指针法
双指针法(快慢指针法):通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。
// 时间复杂度:O(n)
// 空间复杂度:O(1)
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int slowIndex = 0;
for (int fastIndex = 0; fastIndex < nums.size(); fastIndex++) {
if (val != nums[fastIndex]) {
nums[slowIndex++] = nums[fastIndex];
}
}
return slowIndex;
}
};
2.2 双指针法求数组平方
给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序
数组其实是有序的, 只不过负数平方之后可能成为最大数了。
那么数组平方的最大值就在数组的两端,不是最左边就是最右边,不可能是中间。
此时可以考虑双指针法了,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;
}
};
2.3双指针法总结
双指针法的应用可以不占用额外空间和占用额外空间,实质都是对原数组进行两个for循环,两个均从前向后遍历或是两个一个从前往后一个从后向前遍历,将双重循环降为了单次循环for循环,两个变量去控制。
三、滑动窗口
3.1(209)求长度和满足大于s的长度最小的连续子数组
给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的 连续 子数组,并返回其长度。如果不存在返回0.
输入:s = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。符合条件的子数组,返回 0。
//暴力解法
int l=INT_MAX;
for(int s=0;s<nums.size();s++)
{
int sum=0;
for(int e=s;e<nums.size();e++)
{
sum+=nums[e];
if(sum>=target)
{
l=min(l,e-s+1);break;
}
}
}
if(l==INT_MAX) return 0;
return l;
滑动窗口:
滑动窗口也是两个变量去控制,很多时候我们不知道两个变量不用双重for循环怎么去控制,可以只对末尾的那个for循环单次遍历,在while判断条件结束时对第一个变量加一。
int sum=0;
for(int e=0;e<nums.size();e++)
{
sum+=nums[e];
while(sum>=target)
{
l=min(l,e-s+1);
sum-=nums[s];
s++;
}
}
if(l==INT_MAX) return 0;
return l;
四、螺旋矩阵(循环)
4.1(59)输出顺时针排列的正方形
给定一个正整数 n,生成一个包含 1 到 n2 所有元素,且元素按顺时针顺序螺旋排列的正方形矩阵。
输入: 3
输出:
[
[ 1, 2, 3 ],
[ 8, 9, 4 ],
[ 7, 6, 5 ]
]
先定义四个维度,然后四个for循环,并重新设定上边界、右边界、下边界、左边界。
vector<vector<int>> matrix(n, vector<int>(n, 0)); // 使用vector定义一个二维数组
int u=0;int d=n-1;int l=0;int r=n-1;
int count=1;
while(true)
{
for(int i=l;i<=r;i++)//从左到右
{
matrix[u][i]=count++;
}
u++;
if(u>d) break;重新设定上边界,若上边界大于下边界,则遍历遍历完成,下同
for(int i=u;i<=d;i++)//从上到下
{
matrix[i][r]=count++;
}
r--;
if(r<l) break;重新设定右边界,若右边界小于左边界,则遍历遍历完成,下同
for(int i=r;i>=l;i--)//从右到左
{
matrix[d][i]=count++;
}
d--;
if(d<u) break;重新设定下边界,若下边界小于上边界,则遍历遍历完成,下同
for(int i=d;i>=u;i--)//从下到上
{
matrix[i][l]=count++;
}
l++;
if(l>r) break;//重新设定左边界,若左边界大于右边界,则遍历遍历完成,下同
}
return matrix;
}
};
4.2(54)输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。
道理和上述一样,但是有几点需要注意:
首先要判断的就是矩阵是否为空,为空如何处理?而且要把是否为空放在判断四个边界的上面,因为为空matrix[0]就不存在了,执行出错;另外在for循环执行结束后,是先进行加一减一的操作再判断是否大于边界,即++u>d,break;
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
vector<int> ans;
if(matrix.empty()) return ans;
int u=0;int d=matrix.size()-1;;int l=0;int r=matrix[0].size()-1;
while(true)
{
for(int i=l;i<=r;i++)
{
ans.push_back(matrix[u][i]);
}
if(++u>d) break;
for(int i=u;i<=d;i++)
{
ans.push_back(matrix[i][r]);
}
if(--r<l) break;
for(int i=r;i>=l;i--)
{
ans.push_back(matrix[d][i]);
}
if(--d<u) break;
for(int i=d;i>=u;i--)
{
ans.push_back(matrix[i][l]);
}
if(++l>r) break;
}
return ans;
}
};
4.3循环总结
先定义四个维度,然后四个for循环,并重新设定上边界、右边界、下边界、左边界。但是有几点需要注意:
首先要判断的就是矩阵是否为空,为空如何处理?而且要把是否为空放在判断四个边界的上面,因为为空matrix[0]就不存在了,执行出错;另外在for循环执行结束后,是先进行加一减一的操作再判断是否大于边界,即++u>d,break;