目录
1、题目:977. 有序数组的平方 - 力扣(LeetCode)
1、题目:209. 长度最小的子数组 - 力扣(LeetCode)
1、题目:59. 螺旋矩阵 II - 力扣(LeetCode)
一、数组理论基础
- 定义:数组是存放在连续内存空间上的相同类型数据的集合。可以方便的通过下标索引的方式获取到下标对应的数据(下标从0开始)。
- 在删除或增添元素时,需要移动其他元素的地址,以保证内存地址的连续。
- java中数组的存储方式:
public static void test_arr() {
int[][] arr = {{1, 2, 3}, {3, 4, 5}, {6, 7, 8}, {9,9,9}};
System.out.println(arr[0]);
System.out.println(arr[1]);
System.out.println(arr[2]);
System.out.println(arr[3]);
}
输出的地址为:
[I@7852e922
[I@4e25154f
[I@70dea4e
[I@5c647e05
arr这个二维数组包含4个内部数组,每个内部数组有若干元素。打印的是这些内部数组的哈希码。这些内部数组的内存地址是连续的,但它们在外部数组中的引用可以是分散的,因为它们是独立的数组对象。即二维数组的每一行头结点的地址是没有规则的(每一行内部是连续的,但是每一行随机存储)
二、二分查找
视频课:手把手带你撕出正确的二分法 | 二分查找法 | 二分搜索法 | LeetCode:704. 二分查找_哔哩哔哩_bilibili
1、题目:704. 二分查找 - 力扣(LeetCode)
2、思路
用二分法来查找。
难点1:while()里面left是<right还<=?
难点2:更新区间的时候,xx=middle还是middle-1?
二分法有两种思路:左闭右闭即[left, right],或者左闭右开即[left, right)。
1)左闭右闭:
比如想在下面的数组里面查找2,就先定位到数组中间,比完大小后,此时的区间是左边部分。
- 比如比较后middle>target,已经确定middle不会是目标值了,因为后面是左闭右闭的区间,所以可以直接在对半分的时候,把middle这个值摒除掉,直接把right设为middle-1即可。同样如果middle<targrt的时候,直接把left设为middle+1即可。
- while里面要left<=right:因为比如到left=2,right=3,实际上还有两个数字都没有比较。执行一轮还是没找到之后,right=2,此时left=right,还有一个数字没有比较,还需要再比一轮。
2)左闭右开:
[ ) 所以while里面必须left<right,使得左闭右开有意义,[1,1)是不合法的。
同样道理,更新的时候,因为right是没有包含的。所以在更新左边界的时候,因为左闭,又已经确定了middle不为目标值,所以left=middle+1。在更新右边界的时候因为不会比较right,所以直接right=middle。
- while里面要left<right:因为比如到left=2,right=3,实际上只有一个数字没有比较了。所以此时就是执行的最后一轮,不需要再执行下一轮了。
3、代码
1)左闭右闭的代码
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 { // nums[mid] > target
right = mid - 1; // 即修改右区间的下标,把比较区间放在左边
}
}
// 未找到目标值
return -1;
}
}
2)左闭右开的代码
class Solution {
public int search(int[] nums, int target) {
int left = 0, right = nums.length; //相当于区间右边的下标是无效的
// 这样可以防止二分到最后
while (left < right) {
int mid = left + ((right - left) >> 1); // 同样找到中间下标
if (nums[mid] == target) {
return mid;
}
else if (nums[mid] < target) {
left = mid + 1;
}
else { // nums[mid] > target
right = mid; // 这里不一样,相当于最右侧的下标是开区间
}
}
// 未找到目标值
return -1;
}
}
4、复杂度分析
两种方法的复杂度是一样的。(暴力法的时间复杂度为O(n))
- 时间复杂度:O(log n) 因为每次都是对半分
- 空间复杂度:O(1) 不需要额外存储什么
三、移除元素
视频课:数组中移除元素并不容易! | LeetCode:27. 移除元素_哔哩哔哩_bilibili
1、题目:27. 移除元素 - 力扣(LeetCode)
2、思路
因为数组的元素在内存地址中的是连续的,所以不能单独删除元素,只能覆盖。(其实就是实现erase这个库函数的功能,其实可以直接调用)
1)暴力解法(力扣上也能通过)
- 两层循环:从左到右遍历元素,把每一个不等于目标值的数都赋值给数组的从0开始的每一个顺序位置。(即如果找到了一个target,后面所有的元素就往前覆盖(这个元素就先不管了)
2)双指针法(快慢指针法)
通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。(在一个循环里面就可以做,所以复杂度是O(n))
- 快指针:寻找新数组里所需要的元素(即不等于目标值的元素)。
- 慢指针:指向更新新数组下标的位置。(新数组的下标值)
就是在一个数组上进行操作的。(其实和暴力法差不太多)
3、代码
1)暴力解法
class Solution {
public int removeElement(int[] nums, int val) {
int slowIndex = 0;
// for里面从下标为0开始遍历
for (int fastIndex = 0; fastIndex < nums.length; fastIndex++) {
if (nums[fastIndex] != val) {
// 如果不等于目标值,就将该值赋予给数字更新下标对应的位置
nums[slowIndex] = nums[fastIndex];
slowIndex++; // 即数字更新下标初始为0,然后每次右移一个
}
}
return slowIndex; //最后这个更新下标值为多少,就是有多少个不为目标值的数
}
}
2)双指针法
class Solution {
public int removeElement(int[] nums, int val) {
int slow = 0;
int len = nums.length;
// 快慢指针都是从0开始
for (int fast = 0; fast < len; fast++) // 表示快指针向右移动
if (nums[fast] != val) // 如果数不是目标值
nums[slow++] = nums[fast]; // 就赋予给慢指针所在的位置
// 注意这里慢指针下标会在复制之后之后增加1。
// 当然也可以把在下面额外写slow=slow+1;
return slow;
}
}
4、复杂度分析
- 暴力解法的时间复杂度是O(n^2),空间复杂度是O(1)
- 双指针法的时间复杂度是O(n),空间复杂度是O(1)
四、有序数组的平方
视频课:双指针法经典题目 | LeetCode:977.有序数组的平方_哔哩哔哩_bilibili
1、题目:977. 有序数组的平方 - 力扣(LeetCode)
2、思路
就是要先计算每个数的平方和,再递增排序。
1)暴力排序
简单的每个数平方之后,再用快排排个序。
2)双指针法
因为数组本身其实是递增的,只不过平方后,可能负数的平方和会更大。
- 但是,该数组平方和的最大值一定在数组两端 (一定是最左边或者最右边,不会是中间)
- 所以可以只用反复比较数组最左端和最右端的两个数就好,不断从两边往中间比。
- 就可以有两个指针,一个指向最左边,一个指向最右边。然后左指针右移,右指针左移。
- 再有一个新的数组,来存放每次两端比较后更大的数(从右往左放)。
3、代码
1)暴力法
class Solution {
public int[] sortedSquares(int[] nums) {
int[] result = new int[nums.length];
for (int i = 0; i < nums.length; i++){
result[i]=nums[i]*nums[i];
}
Arrays.sort(result); //用Arrays.sort方法对result数组进行排序
return result;
}
}
2)双指针法
class Solution {
public int[] sortedSquares(int[] nums) {
int l = 0; //最左边下标
int r = nums.length - 1; //最右边下标
int[] res = new int[nums.length]; //将返回的数组result[],指定长度
int j = nums.length - 1;
while(l <= r){ //要小于等于,不然最后一个元素就掉了
if(nums[l] * nums[l] > nums[r] * nums[r]){
res[j--] = nums[l] * nums[l++];
//但凡左边界的数大于右边界的数,就把左边界数的平方和放到新数组的最右边,
//然后把新数组的下标左移一个。(原始的左边界指针下标右移)
}else{
res[j--] = nums[r] * nums[r--];
}
//同样的如果有边界的平方和比左边界更大,就把有边界的平方和给新数组最右侧
//然后把新数组的下标左移一个。(原始的右边界指针下标左移)
}
return res;
}
}
4、复杂度分析
- 暴力法:时间复杂度是 O(n + nlogn)
- 1、平方操作:O(n),对每个元素进行平方。
- 2、排序操作:Arrays.sort(result)的时间复杂度为 O(n log n)。
- 双指针法:时间复杂度为O(n)
while
循环从数组的两端开始,向中间遍历,直到l
和r
相遇。这个过程只需要遍历数组一次,因此是线性时间的。- 在每次迭代中,算法计算
nums[l]
或nums[r]
的平方,并将结果放入res
数组的相应位置。比较操作是常数时间的,而赋值操作也是常数时间的。
五、长度最小的子数组
视频课:拿下滑动窗口! | LeetCode 209 长度最小的子数组_哔哩哔哩_bilibili
1、题目:209. 长度最小的子数组 - 力扣(LeetCode)
2、思路
1)暴力法
两个for循环,不断寻找符合条件的子序列。(把所有子序列的情况都列出来了)
2)滑动窗口
- 滑动窗口:就是不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果。
- 相当于暴力法中一个for循环是滑动窗口的起始位置,一个for循环是滑动窗口的终止位置
- 滑动窗口用1个for循环就可以解决。
- 如下图其实也类似于双指针法:不过更像滑动窗口。
- 一个指针表示子序列的终止位置,从左向右移动:
- 窗口的结束位置如何移动:窗口的结束位置就是遍历数组的指针,也就是for循环里的索引。(相当于从左到右一个个移动)
- 窗口的起始位置如何移动:如果当前窗口的值大于等于s了,窗口就要向前移动了(也就是该缩小了)。(相当于从左不断逼近结束位置,缩小子序列长度,直到和小于s了,结束位置就要前移了)比如下图s=7
- 相当于最左边先囊括一个满足条件的序列,再从左压缩序列长度。直到不满足条件后,再往右边扩序列,直到满足条件,再压缩左边界。
- 一个指针表示子序列的终止位置,从左向右移动:
- 滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。
3、代码
1)暴力解法:
class Solution {
public int minSubArrayLen(int s, int[] nums) {
int sum = 0;
int result = Integer.MAX_VALUE; //初始赋予一个最大值
int sublength = 0; // // 子序列的长度
for (int i = 0; i < nums.length; i++) { //设置子序列起点为i
sum = 0;
for (int j=i; j<nums.length; j++){ //设置子序列终止位置为j
sum += nums[j];
if(sum>=s){ // 一旦发现子序列和超过了s,更新result
sublength = j-i+1; // 取子序列的长度
result = (result < sublength ? result : sublength);
break;
}
}
}
return result == Integer.MAX_VALUE ? 0 : result;
}
}
2)滑动窗口法
class Solution {
// 滑动窗口
public int minSubArrayLen(int s, int[] nums) {
int left = 0; //子序列七十位置指针
int sum = 0;
int result = Integer.MAX_VALUE; //初始赋予一个最大值
for (int right = 0; right < nums.length; right++) { //子序列终止位置指针从左向右移
sum += nums[right];
while (sum >= s) { //但凡目前的子序列和>s,就右移起始位置指针
result = Math.min(result, right - left + 1); // 比较计算出最小的序列长度
sum -= nums[left++]; //右移起始指针
}
}
return result == Integer.MAX_VALUE ? 0 : result;
}
}
4、复杂度分析
- 暴力法:时间复杂度很明显是O(n^2)。空间复杂度:O(1)
- 滑动窗口:时间复杂度:O(2n)=O(n),空间复杂度:O(1)
六、螺旋矩阵Ⅱ(面试高频)
视频课:一入循环深似海 | LeetCode:59.螺旋矩阵II_哔哩哔哩_bilibili
1、题目:59. 螺旋矩阵 II - 力扣(LeetCode)
2、思路
给定n,生成的矩阵就是n*n的,数字就是从1~n^2螺旋排列的。重点就是要怎么去填充这个矩阵。如下图,顺时针去填充这个矩阵:
- 关键点其实就是边界上的几个点。所以写边界条件的时候会比较麻烦。这里选择秉持“左闭右开”这样一个固定的原则,去填充每一条边。(每一圈顺序填充4条边)
- 首先是转几圈的问题:转n/2圈,如果n是偶数,几整圈就可以全部填充,如果是奇数,那么到最后最中心的位置就要自己再额外处理一下。
- 然后是起始位置:因为每一圈的起始位置都不是固定的(不能每一圈一开始就i=0),所以要定义起始位置startX(从左到右的起始)、startY(从上到下的起始),都从0开始。
- 然后是终止位置:因为要左闭右开,所以不能包含终止位置(每一圈都会改变),第一圈是n-1,第二圈是n-2,所以还需要一个变量offset。
- 写代码的时候,4条边怎么填充,都要具体细心的分析(其实不难,画图慢慢分析)
3、代码
class Solution {
public int[][] generateMatrix(int n) {
int[][] nums = new int[n][n]; //矩阵是n^2的大小
int startX = 0, startY = 0; // 每一圈的起始点
int offset = 1;
int count = 1; // 矩阵中需要填写的数字(从1开始)
int loop = 1; // 记录当前的圈数
int i, j; // j 代表列, i 代表行;
while (loop <= n / 2) { // 比如n=3,循环1圈就行
//每一圈,都按照顶部、右列、底部、左列的顺序去填充
// 顶部
// 左闭右开,所以判断循环结束时, j 不能等于 n - offset
for (j = startY; j < n - offset; j++) { //j=0、1
nums[startX][j] = count++; // 填充矩阵的顶部
} // 需要填写的数字count要一直自增
// 右列
// 左闭右开,所以判断循环结束时, i 不能等于 n - offset
for (i = startX; i < n - offset; i++) {
nums[i][j] = count++; // 固定列j不变,增加i来向下填充
}
// 底部
// 左闭右开,所以判断循环结束时, j != startY
for (; j > startY; j--) { //不需要对j进行初始化了
nums[i][j] = count++; // 向左填充,所以列j--
}
// 左列
// 左闭右开,所以判断循环结束时, i != startX
for (; i > startX; i--) {
nums[i][j] = count++;
}
// 一圈结束后,如果要进行下一圈,起始行要向右移1个,起始列也要向下移1个
startX++;
startY++;
offset++; // 下一圈左闭右开时边界值也要距离矩阵边界远1格
loop++; // 记录圈数,是while里的判断
}
// n为奇数的时候,单独处理中心位置 (循环到最后[startX][startY]就是中心位置)
if (n % 2 == 1) {
nums[startX][startY] = count;
}
return nums;
}
}
4、复杂度分析
- 时间复杂度 O(n^2): 模拟遍历二维矩阵的时间(要填充多少个数)
- 空间复杂度 O(1)
写在最后的话:java里面什么时候用for什么时候用while?
- while():括号里面跟条件判断,比如left<right
- for():括号里面用来递增一个值,比如for(i=0;1<10;1++)