算法通关村第9关——逢试必考的二分查找(青铜)
前言:
查找算法是一类常见的算法问题,其特点是根据给定的条件在集合中寻找指定的元素或者确定元素是否存在。查找算法的特点包括:
- 搜索空间:查找算法通常在一个有序的数据集合或者搜索空间中进行查找操作。
- 搜索目标:查找算法的目标是找到满足特定条件的元素,或者确定指定元素的存在与否。
- 时间复杂度:不同的查找算法具有不同的时间复杂度,影响着算法的执行效率。
常见的查找算法:
- 线性查找:从集合的第一个元素开始逐个遍历,直到找到目标元素或者遍历完整个集合。
- 二分查找:在有序数组或有序列表中通过不断缩小搜索范围,将目标值与中间元素进行比较,直到找到目标元素或者搜索范围为空。
- 插值查找:根据目标元素与集合中最大值和最小值之间的比例,估算目标元素的位置,并通过比较缩小搜索范围。
- 哈希表查找:通过哈希函数将元素映射为唯一的哈希值,然后根据哈希值进行查找。
- 树结构查找:例如二叉搜索树、平衡二叉搜索树(如AVL树、红黑树)、B树等,利用树的性质提高查找效率。
1. 基本查找(线性查找)
线性查找,也称为顺序查找,是一种简单直接的查找算法,适用于无序或有序数据集合中查找目标元素的操作。其特点包括:
- 顺序遍历:线性查找从集合的第一个元素开始,逐个比较待查找元素和集合中的每个元素,直到找到目标元素或者遍历完整个集合。
- 适用范围广:线性查找适用于任何类型的数据集合,无论是否有序。
- 时间复杂度:线性查找的时间复杂度为 O(n),其中 n 是数据集合中元素的个数。
线性查找的优点是简单易实现,适用于小规模数据集合或者数据集合无序的情况。然而,它的缺点是查找效率较低,当数据集合较大时,需要进行大量的比较操作,导致查找速度相对较慢。
public class LinearSearch {
public static int linearSearch(int[] arr, int target) {
for (int i = 0; i < arr.length; i++) {
if (arr[i] == target) {
return i; // 找到目标元素,返回索引位置
}
}
return -1; // 目标元素不存在,返回-1
}
public static void main(String[] args) {
int[] arr = {4, 2, 8, 5, 1, 7};
int target = 5;
int result = linearSearch(arr, target);
if (result != -1) {
System.out.println("目标元素 " + target + " 存在于数组中,索引位置为 " + result);
} else {
System.out.println("目标元素 " + target + " 不存在于数组中");
}
}
}
以上代码通过定义 linearSearch
方法来实现线性查找功能。在 main
方法中,创建一个整数数组 arr
和目标元素 target
,然后调用 linearSearch
方法进行查找操作。如果找到目标元素,则打印出其存在于数组中的索引位置;否则,打印出目标元素不存在的信息。
例如,在上述代码中,目标元素 5 存在于数组中,输出的结果将会是:“目标元素 5 存在于数组中,索引位置为 3”。
2. 二分查找与分治
分治是一种算法设计策略,将问题划分为若干个子问题,并将子问题的解合并起来得到原问题的解。
在计算机科学中,分治法是一种很重要的算法。字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。这个技巧是很多高效算法的基础,如二分搜索、排序算法(快速排序,归并排序)等等……
任何一个可以用计算机求解的问题所需的计算时间都与其规模有关。问题的规模越小,越容易直接求解,解题所需的计算时间也越少。例如,对于n个元素的排序问题,当n=1时,不需任何计算。n=2时,只要作一次比较即可排好序。n=3时只要作3次比较即可,…。而当n较大时,问题就不那么容易处理了。要想直接解决一个规模较大的问题,有时是相当困难的。
二分查找是一种通过不断将搜索范围缩小一半的方式,快速查找目标元素的算法。
所以,二分查找是一种典型的分治算法。它的基本思想是将有序数组或有序列表等数据集合不断分割成两半,然后根据目标值与中间元素的大小关系,在左半部分或右半部分继续进行查找,直到找到目标元素或者搜索范围为空。
2.1 循环的方式
使用循环方式实现二分查找法
public class BinarySearch {
public static int binarySearch(int[] arr, int target) {
int left = 0;
int right = arr.length - 1;
while (left <= right) {
int mid = (left + right) / 2;
if (arr[mid] == target) {
return mid; // 找到目标元素,返回索引位置
} else if (arr[mid] < target) {
left = mid + 1; // 目标元素在右半部分,更新左边界
} else {
right = mid - 1; // 目标元素在左半部分,更新右边界
}
}
return -1; // 目标元素不存在,返回-1
}
}
但是这段代码并不够好,因为在计算机中,除的效率非常的低,一般可以使用移位运算来代替
将:
int mid = (left + right) /2;
换成
int mid = (left + right)>>1;
如果这样的话,能得到80分,面试官可能会继续问,还会有什么问题。问题就是假如low
和high很大的话,low + high可能会溢出。因此我们可以这么写:
int mid = left+(right - left)>>1;
你觉得可以得到90分,很可惜是0分,因为当你测试的时候,可能会出现死循环,例如原
始序列是1到8,搜索3的时候就死循环了,为什么呢?
这是因为移位的运算符>>优先级比加减要低,解决方法也很简单,加括号就行了。
int mid = left+((right - left) >> 1);
不够这里还有一个需要注意的问题,关于二分查找
while (left <= right)
这个条件:写二分法,区间的定义一般为两种,左闭右闭即[left, right],或者左闭右开即[left, right)。
- 在左闭右闭区间中,循环条件是
left <= right
,表示当左边界与右边界相等时,仍然需要继续查找。而且在更新左右边界时,需要将左边界加一或右边界减一,因为左闭右闭区间包括了边界上的元素。- 在左闭右开区间中,循环条件是
left < right
,表示当左边界大于等于右边界时,即搜索范围为空时,停止查找。在更新左边界时,仍然使用mid + 1
,但在更新右边界时,直接使用mid
,因为右边界是开区间,不包括边界上的元素。
2.2 递归的方式
简单,不多说
public static int binarySearch2(int[] array, int low, int high, int target) {
//递归终止条件
if (low < high) {
int mid = low +(high-low)>>1;
if (array[mid] == target) {
return mid; // 返回目标值的位置,从1开始
}
if (array[mid] > target) {
// 由于array[mid]不是目标值,因此再次递归搜索时,可以将其排除
return binarySearch2(array, low, mid - 1, target);
} else {
// 由于array[mid]不是目标值,因此再次递归搜索时,可以将其排除
return binarySearch2(array, mid + 1, high, target);
}
}
return -1; //表示没有搜索到
}
3. 元素中有重复的二分查找法
假如在上面的基础上,元素存在重复,如果重复则找左侧第一个,请问该怎么做呢?
如果是左侧,就当找到target之后往左遍历
如果是右侧,就找往右遍历
public static int search1(int[] nums, int target) {
if (nums == null || nums.length == 0)
return -1;
int left = 0;
if (nums[0] == target) {
return 0;
}
int right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else {
//找到之后,往左边找
while (mid != 0 && nums[mid] == target)
mid--;
return mid + 1;
}
}
return -1;
}