一)二分查找算法的原理:
704. 二分查找 - 力扣(LeetCode)
1)为什么暴力解法慢,因为暴力解法的只能够一次筛选一个元素,只能干掉一个数,而二分查找是一次筛选出一半的数,因此最差的情况下就是从头到尾遍历整个数组舍去所有数;
2)二段性:二分查找利用数组有序的特性,我们是在数组中随便选取一个点,让这个点的数组值和我们的target的值进行比较,进行比较之后将整个数组划分出来了两段区域,根据大小或者是其他关系,我们可以舍去其中的一段区间,继续在另一段区间进行查找target,此时就可以总结出这个数组是有一定的二段性的,从而总结出二分查找应用的的是二段性;
3)二分模板:朴素的二分模板+查找左边界的二分模板+查找右边界的二分模板
二分查找的适用范围:当发现一个规律,根据这个规律选取某一个点之后,能把这个数组分割成两部分(不一定是中间位置),根据规律性可以有选择的舍去一部分,然后继而继续在另一部分继续查找的时候,此时就可以使用二分查找算法,和数组是否有序无关,将这段数组抽象成一个规律;
1)但是进行查找的时候,可以选取1/4位置的点和目标值作比较,可以选取1/2位置的点,可以选取1/8位置的点,如果选取1/4位置的点和target值作比较,那么直接有可能会干掉3/4的数据,如果选取1/8的位置的点那么有可能干掉7/8的数据,如果选取1/2位置的点有可能干掉1/2的数据,但是综合于数学期望来看,我们还是进行选取1/2位置的点,因为选取1/8位置的点,也有可能仅仅干掉1/8的数据,在中间的点的时间复杂度是最好的,但是本质上来说我们进行寻找的点可以将整个数组区间划分成两部分即可,所以可以选择很多点来作为划分点;
2)我们要频繁的找中点,还要频繁的确定出要进行寻找的区间
3)if(array[mid]
4)if(array[mid]>t)那么mid右边区间的值(包括mid下标的值)应该全部干掉,接下来应该去左边区间进行需按照目标值等于target;
细节问题:
1)循环结束的条件:当我们进行排除mid以及mid的一段区间的时候,此时就会发现,我们的新确定的区间,[mid+1,right)或者是[left,mid-1)这段区间的元素都是未知的,当我们的left和mid最终缩小成一个点的时候,这个区间内只有一个元素,那么这一个元素需不需要进行判断呢?这肯定是需要判断的,因为我们每一次移动区间的时候,这段区间的数都是没有判断过的,这段区间内的数都是未知的,因此left==right也是需要进行循环操作的,所以循环终止条件就是left>right,这样区间内的所有元素都是已经判断完成了;
2)二分查找算法为什么是对的:虽然比较了一次,但是确实可以做到了和暴力解法比较好几次可以做到的事情;
3)为什么二分查找速度快?时间复杂度是多少?
4)求中点下标怎么求?
(left+right)/2可能有溢出的风险,left+right可能会溢出,left+(right-left)/2,就算执行了减法也绝对不会溢出的;
1)没有防溢出:mid=(left+right+1)/2或者是(left+right)/2
2)防溢出:mid=left+(right-left+1)/2或者是(left+right+1)/2,这两种写法的区别就是第一种对于奇数来说求出的中间下标都是一个,唯一的区别就是是否可以除尽,而对于偶数来说,这种方式求出的中间下标就是两个,没有加1求出的是靠左的位置,加1之后求出的是靠右的位置,但是我们仅需找到一个点,只需要根据这个点划分出两段区间即可,两段区间有二段性即可;
class Solution { public int search(int[] array, int target) { int left=0; int right=array.length-1; while(left<=right){ int mid=(right-left)/2+left; if(array[mid]
上面的括号里面的判断条件就是根据二段性来进行判断的
二)优化后的两个二分算法:
34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)
这里面的非递增是中间可以有相等的元素,下面是查找区间左端点和右端点时候的二段性:
1)当去整个数组中目标值等于target的左端点LeftIndex的时候,可以根据leftIndex划分出两段区间,LeftIndex左边全部是小于target的值,LeftIndex右边的值全部是大于等于target的值
2)当去查询区间右端点RightIndex的时候,可以根据rightIndex划分出两段区间,数组中RightIndex左边全部是小于等于target的值RightIndex右边全部是大于target的值;
一)查询区间的左端点:
可以使用一个值也就是数组区间中等于target的左端点将数组分割成两部分
左端点左边的数全部是小于target,右边的数右边的数全部是大于等于target的
1)首先进行计算mid的值,如果array[mid]的值落到了小于t的区间,那么小于t区间内的数都是不可能是符合要求的下标,此时如果array[mid]
此时还需要注意,就算使用二分查找,就算array[mid]==target,最终mid指向的值也不一定是我们最终要进行查询的结果,所以要放在一起进行讨论 2)当array[mid]的值计算出如果要是大于t,或者是等于t,此时array[mid]一定是在大于等于的target的区间范围内的,此时应该让right=mid
3)array[mid]要是等于target可能是mid指向的是大于等于target区间内的第一个元素,此时要是让right=mid-1,那么在如果mid指向的是等于target的第一个元素,那么就永远无法找到符合要求的值,此时right就跳跃出了我们要查询的位置
如果说在这个区间内始终也没有合法的值,这个时候又可以分为两种情况
1)况且数组中的值都是大于target的值,那么此时left根本就不会移动,因为永远不会命中array[mid]
2)数组中的值如果都是小于target的值,那么此时right根本也不会移动,因为永远也不会命中array[mid]>target这个条件,自己画图演示一下,此时left和right都是指向的是数组的最后一个元素,此时left和right相遇,只是需要判断array[left]的值是否等于target元素即可,此时就无需在循环里进行判断了,left和right循环直接跳出循环进行判断即可
细节处理:
一)那么此时循环条件是left
是left<=right的呢? 什么时候执行循环呢? 1)left==right的时候,就是最终的结果,无需进行判断
2)如果你在循环条件使用left<=right的时候程序就会陷入死循环,因为在下面的第一种情况下,left和right都指向了数组中的第一个target出现的元素位置,或者是说right和left都指向了最终结果所在的位置,此时还是继续计算mid下标,此时一定满足条件
3)array[mid]<=array[right],right=mid,此时right就不会移动了,此时就会发生死循环,right会一直不动,right和left会一直指向最终的元素;
4)有结果:left==right的时候相遇的位置就是最终结果,不需要判断
5)全部大于taregt:left一动不动,right等于每一次计算出来的mid,最终left==right==mid=0,此时计算出来的值还是一直大于等于target,right=mid=0,会一直重复这样的过程,但是没有最终结果,此时最终两个指针都不动,重复的操作只是赋值操作和计算下标操作,此时就会出现死循环;
6)全部小于target:right一动不动,left一直等于mid+1,最终left会大于right;
总结:left==right就是最终的结果了
二)求mid中点操作:当进行举例子的时候,只剩下两个元素,这两个元素都比最终要求的元素
2.1)left+(right-left)/2,在元素个数是偶数个的时候,求出来的中点是靠左的位置
2.2)left+(right-left)+1)/2,在元素个数是偶数个的时候,求出来的中点是靠右的位置
但是在朴素二分查找的时候,上面两个求中点的操作都是可以的,奇数位置都是一样的
2.3)但是在这种情况下确是不可以的,假设如果最后只剩下两个元素了,最终就会导致两个元素永远无法相遇,也是会导致最终两个位置left和right不会发生变化,如果使用第一种,就会使其相遇或者是其中一个指针越界,退出循环,反正最终的核心思想就是让mid下标落在left指针的位置
二)查询区间的右端点:
二段性:此时区间右端点的值仍然把区间分成了两部分,左边的区间全部是小于等于target的,右边的区间全部是大于等于target的
1)如果计算出来的array[mid]<=target的时候,left=mid,此时的left仍然是不能跨过target的值的,如果此时left指向的值恰好是等于target值的最右边的端点,此时left就永远错过了查询区间的右端点
2)如果计算出来的array[mid]>target的时候,此时mid所指向的值和mid右边的值一定是不符合要求的元素下标的位置,此时应该是right=mid-1;
细节问题的处理:
一)那么此时循环条件是left
二)选取中点的操作:假设在5,7,7,8,8,10中进行查询8的最后一个元素位置
使用left+(right-left)/2的时候会造成死循环,而使用left+(right-left)+1)/2不会造成死循环
总结:
1)如果循环条件忘了,自己可以画一些都比target数大的和都比target小的进行手写画图演示
2)如果说求中点坐标忘了,那么直接画出两个数都比target大或者都比target小进行演示,或者画出正确的结果进行演示;
class Solution { public int findleftIndex(int[] array,int target){ int left=0; int right=array.length-1; while(left
=target){ right=mid; } } if(left==right&&array[left]==target){ return left; }else{ return -1; } } public int findrightIndex(int[] array,int target){ int left=0; int right=array.length-1; while(left target){ right=mid-1; } } if(left==right&&array[right]==target){ return right; }else{ return -1; } } public int[] searchRange(int[] array, int target) { if(array==null) return new int[]{-1,-1}; int leftindex=-1; int rightindex=-1; leftindex=findleftIndex(array,target); rightindex=findrightIndex(array,target); return new int[]{leftindex,rightindex}; } } 下面这两个语句是不需要进行记忆的,查找区间左端点和查找区间右端点都是根据二段性来进行推导的
至于中间坐标万能语句:下面如果出现-1,上面就+1,下面如果是+1,上面就为0;
1)暴力破解:进行遍历整个数组使用暴力查找,时间复杂度就是O(N)
class Solution { public int[] searchRange(int[] array, int target) { int count=0,index=-1; for(int i=0;i
2)朴素二分查找算法:先来一个左指针,再搞一个右指针,求出中间值之后和target值作比较,从而划分出两段区间,虽然我们可以通过朴素二分来找到target的数,但是我们无法查询到这个数是左区间的下标或者是右区间的下标,所以找到mid的值之后,需要向两边扩散
但是假设数组中的所有元素全部等于target,此时mid向中间扩散,极端情况下查找的时间复杂度仍然是O(N)
lass Solution { public int[] searchRange(int[] array, int target) { int left=0; int right=array.length-1; int leftindex=-1; int rightindex=-1; while(left<=right){ int mid=left+(right-left)/2; if(array[mid]>target) right=mid-1; else if(array[mid]
=0&&array[leftindex]==target) leftindex--; leftindex++; while(rightindex
三)二分查找常见习题:
一)X的平方根:
69. x 的平方根 - 力扣(LeetCode)
1)暴力破解:当这个数的平方和等于x的时候,就返回这个数,如果这个数的平方和大于x,那么就直接返回这个数的前一个数,从1从2从3开始依次进行计算出这些数的平方,从前向后依次暴力的进行枚举;
2)寻找二段性使用二分查找,可以找到一个数,这个数,这个数左边区间的值平方之后的值小于等于x,这个数右边区间的值平方之后值是大于x的,在这里肯定是使用不朴素的二分算法,因为最终的值的平方和可能是小于x,还有可能等于x;
lass Solution { public int mySqrt(int x) { if(x==0||x<1) return 0; long left=1; long right=x; while(left
x){ right=mid-1; } } return (int)left; } } class Solution { public int mySqrt(int x) { if(x==0||x<1){ return 0; } //防止溢出 long left=0, right=x; while(left
x){ right=mid-1; } } return left==0?1:(int)left; } }
二)搜索插入位置
35. 搜索插入位置 - 力扣(Leetcode)
二段性:根据题目的要求看看,把数组按照一定的规则划分成两部分
这个数的插入位置是第一个恰好比它大的数或者是数组的最后位置
所以最终要在数组中查询的值应该大于等于target值的
只需要找到大于等于target值的左端点就可以了,分析mid落在区间的时候,left指针和right指针应该怎么移动,直接就可以写代码了
class Solution { public int searchInsert(int[] array, int target) { if(target>array[array.length-1]) return array.length; int left=0; int right=array.length-1; while(left
=target) right=mid; else if(array[mid]
三)移除元素:
27. 移除元素 - 力扣(Leetcode)
这道题也是典型的一种数组划分数组分块的问题
class Solution { public int removeElement(int[] array, int val) { int left=-1; for(int right=0;right
四)山脉数组的峰顶索引:
852. 山脉数组的峰顶索引 - 力扣(Leetcode)
暴力破解:定义一个指针从right开始向后进行遍历,如果遇到array[right]>array[right+1]就直接返回right元素的下标
时间复杂度可以达到O(N)
二分查找:根据题目将数组划分成两部分:
1)最开始的元素和最后一个元素一定不是峰值
2)峰值包括峰值左边的元素都是满足array[i]>array[i-1]的,而峰值右边的元素都是满足array[i]>array[i-1]
class Solution { public int peakIndexInMountainArray(int[] array) { int left=0; int right=array.length-1; while(left
array[mid-1]) left=mid; if(array[mid] class Solution { public int peakIndexInMountainArray(int[] array) { int left=0,right=array.length-1; while(left
array[mid-1]){ left=mid+1; }else if(array[mid]
五)寻找峰值:
162. 寻找峰值 - 力扣(Leetcode)
一)暴力破解:
一开始的时候array[0]>array[1],此时array[0]就是峰值,因为此时array[-1]是等于负无穷大的
从第一个位置开始向后走,分情况讨论即可,时间复杂度是O(N),本体是严格无序的
class Solution { public int findPeakElement(int[] array) { if(array.length==1) return 0; if(array[0]>array[1]) return 0; for(int i=0;i
array[i+1]){ return i; } } return array.length-1; } } 二)二分查找优化:根据最近两个位置的元素来分析是上升的趋势还是下降的趋势再来进行判断左边还是右边一定有峰值从而来解决这道问题
二分查找算法:根据两个位置的值将数组划分成两段区间,一段区间是一定有结果,一段区间是可能没有结果也有可能有结果,我们就去一定有结果的那一段区间进行寻找,从而直接筛选掉没有结果的那一段区间
class Solution { public int findPeakElement(int[] array) { int left=0; int right=array.length-1; while(left
array[mid+1]) right=mid; if(array[mid]
六)寻找旋转排序数组中的最小值:
153. 寻找旋转排序数组中的最小值 - 力扣(LeetCode)
这个数组是严格满足二段性的:左边的区间都是大于array[right]的,右边的区间都是小于array[right]的
class Solution { public int findMin(int[] array) { int left=0; int right=array.length-1; while(left
array[right]){ left=mid+1; }else if(array[mid]
七)搜索旋转排序数组
33. 搜索旋转排序数组 - 力扣(LeetCode)
class Solution { public int search(int[] array, int target) { int left=0; int right=array.length-1; while(left<=right){ int mid=left+(right-left)/2; if(array[mid]==target){ return mid; } //根据峰值可以将数组分割成两部分,一部分的值都比array[0]大,另一部分都比array[0]小 //1.首先进行判断mid中间位置的值在哪一个区间,是左区间还是右区间 //2.如果mid是在左区间在来进行判断target是在[left-mid)之间还是在(mid到right]之间 //3.如果mid是在右区间在来进行判断target是在(mid,right]之间还是[left,mid)之间 if(array[mid]>=array[0]){//mid在左区间 if(target>=array[0]&&target
array[mid]){ left=mid+1; }else{ right=mid-1; } } } return -1; } }
八)寻找0到n中缺失的数字
下面的时间复杂度就是O(N)
1)直接遍历去找
2)哈希表
3)位运算:相当于是消消乐
4)高斯求和公式:也就是等差数列的求和公式,可以先使用等差数列求和公式将没有缺失数组中的所有元素的和计算出来,然后依次减去数组中的所有元素即可
class Solution { public int missingNumber(int[] nums) { int n=nums.length+1; int sum=n*(n-1)/2; for(int i=0;i
5)根据二段性,可以使用二分查找来进行计算:
这里面的二段性就是:从丢失的数字开始,数组左边的值都是和数组的下标是相等的,从丢失的数字右边,数字的值都和数组下标不相等,所以要进行寻找的就是右侧区间最左边的值的下标
class Solution { public int missingNumber(int[] array) { int left=0; int right=array.length-1; while(left
假设现在有数组:0 1 2 3,其实缺失的数字是4,但是根据二分查找算法最终left和right此时都是指向的是最后一个元素
九)搜索二维矩阵:
1)思路1:先扫描行,再扫描列,行和列进行二分查找,就是简单的针对于每一行和每一列进行二分查找
class Solution { public boolean findRowIndex(int[][] array,int target,int row){ //此时虽然进行检索每一行的值但是行号是不会发生改变的,只是需要固定每一行的下标即可 int left=0,right=array[0].length-1; while(left<=right){ int mid=left+(right-left)/2; if(array[row][mid]
target) right=mid-1; else return true; } return false; } public boolean findLineIndex(int[][] array,int target,int col){ //此时进行检索每一列的值,此时列号是不会发生改变的,但是此时的行号会发生改变 int left=0,right=array.length-1; while(left<=right){ int mid=(left+right)/2; if(array[mid][col]==target) return true; else if(array[mid][col] class Solution { public boolean findrowIndex(int[][] array, int target,int start,int len){ int left=start; int right=len-1; while(left
target){ right=mid-1; }else if(array[mid][start]<=target){ left=mid; } } return array[left][start]==target; } public boolean findlineIndex(int[] array, int target,int start,int len){ int left=0; int right=len-1; while(left target){ right=mid-1; }else if(array[mid]<=target){ left=mid; } } return array[left]==target; } public boolean searchMatrix(int[][] array, int target) { for(int i=0;i 2)思路2:行和列进行移动:
class Solution { public boolean searchMatrix(int[][] array, int target) { //每拿出一个矩阵,左下角的最后一个值永远是这个行的最小值,这个列的最大值 int row=array.length-1; int col=0; while(row>=0&&col
target){ row--; }else if(array[row][col]==target){ return true; }else{ col++; } return false; } }
十)搜索排序数组中的最小值(2)
154. 寻找旋转排序数组中的最小值 II - 力扣(LeetCode)
class Solution { public int findMin(int[] array) { int left=0; int right=array.length-1; while(left
array[right]){ left=mid+1; }else if(array[mid]<=array[right]){ right=mid; } } return array[left]; } } 这个题的致命问题就是如果出现array[mid]==array[array.length-1]的话,那么此时就无法进行判断mid是在左区间还是在右区间,此时就应该跳过第一段区间内大于等于X的值
十一)搜索旋转排序数组
81. 搜索旋转排序数组 II - 力扣(LeetCode)
class Solution { public boolean search(int[] array, int target) { int left=0; int right=array.length-1; int high=array[right]; int low=array[0]; while(left
=array[0]){ if(target>=low&&target
十二)搜索二维矩阵(2)
240. 搜索二维矩阵 II - 力扣(LeetCode)
由题意可知,二维数组每一行从左向右都是递增的,二维数组里面的每一列从上向下都是递增的,我们可以进行选取左上角的值作为基准值
class Solution { public boolean searchMatrix(int[][] array, int target) { int row=array.length-1; int col=array[0].length-1; int rowIndex=0,lineIndex=array[0].length-1; while(rowIndex<=row&&lineIndex>=0){ if(array[rowIndex][lineIndex]
target){ lineIndex--; }else{ return true; } } return false; } }