标签:开区 lse 问题 如何 dmi val 多次 ESS 搜索
前面算法系列文章有写过分治算法基本原理和实践,对于分治算法主要是理解递归的过程。二分法是分治算法的一种,相比分治算法会简单很多,因为少了递归的存在。
在计算机科学中,二分查找算法(英语:binary search algorithm),也称折半搜索算法(英语:half-interval search algorithm)、对数搜索算法(英语:logarithmic search algorithm)[2],是一种在有序数组中查找某一特定元素的搜索算法。搜索过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜索过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组为空,则代表找不到。这种搜索算法每一次比较都使搜索范围缩小一半。
二分查找算法在情况下的复杂度是对数时间。二分查找算法使用常数空间,无论对任何大小的输入数据,算法使用的空间都是一样的。除非输入数据数量很少,否则二分查找算法比线性搜索更快,但数组必须事先被排序。尽管特定的、为了快速搜索而设计的数据结构更有效(比如哈希表),二分查找算法应用面更广。
二分查找算法有许多中变种。比如分散层叠可以提升在多个数组中对同一个数值的搜索。分散层叠有效的解决了计算几何学和其他领域的许多搜索问题。指数搜索将二分查找算法拓宽到无边界的列表。二叉搜索树和B树数据结构就是基于二分查找算法的。
对二分法的概念了解后,下面来看一道示例:
已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到:
若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]
若旋转 7 次,则可以得到 [0,1,2,4,5,6,7]
注意,数组 [a[0], a[1], a[2], ..., a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]] 。
给你一个元素值 互不相同 的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的最小元素 。
示例 1:
输入:nums = [3,4,5,1,2]
输出:1
解释:原数组为 [1,2,3,4,5] ,旋转 3 次得到输入数组。
示例 2:
输入:nums = [4,5,6,7,0,1,2]
输出:0
解释:原数组为 [0,1,2,4,5,6,7] ,旋转 4 次得到输入数组。
示例 3:
输入:nums = [11,13,15,17]
输出:11
解释:原数组为 [11,13,15,17] ,旋转 4 次得到输入数组。
提示:
n == nums.length
1 <= n <= 5000
-5000 <= nums[i] <= 5000
nums 中的所有整数 互不相同
nums 原来是一个升序排序的数组,并进行了 1 至 n 次旋转
下面来看一下我写的一个失败版的答案,此时的我还没入门二分法:
class Solution { public int findMin(int[] nums) { int left = 0; int right = nums.length-1; while (left<=right) { int middle = left + (right -left)/2; if (nums[middle] > nums[left]) { left = middle + 1; }else { right = right-1; } } return nums[left]; } }
输入:[4,5,6,7,8,9,10,0,1,2,3]
输出:10
结果:0
可以看到结果是不对,那这里的问题是什么呢?都说失败是成功之母,我们只有分析清楚为啥我们的解法会存在问题,才能更好地明白二分法的精髓。
先从答案分析,这里输出 10,为啥会是 10。
看上面这张图,代码逻辑写的是 middle > left,那么 left = middle +1; 这个逻辑这么写是没有问题的。
接着看,当不满足 middle > left,说明 middle 处于最小值的右半部分,这时候我们让 right--。那如果 right 就是最小值呢,这时候就会错过最小值。
还有如果 middle 是最大值呢?那么 left= middle +1 就是最小值,此时你再去计算 middle ,就直接把最小值错过了。比如输入数组:[5,6,7,8,9,0,1,2,3,4];
还要考虑一种特殊情况,如果此时只有两个元素了,有两种情况 [1,2],[2,1] ,这时候如果按照 right--,就会直接取到第一个元素。所以在 middle 和 left 相等的时候也要在做额外的判断。
完整版通过代码如下:
class Solution { public int findMin(int[] nums) { int left = 0; int right = nums.length-1; while (left<right) { int middle = left + (right -left)/2; if (nums[middle] > nums[left] && nums[middle] > nums[right]) { left = middle +1;
// 说明最小值就在最右边,此时处于只有两个元素的时候 } else if(middle == left && nums[left] > nums[right]) { left = right; } else { right = right-1; } } return nums[left]; } }
当你看到这段代码后,你懵逼了,这还是二分法嘛,分析下来这么复杂。
那我们来看下官方给的代码:
class Solution { public int findMin(int[] nums) { int low = 0; int high = nums.length - 1; while (low < high) { int pivot = low + (high - low) / 2;
// 最小值一定是在和 high 在一个区间内的,所以这里要判断 pivot 和 high 的大小关系,不能去判断和 low 的关系 if (nums[pivot] < nums[high]) { high = pivot; } else { low = pivot + 1; } } return nums[low]; } }
是不是觉得官方代码简洁易懂。
那为啥这两个解法的代码会差这么多,答案在于 middle 到底是应该和 left 比较,还是应该和 right 比较。
这也说明了方向的选择的重要性。可是我们应该怎么选择呢。这个主要是在分析问题的时候要想清楚。个人觉得也可以这么理解:
本题是找最小值的。从最小值到最右端,这其实就是单调递增的,因此我们只要关注右半部分,抛弃左半部分就好。
那么本题错误原因就是跟左边进行比较,你再怎么找,最后得出值都不在这一部分上,就导致你得添加很多额外的逻辑来确保可以找到值。
PS:对于二分法要时刻关注只有两个元素的情况。这时候 middle = left。这时候注意 left 和 right 之间的关系。
通过这道题目相信大家已经对二分法有一定的认识了。
二分查找的思想就一个:逐渐缩小搜索区间。 如下图所示,它像极了「双指针」算法,left 和 right 向中间走,直到它们重合在一起:
根据看到的中间位置的元素的值 nums[mid] 可以把待搜索区间分为三个部分:
这样就可以获得二分法基本模板:
class Solution { public int search(int[] nums, int target) { int left = 0; int right = nums.length - 1; // 确保 left 和 right 都在数组可取范围内 while (left <= right) { // < 还是 <= 按照自己的习惯即可 int mid = left + (right -left)/2; if (nums[mid] == target) { // 找到结果就返回 return mid; }else if(nums[mid] > target) { right = mid-1; } else { left = mid +1; } }
// 退出循环就说明没找到 return -1; } }
虽然我们看到的写法有很多,但思想就这一个;为什么总是有朋友觉得二分难?因为有很多二分的写法,虽然都对,但是对于新手朋友们来说有一定干扰,因为不同的写法其实对应着不同的前提和应用场景,比起套用模板,审题、练习和思考更重要。「二分查找」就几行代码,完全不需要记忆,也不应该用记忆的方式解题.
下面解释一些细节:
1、模板的结束条件是 left <= right ,也就是结果一定是在 while 里面找到的。否则就是没找到。
2、有些学习资料上说 while (left < right) 表示区间是 [left..rihgt) ,为什么你这里是 [left..rihgt]?
区间的右端点到底是开还是闭,完全由编写代码的人决定,不需要固定。主要还是看你 left 和 right 的取值。 如果 right = nums.length ; 那么其实 right 这个位置是取不到的,也就是开区间了。所以开闭就是看点位能不能取到。
3、有些学习资料给出了三种模板,例如「力扣」推出的 LeetBook 之 「二分查找专题」,应该如何看待它们?
回答:三种模板其实区别仅在于退出循环的时候,区间 [left..right] 里有几个数。
while (left <= right) :退出循环的时候,right 在左,left 在右,区间为空区间,所以要讨论返回 left 和 right;
while (left < right) :退出循环的时候,left 与 right 重合,区间里只有一个元素,这一点是我们很喜欢的;
while (left + 1 < right) :退出循环的时候,left 在左,right 在右,区间里有 2 个元素,需要编写专门的逻辑。这种写法在设置 left 和 right 的时候不需要加 1 和减 1。
看似简化了思考的难度,但实际上屏蔽了我们应该且完全可以分析清楚的细节。退出循环以后一定要讨论返回哪一个,也增加了编码的难度。
我个人的经验是:
while (left <= right) 用在要找的数的性质简单的时候,把区间分成三个部分,在循环体内就可以返回;
while (left < right) 用在要找的数的性质复杂的时候,把区间分成两个部分,在退出循环以后才可以返回;
完全不用 while (left + 1 < right) ,理由是不会使得问题变得更简单,反而很累赘。
很多题目在二分法的基础上有变化,我们要学会灵活变化。还要理解题意。
示例:
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n)
的算法。
示例 1:
输入: nums = [1,3,5,6], target = 5 输出: 2
示例 2:
输入: nums = [1,3,5,6], target = 2 输出: 1
示例 3:
输入: nums = [1,3,5,6], target = 7 输出: 4
示例 4:
输入: nums = [1,3,5,6], target = 0 输出: 0
示例 5:
输入: nums = [1], target = 0 输出: 0
提示:
1 <= nums.length <= 104
-104 <= nums[i] <= 104
nums
为无重复元素的升序排列数组-104 <= target <= 104
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 (nums[mid] == target) { return mid; } if (nums[mid]>target) { right = mid-1; }else { left = mid+1; } } // 没找到,那么 left 就是它所处的位置 return left; } }
注意一点:二分法只是用于有序数组,如果是无序的,此时是无法确定边界的,这时候我们就需要自己创造条件,找到数组的有序部分。
比如下面两道,大家可以自己找二分法题目去练习。
关于二分法的理论就讲到这里了,剩下的就是靠大家多多练习了。
参考文章
https://leetcode-cn.com/problems/search-insert-position/solution/te-bie-hao-yong-de-er-fen-cha-fa-fa-mo-ban-python-/
标签:开区 lse 问题 如何 dmi val 多次 ESS 搜索
原文地址:https://www.cnblogs.com/huansky/p/15054625.html