- 若文章内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系博主删除。
- 本文仅做学习用途,无他用。(记录学习时刻,方便个人在线阅览)
【黑马程序员】数据结构与算法(2023 版)
- 在线观看:数据结构与算法 | Java 程序员必学的数据结构与算法
- 资料领取:数据结构与算法 Java 版(提取码:9987)
文章目录
基础版
需求:在 有序数组 A A A 内,查找值 target
- 如果找到了目标值,返回索引
- 如果找不到目标值,返回 − 1 -1 −1
算法描述 | |
---|---|
前提 | 给定一个内含 n n n 个元素的有序数组 A A A,满足 A 0 ≤ A 1 ≤ A 2 ≤ ⋯ ≤ A n − 1 A_{0}\leq A_{1}\leq A_{2}\leq \cdots \leq A_{n-1} A0≤A1≤A2≤⋯≤An−1,一个待查值 t a r g e t target target |
1 | 设置 l o w = 0 low=0 low=0, h i g h = n − 1 high=n-1 high=n−1 |
2 | 如果 l o w > h i g h low \gt high low>high,结束查找。表示没找到目标值 |
3 | 设置 m i d d l e = f l o o r ( l o w + h i g h 2 ) middle = floor(\frac {low+high}{2}) middle=floor(2low+high) , m i d d l e middle middle 为中间索引, f l o o r floor floor 是向下取整( ≤ l o w + h i g h 2 \leq \frac {low+high}{2} ≤2low+high 的最小整数) |
4 | 如果 t a r g e t < A m i d d l e target < A_{middle} target<Amiddle 设置 h i g h = m i d d l e − 1 high = middle - 1 high=middle−1,跳到第 2 步 |
5 | 如果 A m i d d l e < t a r g e t A_{middle} < target Amiddle<target 设置 l o w = m i d d l e + 1 low = middle + 1 low=middle+1,跳到第 2 步 |
6 | 如果 A m i d d l e = t a r g e t A_{middle} = target Amiddle=target,结束查找,找到了 |
- 强调:设置 m i d d l e = f l o o r ( l o w + h i g h 2 ) middle = floor(\frac {low + high}{2}) middle=floor(2low+high)。middle 为中间索引, f l o o r floor floor 是向下取整(即小于等于 l o w + h i g h 2 \frac {low + high}{2} 2low+high 的最小整数)
/**
* 二分查找_基础版
* <p></p>
* * low、high、middle 指针都可能是查找目标
* * (low > high) 时表示区域内没有要找的了
* * 每次改变 low, high 边界时, middle 已经比较过了(确定其不是目标值),
* * 因此 low 是 (middle + 1) ,high 是 (middle - 1)
* * 向左查找, 比较次数少
* * 向右查找, 比较次数多
*
* @param a : 待查找的升序数组
* @param target : 待查找的目标值
* @return 找到目标值返回索引,找不到则返回 -1
*/
public static int binarySearchBasic(int[] a, int target) {
int low = 0;
int high = a.length - 1;
while (low <= high) { // 证明范围里有内容
int middle = (low + high) >>> 1;
if (target < a[middle]) {
high = middle - 1;
} else if (a[middle] < target) {
low = middle + 1;
} else {
return middle; // 找到目标值了,返回索引
}
}
return -1;
}
- l o w low low、 h i g h high high 对应着搜索区间 [ 0 , a . l e n g t h − 1 ] [0, a.length-1] [0,a.length−1](注意:此处是闭合的区间)
-
l
o
w
<
=
h
i
g
h
low<=high
low<=high 意味着搜索区间内还有未比较的元素,
l
o
w
low
low、
h
i
g
h
high
high 指向的元素也可能是比较的目标
- 思考:如果不加 l o w = = h i g h low==high low==high 行不行?
- 回答:不行,因为这意味着 l o w low low、 h i g h high high 指向的元素会漏过比较
- m i d d l e middle middle 对应着中间位置,中间位置左边和右边的元素可能不相等(差一个),不会影响结果
- 如果某次未找到,那么缩小后的区间内不包含 m i d d l e middle middle
改动版(右边界不在查找范围内)
/**
* 二分查找_改动版(右边界不在查找范围内)
* <p></p>
* * low、middle 指针可能是查找目标
* * high 指针不可能是查找目标
* * 因为 (low >= high) 时表示区域内没有要找的了
* * 改变 low 边界时, middle 已经比较过了(确定其不是目标值), 因此需要 (low = middle + 1)
* * 改变 high 边界时, middle 已经比较过了(不是目标), 同时因为 high 指针不可能是查找目标,所以 (high = middle)
*
* @param a : 待查找的升序数组
* @param target : 待查找的目标值
* @return 找到目标值返回索引,找不到则返回 -1
*/
public static int binarySearchAlternative(int[] a, int target) {
int low = 0;
int high = a.length; // 此时 high 指针指向的一定不是 target
while (low < high) {
int middle = (low + high) >>> 1;
if (target < a[middle]) {
high = middle;
} else if (a[middle] < target) {
low = middle + 1;
} else {
return middle;
}
}
return -1;
}
- l o w low low、 h i g h high high 对应着搜索区间 [ 0 , a . l e n g t h ) [0, a.length) [0,a.length)(注意:此处是左闭右开的区间)
-
l
o
w
<
h
i
g
h
low<high
low<high 意味着搜索区间内还有未比较的元素,
h
i
g
h
high
high 指向的 一定不是 查找目标
- 思考:为啥这次不加 l o w = = h i g h low==high low==high 的条件了?
- 回答:这回
h
i
g
h
high
high 指向的不是查找目标。
如果还加 l o w = = h i g h low==high low==high 条件,就意味着 h i g h high high 指向的还会再次比较,找不到目标值时,程序会陷入死循环
- 如果某次要缩小右边界,那么 h i g h = m i d d l e high=middle high=middle,因为此时的 m i d d l e middle middle 已经 不是 查找目标了
平衡版(循环内缩小范围,循环外比较)
/**
* 二分查找_平衡版
* <p></p>
* * 不奢望循环内通过 middle 找出目标, 缩小区间直至剩 1 个, 剩下的这个可能就是要找的(通过 low 指针)
* * low 指针可能是查找目标
* * high 指针不可能是查找目标
* * 因为上面的设置. 当区域内还剩一个元素时, 我们可以用 (high - middle == 1) 来表示这种情况
* * 改变 low 边界时, middle 可能就是目标, 同时因为 low 指针有可能是查找目标. 所以有 (low = middle)
* * 改变 high 边界时, middle 已经比较过,其并不是目标值, 同时因为 high 指针不可能是目标值. 所以有 (high = middle)
* <p></p>
* * 优点:三分支改为二分支, 循环内比较次数减少
*
* @param a : 待查找的升序数组
* @param target : 待查找的目标值
* @return 找到目标值返回索引,找不到则返回 -1
*/
public static int binarySearchBalance(int[] a, int target) {
int low = 0;
int high = a.length;
while (1 < high - low) { // 范围内待查找的元素个数 > 1 时
int middle = (low + high) >>> 1;
if (target < a[middle]) { // 目标在左边
high = middle;
} else { // 目标在 middle 或右边
low = middle;
}
}
return (target == a[low]) ? low : -1;
}
思想:
- 左闭右开的区间, l o w low low 指向的可能是目标,而 h i g h high high 指向的不是目标
- 不奢望循环内通过
m
i
d
d
l
e
middle
middle 找出目标,缩小区间直至剩 1 个,剩下的这个可能就是要找的目标值(通过
h
i
g
h
high
high)
- h i g h − l o w > 1 high - low > 1 high−low>1 的含义是: 在范围内的待比较的元素个数 > 1 在范围内的待比较的元素个数 > 1 在范围内的待比较的元素个数>1
- 改变 l o w low low 边界时,它指向的可能是目标,因此不能 m i d d l e + 1 middle + 1 middle+1
- 优点:循环内的平均比较次数减少了
- 时间复杂度: Θ ( l o g ( n ) ) \Theta(log(n)) Θ(log(n))
Arrays.binarySearch()
Java 版的二分查找
java/util/Arrays.java
private static int binarySearch0(int[] a, int fromIndex, int toIndex, int key) {
int low = fromIndex;
int high = toIndex - 1;
while (low <= high) {
int mid = (low + high) >>> 1;
int midVal = a[mid];
if (midVal < key)
low = mid + 1;
else if (midVal > key)
high = mid - 1;
else
return mid; // key found
}
return -(low + 1); // key not found.
}
- 例:
[
1
,
3
,
5
,
6
]
[1, 3, 5, 6]
[1,3,5,6] 要插入
2
2
2,那么就是找到一个位置进行插入操作(这个位置左侧元素都比它小)
- 等循环结束,还是没找到目标值。又因为 l o w low low 左侧元素肯定都比 t a r g e t target target 小,故 l o w low low 即插入点
- 插入点取负是为了与找到情况区分
- − 1 -1 −1 是为了把索引 0 0 0 位置的插入点与找到的情况进行区分
- 测试代码
public void test4() {
int[] a = {2, 5, 8};
int target = 4;
int i = Arrays.binarySearch(a, target);
System.out.println("-(插入点 + 1) = " + i); // -2 = -(插入点 + 1)
if (i < 0) {
int insertIndex = Math.abs(i + 1); // 真正的插入点索引
System.out.println("插入点索引:" + insertIndex);
int[] b = new int[a.length + 1];
System.arraycopy(a, 0, b, 0, insertIndex);
System.out.println(Arrays.toString(b));
b[insertIndex] = target;
System.arraycopy(a, insertIndex, b, insertIndex + 1, a.length - insertIndex);
System.out.println(Arrays.toString(b));
}
}
- 控制台输出结果
-(插入点 + 1) = -2
插入点索引:1
[2, 0, 0, 0]
[2, 4, 5, 8]
LeftMost 和 RightMost
LeftMost 初版
有时我们希望返回的是最左侧的重复元素,如果用基础版的二分查找的话
- 对于数组 [ 1 , 2 , 3 , 4 , 4 , 5 , 6 , 7 ] [1, 2, 3, 4, 4, 5, 6, 7] [1,2,3,4,4,5,6,7],查找元素 4 4 4,结果是索引 3 3 3
- 对于数组 [ 1 , 2 , 4 , 4 , 4 , 5 , 6 , 7 ] [1, 2, 4, 4, 4, 5, 6, 7] [1,2,4,4,4,5,6,7],查找元素 4 4 4,结果也是索引 3 3 3,但这并不是最左侧的元素
/**
* 二分查找 LeftMost_1
* 在数组中有多个元素和目标元素相同的情况下,返回最左边的重复元素的索引
*
* @param a : 待查找的升序数组
* @param target : 待查找的目标值
* @return 找到目标值,返回重复元素的最左索引
* 找不目标值,到则返回 -1
*/
public static int binarySearchLeftMost_1(int[] a, int target) {
int low = 0;
int high = a.length - 1;
int candidate = -1;
while (low <= high) {
int middle = (low + high) >>> 1;
if (target < a[middle]) {
high = middle - 1;
} else if (target > a[middle]) {
low = middle + 1;
} else {
// 记录候选位置
candidate = middle;
high = middle - 1;
}
}
return candidate;
}
RightMost 初版
有时我们希望返回的是数组最左侧的重复元素
/**
* 二分查找 RightMost_1
* 在数组中有多个元素和目标元素相同的情况下,返回最右边的重复元素的索引
*
* @param a : 待查找的升序数组
* @param target : 待查找的目标值
* @return 找到目标值,返回重复元素的最右索引
* 找不目标值,到则返回 -1
*/
public static int binarySearchRightMost_1(int[] a, int target) {
int low = 0;
int high = a.length - 1;
int candidate = -1;
while (low <= high) {
int middle = (low + high) >>> 1;
if (target > a[middle]) {
low = middle + 1;
} else if (target < a[middle]) {
high = middle - 1;
} else {
// 记录候选位置
candidate = middle;
low = middle + 1;
}
}
return candidate;
}
LeftMost 改版
下面的代码只是改版之一罢了(在应用那一小节才是更加全面)
/**
* 二分查找 LeftMost_2
*
* @param a 待查找的升序数组
* @param target 待查找的目标值
* @return 返回 (>= target) 的重复元素的最左索引
* 找到目标值,返回与目标值相等的,最靠左的索引位置
* 没找到目标值,返回大于目标值,最靠左的元素的索引位置
*/
public static int binarySearchLeftMost_2(int[] a, int target) {
int low = 0, high = a.length - 1;
while (low <= high) {
int middle = (low + high) >>> 1;
if (target <= a[middle]) {
high = middle - 1;
} else {
low = middle + 1;
}
}
return low;
}
RightMost 改版
下面的代码只是改版之一罢了(在应用那一小节才是更加全面)
/**
* 二分查找 RightMost_2
*
* @param a : 待查找的升序数组
* @param target : 待查找的目标值
* @return 返回 (<= target) 的重复元素的最右索引
* 找到了目标值,返回等于目标值的元素的最右索引
* 没有找到目标值的时候,它返回的是小于目标的最靠右的元素的索引
*/
public static int binarySearchRightMost_2(int[] a, int target) {
int low = 0;
int high = a.length - 1;
while (low <= high) {
int middle = (low + high) >>> 1;
if (target >= a[middle]) {
low = middle + 1;
} else if (target < a[middle]) {
high = middle - 1;
}
}
return low - 1;
}
应用
范围查询:
- 查询 x < 4 x \lt 4 x<4, 0.. l e f t m o s t ( 4 ) − 1 0 .. leftmost(4) - 1 0..leftmost(4)−1
- 查询 x ≤ 4 x \leq 4 x≤4, 0.. r i g h t m o s t ( 4 ) 0 .. rightmost(4) 0..rightmost(4)
- 查询 4 < x 4 \lt x 4<x, r i g h t m o s t ( 4 ) + 1.. ∞ rightmost(4) + 1 .. \infty rightmost(4)+1..∞
- 查询 4 ≤ x 4 \leq x 4≤x, l e f t m o s t ( 4 ) . . ∞ leftmost(4) .. \infty leftmost(4)..∞
- 查询 4 ≤ x ≤ 7 4 \leq x \leq 7 4≤x≤7, l e f t m o s t ( 4 ) . . r i g h t m o s t ( 7 ) leftmost(4) .. rightmost(7) leftmost(4)..rightmost(7)
- 查询 4 < x < 7 4 \lt x \lt 7 4<x<7, r i g h t m o s t ( 4 ) + 1.. l e f t m o s t ( 7 ) − 1 rightmost(4) + 1 .. leftmost(7) - 1 rightmost(4)+1..leftmost(7)−1
求排名: l e f t m o s t ( t a r g e t ) + 1 leftmost(target) + 1 leftmost(target)+1
- t a r g e t target target 可以不存在。如: l e f t m o s t ( 5 ) + 1 = 6 leftmost(5)+1 = 6 leftmost(5)+1=6
- t a r g e t target target 也可以存在。如: l e f t m o s t ( 4 ) + 1 = 3 leftmost(4)+1 = 3 leftmost(4)+1=3
求前任(predecessor): l e f t m o s t ( t a r g e t ) − 1 leftmost(target) - 1 leftmost(target)−1
- l e f t m o s t ( 3 ) − 1 = 1 leftmost(3) - 1 = 1 leftmost(3)−1=1,前任 a 1 = 2 a_1 = 2 a1=2
- l e f t m o s t ( 4 ) − 1 = 1 leftmost(4) - 1 = 1 leftmost(4)−1=1,前任 a 1 = 2 a_1 = 2 a1=2
求后任(successor): r i g h t m o s t ( t a r g e t ) + 1 rightmost(target)+1 rightmost(target)+1
- r i g h t m o s t ( 5 ) + 1 = 5 rightmost(5) + 1 = 5 rightmost(5)+1=5,后任 a 5 = 7 a_5 = 7 a5=7
- r i g h t m o s t ( 4 ) + 1 = 5 rightmost(4) + 1 = 5 rightmost(4)+1=5,后任 a 5 = 7 a_5 = 7 a5=7
求最近邻居:
- 前任和后任距离更近者
LeetCode 部分题目
704 题:二分查找
给定一个 n n n 个元素有序的(升序)整型数组 nums 和一个目标值 target。
写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 − 1 -1 −1。
解法①(基础版)
public class LeetCode704_1 {
public int search(int[] nums, int target) {
int low = 0, high = nums.length - 1;
while (low <= high) {
int middle = (low + high) >>> 1;
if (target < nums[middle]) {
high = middle - 1;
} else if (target > nums[middle]) {
low = middle + 1;
} else {
return middle;
}
}
return -1;
}
}
解法②(改动版)
public class LeetCode704_2 {
public int search(int[] nums, int target) {
int low = 0, high = nums.length;
while (low < high) {
int middle = (low + high) >>> 1;
if (target < nums[middle]) {
high = middle;
} else if (target > nums[middle]) {
low = middle + 1;
} else {
return middle;
}
}
return -1;
}
}
解法③(平衡版)
public class LeetCode704_3 {
public int search(int[] nums, int target) {
int low = 0, high = nums.length;
while (high - low > 1) {
int middle = (low + high) >>> 1;
if (target < nums[middle]) {
high = middle;
} else {
low = middle;
}
}
return (nums[low] == target) ? low : -1;
}
}
35 题·:搜索插入位置
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。
如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O ( l o g n ) O(log n) O(logn) 的算法。
提示:数组 nums 为 升序 排列数组
数组中不存在重复元素
这里的代码和 java/util/Arrays.java
中的 binarySearch0() 方法很像
public class LeetCode35_1 {
public int searchInsert(int[] nums, int target) {
int low = 0, high = nums.length - 1;
while (low <= high) {
int middle = (low + high) >>> 1;
long middleValue = nums[middle];
if (middleValue < target) {
low = middle + 1;
} else if (middleValue > target) {
high = middle - 1;
} else {
return middle; // key found
}
}
return low; // key not found
}
}
数组中存在重复元素
当有多个元素的值和目标值一致时,返回这些重复元素中的最左索引
若数组中不存在与目标值一致的元素,则返回插入点
public int searchInsert(int[] nums, int target) {
int low = 0, high = nums.length - 1;
while (low <= high) {
int middle = (low + high) >>> 1;
if (nums[middle] < target) {
low = middle + 1;
} else {
high = middle - 1;
}
}
return low;
}
34 题:在排序数组中查找元素的第一个和最后一个位置
链接:https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/
给你一个按照非递减顺序排列的整数数组 n u m s nums nums,和一个目标值 t a r g e t target target。
请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 t a r g e t target target,返回 [ − 1 , − 1 ] [-1, -1] [−1,−1]。
你必须设计并实现时间复杂度为 O ( l o g n ) O(log n) O(logn) 的算法解决此问题。
提示: n u m s nums nums 是一个非递减数组,即该数组是一个存在重复元素的升序数组
public class LeetCode34 {
public int[] searchRange(int[] nums, int target) {
int left = leftMost(nums, target);
int right = rightMost(nums, target);
return (left == -1) ? new int[]{-1, -1} : new int[]{left, right};
}
public int leftMost(int[] nums, int target) {
int low = 0, high = nums.length - 1;
int candidate = -1;
while (low <= high) {
int middle = (low + high) >>> 1;
if (target < nums[middle]) {
high = middle - 1;
} else if (target > nums[middle]) {
low = middle + 1;
} else {
candidate = middle;
high = middle - 1;
}
}
return candidate;
}
public int rightMost(int[] nums, int target) {
int low = 0, high = nums.length - 1;
int candidate = -1;
while (low <= high) {
int middle = (low + high) >>> 1;
if (target < nums[middle]) {
high = middle - 1;
} else if (target > nums[middle]) {
low = middle + 1;
} else {
candidate = middle;
low = middle + 1;
}
}
return candidate;
}
}