Skip to content
00:00:00
0

查找算法

一、考纲要求

  1. 掌握各种查找算法的基本思想和实现方法
  2. 理解不同查找算法的时间复杂度和空间复杂度
  3. 能够根据实际场景选择合适的查找算法
  4. 掌握哈希表的构建和冲突处理方法
  5. 了解二叉排序树的性质及其查找过程

二、查找算法概述

查找是根据给定的关键字K,在查找表中确定一个其关键字等于K的数据元素(或记录)。查找算法主要分为两类:

  • 静态查找:查找表结构固定不变,只进行查找操作
  • 动态查找:在查找过程中可以插入和删除记录

常见的查找算法包括:

  1. 顺序查找(线性查找)
  2. 二分查找(折半查找)
  3. 分块查找(索引查找)
  4. 哈希查找(散列查找)
  5. 二叉排序树查找

三、顺序查找

3.1 基本思想

从表的一端开始,顺序扫描查找表,将给定值k与表中每个元素的关键字进行比较,若找到关键字等于k的元素,则查找成功;否则,扫描完整个表仍未找到关键字等于k的元素,则查找失败。

3.2 算法实现

c
int SequentialSearch(int S[], int n, int key) {
    for (int i = 0; i < n; i++) {
        if (S[i] == key) {
            return i;  // 查找成功,返回下标
        }
    }
    return -1;  // 查找失败
}

3.3 性能分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
  • 最好情况:O(1)(第一个元素就是要查找的元素)
  • 最坏情况:O(n)(要查找的元素不在表中或在表的最后一个位置)
  • 平均情况:O(n)

3.4 适用场景

  • 查找表无序时
  • 元素个数较少时
  • 作为其他查找算法的补充

四、二分查找

4.1 基本思想

对有序表进行二分查找,开始时将区间定位在整个表中。若待查关键字与中间位置元素的关键字相等,则查找成功;若待查关键字小于中间位置元素的关键字,则在前半区继续查找;否则在后半区继续查找。每次比较使查找范围缩小一半。

4.2 算法实现

c
int BinarySearch(int S[], int n, int key) {
    int low = 0, high = n - 1;
    while (low <= high) {
        int mid = (low + high) / 2;
        if (S[mid] == key) {
            return mid;  // 查找成功
        } else if (S[mid] < key) {
            low = mid + 1;  // 在后半区查找
        } else {
            high = mid - 1;  // 在前半区查找
        }
    }
    return -1;  // 查找失败
}

4.3 性能分析

  • 时间复杂度:O(log₂n)
  • 空间复杂度:O(1)
  • 最好情况:O(1)(中间元素就是要查找的元素)
  • 最坏情况:O(log₂n)(要查找的元素在表的一端或不在表中)
  • 平均情况:O(log₂n)

4.4 适用场景

  • 查找表有序时
  • 元素个数较多时
  • 需要高效查找时

4.5 注意事项

  • 必须建立在有序表之上
  • 对于频繁插入删除的表,维护有序性的开销较大
  • 可以递归实现,但迭代实现更节省空间

五、分块查找

5.1 基本思想

分块查找是顺序查找和二分查找的折中方案。将查找表分为若干块,每块内部无序但块之间有序(即前一块的最大元素小于后一块的最小元素)。查找过程分两步:

  1. 先对块进行查找(可以用顺序查找或二分查找),确定关键字所在的块
  2. 在确定的块内部进行顺序查找

5.2 表结构

设查找表有n个元素,分为b块,则每块的元素个数为s = n/b(假设均匀分布)。 每块需要一个索引项,包含该块的最大关键字和该块在表中的起始位置。

5.3 算法实现

c
typedef struct {
    int maxKey;      // 块内最大关键字
    int startIndex;  // 块在表中的起始位置
} IndexItem;

