1.概念

数组是存放在连续内存空间上的相同类型数据的集合。
数组可以方便的通过下标索引的方式获取到下标下对应的数据。
举一个字符数组的例子,如图所示:
image.png
需要两点注意的是

  • 数组下标都是从0开始的。
  • 数组内存空间的地址是连续的

正是因为数组的在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。
例如删除下标为3的元素,需要对下标为3的元素后面的所有元素都要做移动操作,如图所示:
image.png
而且大家如果使用C++的话,要注意vector 和 array的区别,vector的底层实现是array,严格来讲vector是容器,不是数组。
数组的元素是不能删的,只能覆盖。
那么二维数组直接上图,大家应该就知道怎么回事了
image.png
那么二维数组在内存的空间地址是连续的么?
不同编程语言的内存管理是不一样的,以C++为例,在C++中二维数组是连续分布的。
我们来做一个实验,C++测试代码如下:

  1. void test_arr() {
  2. int array[2][3] = {
  3. {0, 1, 2},
  4. {3, 4, 5}
  5. };
  6. cout << &array[0][0] << " " << &array[0][1] << " " << &array[0][2] << endl;
  7. cout << &array[1][0] << " " << &array[1][1] << " " << &array[1][2] << endl;
  8. }
  9. int main() {
  10. test_arr();
  11. }
  12. 测试地址为:
  13. 0x7ffee4065820 0x7ffee4065824 0x7ffee4065828
  14. 0x7ffee406582c 0x7ffee4065830 0x7ffee4065834

注意地址为16进制,可以看出二维数组地址是连续一条线的。
一些录友可能看不懂内存地址,我就简单介绍一下, 0x7ffee4065820 与 0x7ffee4065824 差了一个4,就是4个字节,因为这是一个int型的数组,所以两个相邻数组元素地址差4个字节。
0x7ffee4065828 与 0x7ffee406582c 也是差了4个字节,在16进制里8 + 4 = c,c就是12。
如图:
image.png
所以可以看出在C++中二维数组在地址空间上是连续的

2. 二分法

704 二分查找

