因文章篇幅有限,查找和排序分开写(附代码与详细过程 + 注释详解),这篇文章主讲算法中的数据查找。
查找是数据结构中最基本的操作之一,用于从给定的数据集合中找到特定的目标数据。查找的效率直接影响程序的性能,选择合适的查找方法取决于数据的组织形式和应用场景。
查找的分类
查找操作可以按照数据存储结构和应用场景分为以下几类:
- 线性表上的查找
- 顺序查找(线性查找)
- 二分查找(折半查找)
- 索引查找(分块查找)
- 树上的查找
- 二叉搜索树查找
- 平衡二叉树查找(如 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; // 新节点高度为 1
return 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 10
int 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+ 树适合外部存储。
综上。希望该内容能对你有帮助。
以上。仅供学习与分享交流,请勿用于商业用途!转载需提前说明。
我是一个十分热爱技术的程序员,希望这篇文章能够对您有帮助,也希望认识更多热爱程序开发的小伙伴。
感谢!