经典的二分查找法

Posted Jungle笔记

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了经典的二分查找法相关的知识,希望对你有一定的参考价值。

二分查找法是一种基础的算法,应用于在有序元素序列中查找目标值。二分查找法思路清晰,可以描述为以下几个步骤:

  1. 对于一个有序元素组成的序列arr[l...r],假设为从小到大排序,计算中间值mid=(l+r)/2;
  2. 比较目标值target与arr[mid]大小:
    1. target = arr[mid],返回mid,程序结束;
    2. target < arr[mid],更新r=mid-1,重复步骤1;
    3. target > arr[mid],更新l=mid+1,重复步骤1。

二分查找法

二分查找法的代码及其简单:
int binarySearch(int arr[], int n, int target){ assert(n >= 0); int l = 0, r = n - 1; while (l < r){ int mid = l + (r - l) / 2; if (arr[mid] < target){ l = mid + 1; } else if (arr[mid]>target){ r = mid - 1; } else{ return mid; } } return -1;}

其中,对mid的计算改为了mid = l + (r - l) / 2,主要是避免溢出。


问题


对于下面这样一个有序序列,用上面的代码分别查找12、50和95:


测试代码如下:

int main(){ int arr[10] = { 0,12,38,49,50,50,68,79,95,100}; printf("Position:"); for (int i = 0; i < 10; i++){ printf("%4d", i); } printf("
 "); for (int i = 0; i < 10; i++){ printf(" | "); } printf("
Value :"); for (int i = 0; i < 10; i++){ printf("%4d", arr[i]); } printf("

");
printf("BinarySearch "); printf("binarySearch(12), Position is %d ", binarySearch(arr, 10, 12)); printf("binarySearch(50), Position is %d ", binarySearch(arr, 10, 50));  printf("binarySearch(95), Position is %d ", binarySearch(arr, 10, 95));   system("pause"); return 0;}


运行结果如下图:

经典的二分查找法


12和95都可以准确找到位置,因为序列中他们都只出现过一次。50的位置其实也是准确的,只不过序列中有两个50,上述算法找出来的是第1个位置的50。这是因为第一次计算出来的mid位置4就正好是50,所以直接返回了4所以,可以理解如果是下面这样一个有序序列,使用上述代码查找50的位置,也会返回4.

经典的二分查找法


有时候这样可能并不是我们想要的结果。我们可能想查找序列中第一次出现目标值,或者最后一次出现目标值,甚至是查找一个有序序列中第一个不小于目标值的元素出现的位置,或者查找第一个不大于目标值的元素出现的位置


Ceil和Floor


为了解决上述两个问题,同样可以使用二分查找的思想设计ceil和floor函数。ceil意为天花板,即向上查找;floor意为地板,即向下查找。代码如下:

int ceil(int arr[], int n, int target){ int l = 0, r = n; while (l < r){ int mid = l + (r - l) / 2; if (arr[mid] <= target){ l = mid + 1; } else{ r = mid; } } assert(l == r); if (l - 1 >= 0 && arr[l - 1] == target){ return l - 1; } return l;}
int floor(int arr[], int n, int target){ assert(n >= 0); int l = -1, r = n - 1; while (l < r){ int mid = l + (r - l + 1) / 2; if (arr[mid] >= target){ r = mid - 1; } else{ l = mid; } } assert(l == r); if (l + 1 < n && arr[l + 1] == target){ return l + 1; } return l;}


接下来对上述两个函数做个测试:

int main(){ int arr[10] = {0,12,50,50,50,50,50,50,95,100}; printf("Position:"); for (int i = 0; i < 10; i++){ printf("%4d", i); } printf("
 "); for (int i = 0; i < 10; i++){ printf(" | "); } printf("
Value :"); for (int i = 0; i < 10; i++){ printf("%4d", arr[i]); } printf("

");
printf(" Ceil "); printf("ceil(12), Position is %d ", ceil(arr, 12, 12)); printf("ceil(50), Position is %d ", ceil(arr, 12, 50)); printf("ceil(80), Position is %d ", ceil(arr, 12, 80));
printf(" Floor "); printf("floor(12), Position is %d ", floor(arr, 12, 12)); printf("floor(50), Position is %d ", floor(arr, 12, 50)); printf("floor(80), Position is %d ", floor(arr, 12, 80)); printf(" ");
system("pause"); return 0;}

该测试中,我们使用ceil和floor函数分别对给定的有序序列中的12、50和80进行查找。其中,

  • 元素12在序列中存在,且仅有一个;

  • 元素50在序列的第2~第7个位置连续出现6次;

  • 元素80在序列中并不存在。


测试结果如下:

对于目标值50,ceil函数查找的结果为7,floor函数的查找结果为2。对于序列中不存在的元素80,ceil函数查找的结果为8,即95;而floor函数的结果为7。测试结果符合预期。


二分查找相关题目


LeetCode上关于二分查找的题目不少,比如:

  • 69.x的平方根

  • 153.寻找旋转排序数组中的最小值

  • 278. 第一个错误的版本

  • 540. 有序数组中的单一元素

  • ……


比如第540题:


这道题直接用位运算中的异或运算,将特别痛快:

int singleNonDuplicate(vector<int>& nums) { int ret = 0; for (int i = 0; i<nums.size(); i++){ ret ^= nums[i]; } return ret;}

但是题目要求时间复杂度为O(logn),上述代码的时间复杂度为O(n),不符合题目要求。时间复杂度为O(logn)多半就是二分了。一番分析后,代码如下:

int singleNonDuplicate(vector<int>& nums) { int l = 0; int r = nums.size() - 1; while (l<r){ int mid = l + (r - l) / 2; if (mid % 2 == 0){ if (nums[mid] == nums[mid + 1]){ l = mid + 2; } else if (nums[mid] == nums[mid - 1]){ r = mid - 2; } else{ return nums[mid]; } } else{ if (nums[mid] == nums[mid + 1]){ r = mid - 1; } else if (nums[mid] == nums[mid - 1]){ l = mid + 1; } else{ return nums[mid]; } } } return nums[l];}

二分查找法O(logn)的时间复杂度让它成为十分高效的算法。不过别忘了使用二分查找法的前提,即元素有序+数组

以上是关于经典的二分查找法的主要内容,如果未能解决你的问题,请参考以下文章

死磕算法之二分查找法

从0到1学算法二分查找法

从0到1学算法二分查找法

分治法应用之二分查找 快速排序递归排序

java 二分查找法

不光是查找值! "二分搜索"