Day 1&二分查找,移除元素

Day 1

1.二分查找

题目

给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。

两种写法:两种写法的区别主要是在于查找区间的不同。

第一种查找区间为左闭右闭,即[left , right],这就决定了后面的while判断里的条件为while(left <= right),因为当left = right时,在左闭右闭的查找区间里是有意义的。同时当区间缩小时,即middle的值大于target目标值时,middle已经确实不是需要查找的值,这说明要进一步查找左区间,由于左闭右闭,这时候right应该更新为middle-1。如果不这么做,可能导致两个问题:(1)查找次数变多,效率降低 (2)更严重的,当出现特殊情况时,即数组元素为[1,2],target为2,第一次查找middle指向的值为1 < target ,此时如果right更新为middle = 0时,会陷入死循环。

第二种查找区间为左闭右开,此时不同的是right的初始位置应该是数组最后一个元素的下一个位置,即right = num.length,while判断里面的条件就变成了while(left < right),当区间缩小时,即middle的值大于target目标值时,middle已经确实不是需要查找的值,这说明要进一步查找左区间,由于左闭右开,这时候right应该更新为middle。

//左闭右闭
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;
    }
}
//左闭右开
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 if (nums[mid] > target)
                right = mid;
        }
        return -1;
    }
}

几个注意点

  • 1.mid = left + (right - left) / 2,之所以不使用mid = (left + right)/2,是因为当left和right都接近数据范围时,相加会出现溢出的情况。
  • 2.注意两种查找区间代码写法的区别(边界初始条件,判断条件,更新边界条件)。
相关题目
1.搜索插入位置

题目:给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

你可以假设数组中无重复元素。

二分法:本题关键在于定义一个什么样的返回值,可以准确返回插入位置。

分析:

情况1.目标值在数组中,返回mid即可;

情况2.目标值不在数组中,并且小于数组最小值,返回0;

情况3.目标值不在数组中,并且大于数组最大值,返回nums.length;

情况4.目标值不在数组中,并且值属于该数组区间内。

==对情况234进行分析归纳:==很容易想象,经过每次while循环,都会使得[left,right]的左闭右闭区间中的元素减少。

那就有一个问题,减少到最后会是什么情况?

那就是进入最后一次while循环前,[left,right]的左闭右闭区间中只有一个或者两个元素,即

left和right的位置有且仅有2种情况

  • left = right

  • left = right -1

为什么? 举几个特例

  • 数组有一个元素,那么left = right,还需要最后一次while循环
  • 数组有两个元素,那么left = right -1, 分两种情况,需要一或两次while循环
    1. 比如数组 [1,3] ,target=0,此时left = right -1, 然后还需最后一次while
    2. 比如数组 [1,3] ,target=2,一次while循环后,left = right =1,并且变成了一个元素的情况,还要一次while,一共是2个while
  • 数组有三个,四个元素等等,都以此类推,最后都会变成以上的情况

综上,数组最后会变为一个元素,或者两个元素中的情况1

以上就解释清楚了,在进入最后一次while循环前,数组会变为一个或两个元素。

以下是一个元素和两个元素(情况1)的代码注释

  1. 数组只有一个元素,假设nums=[1],left = right

          初始化left = rigth=0
          while(left < =right){         //进入最后一次循环
               int mid = (left + right) / 2;         //mid =0, 此时`left = mid = right=0`
               if (target == nums[mid]) {          
                   return mid;        //若找到,直接返回mid,下面两个else是没有找到target的情况
               } else if (target < nums[mid]) {   
                   right = mid - 1;  //此时数组中仅剩的最后一个数比target大,执行right=mid-1=-1, 而left=0正是要插入的位置
               } else {
                   left = mid + 1;    //此时数组中仅剩的最后一个数比target小,执行left =mid+1=1,  left=1正是要插入的位置
               }
           }//循环结束
    
  2. 数组有两个元素(情况1),假设nums=[1,3],target =0, left = right -1

      初始化left = 0, rigth=1
      while(left < =right){         //进入最后一次循环
           int mid = (left + right) / 2;         //mid =0, 此时`left = mid =0 , right =1 `
           if (target == nums[mid]) {          
               return mid;        //若找到,直接返回mid,下面两个else是没有找到target的情况
           } else if (target < nums[mid]) {   
               right = mid - 1;  //此时target < nums[0],执行right=mid-1=-1, 而left=0正是要插入的位置
           } else {
               left = mid + 1;    //此时target > nums[0],执行left =mid+1=1,  left=1正是要插入的位置
           }
       }//循环结束

综上,除了目标值在数组中返回mid,其他情况返回left即可。