int BlockSearch(int S[], int n, int key, IndexItem index[], int b) {
    // 第一步:在索引表中查找关键字所在的块
    int low = 0, high = b - 1;
    while (low <= high) {
        int mid = (low + high) / 2;
        if (index[mid].maxKey == key) {
            // 在索引表中找到精确匹配
            return index[mid].startIndex;  // 返回块的起始位置
        } else if (index[mid].maxKey < key) {
            low = mid + 1;  // 在后半区查找
        } else {
            high = mid - 1;  // 在前半区查找
        }
    }
    // 循环结束后,low是第一个maxKey大于key的块的索引
    // 因此关键字应该在low-1块中(如果low>0的话)
    int blockIndex = (low > 0) ? low - 1 : 0;
    if (blockIndex >= b) blockIndex = b - 1;
    
    // 第二步:在确定的块内进行顺序查找
    int start = index[blockIndex].startIndex;
    int end = (blockIndex == b - 1) ? n - 1 : index[blockIndex + 1].startIndex - 1;
    
    for (int i = start; i <= end; i++) {
        if (S[i] == key) {
            return i;  // 查找成功
        }
    }
    return -1;  // 查找失败
}

5.4 性能分析

设表长为n,块数为b,则每块长度为s=n/b

  • 第一步:在索引表(长度为b)上查找:O(log₂b) 或 O(b)(取决于使用二分还是顺序)
  • 第二步:在块内部顺序查找:O(s) = O(n/b)
  • 总时间复杂度:O(log₂b + n/b) 或 O(b + n/b)
  • 当b ≈ √n时,时间复杂度达到最小:O(√n)
  • 空间复杂度:O(b)(存储索引表)

5.5 适用场景

  • 元素个数中等时(几千到几万)
  • 插入删除操作不频繁时
  • 作为顺序查找和二分查找之间的折中方案

六、哈希查找

6.1 基本思想

哈希查找(散列查找)是根据关键字直接计算出该元素在表中的存储位置,从而实现快速查找。其核心思想是通过哈希函数H(key)将关键字映射到哈希表的一个地址上。

6.2 哈希函数的构造方法

  1. 直接定址法:H(key) = a·key + b(a、b为常数)
  2. 数字分析法:取关键字的某几位作为哈希地址
  3. 平方取中法:取关键字平方后的中间几位作为哈希地址
  4. 折叠法:将关键字分割成几部分,然后相加取后几位作为哈希地址
  5. 除留余数法:H(key) = key mod p(p为不大于哈希表长度的素数)
  6. 随机数法:H(key) = random(key)(random为随机函数)

6.3 处理冲突的方法

当两个不同的关键字通过哈希函数映射到同一地址时,称为发生冲突(Collision)。

6.3.1 开放定址法(Open Addressing)

当发生冲突时,以探测序列的形式在哈希表中寻找下一个空闲地址。

  • 线性探测:H_i = (H(key) + i) mod m
  • 二次探测:H_i = (H(key) + i²) mod m
  • 伪随机探测:H_i = (H(key) + d_i) mod m(d_i为伪随机数序列)

6.3.2 链地址法(Chaining)

将哈希值相同的元素链接在同一个单链表中。哈希表的每个单元存放一个指针,指向链表的头结点。

6.3.3 再哈希法

同时构造多个不同的哈希函数:H_i = RH_i(key)(i=1,2,...,k),当发生冲突时,再计算另一个哈希函数地址,直到不发生冲突为止。

6.3.4 建立公共溢出区

将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。

6.4 算法实现(以链地址法为例)

c
#define TABLE_SIZE 100

typedef struct Node {
    int key;
    int value;
    struct Node* next;
} Node;

Node* hashTable[TABLE_SIZE];  // 哈希表

// 哈希函数(除留余数法)
int hash(int key) {
    return key % TABLE_SIZE;
}

// 插入元素
void insert(int key, int value) {
    int index = hash(key);
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->key = key;
    newNode->value = value;
    newNode->next = hashTable[index];
    hashTable[index] = newNode;
}

// 查找元素
int search(int key) {
    int index = hash(key);
    Node* current = hashTable[index];
    while (current != NULL) {
        if (current->key == key) {
            return current->value;  // 查找成功
        }
        current = current->next;
    }
    return -1;  // 查找失败
}

6.5 性能分析

  • 时间复杂度:O(1)(平均情况),最坏情况为O(n)(所有元素都映射到同一位置)
  • 空间复杂度:O(n + m)(n为元素个数,m为哈希表长度)
  • 装填因子α = n/m(n为元素个数,m为哈希表长度):
    • 开放定址法:查找成功平均长度为(1 + 1/(1-α))/2,查找失败平均长度为(1 + 1/(1-α)²)/2
    • 链地址法:查找成功平均长度为1 + α/2,查找失败平均长度为α

