数组基础+常用方法
数组是存放在连续内存空间上的相同类型数据的集合。
- 数组下标都是从0开始的。 [0,length-1]
正是因为数组的在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。
数组的元素是不能删的,只能覆盖。
要注意:Java中二维数组地址 每一行之间都不是连续的
常用方法:
获取数组长度:数组名.length
704.二分查找
文章链接:https://programmercarl.com/0704.%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE.html#%E7%AE%97%E6%B3%95%E5%85%AC%E5%BC%80%E8%AF%BE
视频链接:https://www.bilibili.com/video/BV1fA4y1o715/?spm_id_from=333.1007.top_right_bar_window_custom_collection.content.click&vd_source=80cf8293f27c076730af6c32ceeb2689
第一想法:题目给了升序的数组nums,且元素不重复,以及目标值target。用二分,while循环,不断缩小区间范围。但区间的开闭和while条件不太明确。
思路讲解
二分法条件:有序数组+无重复元素
关键:
-
while(left ? right) 是<还是≤
-
更新下标是 = middle,还是 = middle-1?
一般写二分法的时候,要么左闭右闭[left,right]
,要么左闭右开[left,right]
。对区间的定义会影响到边界处理!
区间的定义要作为循环不变量,贯穿整个代码。
左闭右闭写法
left = 0,right = nums.length
- 接下来先看while条件有没有
=
首先要明确:能进入到while循环里的,一定是个合法的区间(符合左闭右闭的定义)
left=right的情况合不合法呢?[left,left]
是合法的。所以应该是 ≤
mid = (left + right) / 2
计算出mid值后,一共有三种情况:
nums[mid] < target
mid下标的值小于target,说明target在mid右侧,缩小到右半区间,更新leftnums[mid] > target
mid下标的值小于target,说明target在mid右侧,缩小到右半区间,更新rightnums[mid] = target
mid下标就是我们要找的值,直接return
第一种情况,nums[mid]已经不是要找的值了,新的区间里不能包含它。结合左闭右闭条件,新的right=mid-1
其他两种情况同理。
- 如果退出while了都还没找到,则返回-1
- 左闭右闭完整代码:
class Solution {
public int search(int[] nums, int target) {
int mid;
int l = 0,r = nums.length-1;
while(l <= r) {
mid = (l + r) / 2;
if(nums[mid] > target) r = mid - 1;
else if(nums[mid] < target) l = mid + 1;
else return mid;
}
return -1;
}
}
左闭右开写法
[left, right)
left = 0,right = nums.length 这样才是[0,nums.length)
- 首先我们先考虑while条件里有没有等号?
能进到while里的一定是合法的。考虑left=right的情况 [left,left)
显然是不合法的!
因此是while(left < right)
-
接着考虑如何更新下标
-
如果是
nums[mid] < target
的情况,要更新left。nums[mid]不是我们要的值,下一次循环里不能包含它。假设是
left = mid + 1
的话,新的区间就是[mid + 1,right)
。mid不包含在新区间里。因此这种情况应该是
left = mid + 1
。 -
如果是
nums[mid] < right
的情况,要更新right。假设是
right = mid + 1
,新的区间就是[left,mid - 1)
,实际上多排除了一个mid-1,这是不对的。因此这种情况应该是
right = mid
。
-
-
左闭右开完整代码:
class Solution {
public int search(int[] nums, int target) {
int mid;
int left = 0,right = nums.length;
while(left < right) {
mid = (left + right) / 2;
if(nums[mid] > target) right = mid;
else if(nums[mid] < target) left = mid + 1;
else return mid;
}
return -1;
}
}
遇到的问题
左闭右开情况下,我第一次left和right还是写成0和nums.length-1,运行后报错
例如在数组只有一个元素的情况下,如[5]
,这样结果是-1.
因此==左闭右开要写成right=nums.length,[0,nums.length) 这样才能包含所有元素!==
27.移除元素
文章链接:https://programmercarl.com/0027.%E7%A7%BB%E9%99%A4%E5%85%83%E7%B4%A0.html#%E7%AE%97%E6%B3%95%E5%85%AC%E5%BC%80%E8%AF%BE
视频链接:https://www.bilibili.com/video/BV12A4y1Z7LP/spm_id_from=333.788&vd_source=80cf8293f27c076730af6c32ceeb2689
第一想法:第一眼没仔细看题目,以为是升序数组,想着用双指针,但其实是无序的。再次考虑想法是,用标志数组,遍历到一个位置是val,就把标志数组当前位置设为空。依次往前填补,同时记录剩余元素个数。但感觉实现不了
数组理论基础
数组并不能删除一个位置上的元素,只能覆盖。新数组在内存里的空间还是原来的(虽然length变了)
其实数组的remove方法是O(n),不是O(1)!,是要将后面的元素整体往前移的
- 对于库函数的使用:如果能直接用库函数解决问题,就不要用!
暴力法
两层for循环,第一层找到val,第二层把它后面的元素一个个往前覆盖掉
O(n^2)
双指针思路
用双指针可以O(n)解决,实际用一层for做了两层for的事情
- 定义一个快指针fast,一个慢指针slow
快指针负责指向新数组里需要的元素(val以外的)
慢指针负责指向新数组的下标(让快指针找到的元素填进去)
slow = 0
fast指针要从0遍历到数组末尾,每找到一个≠ val
的元素就把它赋给nums[slow]
,同时slow++
- 最终直接
return slow
就是新数组的长度了。(这样想:fast每赋值一次给slow,slow++。一共有slow个值)
要真正理解快慢指针各自的含义!
- Java代码
class Solution {
public int removeElement(int[] nums, int val) {
int slow = 0;
int size = 0;
for(int fast = 0; fast < nums.length; fast++) {
if(nums[fast] != val) {
nums[slow] = nums[fast];
slow++;
}
}
return slow;
}
}
个人体会
双指针法其实还是从暴力两层for出发,只不过把第二层for里,后面元素全部往前移动的操作,简化成O(1)的赋值操作。因为有了slow指针统计个数的同时指向新的数组,就不需要往前移了,fast直接赋值给当前位置。
今日总结
- 学习时长:2小时
- 题目难度:还好
今天第一天主要是熟悉刷题的感受,学习了二分法左闭右闭和左闭右开两种写法。(以后自己写还是用左闭右闭,左闭右开要多考虑一点 比较麻烦)。双指针每次听完讲解都恍然大悟,以后自己写又不回了,要多积累双指针能运用的题型。