算法 —— 数组
目录
- 算法 —— 数组
- 1.二分查找
- 1.1二分查找习题集
- 2.双指针法
- 2.1双指针法习题集
- 3.滑动窗口
- 3.1滑动窗口习题集
- 4.二维数组
- 4.1二维数组习题集
1.二分查找
二分查找适用于,在有序排列的数组中查找某一指定元素。
其原理为范围搜索:如果这个元素在某一范围内,那么就一直缩小这个范围;如果这个元素不在此范围内,那么就换一个范围搜索。
假设我们有一有序数组{2,3,3,5,6,8},在此数组中查找5,流程图可以看成是这样:
1.1二分查找习题集
704.二分查找
此题是二分查找的直接应用。需要注意的是,上述有关于二分查找的算法描述里的 end 指针,指向的是有序数组的最后一个元素,是一个左闭右闭的区间 [begin,end] ;但是在题目当中,使用左闭右开的区间 [begin,end) ,即 end 指针指向了有序数组中最后一个元素的下一个位置(end指向的是一个无效元素)。
class Solution { public: int search(vector<int>& nums, int target) { //遵循左闭右开的原则 [begin,end) int begin=0,end=nums.size(); while(begin < end) //遵循左闭右开 { int mid = (begin+end)/2; //锁定范围 if(nums[mid] < target) { begin = mid+1; //mid指向的元素已经确定了比target小,所以+1 } else if(nums[mid] > target) { end = mid; //遵循左闭右开原则,end=mid表示此范围不包括end指向的元素 } else { return mid; } } return -1; } };
35.搜索插入位置
此题代码与上一个题一致。不过在最后没有找到指定的元素时,需要处理:返回一个位置,这个位置是指定元素应该在此有序数组中出现的位置。
分析可知,两个指针( begin 和 end )相邻的时候就是最后一次二分查找。又因为我们遵循左闭右开的原则,实际上这一次查找, begin 指向的元素就是最后一个元素了,如果这个元素都不是我们要查找的指定元素,就说明我们需要处理返回值的问题了。即使没有找到指定元素,但是一定可以保证的是:二分查找已经把该指定元素可能出现的范围缩小到最小、最精确了。也就是说,当这次二分查找没有找到指定元素的时候,又会进行一次 begin 和 end 的更新,更新之后 begin 和 end 一定是指向同一个位置的(即 begin = end),所以他们两个指向的位置,就是指定元素本该出现的位置。
class Solution { public: int searchInsert(vector<int>& nums, int target) { //本题代码与上一题一致 int begin=0,end=nums.size(); while(begin < end) //遵循左闭右开 { int mid = (begin+end)/2; //锁定范围 if(nums[mid] < target) { begin = mid+1; //mid指向的元素已经确定了比target小,所以+1 } else if(nums[mid] > target) { end = mid; //遵循左闭右开原则,end=mid表示此范围不包括end指向的元素 } else { return mid; } } //return end; return begin; //两种都行 } };
34.在排序数组中查找元素的第一个和最后一个位置
由上面两题的经验可以得出,每一次二分查找都能锁定一个位置( mid 位置),那么要查找指定元素(可能多次出现)在有序数组中的起始位置和结束位置,就一定有两次二分查找。那么查找该元素的起始位置一定是这个元素出现的区间的左边界;结束位置一定是这个元素出现的区间的右边界。
那么我们二分查找的目的,就是查找这两个边界。
class Solution { public: vector<int> searchRange(vector<int>& nums, int target) { //获取 target 出现的区间的两个边界 int leftBorder = getLeftBorder(nums,target); int rightBorder = getRightBorder(nums,target); cout << leftBorder << " " << rightBorder << endl; if(leftBorder==-1 || rightBorder==-1) //我们规定当target不存在时,返回值为-1 { return {-1,-1}; } else if(rightBorder - leftBorder >= 1) { return {leftBorder,rightBorder-1}; } return {-1,-1}; } private: int getLeftBorder(vector<int>& nums, int target) { //遵循左闭右开原则 int begin=0,end=nums.size(); int leftBorder = -1; //左边界 while(begin < end) { int mid = (begin+end)/2; if(nums[mid] >= target) //既然mid位置的元素大于或等于了target,那么继续搜索的范围就一定在左边 { end = mid; leftBorder = end; } else { begin = mid+1; } } return leftBorder; } int getRightBorder(vector<int>& nums, int target) { //遵循左闭右开原则 int begin=0,end=nums.size(); int rightBorder = -1; //右边界 while(begin < end) { int mid = (begin+end)/2; if(nums[mid] > target) { end = mid; } else //既然mid位置的元素小于或等于了target,那么继续搜索的范围就一定在右边 { begin = mid+1; rightBorder = begin; } } return rightBorder; } };
69.x的平方根
这个题也是一道典型的二分查找问题。就是在 0~x 区间内,查找 x 的算数平方根。
这道题的关键在于到底是从右往左找还是从左往右找。因为 x 的算数平方根可能不是一个整数,是一个浮点数,但是测试用例明确表示了最后的结果左取整(即使算数平方根是 2.999……,左取整也是 2),所以我们必须从左往右找,才能符合左取整这个条件。
class Solution { public: int mySqrt(int x) { //在 0~x 范围内查找满足条件的数字 long long begin=0,end=(long long)x+1; //遵循左闭右开原则 int last = 0; while(begin < end) { int mid = (begin+end)/2; if((long long)mid*mid <= x) //从左往右找 { last = mid; begin = mid+1; } else { end = mid; } } return last; } };
367.有效的完全平方数
这道题是纯粹的二分查找算法,分析过程不再赘述。
class Solution { public: bool isPerfectSquare(int num) { //遵循左闭右开原则 long long begin=0,end=(long long)num+1; while(begin < end) { int mid=(begin+end)/2; if((long long)mid*mid < num) { begin = mid+1; } else if((long long)mid*mid > num) { end = mid; } else { return mid; } } return false; } };
2.双指针法
双指针法可以说是数组算法里最重要的算法。双指针的算法,第一能够提高代码效率;第二能够在基本的双指针操作上衍生出其他进阶算法(例如滑动窗口算法)。
双指针适用于任何物理内存空间连续的数组或者字符串。下面使用一道习题,来体会双指针的算法。
27.移除元素
这道题是基本的双指针法的直接应用,使用双指针算法后,能够将以前的暴力删除元素(时间复杂度为O(N^2))优化为时间复杂度只有O(N)的算法。
class Solution { public: int removeElement(vector<int>& nums, int val) { //思路就是将数组中非 val 的元素赋给新数组 //只不过这个新数组就是原数组 int slow=0,fast=0; while(fast < nums.size()) { if(nums[fast] != val) { nums[slow++]=nums[fast]; } ++fast; } return slow; //slow 指向删除元素后的数组的最后一个元素位置的下一个位置,就是长度 } };
2.1双指针法习题集
26.删除有序数组中的重复项
我们用画图的形式来描述我们的思路:
class Solution { public: int removeDuplicates(vector<int>& nums) { int slow = 0,fast = 0; while(fast < nums.size()) { if(nums[fast] != nums[slow]) { ++slow; nums[slow] = nums[fast]; } ++fast; } return slow+1; //最后 slow 一定指向最后一个有效元素,所以 slow+1 为长度 } };
283.移动零
思路与上题一致,是双指针算法的直接体现。
具体步骤为:slow 和 fast 指向数组的起始位置,fast 一直向后遍历,如果 fast 指向的元素不为 0 ,那么此元素就与 slow 指向的元素进行交换,然后 slow 向后移动一个位置,fast 继续向后遍历。这个操作会保证当 slow 和 fast 都指向非 0 元素时发生一次原地交换,也就是说只要 fast 指向的为非零元素,就无差别的进行交换,这样可以保证非零元素的相对顺序不会被破坏。
class Solution { public: void moveZeroes(vector<int>& nums) { int slow = 0,fast = 0; while(fast < nums.size()) { if(nums[fast] != 0) { swap(nums[slow++],nums[fast]); } ++fast; } } };
977.有序数组的平方
我们要把有序数组的每个元素进行平方运算,然后将此数组再进行升序排列。最容易想到的暴力思就是挨个遍历数组的每个元素然后进行平方运算,最后将此数组排序。但是我们不用暴力的做法。
我们运用双指针的算法。我们可以发现,有序数组是升序排列的并且含有负数,这就意味着,每个元素进行平方运算后,总是呈“两边大,中间小”。那么我们就使用两个指针 begin 和 end 分别指向此数组的头和尾,案后比较这两个指针指向的元素大小,将较大元素头插到另一个新的数组,然后 begin 和 end 向中间靠拢。
class Solution { public: vector<int> sortedSquares(vector<int>& nums) { vector<int> ret(nums.size()); //等大小的新数组 int index = ret.size()-1; //从后往前放 int begin=0,end=nums.size()-1; //双指针 while(begin <= end) //左闭右闭区间,当最后一次 begin==end 时也要进入循环 { long long numBegin = nums[begin]*nums[begin]; long long numEnd = nums[end]*nums[end]; if(numBegin > numEnd) { ret[index--] = numBegin; ++begin; } else { ret[index--] = numEnd; --end; } } return ret; } };
3.滑动窗口
滑动窗口就是双指针法的进阶算法,两个指针在某一区间内滑来滑去像一个滑动的窗口一样。具体看一道例题。
209.长度最小的子数组
暴力解法就是利用两个嵌套的 for 循环解决,这里不再赘述(超时,过不了OJ)。
我们利用滑动窗口的算法来解决这道题。首先定义两个指针分别为 begin 和 end 指向数组的起始位置,然后 end 一直向后移动,直到 begin 和 end 这个闭区间([begin,end])内的所有元素和大于等于 target,此时记录这个区间的长度;然后 begin 也向后移动,如果每次移动之后的 begin 和 end 区间内的所有元素和大于等于 target ,begin 就一直向后移动,直到不满足这个条件。也就是说,begin 和 end 区间内所有元素和并没有大于等于 target ,那么 end 就一直往后移动;如果满足这个条件,那么就是 begin 一直往后移动。满足条件的时候记录长度,最后的结果只需要这些长度中的最小长度即可。
class Solution { public: int minSubArrayLen(int target, vector<int>& nums) { int begin=0,end=0; //一开始 begin 和 end 指向同一位置 int lenMin = INT32_MAX; //最小长度 int sum = 0; //区间和 while(end < nums.size()) { sum += nums[end]; while(sum >= target) //如果满足条件 begin 就一直向后移动 { lenMin = min(lenMin,end-begin+1); //挑选长度最小的区间 sum -= nums[begin]; //每次移动都会导致区间和改变 ++begin; } ++end; } if(lenMin == INT32_MAX) //没有任何一种情况满足 >=target时 { return 0; } return lenMin; } };
3.1滑动窗口习题集
904.水果成篮
与上一道题一样,这道题也是滑动窗口的经典习题。题目的意思就是求一个"长度最长的子数组",只不过这个子数组有且仅有两个不同的元素。
思路可以是这样的:既然我们最后的结果必须包含两个不同的元素,那么我们定义一个用来记录种类的变量 types ,然后定义一个计数数组,end 指针向后移动时将碰到的每一个元素计数(计数之后如果为 1 ,那么 types 就加 1);直到 begin 和 end 这个区间内 types 为 3(types 为 3 时,刚好不满足子数组的要求),end 停止移动,并将现在的有效区间(begin 和 end区间内 types 为 2)长度做记录,然后向后移动 begin ,直到 begin 和 end 这个区间内 types 不为 3,begin 停止移动,然后再移动 end。
由此可见,滑动窗口的套路都是一样的。最后的结果,我们只需要返回一个最长的长度即可。
class Solution { public: int totalFruit(vector<int>& fruits) { //无脑定义两个指针 int begin=0,end=0; int types = 0; //种类变量 int cnt[fruits.size()]; //根据题目要求定义 0<=fruits[i]<fruits.length memset(cnt,0,sizeof(cnt)); int lenMax = 0; //长度 while(end < fruits.size()) { if(++cnt[fruits[end]] == 1) //将 end 碰到的每个元素做一个计数 { ++types; } while(types > 2) //当 types 为3时,就能保证找到最长子数组 { if(--cnt[fruits[begin]] == 0) //begin 向后移动,计数就得减少 { --types; } ++begin; } /*当types <= 2 时,对长度进行记录*/ lenMax = max(lenMax,end-begin+1); ++end; } return lenMax; } };
4.二维数组
二维数组常见的算法就是按要求打印一些图案,这实际上并没有什么算法,就是逻辑问题。我们以一道题举例。
59. 螺旋矩阵 II
题目的意思很简单,顺时针遍历二维数组。我们要思考的关键就在于每一行每一列到底遍历多少个元素,我们采用的做法是左闭右开原则。
最后就剩下中间的位置没有遍历到,单独处理即可。
class Solution { public: vector<vector<int>> generateMatrix(int n) { vector<vector<int>> ret(n); for(int i=0;i<n;i++) { ret[i].resize(n); } int start_x=0,start_y=0; //每一圈的起始位置 int cnt = n/2; //遍历这个二维数组需要的圈数 int i=0,j=0; //行、列 int offset = 1; //左闭右开,需要记录一下偏移量 int num = 1; while(cnt--) { for(j=start_y;j<n-offset;j++) { ret[start_x][j] = num++; } for(i=start_x;i<n-offset;i++) { ret[i][j] = num++; } for(;j>start_y;j--) { ret[i][j] = num++; } for(;i>start_x;i--) { ret[i][j] = num++; } /*每一圈结束后,偏移量、起始位置都需要更新*/ ++offset; ++start_x; ++start_y; } if(n%2 == 1) //n为奇数时,要处理最后的中间位置 { ret[start_x][start_y] = num++; //最后的起始位置就是中间位置 } return ret; } };
4.1二维数组习题集
54.螺旋矩阵
这道题与上一题非常相似,但是这道题的难度要稍微高一些。
第一,这道题并不一定是正方形矩阵;第二,不好掌握圈数;第三,不好掌握最后剩下的一些元素没有遍历到该如何处理。
我们可以使用两个变量 row 和 col 分别记录二维数组的行、列。那么圈数就是行和列的最小值再除以2;最后有一些元素没有遍历到,如果 row > col ,那么就一定剩下一列没有处理;如果 row < col ,那么就一定剩下一行没有处理。
那么这一行或者这一列的起始位置与上一道题一样。在这个基础上,处理一下到底是再进行一次行便利还是列遍历,遍历多少个元素即可。
class Solution { public: vector<int> spiralOrder(vector<vector<int>>& matrix) { //无脑写两个起始位置 int start_x=0,start_y=0; int row = matrix.size(); //行 int col = matrix[0].size(); //列 int cnt = min(row,col)/2; //圈数 vector<int> ret; int offset = 1; int i=0,j=0; /*老套路*/ while(cnt--) { for(j=start_y;j<col-offset;j++) { ret.push_back(matrix[start_x][j]); } for(i=start_x;i<row-offset;i++) { ret.push_back(matrix[i][j]); } for(;j>start_y;j--) { ret.push_back(matrix[i][j]); } for(;i>start_x;i--) { ret.push_back(matrix[i][j]); } ++offset; ++start_x; ++start_y; } if(min(row,col)%2 == 1) //如果是奇数,就必定剩下某一行或者某一列需要处理 { if(row > col) //此时剩下一列没有处理 { int times = row - col + 1; //剩下的元素个数 int i=start_x; while(times--) { ret.push_back(matrix[i++][start_y]); } } else { int times = col - row + 1; //剩下的元素个数 int j=start_y; while(times--) { ret.push_back(matrix[start_x][j++]); } } } return ret; } };
剑指 Offer 29. 顺时针打印矩阵
这一道题与上一道题是一模一样的,只不过需要注意,这道题需要我们对形参进行判空。即当形参的有效元素为 0 时,我们需要返回一个空容器。
class Solution { public: vector<int> spiralOrder(vector<vector<int>>& matrix) { if(matrix.size() == 0) { return {}; } //无脑写两个起始位置 int start_x=0,start_y=0; int row = matrix.size(); //行 int col = matrix[0].size(); //列 int cnt = min(row,col)/2; //圈数 vector<int> ret; int offset = 1; int i=0,j=0; /*老套路*/ while(cnt--) { for(j=start_y;j<col-offset;j++) { ret.push_back(matrix[start_x][j]); } for(i=start_x;i<row-offset;i++) { ret.push_back(matrix[i][j]); } for(;j>start_y;j--) { ret.push_back(matrix[i][j]); } for(;i>start_x;i--) { ret.push_back(matrix[i][j]); } ++offset; ++start_x; ++start_y; } if(min(row,col)%2 == 1) //如果是奇数,就必定剩下某一行或者某一列需要处理 { if(row > col) //此时剩下一列没有处理 { int times = row - col + 1; //剩下的元素个数 int i=start_x; while(times--) { ret.push_back(matrix[i++][start_y]); } } else { int times = col - row + 1; //剩下的元素个数 int j=start_y; while(times--) { ret.push_back(matrix[start_x][j++]); } } } return ret; } };