开篇
我会在该专栏中叙述我自己在刷算法时的想法以及思路,希望能跟大家交流学习,也会虚心接受大家的批评指正😎。
二分查找
什么是二分查找呢?
-
二分查找所针对的数据结构特点:
- 顺序存储(数组)
- 有一定顺序关系(升序/降序)
-
二分查找的查找方式:
- 将数据一分为二,每次比较中间元素是否是目标元素,如果是就查找成功,否则将查找区间缩短一半,直到找到目标元素的位置或数据无法再分。
在二分查找中有以下几个重点需要把握住:
- 我们在开始编写时先要确定搜索区间:
[head,rear] or [head,rear)
(head,rear为每次搜索范围的首尾指针,区间的选择将直接影响后面缩小区间时head,rear指针的移动规则)-- 后文都选取闭区间 - 正常二分查找时需要判断的状态:中点>目标,中点<目标,中点=目标
- 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指针一定指向了我们要插入的位置。
- 这样会不会对于上面两种情况有影响呢?确实有,当不重复出现时,我们一定能找到并返回了mid,此时应该改为,找到了也让
代码如下:
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
在预先未知的某个下标 k
(0 <= 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];
};
这里有个问题我也没想清楚:
- 希望大家能够答疑解惑,为什么相等的时候要
rear = mid - 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);
}
};
我们的思路是:
- 找到最小值,以此为条件确定二分查找的单调区间。
- 确定二分查找的单调区间并开始查找。
存在以下情况:
在旋转奇数次和偶数次时:target <= nums[nums.length - 1]
。确定了一个区间[minIndex,nums.length-1]
在旋转奇数次时:target > nums[nums.length - 1]
,偶数次时只可能等于(因此会走上面的条件)。确定了一个区间[0,minIndex-1]