刷题笔记–数组专项
2022/4/25
一、二分查找
leetcode链接: https://leetcode-cn.com/problems/binary-search/
使用二分法的前提条件:
- 数组为有序数组;
- 数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下标可能不是唯一的;
二分查找涉及很多的边界条件,首先要确定边界条件。
区间的定义就是不变量。要在二分查找的过程中,保持不变量,就是在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
- right = nums.length - 1; right的值要赋值为数组长度减一
class Solution {
public int search(int[] nums, int target) {
// 避免当 target 小于nums[0] nums[nums.length - 1]时多次循环运算
if (target < nums[0] || target > nums[nums.length - 1]) {
return -1;
}
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + ((right - left) >> 1);
if (nums[mid] == target)
return mid;
else if (nums[mid] < target)
left = mid + 1;
else if (nums[mid] > target)
right = mid - 1;
}
return -1;
}
}
二、移除元素
leetcode链接: https://leetcode-cn.com/problems/remove-element/submissions/
1、暴力解法:双层for循环
一个for循环遍历数组元素 ,第二个for循环更新数组。
但是需要注意的是在循环移动的时候需要动态改变 i 和 size 的值,因为数组被更新了,防止出现连续等于val值的情况。
//暴力解法
int size=nums.length;
for(int i=0;i<size;i++){
if(nums[i]==val){
for(int j=i;j<nums.length-1;j++)
{
nums[j]=nums[j+1];
}
i--;
size--;
}
}
return size;
2、双指针法
双指针法(快慢指针法): 通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。
以快指针作为 for 循环的终止条件;,每次循环快指针的值都加一;
初始化的时候 快慢指针都指向 0 ,在for循环遍历数组的时候,如果数组值等于指定的val值,则什么也不做;
如果值不等于 val 值,则让慢指针加一,并且把快指针的值覆盖慢指针的值,如果没有相等的元素,那么快慢指针指向相同的值,如果出现了相等的元素,会导致慢指针速度比快指针低,直到遍历完成整个数组。
当出现连续的值的时候 比如3 2 2 3 删除 2 ;因为判断的是fast所在的值,因此遇见连续的会fast会继续往下走,slow依旧不动。
并且最后 slow 指针所指向的值就是数组去掉相等值之后的个数。
class Solution {
public int removeElement(int[] nums, int val) {
int fast=0;
int slow=0;
for(fast=0;fast<nums.length;fast++){
if(nums[fast]==val){
}else{
nums[slow]=nums[fast];
slow++;
}
}
return slow;
}
}
三、有序数组的平方
leetcode链接: https://programmercarl.com/0977.%E6%9C%89%E5%BA%8F%E6%95%B0%E7%BB%84%E7%9A%84%E5%B9%B3%E6%96%B9.html
1、暴力解法:
class Solution {
public int[] sortedSquares(int[] nums) {
//暴力解法:平方后排序
for(int i=0;i<nums.length;i++){
nums[i]=nums[i]*nums[i];
}
for(int i=0;i<nums.length;i++){
for(int j=i+1;j<nums.length;j++){
if(nums[i]>nums[j]){
int temp=nums[i];
nums[i]=nums[j];
nums[j]=temp;
}
}
}
return nums;
}
}
2、双指针法
重点:数组其实是有序的, 只不过负数平方之后可能成为最大数了。
那么数组平方的最大值就在数组的两端,不是最左边就是最右边,不可能是中间。
此时可以考虑双指针法了,i指向起始位置,j指向终止位置。
思路:
因此可以考虑使用新数组来保存平方过后的数组值,从最左边和最右边开始比较,当左边值平方大于右边值平方时,说明左边值比所有的值都大,因此放到新数组的最右边,当左边小于右边的时候,右边的值此时为最大,加入到新数组的当前size值指向的位置即可。每次加入后,size值–。
class Solution {
public int[] sortedSquares(int[] nums) {
//使用双指针和新的数组
int size=nums.length;
int left=0;
int right=size-1;
int[] arr=new int[size];
//数组有序
while(left<=right){
int l=nums[left]*nums[left];
int r=nums[right]*nums[right];
if(l>=r){
arr[size-1]=l;
left++;
}else{
arr[size-1]=r;
right--;
}
size--;
}
return arr;
}
}
2022/5/3
四、长度最小的子数组
leetcode链接: https://leetcode-cn.com/problems/minimum-size-subarray-sum/submissions/
1、暴力解法:双重for循环
- 双层for循环 相当于把每一个元素的可能都列出来;
- 每次循环需要重置sum的值;
- 如果当前长度比之前的长度小就赋值给结果;
- 等于10000说明没有连续子数组;
class Solution {
public int minSubArrayLen(int target, int[] nums) {
//双层for循环 相当于把每一个元素的可能都列出来
int result=10000;
int sum=0;
int count=0;
for(int i=0;i<nums.length;i++){
sum=0; //每次循环需要重置sum的值
for(int j=i;j<nums.length;j++){
sum+=nums[j];
if(sum>=target){
count=j-i+1;
//如果当前长度比之前的长度小就赋值给结果
result=result<count?result:count;
break; //只需要记录当前循环的长度最小值
}
}
}
//等于10000说明没有连续子数组
return result==10000?0:result;
}
}
2、滑动窗口(双指针)
所谓滑动窗口,就是不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果。
- 窗口就是 满足其和 ≥ s 的长度最小的 连续 子数组。
- 窗口的起始位置如何移动:如果当前窗口的值大于s了,窗口就要向前移动了(也就是该缩小了)。
- 窗口的结束位置如何移动:窗口的结束位置就是遍历数组的指针,窗口的起始位置设置为数组的起始位置就可以了。
时间复杂度是O(n)
不要以为for里放一个while就以为是O(n^2)啊, 主要是看每一个元素被操作的次数,每个元素在滑动窗后进来操作一次,出去操作一次,每个元素都是被被操作两次,所以时间复杂度是 2 × n 也就是O(n)。
理解:
当和大于目标值的时候,此时就需要移动窗口的起始位置了,因为如果不移动,继续向右移动的话,其和必定会大于目标值而且其**长度肯定会大于第一次和大于目标值时的长度,**因此需要不断调整窗口的起始位置。
当窗口内数据的和小于目标值的时候,此时就不需要再继续移动初始值了,因为继续移动只会使得值越来越小。
总的来说,当窗口内数据的值大于目标值的时候,窗口的左边至少需要移动一次。
class Solution {
public int minSubArrayLen(int target, int[] nums) {
//滑动窗口
int left=0;
int right=0;
int result=Integer.MAX_VALUE;
int tempCount=0; //临时计数
int sum=0;
//以窗口的右边指针为循环条件,遍历数组
for(;right<nums.length;right++){
sum+=nums[right];
while(sum>=target){
tempCount=right-left+1;
result=result<tempCount?result:tempCount;
sum-=nums[left++]; //先减去左指针的数组值 再让左指针减一
}
}
return result==Integer.MAX_VALUE?0:result;
}
}
五、螺旋矩阵 II
leetcode链接: https://leetcode-cn.com/problems/spiral-matrix-ii/
按「圈」进行构建。
宫水三叶大佬的题解:
https://leetcode-cn.com/problems/spiral-matrix-ii/solution/yi-ti-shuang-jie-xiang-jie-xing-zhuang-j-24x8/
使用「左上角」(x1,y1)(x1,y1) &「右下角」(x2,y2)(x2,y2) 来确定某个「圈」,进行构建。
完成后,令「左上角」&「右下角」往里收,使得 (x1+1,y1+1) 和 ((x2−1,y2−1),执行相同的构建规则。
看来几个题解,感觉还是三叶大佬的这个方法比较容易实现,也容易理解。
- 与之不同的是我并没有使用递归,而是判断当前值是否小于等于n平方作为while循环的条件,每次循环把圈缩小,即x1 + 1, y1 + 1,x2 - 1, y2 - 1。
- 每次循环的时候需要判断x1>x2||y1>y2,当x1>x2时可以说明圈已经缩小到最小了,即圈中只有一个元素;再小的时候就返回空,跳出循环;
- 同时每条边判断的时候采用坚持 左闭右开的原则,即每一个边的最后一个元素的值不包含在本次循环写入中,切忌不能一会左闭右开,一会左闭右闭;坚持一个原则;
- for(int i=y1;i<y2;i++) 其次发现一个问题,当n为奇数的时候,即最后一个圈一个元素时x1==x2,此时因为左闭右开的原则,无法写入最后一个值,因此加入了一个判断 if(x1==x2){arr[x1][y1]=n*n;}
class Solution {
public int[][] generateMatrix(int n) {
//定义坐标画圈求解
int[][] arr=new int[n][n];
circle(0,0,n-1,n-1,n,arr);
return arr;
}
public void circle(int x1,int y1,int x2,int y2,int n,int[][] arr){
int val=1; //保存当前的值
int size=n*n;
while(val<=size){
if(x1>x2||y1>y2){ //说明此时模拟已经结束
return ;
}
if(x1==x2){
arr[x1][y1]=n*n;
return;
}
//实行左闭右开
//从左到右
for(int i=y1;i<y2;i++) arr[x1][i]=val++;
//从上到下
for(int i=x1;i<x2;i++) arr[i][y2]=val++;
//从右到左
for(int i=y2;i>y1;i--) arr[x2][i]=val++;
//从下到上
for(int i=x2;i>x1;i--) arr[i][y1]=val++;
x1++;y1++;
x2--;y2--;
}
}
}