搜索
算法入门
二分查找
左闭右开区间
二分查找插入点
无重复元素
存在重复元素
二分查找边界
查找左边界
查找右边界
哈希优化策略
线性查找
哈希查找
相关例题
leetcode704.二分查找
法一:二分查找
leetcode278.第一个错误的版本
法一:二分查找
leetcode724.寻找数组的中心下标
法一:前缀和
leetcode287.寻找重复数
法一:快慢指针
leetcode154.寻找旋转排序数组中的最小值Ⅱ
法一:二分查找
/**
* 二分查找(双闭区间)
* 在有序数组中查找指定目标值的索引。
*/
int binarySearch(int[] nums, int target) {
// 初始化双闭区间 [0, n-1],即 i 和 j 分别指向数组的首元素、尾元素
int i = 0, j = nums.length - 1;
// 循环,当搜索区间为空时跳出(当 i > j 时为空)
while (i <= j) {
int m = i + (j - i) / 2; // 计算中点索引 m,避免溢出
// 比较中间值与目标值
if (nums[m] < target) {
// 此情况说明 target 在区间 [m+1, j] 中
i = m + 1;
} else if (nums[m] > target) {
// 此情况说明 target 在区间 [i, m-1] 中
j = m - 1;
} else {
// 找到目标元素,返回其索引
return m;
}
}
// 未找到目标元素,返回 -1
return -1;
}
/**
* 二分查找(左闭右开)
* 在有序数组中查找指定目标值的索引。
*/
int binarySearch(int[] nums, int target) {
// 初始化左指针为 0,右指针为数组长度
int left = 0, right = nums.length; // 右指针为 nums.length,表示数组末尾的下一个位置
// 循环,当搜索区间为空时跳出(当 left >= right 时为空)
while (left < right) {
int mid = left + (right - left) / 2; // 计算中点索引 mid,避免溢出
// 比较中间值与目标值
if (nums[mid] < target) {
// 此情况说明 target 在区间 [mid + 1, right) 中
left = mid + 1;
} else {
// 此情况说明 target 在区间 [left, mid) 中(包括 mid 本身)
right = mid;
}
}
// 检查目标值是否存在于数组中
if (left < nums.length && nums[left] == target) {
return left; // 找到目标元素,返回其索引
}
// 未找到目标元素,返回 -1
return -1;
}
left
是闭合的(包括当前元素),而 right
是开口的(不包括 right
指向的元素)。nums[mid] < target
,那么目标值在 mid + 1
到 right
范围内。nums[mid] >= target
),目标值在 left
到 mid
范围内,包含 mid
。left
指向的元素与目标值,判断目标值是否存在。这种形式常用于需要搜索的范围不包含右端点的情况,可以更清晰地处理边界值和避免某些特殊边界条件的复杂性。
但是由于“双闭区间”表示中的左右边界都被定义为闭区间,因此通过指针 和指针 缩小区间的操作也是对称的。这样更不容易出错,因此一般建议采用“双闭区间”的写法 。
/**
* 二分查找插入点(无重复元素)
* 在有序数组中查找目标值的插入位置。
* 如果目标值存在,则返回它的位置;如果不存在,则返回可以插入的位置。
*/
int binarySearchInsertionPoint(int[] nums, int target) {
int left = 0, right = nums.length; // 初始化左指针为 0,右指针为数组长度
// 循环,当搜索区间 [left, right) 不为空时继续
while (left < right) {
int mid = left + (right - left) / 2; // 计算中点索引 mid,避免溢出
// 比较中间值与目标值
if (nums[mid] < target) {
// target 在区间 [mid + 1, right) 中
left = mid + 1;
} else {
// target 在区间 [left, mid) 中(包括 mid)
right = mid;
}
}
// 循环结束时,left 即为目标值的插入位置
return left;
}
[left, right)
,包含 left
,不包含 right
,所以右指针设置为数组的长度。target
存在于数组中,通过返回索引可以获取其位置;如果不存在,则左指针left
会指向可以插入的位置。left
小于 right
,这表示区间未完全重合。target
是否存在,left
都是正确的插入位置。/**
* 二分查找插入点(存在重复元素)
* 在有序数组中查找目标值的插入位置。
* 如果目标值存在,则返回它的位置中最左侧的索引;
* 如果不存在,则返回可以插入的位置。
*/
int binarySearchInsertionPointWithDuplicates(int[] nums, int target) {
int left = 0, right = nums.length; // 初始化左指针为 0,右指针为数组长度
// 循环,当搜索区间 [left, right) 不为空时继续
while (left < right) {
int mid = left + (right - left) / 2; // 计算中点索引 mid,避免溢出
// 比较中间值与目标值
if (nums[mid] < target) {
// target 在区间 [mid + 1, right) 中
left = mid + 1;
} else {
// target 在区间 [left, mid] 中(包括 mid)
right = mid;
}
}
// 在搜索完成后,left 会指向最左侧可以插入目标值的位置
return left;
}
[left, right)
,包含 left
但不包含 right
,右指针为数组的长度。nums[mid]
等于 target
,则将 right
更新为 mid
,这会保证搜索的左半部分包含重叠值,并能返回最左侧位置。left
指向的即为 target
的插入点,无论 target
是否存在于数组中。/**
* 二分查找最左一个 target
* 在有序数组中查找目标值最左侧的位置。
*/
int binarySearchLeftEdge(int[] nums, int target) {
int left = 0; // 初始化左指针
int right = nums.length; // 初始化右指针,右指针为数组长度
// 循环,当搜索区间 [left, right) 不为空时继续
while (left < right) {
int mid = left + (right - left) / 2; // 计算中点索引 mid,避免溢出
// 比较中间值与目标值
if (nums[mid] < target) {
left = mid + 1; // target 在右半部分,更新左指针
} else {
right = mid; // target 在左半部分或是 mid,自左相等,所以这部分包含 mid
}
}
// 此时 left 指向可能的插入位置,需检查是否为目标值的索引
if (left < nums.length && nums[left] == target) {
return left; // 找到最左边的 target,返回索引
}
// 未找到 target,返回 -1
return -1;
}
while (left < right)
,确保左右指针交替。int mid = left + (right - left) / 2;
计算中点,以避免大数组时可能发生的整数溢出。left
指向的元素是否为目标值,若是,返回索引;若否,返回 -1。/**
* 二分查找右边界
* 在有序数组中查找目标值最右侧的位置。
*/
int binarySearchRightEdge(int[] nums, int target) {
int left = 0; // 初始化左指针
int right = nums.length; // 初始化右指针,右指针为数组长度
// 循环,当搜索区间 [left, right) 不为空时继续
while (left < right) {
int mid = left + (right - left) / 2; // 计算中点索引 mid,避免溢出
// 比较中间值与目标值
if (nums[mid] <= target) {
left = mid + 1; // target 在右半部分,更新左指针
} else {
right = mid; // target 在左半部分,更新右指针
}
}
// 此时 left 已经指向了目标值的右边界的下一个位置
// 检查 left - 1 是否是目标值的索引
if (left > 0 && nums[left - 1] == target) {
return left - 1; // 找到最右边的 target,返回索引
}
// 未找到 target,返回 -1
return -1;
}
while (left < right)
,确保顺利搜索直到左右指针交替。int mid = left + (right - left) / 2;
用于安全地避免整型溢出。nums[mid]
小于或等于 target
,则我们将左指针移动到 mid + 1
,这样在目标值存在的情况下能够继续寻找右边界。left
指向的元素是 target
的右边界的下一个位置。因此,检查 left - 1
是否等于目标值,用于返回最右侧目标值的索引。/**
* 方法一:暴力枚举
* 在给定数组中查找两个数的索引,使其和等于目标值。
*/
int[] twoSumBruteForce(int[] nums, int target) {
int size = nums.length; // 获取数组的长度
// 两层循环,时间复杂度为 O(n^2)
for (int i = 0; i < size - 1; i++) {
for (int j = i + 1; j < size; j++) {
// 检查 nums[i] 和 nums[j] 的和是否等于目标值
if (nums[i] + nums[j] == target) {
// 找到符合条件的索引,返回结果
return new int[] { i, j };
}
}
}
// 未找到符合条件的两个数,返回空数组
return new int[0];
}
target
。以空间换时间
import java.util.HashMap;
/**
* 方法二:哈希查找
* 在给定数组中查找两个数的索引,使其和等于目标值。
* 使用哈希表来提高查找效率。
*/
int[] twoSumHashMap(int[] nums, int target) {
HashMap map = new HashMap<>(); // 创建哈希表
// 遍历数组
for (int i = 0; i < nums.length; i++) {
int complement = target - nums[i]; // 计算当前数的补数
// 检查补数是否在哈希表中
if (map.containsKey(complement)) {
// 找到符合条件的索引,返回结果
return new int[] { map.get(complement), i };
}
// 将当前数及其索引存入哈希表
map.put(nums[i], i);
}
// 未找到符合条件的两个数,返回空数组
return new int[0];
}
HashMap
存储数组中元素及其索引。键是元素的值,值是元素的索引。target - nums[i]
),并在哈希表中查看该补数是否已经存在。704. 二分查找https://leetcode.cn/problems/binary-search/
public class Method01 {
/**
* 在有序数组中使用二分查找法查找目标值
* @param nums 有序数组
* @param target 需要查找的目标值
* @return 目标值在数组中的索引,如果不存在则返回 -1
*/
public int search(int[] nums, int target) {
int len = nums.length; // 数组的长度
int left = 0; // 左边界初始化为数组的起始索引
int right = len - 1; // 右边界初始化为数组的末尾索引
// 当左边界小于等于右边界时,继续进行查找
while (left <= right) {
int mid = left + (right - left) / 2; // 计算中间索引
// 如果中间值大于目标值,说明目标值在左半部分
if (nums[mid] > target) {
right = mid - 1; // 更新右边界
}
// 如果中间值小于目标值,说明目标值在右半部分
else if (nums[mid] < target) {
left = mid + 1; // 更新左边界
}
// 如果中间值等于目标值,返回中间索引
else {
return mid; // 找到目标值,返回其索引
}
}
// 如果目标值不存在于数组中,返回 -1
return -1;
}
}
278. 第一个错误的版本https://leetcode.cn/problems/first-bad-version/
public class Method01 extends VersionControl {
public int firstBadVersion(int n) {
int left = 1; // 左边界初始化为 1,表示从第 1 个版本开始
int right = n; // 右边界初始化为 n,表示到第 n 个版本为止
// 二分查找,直到 left 和 right 相遇
while (left < right) {
int mid = left + (right - left) / 2; // 计算中间版本
// 如果 mid 是坏的版本
if (isBadVersion(mid)) {
right = mid; // 则第一个坏版本在 mid 左侧(包括 mid)
} else {
left = mid + 1; // 否则第一个坏版本在 mid 的右侧
}
}
// 当结束时,left 和 right 相等,指向第一个坏版本
return left; // 返回第一个坏版本的版本号
}
}
724. 寻找数组的中心下标https://leetcode.cn/problems/find-pivot-index/
public class Method01 {
/**
* 找到数组的枢轴索引
* @param nums 输入的整数数组
* @return 数组的枢轴索引,如果不存在则返回 -1
*/
public int pivotIndex(int[] nums) {
// 计算数组元素的总和
int sum = Arrays.stream(nums).sum();
int leftSum = 0; // 初始化左边的和为 0
// 遍历数组
for (int i = 0; i < nums.length; i++) {
// 检查左边的和是否等于右边的和
// 右边的和可以通过总和减去左边的和和当前元素得到
if (leftSum == sum - leftSum - nums[i]) {
return i; // 如果相等,返回当前索引 i 作为枢轴索引
}
// 更新左边的和,加入当前元素
leftSum += nums[i];
}
// 如果没有找到枢轴索引,返回 -1
return -1;
}
}
287. 寻找重复数https://leetcode.cn/problems/find-the-duplicate-number/
public class Method01 {
/**
* 使用快慢指针方法查找数组中的重复数字。
*
* @param nums 输入的数组,其中的数字范围在 [1, n] 之间,并且数组长度为 n + 1,
* 必定存在一个重复的数字。
* @return 返回找到的重复数字。
*/
public int findDuplicate(int[] nums) {
// 快指针和慢指针初始化
int slow = nums[0]; // 慢指针从数组的第一个元素开始
int fast = nums[nums[0]]; // 快指针从数组的第一个元素的指向位置开始
// 找到慢指针和快指针相遇的位置
while (slow != fast) {
slow = nums[slow]; // 慢指针每次移动一步
fast = nums[nums[fast]]; // 快指针每次移动两步
}
// 重置快指针为 0
fast = 0;
// 找到重复数字所在的位置
// 此时慢指针和快指针相遇处即为环的入口
while (slow != fast) {
slow = nums[slow]; // 慢指针也从相遇点开始,移动一步
fast = nums[fast]; // 快指针从 0 开始,移动一步
}
// 返回重复的数字
return slow; // slow 和 fast 在环的入口相遇,因此返回 slow
}
}
154. 寻找旋转排序数组中的最小值 IIhttps://leetcode.cn/problems/find-minimum-in-rotated-sorted-array-ii/
public class Method01 {
/**
* 寻找旋转排序数组中的最小值。
* 假设数组是通过某种方式旋转过的,可能包含重复元素。
*
* @param nums 输入的旋转排序数组
* @return 数组中的最小值
*/
public int findMin(int[] nums) {
int left = 0; // 初始化左指针为数组的起始位置
int right = nums.length - 1; // 初始化右指针为数组的结束位置
// 当左指针小于右指针时继续循环
while (left < right) {
int mid = left + (right - left) / 2; // 计算中间指针,避免可能的溢出
if (nums[mid] < nums[right]) { // 判断中间值与右边值的大小关系
right = mid; // 当中间值小于右边值,说明最小值在左半部分
} else if (nums[mid] > nums[right]) {
left = mid + 1; // 当中间值大于右边值,说明最小值在右半部分
} else { // 当中间值等于右边值时,可能会错过最小值,缩小右边界
right--; // 缩小右边界,排除掉右边的重复值
}
}
// 循环结束,left 指向最小值
return nums[left]; // 返回最小值
}
}
文章记录了学习Krahets的《Hello 算法》的轨迹,代码均使用Java语言,原书支持 Python、C++、Java、C#、Go、Swift、JavaScript、TypeScript、Dart、 Rust、C 和 Zig 等语言。
教程链接:krahets/hello-algo: 《Hello 算法》:动画图解、一键运行的数据结构与算法教程。支持 Python, Java, C++, C, C#, JS, Go, Swift, Rust, Ruby, Kotlin, TS, Dart 代码。简体版和繁体版同步更新,English version ongoing (github.com)编辑https://github.com/krahets/hello-algohttps://github.com/krahets/hello-algo