数组刷题
主要参考《代码随想录》,次要参考leetcode上面各种好的题解思路
基础知识
- 定义:数组是存放在连续内存空间上的相同类型的数据集合
- 特点:
- 数组下标从0开始的
- 数组在内存的地址空间是连续的
- 随机存取,但是增删元素需要移动其他元素,比较慢
- 数组在内存空间的地址时连续的,需要注意的时是
- 数组下标是从0开始的
- 数组内存空间是连续的
- vector是容器,底层是用array实现的
- java在二维数组的内存空间是由row个具有column元素的连续空间组成的,而不是row*column组成的
一、二分查找
基本二分法
-
letcode网址https://leetcode.cn/problems/binary-search/submissions/
-
算法特点
- 有序无重复的数组,有重复元素返回的下标可能不唯一
- 逻辑简单,但是确定边界比较繁琐
-
区间定义方式
- 左闭右闭 [left, right]
- 左闭右开 [left, right)
- 边界的处理就是根据区间的定义进行的
-
左闭右闭代码
class Solution { public: int search(vector<int>& 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;// 未找到目标值 } };
搜索插入位置
-
letcode网址https://leetcode.cn/problems/search-insert-position/
-
代码
class Solution { public: int searchInsert(vector<int>& 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;// 取整总是取的前一个 } };
在排序数组中查找元素的第一个和最后一个位置
-
letcode网址https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/
-
代码
class Solution { public: vector<int> searchRange(vector<int>& 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<int>& 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<int>& 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; } };
x 的平方根
-
letcode网址https://leetcode.cn/problems/sqrtx/
-
代码
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 } };
有效的完全平方数
-
letcode网址https://leetcode.cn/problems/valid-perfect-square/submissions/
-
代码
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; } };
二、移除元素
erase函数的实现
-
数组 中的元素不能删除,只能覆盖,删除中间元素需要后面的元素都向前移动进行覆盖
-
使用双指针:快指针寻找所需要的元素赋值给慢指针所指向的位置
-
代码实现https://leetcode.cn/problems/remove-element/submissions/
class Solution { public: int removeElement(vector<int>& 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;// 数组的新长度 } };
26.删除排序数组中的重复项
- 代码
int removeDuplicates(vector<int>& 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; }
283.移动零
-
代码
void moveZeroes(vector<int>& 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; }
844.比较含退格的字符串
-
使用双指针,将字符串的处理封装成函数,时间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; } };
977.有序数组的平方
-
寻找多种情况中,固定的哪一点。例如:有序(最值一定在端点处)
-
代码
class Solution { public: vector<int> sortedSquares(vector<int>& nums) { int p = nums.size() - 1; vector<int> 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; } };
三、滑动窗口
长度最小的子数组
-
滑动窗口的核心:根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将O(n^2)暴力解法降为O(n)。
-
代码
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; }
904.水果成篮
-
题目地址https://leetcode.cn/problems/fruit-into-baskets/
-
代码
int totalFruit(vector<int>& fruits) { // 设置窗口边界 int slow = 0; int fast = 0; // 设置条件判定值 unordered_map<int, int> 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; }
- 代码的开始先考虑边界值和正确性验证
-
什么时候使用哈希法? 查询集合中的一个元素是否存在
-
map是一种key value的存储结构,key保存要查询的键值,value保存对应其他信息
- std::map使用红黑树实现,key有序且不可重复,查询和增删效率均为O(log N)
- std::multimap使用红黑树实现,key有序且可重复,查询和增删效率均为O(log N)
- std::unordered_map使用哈希表,key无序且不可重复,查询和增删效率均为O(1)
-
map类型容器操作方法
unordered_map<int, string> mp; // 容器的插入 // 1. 直接使用[] 符号 mp[key] = "value";// mp[key]整体表示值 // 2. 使用make_pair构造对象进行插入 mp.insert(make_pair(key, "value")); // 3. 使用pair方法构造键值对插入 mp.insert(pair<int, string>(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<int, string>::iterator iter; for(iter = mp.begin(); iter != mp.end(); iter++) { cout<<iter->first<<": "<<iter->second<<endl; }
-
最小滑窗模板:给定数组 nums,定义滑窗的左右边界 i, j,求满足某个条件的滑窗的最小长度。
while j < len(nums): 判断[i, j]是否满足条件 while 满足条件: 不断更新结果(注意在while内更新!) i += 1 (最大程度的压缩i,使得滑窗尽可能的小) j += 1
-
最大滑窗模板:给定数组 nums,定义滑窗的左右边界 i, j,求满足某个条件的滑窗的最大长度。
while j < len(nums): 判断[i, j]是否满足条件 while 不满足条件: i += 1(一旦满足条件了就退出压缩i的过程,使得滑窗尽可能的大) 不断更新结果(注意在while外更新!) j += 1
76.最小覆盖子串
-
代码
class Solution { public: string minWindow(string s, string t) { unordered_map<char, int> sMap;// s的哈希表 unordered_map<char, int> 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; } };
- 记录最小窗口或最大窗口在外部的遍历循环结束,进行最值的记录
- str.substr(开始下标,下标后的位数),返回相应的子串
四、循环不变量
螺旋矩阵
-
代码
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; } };
- 寻找统一化的规则
- 特例:一般是开始的0值或者结束时出现特殊情况
- 细节:每一圈的长度均-1,每一圈的位置均加一
- 整体:总共遍历n/2圈,奇数要额外加(n/2, n/2)圈
- 寻找统一化的规则
顺时针打印矩阵
-
代码
class Solution { public: vector<int> spiralOrder(vector<vector<int>>& matrix) { if (matrix.size() == 0 || matrix[0].size() == 0) return {}; vector<int> 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; } };
c