不仅仅是算法--二分查找(binary search)的心路历程
Posted 前端小苑
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了不仅仅是算法--二分查找(binary search)的心路历程相关的知识,希望对你有一定的参考价值。
精品技术文章,热门资讯第一时间送达
文章将结合面试的真实经历进行描述——你以为我想考你二分查找,其实我只想知道你是不是我要招的人?思路是否清晰?想法是否开阔?做法是否严谨?逻辑是否正确......
时间复杂度:白话讲就是我的代码运行需要多少时间,但是我没法一行行执行完数秒,如果需要36000秒那你肯定等不了,因此抽象出了一个公式用来表示耗费时间的多少,O(1), O(log n), O(n), O(n log n), O(n2), O(nk), O(2n),从左往右依次是最快到最慢,所以我们可以度量你写的算法大概是个什么级别。
图片摘自百度图片
概念:什么是二分(折半)查找(binary search)? 从一个有序(必须有序)的数组(必须是线性数组)中查找到关键字。假设一个整型数组中数字有序且不重复,那么我想找到一个关键值3是否存在,有什么方式?
第一想法那就是二分查找,NO!NO!NO!你真是高手,普通的人的想法应该是写一个for循环,遍历一遍,依次比较,然后我就知道它有没有了,并且知道它的索引在哪?那么恭喜你是一个正常人思维,写出了一个时间复杂度为N的算法。但是在大数据量的情况下,循环查找无法支撑,就需要更加有效的方式来进行处理。
面试:手写时一般我会写成整型数组,因为这样最简单,不简单怎么让面试官从最低点开始一点点往上发挥,难道一上来就直接困难难度,接着game over?
拉回正题,二分查找之所以常考,因为它简单,但是高效,并且应用广泛。可以找相等的关键数,也可以找大于小于的临界数,还可以找一个大于小于的范围。时间复杂度lgN,你知道它有多难么?因为平时你写的代码最多也就是时间复杂度为N的,一个for循环。lgN你可以想象为一个无限循环的半个N的时间复杂度(在半个N上再半个N),你说它牛皮不?
ok!开始写第一行代码,首先说明我接下来要写的只是一个局部方法,并不是一个完整的代码文件。
public int binarySearch(int[] arr, int target) {
}
方法名写完了,一般人接下来可能要写二分查找的头和尾了,然后加起来除以二了。少年我只想说你太天真了,接下来就看你严谨不严谨。
if (arr == null || arr.length == 0) return -1;
先看看数组是否有内容,写完它你已经是一个严谨的少年了!!接着定义数组的头和尾,找中间点:
int low = 0;
int high = arr.length - 1;
int mid = (low + high) / 2;
你以为到这里就完美了么?此处我们可以开阔想法,额外展示我们对计算机01运算的了解,位移运算才是它最快的实现方式,对于乘(除)2的幂次可以对应左(右)移幂次,改为:
int mid = (low + high) 1;
接下来开始进入关键环节:查找,如果mid索引处的值和我们要找的数相等,恭喜你,你已经找到了!如果小于你要找的值,由于数组有序,那你要找的target值一定在右边啦,同理大于就在左边啦。
if (arr[mid] == target) {
return mid;
} else if (arr[mid] < target) {
// 往右边找
low = mid + 1;
} else {
// 往左边找
high = mid -1;
}
主体部分写完了,外层还需要一个循环让它自己执行到找到目标值或这找完数组为止
while(low <= high) {
}
组合起来就是:
public int binarySearch(int[] arr, int target) {
if (arr == null || arr.length == 0) return -1;
int low = 0;
int high = arr.length - 1;
while(low <= high) {
int mid = (low + high) / 2;
if (arr[mid] == target) {
return mid;
} else if (arr[mid] < target) {
// 往右边找
low = mid + 1;
} else {
// 往左边找
high = mid -1;
}
}
return -1;
}
如果只是如此简单,你觉得面试官是大学考试级别么?刚才我们问了数字不重复的情况,比较简单,假设其中目标值有重复我想知道其中目标值的索引范围,你有什么方法么?
现在我们来拆分这个问题的真正要问什么?普通二分查找,要找到目标值即可,现在目标值有重复,要给出范围,也就是要找到重复值大一个数的位置(找上限)和小一个数的位置(找下限),两者即是要找的范围。当问题简化后,其实会发现很简单,以其中上限为例分析:
因为数组有序(小到大),所以上限一定就在最后一个目标值的右侧,也就是在普通二分法的基础上,当我们找到目标时并不停止,由于要找大于目标的位置,那么此时应该继续向右查找,直到跳出循环的条件值为止,然后返回low索引
(为什么返回low索引,是因为找到上限的前提,一定是先找到了目标值,此时的索引移动方向,一定是low不断的的逼近上限,low的值一定是 <= mid值的,一旦mid所在值 > 目标值 ,便会变成high向左移动,由于跳出循环的条件是high < low值,并且low的值一定是小于等于上限值的,说明此时low的索引就是我们要找的上限值)
public int searchBigger(int[] arr, int target) {
if (arr == null || arr.length == 0) return -1;
int low = 0;
int high = arr.length - 1;
while(low <= high) {
int mid = (low + high) >> 1;
if (arr[mid] <= target) {
// 往右边找
low = mid + 1;
} else {
// 往左边找
high = mid -1;
}
}
return low;
}
同理,找下限值:
public int searchSmaller(int[] arr, int target) {
if (arr == null || arr.length == 0) return -1;
int low = 0;
int high = arr.length - 1;
while(low <= high) {
int mid = (low + high) >> 1;
if (arr[mid] < target) {
// 往右边找
low = mid + 1;
} else {
// 往左边找
high = mid -1;
}
}
return high;
}
将两者组合即得到了范围值。笔者当时在面试时并不是这样作答的,而是找到目标值后向前向后遍历,获取到上限和下限,但是这样做最大的缺点就是,循环遍历是时间复杂度为O(N)的做法,相当于在复杂度lgN的复杂度中退化到O(N)的复杂度,得到了结果却不是最佳方案,上述,调用了两次函数,但是每次时间复杂度都是O(lgN),2O(lgN)约等于O(lgN),所以解法较优。
结束语:时间复杂度会专门拿一个帖子来讲,其中也会分析为什么二分查找的时间复杂度为O(lgN)
以上是关于不仅仅是算法--二分查找(binary search)的心路历程的主要内容,如果未能解决你的问题,请参考以下文章