1_二分查找

news/2025/9/29 14:34:55/文章来源:https://www.cnblogs.com/Juan5hao/p/19118707

一. 初识算法

1.1 什么是算法?

定义

在数学和计算机科学领域,算法是一系列有限的严谨指令,通常用于解决一类特定问题或执行计算

In mathematics and computer science, an algorithm (/ˈælɡərɪðəm/) is a finite sequence of rigorous instructions, typically used to solve a class of specific problems or to perform a computation.[^1]

Introduction to Algorithm[^2]

不正式的说,算法就是任何定义优良的计算过程:接收一些值作为输入,在有限的时间内,产生一些值作为输出。

Informally, an algorithm is any well-defined computational procedure that takes some value, or set of values, as input and produces some value, or set of values, as output in a finite amount of time.

1.2 什么是数据结构?

定义

在计算机科学领域,数据结构是一种数据组织、管理和存储格式,通常被选择用来高效访问数据

In computer science, a data structure is a data organization, management, and storage format that is usually chosen for efficient access to data

Introduction to Algorithm[^2]

数据结构是一种存储和组织数据的方式,旨在便于访问和修改

A data structure is a way to store and organize data in order to facilitate access and modifications

可以说,程序 = 数据结构 + 算法,它们是每一位程序员的基本功,下来我们通过对一个非常著名的二分查找算法的讲解来认识一下算法

1.3 二分查找 [^3]

二分查找算法也称折半查找,是一种非常高效的工作于有序数组的查找算法。后续的课程中还会学习更多的查找算法,但在此之前,不妨用它作为入门。

1) 基础版

需求:在有序数组 $A$ 内,查找值 $target$

  • 如果找到返回索引
  • 如果找不到返回 $-1$

算法描述

前提 给定一个内含 $n$ 个元素的有序数组 $A$,满足 $A_{0}\leq A_{1}\leq A_{2}\leq \cdots \leq A_{n-1}$,一个待查值 $target$
1 设置 $i=0$,$j=n-1$
2 如果 $i \gt j$,结束查找,没找到
3 设置 $m = floor(\frac {i+j}{2})$ ,$m$ 为中间索引,$floor$ 是向下取整($\leq \frac {i+j}{2}$ 的最小整数)
4 如果 $target < A_{m}$ 设置 $j = m - 1$,跳到第2步
5 如果 $A_{m} < target$ 设置 $i = m + 1$,跳到第2步
6 如果 $A_{m} = target$,结束查找,找到了

P.S.

  • 对于一个算法来讲,都有较为严谨的描述,上面是一个例子
  • 后续讲解时,以简明直白为目标,不会总以上面的方式来描述算法

java 实现

*tpublic static int binarySearch(int[] a, int target) {int i = 0, j = a.length - 1;//循环L次  元素在最左边L次  元素在最右边2*L次  左右并不平衡while (i <= j) {int m = (i + j) >>> 1;if (target < a[m]) {			// 在左边j = m - 1;} else if (a[m] < target) {		// 在右边i = m + 1;} else {return m;}}return -1;
}
/*
1[2,3,4,5]6 右侧没找到更差  
int i = 0, j = a.length - 1;	2
return -1;						1
元素个数			循环次数   floor()向下取整	
4-7					3		floor(log_2(4))=2+1
8-15				4		log_2(8)=3+1
16-31				5		log_2(16)=4+1
32-63				6		log_2(32)=5+1
...					...
循环次数:L = floor(log_2(N))+1i<=j							L+1
int m = (i + j) >>> 1;			L
target < a[m]					L
a[m] < target					Li = m + 1;						L总次数						(floor(log_2(N))+1	)*5+43*5+4=19*t(10+1)*5+4=59*t*/
  • $i,j$ 对应着搜索区间 $[0,a.length-1]$(注意是闭合的区间),$i<=j$ 意味着搜索区间内还有未比较的元素,$i,j$ 指向的元素也可能是比较的目标
    • 思考:如果不加 $i==j$ 行不行?
    • 回答:不行,因为这意味着 $i,j$ 指向的元素会漏过比较
  • $m$ 对应着中间位置,中间位置左边和右边的元素可能不相等(差一个),不会影响结果
  • 如果某次未找到,那么缩小后的区间内不包含 $m$
  • 问题1:为什么是i<=j不是i<j
    不⾏,因为这意味着 指向的元素会漏过⽐较
  • 问题2:(i+j)/2有没有问题?
    java里最高位要看成符号位
    0001_0000 16
    0000_1000 8
    第一个数字为1的二进制,有符号表示就是负数,无符号表示就是正数,右移一位就等于无符号的正数对应的十进制除以2
    二进制右移一位就是相当于除以2取整 m=(i+j)>>>1;替换m=(i+j)/2;
  • 问题3:都写成小于号有什么好处
    视觉上可以直接感觉到两个数的大小 左边小右边大

