算法筑基(二):搜索算法------从线性查找到图搜索,精准定位数据
📖 前言
如果说排序是让数据变得有序,那么搜索就是从这些数据中快速找到你想要的目标。无论是数据库中的一条记录、网页中的关键词,还是地图上的最短路径,背后都离不开高效的搜索算法。
在本文中,我们将从最直观的线性搜索开始,逐步深入到二分搜索、哈希查找,最后介绍图搜索中的两大核心:深度优先搜索(DFS)和广度优先搜索(BFS)。每个算法都配有完整的C语言代码 、逐行注释 和实际案例 ,让你掌握搜索的本质。最后附上课后练习及答案,巩固所学知识。
📌 本文目录
- 线性搜索 ------ 最朴素的查找
- 二分搜索 ------ 有序数据的神器
- 哈希查找 ------ 用空间换时间
- 深度优先搜索(DFS) ------ 一条路走到黑
- 广度优先搜索(BFS) ------ 层层推进
- 搜索算法对比总结
- 课后练习与答案
1. 线性搜索 ------ 最朴素的查找
1.1 核心思想
从数据结构的第一个元素开始,逐个与目标值比较,直到找到目标或遍历完所有元素。
1.2 C语言实现
c
#include <stdio.h>
// 线性搜索函数,返回目标值的下标,未找到返回 -1
int linearSearch(int arr[], int n, int target) {
for (int i = 0; i < n; i++) {
if (arr[i] == target) {
return i; // 找到,返回下标
}
}
return -1; // 未找到
}
int main() {
int arr[] = {5, 2, 9, 1, 7, 4};
int n = sizeof(arr) / sizeof(arr[0]);
int target = 7;
int result = linearSearch(arr, n, target);
if (result != -1) {
printf("目标值 %d 在数组中的下标为 %d\n", target, result);
} else {
printf("未找到目标值 %d\n", target);
}
return 0;
}
1.3 案例:查找学生信息
假设有一个学号数组,需要快速判断某个学号是否存在。如果数据量小,线性搜索足够。
1.4 复杂度分析
- 最好情况:O(1),第一个就是目标。
- 最坏情况:O(n),遍历全部。
- 平均情况:O(n)。
- 空间复杂度:O(1)。
1.5 适用场景
- 数据无序。
- 数据量很小。
- 只需要查找一次,无需构建额外结构。
2. 二分搜索 ------ 有序数据的神器
2.1 核心思想
在有序序列中,每次取中间元素与目标比较,根据大小关系缩小一半查找范围。
2.2 C语言实现(迭代版本)
c
int binarySearch(int arr[], int n, int target) {
int left = 0, right = n - 1;
while (left <= right) {
int mid = left + (right - left) / 2; // 防止溢出
if (arr[mid] == target) {
return mid;
} else if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1;
}
2.3 递归版本
c
int binarySearchRecursive(int arr[], int left, int right, int target) {
if (left > right) return -1;
int mid = left + (right - left) / 2;
if (arr[mid] == target) return mid;
if (arr[mid] < target)
return binarySearchRecursive(arr, mid + 1, right, target);
else
return binarySearchRecursive(arr, left, mid - 1, target);
}
2.4 案例:字典查单词
英语字典按字母顺序排列,我们不可能一页页翻,而是每次翻到中间,根据单词大小决定往前还是往后,这就是二分搜索的直观体现。
2.5 复杂度分析
- 时间复杂度:O(log n),非常高效。
- 空间复杂度:迭代 O(1),递归 O(log n)(栈深度)。
2.6 注意
- 前提:数据必须有序。
- 二分搜索的变体很多,如查找第一个/最后一个等于目标的位置、查找插入点等。
3. 哈希查找 ------ 用空间换时间
3.1 核心思想
通过一个哈希函数将关键字映射到表中的位置,实现接近 O(1) 的查找。哈希表是搜索算法的终极武器之一。
3.2 简单的哈希表实现(拉链法)
由于 C 语言没有内置哈希表,我们实现一个简单的整数哈希表,用于演示思想。
c
#include <stdio.h>
#include <stdlib.h>
#define TABLE_SIZE 10
// 链表节点
typedef struct Node {
int key;
struct Node* next;
} Node;
// 哈希表结构
Node* hashTable[TABLE_SIZE];
// 哈希函数
int hash(int key) {
return key % TABLE_SIZE;
}
// 插入
void insert(int key) {
int index = hash(key);
Node* newNode = (Node*)malloc(sizeof(Node));
newNode->key = key;
newNode->next = hashTable[index];
hashTable[index] = newNode;
}
// 查找
int search(int key) {
int index = hash(key);
Node* cur = hashTable[index];
while (cur != NULL) {
if (cur->key == key) {
return 1; // 找到
}
cur = cur->next;
}
return 0; // 未找到
}
// 释放内存
void freeTable() {
for (int i = 0; i < TABLE_SIZE; i++) {
Node* cur = hashTable[i];
while (cur != NULL) {
Node* temp = cur;
cur = cur->next;
free(temp);
}
}
}
int main() {
// 初始化
for (int i = 0; i < TABLE_SIZE; i++) {
hashTable[i] = NULL;
}
// 插入一些数据
insert(5);
insert(15);
insert(25);
insert(7);
// 查找
printf("查找 15: %s\n", search(15) ? "找到" : "未找到");
printf("查找 8: %s\n", search(8) ? "找到" : "未找到");
freeTable();
return 0;
}
3.3 案例:电话号码簿
将姓名通过哈希函数映射到地址簿的某个位置,查找时直接计算位置,无需遍历。
3.4 复杂度分析
- 平均情况:O(1)
- 最坏情况:O(n)(哈希冲突严重时退化为链表)
3.5 关键点
- 哈希函数设计:尽量均匀分布。
- 冲突解决:拉链法、开放地址法等。
- 哈希表在工程中应用极广,如缓存、数据库索引。
4. 深度优先搜索(DFS) ------ 一条路走到黑
4.1 核心思想
从起点出发,沿着一条路径不断深入,直到无法继续,然后回溯到上一个节点,尝试另一条路径。常用递归或栈实现。
4.2 在图中应用(邻接表表示)
为了演示,我们构建一个简单的图,并实现 DFS 遍历。
c
#include <stdio.h>
#include <stdlib.h>
#define MAX_VERTICES 100
// 邻接表节点
typedef struct AdjNode {
int vertex;
struct AdjNode* next;
} AdjNode;
// 图结构
typedef struct Graph {
AdjNode* adjLists[MAX_VERTICES];
int visited[MAX_VERTICES];
int numVertices;
} Graph;
// 创建图
Graph* createGraph(int vertices) {
Graph* graph = (Graph*)malloc(sizeof(Graph));
graph->numVertices = vertices;
for (int i = 0; i < vertices; i++) {
graph->adjLists[i] = NULL;
graph->visited[i] = 0;
}
return graph;
}
// 添加边(无向图)
void addEdge(Graph* graph, int src, int dest) {
// 添加 src -> dest
AdjNode* newNode = (AdjNode*)malloc(sizeof(AdjNode));
newNode->vertex = dest;
newNode->next = graph->adjLists[src];
graph->adjLists[src] = newNode;
// 添加 dest -> src(无向图)
newNode = (AdjNode*)malloc(sizeof(AdjNode));
newNode->vertex = src;
newNode->next = graph->adjLists[dest];
graph->adjLists[dest] = newNode;
}
// DFS 递归实现
void DFS(Graph* graph, int vertex) {
graph->visited[vertex] = 1;
printf("%d ", vertex);
AdjNode* temp = graph->adjLists[vertex];
while (temp != NULL) {
int adjVertex = temp->vertex;
if (!graph->visited[adjVertex]) {
DFS(graph, adjVertex);
}
temp = temp->next;
}
}
int main() {
Graph* graph = createGraph(6);
addEdge(graph, 0, 1);
addEdge(graph, 0, 2);
addEdge(graph, 1, 3);
addEdge(graph, 1, 4);
addEdge(graph, 2, 5);
printf("DFS 遍历顺序(从顶点0开始): ");
DFS(graph, 0);
printf("\n");
return 0;
}
4.3 案例:迷宫求解
在迷宫中,DFS 可以沿着一条路径一直走,直到死胡同再返回,最终找到出口。非常适合探索所有可能路径。
4.4 复杂度分析
- 时间复杂度:O(V + E),V 为顶点数,E 为边数。
- 空间复杂度:O(V)(递归栈或显式栈)。
4.5 应用场景
- 拓扑排序
- 寻找连通分量
- 回溯算法(如八皇后、数独)
- 树的前/中/后序遍历
5. 广度优先搜索(BFS) ------ 层层推进
5.1 核心思想
从起点出发,先访问所有邻接点,再依次访问邻接点的邻接点,像水波一样向外扩散。通常用队列实现。
5.2 BFS 实现(队列)
c
#include <stdio.h>
#include <stdlib.h>
#define MAX_VERTICES 100
// 队列结构(简单循环队列)
typedef struct Queue {
int items[MAX_VERTICES];
int front, rear;
} Queue;
Queue* createQueue() {
Queue* q = (Queue*)malloc(sizeof(Queue));
q->front = -1;
q->rear = -1;
return q;
}
int isEmpty(Queue* q) {
return q->front == -1;
}
void enqueue(Queue* q, int value) {
if (q->rear == MAX_VERTICES - 1) return;
if (q->front == -1) q->front = 0;
q->rear++;
q->items[q->rear] = value;
}
int dequeue(Queue* q) {
if (isEmpty(q)) return -1;
int item = q->items[q->front];
if (q->front == q->rear) {
q->front = -1;
q->rear = -1;
} else {
q->front++;
}
return item;
}
// BFS 函数
void BFS(Graph* graph, int startVertex) {
// 重置 visited
for (int i = 0; i < graph->numVertices; i++) {
graph->visited[i] = 0;
}
Queue* queue = createQueue();
graph->visited[startVertex] = 1;
enqueue(queue, startVertex);
while (!isEmpty(queue)) {
int current = dequeue(queue);
printf("%d ", current);
AdjNode* temp = graph->adjLists[current];
while (temp != NULL) {
int adj = temp->vertex;
if (!graph->visited[adj]) {
graph->visited[adj] = 1;
enqueue(queue, adj);
}
temp = temp->next;
}
}
free(queue);
}
// 主函数沿用之前的图结构
int main() {
Graph* graph = createGraph(6);
addEdge(graph, 0, 1);
addEdge(graph, 0, 2);
addEdge(graph, 1, 3);
addEdge(graph, 1, 4);
addEdge(graph, 2, 5);
printf("BFS 遍历顺序(从顶点0开始): ");
BFS(graph, 0);
printf("\n");
return 0;
}
5.3 案例:社交网络好友推荐
BFS 可以找到一个人所有距离为 2 的好友(朋友的朋友),用于推荐。
5.4 复杂度分析
- 时间复杂度:O(V + E)
- 空间复杂度:O(V)(队列存储)
5.5 应用场景
- 无权图的最短路径
- 网络爬虫
- 连通性检测
- 层次遍历(树)
📊 搜索算法对比总结
| 算法 | 数据结构要求 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|---|
| 线性搜索 | 无 | O(n) | O(1) | 小数据、无序 |
| 二分搜索 | 有序 | O(log n) | O(1) | 静态有序数据 |
| 哈希查找 | 哈希表 | 平均 O(1) | O(n) | 大量查找、动态插入 |
| DFS | 图/树 | O(V+E) | O(V) | 连通分量、回溯 |
| BFS | 图/树 | O(V+E) | O(V) | 最短路径(无权)、层次遍历 |
🎯 如何选择搜索算法?
- 数据有序:优先二分搜索,简单高效。
- 数据动态变化且需要快速查找:哈希表是首选。
- 数据量极小:线性搜索足矣,无需复杂结构。
- 在图或树中搜索:DFS 适合寻找所有可能解(如迷宫),BFS 适合找最短路径。
✍️ 课后练习与答案
练习题目
- 实现一个二分搜索变体:在有序数组中查找目标值的第一个出现位置。
- 用 C 语言实现一个简单的哈希表,支持字符串键(使用 djb2 哈希函数)。
- 基于 DFS 实现一个迷宫求解程序(二维矩阵表示迷宫)。
- 使用 BFS 计算无权图中两个节点之间的最短路径长度,并输出路径。
参考答案
1. 二分搜索查找第一个出现位置
c
int binarySearchFirst(int arr[], int n, int target) {
int left = 0, right = n - 1;
int result = -1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] == target) {
result = mid;
right = mid - 1; // 继续向左搜索
} else if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return result;
}
2. 字符串键哈希表(拉链法)
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define TABLE_SIZE 100
typedef struct StrNode {
char* key;
int value;
struct StrNode* next;
} StrNode;
StrNode* hashTableStr[TABLE_SIZE];
unsigned int hashStr(const char* key) {
unsigned long hash = 5381;
int c;
while ((c = *key++)) {
hash = ((hash << 5) + hash) + c;
}
return hash % TABLE_SIZE;
}
void insertStr(const char* key, int value) {
int idx = hashStr(key);
StrNode* cur = hashTableStr[idx];
while (cur) {
if (strcmp(cur->key, key) == 0) {
cur->value = value;
return;
}
cur = cur->next;
}
StrNode* newNode = (StrNode*)malloc(sizeof(StrNode));
newNode->key = (char*)malloc(strlen(key) + 1);
strcpy(newNode->key, key);
newNode->value = value;
newNode->next = hashTableStr[idx];
hashTableStr[idx] = newNode;
}
int searchStr(const char* key) {
int idx = hashStr(key);
StrNode* cur = hashTableStr[idx];
while (cur) {
if (strcmp(cur->key, key) == 0) return cur->value;
cur = cur->next;
}
return -1;
}
3. DFS 迷宫求解(4方向,1为墙,0为路)
c
#include <stdio.h>
#include <stdbool.h>
#define ROWS 5
#define COLS 5
int maze[ROWS][COLS] = {
{0, 1, 0, 0, 0},
{0, 1, 0, 1, 0},
{0, 0, 0, 1, 0},
{0, 1, 1, 0, 0},
{0, 0, 0, 0, 0}
};
int visited[ROWS][COLS];
int dirs[4][2] = {{-1,0},{1,0},{0,-1},{0,1}};
bool dfs(int x, int y, int endX, int endY) {
if (x == endX && y == endY) {
printf("(%d,%d) ", x, y);
return true;
}
visited[x][y] = 1;
for (int i = 0; i < 4; i++) {
int nx = x + dirs[i][0];
int ny = y + dirs[i][1];
if (nx >= 0 && nx < ROWS && ny >= 0 && ny < COLS && !visited[nx][ny] && maze[nx][ny] == 0) {
if (dfs(nx, ny, endX, endY)) {
printf("(%d,%d) ", x, y);
return true;
}
}
}
return false;
}
4. BFS 最短路径(无权图)
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_V 100
typedef struct {
int v;
int dist;
int parent;
} NodeInfo;
void bfsShortestPath(Graph* graph, int start, int end) {
int visited[MAX_V] = {0};
int parent[MAX_V];
memset(parent, -1, sizeof(parent));
int queue[MAX_V], front = 0, rear = 0;
visited[start] = 1;
queue[rear++] = start;
while (front < rear) {
int cur = queue[front++];
if (cur == end) break;
AdjNode* temp = graph->adjLists[cur];
while (temp) {
int adj = temp->vertex;
if (!visited[adj]) {
visited[adj] = 1;
parent[adj] = cur;
queue[rear++] = adj;
}
temp = temp->next;
}
}
if (!visited[end]) {
printf("没有路径\n");
return;
}
// 输出路径(逆序)
int path[MAX_V], len = 0;
for (int v = end; v != -1; v = parent[v]) {
path[len++] = v;
}
printf("最短路径长度: %d\n路径: ", len - 1);
for (int i = len - 1; i >= 0; i--) {
printf("%d ", path[i]);
}
printf("\n");
}
🌟 寄语
搜索是计算机解决问题的核心手段之一。掌握了线性、二分、哈希这些基本查找,你就拥有了处理大多数数据查找问题的能力。而 DFS 和 BFS 则是你进入图论和高级算法世界的钥匙。
请务必动手实践:自己敲一遍代码,调试,修改参数,观察输出。尤其是图搜索,建议画出图的结构,手动模拟算法过程,你会发现它们并不难理解。
下一篇文章我们将进入图论算法,学习最短路径、最小生成树等更复杂的图算法。准备好你的 C 语言环境,我们继续进阶!
如果对本文有任何疑问,欢迎评论区交流。
如果觉得有帮助,请点赞、收藏,让更多同学一起进步!