6.6 适用场景

  • 查找、插入、删除操作都非常频繁时
  • 对查找速度要求很高时
  • 元素分布较均匀时
  • 内存允许开辟较大空间时

七、二叉排序树查找

7.1 基本思想

二叉排序树(Binary Search Tree, BST)是一棵空树或具有以下性质的二叉树:

  1. 若左子树不空,则左子树上所有结点的值均小于它的根结点的值
  2. 若右子树不空,则右子树上所有结点的值均大于它的根结点的值
  3. 左、右子树也分别为二叉排序树
  4. 没有键值相等的节点(即没有相同的关键字)

查找过程:从根节点开始,比较根节点关键字与给定值:

  • 若相等,则查找成功
  • 若给定值小于根节点关键字,则在左子树中继续查找
  • 若给定值大于根节点关键字,则在右子树中继续查找
  • 若到达空子树仍未找到,则查找失败

7.2 算法实现

c
typedef struct BSTNode {
    int key;
    int value;
    struct BSTNode* left;
    struct BSTNode* right;
} BSTNode;

// 查找元素
BSTNode* BSTSearch(BSTNode* root, int key) {
    if (root == NULL || root->key == key) {
        return root;  // 查找成功(找到节点)或查找失败(空树)
    }
    if (key < root->key) {
        return BSTSearch(root->left, key);  // 在左子树中查找
    } else {
        return BSTSearch(root->right, key);  // 在右子树中查找
    }
}

// 非递归实现
BSTNode* BSTSearch_NoRec(BSTNode* root, int key) {
    while (root != NULL && root->key != key) {
        if (key < root->key) {
            root = root->left;
        } else {
            root = root->right;
        }
    }
    return root;  // 返回找到的节点或NULL
}

7.3 性能分析

  • 时间复杂度:
    • 最好情况:O(log₂n)(树平衡时)
    • 最坏情况:O(n)(树退化为链表时,如有序序列构建BST)
    • 平均情况:O(log₂n)(假设所有可能的二叉排序树出现的概率相等)
  • 空间复杂度:O(h)(h为树的高度,递归时为栈空间)

7.4 构建二叉排序树

c
BSTNode* insertBST(BSTNode* root, int key, int value) {
    if (root == NULL) {
        // 创建新节点
        BSTNode* newNode = (BSTNode*)malloc(sizeof(BSTNode));
        newNode->key = key;
        newNode->value = value;
        newNode->left = newNode->right = NULL;
        return newNode;
    }
    if (key < root->key) {
        root->left = insertBST(root->left, key, value);
    } else if (key > root->key) {
        root->right = insertBST(root->right, key, value);
    }
    // 如果key已存在,则不插入(或者可以选择更新值)
    return root;
}

7.5 适用场景

  • 需要频繁插入、删除和查找操作时
  • 数据呈随机分布时(能够保持较好的平衡性)
  • 作为动态查找表的实现方式
  • 但需要注意:若输入数据是有序的,则会导致树严重失衡,此时应考虑使用平衡二叉树(如AVL树、红黑树)

八、各查找算法性能比较

查找算法时间复杂度(平均)时间复杂度(最坏)空间复杂度前置条件适用场景
顺序查找O(n)O(n)O(1)小规模、无序表
二分查找O(log₂n)O(log₂n)O(1)有序表大规模有序静态表
分块查找O(√n)O(√n)O(√n)块内无序、块间有序中等规模、查找为主
哈希查找O(1)O(n)O(n+m)查找为主、要求O(1)查找
二叉排序树查找O(log₂n)O(n)O(n)动态查找表、插入删除频繁

九、高频考点分析

9.1 必考题型

  1. 算法时间复杂度分析:经常考察各查找算法在不同情况下的时间复杂度
  2. 算法实现:要求用C语言实现某个查找算法
  3. 算法对比:比较不同查找算法的优缺点和适用场景
  4. 哈希函数构造:考察常用哈希函数的设计方法
  5. 冲突处理方法:考察开放定址法、链地址法等冲突处理技术
  6. 二叉排序树性质:考察BST的查找、插入、删除过程及其时间复杂度