class Solution {
    public int searchInsert(int[] nums, int target) {
        
        int left = 0;
        int right = nums.length-1;
        while(left <= right){
            int mid = left+(right-left)/2;
            if(target<nums[mid]){
                right = mid-1;
            }else if(target > nums[mid]){
                left = mid+1;
            }else if(target == nums[mid]){
                return mid;
            }
        }return left;
 }

2.移除元素

题目

给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。

不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并原地修改输入数组。

元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

示例 1: 给定 nums = [3,2,2,3], val = 3, 函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。 你不需要考虑数组中超出新长度后面的元素。

示例 2: 给定 nums = [0,1,2,2,3,0,4,2], val = 2, 函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。

因为数组在内存空间中是连续的,因此不能删除数组的元素,只能覆盖。

解法一:两层for循环暴力解法,即先执行外循环遍历数组元素,查找是否有等于val的元素,如果找到了,那就执行内循环,将这个元素之后的每个元素都赋给前一个元素,最终的效果就是除了这个等于val的元素,其余元素都向前移动一位,数组容量减一。如下图所示。

27.移除元素-暴力解法

//两层for循环暴力求解
class Solution {
    public int removeElement(int[] nums, int val) {
        int size = nums.length;
        for(int i = 0;i < size;i++){
            if(nums[i] == val){
                for(int j =i+1;j< size; j++ ){
                    nums[j-1] = nums[j];
                }i--;size--;
            }
        }return size;
    }
}

几个注意点

  • 1.为什么要定义一个size变量:因为要最终返回有效数组的容量,在本题中,实际上数组的容量是不变的,但是我们通过往前覆盖的方式,每覆盖一次,相当于去除一个无效值,即实际数组容量-1 = 有效数据容量;另外,在for循环的终止条件中,如果不是使用动态的size,而是使用nums.length的话,会出错,因为每覆盖一次,需要处理的数就少一个,就需要更新size值。这也解释了size–的意义。
  • 2.为什么要执行 i-- ,当执行一次覆盖的时候,比如说在下标为2的位置找到了等于val的元素,执行完覆盖以后,原本下标为3的元素就到了下标为2的位置,此时因为第一个for循环的i++,如果不执行i–,i 就会变成3,这样就直接判断原本下标为4的元素了,而原本下标为3的元素就会漏掉,所以要执行i–去抵消i++,这样才会对原本下标为3的元素进行判断。

解法2:双指针(快慢指针)法 【重在理解】

定义快慢指针

  • 快指针:寻找新数组的元素 ,新数组就是不含有目标元素的数组

  • 慢指针:指向更新 新数组下标的位置

    初始时快慢指针都在数组起点,由快指针的移动带动慢指针的移动,即:如果快指针指向的元素是我们需要的元素,慢指针也向前移动一步,同时该元素后面的元素都向前覆盖一位,如果快指针指向的元素是不需要的,慢指针不动,当快指针遍历完整个数组时,慢指针的索引就是有效数据组的长度,并且从0到慢指针索引位置的元素都是需要的元素。可以理解为快指针寻找需要的元素赋值给慢指针指向的位置。

    如下图所示。

27.移除元素-双指针法

//快慢指针法
class Solution {
    public int removeElement(int[] nums, int val) {
        // 快慢指针
        int slowIndex = 0;
        for (int fastIndex = 0; fastIndex < nums.length; fastIndex++) {
            if (nums[fastIndex] != val) {
                nums[slowIndex] = nums[fastIndex];
                slowIndex++;
            }
        }
        return slowIndex;
    }
}

几个注意点

  • 为什么返回的slowIndex就是有效数组的长度:slow是从0开始的,如果快指针最后一个元素是我们需要的,那就是覆盖以后slowIndex+1,正好是数组容量(slow在+1之前是有效数组的最后一个元素的索引,就是等于数组容量-1);如果不是我们需要的,那就是在上一次覆盖完以后的+1正好变成了数组容量。

解法3:

替换移除法:

  • 主要思路是遍历数组 nums,遍历指针为 i,总长度为 ans
  • 在遍历过程中如果出现数字与需要移除的值不相同时,则 i 自增 1 ,继续下一次遍历
  • 如果相同的时候,则将 nums[i]与nums[ans-1] 交换,即当前数字和数组最后一个数字进行交换,交换后就少了一个元素,故而 ans 自减 1
//替换移除
class Solution {
    public int removeElement(int[] nums, int val) {
        int ans = nums.length;
        for (int i = 0; i < ans;) {
            if (nums[i] == val) {
                nums[i] = nums[ans - 1];
                ans--;
            } else {
                i++;
            }
        }
        return ans;
    }
}
        int ans = nums.length;
        for (int i = 0; i < ans;) {
            if (nums[i] == val) {
                nums[i] = nums[ans - 1];
                ans--;
            } else {
                i++;
            }
        }
        return ans;
    }
}
  • 17
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值