数组之二分&LeetCode27.移除数组-JavaScript超好懂😍

120 阅读3分钟

二分法

手撕二分

二分就是有序区间内找值,先一分为2比较中间值和目标值判断下一个二分的区间,再二分,直到中间值等于目标值
704. 二分查找:在长度为n升序的整型数组numstarget,找到返回下标,没找到返回-1
数组有序,且无重复元素,(因为一旦有重复元素,使用二分查找法返回的元素下标可能不是唯一的,这些都是使用二分法的前提条件)
二分两种区间:

  • [l , r]左闭右闭
  • [l , r)左闭右开

首先我们先弄清,这个区间的含义是:目标值target可能在的区间

  1. 左闭右闭(确定好后,全程就得保持区间不变)

先设左右初始值l = 0, r = n - 1满足[l, r],进入while循环,l = r满足区间=>循环条件while(l <= r)
image.png会有以下三种情况:

  • nums[mid] > target时,mid显然不在含目标值的[l, r]内=>r = mid - 1
  • nums[mid] < target时,同理l = mid + 1
  • nums[mid] == target即找到目标值,直接返回
/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number}
 */
var search = function(nums, target) {//[]左闭右闭
    let l = 0, mid = 0, r = nums.length - 1; //定义初始值满足[]区间
    while (l <= r) {  //l = r在区间内,循环二分
        mid = l + ((r - l) >> 1)  //位运算防止大数溢出=(l + r)/2
        if (nums[mid] > target) { //target在左边
            r = mid -  1
        } else if (nums[mid] < target) { //target在右边
            l = mid + 1
        } else { //nums[mid] == target 直接返回
            return mid
        }
    }
    return -1 //循环结束说明没有找到,返回-1
};
  1. 左闭右开

先设左右初始值l = 0, r = n满足[l, r),进入while循环,为满足区间=>循环条件while(l < r)
同理三种情况:

  • nums[mid] > target时,区间本来就不包括r也不包括mid,所以r = mid
  • nums[mid] < target时,同理l = mid + 1
  • nums[mid] == target即找到目标值,直接返回

最重要是:区间的定义不变-->循环的不变量,写不写=,下一个值是什么,都要满足区间定义, 根据区间来

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number}
 */
var search = function(nums, target) { //[)
    let l = 0, mid = 0, r = nums.length
    while (l < r) { //[l, r)区间
        mid = l + ((r - l) >> 1) //取中点比较中间值和目标值,更新区间
        if (nums[mid] > target) { //target在左边
            r = mid  //区间不包括mid,也不包括r,所以r = mid
        } else if (nums[mid] < target) { //在右边
            l = mid + 1 //mid不在[)
        } else {
            return mid
        }
    }
    return -1
};
1. 确定区间:target可能在的范围[l,r] / [l, r)
2. 满足区间条件while循环
3. 比较中间值与目标值,更新区间直到找到

27.移除数组

27. 移除元素
不用辅助数组,原地移除(删除)数组nums中的等于val的元素,被移出的部分不用管

三种方法

(重点)快慢双指针:把一个数组当两个用(顺序不变)
  • 快指针:遍历旧数组,找要装进新数组的元素
  • 慢指针:新数组下标
/**
 * @param {number[]} nums
 * @param {number} val
 * @return {number}
 */
var removeElement = function(nums, val) { //快(老)慢(新)双指针
    let slow = 0 //慢指针相当于新的数组下标
    for (let fast = 0; fast < nums.length; fast++) { //快指针遍历旧数组
        if (nums[fast] !==  val) { //不是要移除元素时加入新数组
            nums[slow++] = nums[fast]
        }
    }
    return slow //返回数组个数
};

前后双指针:把不要的数移到最后(顺序变)
  • 前指针:从前面开始找找不要的数
  • 后指针:从后面找要的数,交换

image.png

/**
 * @param {number[]} nums
 * @param {number} val
 * @return {number}
 */
var removeElement = function(nums, val) { //前(找要删)后(找不删)双指针
  let l = 0,r = nums.length - 1; //区间为左闭右闭[]
  
  while(l <= r){  //继续循环,进行下一次查找的条件
    // 下面加 l <= r 是为了防止找的时候 指针穿过 
    while(l <= r && nums[l] !== val) l++; //找左边=val要删的值 
    while(l <= r && nums[r] === val) r--; //找右边!=val不删的值
    // 一定要l < r时才覆盖,最后一次退出循环时,l > r不能交换
    if(l < r) nums[l++] = nums[r--];//用不删的值覆盖要删的
    //保持指针指向下一个要判断的值
  }

  return l; //l找被移除的,最后正好是指向第一个不满足的,也就是size大小
};
let l = 0, r = nums.length - 1
while (l <= r) {
    while (l <= r && nums[l] !== val) l++
    while (l <= r && nums[r] === val) r--
    if(l < r) nums[l++] = nums[r--]
}
return l

注意暴力解法的代码细节(顺序不变)
  1. i先找到要删的数
  2. 把后面的都往前移一位
  • 注意:因为i后所有数都前移了一位,i也得往前移一个,才能下一次for循环i++后指向下一个要判断的值(不然直接++会跳过刚移到i位置上的数)
  • 在移动的过程中数组长度变化,循环条件应该为变化的size
/**
 * @param {number[]} nums
 * @param {number} val
 * @return {number}
 */
var removeElement = function(nums, val) { //暴力
  let size = nums.length //nums.length为数组实际大小不会改变,size等会要返回的删除之后的大小
  for (let i = 0; i < size; i++) {
    if (nums[i] === val) { //找到要删除的值
      for (let j = i + 1; j < size; j++) {
        nums[j-1] = nums[j]
      }
      size--; //数组大小减一
      i--; //i后面的值都向前移动了一位,如果i不前移,接下来执行i++会跳过一个数没判断,所以一定要i--
    }
  }
  return size
};