算法筑基(二):搜索算法——从线性查找到图搜索,精准定位数据

算法筑基(二):搜索算法------从线性查找到图搜索,精准定位数据


📖 前言

如果说排序是让数据变得有序,那么搜索就是从这些数据中快速找到你想要的目标。无论是数据库中的一条记录、网页中的关键词,还是地图上的最短路径,背后都离不开高效的搜索算法。

在本文中,我们将从最直观的线性搜索开始,逐步深入到二分搜索、哈希查找,最后介绍图搜索中的两大核心:深度优先搜索(DFS)和广度优先搜索(BFS)。每个算法都配有完整的C语言代码逐行注释实际案例 ,让你掌握搜索的本质。最后附上课后练习及答案,巩固所学知识。


📌 本文目录

  1. 线性搜索 ------ 最朴素的查找
  2. 二分搜索 ------ 有序数据的神器
  3. 哈希查找 ------ 用空间换时间
  4. 深度优先搜索(DFS) ------ 一条路走到黑
  5. 广度优先搜索(BFS) ------ 层层推进
  6. 搜索算法对比总结
  7. 课后练习与答案

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 适合找最短路径。

✍️ 课后练习与答案

练习题目

  1. 实现一个二分搜索变体:在有序数组中查找目标值的第一个出现位置。
  2. 用 C 语言实现一个简单的哈希表,支持字符串键(使用 djb2 哈希函数)。
  3. 基于 DFS 实现一个迷宫求解程序(二维矩阵表示迷宫)。
  4. 使用 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 语言环境,我们继续进阶!


如果对本文有任何疑问,欢迎评论区交流。
如果觉得有帮助,请点赞、收藏,让更多同学一起进步!

相关推荐
Alicx.1 小时前
dfs由易到难
算法·蓝桥杯·宽度优先
_日拱一卒2 小时前
LeetCode:找到字符串中的所有字母异位词
算法·leetcode
云泽8082 小时前
深入 AVL 树:原理剖析、旋转算法与性能评估
数据结构·c++·算法
Wilber的技术分享3 小时前
【LeetCode高频手撕题 2】面试中常见的手撕算法题(小红书)
笔记·算法·leetcode·面试
邪神与厨二病3 小时前
Problem L. ZZUPC
c++·数学·算法·前缀和
梯度下降中4 小时前
LoRA原理精讲
人工智能·算法·机器学习
IronMurphy4 小时前
【算法三十一】46. 全排列
算法·leetcode·职场和发展
czlczl200209254 小时前
力扣1911. 最大交替子序列和
算法·leetcode·动态规划
靴子学长5 小时前
Decoder only 架构下 - KV cache 的理解
pytorch·深度学习·算法·大模型·kv