二分查找

开篇

我会在该专栏中叙述我自己在刷算法时的想法以及思路,希望能跟大家交流学习,也会虚心接受大家的批评指正😎。

二分查找

什么是二分查找呢?

  • 二分查找所针对的数据结构特点:

    • 顺序存储(数组)
    • 有一定顺序关系(升序/降序)
  • 二分查找的查找方式:

    • 将数据一分为二,每次比较中间元素是否是目标元素,如果是就查找成功,否则将查找区间缩短一半,直到找到目标元素的位置或数据无法再分。

在二分查找中有以下几个重点需要把握住:

  1. 我们在开始编写时先要确定搜索区间:[head,rear] or [head,rear) (head,rear为每次搜索范围的首尾指针,区间的选择将直接影响后面缩小区间时head,rear指针的移动规则)-- 后文都选取闭区间
  2. 正常二分查找时需要判断的状态:中点>目标,中点<目标,中点=目标
  3. i和j在每次二分后,都会向目标值靠近,最后会出现什么情况? 找到目标值(mid为索引)或没找到目标值(head>rear跳出循环)

带着如上三点,我们做些题体会一下:

LeetCode-704.二分查找

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

上代码,注意看注释:

js 复制代码
var search = function (nums, target) {
  let head = 0, rear = nums.length - 1;
  while (head <= rear) {
    //中点位置 向下取整,以head最后结果
    let mid = Math.floor(head + (rear - head) / 2)

    if (target < nums[mid]) {//目标值<中点
      //目标值在mid左边,因此更新rear到mid-1
      rear = mid - 1;
    } else if (target > nums[mid]) {//目标值>中点
      //目标值在mid右边,因此更新head到mid+1
      head = mid + 1;
    } else {//目标值=中点
      //找到了目标值,因此可以
      return mid;
    }
  }
  return -1;
};

可以看到

  • 由于我们选择的是闭区间,因此每次范围确定都向内缩进,可以这样理解:保持搜索范围的状态一致性(一开始我们选择的闭区间是我们从未访问过的值,因此更新区间范围时也更新为从未访问过的值)
  • 循环条件是head<=rear,因为头尾指针都会向target缩进,会出现以下两种情况:
    • 范围内剩两个未遍历 的元素head + 1 === rear,我们的向下取整计算的mid将为head,此时只要再循环一遍,如果head的值就是目标值,那么我们成功找到了(return mid),如果不是目标值,那么会执行一次范围变换,无论是头还是尾的变换都会破坏head === rear这个条件,因此跳出循环,不存在目标值。
    • 范围内只剩一个未遍历 的元素head === rear,此时我们还需要遍历一遍确定是否目标值,存在目标值则返回,不存在则会破坏条件进而结束循环。

如上所述,要么剩下一个未遍历的点,要么两个,由于都是未遍历的点,所以还需要再遍历一层,进而决定了循环条件需要等号。

在现在的基础上我们再来几道题,其中单独会依次递增,但是我们都能从中学到很多,加油!!!🤩

LeetCode-35.搜索插入位置

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。 我们把题目升级一点,目标值也可能多次出现,此时插入位置为第一个目标值出现的位置 这道题存在以下几种可能:目标值重复出现,目标住不重复出现,无目标值。

  • 对于目标值不重复出现的情况,rear和head都再向目标值趋近,最终找到目标值,那么找的的索引即为所求
  • 对于无目标值,最终我们根据上一道题的二分,我们就会跳出循环,此时rear指向第一个小于目标值的点,head指向的就是目标值的插入结点
  • 对于目标值重复出现的情况,我们在二分过程中可能会定位到某个目标值,此时应该移动rear,使rear一直指向第一个小于目标值的值 -->rear = mid-1;
    • 这样会不会对于上面两种情况有影响呢?确实有,当不重复出现时,我们一定能找到并返回了mid,此时应该改为,找到了也让rear = mid-1 保持一致,并且结束循环。最后得到的结果就是head指针一定指向了我们要插入的位置。

代码如下:

js 复制代码
  let head = 0, rear = nums.length - 1;
  while (head <= rear) {
    let mid = Math.floor(head + (rear - head) / 2);
    if (nums[mid] < target) {
      head = mid + 1;
    } else if (nums[mid] > target) {
      rear = mid - 1;
    } else {
      rear = mid - 1;
    }
  }
  return head;

