代码随想录:数组
二分查找
使用basic二分查找的特征:
- 数组是单调的
- 没有重复元素(有重复元素可能会有多个结果)
left middle right
使用二分查找关键的是如何定义边界条件和终止条件,不同的定义写法也不一样。下面给出了两种定义
左闭右闭 [left, right] 使得循环终止条件为 while(left <= right)
左闭右开 [left, rught) 使得循环终止条件为 while(left < right)
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 | //左闭右闭 int search1(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; } //左闭右开 int search2(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; } |
例子:使用二分法寻找target在数组中的左右边界
给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
进阶:可以设计并实现时间复杂度为 $O(\log n)$ 的算法解决此问题。
思路:使用两个二分法来分别查找左右边界;
//在升序数组中找某个target的左右边界 class findBoundInArraySolution { 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 if(nums[middle] == target){ // 寻找右边界,nums[middle] == target的时候更新left left = middle + 1; rightBorder = left; } else { left = middle + 1; } } 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 if (nums[middle] == target) { right = middle - 1; leftBorder = right; } else { left = middle + 1; } } return leftBorder; } }; int main() { //test_arr(); //int nums[10] = {-1,0,3,5,9,12}; vector<int>nums = { -1, 0, 3, 5,5, 9, 12 }; vector<int> ret2; findBoundInArraySolution s; ret2 = s.searchRange(nums, 5); cout << ret2[0]<<":"<<ret2[1] << endl; return 0; }
思路:使用一个二分法查找target,然后左右滑查找相同元素定边界
def searchRange(nums: List[int], target: int) -> List[int]: def binarySearch(nums:List[int], target:int) -> int: left, right = 0, len(nums)-1 while left<=right: # 不变量:左闭右闭区间 middle = left + (right-left) // 2 if nums[middle] > target: right = middle - 1 elif nums[middle] < target: left = middle + 1 else: return middle return -1 index = binarySearch(nums, target) if index == -1:return [-1, -1] # nums 中不存在 target,直接返回 {-1, -1} # nums 中存在 targe,则左右滑动指针,来找到符合题意的区间 left, right = index, index # 向左滑动,找左边界 while left -1 >=0 and nums[left - 1] == target: left -=1 # 向右滑动,找右边界 while right+1 < len(nums) and nums[right + 1] == target: right +=1 return [left, right] nums = [ -1, 0, 3, 5,5, 9, 12] ret = searchRange(nums,5) print(ret)
删除数组中的元素
不管是一维数组还是二维数组,在内存中都是处于连续地址,所以不能删除某一个元素的那个空间,只能覆盖
如果删除中间的某一个元素,在一些题目中,那么可能要把其他元素往前移
有两个思路,一个是暴力破解$O(n^2)$,干就完了;另一个是使用双指针,快慢指针$O(n)$云云
暴力破解,每找到一个要删的元素就把后面的所有元素往前移
// 时间复杂度: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; } };
快慢指针,每找到一个要留下的元素就把它存在新头的后面
class Solution: """双指针法 时间复杂度:O(n) 空间复杂度:O(1) """ @classmethod def removeElement(cls, nums: List[int], val: int) -> int: fast = slow = 0 while fast < len(nums): if nums[fast] != val: nums[slow] = nums[fast] slow += 1 # 当 fast 指针遇到要删除的元素时停止赋值 # slow 指针停止移动, fast 指针继续前进 fast += 1 return slow
有序数组的平方
给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。
直接平方+一次排序 $O(n + n\log n)$:
class Solution { public: vector<int> sortedSquares(vector<int>& A) { for (int i = 0; i < A.size(); i++) { A[i] *= A[i]; } sort(A.begin(), A.end()); // 快速排序 return A; } };
这个有序数组可能有负数,因为平方,最小的可能要跑到最大的位置,但是因为它有序,所以取绝对值最大的一定在两端,那么使用双指针就可以达到$O(n)$
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; } };
这里的双指针是 i 和 j
滑动窗口
用一道最经典的题目来说滑动窗口:
给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的 连续 子数组,并返回其长度。如果不存在符合条件的子数组,返回 0。
暴力破解显然也可以做但是复杂度$O(n^2)$
所谓滑动窗口,就是不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果。尽管有点像双指针,他就是一个窗口在滑,so...
使用滑动窗口有几个重要的问题要搞清楚
- 窗口内是什么?
- 窗口左端怎么移动?
- 窗口右端怎么移动?
- 结束条件
复杂度 $O(n)$解法:
窗口内是正标定的目标子序列
当子序列的和大于target时候,就把左端往前移(缩短子序列)
当子序列的和小于target时候,就把右端往前移(延长子序列)
结束条件:窗口右端从0到末尾 $O(n)$ 体现在这
class minLengthSumSolution { public: int minSubArrayLen(int s, vector<int>& nums) { int result = INT32_MAX; int sum = 0; // 滑动窗口数值之和 int i = 0; // 滑动窗口起始位置 int subLength = 0; // 滑动窗口的长度 for (int j = 0; j < nums.size(); j++) { sum += nums[j]; // 注意这里使用while,每次更新 i(起始位置),并不断比较子序列是否符合条件 while (sum >= s) { subLength = (j - i + 1); // 取子序列的长度 result = result < subLength ? result : subLength; sum -= nums[i++]; // 这里体现出滑动窗口的精髓之处,不断变更i(子序列的起始位置) } } // 如果result没有被赋值的话,就返回0,说明没有符合条件的子序列 return result == INT32_MAX ? 0 : result; } };
生成螺旋矩阵的二维数组,和顺时针打印矩阵
给定一个正整数 n,生成一个包含 1 到 $n^2$ 所有元素,且元素按顺时针顺序螺旋排列的正方形矩阵。
示例:
输入: 3 输出: [ [ 1, 2, 3 ], [ 8, 9, 4 ], [ 7, 6, 5 ] ]
主要考察过程的模拟
当n一大,这道题有非常多的循环,如果不定好边界怎么处理,将非常麻烦
从上图可以看出,每一个颜色都是一条边,当到了拐角处,拐角处的位置让给新的边
那么就可以写代码了.
上述矩阵生成之后要按顺时针打印出来
class buildSpiralMatrixSolution { public: vector<vector<int>> buildMatrix(int n) { vector<vector<int>> res(n, vector<int>(n, 0)); 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 < starty + n - offset; j++) { res[startx][j] = count++; } // 模拟填充右列从上到下(左闭右开) for (i = startx; i < startx + 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 += 2; } // 如果n为奇数的话,需要单独给矩阵最中间的位置赋值 if (n % 2) { res[mid][mid] = count; } return res; } vector<int> spiralOrder(vector<vector<int>>& matrix) { //https://leetcode-cn.com/problems/shun-shi-zhen-da-yin-ju-zhen-lcof/ //顺时针打印矩阵 //边界法 if (matrix.size() == 0) return vector<int>(); int l = 0; //最左边的列 int r = matrix[0].size() - 1; //最右边的列 int u = 0;//最上面一行 int d = matrix.size() - 1;//最底下一行 vector<int> res; while (true) { //从左往右输出最上面一行 for (int i = l; i <= r; i++) { res.push_back(matrix[u][i]); } //更新顶 u += 1; if (u > d) break; //从上到下输出最右边一行 for (int i = u; i <= d; i++) { res.push_back(matrix[i][r]); } //更新右 r -= 1; if (l > r) break; //从右往左输出最底下一行 for (int i = r; i >= l; i--) { res.push_back(matrix[d][i]); } //更新底 d -= 1; if (u > d) break; //从下往上输出最左边一行 for (int i = d; i >= u; i--) { res.push_back(matrix[i][l]); } //更新左 l += 1; if (l > r) break; } return res; } };
c/c++数组基础知识
- 数组是存放在连续内存空间上的相同类型数据的集合。
- 数组下标都是从0开始的。
- 数组内存空间的地址是连续的
- 数组的元素是不能删的,只能覆盖。
- 二分法是一种思想,有使用的固定条件
- 双指针是一种使用一个循环做两个循环做的事情的替代方法
- 滑动窗口注意事项要搞清楚
- 行为模拟的化,抓住循环不变量
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异
· 三行代码完成国际化适配,妙~啊~