• 1. 刷题——数组


    数组刷题

    主要参考《代码随想录》,次要参考leetcode上面各种好的题解思路

    基础知识

    1. 定义:数组是存放在连续内存空间上的相同类型的数据集合
    2. 特点:
      • 数组下标从0开始的
      • 数组在内存的地址空间是连续的
      • 随机存取,但是增删元素需要移动其他元素,比较慢
    3. 数组在内存空间的地址时连续的,需要注意的时是
      • 数组下标是从0开始的
      • 数组内存空间是连续的
    4. vector是容器,底层是用array实现的
    5. java在二维数组的内存空间是由row个具有column元素的连续空间组成的,而不是row*column组成的

    一、二分查找

    基本二分法
    1. letcode网址https://leetcode.cn/problems/binary-search/submissions/

    2. 算法特点

      • 有序无重复的数组,有重复元素返回的下标可能不唯一
      • 逻辑简单,但是确定边界比较繁琐
    3. 区间定义方式

      • 左闭右闭 [left, right]
      • 左闭右开 [left, right)
      • 边界的处理就是根据区间的定义进行的
    4. 左闭右闭代码

      class Solution {
      public:
          int search(vector& nums, int target) {
              int left = 0;
              int right = nums.size() - 1; // 定义target在左闭右闭的区间里
              
              while (left <= right) { // 前闭后闭用<=
              // 当left==right,区间[left, right]依然有效,所以用 <=
                  int middle = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2
                  if (nums[middle] > target) {// 结果在左区间
                      right = middle - 1; 
                      // target A在左区间,所以[left, middle - 1]
                  } else if (nums[middle] < target) {// 结果在右区间
                      left = middle + 1; 
                      // target 在右区间,所以[middle + 1, right]
                  } else { // 相等找到目标值
                      return middle; // 数组中找到目标值,直接返回下标
                  }
              }
              return -1;// 未找到目标值
          }
      };
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
    搜索插入位置
    1. letcode网址https://leetcode.cn/problems/search-insert-position/

    2. 代码

      class Solution {
      public:
          int searchInsert(vector& nums, int target) {
              int n = nums.size();
              int left = 0;
              int right = n - 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;
                  }
              }
              // 分别处理如下四种情况
              // 目标值在数组所有元素之前  [0, -1]
              // 目标值等于数组中某一个元素  return middle;
              // 目标值插入数组中的位置 [left, right],return  right + 1
              // 目标值在数组所有元素之后的情况 [left, right], 因为是右闭区间,所以 return right + 1
              return right + 1;// 取整总是取的前一个
          }
      };
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
    在排序数组中查找元素的第一个和最后一个位置
    1. letcode网址https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/

    2. 代码

      class Solution {
      public:
          vector searchRange(vector& nums, int target) {
              int rightBorder = getRightBorder(nums, target);
              int leftBorder = getLeftBorder(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& nums, int target) {
              int left = 0;
              int right = nums.size() - 1;
              int mid;
              int rightBorder = -2;
      
              while (left <= right) {
                  mid = (left + right) / 2;
                  if (target < nums[mid]) {
                      right = mid - 1;
                  }else{
                      left = mid + 1;
                      rightBorder = left;
                  }
              }
              return rightBorder;
          }
          
          int getLeftBorder(vector& nums, int target) {
              int left = 0;
              int right = nums.size() - 1;
              int mid;
              int leftBorder = -2;
               
              while (left <= right) {
                  mid = (left + right) / 2;
                  if (target > nums[mid]){
                      left = mid + 1;
                  }else{
                      right = mid - 1;
                      leftBorder = right;
                  }
              }
              return leftBorder;
      
          }
      };
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
    x 的平方根
    1. letcode网址https://leetcode.cn/problems/sqrtx/

    2. 代码

      class Solution {
      public:
          int mySqrt(int x) {
              // x的平方根一定在[1, x/2+1]内
              int left = 1;
              int right = (x >> 1) + 1; // 除以2的倍数可以使用位运算
              // 区间使用方式:前闭后闭
              while (left <= right) {
                  int mid = left + ((right - left) >> 1); // 防止溢出,使用位运算加快速度
                  if (mid > x / mid) {  // 尽量使用除法和减法防止数值太大而内存溢出
                      right = mid - 1;
                  } else if (mid < x / mid) {
                      left = mid + 1;
                  } else {
                      return mid;
                  }
              }
              return right; // 终止条件left > right,因为是向下取整,所以返回的是right     
          }
      };
      
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
    有效的完全平方数
    1. letcode网址https://leetcode.cn/problems/valid-perfect-square/submissions/

    2. 代码

      class Solution {
      public:
          bool isPerfectSquare(int num) {
              int left = 1;
              int right = (num >> 1) + 1;
              long mid;
      
              while (left <= right) {
                  mid = left + ((right - left) >> 1);
                  if (num < mid*mid) {// 不能使用num / mid 和mid进行比较,会向下取整导致错误
                      right = mid - 1;
                  }else if (num > mid*mid){
                      left = mid + 1;
                  }else
                      return true;
              }
              return false;
          }
      };
      
      
      // 完全平方数可以使用从1开始的连续奇数相加得到,不能得到的就是非完全平方数
      
      class Solution 
      {
      public:
          bool isPerfectSquare(int num) 
          {
              int num1 = 1;
              while(num > 0) 
              {
                  num -= num1;
                  num1 += 2;
              }
              return num == 0;
          }
      };
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38

    二、移除元素

    erase函数的实现
    1. 数组 中的元素不能删除,只能覆盖,删除中间元素需要后面的元素都向前移动进行覆盖

    2. 使用双指针:快指针寻找所需要的元素赋值给慢指针所指向的位置

    3. 代码实现https://leetcode.cn/problems/remove-element/submissions/

      class Solution {
      public:
          int removeElement(vector& nums, int val) {
              int slow = 0;
              // 快指针寻找所需元素,慢指针获取赋值位置
              for(int fast = 1; fast < nums.size(); ++fast){
                  if (nums[fast] != val){
                      nums[slow] = nums[fast];
                      slow++; 
                  }
              }
              return slow;// 数组的新长度
          }
      };
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
    26.删除排序数组中的重复项
    1. 代码
      int removeDuplicates(vector& nums) {
             int slow = 0;
             for (int fast = 1; fast  < nums.size() ; ++fast){
                 if (nums[fast] == nums[slow]){
                     continue;
                 }else{
                 // 不相等就覆盖下一个slow的位置
                 ++slow;
                 nums[slow] = nums[fast];
                 }
             }   
             return slow + 1;
         }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
    283.移动零
    1. 代码

      void moveZeroes(vector& nums) {
              int slow = 0;
              for(int fast = 0; fast < nums.size(); ++fast){
                  if (nums[fast] != 0) 
                      nums[slow++] = nums[fast];
              }
          // 慢指针最后停下的位置是判断条件符合的最后一个
              while (slow < nums.size()) 
                  nums[slow++] = 0;
          }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
    844.比较含退格的字符串
    1. 使用双指针,将字符串的处理封装成函数,时间100%,但是空间因为堆栈消耗可能大一些,只是超过60%

      class Solution {
      public:
          bool backspaceCompare(string s, string t) {
              int sLength = 0;
              int tLength = 0;
              int ptr = 0;
              // 对两个串都进行消减处理
              handleString(s, sLength);
              handleString(t, tLength);
              // 长度不等,直接否定
              if (sLength != tLength)
                  return false;
              // 比较每个元素是否相同
              while(ptr < sLength) {
                  if (s[ptr] != t[ptr])
                      return false;
                  ptr++;
              }
              return true;
          }
      
      private:
          // 对每个字符串进行消减
          void handleString(string &str, int &length) {
              int slow = 0;
              int fast = 0;
              while (fast < str.size()) {
                  if (str[fast] != '#') {
                      str[slow] = str[fast];
                      slow++;
                      fast++;
                  } else {
                      fast++;
                      if (slow != 0) slow--;// 退格到0将无法继续退格
                  }
              }
              length = slow;
          }
      };
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
    977.有序数组的平方
    1. 寻找多种情况中,固定的哪一点。例如:有序(最值一定在端点处)

    2. 代码

      class Solution {
      public:
          vector sortedSquares(vector& nums) {
              int p = nums.size() - 1;
              vector result(nums.size(), 0); // 初始化结果数组
              int left = 0;
              int right = nums.size() - 1;
              
              while (left <= right) {
                  if ((nums[left] * nums[left]) > (nums[right] * nums[right])) {
                      result[p--] = nums[left] * nums[left];
                      left++;
                  } else {
                      result[p--] =   nums[right] * nums[right];
                      right--;// 左边界++,右边界--
                  }
              }
              return result;
          }
      };
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20

    三、滑动窗口

    长度最小的子数组
    1. 滑动窗口的核心:根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将O(n^2)暴力解法降为O(n)。

    2. 代码

       int minSubArrayLen(int target, vector<int>& nums) {
              // 双指针定义
              int slow = 0;
              int fast = 0;
              // 过程变量记录
              int minLength = 0;
              int sum = 0;
              int result = INT32_MAX;// int的最大值
           
              for(;fast < nums.size(); ++fast) {
                  sum += nums[fast];
                  while (sum >= target) {// 核心就是if改为while
                      minLength = fast - slow + 1;// 取连续子序列的长度
                      result = result < minLength ? result : minLength;
                      // 比较记录最小长度
                      sum -= nums[slow++];// 慢指针滑动
                  }
              }
           
              return result == INT32_MAX ? 0 : result;
          }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
    904.水果成篮
    1. 题目地址https://leetcode.cn/problems/fruit-into-baskets/

    2. 代码

       int totalFruit(vector& fruits) {
              // 设置窗口边界
              int slow = 0;
              int fast = 0;
              // 设置条件判定值
              unordered_map basket; //创建篮子
              int fruit_counter = 0; //篮子中的水果种类数
              int length = 0;
      
              while (fast < fruits.size()) {
                  // 快指针前进的条件
                  if (basket[fruits[fast]] == 0)// 水果种类fruits[fast]是键,basket[fruits[fast]]是值
                      fruit_counter++;
                  basket[fruits[fast]]++; // 插入新水果 
                  // 慢指针前进的条件
                  while (fruit_counter > 2) {
                      basket[fruits[slow]]--;
                      if (basket[fruits[slow]] == 0) {
                          fruit_counter--;
                      }
                      slow++;
                  }
                  // 善后处理
                  length = max(length, fast - slow + 1);
                  fast++;
              }
              
              return length;
          }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 代码的开始先考虑边界值和正确性验证
    3. 什么时候使用哈希法? 查询集合中的一个元素是否存在

    4. map是一种key value的存储结构,key保存要查询的键值,value保存对应其他信息

      1. std::map使用红黑树实现,key有序且不可重复,查询和增删效率均为O(log N)
      2. std::multimap使用红黑树实现,key有序且可重复,查询和增删效率均为O(log N)
      3. std::unordered_map使用哈希表,key无序且不可重复,查询和增删效率均为O(1)
    5. map类型容器操作方法

      unordered_map mp;
      // 容器的插入
      // 1. 直接使用[] 符号
      mp[key] = "value";// mp[key]整体表示值
      // 2. 使用make_pair构造对象进行插入
      mp.insert(make_pair(key, "value"));
      // 3. 使用pair方法构造键值对插入
      mp.insert(pair(key, "d"));
      
      // 查找方法
      if (mp.find(key) != mp.end())
          function;// 找到返回迭代器,找不到返回mp.end()
          
      // 统计方法
      mp.count(key);// 统计key在map中出现的次数,没有返回0
      
      // 获取key的值
      // 1. 使用迭代器
      string val = mp.find(key)->second;
      
      // 2. at方法,不存在会报异常
      string val = mp.at(2);
      
      // 3. 使用[]操作符,不存在会插入
      string val = mp[key];
      
      // 删除元素
      // 1. 根据键删除
      mp.erase(key);
      // 2.根据值删除
      for(iter = mp.begin(); iter!=mp.end();) {
          if (iter->second=="value") {
              mymap.erase(iter++);
          } else {// 防止删除后迭代器失效
              iter++;
          }
      }
      
      // 遍历map
      map::iterator iter;
      for(iter = mp.begin(); iter != mp.end(); iter++) {
          cout<first<<": "<second<
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
  • 最小滑窗模板:给定数组 nums,定义滑窗的左右边界 i, j,求满足某个条件的滑窗的最小长度。

    while j < len(nums):
        判断[i, j]是否满足条件
        while 满足条件:
            不断更新结果(注意在while内更新!)
            i += 1 (最大程度的压缩i,使得滑窗尽可能的小)
        j += 1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
  • 最大滑窗模板:给定数组 nums,定义滑窗的左右边界 i, j,求满足某个条件的滑窗的最大长度。

    while j < len(nums):
        判断[i, j]是否满足条件
        while 不满足条件:
            i += 1(一旦满足条件了就退出压缩i的过程,使得滑窗尽可能的大)
        不断更新结果(注意在while外更新!)
        j += 1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
  • 76.最小覆盖子串
    1. 代码

      class Solution {
      public:
          string minWindow(string s, string t) {
              unordered_map sMap;// s的哈希表
              unordered_map tMap;// t的哈希表
              // 初始化t的哈希表
              for (auto c: t) // c是临时变量
                  tMap[c]++ ;
              
              string res;
              int cnt = 0;// 主串中必需字符的数量
              for (int fast = 0, slow = 0; fast < s.size(); fast++ ) {
                  // 
                  sMap[s[fast]]++ ;// 主串哈希表的插值
                  if (sMap[s[fast]] <= tMap[s[fast]]) 
                      cnt++ ;
                  // 剔除不符合的字符,进行窗口的缩紧
                  while (sMap[s[slow]] > tMap[s[slow]]) 
                      sMap[s[slow++]]-- ;
                  // 符合条件的子串,并迭代记录最小的
                  if (cnt == t.size()) {
                      if (res.empty() || fast - slow + 1 < res.size())
                          res = s.substr(slow, fast - slow + 1);
                  }
              }
              return res;
          }
      };
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 记录最小窗口或最大窗口在外部的遍历循环结束,进行最值的记录
      • str.substr(开始下标,下标后的位数),返回相应的子串

    四、循环不变量

    螺旋矩阵
    1. 代码

      class Solution {
      public:
       vector> generateMatrix(int n) {
              vector> res(n, vector(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;
          }
      };
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 寻找统一化的规则
        • 特例:一般是开始的0值或者结束时出现特殊情况
        • 细节:每一圈的长度均-1,每一圈的位置均加一
        • 整体:总共遍历n/2圈,奇数要额外加(n/2, n/2)圈
    顺时针打印矩阵
    1. 代码

      class Solution {
      public:
          vector spiralOrder(vector>& matrix) {
              if (matrix.size() == 0 || matrix[0].size() == 0) 
                  return {};
              vector res;
              int top = 0;                      //上边界
              int bottom = matrix.size() - 1;   //下边界
              int left = 0;                     //左边界
              int right = matrix[0].size() - 1; //右边界
              
              while (true) {
                  // 从左到右
                  for (int i = left; i <= right; ++ i) 
                      res.push_back(matrix[top][i]);
                  // 每次从左往右执行一次,则要往下移一层
                  if (++ top > bottom) break;
                  // 从上到下
                  for (int i = top; i <= bottom; ++ i) 
                      res.push_back(matrix[i][right]);
                  // 每次从上到下执行一次,则要往左移一列
                  if (-- right < left) break;
                  // 从右到左
                  for (int i = right; i >= left; -- i) 
                      res.push_back(matrix[bottom][i]);
                  // 每次从右到左执行一次,则要往上移一行
                  if (-- bottom < top) break;
                  // 从下到上
                  for (int i = bottom; i >= top; -- i) 
                      res.push_back(matrix[i][left]);
                  // 每次从下到上执行一次,则要往右移一列
                  if (++ left > right) break;
              }
              return res;
          }
      };
      
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38

    c

  • 相关阅读:
    软件测试概念介绍 -- 小白入门必看
    C++保留小数点后两位(floor&ceil&round)详解
    【Springboot】整合wxjava实现 微信小程序:授权登录
    计算机网络(自顶向下)—第四章测验题
    计算机毕业论文选题java毕业设计软件源代码springMVC+mysql实现进销存系统仓库管理系统[包运行成功]
    MacBook将iPad和iPhone备份到移动硬盘
    JavaGUI------------常用的组件(单选、复选框、下拉列表)
    Swift中的strong, weak, unowned
    艾美捷热转移稳定性检测试剂盒:简单、灵敏、均匀的荧光测定法
    C/S - Exploits 学习笔记
  • 原文地址:https://blog.csdn.net/qq_43840665/article/details/126570029