9.2 常见失分点

  1. 忘记二分查找必须在有序表上进行
  2. 混淆顺序查找和二分查找的时间复杂度
  3. 哈希函数设计不当导致冲突过多
  4. 开放定址法中的聚集问题( primary clustering 和 secondary clustering )
  5. 链地址法中链表过长导致性能下降
  6. 二叉排序树在极端情况下退化为链表的情况
  7. 分块查找中块大小的确定方法

9.3 解题技巧

  1. 先明确查找表的性质(有序/无序、静态/动态)
  2. 根据数据规模选择合适的算法
  3. 注意算法的边界条件(空表、单元素表等)
  4. 哈希查找要考虑装填因子和冲突处理方法
  5. 二叉排序树要注意平衡性问题
  6. 分块查找要记住“块内无序、块间有序”的特点
  7. 时间复杂度分析要考虑最好、最坏和平均情况

十、典型例题

例题1:二分查找变种

已知一个有序数组 A[0..n-1],求第一个大于等于给定值 key 的元素位置。

解题思路: 这是二分查找的变种,修改判断条件即可。

c
int lowerBound(int A[], int n, int key) {
    int low = 0, high = n - 1;
    int result = n;  // 默认返回n(表示所有元素都小于key)
    
    while (low <= high) {
        int mid = (low + high) / 2;
        if (A[mid] >= key) {
            result = mid;  // 可能的答案,继续向左找更小的索引
            high = mid - 1;
        } else {
            low = mid + 1;
        }
    }
    return result;
}

例题2:哈希冲突计算

哈希表长度为11,使用除留余数法 H(key) = key mod 11,已插入关键字:{15, 26, 27, 30, 28, 39},问发生冲突的次数。

解题步骤

  1. 计算每个关键字的哈希地址:
    • 15 mod 11 = 4
    • 26 mod 11 = 4(与15冲突,第1次冲突)
    • 27 mod 11 = 5
    • 30 mod 11 = 8
    • 28 mod 11 = 6
    • 39 mod 11 = 6(与28冲突,第2次冲突)
  2. 总冲突次数:2次

例题3:二叉排序树查找路径

已知关键字序列 {45, 24, 53, 12, 37, 48, 75} 按此顺序插入到空的二叉排序树中,查找关键字 37 的查找路径。

解题步骤

  1. 构建二叉排序树:
    • 插入45:根节点
    • 插入24:45的左子节点
    • 插入53:45的右子节点
    • 插入12:24的左子节点
    • 插入37:24的右子节点(因为12<37<24?不对,应该是:24<37<45,所以是24的右子节点)
    • 插入48:53的左子节点
    • 插入75:53的右子节点
  2. 查找路径:45 → 24 → 37
  3. 路径长度:3(经过3个节点)

十一、总结与建议

11.1 算法选择原则

  1. 小规模无序表(n<50):顺序查找简单直接
  2. 大规模有序静态表:二分查找效率最高
  3. 中等规模查找为主表:分块查找折中选择
  4. 查找、插入、删除都频繁的动态表
    • 如果要求O(1)查找:哈希表
    • 如果需要有序遍历:平衡二叉树(AVL/红黑树)
  5. 内存受限且查找不频繁:顺序查找或分块查找

11.2 学习建议

  1. 重点掌握:二分查找、哈希查找(尤其是哈希函数设计和冲突处理)、二叉排序树的基本操作
  2. 重点理解:各算法的时间空间复杂度分析方法
  3. 重点練習:用代码实现各种查找算法,尤其是带有边界条件的变种问题
  4. 重点對比:熟悉各算法的优缺点和适用场景,能够根据具体问题选择合适的算法
  5. 重點擴展:了解平衡二叉树(AVL树、红黑树)、B树、B+树等高级查找结构

11.3 考试提醒

  1. 查找算法在软考软件设计师中考查比例适中,通常会有1-2道选择题和1道综合题
  2. 选择题考点:时间复杂度、空间复杂度、适用场景、算法特点
  3. 综合题考点:算法实现、性能分析、哈希函数设计、冲突处理、二叉排序树操作
  4. 注意题目可能会给出具体数据要求计算查找次数或比较算法性能
  5. 哈希查找和二叉排序树是考查重点,要掌握细节

:本博客内容参考《软件设计师教程(第5版)》及相关考试大纲编写,适用于软考软件设计师查找算法章节的学习和复习。 生成时间:2026-03-14 16:30

最近更新