数据结构 07(上)|查找:顺序、折半、分块,ASL 到底怎么

2024 年第 5 题,四个选项我挨个判断,最后自信地选了 C。

题目问:下列数据结构中不适合直接使用折半查找的是哪些?

  • I 有序链表

  • II 无序数组

  • III 有序静态链表

  • IV 无序静态链表

我的判断是:I 不行(链表不能随机存取)、II 不行(无序)、IV 不行(两个毛病都占),但 III 有序静态链表我觉得可以------它物理上不就是用数组存的吗?数组能随机存取,那不就能折半了?

答案是 D:四个全都不行。

我漏掉的关键点是:静态链表中,下标的相邻不等于逻辑上的相邻 。它虽然躺在数组里,但元素的先后顺序是由 cur 游标串起来的,A[0] 后面接的可能是 A[7]、再接 A[3]。你去取 A[(low+high)/2] 这个"正中间的下标",取到的根本不是逻辑上排在中间的那个元素。

一句话:折半查找要的不是"存在数组里",而是"第 k 个元素能在 O(1) 时间内直接拿到"。


📍 查找系列共三篇 : 上篇(本篇)线性查找 ------顺序查找、折半查找、分块查找; 中篇 树形查找 ------二叉排序树、平衡二叉树; 下篇 多路与散列------B 树、B+ 树、散列表。

〇、先花两分钟:查找、ASL、判定树是什么

查找(Search):在一堆数据里找到关键字等于给定值的那个元素。

ASL (Average Search Length,读作"平均查找长度"):查找过程中平均需要比较多少次关键字。它是衡量查找算法效率的核心指标。

复制代码
ASL = Σ (查到第 i 个元素所需的比较次数 × 查到第 i 个元素的概率)

等概率、每个元素都要考虑时,化简成:

复制代码
ASL = (所有元素的比较次数之和) ÷ 元素个数

判定树 :把折半查找过程中"每次跟谁比"画成一棵二叉树。树里的每个结点是一次比较的对象,层数就是比较次数。

折半查找判定树、三种 ASL 公式推导、分块查找最优块长、跳跃查找步骤


一、ASL 的定义:先搞清"比较次数"是怎么数的

这是全章的地基,很多题做错不是因为公式不会,而是比较次数数错了。

一条约定:每拿待查关键字和表中一个元素比一次,就算 1 次比较。

举 5 个元素的有序表 10, 20, 30, 40, 50,等概率查每个元素:

元素 顺序查找需要的比较次数
10 1
20 2
30 3
40 4
50 5

查找成功时:

复制代码
ASL成功 = (1 + 2 + 3 + 4 + 5) ÷ 5 = 3

⚠️ 注意这里数的是"从头到尾逐个比",找到 50 要比 5 次(比完前 4 个都不同,第 5 次才相等)。有人会想成"找到就停,最后那个不算",那就错了。

概率不等时要加权,不能除以 n

如果各个元素被查找的概率不一样,就必须按概率加权。举个 3 个元素的例子:

元素 位置 比较次数 被查找的概率
a 第 1 个 1 0.6
b 第 2 个 2 0.3
c 第 3 个 3 0.1
复制代码
ASL = 1×0.6 + 2×0.3 + 3×0.1 = 0.6 + 0.6 + 0.3 = 1.5

(等概率时是 (1+2+3)/3 = 2。可见把查找频率高的元素往前放,能明显降低 ASL。)


二、顺序查找:ASL = (n+1)/2 是怎么来的

顺序查找就是从头到尾挨个比。n 个元素,查第 1 个比 1 次、查第 2 个比 2 次......查第 n 个比 n 次。

复制代码
ASL成功 = (1 + 2 + … + n) ÷ n
        = [n(n+1)/2] ÷ n
        = (n + 1) / 2

这个公式怎么读:「平均要找一半左右的位置」。n = 5 时 (5+1)/2 = 3,和上面手算的一致。

查找失败时,要把整表扫完才知道没有,所以:

复制代码
ASL失败 = n

顺序查找的时间复杂度是 O(n),优点是:

  • 对存储没要求,顺序表和链表都行

  • 不要求元素有序

哨兵(监视哨)

把待查关键字先放到下标 0(或 n),从后往前比,循环里就不用再判断下标有没有越界了。

