考研408「查找」知识点全解析
一、顺序查找
1.1 定义与基本思想
顺序查找(Sequential Search)是一种线性查找算法,适用于无序或有序的线性表。其核心思想是从表的一端开始逐个比较元素,直到找到目标值或遍历完整个表。
时间复杂度 :
• 最好情况: O ( 1 ) O(1) O(1)(目标在第一个位置)
• 最坏情况: O ( n ) O(n) O(n)(目标在最后一个位置或不存在)
• 平均情况: O ( n ) O(n) O(n)
适用场景:小规模数据或无序数据。
1.2 算法实现与易错点
代码示例(C语言):
c
int sequentialSearch(int arr[], int n, int key) {
for (int i = 0; i < n; i++) {
if (arr[i] == key) {
return i; // 返回索引
}
}
return -1; // 未找到
}
易错点:
- 边界条件处理 :循环条件是否包含
i < n
?若数组从1开始存储,需调整索引范围。 - 哨兵优化 :可在数组末尾设置哨兵(
arr[n] = key
),减少比较次数。
平均查找长度(ASL) :
• 查找成功: 1 n ∑ i = 1 n i = n + 1 2 \frac{1}{n}\sum_{i=1}^{n}i = \frac{n+1}{2} n1∑i=1ni=2n+1
• 查找失败: 1 n ∑ i = 1 n i = n + 1 2 \frac{1}{n}\sum_{i=1}^{n}i = \frac{n+1}{2} n1∑i=1ni=2n+1。
1.3 真题与模拟题
2023年408真题:
对长度为n的顺序表进行查找,若采用顺序查找法,在等概率情况下查找成功的平均查找长度为( )。
A. n n n
B. ( n + 1 ) / 2 (n+1)/2 (n+1)/2
C. n / 2 n/2 n/2
D. log 2 n \log_2 n log2n
答案 :B
解析 :平均查找长度公式为 1 n ∑ i = 1 n i = n + 1 2 \frac{1}{n}\sum_{i=1}^{n}i = \frac{n+1}{2} n1∑i=1ni=2n+1。
模拟题1 :
题目 :设计一个顺序查找算法,在无序数组[3, 1, 4, 1, 5](@ref)
中查找元素4
,并说明哨兵优化如何减少比较次数。
解答:
c
int sequentialSearchWithSentinel(int arr[], int n, int key) {
arr[0] = key; // 设置哨兵
int i = n;
while (arr[i] != key) {
i--;
}
return i == n ? -1 : i; // 排除哨兵
}
优化效果 :减少一次边界判断,时间复杂度仍为 O ( n ) O(n) O(n)。
二、二分查找
2.1 定义与适用条件
二分查找(Binary Search)要求有序顺序表,通过不断缩小搜索范围来定位目标值。其核心步骤为:
- 确定中间位置
mid
。 - 比较
arr[mid]
与目标值。 - 调整搜索区间为左半部分或右半部分。
时间复杂度 : O ( log n ) O(\log n) O(logn)。
2.2 算法实现与难点
代码示例(非递归):
c
int binarySearch(int arr[], int low, int high, int key) {
while (low <= high) {
int mid = low + (high - low) / 2; // 防止溢出
if (arr[mid] == key) return mid;
else if (arr[mid] < key) low = mid + 1;
else high = mid - 1;
}
return -1;
}
难点与易错点:
- 循环终止条件 :必须为
low <= high
,否则可能漏查边界元素。 - 中间值计算 :避免整数溢出,应使用
mid = low + (high - low) / 2
而非(low + high)/2
。 - 判定树性质 :二分查找生成的判定树是一棵平衡二叉树,查找成功时的ASL为 log 2 ( n ) − 1 \log_2(n) - 1 log2(n)−1。
2.3 真题与模拟题
2021年408真题:
对长度为12的有序表进行二分查找,查找失败时最多需要比较( )次。
A. 3
B. 4
C. 5
D. 6
答案 :B
解析 :二分查找的最大比较次数为 ⌈ log 2 ( n + 1 ) ⌉ = 4 \lceil \log_2(n+1) \rceil = 4 ⌈log2(n+1)⌉=4。
模拟题2 :
题目 :有序数组[1, 3, 5, 7, 9](@ref)
中查找元素6
,画出二分查找的判定树并计算查找长度。
解答:
判定树层级:
1. 根节点(mid=3)→ 比较6>5 → 右子树
2. mid=7 → 比较6<7 → 左子树
3. mid=5 → 比较6>5 → 右子树(无元素)
查找长度:3次
ASL : ( 1 × 1 + 2 × 2 + 3 × 2 ) / 5 = 1.8 (1 \times 1 + 2 \times 2 + 3 \times 2)/5 = 1.8 (1×1+2×2+3×2)/5=1.8。
三、分块查找
3.1 定义与核心思想
分块查找(索引顺序查找)结合了顺序查找和二分查找的优点,将数据分为若干块,每块内用顺序查找,块间用二分查找。
时间复杂度 :
• 查找成功: O ( n ) O(\sqrt{n}) O(n )(假设块数与块内元素数平衡)
• 查找失败: O ( n ) O(\sqrt{n}) O(n )
3.2 算法实现与难点
代码示例:
c
// 块间二分查找
int blockSearch(int blocks[], int blockSize, int key) {
int low = 0, high = blockSize - 1;
while (low <= high) {
int mid = low + (high - low) / 2;
if (blocks[mid].max < key) low = mid + 1;
else high = mid - 1;
}
// 块内顺序查找
int start = (low - 1) * blockSize;
for (int i = start; i < start + blockSize; i++) {
if (arr[i] == key) return i;
}
return -1;
}
难点:
- 块划分策略:块内无序时需用顺序查找,块间有序时可用二分查找。
- 索引表维护:插入/删除操作需调整索引表,可能影响效率。
3.3 真题与模拟题
2024年408真题:
设初始有序序列为
[1, 3, 5, 7, 9, 12, 15, 18, 22, 25](@ref)
,分块大小为3,查找元素18
的步骤为( )。A. 块间查找→块内查找
B. 块内查找→块间查找
C. 直接顺序查找
D. 直接二分查找
答案 :A
解析 :块间二分查找确定块索引为5(18 < 22
),块内顺序查找找到元素。
模拟题3 :
题目 :设计分块查找算法,在块大小为4的索引表中查找元素15
,并计算平均查找长度。
解答:
索引表:[5, 9, 15, 25](@ref)(块最大值)
数据块:
1-4:
5-8:
9-12:
查找步骤:
1. 块间二分查找确定块索引2(15 < 25)
2. 块内顺序查找找到索引3
ASL = (1+2+3)/4 = 1.5
四、哈希查找
4.1 定义与核心概念
哈希表(Hash Table)通过哈希函数将关键字映射到存储位置,实现 O ( 1 ) O(1) O(1)时间复杂度的查找。关键问题包括:
• 哈希函数设计 :除留余数法、平方取中法等。
• 冲突处理:开放定址法(线性探测、二次探测)、链地址法。
装填因子公式 : α = 表中记录数 哈希表长度 \alpha = \frac{表中记录数}{哈希表长度} α=哈希表长度表中记录数,一般要求 α < 0.75 \alpha < 0.75 α<0.75。
4.2 算法实现与难点
链地址法示例:
c
typedef struct HashNode {
int key;
struct HashNode* next;
} HashNode;
HashNode* hashTable[TABLE_SIZE] = {NULL};
void insert(int key) {
int index = key % TABLE_SIZE;
HashNode* newNode = (HashNode*)malloc(sizeof(HashNode));
newNode->key = key;
newNode->next = hashTable[index];
hashTable[index] = newNode;
}
int search(int key) {
int index = key % TABLE_SIZE;
HashNode* current = hashTable[index];
while (current != NULL) {
if (current->key == key) return index;
current = current->next;
}
return -1;
}
易错点:
- 探测序列选择 :二次探测需保证步长为 1 2 , − 1 2 , 2 2 , − 2 2 , . . . 1^2, -1^2, 2^2, -2^2,... 12,−12,22,−22,...,否则可能无法遍历全表。
- 删除操作:开放定址法中删除记录需标记为"已删除",否则破坏查找链。
4.3 真题与模拟题
2018年408算法题:
设计算法找出数组中未出现的最小正整数。
参考解法:利用哈希表记录已出现的正整数,从1开始遍历查找。
模拟题4 :
题目 :哈希表长为7,哈希函数 H ( k e y ) = k e y % 7 H(key) = key \% 7 H(key)=key%7,采用线性探测法处理冲突。依次插入关键字序列{8, 15, 22, 30}
,画出最终哈希表结构,并计算查找成功的平均查找长度(ASL)。
解答:
地址 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
关键字 | 15 | 22 | 30 | 8 | |||
ASL = 1 + 2 + 3 + 1 4 = 1.75 \frac{1+2+3+1}{4} = 1.75 41+2+3+1=1.75。 |
五、B树与B+树
5.1 定义与适用场景
B树和B+树是多路平衡查找树,适用于数据库和文件系统索引,支持高效的范围查询和动态数据插入。
B树性质 :
• 每个节点最多有 m m m个子节点(阶为 m m m)
• 所有叶子节点在同一层。
5.2 算法实现与难点
B树插入操作:
- 从根节点开始查找合适的叶子节点
- 若叶子节点未满,直接插入
- 若叶子节点已满,分裂节点并调整父节点。
难点:
- 节点分裂策略:需保证树的高度平衡,避免性能退化为链表。
- 范围查询优化:B+树通过叶子节点链表加速范围遍历。
5.3 真题与模拟题
2022年408真题:
B树中每个节点最多有5个子节点,根节点至少有( )个子节点。
A. 1
B. 2
C. 3
D. 5
答案 :B
解析 :B树根节点至少有2个子节点(阶 m ≥ 2 m \geq 2 m≥2)。
模拟题5 :
题目 :设计B+树插入算法,在3阶B树中插入序列{10, 20, 5, 6, 15, 30}
,并画出最终结构。
解答:
根节点分裂为和
左子树插入15,右子树插入30
ASL计算略(需遍历路径)
六、哈希表冲突处理(续)
6.1 链地址法(Chaining)
链地址法通过将哈希表的每个槽位指向一个链表,将冲突的元素存储在链表中。具体实现如下:
核心思想 :
每个哈希表槽位(bucket)维护一个链表,当发生冲突时,将新元素插入对应槽位的链表尾部。
代码示例:
c
typedef struct HashNode {
int key;
struct HashNode* next;
} HashNode;
HashNode* hashTable[TABLE_SIZE] = {NULL};
void insert(int key) {
int index = key % TABLE_SIZE;
HashNode* newNode = (HashNode*)malloc(sizeof(HashNode));
newNode->key = key;
newNode->next = hashTable[index];
hashTable[index] = newNode;
}
int search(int key) {
int index = key % TABLE_SIZE;
HashNode* current = hashTable[index];
while (current != NULL) {
if (current->key == key) return index;
current = current->next;
}
return -1;
}
优点:
- 解决聚集问题:链表分散存储冲突元素,避免线性探测的聚集现象。
- 动态扩容:链表长度可变,无需预先确定哈希表大小。
缺点:
- 额外空间开销:需存储链表指针,空间复杂度较高。
- 查找效率不稳定 :链表过长时,查找时间可能退化为 O ( n ) O(n) O(n)。
6.2 再哈希法(Rehashing)
再哈希法通过使用多个哈希函数,逐步探测空闲位置。具体步骤如下:
核心思想 :
当发生冲突时,使用第二个哈希函数计算步长,重新探测位置。公式为:
h i ( k e y ) = ( h 1 ( k e y ) + i ⋅ h 2 ( k e y ) ) % m h_i(key) = (h_1(key) + i \cdot h_2(key)) \% m hi(key)=(h1(key)+i⋅h2(key))%m
其中, h 1 h_1 h1和 h 2 h_2 h2为两个不同的哈希函数, i i i为探测次数。
优点:
- 均匀分布:二次哈希函数可减少探测序列的聚集性。
- 灵活性:支持多种哈希函数组合,适应不同数据分布。
缺点:
- 计算复杂度高:需多次调用哈希函数,性能开销较大。
- 步长选择敏感 :若 h 2 h_2 h2与 m m m不互质,可能导致探测序列循环。
6.3 双重哈希法(Double Hashing)
双重哈希法是再哈希法的一种优化,使用两个独立哈希函数,步长由第二个哈希函数决定。公式为:
h i ( k e y ) = ( h 1 ( k e y ) + i ⋅ h 2 ( k e y ) ) % m h_i(key) = (h_1(key) + i \cdot h_2(key)) \% m hi(key)=(h1(key)+i⋅h2(key))%m
其中, h 2 ( k e y ) h_2(key) h2(key)必须满足与 m m m互质。
优点:
- 最优探测序列 :理论证明双重哈希法能保证最坏情况下 O ( 1 ) O(1) O(1)的探测次数。
- 避免聚集:探测序列跳跃性更强,减少元素堆积。
缺点:
- 哈希函数设计复杂 :需确保 h 2 h_2 h2与 m m m互质,实现难度较高。
- 扩容限制:哈希表扩容后需重新计算所有元素的哈希值。
6.4 扩容策略
当哈希表负载因子 α \alpha α超过阈值(通常为0.7)时,需扩容并重新哈希所有元素。具体步骤如下:
- 扩容:将哈希表大小扩展为原大小的2倍,并选择一个质数作为新表长。
- 重新哈希:使用原哈希函数计算新位置,或调整哈希函数以适应新表长。
示例 :
原表长 M = 11 M=11 M=11,扩容后 M = 23 M=23 M=23(质数),重新计算所有元素位置。
优点:
- 维持高效性能 :负载因子降低后,查找、插入、删除操作均摊时间复杂度接近 O ( 1 ) O(1) O(1)。
- 减少冲突:更大的表空间降低哈希冲突概率。
缺点:
- 时间开销大 :扩容和重新哈希需 O ( n ) O(n) O(n)时间。
- 空间浪费:扩容后可能出现大量空闲槽位。
6.5 不同冲突处理方法的对比与选择
方法 | 时间复杂度(均摊) | 空间复杂度 | 聚集问题 | 实现复杂度 | 适用场景 |
---|---|---|---|---|---|
链地址法 | O ( 1 ) O(1) O(1) | O ( n ) O(n) O(n) | 否 | 中等 | 冲突频繁、需动态扩容 |
开放定址法 | O ( 1 ) O(1) O(1)(理想) | O ( n ) O(n) O(n) | 是 | 低 | 冲突较少、表较小 |
再哈希法 | O ( 1 ) O(1) O(1) | O ( n ) O(n) O(n) | 否 | 高 | 需灵活探测序列 |
双重哈希法 | O ( 1 ) O(1) O(1) | O ( n ) O(n) O(n) | 否 | 中等 | 需最优探测序列 |
选择建议:
- 链地址法:适用于冲突率高、需频繁插入/删除的场景。
- 开放定址法:适用于冲突率低、表较小的场景。
- 双重哈希法:适用于对探测序列均匀性要求高的场景。
6.6 真题与模拟题
2024年408真题:
哈希表采用链地址法处理冲突,若表长为13,哈希函数 H ( k e y ) = k e y % 13 H(key) = key \% 13 H(key)=key%13,依次插入关键字序列
{18, 14, 01, 68, 27, 55, 79}
,画出哈希表结构。
答案:
索引 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
|----|---|---|---|---|---|---|---|---|---|----|----|----|----|
| 关键字 | | | | 18 | 14 | 01 | 68 | 27 | 55 | 79 | | | |
模拟题6 :
题目 :设计双重哈希法,哈希表长为23,哈希函数 h 1 ( k e y ) = k e y % 23 h_1(key) = key \% 23 h1(key)=key%23, h 2 ( k e y ) = 5 − ( k e y % 5 ) h_2(key) = 5 - (key \% 5) h2(key)=5−(key%5),插入元素{10, 20, 30}
,计算探测序列。
解答:
插入10:
h1=10%23=10, h2=5-0=5 → 步长5
探测位置:10+5=15 → 空 → 插入
插入20:
h1=20%23=20, h2=5-0=5 → 步长5
探测位置:20+5=25→25%23=2 → 空 → 插入
插入30:
h1=30%23=7, h2=5-0=5 → 步长5
探测位置:7+5=12 → 空 → 插入