开启力扣刷题之路 >数组> 二分法
Posted 王六六的IT日常
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了开启力扣刷题之路 >数组> 二分法相关的知识,希望对你有一定的参考价值。
【代码随想录】力扣刷题攻略 <<< 这是大佬的git链接,感兴趣的小伙伴一起追随大佬的脚步,少走弯路噢。---- 搬运工是我了。。。
根据大佬总结的刷题攻略开始力扣刷题!冲鸭!
前言
面对leetcode上近两千道题目,从何刷起?
【代码随想录】大佬说道:
按照如下类型来刷,数组-> 链表-> 哈希表->字符串->栈与队列->树->回溯->贪心->动态规划->图论->高级数据结构
再从简单刷起,做了几个类型题目之后,再慢慢做中等题目、困难题目。
一、数组理论基础
数组是非常基础的数据结构,在面试中,考察数组的题目一般在思维上都不难,主要是考察对代码的掌控能力
也就是说,想法很简单,但实现起来可能就不是那么回事了。首先要知道数组在内存中的存储方式,这样才能真正理解数组相关的面试题
数组是存放在连续内存空间上的相同类型数据的集合。
数组可以方便的通过下标索引的方式获取到下标下对应的数据。
举一个字符数组的例子,如图所示:
需要两点注意的是
- 数组下标都是从0开始的。
- 数组内存空间的地址是连续的
正是因为数组的在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。
例如删除下标为3的元素,需要对下标为3的元素后面的所有元素都要做移动操作,如图所示:
数组的元素是不能删的,只能覆盖。
那么二维数组在内存的空间地址是连续的么?
不同编程语言的内存管理是不一样的,来做一个实验,Java代码如下:
Java是没有指针的,同时也不对程序员暴漏其元素的地址,寻址操作完全交给虚拟机。
public static void test_arr() {
int[][] arr = {{1, 2, 3}, {3, 4, 5}, {6, 7, 8}, {9,9,9}};
System.out.println(arr[0]);
System.out.println(arr[1]);
System.out.println(arr[2]);
System.out.println(arr[3]);
}
输出的地址为:
[I@7852e922
[I@4e25154f
[I@70dea4e
[I@5c647e05
这里的数值也是16进制,这不是真正的地址,而是经过处理过后的数值了,我们也可以看出,二维数组的每一行头结点的地址是没有规则的,更谈不上连续。
所以Java的二维数组可能是如下排列的方式:
大佬在git里给了C++相关方面的解读,我是学的java所以没写C++的解题思路和代码,学C++的小伙伴可以从下面的链接点进去了解相关的知识。
二、数组 ~ 二分法
题目链接:https://leetcode-cn.com/problems/binary-search/ (大佬把题目链接也甩过来了,好贴心~)
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
示例 1:
输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4
示例 2:
输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1
提示:
- 你可以假设 nums 中的所有元素是不重复的。
- n 将在 [1, 10000]之间。
- nums 的每个元素都将在 [-9999, 9999]之间。
1. 思路
二分查找是一种基于比较目标值(target)和数组中间元素(mid)的教科书式算法。
- 如果target等于mid,则找到目标值。
- 如果target较小,继续在左侧搜索。
- 如果target较大,则继续在右侧搜索。
这道题目的前提是数组为有序数组,同时题目还强调数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下标可能不是唯一的,这些都是使用二分法的前提条件,当看到题目描述满足如上条件的时候,可要想一想是不是可以用二分法。
二分查找涉及的很多的边界条件,逻辑比较简单,但就是写不好。例如到底是 while(left < right)
还是 while(left <= right)
,到底是right = middle
呢,还是要right = middle - 1
呢?
大家写二分法经常写乱,主要是因为对区间的定义没有想清楚,区间的定义就是不变量。要在二分查找的过程中,保持不变量,就是在while寻找中每一次边界的处理都要坚持根据区间的定义来操作,这就是循环不变量规则。
分析二分查找的一个技巧是:不要出现 else,而是把所有情况用 else if 写清楚,这样可以清楚地展现所有细节。
写二分法,区间的定义一般为两种,左闭右闭即[left, right],或者左闭右开即[left, right)。
下面根据这两种区间的定义分别讲解两种不同的二分写法。
1.1 二分法第一种写法
我们定义 target 是在一个在左闭右闭的区间里,也就是[left, right] (这个很重要非常重要)。
区间的定义这就决定了二分法的代码应该如何写,因为定义target在[left, right]区间,所以有如下两点:
- while (left <= right) 要使用 <= ,因为left == right是有意义的,所以使用 <=
- if (nums[mid] > target) right 要赋值为 mid- 1,因为当前这个nums[mid]一定不是target,那么接下来要查找的左区间结束下标位置就是 mid - 1
算法:
1.初始化指针 left = 0, right = nums.length - 1。
2.当 left <= right:
比较中间元素 nums[mid] 和目标值 target 。
- 如果 target = nums[mid],返回 mid。
- 如果 target < nums[mid],则在左侧继续搜索 right = mid- 1。[ left , mid - 1 ]
- 如果 target > nums[mid],则在右侧继续搜索 left = mid+ 1。[ mid + 1 , right ]
例如在数组:1,2,3,4,7,9,10中查找元素2,如图所示:
代码如下:
计算 mid 时需要防止溢出,代码中 int mid = left + (right - left) / 2 和 int mid = (left + right) / 2 的结果相同,有效防止了 left 和 right 太大直接相加导致溢出。
//版本一
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; // 定义target在左闭右闭的区间里,[left, right]
while (left <= right) { // 当left==right,区间[left, right]依然有效,所以用 <=
int mid = left + (right - left) /2 ; // 防止溢出 等同于(left + right)/2
if (nums[mid] == target)
return mid; // 数组中找到目标值,直接返回下标
else if (nums[mid] < target)
left = mid + 1; // target 在右区间,搜索区间为[middle + 1, right]
else if (nums[mid] > target)
right = mid - 1; // target 在左区间,搜索区间为[left, middle - 1]
}
return -1; // 未找到目标值
}
}
>>>>>>>>>> 为什么 while 循环的条件中是 <=,而不是 <?
答:因为初始化 right 的赋值是 nums.length - 1,即最后一个元素的索引,而不是 nums.length。
这二者可能出现在不同功能的二分查找中,区别是:前者相当于两端都闭区间 [left, right],后者相当于左闭右开区间 [left, right),因为索引大小为 nums.length 是越界的。
该算法中使用的是[left, right] 两端都闭的区间。这个区间其实就是每次进行搜索的区间。
while(left <= right) 的终止条件是 left == right + 1,写成区间的形式就是 [right + 1, right],或者带个具体的数字进去 [3, 2],可见这时候区间为空,因为没有数字既大于等于 3 又小于等于 2 的吧。所以这时候 while 循环终止是正确的,直接返回 -1 即可。
while 循环什么时候应该终止?搜索区间为空的时候应该终止,意味着你没得找了,等于没找到。
作者:labuladong
链接:https://leetcode-cn.com/problems/binary-search/solution/er-fen-cha-zhao-xiang-jie-by-labuladong/
来源:力扣(LeetCode)
大佬在git里给了C++相关方面的解读,我是学的java所以没写C++的解题思路和代码,学C++的小伙伴可以从下面的链接点进去了解相关的知识。
1.2 二分法第二种写法
如果说 target 是定义在一个在左闭右开的区间里,也就是 [left, right) ,那么二分法的边界处理方式则截然不同。
有如下两点:
- while (left < right),这里使用 < ,因为left == right在区间[left, right)是没有意义的
- if (nums[mid] > target), right 更新为 mid,因为当前nums[mid]不等于target,去左区间继续寻找,而寻找区间是左闭右开区间,所以right更新为mid,即:下一个查询区间不会去比较nums[mid]
在数组:1,2,3,4,7,9,10中查找元素2,如图所示:(注意和方法一的区别)---- 向下取整
代码如下:
计算 mid 时需要防止溢出,代码中 int mid = left + (right - left) / 2 和 int mid = (left + right) / 2 的结果相同,有效防止了 left 和 right 太大直接相加导致溢出。
//版本二
class Solution { //左闭右开
public int search(int[] nums, int target) {
int left = 0, right = nums.length; // 定义target在左闭右开的区间里,即:[left, right)
while (left < right) { // 因为left == right的时候,在[left, right)是无效的空间,所以使用 <
int mid = left + (right - left) /2;
if (nums[mid] == target) // 数组中找到目标值,直接返回下标
return mid;
else if (nums[mid] < target) // target 在右区间,搜索区间为[middle + 1, right)
left = mid + 1;
else if (nums[mid] > target) // target 在左区间,搜索区间为[left, middle)
right = mid;
}
return -1; // 未找到目标值
}
}
while(left < right) 的终止条件是 left == right,写成区间的形式就是 [left, right]
或者带个具体的数字进去 [2, 2],这时候区间非空,还有一个数 2,但此时 while 循环终止了。也就是说这区间 [2, 2] 被漏掉了,索引 2 没有被搜索,如果这时候直接返回 -1 就是错误的。
作者:labuladong
链接:https://leetcode-cn.com/problems/binary-search/solution/er-fen-cha-zhao-xiang-jie-by-labuladong/
来源:力扣(LeetCode)
大佬在git里给了C++相关方面的解读,我是学的java所以没写C++的解题思路和代码,学C++的小伙伴可以从下面的链接点进去了解相关的知识。
2. 总结
二分法是非常重要的基础算法,为什么很多同学对于二分法都是一看就会,一写就废?
其实主要就是对区间的定义没有理解清楚,在循环中没有始终坚持根据查找区间的定义来做边界处理。
区间的定义就是不变量,那么在循环中坚持根据查找区间的定义来做边界处理,就是循环不变量规则。
根据两种常见的区间定义,给出了两种二分法的写法,每一个边界为什么这么处理,都根据区间的定义做了详细介绍。
3. 相关题目推荐
- 35.搜索插入位置
- 34.在排序数组中查找元素的第一个和最后一个位置
- 69.x 的平方根
- 367.有效的完全平方数
总结
第一种,最基本的二分查找算法:
因为初始化 right = nums.length - 1
所以决定了「搜索区间」是 [left, right] 左闭右闭
所以决定了 while (left <= right)
同时也决定了 left = mid+1 和 right = mid-1因为我们只需找到一个 target 的索引即可
所以当 nums[mid] == target 时可以立即返回
第二种,寻找左侧边界的二分查找:
因为初始化 right = nums.length
所以决定了「搜索区间」是 [left, right) 左闭右开
所以决定了 while (left < right)
同时也决定了 left = mid + 1 和 right = mid因为我们需找到 target 的最左侧索引
所以当 nums[mid] == target 时不要立即返回
而要收紧右侧边界以锁定左侧边界
第三种,寻找右侧边界的二分查找:
因为初始化 right = nums.length
所以决定了「搜索区间」是 [left, right) 左闭右开
所以决定了 while (left < right)
同时也决定了 left = mid + 1 和 right = mid因为我们需找到 target 的最右侧索引
所以当 nums[mid] == target 时不要立即返回
而要收紧左侧边界以锁定右侧边界又因为收紧左侧边界时必须 left = mid + 1
所以最后无论返回 left 还是 right,必须减一
作者:labuladong
链接:https://leetcode-cn.com/problems/binary-search/solution/er-fen-cha-zhao-xiang-jie-by-labuladong/
来源:力扣(LeetCode)
三种情况代码如下:
int binary_search(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while(left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if(nums[mid] == target) {
// 直接返回
return mid;
}
}
// 直接返回
return -1;
}
//左侧边界
int left_bound(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] == target) {
// 别返回,锁定左侧边界
right = mid - 1; //注意
}
}
// 最后要检查 left 越界的情况
if (left >= nums.length || nums[left] != target)
return -1;
return left;
}
//右侧边界
int right_bound(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] == target) {
// 别返回,锁定右侧边界
left = mid + 1; //注意
}
}
// 最后要检查 right 越界的情况
if (right < 0 || nums[right] != target)
return -1;
return right;
}
对于寻找左右边界的二分搜索,常见的手法是使用左闭右开的「搜索区间」,根据逻辑将「搜索区间」全都统一成了两端都闭。
复盘开始:
1、分析二分查找代码时,不要出现 else,全部展开成 else if 方便理解。
2、注意「搜索区间」和 while 的终止条件,如果存在漏掉的元素,记得在最后检查。
3、如需定义左闭右开的「搜索区间」搜索左右边界,只要在 nums[mid] == target 时做修改即可,搜索右侧时需要减一。
4、如果将「搜索区间」全都统一成两端都闭,好记,只要稍改 nums[mid] == target 条件处的代码和返回的逻辑即可,推荐拿小本本记下,作为二分搜索模板。
作者:labuladong
链接:https://leetcode-cn.com/problems/binary-search/solution/er-fen-cha-zhao-xiang-jie-by-labuladong/
来源:力扣(LeetCode)
二分法写到这里~~~~~~~~~~~~~~
以上是关于开启力扣刷题之路 >数组> 二分法的主要内容,如果未能解决你的问题,请参考以下文章