推荐写法:

复制代码
A[0] = x;          // 哨兵
i = n;
while (A[i] != x) i--;

循环结束后若 i == 0 就是查找失败,否则 i 就是位置。

⚠️ 408 里有个细节容易考:加了哨兵之后,从后往前比时,查第 n 个元素比较 1 次、查第 1 个元素比较 n 次,ASL 仍然是 (n+1)/2,只是省掉了越界判断的开销。

有序表的顺序查找可以提前终止

如果表是有序的,顺序查找时一旦发现"当前元素已经比待查关键字大了",就能直接判定失败,不必扫完整个表。

n 个元素时,失败有 n+1 种情况(元素落在 n−1 个间隙里,或者比最小的小、比最大的大):

落入的位置 比较次数
第 1 个位置之前 1
第 1、2 个之间 2
... ...
第 n−1、n 个之间 n
第 n 个之后 n
复制代码
ASL失败 = [1 + 2 + … + n + n] ÷ (n + 1)
        = [n(n+1)/2 + n] ÷ (n + 1)
        = n(n+3) / [2(n+1)]

n = 5 时 = 40/12 ≈ 3.33,明显小于无序表的 ASL失败 = n = 5。

这个点常出判断题:「对有序表做顺序查找,查找失败时平均比较次数约为 n/2」------说法是对的。


三、折半查找的两个前提:有序 + 随机存取

折半查找(二分查找):每次拿"中间那个元素"跟待查关键字比,比完就排除掉一半,再在剩下的一半里继续。

它成立必须同时满足两个条件:

前提 原因
元素有序(按关键字排序) 否则"比中间值小就往左"这个判断不成立
支持随机存取(顺序存储) 否则拿不到"正中间那个元素"

2024(5) 逐个验(答案 D):

选项 有序? 随机存取? 结论
I 有序链表 ✓ ✗ 链式 不适合
II 无序数组 ✗ ✓ 不适合
III 有序静态链表 ✓ ✗ 逻辑次序由游标决定 不适合
IV 无序静态链表 ✗ ✗ 不适合

2024(5) 答案:D。

⚠️ 记牢 III 这条:静态链表的下标相邻 ≠ 逻辑相邻,这是我踩的那个坑。

折半查找的代码长什么样

非递归写法(标准写法,要会认、会填空):

复制代码
int BinarySearch(int A[], int n, int x) {
    int low = 0, high = n - 1;
    while (low <= high) {
        int mid = (low + high) / 2;
        if (A[mid] == x)   return mid;          // 找到了
        else if (A[mid] > x) high = mid - 1;    // 目标在左半边
        else                 low  = mid + 1;    // 目标在右半边
    }
    return -1;                                  // 查找失败
}

怎么读:

  • low 和 high 圈定的是当前还可能包含目标的下标区间 ,闭区间 [low, high]

  • 循环条件是 low <= high (不是 <)------ 因为 low == high 时区间里还剩一个元素没比

  • 取完 mid 后若 A[mid] > x,说明目标只可能在左边,于是把右边界收到 mid − 1

  • 一路没撞上就返回 −1

⚠️ 循环条件写成 low < high 是最常见的错,那样会把最后一个元素漏掉。

递归写法(了解即可):

复制代码
int BinRec(int A[], int low, int high, int x) {
    if (low > high) return -1;
    int mid = (low + high) / 2;
    if (A[mid] == x)   return mid;
    if (A[mid] > x)    return BinRec(A, low, mid - 1, x);
    return BinRec(A, mid + 1, high, x);
}

递归版每往下走一层区间就减半,递归深度 = 判定树高度 = ⌊log₂n⌋ + 1 ,所以空间复杂度是 O(log n)(递归栈)。


四、折半查找判定树:怎么画

画法规则(按 408 通用约定,向下取整):

复制代码
mid = ⌊(low + high) / 2⌋

左子树 是所有比 Amid 小的元素(下标 low ~ mid−1),右子树是比它大的(下标 mid+1 ~ high)。递归下去。

拿 11 个元素 a₁ ~ a₁₁ 举例:

步骤 low high mid = ⌊(low+high)/2⌋ 根
1 1 11 6 a₆
2(左) 1 5 3 a₃
3(右) 7 11 9 a₉
4 1 2 1 a₁
5 4 5 4 a₄
6 7 8 7 a₇
7 10 11 10 a₁₀

画出来:

复制代码
                a₆                  ← 第 1 层(比较 1 次)
            /        \
         a₃            a₉           ← 第 2 层(比较 2 次)
       /    \        /    \
     a₁     a₄     a₇      a₁₀      ← 第 3 层(比较 3 次)
      \      \      \       \
      a₂     a₅     a₈      a₁₁     ← 第 4 层(比较 4 次)

层号就是比较次数:查 a₆ 比 1 次,查 a₂ 比 4 次。

快速画法(考试时不用递归推):判定树每层元素个数按 1、2、4、8... 翻倍,直到凑满 n 个,最后一层不满就剩多少算多少。

元素总数 n 各层元素个数 树高
7 1、2、4 3
11 1、2、4、4 4
16 1、2、4、8、1 5

(校验:每行加起来都等于对应的 n ✓)

这条能让你在十秒内判断出树高,进而直接得到"最多比较次数"。

⚠️ 关于取整:有的教材用 ⌈(low+high)/2⌉(向上取整),画出来的树形会左右镜像不同,但层数分布和 ASL 结论完全一样。408 真题里的标准答案按 ⌊⌋ 处理,考试时用哪个都不会影响"最多比较几次"这类问题的答案。


五、判定树的两条性质(2015(7) 就考这个)

性质 1:判定树是一棵二叉排序树。 任意结点,左子树所有元素 < 它 < 右子树所有元素。原因很直接:折半时"小的往左、大的往右"。

性质 2:判定树是一棵平衡二叉树(任意结点左右子树高度差不超过 1)。 因为每次都是从正中间切开,两边元素个数最多差 1。

这两条性质直接催生了 2015(7) 这种题:

下列选项中,不能构成折半查找中关键字比较序列的是( )。

做法:把序列当成"查找路径",检查每一步是否落在上一步划定的区间内。

以 A 选项 500, 200, 450, 180 为例:

步 当前值 上一步划定的区间 在区间内?
1 500 (−∞, +∞) ✓
2 200 小于 500 → (−∞, 500) ✓
3 450 大于 200 且小于 500 → (200, 500) ✓
4 180 应为 (200, 500),但 180 < 200 ✗

180 落在了区间外面,路径不合法。

其余三项验一遍(都能通过):

  • B 500, 450, 200, 180 → 区间依次收窄为 (−∞,500) → (−∞,450) → (−∞,200),180 在内 ✓

  • C 180, 500, 200, 450 → (180,+∞) → (180,500) → (200,500),450 在内 ✓

  • D 180, 200, 500, 450 → (180,+∞) → (200,+∞) → (200,+∞),450 在内 ✓

2015(7) 答案:A。

(等价的另一种思路:把四个选项直接画成树,看是不是二叉排序树------A 中 450 和 180 都挂在 200 下面,180 < 200 却在 200 的右子树方向,违反 BST 性质。)


六、查找成功的 ASL:用判定树一层层数

还是 11 个元素的例子,各层元素个数:第 1 层 1 个、第 2 层 2 个、第 3 层 4 个、第 4 层 4 个。

复制代码
ASL成功 = (1×1 + 2×2 + 3×4 + 4×4) ÷ 11
        = (1 + 4 + 12 + 16) ÷ 11
        = 33 ÷ 11
        = 3

有个更快的公式:n 个元素、判定树高度 h 时,

复制代码
ASL成功 ≈ log₂(n + 1) − 1

这个怎么读:折半查找的平均比较次数约等于「log₂(n+1) 再减 1」。n=11 时 log₂12 ≈ 3.58,减 1 得 2.58------和实际值 3 有出入,因为判定树不是满树。

⚠️ 所以这个近似式只能用来估算数量级,考场上凡是要求精确值的题,一律老老实实用判定树分层数,别拿近似式硬算。

再拿 n = 16(2010(9) 那个长度)完整算一遍,熟悉一下流程:

按 mid = ⌊(low+high)/2⌋ 逐层切分,得到的判定树各层元素个数:

层数 该层元素 个数
1 a₈ 1
2 a₄、a₁₂ 2
3 a₂、a₆、a₁₀、a₁₄ 4
4 a₁、a₃、a₅、a₇、a₉、a₁₁、a₁₃、a₁₅ 8
5 a₁₆ 1

(校验:1+2+4+8+1 = 16 ✓,正好等于元素总数,说明没漏没重)

复制代码
ASL成功 = (1×1 + 2×2 + 3×4 + 4×8 + 5×1) ÷ 16
        = (1 + 4 + 12 + 32 + 5) ÷ 16
        = 54 ÷ 16
        ≈ 3.38

顺带验证:树高 5 = ⌊log₂16⌋ + 1,跟 2010(9) 的答案完全一致。


七、最多比较次数:两个等价公式

这是折半查找考得最频繁的计算点。

复制代码
最多比较次数 = ⌊log₂ n⌋ + 1  =  ⌈log₂ (n + 1)⌉

两个式子等价,挑顺手的用。

2010(9) :长度 16 的顺序表,折半查找一个不存在的元素,最多比较几次?

复制代码
⌊log₂ 16⌋ + 1 = 4 + 1 = 5

2010(9) 答案:B(5)。

2023(8):600 个元素的有序顺序表,折半查找关键字比较次数最多是多少?

复制代码
log₂ 600 ≈ 9.23
⌊9.23⌋ + 1 = 9 + 1 = 10

(用另一个式子验:⌈log₂ 601⌉ = ⌈9.23⌉ = 10 ✓)

2023(8) 答案:B(10)。

n = 600 时,为什么是 10 次

不套公式,直接推一遍:每比较一次区间砍一半,从 600 个元素开始------

第 k 次比较 比较时区间内还有约几个元素
1 600
2 300
3 150
4 75
5 37
6 18
7 9
8 4
9 2
10 1

第 9 次比较时还剩 2 个元素,必须再比 1 次才能确认,所以是 10 次。

(跟 ⌈log₂601⌉ = 10 对得上------手算和公式互为印证,考场上任取一种都行。)

⚠️ 三个提醒:

  1. "最多比较次数"等价于"判定树的高度",别把它和 ASL 混为一谈(ASL 是平均值,最多比较次数是最大值)。

  2. 题目说"查找不存在的元素"时,比较次数仍是判定树高度------因为不成功的查找也要一路比到叶子。

  3. 看到 ⌈x⌉ 不认识?它叫上取整 (读作"ceiling"),就是"向上取到最近的整数";⌊x⌋ 是下取整(floor),向下取。


八、查找失败时,比较次数怎么数

失败的情况要引入外部结点(也叫"失败结点"):在判定树所有空指针的位置补上方形结点,每个方形结点代表一次失败的查找。

算法只有一句:

复制代码
ASL失败 = (所有外部结点的层数之和) ÷ 外部结点个数      (外部结点个数 = n + 1)

拿第 4 节那棵 11 元素的判定树,把 12 个外部结点逐个标出来:

外部结点挂在谁的下面 层数 个数
a₁、a₄、a₇、a₁₀ 的左侧空位 4 4 个
a₂、a₅、a₈、a₁₁ 的左右两个空位 5 8 个
复制代码
层数之和 = 4×4 + 5×8 = 16 + 40 = 56
ASL失败  = 56 ÷ 12 ≈ 4.67

对照一下成功的情况(第 6 节算过 ASL成功 = 3)------失败时要高不少,原因很直观:成功的查找可能半路就撞上了,失败的查找必须一路比到底才能确认"确实没有"。

⚠️ 两个提醒:

  1. 别拿近似式硬套 。ASL失败 ≈ log₂(n+1) 这类式子只在判定树接近满树(n = 2^h − 1)时才准,遇到 n = 11、n = 600 这种"不凑整"的数字会明显失真。考场上要求精确值时,一律画判定树数层数。

  2. 外部结点个数恒等于 n + 1,可以用它快速校验自己有没有数漏。


九、分块查找:两块 ASL 相加

分块查找(索引顺序查找):把 n 个元素分成若干块,满足

  • 块间有序:第 i 块的最大关键字 < 第 i+1 块的最小关键字

  • 块内无序:每块内部随便排

查找分两步:

  1. 先确定在哪个块(在索引表里查,可以顺序查也可以折半查)

  2. 再在块内查找(块内无序,只能顺序查)