[

](https://leetcode-cn.com/problems/binary-search/)

思路

这道题目的前提是数组为有序数组,同时题目还强调数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下标可能不是唯一的,这些都是使用二分法的前提条件,当大家看到题目描述满足如上条件的时候,可要想一想是不是可以用二分法了。
二分查找涉及的很多的边界条件,逻辑比较简单,但就是写不好。例如到底是 while(left < right) 还是 while(left <= right),到底是right = middle呢,还是要right = middle - 1呢?
大家写二分法经常写乱,主要是因为对区间的定义没有想清楚,区间的定义就是不变量。要在二分查找的过程中,保持不变量,就是在while寻找中每一次边界的处理都要坚持根据区间的定义来操作,这就是循环不变量规则。
写二分法,区间的定义一般为两种,左闭右闭即[left, right],或者左闭右开即[left, right)。
下面我用这两种区间的定义分别讲解两种不同的二分写法。

写法一

第一种写法,我们定义 target 是在一个在左闭右闭的区间里,也就是[left, right] (这个很重要非常重要)
区间的定义这就决定了二分法的代码应该如何写,因为定义target在[left, right]区间,所以有如下两点:

  • while (left <= right) 要使用 <= ,因为left == right是有意义的,所以使用 <=
  • if (nums[middle] > target) right 要赋值为 middle - 1,因为当前这个nums[middle]一定不是target,那么接下来要查找的左区间结束下标位置就是 middle - 1

例如在数组:1,2,3,4,7,9,10中查找元素2,如图所示:
image.png

  1. // 版本一
  2. class Solution {
  3. public:
  4. int search(vector<int>& nums, int target) {
  5. int left = 0;
  6. int right = nums.size() - 1; // 定义target在左闭右闭的区间里,[left, right]
  7. while (left <= right) { // 当left==right,区间[left, right]依然有效,所以用 <=
  8. int middle = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2
  9. if (nums[middle] > target) {
  10. right = middle - 1; // target 在左区间,所以[left, middle - 1]
  11. } else if (nums[middle] < target) {
  12. left = middle + 1; // target 在右区间,所以[middle + 1, right]
  13. } else { // nums[middle] == target
  14. return middle; // 数组中找到目标值,直接返回下标
  15. }
  16. }
  17. // 未找到目标值
  18. return -1;
  19. }
  20. };

方法二

如果说定义 target 是在一个在左闭右开的区间里,也就是[left, right) ,那么二分法的边界处理方式则截然不同。
有如下两点:

  • while (left < right),这里使用 < ,因为left == right在区间[left, right)是没有意义的
  • if (nums[middle] > target) right 更新为 middle,因为当前nums[middle]不等于target,去左区间继续寻找,而寻找区间是左闭右开区间,所以right更新为middle,即:下一个查询区间不会去比较nums[middle]

在数组:1,2,3,4,7,9,10中查找元素2,如图所示:(注意和方法一的区别
image.png

  1. // 版本二
  2. class Solution {
  3. public:
  4. int search(vector<int>& nums, int target) {
  5. int left = 0;
  6. int right = nums.size(); // 定义target在左闭右开的区间里,即:[left, right)
  7. while (left < right) { // 因为left == right的时候,在[left, right)是无效的空间,所以使用 <
  8. int middle = left + ((right - left) >> 1);
  9. if (nums[middle] > target) {
  10. right = middle; // target 在左区间,在[left, middle)中
  11. } else if (nums[middle] < target) {
  12. left = middle + 1; // target 在右区间,在[middle + 1, right)中
  13. } else { // nums[middle] == target
  14. return middle; // 数组中找到目标值,直接返回下标
  15. }
  16. }
  17. // 未找到目标值
  18. return -1;
  19. }
  20. };

35. 搜索插入位置

思路

image.png

  • 目标值在数组所有元素之前
  • 目标值等于数组中某一个元素
  • 目标值插入数组中的位置
  • 目标值在数组所有元素之后

这四种情况确认清楚了,就可以尝试解题了。
接下来我将从暴力的解法和二分法来讲解此题,也借此好好讲一讲二分查找法。

暴力法

  1. class Solution {
  2. public:
  3. int searchInsert(vector<int>& nums, int target) {
  4. for (int i = 0; i < nums.size(); i++) {
  5. // 分别处理如下三种情况
  6. // 目标值在数组所有元素之前
  7. // 目标值等于数组中某一个元素
  8. // 目标值插入数组中的位置
  9. if (nums[i] >= target) { // 一旦发现大于或者等于target的num[i],那么i就是我们要的结果
  10. return i;
  11. }
  12. }
  13. // 目标值在数组所有元素之后的情况
  14. return nums.size(); // 如果target是最大的,或者 nums为空,则返回nums的长度
  15. }
  16. };
  • 时间复杂度:$O(n)$
  • 空间复杂度:$O(1)$

    二分法一(左闭右闭)

    1. class Solution {
    2. public:
    3. int searchInsert(vector<int>& nums, int target) {
    4. int n = nums.size();
    5. int left = 0;
    6. int right = n - 1; // 定义target在左闭右闭的区间里,[left, right]
    7. while (left <= right) { // 当left==right,区间[left, right]依然有效
    8. int middle = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2
    9. if (nums[middle] > target) {
    10. right = middle - 1; // target 在左区间,所以[left, middle - 1]
    11. } else if (nums[middle] < target) {
    12. left = middle + 1; // target 在右区间,所以[middle + 1, right]
    13. } else { // nums[middle] == target
    14. return middle;
    15. }
    16. }
    17. //跳出循环时[right,left]
    18. // 目标值插入数组中的位置 [left, right],return right + 1
    19. // 目标值在数组所有元素之后的情况 [left, right], return right + 1
    20. return right + 1; //或者return left;
    21. }
    22. };

二分法二(左闭右开)

  1. class Solution {
  2. public:
  3. int searchInsert(vector<int>& nums, int target) {
  4. int n = nums.size();
  5. int left = 0;
  6. int right = n; // 定义target在左闭右开的区间里,[left, right) target
  7. while (left < right) { // 因为left == right的时候,在[left, right)是无效的空间
  8. int middle = left + ((right - left) >> 1);
  9. if (nums[middle] > target) {
  10. right = middle; // target 在左区间,在[left, middle)中
  11. } else if (nums[middle] < target) {
  12. left = middle + 1; // target 在右区间,在 [middle+1, right)中
  13. } else { // nums[middle] == target
  14. return middle; // 数组中找到目标值的情况,直接返回下标
  15. }
  16. }
  17. // left = right时跳出循环
  18. // 目标值插入数组中的位置 [left, right) ,return right 即可
  19. // 目标值在数组所有元素之后的情况 [left, right),return right 即可
  20. return right; //或者return left;
  21. }
  22. };

34 在排序数组中查找元素的第一个和最后一个位置

思路

寻找target在数组里的左右边界,有如下三种情况:

  • 情况一:target 在数组范围的右边或者左边,例如数组{3, 4, 5},target为2或者数组{3, 4, 5},target为6,此时应该返回{-1, -1}
  • 情况二:target 在数组范围中,且数组中不存在target,例如数组{3,6,7},target为5,此时应该返回{-1, -1}
  • 情况三:target 在数组范围中,且数组中存在target,例如数组{3,6,7},target为6,此时应该返回{1, 1}

这三种情况都考虑到,说明就想的很清楚了。
接下来,在去寻找左边界,和右边界了。

二分法一(左闭右闭)

寻找右边界时,等号赋给nums[middle] < target,即nums[middle] <= target,此时left = mid+1,跳出循环时,left在最右边的target的右边即右边界+1
寻找左边界时,等号赋给nums[middle] > target,即nums[middle] >= target,此时right= mid -1 ,跳出循环时,right在最左边的target的左边即左边界-1

class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        int leftBorder = getLeftBorder(nums, target);
        int rightBorder = getRightBorder(nums, target);
        // 情况一
        if (leftBorder == -2 || rightBorder == -2) return {-1, -1};
        // 情况三
        if (rightBorder - leftBorder > 1) return {leftBorder + 1, rightBorder - 1};
        // 情况二
        return {-1, -1};
    }
private:
     int getRightBorder(vector<int>& nums, int target) {
        int left = 0;
        int right = nums.size() - 1;
        int rightBorder = -2; // 记录一下rightBorder没有被赋值的情况
        while (left <= right) {
            int middle = left + ((right - left) / 2);
            if (nums[middle] > target) {
                right = middle - 1;
            } else { // 寻找右边界,nums[middle] == target的时候更新left
                left = middle + 1;
                rightBorder = left;
            }
        }
        return rightBorder;
    }
    int getLeftBorder(vector<int>& nums, int target) {
        int left = 0;
        int right = nums.size() - 1;
        int leftBorder = -2; // 记录一下leftBorder没有被赋值的情况
        while (left <= right) {
            int middle = left + ((right - left) / 2);
            if (nums[middle] >= target) { // 寻找左边界,nums[middle] == target的时候更新right
                right = middle - 1;
                leftBorder = right;
            } else {
                left = middle + 1;
            }
        }
        return leftBorder;
    }
};

