系列文章导引
开源项目
本系列所有文章都将会收录到GitHub
中统一收藏与管理,欢迎ISSUE
和Star
。
GitHub传送门:Kiner算法算题记
二分查找算法
通过头尾指针确定查找区间,然后通过不断地缩小查找区间来缩小我们的查找范围,最终找到目标值。所以二分查找其实二分的是查找区间
。二分查找在每一次调整查找区间时,一定会保证如果存在目标值,那么这个目标值一定存在与我们调整后的查找区间当中。
PS:二分查找的条件是在有序数组中查找,无序数组需要从小到大排序后再进行二分查找
# 使用二分法查找x在arr数组中的位置,不存在则返回-1
x = 8
arr = [1,2,3,4,5,6,7,8,8,9,9]
# 首先,我们先定义头尾两个指针分别指向数组第一位和最后一位元素,头尾指针之间的数组就是我们本次的查找区间。而通过头尾指针,我们就能够确定这个查找区间中间值mid的位置,通过将中间值mid与目标值x进行对比,我们就可以确定下一个查找区间的范围。
# 当arr[mid]===x时,说明刚已经找到了目标值,直接返回mid作为目标值索引
# 当arr[mid]<x时,由于我们的数组时有序数组,因此我们可以确定,mid以及mid之前的所有值都比目标值x小,那么我们就应该将我们的搜索区间调整为mid+1 到 尾指针所指向的位置。
# 当arr[mid]>x时,原理同上,便可以确定我们下一次查找的区间为头指针 到 mid - 1的位置
# 就这样,我们每次查找区间缩小一半,相比起我们顺序查找来说,效率提高还是相当可观的
[ 1 , 2, 3 , 4 , 5 , 6 , 7 , 8 , 8 , 9 , 9 ]
^ ^ ^
| | |
头指针 mid 尾指针
# 如上,我们可以让x与mid所指向的数6对比,发现8比6大,我们就可以把查找区间缩小为:
[ 1 , 2, 3 , 4 , 5 , 6 , 7 , 8 , 8 , 9 , 9 ]
^ ^ ^
| | |
头指针 mid 尾指针
# 通过这一轮查找,发现mid所指向的值刚好与x相等,因此返回mid作为目标值的索引即可
接下来使用js实现一个最简单的二分查找算法
// 为了方便查看二分查找的过程,打印每一次二分的情况,可以观察一下控制台的输出结果便可以知道每一次二分查找的过程了
function outputBinarySearchProcess(arr, target, min, max, mid){
let idxLog = '';
let lineLog = '';
let valLog = '';
let arrowLog1 = '';
let arrowLog2 = '';
for(let i=0;i<arr.length;i++) {
idxLog+=`${i}\t`;
lineLog+='-\t';
valLog+=`${arr[i]}\t`;
if(i===min || i === max || i === mid) {
arrowLog1+="^\t";
} else {
arrowLog1+=" \t";
}
}
console.log("索引\t"+idxLog);
console.log(" \t"+lineLog);
console.log("数值\t"+valLog);
console.log(" \t"+arrowLog1);
console.log('\n');
}
/**
* 二分查找
* @param arr 待查找数组
* @param target 待查找目标值
* @param [size] 可选,待查找数组的长度,如果不需要查找整个数组的情况下可以指定长度
*
*/
function binarySearch(arr,target, size=arr.length){
// 定义头尾指针以及计算对应中间值
let min = 0, max = size - 1;
while(min<=max) {
// 计算中间值索引
let mid = (min + max) >> 1;
// 方便查看二分的过程输出一些辅助信息
outputBinarySearchProcess(arr, target, min, max, mid);
if(arr[mid] === target) return mid;
else if(arr[mid] < target) min = mid + 1;
else max = mid - 1;
}
return -1;
}
const arr = [0,1,2,3,4,5,6,7,8,9];
console.log("====================[查找8]===========================");
console.log(`查找8索引:`+binarySearch(arr, 8));// 8
console.log("====================[查找3]===========================");
console.log(`查找3索引:`+binarySearch(arr, 3));// 3
console.log("====================[查找7]===========================");
console.log(`查找7索引:`+binarySearch(arr, 7));// 7
console.log("====================[查找6]===========================");
console.log(`查找6索引:`+binarySearch(arr, 6));// 6
以上为标准的二分查找算法,之前还看到过一些其他的二分查找的骚操作,在大范围内使用二分查找,小范围内使用顺序查找。使用这种方式,可以巧妙的避免掉我们对于二分查找边界条件的处理,降低代码出错的几率。
// 为了方便查看二分查找的过程,打印每一次二分的情况,可以观察一下控制台的输出结果便可以知道每一次二分查找的过程了
// 其中︽代表头/尾指针与mid重合,︿代表头指针或尾指针或mid
function outputBinarySearchProcess(arr, target, min, max, mid){
let idxLog = '';
let lineLog = '';
let valLog = '';
let arrowLog1 = '';
let arrowLog2 = '';
for(let i=0;i<arr.length;i++) {
idxLog+=`${i}\t`;
lineLog+='-\t';
valLog+=`${arr[i]}\t`;
if(i===min || i === max || i === mid) {
if((i===min&&i==mid)||(i===max&&i==mid)){
arrowLog1+="︽\t";
}else{
arrowLog1+="︿\t";
}
} else {
arrowLog1+=" \t";
}
}
console.log("索引\t"+idxLog);
console.log(" \t"+lineLog);
console.log("数值\t"+valLog);
console.log(" \t"+arrowLog1);
console.log('\n');
}
/**
* 二分查找v1:大的区间范围使用二分查找,小的区间范围使用顺序查找
* @param arr 待查找数组
* @param target 待查找目标值
* @param [size] 可选,待查找数组的长度,如果不需要查找整个数组的情况下可以指定长度
*
*/
function binarySearchV1(arr,target, size=arr.length){
// 定义头尾指针以及计算对应中间值
let min = 0, max = size - 1;
while(max - min > 3) {
// 计算中间值索引
let mid = (min + max) >> 1;
// 如果待查找数值的数字都极大的话,有可能相加之后除以2就不是我们要的目标值了,可以按照下面方式优化
// 中间值 = 最小值 + 区间大小的一半
// let mid = min + (max-min) >> 1;
// 方便查看二分的过程输出一些辅助信息
outputBinarySearchProcess(arr, target, min, max, mid);
if(arr[mid] === target) return mid;
else if(arr[mid] < target) min = mid + 1;
else max = mid - 1;
}
outputBinarySearchProcess(arr, target, min, max, (min+max)>>1);
// 区间大小小于或等于3时,直接顺序查找
for(let i=min;i<=max;i++) {
if(arr[i] === target) return i;
}
return -1;
}
const arr = [0,1,2,3,4,5,6,7,8,9];
console.log("====================[查找8]===========================");
console.log(`查找8索引:`+binarySearchV1(arr, 8));// 8
console.log("====================[查找3]===========================");
console.log(`查找3索引:`+binarySearchV1(arr, 3));// 3
console.log("====================[查找7]===========================");
console.log(`查找7索引:`+binarySearchV1(arr, 7));// 7
console.log("====================[查找6]===========================");
console.log(`查找6索引:`+binarySearchV1(arr, 6));// 6
二分查找的泛型情况
二分查找的
0-1模型
(读:零一模型):情况一:
[0,0,0,0,0,0,1,1,1,1,1]
在上述数组中找到第一个1
解析:这种情况我们也是用双指针的方式来不断缩小查找区间。首先通过头尾指针计算出中间指针的索引,如果中间指针索引所指向的数字为0,那么说明中间指针mid和之前的数都为0,我们就把头指针移动到mid+1的位置。如果中间指针mid指向的数字是1,那么说明中间指针指向的值有可能就是我们要找的目标值,根据二分查找每次调整区间都必须把结果包含区间中(如果存在结果的话)的原则,此时因为mid有可能是我们的结果,所以需要将尾指针移动到mid所在的位置。循环上述操作,直到头尾指针中的一个与mid相遇就找到了我们要找的值的索引
情况二:
[1,1,1,1,0,0,0,0,0,0]
在上述数组中找到最后一个1
解析:这种情况我们可以把数组每一位数字“取反”,即将0变成1,1变成0,然后得出:[0,0,0,0,1,1,1,1,1,1],我们要找原数组最后一个1,就相当于找新数组第一个1的前一位,至于找第一个1的过程就跟情况一一样了。
上面简单的介绍了一下二分查找的0-1模型
,那么,既然说是模型,应该能够适合很多种情况的处理,那这个0-1模型
到底适合处理怎样的实际问题呢?
如:在数组[4,7,9,12,15,15,16,21,33,44,54]中查找第一个大于15的数的索引,看似跟我们的0-1模型
没啥关系,实际上,我们可以转换一下思维方式:将数组里面满足条件即大于等于15的数标记为1,不满足条件的数字标记为0,这样,我们就得到一个新的数组:[0,0,0,0,1,1,1,1,1,1,1,1],现在就变成了求取0-1模型
中第一个1的问题了。
从上面的例子中我们可以看出,0-1模型
确实是一个泛型情况,通过一定的思维转换能将很多问题转换成0-1模型
的问题来解决。思维转换的要点是:将满足条件的数据看成1,不满足条件的数据看成0,然后根据0-1模型的查找方式找到最终问题的解。
光说不练假把式,下面来用js实现一下0-1模型
// 为了方便查看二分查找的过程,打印每一次二分的情况,可以观察一下控制台的输出结果便可以知道每一次二分查找的过程了
// 其中︽代表头/尾指针与mid重合,︿代表头指针或尾指针或mid
function outputBinarySearchProcess(arr, target, min, max, mid){
let idxLog = '';
let lineLog = '';
let valLog = '';
let arrowLog1 = '';
let arrowLog2 = '';
for(let i=0;i<arr.length;i++) {
idxLog+=`${i}\t`;
lineLog+='-\t';
valLog+=`${arr[i]}\t`;
if(i===min || i === max || i === mid) {
if((i===min&&i==mid)||(i===max&&i==mid)){
arrowLog1+="︽\t";
}else{
arrowLog1+="︿\t";
}
} else {
arrowLog1+=" \t";
}
}
console.log("索引\t"+idxLog);
console.log(" \t"+lineLog);
console.log("数值\t"+valLog);
console.log(" \t"+arrowLog1);
console.log('\n');
}
/**
* 二分查找
* @param arr 待查找数组
* @param target 待查找目标值
* @param [size] 可选,待查找数组的长度,如果不需要查找整个数组的情况下可以指定长度
*
*/
function binarySearchV2(arr,target, size=arr.length){
// 定义头尾指针以及计算对应中间值
let min = 0, max = size - 1;
// 此处的终止条件从小于等于改为小于
while(max - min > 3) {
// 计算中间值索引
let mid = (min + max) >> 1;
// 如果待查找数值的数字都极大的话,有可能相加之后除以2就不是我们要的目标值了,可以按照下面方式优化
// 中间值 = 最小值 + 区间大小的一半
// let mid = min + (max-min) >> 1;
// 方便查看二分的过程输出一些辅助信息
outputBinarySearchProcess(arr, target, min, max, mid);
if(arr[mid] < target) min = mid + 1;
else max = mid;
}
outputBinarySearchProcess(arr, target, min, max, (min+max)>>1);
// 区间大小小于或等于3时,直接顺序查找
for(let i=min;i<=max;i++) {
if(arr[i] >= target) return i;
}
return -1;
}
const arr = [0,1,2,3,4,5,6,7,8,9];
console.log("====================[第一个大于等于5的数]===========================");
console.log(`第一个大于等于5的数的索引:`+binarySearchV2(arr, 5));// 5
console.log("====================[第一个大于等于2的数]===========================");
console.log(`第一个大于等于2的数的索引:`+binarySearchV2(arr, 2));// 2
console.log("====================[第一个大于等于8的数]===========================");
console.log(`第一个大于等于8的数的索引:`+binarySearchV2(arr, 8));// 8
console.log("====================[第一个大于等于22的数]===========================");
console.log(`第一个大于等于22的数的索引:`+binarySearchV2(arr, 22));// -1
二分中的数组与函数的关系
有一定编程基础的同学应该都是知道,数组获取某个值:F[x]=y
与函数获取某个值F(x)=y
都是为了通过x
获取目标映射值y
,其中,数组是从下标到值的映射,而函数式从入参到函数值(返回值)的映射。
我们刚刚讲了使用二分查找,通过数组的值求取对应的下标,那么,同理,我们是否能够通过函数值利用二分查找求解函数的入参呢?
答案是可以的,不过有一个先决条件。我们再使用二分查找查找数组中某个值的索引时,必须保证数组是有序的(即数组是单调的),因此,使用二分查找根据函数值逆推函数入参时,也要保证这个函数是单调函数
例如:F(x) = 2x
这个单调函数与单调数组[0,2,4,6,8,10],我们可以发现,这个单调函数的参数x如果与数组的索引对应,那么函数的值,也会跟数组索引对应的值相对应。如x传入2,函数值为4,而在数组中,索引2对应的值也是4。
这样,大家是否觉得单调数组和单调函数之间的界限没有那么明确了呢?感觉这两货就是一个东西呀。因此,在思维逻辑层面中,我们把单调函数当做是压缩的单调数组,而单调数组当做是展开的单调函数。函数使用的是计算资源,而数组使用的是存储资源,而计算资源和存储资源本质上没有什么差别,因此,在我们计算机中有这样一种说法叫做:用时间(计算资源)换空间(存储资源)或用空间(存储资源)换时间(计算资源)。任何可以应用于数组的算法,都可以应用于某种性质的函数上。