于是:

复制代码
ASL = ASL块间 + ASL块内

设总元素 n、每块 s 个元素,则块数 b = n / s。

都用顺序查找时(索引表无序、块内也无序):

复制代码
ASL = (b + 1) / 2 + (s + 1) / 2

索引表用折半查找、块内顺序查找时:

复制代码
ASL = ⌈log₂(b + 1)⌉ + (s + 1) / 2

举个完整的例子

n = 100 个元素,分成 10 块、每块 10 个。索引表和块内都用顺序查找:

复制代码
ASL = (b + 1)/2 + (s + 1)/2 = 11/2 + 11/2 = 5.5 + 5.5 = 11

把索引表改成折半查找(索引表本来就是有序的):

复制代码
ASL = ⌈log₂(10 + 1)⌉ + 11/2 = 4 + 5.5 = 9.5

省了 1.5 次。所以索引表能用折半就用折半------它没有"必须顺序查"的理由,放着 O(log n) 不用是浪费。


十、最优块长为什么是 √n(2025(7) 完整推导)

2025(7):400 个元素,等概率,均匀分块,块间和块内都用顺序查找,为效率最高每块应包含几个元素?

设每块 s 个,块数 b = n / s,代入上面的公式:

复制代码
ASL(s) = (b + 1)/2 + (s + 1)/2
       = (n/s + 1)/2 + (s + 1)/2
       = (n/s + s)/2 + 1

要让 ASL 最小,就是让 n/s + s 最小。这里用均值不等式(高中数学):

复制代码
对于正数 a、b:a + b ≥ 2√(ab),等号在 a = b 时成立

取 a = n/s、b = s:

复制代码
n/s + s ≥ 2√(n/s · s) = 2√n

等号成立的条件:n/s = s,即

复制代码
s² = n    →    s = √n

代入 n = 400:

复制代码
s = √400 = 20

如果不太想得起来均值不等式,用求导也能得到同一个结论:

复制代码
f(s) = n/s + s
f′(s) = −n/s² + 1
令 f′(s) = 0  →  n/s² = 1  →  s² = n  →  s = √n

两种方法结果一致,考场上写哪种都行。我更推荐均值不等式------不用写导数符号,两步就出结论。

2025(7) 答案:C(20)。

⚠️ 常见的两个错法:

  • 把"块数"当成答案 → 400/20 = 20 块,这题凑巧也是 20,但换个 n 就露馅了(比如 n=100 时 s=10、块数=10,也凑巧;n=1000 时 s≈31.6、块数≈31.6,就不一样了)。记住求的是"每块元素数 s"。

  • 忘了 ASL 里那两个 +1,直接对 n/s + s 求导------结论一样(s=√n),但考试要写的是 ASL 的完整表达式,别把 +1 漏掉。


十一、跳跃查找:步长不为 1 的顺序查找(2016(9))

2016(9) 给了一段伪代码,思路是"每次跳 3 个位置":

复制代码
k = 0;
while (k < n 且 A[k] < x) k = k + 3;
if (k < n 且 A[k] == x) 查找成功;
else if (k - 1 < n 且 A[k - 1] == x) 查找成功;
else if (k - 2 < n 且 A[k - 2] == x) 查找成功;
else 查找失败;

读法:先每 3 步跨一次往前冲,等 A[k] >= x 时停住,然后往回退最多 2 个位置逐个检查。这是顺序查找的变种,步长从 1 变成 3。

题目问:与折半查找相比,什么情况下它可能比较次数更少?

  • x 在靠近开头处 :跳不了几步就停了,总比较次数很少 → 答案 B

  • x 在结尾处:要跳 n/3 次,比折半的 log₂n 次多得多 ✗

  • x 在中间:跳 n/6 次,同样吃亏 ✗

  • x 不在数组中:跳跃查找也要跳到尾部 ✗

2016(9) 答案:B(当 x 接近数组开头处)。

⚠️ 这道题不需要精确计算,定性判断就够:跳跃查找省时间的前提是"目标在前面"。


十二、三种线性查找横向对比

