【算法】查找与排序

因文章篇幅有限,查找和排序分开写(附代码与详细过程 + 注释详解),这篇文章主讲算法中的数据查找。

查找是数据结构中最基本的操作之一,用于从给定的数据集合中找到特定的目标数据。查找的效率直接影响程序的性能,选择合适的查找方法取决于数据的组织形式和应用场景。

查找的分类

查找操作可以按照数据存储结构和应用场景分为以下几类:

  1. 线性表上的查找
    • 顺序查找(线性查找)
    • 二分查找(折半查找)
    • 索引查找(分块查找)
  2. 树上的查找
    • 二叉搜索树查找
    • 平衡二叉树查找(如 AVL 树、红黑树)
    • B 树和 B+ 树查找
  3. 散列表上的查找
    • 基于哈希表的查找(hash查找)
  4. 图的查找
    • 广度优先搜索(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的应用

  1. 最短路径问题(无权图):
    • BFS 能够找到从起始点到目标点的最短路径(路径长度以边的数量衡量)。
    • 应用场景:
      • 地图导航(如最短路径规划)。
      • 网络传输中的最短跳数计算。
  2. 连通性检查
    • 检查图中是否存在连接的路径,或统计连通分量的数量。
    • 应用场景
      • 网络连通性分析。
      • 图的分区问题。
  3. 层次遍历
    • 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的应用

  1. 路径搜索问题
    • 找到从起点到目标点的所有路径。
    • 应用场景
      • 迷宫求解。
      • 寻找所有可能的方案。
  2. 拓扑排序
    • 对有向无环图(DAG)进行排序,以确定任务的依赖顺序。
    • 应用场景
      • 编译器中的任务调度。
      • 项目任务规划。
  3. 连通性检查
    • 统计图的连通分量,判断两个节点是否连通。
    • 应用场景
      • 网络故障分析。
      • 图的分区问题。
  4. 图的循环检测
    • 检测图中是否存在环。
    • 应用场景
      • 死锁检测。
      • 图依赖的验证。

BFS 与 DFS 的对比:

算法特点适用场景
广度优先搜索(BFS)逐层遍历,找到目标节点时路径一定是最短的(适用于无权图)。最短路径、层次遍历、连通性检查。
深度优先搜索(DFS)沿着路径搜索到底再回溯,适合搜寻所有路径或解决需要回溯的问题。所有路径、拓扑排序、循环检测。
选择使用 BFS 或 DFS:如果关心路径最短,使用 BFS。如果关心所有可能的路径或需要深入探索,使用 DFS。

选择合适的查找方法取决于数据的组织形式、应用场景和性能需求。例如,小规模无序数据:使用顺序查找更为简单易用;有序数据:使用二分查找快速高效;动态数据集合:使用二叉搜索树或平衡树;大规模数据:哈希查找或 B+ 树适合外部存储。

综上。希望该内容能对你有帮助。

以上。仅供学习与分享交流,请勿用于商业用途!转载需提前说明。

我是一个十分热爱技术的程序员,希望这篇文章能够对您有帮助,也希望认识更多热爱程序开发的小伙伴。
感谢!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猫猫的小茶馆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值