聊聊二分算法
前言: 二分查找作为很常见的一种算法,基本思想是定义头和尾双指针,计算中间的index指针,每次去和数组的中间值和目标值进行比较,如果相同就直接返回,如果目标值小于中间值就将尾指针重新赋值为中间值-1,头指针不变,相当于从左边区域去找;如果目标值大于中间值就将头指针赋值为中间值+1,尾巴指针不变,相当于从右边区间去找元素.依次循环这个过程,将区间一层层的压缩,最终就可以得到最终的目标值的index.
目录
一:二分使用条件和时间复杂度
二:二分的基本写法
三:二分的相关问题
四:总结
正文
一:二分的使用条件和时间复杂度
1.1:①必须是单调递增或者递减数组
注意这里是单调递增或者递减,而不是全部递增或者递减,这点很重要。如果是完全乱序的数组,那么二分算法就会完全失效。二分的本质就是借助于单调性然后比较中间节点的值来达到缩小范围去查询元素的目的。如果是乱序的,那么就无法比对中间值来达到缩减区间的目的
②必须是线性结构
对于像图二叉树等结构,二分是不合适的,因为没办法去用二分,这是由结构决定的
1.2:二分的时间复杂度
二分查找的时间复杂度是o(logn),关于二分的时间复杂度是怎么计算出来的呢?假如数组的长度是n,每次进行二分查找的时候是n/2,下次是n/4,再一次是n/8。在最坏的情况下,每次都找不到目标值,那么就有可以设查找的次数为T,(n/2)^T=1;则T=logn,底数是2,时间复杂度为O(logn)
二:二分的基本写法
2.1:图解二分
上述的例子是在{1,3,5,6,12,14,19}这个数组中寻找target=3这个元素,因为数组的长度是7,中点的index=3,值为6,发现3小于6,所以最终值肯定在中点的左半区域里,因此右指针左移动=2,接下来是0、1、 2这三个元素中查找,找到中点3,发现3=target,因此直接返回最终index=1。分析其过程,发现一共用了三次,就完成了元素的查找,总体来说效率还是很高的。
2.2:二分查找的标准写法
1 /** 2 * 二分查找 3 * 4 * @param nums 5 * @param target 6 * @return 7 */ 8 public int binarySearch(int[] nums, int target) { 9 10 int lo = 0; 11 int hi = nums.length - 1; 12 while (lo < hi) { 13 int middle = (low + hi) / 2; 14 if (nums[middle] < target) { 15 lo = middle + 1; 16 } else if (nums[middle] > target) { 17 hi = middle - 1; 18 } else { 19 return middle; 20 } 21 } 22 return -1; 23 }
三:二分变种问题
3.1:找出有重复数据的数组元素的第一个值
在1.1里谈了二分的适用条件,但是需要注意的一点是,并没有排除到数组没有重复的元素,在有重复元素的情况下二分可能会存在一个问题:查找出来的元素并不是第一次出现的,假如我们要求必须查找出来第一次出现的元素,那二分又怎么写呢?
首先思考一点:找出重复数组的第一个值,那么这个值肯定是第一个找出数字的index左侧,当中间值等于目标值的时候我们不能立刻返回,因为此时它并不一定是第一次出现的(当然它也有可能就是第一次出现),之后我们需要将它继续划分,直到left>right的值后,返回当前的left左边的值就是它第一次出现的位置(如果元素确实存在的话)。每次去查找元素的时候,我们寻找目标值的时候就需要一直向左侧逼近,当发现在这个值等于目标值的时候不能立刻返回,必须再次移动区间,直到逼近目标值的index再返回
1 /** 2 * 找到重复元素出现的第一个值 3 * 4 * @param array 5 * @param target 6 * @return 7 */ 8 public static int findFirstBinarySearch(int[] array, int target) { 9 int lo = 0; 10 int hi = array.length - 1; 11 12 while (lo <= hi) { 13 int mid = (lo + hi) / 2; 14 //注意这里不再有array[mid]=target return mid; 15 if (array[mid] >= target) { 16 hi = mid - 1; 17 } else { 18 lo = mid + 1; 19 } 20 } 21 //防止lo越界 并且判断lo的值 22 if (lo < array.length && array[lo] == target) { 23 return lo; 24 } 25 26 return -1; 27 }
3.1:找出有重复数据的数组元素的最后一个值
找到最后一个元素,那么它出现的值肯定在找出的第一个值的右侧,同样在找到middle值等于目标值的时候不能立刻返回,指针还需要继续移动.因此就需要将值进行向右逼近,直到找到middle等于目标值的最后一个,此时返回的index一定是目标值的最后一个(假如存在目标值的话),最终取right索引,因为在条件不成立的时候,目标的索引值肯定在最终肯定是在middle的右边
/** * 找出数组中最后一个重复的数字 * @param array * @param key * @return */ static int findLastBinarySearch(int[] array, int key) { int lo = 0; int hi = array.length - 1; while (lo <= hi) { int mid = (lo + hi) / 2; if (array[mid] <= key) { //尽可能在右半区域里找 lo = mid + 1; } else { hi = mid - 1; } }
// if (hi >= 0 && array[hi] == key) { return hi; } return -1; }
3.3:求一个数的平方根
初步看这道题,很难想到用二分去做。但是仔细一想,二分可以解决这个问题的,假如数字是8,那么我们可以分为一个数组为[1,2,3,4,5,6,7,8];这个数组属于单调递增的,完全满足二分的使用条件,题目要求返回的是整数部分,因此8的平方根肯定在这个数组中的某一个数字,每个数的平方也是单调自增的,因此完全可以用二分查找来解决这个问题:
public int sqrt(int n) { //0和1返回它本身 if (n==0||n==1){ return n; } //左指针 int lo = 0; //右指针 一个数的平方根肯定小于这个数的一半 int hi = n / 2; //存储结果值 默认-1 int res = -1; while (lo <= hi) { //取中间值 int mid = lo + (hi - lo) / 2; //计算平方 int square = mid * mid; if (square == n) { return mid; } else if (square < n) { //对于非完全平方根 结果肯定是在左区间 res = mid; lo = mid + 1; } else { hi = mid - 1; } } return res; }
3.4:旋转数组的最小数字
在1.1中讨论过二分的使用条件,是单调递增,注意是单调递增,并不是全部递增。在旋转数组的最小数字这个问题中,它的整体数组并不是全量递增或者递减的,但是它依然适用于二分,不过此时需要将原二分进行改造一下:
public int minarrayinRotationArray(int[] numbers) { //左边指针 int lo = 0; //右边指针 int hi = numbers.length - 1; while (lo < hi) { //中间指针 int middle = (lo + hi) / 2; //中间值大于右边指针值 说明寻找的旋转点一定在右区间 if (numbers[middle] > numbers[hi]) { lo = middle + 1; //中间值小于右边指针值 说明旋转点一定在左区间 } else if (numbers[middle] < numbers[hi]) { hi = middle; //如果中间值等于右边指针值 无法确定旋转点在哪 右指针减一 缩小区间 } else { hi--; } } return numbers[lo]; }
四:总结
本篇博客介绍了常见二分方法的思想以及时间复杂度的由来,还有二分的基本变形题,在找第一个出现的目标值和最后一个目标值的时候如何用二分去找。以及二分不太容易想到的寻找一个数的平方根和旋转数组的最小值,解决这两个问题能够帮助更加深刻的理解二分法。二分作为一种常见的算法,能够知道普通的二分还是不够的,灵活掌握并运用二分并解决实际中出现的问题,是值得我们思考的。