项目 顺序查找 折半查找 分块查找
存储要求 顺序 / 链式都行 必须顺序存储 顺序 / 链式都行
有序要求 不要求 必须有序 块间有序、块内无序
ASL(成功) (n+1)/2 ≈ log₂(n+1) − 1 (b+1)/2 + (s+1)/2
时间复杂度 O(n) O(log n) O(√n)(取最优块长时)
适用场景 无序、链表 有序顺序表 块间有序的大表

一句口诀:"顺序随便、折半挑剔、分块折中"。

怎么选?按这个顺序问自己:

  1. 数据是有序的吗?无序 → 只能顺序查找,折半和分块都用不了

  2. 是顺序存储还是链式存储?链式 → 折半直接出局(随机存取这条不满足)

  3. 数据量很大、又能在插入时维持块间有序 → 分块查找更划算

  4. 有序 + 顺序存储 + 很少插入 → 折半查找,O(log n) 最优


十三、易错点集中讲

1. 把"最多比较次数"当成 ASL。 最多比较次数 = 判定树高度(最大值),ASL = 平均值。二者差的不是一星半点。

2. 折半查找忘了"必须随机存取"。 链式存储的有序 链表照样不能折半------2024(5) 的 I 和 III 就是两个典型。

3. 分块查找求的是"每块元素数 s",不是"块数 b"。 2025(7) 里 n=400 时 s=20、b=20,数值恰好相同,极易掩盖概念错误。

4. 顺序查找的比较次数数到 n 就停。 查最后一个元素要比较 n 次(不是 n−1 次),ASL 才是 (n+1)/2。

5. 用近似式硬算非满树的 ASL。 n = 600 这类"不凑整"的数字,近似式误差很大,必须画判定树分层数。


十四、速查表(文末收藏版)

三个 ASL 公式

查找方式 查找成功 ASL 备注
顺序查找 (n + 1) / 2 查找失败 = n
折半查找 用判定树分层数计算 近似式 ≈ log₂(n+1) − 1
分块查找 (b + 1)/2 + (s + 1)/2 b = n/s;最优 s = √n

折半查找的两个关键量

项目 公式
最多比较次数(= 判定树高度) ⌊log₂ n⌋ + 1 = ⌈log₂(n+1)⌉
外部结点个数 n + 1
判定树性质 二叉排序树 + 平衡二叉树

折半查找的两个前提

复制代码
① 元素有序(按关键字排列)
② 支持随机存取(顺序存储;链式、静态链表都不行)

真题考频(2010---2025,线性查找 6 道)

年份(题号) 考点
2010(9) n=16 折半查找不存在的元素,最多 5 次
2015(7) 判定树比较序列合法性(答案 A)
2016(9) 跳跃查找,x 靠近开头时更快
2023(8) n=600 折半查找最多比较 10 次
2024(5) 不适合折半的四种结构(答案 D,全都不行)
2025(7) 分块查找最优块长 √400 = 20

一句话记忆

折半查找的前提是"有序 + 能一步跳到中间",缺一个都不行;最多比较次数 = ⌊log₂n⌋+1;分块查找最优块长 = √n。

相关推荐
Mortalbreeze1 小时前
0 - 1 BFS 算法详解:从基本原理到经典题目建模
数据结构·c++·算法·图论·宽度优先
shehuiyuelaiyuehao16 小时前
算法56,重排链表
数据结构·算法·链表
suhan17 小时前
【数据结构】七、查找:1.查找的概念、线性结构查找(顺序、折半(二分)、插值、稠密、分块、倒排)
数据结构·算法·排序算法
极客 - L U17 小时前
卡尔曼滤波从零到精通
c语言·数据结构·单片机·嵌入式硬件·mcu
Sarvartha20 小时前
栈的四则运算、栈的递归应用
数据结构·算法
GG.WinGel1 天前
三元组中心问题
java·数据结构·算法·青少年编程·蓝桥杯
程序员清风1 天前
缓存、索引与数据结构的工程选择
数据结构·spring·缓存
爱吃火鸡面呀1 天前
Doris 数据划分实战:List 分区、分桶列与桶数选择
数据结构·list
BK贝壳1 天前
亿级用户签到位图选型复盘:用 bool-hybrid-array 扛冷启动打卡,也把它和 Roaring/Redis 的边界测清楚了
数据结构·python·性能优化·numpy