LeetCode-34.在排序数组中查找元素的第一个和最后一个位置

给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 target,返回 [-1, -1]

你有没有发现,实际上上一道题我们找到的就是这道题的开始位置。那么我们只需要照猫画虎找到结束位置就行了。 此时对于寻找最后一个位置,我们只要保证每次查找到目标值时,让head指向第一个大于目标值的值即可(head = mid + 1

js 复制代码
const findFirstIndex = (nums, target) => {
  let head = 0, rear = nums.length - 1;
  while (head <= rear) {
    const mid = Math.floor(head + (rear - head) / 2);
    if (target < nums[mid]) {
      rear = mid - 1;
    } else if (target > nums[mid]) {
      head = mid + 1;
    } else {
      rear = mid - 1;
    }
  }
  return head;
}

const findLastIndex = (nums, target) => {
  let head = 0, rear = nums.length - 1;
  while (head <= rear) {
    const mid = Math.floor(head + (rear - head) / 2);
    if (target < nums[mid]) {
      rear = mid - 1;
    } else if (target > nums[mid]) {
      head = mid + 1;
    } else {
      head = mid + 1;
    }
  }
  return rear;
}

var searchRange = function (nums, target) {
  const first = findFirstIndex(nums, target);
  //由于可能不存在目标值,因此我们对查找到的索引进行验证,如果验证失败直接返回[-1,-1];
  if (nums[first] !== target) {
    return [-1, -1];
  }
  const last = findLastIndex(nums, target);
  return [first, last];
};

LeetCode-69.x 的平方根

给你一个非负整数 x ,计算并返回 x算术平方根

由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。

求算术平方根与二分有什么关系?其实这个题并不难,假设给你一个32,让你求他的算术平方根,那么这个值一定小于8,事实上,他会小于32/2=16,那么问题就简化为了,在1-16之间找到一个值的平方是第一个大于32的,接着我们可以再接着二分范围,8的平方大于32,那么范围就缩小到1-8之间,以此类推即可找到这个值。

把这个问题再形象化一点:实际上就是让我们在 0-x/2 之间找一个数,这个数是满足条件(平方小于等于x的最大值(最右边的值)!!!);

上代码:

js 复制代码
var mySqrt = function (x) {
  let head = 1, rear = Math.ceil(x / 2);
  while (head <= rear) {
    const mid = Math.floor(head + (rear - head) / 2);
    if (mid * mid > x) {
      rear = mid - 1;
    } else if (mid * mid < x) {
      head = mid + 1;
    } else {
      head = mid + 1;
    }
  }
  return rear;
};

接下来要上难度了👻

LeetCode-153.寻找旋转排序数组中的最小值

整数数组 nums 按升序排列,数组中的值 互不相同

在传递给函数之前,nums 在预先未知的某个下标 k0 <= k < nums.length)上旋转 了n次。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转奇数次后变为 [4,5,6,7,0,1,2] 。给你 旋转n次后 的数组 nums 。请你找出并返回数组中的最小元素。

我们知道二分法能够解决有序数组查找的问题,请一定不要陷入升序降序之中。就像这道题一样,二分法同样可以解决,二分法可以解决具有两段性的问题,什么是具有两段性呢?数组在局部有共同规律,在整体也有规律,两部分都是升序的,并且前一部分可能比后一部分大。我们可以通过与数组第一个元素比较来找到最大值,可以通过与最后一个元素比较找到最小值(像此题一样)。

看图可能会更清楚:

两部分一定被数组首尾元素分离(这个分离点的值一定小于数组末尾的值),这就是我们逐渐缩小搜索范围的依据

TypeScript 复制代码
var findMin = function (nums) {
  let = head = 0;
  let rear = nums.length - 1;
  let rearV = nums[rear];
  while (head <= rear) {
    let mid = Math.floor(head + (rear - head) / 2)
    if (nums[mid] < rearV) {
      rear = mid - 1;
    } else if (nums[mid] > rearV) {
      head = mid + 1;
    } else {
      rear = mid - 1;
    }
  }
  return nums[head];
};

