文章目录
把今天最好的表现当作明天最新的起点…….~
引言
当遇到查找特定元素的时候,最容易想到的就是暴力解法:直接遍历。这种做法简单粗暴,可以解题,但是时间复杂度过高,所以我们可以用二分法来提高效率。我周围的人几乎都认为二分查找很简单,但事实真的如此吗?二分查找真的很简单吗?并不简单。个人认为其可以这样理解:思路很简单,细节是魔鬼。
一、基本查找
在开始之前,先让我们来了解一下什么是基本查找。基本查找,也称之为线性查找、顺序查找。其算法核心就是 从0索引开始挨个往后查找,如果找到了,就给出下标值。利用基本查找,可以查询某个元素是否存在。例如,在n个元素的数组array中寻找特定的数字num,采用顺序查找法,代码实现如下所示:
public class SearchAlgorithm {
public static void main(String[] args) {
int[] numArray = {34, 56, -57, 68, 346, 345, 35, 23, 123, 40};
System.out.println("下标为:" + search(numArray, number));
}
public static int search(int[] numArray, int number) {
for (int i = 0; i < numArray.length; i++) {
if (numArray[i] == number) {
return i;
}
}
return -1;
}
}
二、二分查找算法
2.1 算法介绍
大家都应该玩过猜数字的游戏吧!给定一个数字的范围随机抽取一个数字,然后玩家轮流猜数字,猜错时告诉玩家结果数字是大于猜测数字还是小于。那么,该怎么猜数字最快得出答案呢?当然就是折半查找了。为什么说这样效率最高呢?因为每一次选择数字,无论偏大还是偏小,都可以让剩下的选择范围缩小一半。例如,给定范围0到1000的整数,第一次我们选择500,发现偏大了,那么下一次的选择范围,就变成了1到499,如下图所示:
第二次我们选择250,发现还是偏大了,那么下一次的选择范围,就变成了1到249,如下图所示:
第三次我们选择125,发现偏小了,那么下一次的选择范围,就变成了126到249,如下图所示:
以此类推,直到最终找到想要的元素,或者选择范围等于0为止。上述这个过程,就是所谓的二分查找算法了。
二分查找,故此也称为折半查找(Binary Search),是一种效率较高的查找方法。它要求线性表必须采用顺序存储结构,且表中元素按关键字有序排列。二分查找的基本思想是将n个元素分成大致相等的两部分,通过比较中间元素与目标值,不断缩小查找范围,直至找到目标值或确定目标值不存在于数组中。
注:使用二分查找的前提条件是,数组已经是有序的。
2.2 算法实现
二分查找是一种高效的搜索算法,其原理是假设数组中元素是按升序排列,不断将查找范围逐渐减半,快速定位目标元素,直到找到目标元素或确定目标元素不存在。算法思路如下:
- 确定搜索范围:首先确定整个有序数组的搜索范围,即左边界和右边界。通常初始时左边界为数组的第一个元素索引,右边界为数组的最后一个元素索引。
- 计算中间元素:计算左边界和右边界的中间索引,可以使用
(low + high) / 2
进行计算,查找到目标列表的中间元素,这个中间元素将用于与目标元素进行比较。 - 比较与目标元素:
- 如果目标元素等于中间元素,则找到了目标,返回中间元素的索引。
- 如果目标元素小于中间元素,则说明目标可能在左半边,更新右边界为中间元素的前一个索引。
- 如果目标元素大于中间元素,则说明目标可能在右半边,更新左边界为中间元素的后一个索引。
- 如果在某一步骤数组为空,则代表要查找的元素不在目标列表中。
- 缩小搜索范围:根据上一步的比较结果,缩小搜索范围。
- 如果目标在左半边,就在左半边继续进行二分查找;
- 如果目标在右半边,就在右半边继续进行二分查找。
- 重复这个过程,不断缩小搜索范围,直到找到目标元素或搜索范围为空。
- 重复步骤:重复执行步骤 2 到步骤 4,直到找到目标元素或搜索范围为空。如果搜索范围为空,说明目标元素不存在于数组中。
以有序数组 {10, 14, 19, 26, 27, 31, 33, 35, 42, 44} 、查找元素 33为例,逐步图解何为二分查找,如下所示:
- 初始状态:
- 第一轮查找:根据 27<33,可以判定 33 位于 27 右侧的区域,更新搜索区域为元素 27 右侧的区域。
- 第二轮查找:35>33,可以判定 33 位于 35 左侧的区域,更新搜索区域。
- 第三轮查找:31<33,可以判定 33 位于 31 右侧的区域,更新搜索区域。
- 第四轮查找:搜索区域内中间元素的位置是 [(7+7)/2]=7,因此中间元素是 33,此元素就是要找的目标元素。
2.3 代码实现
二分查找并不是只是简简单单的去判断一个数组中是否存在目标值,它是一种解决问题的思想。算法的核心是在于利用二分思想,每次都与区间的中间数据比对大小,缩小查找区间的范围。算法的思路就是设定两个指针start、end 分别指向数组元素的首、尾两端,然后比较数组中间结点和待查找元素。如果待查找元素小于中间元素,那么表明带查找元素在数组的前半段,那么将 end=mid-1;如果待查找元素大于中间元素,那么表明该元素在数组的后半段,将 start=mid+1;如果中间元素等于待查找元素,那么返回mid的值。
简单的二分查找算法的代码如下:
public class SearchAlgorithm {
public static void main(String[] args) {
int[] numArray = {2, 4, 5, 7, 9, 11, 16, 23, 45, 67};
System.out.println("元素【2】下标为:" + binarySearch(numArray, 5));
System.out.println("元素【23】下标为:" + binarySearch(numArray, 23));
System.out.println("元素【1】下标为:" + binarySearch(numArray, 1));
System.out.println("元素【110】下标为:" + binarySearch(numArray, 110));
}
public static int binarySearch(int[] numArray, int target) {
if (numArray == null || numArray.length == 0) {
return -1;
}
// 通过 start、end来表示当前搜索范围的左右边界。初始时,start 指向数组的第一个元素索引,end 指向数组的最后一个元素索引。
int start = 0, end = numArray.length - 1;
while (start <= end) {
// 计算出当前搜索范围的中间索引 mid,这个中间索引对应的元素就是我们要和目标元素进行比较的值。
int middle = start + (end - start) / 2;
// 用 middle 指向的元素和要查找的元素进行比较。
if (numArray[middle] > target) {
// 如果大于要查找的值,那说明要查找的元素在[start, middle-1]之间
end = middle - 1;
} else if (numArray[middle] < target) {
// 如果小于要查找的值,那要查找的值肯定在[middle+1, end]之间
start = middle + 1;
} else {
// 如果等于,则找到目标值,直接返回下标
return middle;
}
}
// 如果循环结束仍未找到目标元素,返回-1,表示未找到
return -1;
}
}
二分查找算法除了用上面的循环实现,实际上还可以用递归实现。简单二分查找算法的递归代码如下:
public class SearchAlgorithm {
public static void main(String[] args) {
int[] numArray = {10, 14, 19, 26, 27, 31, 33, 35, 42, 44};
int number = 33;
System.out.println("下标为:" + binarySearch(numArray, number, 0, numArray.length - 1));
}
public static int binarySearch(int[] numArray, int target, int start, int end) {
if (numArray == null || start > end || target < numArray[start] || target > numArray[end]) {
return -1;
}
int center = start + (end - start) / 2;
if (numArray[center] == target) {
// 如果等于,则找到目标值,直接返回下标
return center;
} else if (numArray[center] < target) {
// 如果小于,则去右半边继续找
return binarySearch(numArray, target, center + 1, end);
} else {
// 如果大于,则去左半边继续找
return binarySearch(numArray, target, start, center - 1);
}
}
}
上述的是二分查找的最基础、最基本的形式,这是一个标准的二分查找,用于查找可以通过访问数组中的单个索引来确定的元素。
2.4 算法复杂度
二分类似于猜数字游戏,每次都猜给定区间的中间值,但是前提条件是此序列已经排好序了。如果猜小了,下一次就到右区间猜;如果猜大了,下次就到左区间猜。这样逐渐逼近正确的结果,每次缩小一半的范围,因此 二分查找的时间复杂度是O(log(n)),最坏情况下的时间复杂度是O(n),其中 n 是数组的长度。
三、算法示例
根据前文实现的简单二分查找代码,我们知道区间的中间元素跟要查找的元素的大小关系有三种情况:大于、小于、等于。对于中间元素大于要查找的元素的情况,我们需要更新 high= mid-1
;对于中间元素小于要查找的元素的情况,我们需要更新 low=mid+1
。这两点和简单二分查找代码一样,但是当中间元素等于要查找的元素时,二分查找算法的变形有着不同的处理形式,毕竟不同的人有着不同的解决方法嘛!
3.1 查找第一个值等于给定值的元素
要使用二分查找算法来查找第一个值等于给定值的元素,我们可以在标准的二分查找算法上进行一些修改。具体来说,当我们在数组中找到一个等于目标值的元素时,我们不会立即返回,而是继续向左搜索,以确保我们找到的是第一个这样的元素。以下是一个Java方法的示例,它使用二分查找来查找第一个值等于给定值的元素的索引:
public class SearchAlgorithm {
public static void main(String[] args) {
int[] numArray = {2, 4, 5, 9, 11, 16, 16, 45, 67};
int number = 116;
System.out.println("查找第一个值等于【" + number + "】的元素下标:" + binarySearch(numArray, number));
}
public static int binarySearch(int[] numArray, int target) {
if (numArray == null || numArray.length == 0) {
return -1;
}
int start = 0, end = numArray.length - 1, result = -1;
while (start <= end) {
int middle = start + (end - start) / 2;
if (numArray[middle] > target) {
end = middle - 1;
} else if (numArray[middle] < target) {
start = middle + 1;
} else {
result = middle;
end = middle - 1;
}
}
return result;
}
}
在这个示例中,binarySearch
方法使用二分查找算法来查找目标值在数组中的第一个出现位置。当我们找到一个等于目标值的元素时,我们更新结果result
并继续向左搜索,直到start
大于end
,此时我们已经搜索了整个可能包含目标值的区域。
3.2 查找最后一个值等于给定值的元素
要利用二分查找算法来查找最后一个值等于给定值的元素,我们可以在标准的二分查找算法上进行修改。当找到一个等于目标值的元素时,我们不会立即返回,而是继续向右搜索,以确保找到的是最后一个这样的元素。以下是一个Java方法的示例,它使用二分查找来查找最后一个值等于给定值的元素的索引:
public class SearchAlgorithm {
public static void main(String[] args) {
int[] numArray = {2, 4, 5, 9, 11, 16, 16, 45, 67};
int number = 16;
System.out.println("查找最后一个值等于【" + number + "】的元素下标:" + binarySearch(numArray, number));
}
public static int binarySearch(int[] numArray, int target) {
if (numArray == null || numArray.length == 0) {
return -1;
}
int start = 0, end = numArray.length - 1, result = -1;
while (start <= end) {
int middle = start + (end - start) / 2;
if (numArray[middle] > target) {
end = middle - 1;
} else if (numArray[middle] < target) {
start = middle + 1;
} else {
result = middle;
start = middle + 1;
}
}
return result;
}
}
在这个示例中,binarySearch
方法使用二分查找算法来查找目标值在数组中的最后一个出现位置。当我们找到一个等于目标值的元素时,我们更新结果result
并继续向右搜索,直到start
大于end
,此时我们已经搜索了整个可能包含目标值的区域。
3.3 查找第一个值大于等于给定值的元素
要利用二分查找算法来查找第一个值大于等于给定值的元素,我们可以在标准的二分查找算法上进行修改。当找到一个元素大于等于目标值时,我们需要确保它是第一个这样的元素,这通常意味着我们需要向左检查以确保没有更小的元素也满足条件。但是,由于我们只需要找到第一个这样的元素,一旦我们找到一个满足条件的元素,我们就可以停止搜索了。以下是一个Java方法的示例,它使用二分查找来查找第一个值大于等于给定值的元素的索引:
public class SearchAlgorithm {
public static void main(String[] args) {
int[] numArray = {2, 4, 5, 9, 11, 16, 16, 16, 45, 67};
int number = 46;
int index = binarySearch(numArray, number);
System.out.println("查找第一个大于等于【" + number + "】的元素下标:" + (index > -1 ? numArray[index] : index));
}
public static int binarySearch(int[] numArray, int target) {
if (numArray == null || numArray.length == 0) {
return -1;
}
int start = 0, end = numArray.length - 1, result = -1;
while (start <= end) {
int middle = start + (end - start) / 2;
if (numArray[middle] >= target) {
// 找到第一个大于等于目标值的元素,但可能不是最左边的,尝试向左搜索,看是否有更小的元素也满足条件
result = middle;
end = middle - 1;
} else {
start = middle + 1;
}
}
return result;
}
}
在这个示例中,binarySearch
方法使用二分查找算法来查找第一个值大于等于给定值的元素的索引。一旦找到一个元素满足条件,我们检查它是否是第一个这样的元素(即它左边的元素是否小于目标值)。如果是,我们返回该元素的索引;如果不是,我们继续向左搜索。
3.4 查找最后一个值小于等于给定值的元素
要利用二分查找算法来查找最后一个值小于给定值的元素,我们需要在标准的二分查找算法上进行一些调整。基本思路是,当我们找到一个元素小于目标值时,我们应该继续向右搜索,直到找到一个不满足条件的元素(即大于或等于目标值的元素)为止,然后返回上一个访问的元素的索引,它必然是最后一个小于目标值的元素。以下是一个Java方法的示例,它使用二分查找来查找最后一个值小于给定值的元素的索引:
public class SearchAlgorithm {
public static void main(String[] args) {
int[] numArray = {2, 4, 5, 9, 11, 16, 45, 67};
int number = 46;
int index = binarySearch(numArray, number);
System.out.println("查找最后一个值小于等于【" + number + "】的元素下标:" + (index > -1 ? numArray[index] : index));
}
public static int binarySearch(int[] numArray, int target) {
if (numArray == null || numArray.length == 0) {
return -1;
}
int start = 0, end = numArray.length - 1, result = -1;
while (start <= end) {
int middle = start + (end - start) / 2;
if (numArray[middle] > target) {
end = middle - 1;
} else {
result = middle;
start = middle + 1;
}
}
return result;
}
}
3.5 查找多个值等于给定值的元素
二分查找算法通常用于在一个已排序的数组中查找一个特定的元素,若是想要查找多个值等于给定值的元素,实际上是在查找一个值域。首先,可以使用二分查找来找到该值的第一个出现位置,然后从这个位置开始向后搜索,直到找到该值的最后一个出现位置。以下是一个Java方法的示例,它使用了二分查找来找到给定数组中所有等于给定值的元素的起始和结束索引:
public class SearchAlgorithm {
public static void main(String[] args) {
int[] numArray = {2, 4, 5, 9, 11, 16, 45, 67};
int target = 1;
int[] range = findRange(numArray, target);
// // 初始化范围数组,-1 表示未找到
if (range[0] != -1 && range[1] != -1) {
System.out.println("数组范围: [" + range[0] + ", " + range[1] + "]");
} else {
System.out.println("在数组中找不到值。");
}
}
public static int[] findRange(int[] numArray, int target) {
int[] range = {-1, -1};
if (numArray == null || numArray.length == 0) {
return range;
}
// 查找第一个等于 target 的元素的索引
int left = findFirstIndex(numArray, target);
// 如果找到了第一个元素,则继续查找最后一个元素
if (left != -1) {
int right = findLastIndex(numArray, target, left);
range[0] = left;
range[1] = right;
}
return range;
}
/**
* 使用二分查找找到第一个等于 target 的元素的索引
*/
private static int findFirstIndex(int[] numArray, int target) {
int start = 0, end = numArray.length - 1, firstIndex = -1;
while (start <= end) {
int middle = start + (end - start) / 2;
if (numArray[middle] >= target) {
end = middle - 1;
firstIndex = numArray[middle] == target ? middle : firstIndex;
} else {
start = middle + 1;
}
}
return firstIndex;
}
/**
* 从给定索引开始向后查找,找到最后一个等于 target 的元素的索引
*/
private static int findLastIndex(int[] numArray, int target, int startIndex) {
int end = numArray.length - 1, lastIndex = startIndex;
while (startIndex <= end) {
if (numArray[startIndex] == target) {
lastIndex = startIndex;
startIndex++;
} else {
break;
}
}
return lastIndex;
}
}
四、结语
二分查找是一种高效的、适用于有序数组中查找某一特定元素的搜索算法,有点类似分治思想。即每次都通过跟区间中的中间元素对比,将待查找的区间缩小为一半,直到找到要查找的元素,或者区间被缩小为 0
。它的时间复杂度为 O(log n),其中 n 是数组的长度。由于每次迭代都将搜索范围减半,因此它比线性查找算法更加高效,特别是对于大型有序数组。通过仔细实现和理解二分查找算法,可以帮助我们在 Java 中轻松应用它来解决各种查找问题。
不过它的缺陷却也是那么明显的,就是数据必须是有序的,而我们很难保证我们的数组都是有序的。如果数据是乱的,先排序再用二分法查找得到的索引没有意义,只能确定当前数字在数组中是否存在,因为排序后的数字位置发生了变化。因此,折半查找方法适用于不经常变动而查找频繁的有序列表。