因文章篇幅有限,查找和排序分开写(附代码与详细过程 + 注释详解),这篇文章主讲算法中的数据查找。
查找是数据结构中最基本的操作之一,用于从给定的数据集合中找到特定的目标数据。查找的效率直接影响程序的性能,选择合适的查找方法取决于数据的组织形式和应用场景。
查找的分类
查找操作可以按照数据存储结构和应用场景分为以下几类:
- 线性表上的查找
- 顺序查找(线性查找)
- 二分查找(折半查找)
- 索引查找(分块查找)
- 树上的查找
- 二叉搜索树查找
- 平衡二叉树查找(如 AVL 树、红黑树)
- B 树和 B+ 树查找
- 散列表上的查找
- 基于哈希表的查找(hash查找)
- 图的查找
- 广度优先搜索(BFS)
- 深度优先搜索(DFS)
线性表上的查找
1. 顺序查找(线性查找)
逐一扫描线性表中的元素,直到找到目标值或扫描完所有元素。适用于无序集合。
时间复杂度
- 最好情况:
O(1)
(目标值在第一个位置)。 - 最坏情况:
O(n)
(目标值不在表中)。 - 平均情况:
O(n/2) ≈ O(n)
。
int linearSearch(int arr[], int n, int target) {for (int i = 0; i < n; i++) {if (arr[i] == target) {return i; // 找到并返回索引}}return -1; // 未找到
}
// 适用场景
// 数据无序。
// 数据规模小,查找次数少。
2. 二分查找(折半查找)
在有序集合中,每次将查找范围缩小一半,直至找到目标值。适用于静态有序集合。
时间复杂度
- 最好情况:
O(1)
。 - 最坏情况:
O(log n)
。 - 平均情况:
O(log n)
。
int half_search(int *a,int elem)
{int low,high,mid; //设置三个指向标志位low=0; //低位下标high=N-1; //高位下标while(low<=high) //当low>high 表中没有对应数据{mid=(low+high)/2; //不断进行操作的中间下标if(elem>a[mid]) //要查的数据与中间下标指向的值做比较{low=mid+1; //大于mid指向值时,低位下标指向位置改变}if(elem<a[mid]) //小于mid指向值时,高位下标指向位置改变{high=mid-1;}if(elem==a[mid]) //等于中间值时{return mid+1; //返回的是排在数组中第几个}}return -1; // 未找到
}
// 适用场景
// 数据有序。
// 需要频繁查找。
3. 索引查找(分块查找)
将线性表划分为多个块,块内元素无序,但块间有序。通过索引表定位块,再在块内使用顺序查找。
时间复杂度
- 查找索引表:
O(√n)
。 - 块内顺序查找:
O(√n)
。 - 总复杂度:
O(2√n) ≈ O(√n)
。
int blockSearch(int index[], int blocks[][10], int indexSize, int blockSize, int target) {int block = -1;// 查找索引表for (int i = 0; i < indexSize; i++) {if (target <= index[i]) {block = i;break;}}if (block == -1) return -1; // 未找到// 在对应的块中顺序查找for (int j = 0; j < blockSize; j++) {if (blocks[block][j] == target) {return block * blockSize + j; // 返回全局索引}}return -1; // 未找到
}
// 适用场景
// 数据量大,且对查找效率要求较高。
// 索引表占用空间较小。
树上的查找
1. 二叉搜索树查找
在二叉搜索树中,每个节点的左子树节点值小于根节点值,右子树节点值大于根节点值。查找时,根据节点值大小递归向左或右子树查找。
时间复杂度
- 平衡树:
O(log n)
。 - 退化为链表时:
O(n)
。
Node* search(Node* root, int key) {if (root == NULL || root->data == key) {return root; // 找到目标值或未找到}if (key < root->data) {return search(root->left, key); // 左子树查找}return search(root->right, key); // 右子树查找
}
// 适用场景
// 动态数据集合。
// 数据插入和删除频繁。
2. 平衡二叉树查找
平衡二叉树(如 AVL 树、红黑树)通过平衡旋转维持树的高度在 O(log n)
。查找操作与二叉搜索树类似。
时间复杂度
O(log n)
。
#include <stdio.h>
#include <stdlib.h>// 定义 AVL 树节点
typedef struct Node {int data; // 节点值struct Node* left; // 左子树struct Node* right; // 右子树int height; // 节点高度
} Node;// 创建新节点
Node* createNode(int data) {Node* newNode = (Node*)malloc(sizeof(Node));newNode->data = data;newNode->left = NULL;newNode->right = NULL;newNode->height = 1; // 新节点高度为 1return newNode;
}// 适用场景
// 动态数据集合,要求查找性能稳定。
3. B 树和 B+ 树查找
B 树是一种多叉平衡树,适用于外部存储(如磁盘)。B+ 树是 B 树的改进版本,叶子节点通过链表连接,适合范围查找。
时间复杂度
O(log n)
。
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>#define MAX_KEYS 3 // B 树的阶数为 4(MAX_KEYS + 1)// B 树的查找过程
// B 树节点定义
typedef struct BTreeNode {int keys[MAX_KEYS]; // 节点中的关键字struct BTreeNode* children[MAX_KEYS + 1]; // 子节点指针int keyCount; // 当前节点的关键字数量bool isLeaf; // 是否是叶子节点
} BTreeNode;// 创建 B 树节点
BTreeNode* createNode(bool isLeaf) {BTreeNode* newNode = (BTreeNode*)malloc(sizeof(BTreeNode));newNode->isLeaf = isLeaf;newNode->keyCount = 0;for (int i = 0; i <= MAX_KEYS; i++) {newNode->children[i] = NULL;}return newNode;
}// 查找关键字
BTreeNode* search(BTreeNode* root, int key) {if (root == NULL) return NULL;int i = 0;// 在当前节点中查找关键字while (i < root->keyCount && key > root->keys[i]) {i++;}// 如果找到关键字,返回当前节点if (i < root->keyCount && key == root->keys[i]) {return root;}// 如果是叶子节点,说明找不到if (root->isLeaf) {return NULL;}// 否则递归到对应的子节点return search(root->children[i], key);
}// 插入关键字到非满节点
void insertNonFull(BTreeNode* node, int key) {int i = node->keyCount - 1;// 如果是叶子节点if (node->isLeaf) {// 插入到合适的位置while (i >= 0 && node->keys[i] > key) {node->keys[i + 1] = node->keys[i];i--;}node->keys[i + 1] = key;node->keyCount++;} else {// 如果是内部节点while (i >= 0 && node->keys[i] > key) {i--;}i++;// 如果子节点已满,先分裂if (node->children[i]->keyCount == MAX_KEYS) {splitChild(node, i);if (key > node->keys[i]) {i++;}}// 递归插入到子节点insertNonFull(node->children[i], key);}
}// 分裂子节点
void splitChild(BTreeNode* parent, int index) {BTreeNode* child = parent->children[index];BTreeNode* newChild = createNode(child->isLeaf);newChild->keyCount = MAX_KEYS / 2;// 将后半部分的关键字复制到新节点for (int i = 0; i < MAX_KEYS / 2; i++) {newChild->keys[i] = child->keys[i + MAX_KEYS / 2 + 1];}// 如果不是叶子节点,将子节点指针也复制if (!child->isLeaf) {for (int i = 0; i <= MAX_KEYS / 2; i++) {newChild->children[i] = child->children[i + MAX_KEYS / 2 + 1];}}child->keyCount = MAX_KEYS / 2;// 将新节点插入到父节点for (int i = parent->keyCount; i > index; i--) {parent->children[i + 1] = parent->children[i];parent->keys[i] = parent->keys[i - 1];}parent->children[index + 1] = newChild;parent->keys[index] = child->keys[MAX_KEYS / 2];parent->keyCount++;
}// 插入关键字到 B 树
BTreeNode* insert(BTreeNode* root, int key) {if (root == NULL) {root = createNode(true);root->keys[0] = key;root->keyCount = 1;return root;}// 如果根节点已满if (root->keyCount == MAX_KEYS) {BTreeNode* newRoot = createNode(false);newRoot->children[0] = root;splitChild(newRoot, 0);int i = 0;if (newRoot->keys[0] < key) {i++;}insertNonFull(newRoot->children[i], key);return newRoot;}// 否则直接插入insertNonFull(root, key);return root;
}// 中序遍历
void inorderTraversal(BTreeNode* root) {if (root != NULL) {int i;for (i = 0; i < root->keyCount; i++) {inorderTraversal(root->children[i]);printf("%d ", root->keys[i]);}inorderTraversal(root->children[i]);}
}int main() {BTreeNode* root = NULL;// 插入关键字root = insert(root, 10);root = insert(root, 20);root = insert(root, 5);root = insert(root, 6);root = insert(root, 12);root = insert(root, 30);root = insert(root, 7);root = insert(root, 17);printf("Inorder traversal of B-Tree: ");inorderTraversal(root);printf("\n");// 查找关键字int key = 6;BTreeNode* result = search(root, key);if (result != NULL) {printf("Key %d found in the B-Tree.\n", key);} else {printf("Key %d not found in the B-Tree.\n", key);}return 0;
}
// 使用场景
// B 树适用于动态插入、删除和查找操作。
// B+ 树更适合范围查询和顺序访问。
// B 树和 B+ 树查找时间复杂度均为 O(log n),非常高效,广泛应用于数据库索引和文件系统中。
// 数据量极大,存储在磁盘或其他外部存储介质上。
// 数据库索引结构。
B+ 树是 B 树的改进版本,所有数据存储在叶子节点中,叶子节点通过链表相连,适合范围查询。由于实现较复杂,这里不加以演示。
散列表上的查找
1. 哈希查找(hash查找)
利用哈希函数将关键字直接映射到数组索引,查找时通过计算哈希值快速定位。
时间复杂度
- 理想情况:
O(1)
。 - 最坏情况(冲突严重):
O(n)
。
#define SIZE 10int hash(int key) {return key % SIZE; // 哈希函数
}void insert(int table[], int key) {int index = hash(key);while (table[index] != -1) { // 线性探测解决冲突index = (index + 1) % SIZE;}table[index] = key;
}int search(int table[], int key) {int index = hash(key);while (table[index] != -1) {if (table[index] == key) return index; // 找到目标值index = (index + 1) % SIZE;}return -1; // 未找到
}
// 适用场景
// 高效查找需求,数据量大但内存充足。
// 实现字典、集合等数据结构。
图的查找
1. 广度优先搜索(BFS)
- BFS 是一种逐层遍历图的算法:
- 先访问起始节点。
- 然后访问所有直接相邻的节点(第一层)。
- 再访问上一层节点的下一层邻居,依此类推。
- BFS 使用队列作为辅助数据结构,先进先出(FIFO)保证了按层访问节点。
代码描述:
给定一个无向图,求从起点节点 0 到目标节点 4 的最短路径。
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#define MAX 100// BFS 实现最短路径
void bfsShortestPath(int graph[MAX][MAX], int n, int start, int end) {int visited[MAX] = {0}; // 记录节点是否被访问int distance[MAX] = {0}; // 记录起点到每个节点的距离int previous[MAX]; // 记录路径for (int i = 0; i < n; i++) previous[i] = -1;int queue[MAX];int front = 0, rear = 0;// 起点入队queue[rear++] = start;visited[start] = 1;while (front < rear) {int current = queue[front++];// 遍历当前节点的邻居for (int i = 0; i < n; i++) {if (graph[current][i] == 1 && !visited[i]) {queue[rear++] = i; // 将邻居节点入队visited[i] = 1; // 标记为已访问distance[i] = distance[current] + 1;previous[i] = current; // 记录路径// 如果找到目标节点,停止搜索if (i == end) {front = rear;break;}}}}// 输出路径和距离if (visited[end]) {printf("Shortest distance: %d\n", distance[end]);printf("Path: ");int path[MAX], pathLength = 0, node = end;while (node != -1) {path[pathLength++] = node;node = previous[node];}for (int i = pathLength - 1; i >= 0; i--) {printf("%d ", path[i]);}printf("\n");} else {printf("No path found from %d to %d\n", start, end);}
}int main() {// 定义无向图(邻接矩阵)int graph[MAX][MAX] = {{0, 1, 1, 0, 0},{1, 0, 1, 1, 0},{1, 1, 0, 0, 1},{0, 1, 0, 0, 1},{0, 0, 1, 1, 0}};int n = 5; // 节点数量int start = 0, end = 4;bfsShortestPath(graph, n, start, end);return 0;
}
输出:
Shortest distance: 2
Path: 0 2 4
BFS的应用
- 最短路径问题(无权图):
- BFS 能够找到从起始点到目标点的最短路径(路径长度以边的数量衡量)。
- 应用场景:
- 地图导航(如最短路径规划)。
- 网络传输中的最短跳数计算。
- 连通性检查
- 检查图中是否存在连接的路径,或统计连通分量的数量。
- 应用场景:
- 网络连通性分析。
- 图的分区问题。
- 层次遍历
- BFS 可以用于按层次访问节点。
- 应用场景:
- 社交网络中的层级推荐。
- 树的层次遍历。
2. 深度优先搜索(DFS)
- DFS 是一种尽可能沿着一条路径探索下去的图遍历算法:
- 访问起始节点后,优先访问其第一个未访问的邻居。
- 如果当前路径走到尽头,则回溯到上一个节点,继续探索其他未访问的邻居。
- DFS 使用栈作为辅助数据结构,递归实现中使用函数调用栈。
代码描述:
给定一个有向图,找到从起点节点 0 到目标节点 4 的所有路径。
#include <stdio.h>
#include <stdlib.h>// 打印路径
void printPath(int path[], int pathLength) {for (int i = 0; i < pathLength; i++) {printf("%d ", path[i]);}printf("\n");
}// DFS 寻找所有路径
void dfsAllPaths(int graph[100][100], int n, int current, int end, int visited[], int path[], int pathLength) {visited[current] = 1; // 标记当前节点为已访问path[pathLength++] = current; // 添加当前节点到路径中if (current == end) {// 如果到达目标节点,输出路径printPath(path, pathLength);} else {// 遍历当前节点的邻居for (int i = 0; i < n; i++) {if (graph[current][i] == 1 && !visited[i]) {dfsAllPaths(graph, n, i, end, visited, path, pathLength);}}}visited[current] = 0; // 回溯,标记当前节点为未访问
}int main() {// 定义有向图(邻接矩阵)int graph[100][100] = {{0, 1, 1, 0, 0},{0, 0, 1, 1, 0},{0, 0, 0, 0, 1},{0, 0, 0, 0, 1},{0, 0, 0, 0, 0}};int n = 5; // 节点数量int start = 0, end = 4;int visited[100] = {0};int path[100];int pathLength = 0;printf("All paths from %d to %d:\n", start, end);dfsAllPaths(graph, n, start, end, visited, path, pathLength);return 0;
}
输出:
All paths from 0 to 4:
0 1 3 4
0 1 2 4
0 2 4
DFS的应用
- 路径搜索问题
- 找到从起点到目标点的所有路径。
- 应用场景:
- 迷宫求解。
- 寻找所有可能的方案。
- 拓扑排序
- 对有向无环图(DAG)进行排序,以确定任务的依赖顺序。
- 应用场景:
- 编译器中的任务调度。
- 项目任务规划。
- 连通性检查
- 统计图的连通分量,判断两个节点是否连通。
- 应用场景:
- 网络故障分析。
- 图的分区问题。
- 图的循环检测
- 检测图中是否存在环。
- 应用场景:
- 死锁检测。
- 图依赖的验证。
BFS 与 DFS 的对比:
算法 | 特点 | 适用场景 |
---|---|---|
广度优先搜索(BFS) | 逐层遍历,找到目标节点时路径一定是最短的(适用于无权图)。 | 最短路径、层次遍历、连通性检查。 |
深度优先搜索(DFS) | 沿着路径搜索到底再回溯,适合搜寻所有路径或解决需要回溯的问题。 | 所有路径、拓扑排序、循环检测。 |
选择使用 BFS 或 DFS: | 如果关心路径最短,使用 BFS。 | 如果关心所有可能的路径或需要深入探索,使用 DFS。 |
选择合适的查找方法取决于数据的组织形式、应用场景和性能需求。例如,小规模无序数据:使用顺序查找更为简单易用;有序数据:使用二分查找快速高效;动态数据集合:使用二叉搜索树或平衡树;大规模数据:哈希查找或 B+ 树适合外部存储。
综上。希望该内容能对你有帮助。
以上。仅供学习与分享交流,请勿用于商业用途!转载需提前说明。
我是一个十分热爱技术的程序员,希望这篇文章能够对您有帮助,也希望认识更多热爱程序开发的小伙伴。
感谢!