文章来源:代码随想录 数组基础
- 数组下标都是从0开始的。
- 数组内存空间的地址是连续的
704.二分查找
做题记录
太久没有写java,看到题目第一反应是使用indexof()进行target的提取,但是在实行过程中发现java的indexof()是String的method,并且范例中有负数的存在,因此该方法不可行。
后来又仔细审题发现题目规定数组是有序升序数组且无重复元素,因此想到使用二分法进行查找。
代码实现过程中的问题
凭借记忆中的二分法逻辑完成以下代码,但是不断出现超出时间限制的error,因此开始debug原因。
int left = 0;
int right = nums.length;
while (left < right){
int mid = (left + right) / 2;
if (nums[mid] == target){
return mid;
}else if(nums[mid] > target){
right = mid;
}else{
left = mid;
}
}
return -1;
超出时间限制主要推断为while进入死循环,因此尝试进行更改left < right为left <= right以及使用System.out.println()打印出left和right的变动,最终更改失败,转向观察随想录中的代码。
文章解题思路
讲解文章链接:704
文章指出实现二分法代码的最重要规则是循环不变量规则,而我这次二分法没有成功实现也是没有深度了解这一规则,对区间的定义(不变量)没有想清楚。该规则具体表现在while寻找中每一次边界的处理都要坚持根据区间的定义来操作,而二分法最主要的区间定义分为两种,左闭右闭即[left, right],或者左闭右开即[left, right)。
方法一:左闭右闭
- while (left <= right) 要使用 <= ,因为left == right是有意义的,所以使用 <=
- if (nums[middle] > target) right 要赋值为 middle - 1,因为当前这个nums[middle]一定不是target,那么接下来要查找的左区间结束下标位置就是 middle - 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;
方法二:左闭右开
- while (left < right),这里使用 < ,因为left == right在区间[left, right)是没有意义的
- if (nums[middle] > target) right 更新为 middle,因为当前nums[middle]不等于target,去左区间继续寻找,而寻找区间是左闭右开区间,所以right更新为middle,即:下一个查询区间不会去比较nums[middle]
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 if (nums[mid] > target)
right = mid;
}
return -1;
归纳和更正
在观察两种方法后,认为原本的代码更加符合左闭右开的形式,比对后发现,问题为当target大于nums[mid], 也就是说要转向右区间查找时,left的更新没有mid+1。更新后的代码如下:
int left = 0;
int right = nums.length;
int cnt = -1;
while (left < right){
int mid = (left + right) / 2;
if (nums[mid] == target){
cnt = mid;
break;
}else if(nums[mid] > target){
right = mid;
}else{
left = mid + 1;
}
}
return cnt;
疑问和思考
-
开和闭到底是什么意思?为什么right闭开一个要做-1,而另一个不做?
这个问题阻碍了我对二分法代码实现的一个理解和记忆,后面我对这个问题进行更深入的了解。结论如下,闭的意思为在每次搜索的过程中,闭合的值也会被考虑进搜索范围,而开则是不会被考虑进搜索范围的那一个。因此在更新right时,如果mid被确认不是target,那么在闭的情况下mid就要被剔除(mid-1),在开的情况下,因为开的数本身不会考虑进搜索范围,因此直接赋值mid。而对于left而言,因为left的初始值始终为0,是搜索区间的一部分,因此被看作闭,且每次更新都要将无关的mid给剔除(mid+1)
27.移除元素
做题记录
第一次看到题目认为这两点比较重要:
- 原地更改
- 更改
nums数组,使nums的前k个元素包含不等于val的元素。nums的其余元素和nums的大小并不重要。
因此初步设想为顺序遍历数组,如果发现val则遍历后面的元素将所有不是val的元素提上来进行覆盖,Leetcode显示为通过,但时间复杂度为。
int cnt = 0;
for(int i=0;i<nums.length;i++){
if (nums[i] == val){
for(int j=i+1;j<nums.length;j++){
if(nums[j] != val){
nums[i] = nums[j];
i++;
cnt++;
}
}
return cnt;
}else{
cnt++;
}
}
return cnt;
文章解题思路
讲解文章链接:27
1. 双指针法*(重点需要掌握)
双指针法(快慢指针法): 通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。
定义快慢指针:
- 快指针:寻找新数组的元素 ,新数组就是不含有目标元素的数组
- 慢指针:指向更新 新数组下标的位置
双指针法(快慢指针法)在数组和链表的操作中是非常常见的,很多考察数组、链表、字符串等操作的面试题,都使用双指针法。
// 快慢指针
int slowIndex = 0;
for (int fastIndex = 0; fastIndex < nums.length; fastIndex++) {
if (nums[fastIndex] != val) {
nums[slowIndex] = nums[fastIndex];
slowIndex++;
}
}
return slowIndex;
观察后发现,这里主要是在i的遍历基础上增加了一个变量用于定位覆盖的元素index,因此可根据我本身的代码基础上进行修改。
int pos = 0;
for(int i=0;i<nums.length;i++){
if (nums[i] == val){
continue;
}else{
nums[pos] = nums[i];
pos++;
}
}
return pos;
2. 相向双指针法
版本1:
int left = 0;
int right = nums.length - 1;
while(right >= 0 && nums[right] == val) right--; //将right移到从右数第一个值不为val的位置
while(left <= right) {
if(nums[left] == val) { //left位置的元素需要移除
//将right位置的元素移到left(覆盖),right位置移除
nums[left] = nums[right];
right--;
}
left++;
while(right >= 0 && nums[right] == val) right--;
}
return left;
版本2:
int left = 0;
int right = nums.length - 1;
while(left <= right){
if(nums[left] == val){
nums[left] = nums[right];
right--;
}else {
// 这里兼容了right指针指向的值与val相等的情况
left++;
}
}
return left;
归纳和思考
在这一题中,掌握和理解双指针法尤其重要,因此需要多做会运用到这种方法的题目。从文章摘抄出相关题目推荐如下,后续补充内容以作记录。