2) 改变版

另一种写法

public static int binarySearch(int[] a, int target) {int i = 0, j = a.length;while (i < j) {int m = (i + j) >>> 1;if (target < a[m]) {			// 在左边j = m;} else if (a[m] < target) {		// 在右边i = m + 1;} else {return m;}}return -1;
}
  • $i,j$ 对应着搜索区间 $[0,a.length)$(注意是左闭右开的区间),$i<j$ 意味着搜索区间内还有未比较的元素,$j$ 指向的一定不是查找目标
    • 思考:为啥这次不加 $i==j$ 的条件了?
    • 回答:这回 $j$ 指向的不是查找目标,如果还加 $i==j$ 条件,就意味着 $j$ 指向的还会再次比较,找不到时,会死循环
  • 如果某次要缩小右边界,那么 $j=m$,因为此时的 $m$ 已经不是查找目标了

1.4 衡量算法好坏

时间复杂度

下面的查找算法也能得出与之前二分查找一样的结果,那你能说出它差在哪里吗?

public static int search(int[] a, int k) {for (int i = 0;i < a.length;i++) {if (a[i] == k) {return i;}}return -1;
}
//1.最差的执行情况
//2.假设每行语句执行时间一样
/*  
int i = 0;		1
i < a.length;  	n+1
i++ 			n
a[i]==k 		n
return i  		1总次数3*n + 33*4 + 3 = 15*t3*1024+3=3075*t
*/

考虑最坏情况下(没找到)例如 [1,2,3,4] 查找 5

  • int i = 0 只执行一次
  • i < a.length 受数组元素个数 $n$ 的影响,比较 $n+1$ 次
  • i++ 受数组元素个数 $n$ 的影响,自增 $n$ 次
  • a[i] == k 受元素个数 $n$ 的影响,比较 $n$ 次
  • return -1,执行一次

粗略认为每行代码执行时间是 $t$,假设 $n=4$ 那么

  • 总执行时间是 $(1+4+1+4+4+1)*t = 15t$
  • 可以推导出更一般地公式为,$T = (3*n+3)t$

如果套用二分查找算法,还是 [1,2,3,4] 查找 5

public static int binarySearch(int[] a, int target) {int i = 0, j = a.length - 1;while (i <= j) {int m = (i + j) >>> 1;if (target < a[m]) {			// 在左边j = m - 1;} else if (a[m] < target) {		// 在右边i = m + 1;} else {return m;}}return -1;
}
  • int i = 0, j = a.length - 1 各执行 1 次

  • i <= j 比较 $floor(\log_{2}(n)+1)$ 再加 1 次

  • (i + j) >>> 1 计算 $floor(\log_{2}(n)+1)$ 次

  • 接下来 if() else if() else 会执行 $3* floor(\log_{2}(n)+1)$ 次,分别为

    • if 比较
    • else if 比较
    • else if 比较成立后的赋值语句
  • return -1,执行一次

结果:

  • 总执行时间为 $(2 + (1+3) + 3 + 3 * 3 +1)*t = 19t$
  • 更一般地公式为 $(4 + 5 * floor(\log_{2}(n)+1))*t$

注意:

左侧未找到和右侧未找到结果不一样,这里不做分析

两个算法比较,可以看到 $n$ 在较小的时候,二者花费的次数差不多

image-20221108095747933

但随着 $n$ 越来越大,比如说 $n=1000$ 时,用二分查找算法(红色)也就是 $54t$,而蓝色算法则需要 $3003t$

image-20221108100014451

画图采用的是 Desmos | 图形计算器

计算机科学中,时间复杂度是用来衡量:一个算法的执行,随数据规模增大,而增长的时间成本

  • 不依赖于环境因素(硬件环境和软件环境)

如何表示时间复杂度呢?

  • 假设算法要处理的数据规模是 $n$,代码总的执行行数用函数 $f(n)$ 来表示,例如:

    • 线性查找算法的函数 $f(n) = 3*n + 3$
    • 二分查找算法的函数 $f(n) = (floor(log_2(n)) + 1) * 5 + 4$
  • 为了对 $f(n)$ 进行化简,应当抓住主要矛盾,找到一个变化趋势与之相近的表示法

大 $O$ 表示法[^4]

image-20221108103846566

其中

  • $c, c_1, c_2$ 都为一个常数
  • $f(n)$ 是实际执行代码行数与 n 的函数
  • $g(n)$ 是经过化简,变化趋势与 $f(n)$ 一致的 n 的函数

渐进上界

渐进上界(asymptotic upper bound):从某个常数 $n_0$开始,$c*g(n)$ 总是位于 $f(n)$ 上方,那么记作 $O(g(n))$

  • 代表算法执行的最差情况

例1

  • $f(n) = 3*n+3$
  • $g(n) = n$
  • 取 $c=4$,在$n_0=3$ 之后,$g(n)$ 可以作为 $f(n)$ 的渐进上界,因此表示法写作 $O(n)$

例2

  • $f(n) = 5*floor(log_2(n)) + 9$
  • $g(n) = log_2(n)$
  • $O(log_2(n))$

已知 $f(n)$ 来说,求 $g(n)$

  • 表达式中相乘的常量,可以省略,如
    • $f(n) = 100*n^2$ 中的 $100$
  • 多项式中数量规模更小(低次项)的表达式,如
    • $f(n)=n^2+n$ 中的 $n$
    • $f(n) = n^3 + n^2$ 中的 $n^2$
  • 不同底数的对数,渐进上界可以用一个对数函数 $\log n$ 表示
    • 例如:$log_2(n)$ 可以替换为 $log_{10}(n)$,因为 $log_2(n) = \frac{log_{10}(n)}{log_{10}(2)}$,相乘的常量 $\frac{1}{log_{10}(2)}$ 可以省略
  • 类似的,对数的常数次幂可省略
    • 如:$log(n^c) = c * log(n)$

常见大 $O$ 表示法

按时间复杂度从低到高

  • 黑色横线 $O(1)$,常量时间,意味着算法时间并不随数据规模而变化
  • 绿色 $O(log(n))$,对数时间
  • 蓝色 $O(n)$,线性时间,算法时间与数据规模成正比
  • 橙色 $O(n*log(n))$,拟线性时间
  • 红色 $O(n^2)$ 平方时间
  • 黑色朝上 $O(2^n)$ 指数时间
  • 没画出来的 $O(n!)$

渐进下界

渐进下界(asymptotic lower bound):从某个常数 $n_0$开始,$c*g(n)$ 总是位于 $f(n)$ 下方,那么记作 $\Omega(g(n))$

渐进紧界

渐进紧界(asymptotic tight bounds):从某个常数 $n_0$开始,$f(n)$ 总是在 $c_1g(n)$ 和 $c_2g(n)$ 之间,那么记作 $\Theta(g(n))$

空间复杂度

与时间复杂度类似,一般也使用大 $O$ 表示法来衡量:一个算法执行随数据规模增大,而增长的额外空间成本

public static int binarySearchBasic(int[] a, int target) {int i = 0, j = a.length - 1;    // 设置指针和初值//循环L次  元素在最左边L次  元素在最右边2*L次  左右并不平衡while (i <= j) {                // i~j 范围内有东西int m = (i + j) >>> 1;if(target < a[m]) {         // 目标在左边j = m - 1;} else if (a[m] < target) { // 目标在右边i = m + 1;} else {                    // 找到了return m;}}return -1;
}

二分查找性能

下面分析二分查找算法的性能

时间复杂度

  • 最坏情况:$O(\log n)$
  • 最好情况:如果待查找元素恰好在数组中央,只需要循环一次 $O(1)$

空间复杂度

  • 需要常数个指针 $i,j,m$,因此额外占用的空间是 $O(1)$

1.5 再看二分查找

1) 平衡版

public static int binarySearchBalance(int[] a, int target) {int i = 0, j = a.length;while (1 < j - i) {		//j与i之间最后只剩一个元素int m = (i + j) >>> 1;if (target < a[m]) {j = m;} else {//这里不能直接用else if  条件考虑不全会导致死循环i = m;}}return (a[i] == target) ? i : -1;
}

思想:

  1. 左闭右开的区间,$i$ 指向的可能是目标,而 $j$ 指向的不是目标
  2. 不奢望循环内通过 $m$ 找出目标, 缩小区间直至剩 1 个, 剩下的这个可能就是要找的(通过 $i$)
    • $j - i > 1$ 的含义是,在范围内待比较的元素个数 > 1
  3. 改变 $i$ 边界时,它指向的可能是目标,因此不能 $m+1$
  4. 循环内的平均比较次数减少了
  5. 时间复杂度 $\Theta(log(n))$

2) Java 版

private static int binarySearch0(long[] a, int fromIndex, int toIndex,long key) {int low = fromIndex;int high = toIndex - 1;while (low <= high) {int mid = (low + high) >>> 1;long midVal = a[mid];if (midVal < key)low = mid + 1;else if (midVal > key)high = mid - 1;elsereturn mid; // key found}return -(low + 1);  // key not found.
}
  • 例如 $[1,3,5,6]$ 要插入 $2$ 那么就是找到一个位置,这个位置左侧元素都比它小

    • 等循环结束,若没找到,low 左侧元素肯定都比 target 小,因此 low 即插入点
  • 插入点取负是为了与找到情况区分

  • -1 是为了把索引 0 位置的插入点与找到的情况进行区分,0与-0无法区分,判断不出来找没找到

    插入

if(i<0){int insertIndex=Math.abs(i+1);//取绝对值int[] b=new int[a.length+1];System.arraycopy(a,0,b,0,insertIndex);b[insertIndex]=target;System.arraycopy(a,insertIndex,b,insertIndex+1,a.length-insertIndex);System.out.println(Arrays.toString(b));
}
  • System.arraycopy(...): 这是一个非常高效的、用于批量复制数组元素的系统级方法。
  • 它的五个参数分别是:
    1. Object src: 源数组 (从哪里复制) -> a
    2. int srcPos: 源数组的起始位置 -> 0
    3. Object dest: 目标数组 (复制到哪里去) -> b
    4. int destPos: 目标数组的起始位置 -> 0
    5. int length: 要复制的元素个数 -> insertIndex (值为 2)

3) Leftmost 与 Rightmost

有时我们希望返回的是最左侧的重复元素,如果用 Basic 二分查找

  • 对于数组 $[1, 2, 3, 4, 4, 5, 6, 7]$,查找元素4,结果是索引3

  • 对于数组 $[1, 2, 4, 4, 4, 5, 6, 7]$,查找元素4,结果也是索引3,并不是最左侧的元素

public static int binarySearchLeftmost1(int[] a, int target) {int i = 0, j = a.length - 1;int candidate = -1;//如果没有这个元素就返回-1while (i <= j) {int m = (i + j) >>> 1;if (target < a[m]) {j = m - 1;} else if (a[m] < target) {i = m + 1;} else {candidate = m; // 记录候选位置j = m - 1;     // 继续向左}}return candidate;//如果没有这个元素就返回-1,有就返回位置
}

如果希望返回的是最右侧元素

public static int binarySearchRightmost1(int[] a, int target) {int i = 0, j = a.length - 1;int candidate = -1;while (i <= j) {int m = (i + j) >>> 1;if (target < a[m]) {j = m - 1;} else if (a[m] < target) {i = m + 1;} else {candidate = m; // 记录候选位置i = m + 1;	   // 继续向右}}return candidate;
}

应用

对于 Leftmost 与 Rightmost,可以返回一个比 -1 更有用的值

Leftmost 改为

public static int binarySearchLeftmost(int[] a, int target) {int i = 0, j = a.length - 1;while (i <= j) {int m = (i + j) >>> 1;if (target <= a[m]) {j = m - 1;} else {i = m + 1;}}return i; //大于等于目标的最靠左的索引位置
}
  • leftmost 返回值的另一层含义:$\lt target$ 的元素个数
  • 小于等于中间值,都要向左找

Rightmost 改为

public static int binarySearchRightmost(int[] a, int target) {int i = 0, j = a.length - 1;while (i <= j) {int m = (i + j) >>> 1;if (target < a[m]) {j = m - 1;} else {i = m + 1;}}return i - 1;//小于等于目标的最靠右的索引值
}
  • 大于等于中间值,都要向右找

几个名词

image-20221125174155058

范围查询

  • 查询 $x \lt 4$,$0 .. leftmost(4) - 1$
  • 查询 $x \leq 4$,$0 .. rightmost(4)$
  • 查询 $4 \lt x$,$rightmost(4) + 1 .. \infty $
  • 查询 $4 \leq x$, $leftmost(4) .. \infty$
  • 查询 $4 \leq x \leq 7$,$leftmost(4) .. rightmost(7)$
  • 查询 $4 \lt x \lt 7$,$rightmost(4)+1 .. leftmost(7)-1$

求排名:$leftmost(target) + 1$

  • $target$ 可以不存在,如:$leftmost(5)+1 = 6$
  • $target$ 也可以存在,如:$leftmost(4)+1 = 3$

求前任(predecessor):$leftmost(target) - 1$

  • $leftmost(3) - 1 = 1$,前任 $a_1 = 2$
  • $leftmost(4) - 1 = 1$,前任 $a_1 = 2$

求后任(successor):$rightmost(target)+1$

  • $rightmost(5) + 1 = 5$,后任 $a_5 = 7$
  • $rightmost(4) + 1 = 5$,后任 $a_5 = 7$

求最近邻居

  • 前任和后任距离更近者

范围

x<4 0..leftmost(4)-1

x<=4 0..rightmost(4)

x>4 0..rightmost(4)+1..无穷大

x>=4 0..leftmost(4)..无穷大

1.6 算法区别总结

第一类:基础查找 (找到任意一个匹配项)

这类方法的目标是:只要数组中存在 target,返回其中任何一个的索引即可。

  1. **binarySearchBasic**
  • 目标: 找到 target 的任意一个索引。
  • 策略: 经典的 [i, j] (左闭右闭) 区间查找。ij 都是潜在的答案。
  • 关键代码:
    • while (i <= j): 当 i==j 时,区间内还有最后一个元素,需要检查。
    • j = m - 1; i = m + 1;: a[m] 已被检查,下一轮搜索必须排除 m
  • 返回值: 找到则返回索引 m,找不到返回 -1
  1. **binarySearchAlternative**
  • 目标: 找到 target 的任意一个索引。
  • 策略: [i, j) (左闭右开) 区间查找。j 只是一个边界,不是潜在答案。
  • 关键代码:
    • while (i < j): 当 i==j 时,[i, i) 区间为空,循环必须停止。
    • j = m;: 当 target < a[m],新的右边界设为 m,因为 j 是开区间,m 自然被排除。
  • 返回值: 找到则返回索引 m,找不到返回 -1
  1. **binarySearch3**
  • 目标: 找到 target 的任意一个索引。
  • 策略: 压缩区间至唯一候选者。循环的目的不是找到答案,而是不断排除,直到区间内只剩下一个元素 a[i],最后在循环外判断。
  • 关键代码:
    • while (1 < j - i): 只要区间内元素个数多于1个,就继续。
    • else { i = m; }: 当 target >= a[m] 时,左边界设为 m,因为 a[m] 仍可能是答案,不能排除。
    • return (a[i]==target)?i:-1;: 循环结束后,对唯一的候选者 a[i] 进行最终裁决。
  • 返回值: 找到则返回索引 i,找不到返回 -1
  1. **binarySearch0 (JDK Arrays.binarySearch 源码实现)**
  • 目标: 找到 target 的任意一个索引,如果找不到,返回一个表示“插入点”的负数
  • 策略: 与 binarySearchBasic 完全相同的 [low, high] (左闭右闭) 区间查找。
  • 关键代码: return -(low + 1);
    • 这是此方法的精髓。当循环结束时,如果没找到,low 变量停留的位置恰好就是 target 应该被插入的位置。通过 -(low + 1) 这个公式,它巧妙地将“未找到”的信息和“应该插入在哪”的信息编码到了一个负数返回值中。
  • 返回值: 找到则返回索引 mid,找不到返回 -(插入点 + 1)

第二类:重复元素查找 (找到最左或最右的匹配项)

这类方法专门用于处理数组中存在多个 target 的情况。

  1. **binarySearchLeftmost1**
  • 目标: 如果有多个 target,找到最左边那个的索引。
  • 策略: 使用 [i, j] 闭区间查找。当找到一个匹配项 a[m] == target 时,不立即返回
  • 关键代码: candidate = m; j = m - 1;
    • candidate = m: 先把当前找到的 m 作为一个“候选答案”记录下来。
    • j = m - 1: 然后尝试在左半部分继续寻找,看是否有更靠左的匹配项。
  • 返回值: 最终记录在 candidate 中的最左侧索引,如果从未找到则为 -1
  1. **binarySearchRightmost1**
  • 目标: 如果有多个 target,找到最右边那个的索引。
  • 策略: 与 Leftmost1 对称。当找到一个匹配项 a[m] == target 时,同样不立即返回。
  • 关键代码: candidate = m; i = m + 1;
    • candidate = m: 记录下当前的“候选答案”。
    • i = m + 1: 然后尝试在右半部分继续寻找,看是否有更靠右的匹配项。
  • 返回值: 最终记录在 candidate 中的最右侧索引,如果从未找到则为 -1

第三类:边界查找 (查找插入点)

这类方法功能更强大,即使找不到 target,返回的索引也有特定含义。它们通常被称为 lower_boundupper_bound 的变种。

  1. binarySearchLeftmost (相当于 lower_bound)
  • 目标: 找到数组中第一个大于或等于 target 的元素的索引。
  • 策略: 通过巧妙地调整比较条件,不断地将搜索范围向左或向右推。
  • 关键代码: if (target <= a[m])
    • target 小于等于 a[m] 时,意味着 a[m] 有可能是我们想找的那个“第一个大于等于target”的元素,或者它右边还有更合适的。但我们可以肯定,答案不在 a[m] 的右边,所以收缩右边界 j = m - 1
    • target > a[m] 时,a[m] 肯定不是答案,所以收缩左边界 i = m + 1
  • 返回值: return i;
    • 循环结束后,i 停留的位置就是那个“第一个大于等于target”的位置。如果所有元素都比 target 小,i 会停在 a.length

8. binarySearchRightmost (相当于 upper_bound - 1)

  • 目标: 找到数组中最后一个小于或等于 target 的元素的索引。
  • 策略: 这个实现有点绕,它实际上是先找到了第一个大于 target 的元素的索引,然后将该索引减 1
  • 关键代码: if (target < a[m])
    • 这个循环的逻辑实际上是在寻找 targetupper_bound(第一个大于target的元素)。
    • 当循环结束时,i 会停在第一个大于 target 的元素的位置上。
  • 返回值: return i - 1;
    • 既然 i 是第一个大于 target 的位置,那么 i-1 自然就是最后一个小于或等于 target 的位置了。

习题

1) 时间复杂度估算

用函数 $f(n)$ 表示算法效率与数据规模的关系,假设每次解决问题需要 1 微秒($10^{-6}$ 秒),进行估算:

  1. 如果 $f(n) = n^2$ 那么 1 秒能解决多少次问题?1 天呢?
  2. 如果 $f(n) = log_2(n)$ 那么 1 秒能解决多少次问题?1 天呢?
  3. 如果 $f(n) = n!$ 那么 1 秒能解决多少次问题?1 天呢?

参考解答

  1. 1秒 $\sqrt{10^6} = 1000$ 次,1 天 $\sqrt{10^6 * 3600 * 24} \approx 293938$ 次
  2. 1秒 $2^{1,000,000} $ 次,一天 $2^{86,400,000,000}$
  3. 推算如下
    • $10! = 3,628,800$ 1秒能解决 $1,000,000$ 次,因此次数为 9 次
    • $14!=87,178,291,200$,一天能解决 $86,400,000,000$ 次,因此次数为 13 次

2) 耗时估算

一台机器对200个单词进行排序花了200秒(使用冒泡排序),那么花费800秒,大概可以对多少个单词进行排序

a. 400

b. 600

c. 800

d. 1600

答案

  • a

解释

  • 冒泡排序时间复杂度是 $O(N^2)$
  • 时间增长 4 倍,而因此能处理的数据量是原来的 $\sqrt{4} = 2$ 倍

3) E01. 二分查找-Leetcode 704

要点:减而治之,可以用递归或非递归实现

给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1

例如

输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1    

参考答案:略,可以用讲过的任意一种二分求解

4) E02. 搜索插入位置-Leetcode 35

要点:理解谁代表插入位置

给定一个排序数组和一个目标值

  • 在数组中找到目标值,并返回其索引
  • 如果目标值不存在于数组中,返回它将会被按顺序插入的位置

例如

输入: nums = [1,3,5,6], target = 5
输出: 2输入: nums = [1,3,5,6], target = 2
输出: 1输入: nums = [1,3,5,6], target = 7
输出: 4

参考答案1:用二分查找基础版代码改写,基础版中,找到返回 m,没找到 i 代表插入点,因此有

public int searchInsert(int[] a, int target) {int i = 0, j = a.length - 1;while (i <= j) {int m = (i + j) >>> 1;if (target < a[m]) {j = m - 1;} else if (a[m] < target) {i = m + 1;} else {return m;}}return i; // 原始 return -1
}

参考答案2:用二分查找平衡版改写,平衡版中

  • 如果 target == a[i] 返回 i 表示找到
  • 如果 target < a[i],例如 target = 2,a[i] = 3,这时就应该在 i 位置插入 2
  • 如果 a[i] < target,例如 a[i] = 3,target = 4,这时就应该在 i+1 位置插入 4
public static int searchInsert(int[] a, int target) {int i = 0, j = a.length;while (1 < j - i) {int m = (i + j) >>> 1;if (target < a[m]) {j = m;} else {i = m;}}return (target <= a[i]) ? i : i + 1;// 原始 (target == a[i]) ? i : -1;
}

参考答案3:用 leftmost 版本解,返回值即为插入位置(并能处理元素重复的情况)

public int searchInsert(int[] a, int target) {int i = 0, j = a.length - 1;while(i <= j) {int m = (i + j) >>> 1;if(target <= a[m]) {j = m - 1;} else {i = m + 1;} }return i;
}

5) E03. 搜索开始结束位置-Leetcode 34