二分法二(左闭右开)

寻找右边界时,等号赋给nums[middle] < target,即nums[middle] <= target,此时left = mid+1,跳出循环时,left在最右边的target的右边即右边界+1
寻找左边界时,等号赋给nums[middle] > target,即nums[middle] >= target,此时right= mid -1 ,跳出循环时,right在最左边的target即左边界

class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        int rightboard = getright(nums,target);
        int leftboard = getleft(nums,target);
        if(leftboard==-2||rightboard==-2) return {-1,-1};
        if(rightboard-leftboard>0) return {leftboard,rightboard-1};
        return {-1,-1};

    }
    int getright(vector<int>& nums, int target)
    {   
        int left = 0,right =nums.size();
        int rightboard = -2;
        while(left<right)
        {
            int mid = left+(right-left)/2;
            if(nums[mid]>target)
                right = mid;
            else
            {
                left = mid+1;
                rightboard = left;
            }
        }
        return rightboard;
    }
    int getleft(vector<int>& nums, int target)
    {   
        int left = 0,right =nums.size();
        int leftboard = -2;
        while(left<right)
        {
            int mid = left+(right-left)/2;
            if(nums[mid]>=target)
            {
                right = mid;
                leftboard = right;
            }
            else
                left = mid+1;
        }
        return leftboard;
    }
};

