算法训练营DAY01 | 数组基础、704. 二分查找、27. 移除元素

112 阅读6分钟

文章来源:代码随想录 数组基础

  • 数组下标都是从0开始的。
  • 数组内存空间的地址是连续的

704.二分查找

Leetcode链接

做题记录

太久没有写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 < rightleft <= 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;

疑问和思考

  1. 开和闭到底是什么意思?为什么right闭开一个要做-1,而另一个不做?

    这个问题阻碍了我对二分法代码实现的一个理解和记忆,后面我对这个问题进行更深入的了解。结论如下,闭的意思为在每次搜索的过程中,闭合的值也会被考虑进搜索范围,而开则是不会被考虑进搜索范围的那一个。因此在更新right时,如果mid被确认不是target,那么在闭的情况下mid就要被剔除(mid-1),在开的情况下,因为开的数本身不会考虑进搜索范围,因此直接赋值mid。而对于left而言,因为left的初始值始终为0,是搜索区间的一部分,因此被看作闭,且每次更新都要将无关的mid给剔除(mid+1)

27.移除元素

Leetcode链接

做题记录

第一次看到题目认为这两点比较重要:

  • 原地更改
  • 更改 nums 数组,使 nums 的前 k 个元素包含不等于 val 的元素。nums 的其余元素和 nums 的大小并不重要

因此初步设想为顺序遍历数组,如果发现val则遍历后面的元素将所有不是val的元素提上来进行覆盖,Leetcode显示为通过,但时间复杂度为O(N2)O(N^2)

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;

归纳和思考

在这一题中,掌握和理解双指针法尤其重要,因此需要多做会运用到这种方法的题目。从文章摘抄出相关题目推荐如下,后续补充内容以作记录。