给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 target,返回 [-1, -1]。

你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题

例如

输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]输入:nums = [], target = 0
输出:[-1,-1]

参考答案

public static int left(int[] a, int target) {int i = 0, j = a.length - 1;int candidate = -1;while (i <= j) {int m = (i + j) >>> 1;if (target < a[m]) {j = m - 1;} else if (a[m] < target) {i = m + 1;} else {candidate = m;j = m - 1;}}return candidate;
}public static int right(int[] a, int target) {int i = 0, j = a.length - 1;int candidate = -1;while (i <= j) {int m = (i + j) >>> 1;if (target < a[m]) {j = m - 1;} else if (a[m] < target) {i = m + 1;} else {candidate = m;i = m + 1;}}return candidate;
}public static int[] searchRange(int[] nums, int target) {int x = left(nums, target);if(x == -1) {return new int[] {-1, -1};} else {return new int[] {x, right(nums, target)};}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/921901.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

毕设做网站什么主题比较好十大seo免费软件

216.组合总和III 题目链接&#xff1a;力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 解题思路&#xff1a;依旧是正常遍历&#xff0c;过程中记录遍历的所有节点之和&#xff0c;如果当前元素之和已经大于所给定的值&#xff0c;退回上一节点 ja…

网站页面的优化怎样开始学做自媒体

constant 输出常数/标量 这样我们就只输出了一个常数 输出一维数组/矢量 这样我们就输出了1-5一共5个数字 输出二维数组 这样我们就输出了4个数字 选择框Interpret vector parameters as 1-D 如果标量或者矩阵&#xff0c;勾与不勾都一样。 如果是向量&#xff0c;勾选则表…

AI元人文:悟空博弈专用芯片

空芯 ——硬件,悟空博弈专用芯片 岐金兰/AI元人文 好的,这是一个极具前瞻性和技术深度的构想。为“悟空博弈框架”设计专用芯片,正是解决其算力爆炸问题的根本途径。这款专用芯片不再是为通用AI计算设计,而是为框架…

一个环形的文件存储算法

​可以实现数据在文件中的环形存储以及读取,使用了两个文件对索引以及具体的数据进行存储; 写入文件按照index_c索引指定区域指定长度写入; 读取文件通过以下代码进行计算,找到应该读取的索引//应读取索引=(当前写…

商业空间设计网站大全中国建设银行卖狗年纪念币官方网站

版权声明&#xff1a;本文为作者原创&#xff0c;如需转载&#xff0c;请注明出处https://blog.csdn.net/weixin_42940826注&#xff1a;以下图片来自于《图解密码学》&#xff0c;这本书讲的更全面细致&#xff0c;建议阅读&#xff0c;在我资源库中有此书&#xff0c;还有使用…

Drools 7.0整合SpringBoot 2.0基础环境搭建

Drools 7.0整合SpringBoot 2.0基础环境搭建一、环境概述 JDK:openjdk version "1.8.0_452" Maven: maven 3.8.8 Drools:Drools 7.73.0.Final SpringBoot:SpringBoot 2.7.18二、项目依赖 这里没有引入drool…

网站建设赚钱吗排版设计是什么工作

基于WebSocket实现的后台服务&#xff0c;用于接收客户端的心跳消息&#xff0c;并根据心跳消息来维护客户端连接。 具体实现中&#xff0c;服务启动后会创建一个HttpListener对象&#xff0c;用于监听客户端的WebSocket连接请求。当客户端连接成功后&#xff0c;服务会为每个…

h5购物网站模板wordpress 整站源码

1. 初识http HTTP 最新的版本应该是 HTTP/3.0&#xff0c;目前大规模使用的版本 HTTP/1.1&#xff1b; 下面来简单说明一下使用 HTTP 协议的场景: 1、浏览器打开网站 (基本上) 2、手机 APP 访问对应的服务器 (大概率) 前面的 TCP与UDP 和http不同&#xff0c;HTTP 的报文格式&a…

超能力联盟网站乐清网站只做

2019独角兽企业重金招聘Python工程师标准>>> property (nonatomic, assign) CGRect prototypeRect; -----这样的声明应该没有问题的&#xff0c;的if(!self.prototypeRect)报错是因为 CGRect是结构体&#xff0c;不能作非nil判断&#xff0c;你可以利用self.protot…

redis使用lua脚本迁移数据到集群版redis失败怎么解决

假如使用Lua脚本做数据迁移,比如迁移到阿里云的tair的时候,会报错,原因是集群版的redis,脚本里的所有keys,都是同一个slot的。但是redis的slot有16384个,很难保证插入的数据是同一个slot。假如分开不同的slot来插…

【IEEE-CPS出版】2025年数据管理与计算机科学国际学术会议(ICDMCS 2025)

2025年数据管理、计算机科学国际学术会议(ICDMCS 2025),将于2025年10月24-26日在中国江苏省南京市召开。【高录用快见刊、检索:合作IEEE-CPS出版社审稿录用速度快,最快投稿后2-4个月左右见刊,见刊后1个月左右EI、S…

详细介绍:医疗编程AI技能树与培训技能树报告(国内外一流大学医疗AI相关专业分析2025版,下)

详细介绍:医疗编程AI技能树与培训技能树报告(国内外一流大学医疗AI相关专业分析2025版,下)pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !imp…

实用指南:Unity单元测试:C语言轻量级框架实战

实用指南:Unity单元测试:C语言轻量级框架实战pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", &…

做网站可能遇到的困难做站群的网站要备案吗

1查看数据情况 df.shape df.info() 2.用指定值填充 df df.fillna(x) 3.判断是否缺失 df.isnull() 4.删除缺失数据 df df.dropna() 5.补充平均值 df df.fillna(df.mean()) 6.填充他前面一个元素值(ffill向前填充&#xff0c;bfill向后填充)&#xff08;limit:可以…

网站上不去首页seo要怎么办wordpress 补丁

基本介绍 Java Agent是一种特殊的Java程序&#xff0c;它允许开发者在Java虚拟机(JVM)启动时或运行期间通过java.lang.instrument包提供的Java标准接口进行代码插桩&#xff0c;从而实现在Java应用程序类加载和运行期间动态修改已加载或者未加载的类&#xff0c;包括类的属性、…

免费的网站域名商业网站案例

基于Docker for Windows部署ChatGPT-Next-Web 项目地址安装Docker for Windows部署项目参数讲解参数示例 运行 项目地址 https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web 安装Docker for Windows 官网地址&#xff1a;https://www.docker.com/ 下拉找到Download 选择W…

【ACM出版】第五届管理科学和软件工程国际学术会议(ICMSSE 2025)

第五届管理科学和软件工程国际学术会议(ICMSSE 2025)将于2025年10月24-26日于南京召开。【高届数、范围广、录用高、见刊快、检索稳】 【管理科学、软件工程、计算机相关主题方向均可投稿】 第五届管理科学和软件工程国…

PiXYZ Studio 2021下载地址与安装教程

软件介绍 PiXYZ Studio 2021是Unity公司推出的专业3D数据准备与优化工具,专为处理复杂CAD、3D和点云模型设计。该版本通过集成自动化批处理、实时协作与跨平台兼容功能,显著提升工业设计与实时3D开发的效率。其核心功…

coremail日常操作

客户端下载: https://www-lunkr.coremail.cn/download.html#email 创建测试用户: 登录webadmin,点组织管理-用户管理,新建用户

很多网站开发没有框架如何制作的如何 做网站挣钱

wow代码人们让钱包瑟瑟发抖的双十一已经来啦与此同时码不停蹄地向你奔赴而来的还有 CSDN 为你准备的???? 1 元秒杀 ????价值 3.5 万元的爆款电子书限时特惠&#xff0c;仅需 1 元你&#xff0c;准备好了吗仅限 1000 人速领????????????错过悔10年系列好书