3. 双指针

27.移除元素

快慢指针

用慢指针记录不等于val的数字位置,快指针遍历数组
当快指针所指数字等于val时,快指针+1,慢指针不动,直到快指针所指数字不等于val,将快指针所指数字的值将慢指针所指的值覆盖,同时快慢指针都+1;

class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int low = 0;
        for(int i = 0;i<nums.size();i++)
        {
            if(nums[i] != val)
            {
                nums[low] = nums[i];
                low++;
            }
        }
        return low;
    }
};

双向指针

左右指针遍历数组,当left>right时遍历结束,此时left指针所指的为最终数组末尾的下一个元素。
左指针用于寻找第一个等于val的数字,右指针用于寻找第一个不等于val的数字,然后将右指针数字覆盖至左指针所指数字。

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

26. 删除有序数组中的重复项

法一:

同27

class Solution {
public:
    int removeDuplicates(vector<int>& nums) {
        int slow = 0;
        for(int i = 0;i<nums.size();i++)
        {
            if(nums[i] != nums[slow])
                nums[++slow] = nums[i];
        }
        return slow+1;
    }
};

法二:

慢指针用于记录有效数组的末尾,快指针用于遍历数组,循环找到同个数字的末尾,并将数字赋给slow+1位置;

class Solution {
public:
    int removeDuplicates(vector<int>& nums) {
        int slow = 0,fast = 0;
        while(fast<nums.size())
        {
            int temp = nums[fast];
            while(fast<nums.size() && nums[fast]== temp)
                fast++;
            if(fast<nums.size())
                nums[++slow] = nums[fast];
        }
        return slow+1;
    }
};

977. 有序数组的平方

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

class Solution {
public:
    vector<int> sortedSquares(vector<int>& nums) {
        vector<int> result(nums.size());
        int k = nums.size()-1;
        for(int i=0,j=nums.size()-1;i<=j;)
        {
            if(nums[i]*nums[i]>nums[j]*nums[j])
            {
                result[k--] = nums[i]*nums[i];
                i++;
            }
            else
            {
                result[k--] = nums[j]*nums[j];
                j--;
            }
        }
        return result;
    }
};

209.长度最小的子数组

滑动窗口

快指针维护窗口的右边界,慢指针维护窗口的左边界。当窗口内和小于target时,fast++扩大右边界,反之,slow++,缩小左边界。
result用于记录最小数组长度,初始化为INA_MAX

class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int len = 0;
        int slow = 0,fast =0;
        int sum = 0;
        int result = INT_MAX;
        while(fast<nums.size())
        {
            sum += nums[fast];
            while(sum>=target)
            {
                len = fast-slow+1;
                result = min(result,len);
                sum -= nums[slow];
                slow++;
            }
            fast++; 
        }
        return result==INT_MAX?0:result;
    }
};

59.螺旋矩阵

因为要添加的元素为连续数字,循环终止条件可设为num<n*n.

class Solution {
public:
    vector<vector<int>> generateMatrix(int n) {
        int top=0,down=n-1,left = 0, right = n-1;
        vector<vector<int>> result(n,vector<int>(n));
        int num = 1;
        while(num<=n*n)
        {
            for(int i = left; i<=right;i++)
                result[top][i] = num++;
            top++;
            for(int i = top; i<=down;i++)
                result[i][right] = num++;
            right--;
            for(int i = right; i>=left;i--)
                result[down][i] = num++;
            down--;
            for(int i = down; i>=top;i--)
                result[i][left] = num++;
            left++;
        }
        return result;
    }
};