这里有个问题我也没想清楚:

  1. 希望大家能够答疑解惑,为什么相等的时候要rear = mid - 1呢?
    1. 我一开始的想法是,这道题能够被二分,说明存在共性,那么假设我第一次循环的时候就相等了,那么此时我该移动谁?由于最后一个元素一定不是我想要的,因此我移动了尾指针。但是我不能以"能够被二分"作为出发点😢

想通了,应该这样想:如果此时mid的值等于了末尾值,那么说明末尾的值已经被我们访问过了,为了保持查询范围的一致性,我们应该更新尾指针。

然后是一道该题的进阶题目👇👇👇

LeetCode-33.搜索旋转排序数组

整数数组 nums 按升序排列,数组中的值 互不相同 。旋转n次同上。

给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1

结合代码分析:

js 复制代码
/**
 * 获取最小值索引
 */
const findMinIndex = (nums, head, rear, target) => {
  while (head <= rear) {
    let mid = Math.floor(head + (rear - head) / 2)
    if (nums[mid] < target) {
      rear = mid - 1;
    } else if (nums[mid] > target) {
      head = mid + 1;
    } else {
      rear = mid - 1;
    }
  }
  return head;
}
/**
 * 有序数组中的二分查找
 */
const binarySearch = (nums, head, rear, target) => {
  while (head <= rear) {
    let mid = Math.floor(head + (rear - head) / 2)
    if (nums[mid] < target) {
      head = mid + 1;
    } else if (nums[mid] > target) {
      rear = mid - 1;
    } else {
      return mid;
    }
  }
  return -1;
}
var search = function (nums, target) {
  //第一次找到分界点,即最小值。第二次在单调区间找到目标值
  const minIndex = findMinIndex(nums, 0, nums.length - 1, nums[nums.length - 1])
  //找到了直接返回
  if (nums[minIndex] === target) {
    return minIndex;
  }
  //确定单调区间
  if (target <= nums[nums.length - 1]) {
      //如果目标值小于等于末尾值,则当前数组无论在旋转奇数次还是偶数次,搜索范围都是minIndex到nums.length-1
    return binarySearch(nums, minIndex, nums.length - 1, target);
  } else {
      //如果目标值大于末尾值,则只有一种可能,当前数组旋转了奇数次,并且搜索范围为0,minIndex-1
    return binarySearch(nums, 0, minIndex - 1, target);
  }
};

我们的思路是:

  1. 找到最小值,以此为条件确定二分查找的单调区间。
  2. 确定二分查找的单调区间并开始查找。

存在以下情况:

在旋转奇数次和偶数次时:target <= nums[nums.length - 1] 。确定了一个区间[minIndex,nums.length-1] 在旋转奇数次时:target > nums[nums.length - 1] ,偶数次时只可能等于(因此会走上面的条件)。确定了一个区间[0,minIndex-1]

相关推荐
Jiaberrr16 分钟前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui
我是哈哈hh1 小时前
专题十_穷举vs暴搜vs深搜vs回溯vs剪枝_二叉树的深度优先搜索_算法专题详细总结
服务器·数据结构·c++·算法·机器学习·深度优先·剪枝
Tisfy1 小时前
LeetCode 2187.完成旅途的最少时间:二分查找
算法·leetcode·二分查找·题解·二分
Mephisto.java1 小时前
【力扣 | SQL题 | 每日四题】力扣2082, 2084, 2072, 2112, 180
sql·算法·leetcode
robin_suli1 小时前
滑动窗口->dd爱框框
算法
丶Darling.1 小时前
LeetCode Hot100 | Day1 | 二叉树:二叉树的直径
数据结构·c++·学习·算法·leetcode·二叉树
labuladuo5202 小时前
Codeforces Round 977 (Div. 2) C2 Adjust The Presentation (Hard Version)(思维,set)
数据结构·c++·算法
安冬的码畜日常2 小时前
【D3.js in Action 3 精译_029】3.5 给 D3 条形图加注图表标签(上)
开发语言·前端·javascript·信息可视化·数据可视化·d3.js
jiyisuifeng19912 小时前
代码随想录训练营第54天|单调栈+双指针
数据结构·算法
太阳花ˉ2 小时前
html+css+js实现step进度条效果
javascript·css·html