每周一算法之二分查找(Kotlin描述)

简述: 从这篇文章起就会开启另外一个系列就是上篇文章中提到的每周学习一个基本算法,会结合LeetCode上题目来作分析。解题的语言通常是Kotlin或Java. 若是涉及到一些有关Kotlin的知识点也会作一些介绍。若是平时就养成学习数据结构算法以及刷题的习惯,无论从此你是面试(愿今后不再是面试造火箭平时拧螺丝了)或在实际上工做中都会对你有很大帮助。这也是这个系列文章的目的。面试

1、时间复杂度

最坏时间复杂度 O(log n)算法

最优时间复杂度 O(1)数组

平均时间复杂度 O(log n)bash

2、基本思想

在一个有序的列表中,要查找的数与列表中间的位置相比,若相等说明找到了,若要查找的数大于列表中间的数,说明要查找的数可能在有序列表的后半部分;若要查找的数小于列表中间的数,说明要查找的数可能在有序列表的前半部分;而后相似上述操做缩短查找范围,最后直到查找到最后一个数,若是不等于要查找的数,那么查找范围就为空。数据结构

3、算法步骤

给定一个包含n个带值的元素数组或序列A[0], ... A[n-1],使A[0] <= ... <= A[n-1],以及目标值Target.app

  • 一、令 low = 0, high = n - 1
  • 二、若low > high, 则表示查找失败结束
  • 三、令mid(中间值元素)为 (low + high) / 2的值向下取整 (注意: 在具体实现中为了防止溢出,通常会采用 low + (high - low) / 2的值向下取整 或者直接采用位运算的移位运算来实现除2的操做。这个后面会有具体题目来讲明)
  • 四、若Target > A[mid], 令 low = mid + 1 (说明要查找的值在序列或数组后半部分(除去自身),移动low游标,缩小查找范围),并回到步骤2
  • 五、若是Target < A[mid], 令 high = mid - 1 (说明要查找的值在序列或数组前半部分(除去自身),移动high游标,缩小查找范围),并回到步骤2
  • 六、若是Target == A[mid], 查找成功并结束,返回mid下标值。

4、算法过程演示

5、代码实现(Kotlin语言描述)

二分查找算法主要有两种实现方式,一种是循环递推的方式,另外一种则是递归的方式函数

  • 一、 循环递推实现方式
  • 二、递归实现方式

6、为何Java中mid = (low + high) / 2方式会溢出

相信刷过LeetCode题目的小伙伴们可能会在刷二分查找的题目过程会遇到超过期间限制的错误 post

不知道你们有没有去分析过为何会获得超时啊,我都用了二分查找了时间复杂度都变成 O(log n) 呢,为啥还会超时呢。其实是int数据类型溢出致使出现负数,使得代码进入了死循环,而后致使超时,最后OJ给你个超出时间错误。

  • 一、出现问题的缘由

咱们能够肯定 lowhigh 都是非负数,那么也就是二进制表示的最高位符号位是0,而且lowhigh 都是31位二进制的整数性能

假设下面这种场景:学习

high = 0100 0000 0000 0000 0000 0000 0000 0000 = 1073741824 (Integer.MAX_VALUE的一半)
low  = 0100 0000 0000 0000 0000 0000 0000 0000 = 1073741824 (Integer.MAX_VALUE的一半)
复制代码

当执行 low + high 操做时,进行二进制运算,以下

high + low = 1000 0000 0000 0000 0000 0000 0000 0000
复制代码

针对上述high + low 运算的结果,若是是无符号的32位(4个字节)Integer来讲就表示 2147483648 (Integer.MAX_VALUE的大小);若是是有符号的32位(4个字节)Integer来讲就表示 -2147483648。 须要特别注意的是Java或Kotlin中是不支持无符号的Integer类型,只存在有符号的Integer类型

因此问题就来了,若是是在Java或Kotlin中 (low + high) / 2的值就变成了负数 -1073741824low = mid + 1, low就变成负数了。而后target的值会一直比mid要大 low就不断累加,直到low又累加到1073741824mid 又变成 -1073741824,不断往复进入了死循环致使超时。能够看下面这个例子:

运算结果:

  • 二、解决该问题的方式

针对上述问题,你可能看到两种解决问题的办法,一种是采用 low + (high - low) / 2,另外一种就是 (low + high) >>> 1 或 Kotlin中的 (low + high) ushr 1.

(high + low) >>> 1 = 0100 0000 0000 0000 0000 0000 0000 0000 = 1073741824
复制代码

一块儿来看下例子:

运行结果:

7、补充一下Kotlin和Java中的位运算的知识点

  • 一、Java中的 >>>>> (或Kotlin中的 ushrshr ) 的区别

实际上无符号右移运算符>>>(或kotlin中的ushr)和右移运算符>>(或kotlin中的shr)是同样的,只不过右移时左边是补上符号位,而无符号右移运算符是补上0

  • 二、Kotlin中的位运算

在Kotlin中抛弃了Java那种直接使用 >>>、>>、<<、&、~、|、^这些非语义化的符号来实现位运算,说真的这样符号对代码可读性确实下降了不少,看过源码小伙伴就知道,不少源码中为了追求代码的运行效率,每每会采用位运算,可是代码理解和读起来就有点费力了。然而很高兴的是Kotlin却采用一种更加语义化的中缀调用函数(infix)来实现位运算,可以作到真正的简明识义, 而且用起来就像是在使用运算符同样,可是它更加具备含义。

8、LeetCode上二分查找相关的题目(练一练)

注意: 在作二分查找题目以前,给几点建议。

  • 一、真正在作题过程不多会有直接写标准的二分查找的题目,通常都是须要变型,转化成二分查找的问题。因此掌握二分查找思想比掌握实现方式更重要。
  • 二、通常是二分查找去解题有个很明显的特征那就是 升序数组或有序数组,以及在一些查找数中对时间复杂度要求比较高,好比时间复杂度必须低于O(n), 很明显你不能直接用循环去作,二分查找的平均时间复杂度是O(log n) 明显低于 O(n), 可能就须要你考虑是否能用二分查找。
  • 三、还有一个典型使用二分查找的题目,就是求平方根或者求彻底平方数,有个通用结论是: 一个非负数n的平方根小于n/2 + 1。因此就转化了从[0,n/2 + 1]查找符合的平方根或彻底平方数。
  • 一、两数之和 II - 输入有序数组

  • 二、有效的彻底平方数

  • 三、x的平方根

  • 四、山脉数组的峰顶索引

  • 五、标准的二分查找

  • 六、寻找比目标字母大的最小字母

  • 七、猜数字大小

  • 八、第一个错误的版本

  • 九、求两个数组的交集

  • 十、两个数组的交集 II

欢迎关注Kotlin开发者联盟,这里有最新Kotlin技术文章,每周会不按期翻译一篇Kotlin国外技术文章。若是你也喜欢Kotlin,欢迎加入咱们~~~

Kotlin系列文章,欢迎查看:

原创系列:

Effective Kotlin翻译系列

翻译系列:

实战系列:

相关文章
相关标签/搜索