查找是CSP-J初赛必考 的内容,每年至少1-2题,总分值约4-6分 。下面我从顺序查找→二分查找→插值查找→哈希查找,系统讲解所有考点。
一、查找算法总览
| 算法 | 时间复杂度 | 前提条件 | 空间复杂度 | CSP-J考频 |
|---|---|---|---|---|
| 顺序查找 | O(n) | 无 | O(1) | ★★★★★ |
| 二分查找 | O(log n) | 有序 | O(1) | ★★★★★ |
| 插值查找 | O(log log n)~O(n) | 有序且均匀分布 | O(1) | ★ |
| 哈希查找 | O(1)平均 | 哈希表 | O(n) | ★★ |
二、顺序查找(线性查找)
2.1 基本思想
从数组的第一个元素开始,逐个比较,直到找到目标或遍历完整个数组。
2.2 代码模板
cpp
int seqSearch(int a[], int n, int x) {
for (int i = 0; i < n; i++) {
if (a[i] == x) return i; // 找到返回下标
}
return -1; // 未找到
}
2.3 特点分析
| 特性 | 说明 |
|---|---|
| 时间复杂度(最好) | O(1) ------ 第一个元素就是目标 |
| 时间复杂度(最坏) | O(n) ------ 目标在末尾或不存在 |
| 时间复杂度(平均) | O(n) |
| 空间复杂度 | O(1) |
| 适用条件 | 无任何要求,数组可以无序 |
| 稳定性 | 不涉及 |
2.4 典型例题
题目: 在数组
[8, 3, 5, 9, 2, 7, 1]中查找元素 7,顺序查找需要比较( )次。A. 3 B. 4 C. 5 D. 6
答案:D
解析: 依次比较:
- 8≠7(第1次)
- 3≠7(第2次)
- 5≠7(第3次)
- 9≠7(第4次)
- 2≠7(第5次)
- 7=7(第6次,找到)
所以需要比较6次,选D。
三、二分查找(折半查找)★★★★★
3.1 基本思想
在有序数组中,每次取中间元素与目标比较:
- 相等 → 找到
- 目标 < 中间值 → 在左半部分继续找
- 目标 > 中间值 → 在右半部分继续找
3.2 代码模板
写法1:闭区间 l, r
cpp
int binarySearch(int a[], int n, int x) {
int l = 0, r = n - 1;
while (l <= r) { // 注意:l <= r
int mid = l + (r - l) / 2; // 防溢出写法
if (a[mid] == x) return mid;
else if (a[mid] < x) l = mid + 1;
else r = mid - 1;
}
return -1;
}
写法2:左闭右开 [l, r)
cpp
int binarySearch(int a[], int n, int x) {
int l = 0, r = n;
while (l < r) { // 注意:l < r
int mid = l + (r - l) / 2;
if (a[mid] == x) return mid;
else if (a[mid] < x) l = mid + 1;
else r = mid;
}
return -1;
}
3.3 常见变形
查找第一个 ≥ x 的位置(lower_bound):
cpp
int lowerBound(int a[], int n, int x) {
int l = 0, r = n;
while (l < r) {
int mid = l + (r - l) / 2;
if (a[mid] >= x) r = mid;
else l = mid + 1;
}
return l; // 第一个 >= x 的位置
}
查找第一个 > x 的位置(upper_bound):
cpp
int upperBound(int a[], int n, int x) {
int l = 0, r = n;
while (l < r) {
int mid = l + (r - l) / 2;
if (a[mid] > x) r = mid;
else l = mid + 1;
}
return l; // 第一个 > x 的位置
}
3.4 特点分析
| 特性 | 说明 |
|---|---|
| 时间复杂度(最好) | O(1) ------ 第一次就找到 |
| 时间复杂度(平均/最坏) | O(log₂n) |
| 空间复杂度(迭代) | O(1) |
| 空间复杂度(递归) | O(log n)(递归栈) |
| 前提条件 | 数组必须有序(升序或降序) |
| 最坏比较次数 | ⌊log₂n⌋ + 1 |
3.5 查找次数计算
公式: 在n个元素的有序数组中二分查找,最多需要比较 ⌊log₂n⌋ + 1 次。
| n | ⌊log₂n⌋ + 1 | 举例 |
|---|---|---|
| 1 | 1 | 1个元素 |
| 2-3 | 2 | 3个元素最多2次 |
| 4-7 | 3 | 7个元素最多3次 |
| 8-15 | 4 | 15个元素最多4次 |
| 16-31 | 5 | 31个元素最多5次 |
记忆口诀: "2的k次方减1个元素,最多k次"
3.6 典型例题
例题1(基础查找过程)
题目: 在有序数组
[1, 3, 5, 7, 9, 11, 13]中查找 9,二分查找过程中 mid 的取值依次是( )A. 4, 6, 5 B. 3, 5, 4 C. 4, 5, 6 D. 3, 4, 5
答案:B
解析:
- 初始 l=0, r=6, mid=3(a3=7)
- 9 > 7,去右半:l=4, r=6, mid=5(a5=11)
- 9 < 11,去左半:l=4, r=4, mid=4(a4=9,找到)
mid取值:3, 5, 4
选B。
例题2(查找次数)
题目: 在 100 个元素的有序数组中,二分查找最多需要比较( )次。
A. 6 B. 7 C. 8 D. 9
答案:B
解析: 最多比较次数 = ⌊log₂100⌋ + 1
- 2⁶ = 64,2⁷ = 128
- ⌊log₂100⌋ = 6
- 最多 = 6 + 1 = 7 次
选B。
例题3(lower_bound 变形)
题目: 在有序数组
[1, 3, 5, 7, 7, 7, 9]中,用 lower_bound 找第一个 ≥ 7 的位置是( )A. 2 B. 3 C. 4 D. 5
答案:B
解析: lower_bound 返回第一个 ≥ x 的位置。
数组索引:0:1, 1:3, 2:5, 3:7, 4:7, 5:7, 6:9
第一个 ≥ 7 的位置是索引 3。
选B。
例题4(递归二分)
题目: 以下递归二分查找的时间复杂度是( )
cpp
int bs(int a[], int l, int r, int x) { if (l > r) return -1; int mid = (l + r) / 2; if (a[mid] == x) return mid; if (a[mid] > x) return bs(a, l, mid-1, x); return bs(a, mid+1, r, x); }A. O(n) B. O(log n) C. O(n log n) D. O(n²)
答案:B
解析: 每次将搜索范围缩小一半,递归深度 O(log n),每次 O(1),总 O(log n)。
四、插值查找(了解即可)
4.1 基本思想
二分查找总是取中间位置,而插值查找根据目标值在有序数组中的位置比例来估算mid。
公式:
text
mid = l + (r - l) * (x - a[l]) / (a[r] - a[l])
4.2 特点
| 特性 | 说明 |
|---|---|
| 适用场景 | 数据有序且均匀分布(如 1,2,3,4,5,6...) |
| 最好时间复杂度 | O(log log n) |
| 最坏时间复杂度 | O(n) |
| 与二分对比 | 分布均匀时比二分快,不均匀时可能退化 |
CSP-J要求: 只要知道插值查找存在,知道它是二分查找的改进版即可,不要求手写代码。
五、哈希查找(Hash)
5.1 基本思想
通过哈希函数将关键字映射到表中的位置,直接访问。
5.2 核心概念
| 概念 | 说明 |
|---|---|
| 哈希函数 | 将关键字映射为数组索引 |
| 冲突 | 两个不同关键字映射到同一位置 |
| 负载因子 | 元素个数 / 表长度 |
| 拉链法 | 同一位置的元素用链表存储 |
| 开放地址法 | 冲突时找下一个空位置 |
5.3 特点
| 特性 | 说明 |
|---|---|
| 平均时间复杂度 | O(1) |
| 最坏时间复杂度 | O(n)(大量冲突) |
| 空间复杂度 | O(n) |
| 适用条件 | 需要设计哈希函数和解决冲突 |
5.4 典型例题(哈希查找)
题目: 哈希表中,装填因子(负载因子)越大,下列说法正确的是( )
A. 查找效率越高 B. 冲突概率越大
C. 空间利用率越低 D. 冲突概率越小
答案:B
解析: 装填因子 = 元素个数 / 表长度。装填因子越大,说明表越"满",冲突概率越大,查找效率越低。
选B。
六、查找算法对比表
| 对比项 | 顺序查找 | 二分查找 | 哈希查找 |
|---|---|---|---|
| 时间复杂度(平均) | O(n) | O(log n) | O(1) |
| 时间复杂度(最坏) | O(n) | O(log n) | O(n) |
| 空间复杂度 | O(1) | O(1) | O(n) |
| 数据要求 | 无 | 有序 | 需哈希函数 |
| 优缺点 | 简单但慢 | 快但要求有序 | 极快但需额外空间 |
七、程序阅读题中的查找算法识别
特征识别表
| 代码特征 | 算法 |
|---|---|
for(i=0; i<n; i++) if(a[i]==x) |
顺序查找 |
while(l<=r){ mid=(l+r)/2; ... } |
二分查找 |
mid = l + (r-l)*(x-a[l])/(a[r]-a[l]) |
插值查找 |
hash[key] = value |
哈希查找 |
八、二分查找的易错点与陷阱
陷阱1:边界条件错误
cpp
while (l < r) { ... } // 适用左闭右开 [l, r)
while (l <= r) { ... } // 适用闭区间 [l, r]
陷阱2:mid 计算溢出
cpp
int mid = (l + r) / 2; // l+r 可能超过 int 范围
int mid = l + (r - l) / 2; // ✅ 安全写法
陷阱3:死循环
cpp
while (l < r) {
mid = (l + r) / 2;
if (a[mid] < x) l = mid; // ❌ 可能死循环
else r = mid;
}
// 正确写法:l = mid + 1
陷阱4:数组未排序
二分查找的大前提是数组有序。如果数组未排序,二分查找结果错误。
九、查找算法知识思维导图
查找算法
↓
┌──────────────┼──────────────┐
↓ ↓ ↓
顺序查找 二分查找 哈希查找
无要求 有序数组 哈希函数
O(n) O(log n) O(1)平均
↓ ↓ ↓
遍历比较 lower_bound 拉链法/开放地址
upper_bound 冲突处理
↓
递归/迭代两种实现
十、实战检测(8题自测)
| 题号 | 题目 | 你的答案 |
|---|---|---|
| 1 | 在数组 [2,5,8,12,16,23,38,56,72,91] 中二分查找 23,mid 的取值依次是? |
|
| 2 | 1024 个元素的有序数组,二分查找最多需要比较几次? | |
| 3 | 顺序查找的时间复杂度是? | |
| 4 | 哈希查找的平均时间复杂度是? | |
| 5 | lower_bound 返回的是? | |
| 6 | 二分查找的前提条件是? | |
| 7 | int mid = (l + r) / 2 的潜在风险是? |
|
| 8 | 插值查找适合什么类型的数据? |
- 4, 6, 5(a4=16→右,a6=38→左,a5=23找到)
- 11次(2¹⁰=1024,⌊log₂1024⌋+1=10+1=11)
- O(n)
- O(1)
- 第一个 ≥ x 的位置
- 数组必须有序
- l+r 可能超出 int 范围(溢出)
- 有序且均匀分布的数据
最后建议: 查找算法中,顺序查找 和二分查找是必考内容,必须熟练掌握。哈希查找了解基本概念即可。重点关注:
- 二分查找的边界条件(l <= r 还是 l < r)
- mid 的计算方式(防溢出写法)
- 查找次数的计算(⌊log₂n⌋ + 1)
- lower_bound / upper_bound 的含义(程序阅读题常考)