当前位置: 首页 > article >正文

高级数据结构应用:并查集、跳表、布隆过滤器与缓存结构

高级数据结构应用:并查集、跳表、布隆过滤器与缓存结构

在解决复杂问题时,选择合适的数据结构往往是成功的关键。本文将深入探讨四种强大而实用的高级数据结构:并查集、跳表、布隆过滤器和高效缓存结构(LRU和LFU),包括它们的原理、实现、复杂度分析和实际应用场景。

1. 并查集(Disjoint Set / Union-Find)

1.1 基本原理

并查集是一种树形数据结构,用于处理一些不相交集合的合并及查询问题。主要支持两种操作:

  • 查找(Find):确定元素属于哪一个子集
  • 合并(Union):将两个子集合并成一个集合

并查集的核心思想是使用"代表元素"(或称"根节点")来表示一个集合,并通过父指针将同一集合中的元素链接起来。

1.2 C++实现

下面是一个带路径压缩和按秩合并优化的并查集实现:

#include <iostream>
#include <vector>

class DisjointSet {
   
private:
    std::vector<int> parent; // 父节点
    std::vector<int> rank;   // 秩(树的高度上界)
    int count;               // 集合数量
    
public:
    // 构造函数,初始化n个单元素集合
    DisjointSet(int n) {
   
        count = n;
        parent.resize(n);
        rank.resize(n, 0);
        
        // 初始时,每个元素的父节点是自己
        for (int i = 0; i < n; i++) {
   
            parent[i] = i;
        }
    }
    
    // 查找操作:返回元素x所属集合的代表元素
    // 使用路径压缩优化
    int find(int x) {
   
        // 路径压缩:将x到根节点路径上的所有节点直接连接到根节点
        if (parent[x] != x) {
   
            parent[x] = find(parent[x]);
        }
        return parent[x];
    }
    
    // 合并操作:合并元素x和y所属的集合
    // 使用按秩合并优化
    void unionSets(int x, int y) {
   
        int rootX = find(x);
        int rootY = find(y);
        
        // 如果已经在同一集合中,则不需要合并
        if (rootX == rootY) {
   
            return;
        }
        
        // 按秩合并:将较矮的树连接到较高的树下
        if (rank[rootX] < rank[rootY]) {
   
            parent[rootX] = rootY;
        } else if (rank[rootX] > rank[rootY]) {
   
            parent[rootY] = rootX;
        } else {
   
            // 如果秩相同,则任意选择一个作为根,并增加秩
            parent[rootY] = rootX;
            rank[rootX]++;
        }
        
        // 集合数量减1
        count--;
    }
    
    // 判断元素x和y是否在同一集合中
    bool isConnected(int x, int y) {
   
        return find(x) == find(y);
    }
    
    // 返回集合数量
    int getCount() const {
   
        return count;
    }
};

1.3 优化技术

  1. 路径压缩(Path Compression):在Find操作中,将查找路径上的所有节点都直接连接到根节点,减少后续查询的深度。

  2. 按秩合并(Union by Rank):在Union操作中,总是将较小的树(秩较小)连接到较大的树(秩较大)下,避免树过深。

  3. 按大小合并(Union by Size):类似按秩合并,但使用集合元素数量而不是树高作为决策依据。

1.4 复杂度分析

设n为元素数量,m为操作次数:

操作 时间复杂度(最坏) 时间复杂度(平均/摊销)
构造 O(n) O(n)
Find O(log n) O(α(n))*
Union O(log n) O(α(n))*

*α(n)是阿克曼函数的反函数,增长极其缓慢,在实际应用中可视为常数(≤4)。

空间复杂度:O(n),用于存储父节点和秩信息。

1.5 应用场景

  1. 连通性问题:判断网络中节点之间是否连通,例如社交网络中的朋友关系。

  2. 最小生成树算法:Kruskal算法中用于检测边是否会形成环。

  3. 等价类划分:将具有等价关系的元素归类。

  4. 集合合并:处理集合的动态合并操作。

1.6 实际应用示例

连通块问题
// 计算图中的连通分量数量
int countComponents(int n, std::vector<std::vector<int>>& edges) {
   
    DisjointSet ds(n);
    
    // 连接所有的边
    for (const auto& edge : edges) {
   
        ds.unionSets(edge[0], edge[1]);
    }
    
    return ds.getCount();
}
Kruskal算法实现最小生成树
#include <algorithm>

struct Edge {
   
    int src, dest, weight;
    
    // 用于按权重排序
    bool operator<(const Edge& other) const {
   
        return weight < other.weight;
    }
};

std::vector<Edge> kruskalMST(int n, std::vector<Edge>& edges) {
   
    std::vector<Edge> result;
    DisjointSet ds(n);
    
    // 按边的权重从小到大排序
    std::sort(edges.begin(), edges.end());
    
    // 按权重从小到大尝试加入边,如果不构成环则加入MST
    for (const auto& edge : edges) {
   
        int src = edge.src;
        int dest = edge.dest;
        
        // 如果src和dest不在同一连通分量中
        if (!ds.isConnected(src, dest)) {
   
            // 加入此边到MST
            result.push_back(edge);
            
            // 合并连通分量
            ds.unionSets(src, dest);
        }
    }
    
    return result;
}

2. 跳表(Skip List)

2.1 基本原理

跳表是一种随机化的数据结构,基于链表,但允许快速查找、插入和删除。其核心思想是通过维护多层链表,来实现"快速跳跃"查找,从而获得对数级别的操作复杂度。

跳表的特点:

  • 每个节点包含一个值和多个前向指针
  • 底层是一个普通的有序链表
  • 上层链表是下层链表的"快速通道",可以跳过多个元素
  • 节点层数采用随机化策略确定,保证了平均性能

2.2 C++实现

#include <iostream>
#include <vector>
#include <cstdlib>
#include <ctime>
#include <climits>

class SkipList {
   
private:
    // 跳表节点结构
    struct Node {
   
        int value;                 // 节点值
        std::vector<Node*> forward; // 前向指针数组
        
        Node(int val, int level) : value(val), forward(level, nullptr) {
   }
    };
    
    Node* head;         // 头节点
    int maxLevel;       // 最大层数
    int currentLevel;   // 当前层数
    float probability;  // 层数增加的概率(通常0.5)
    
    // 随机生成节点的层数
    int randomLevel() {
   
        int level = 1;
        // 生成[0,1)的随机数,如果小于probability且未超过最大层数,则层数加1
        while ((static_cast<float>(rand()) / RAND_MAX) < probability && level < maxLevel) {
   
            level++;
        }
        return level;
    }
    
public:
    // 构造函数
    SkipList(int maxLevel = 16, float p = 0.5) 
        : maxLevel(maxLevel), currentLevel(1), probability(p) {
   
        // 初始化随机数生成器
        srand(static_cast<unsigned int>(time(nullptr)));
        
        // 创建头节点,层数为最大层数
        head = new Node(INT_MIN, maxLevel);
    }
    
    // 析构函数
    ~SkipList() {
   
        Node* current = head;
        Node* next;
        
        while (current) {
   
            next = current->forward[0]; // 下一个节点
            delete current;
            current = next;
        }
    }
    
    // 搜索值为target的节点
    bool search(int target) {
   
        Node* current = head;
        
        // 从最高层开始查找
        for (int i = currentLevel - 1; i >= 0; i--) {
   
            // 在当前层向前移动,直到找到一个值大于或等于target的节点,或到达末尾
            while (current->forward[i] && current->forward[i]->value < target) {
   
                current = current->forward[i];
            }
        }
        
        // 到达最底层,检查下一个节点是否是要找的值
        current = current->forward[0];
        
        // 如果找到了目标值,返回true
        return current && current->value == target;
    }
    
    // 插入节点
    void insert(int value) {
   
        // 用于记录每一层的插入位置
        std::vector<Node*> update(maxLevel, nullptr);
        Node* current = head;
        
        // 从最高层开始查找插入位置
        for (int i = currentLevel - 1; i >= 0; i--) {
   
            while (current->forward[i] && current->forward[i]->value < value) {
   
                current = current->forward[i];
            }
            update[i] = current;
        }
        
        // 随机生成新节点的层数
        int newLevel = randomLevel();
        
        // 如果新层数大于当前层数,更新update数组
        if (newLevel > currentLevel) {
   
            for (int i = currentLevel; i < newLevel; i++) {
   
                update[i] = head;
            }
            currentLevel = newLevel;
        }
        
        // 创建新节点
        Node* newNode = new Node(value, newLevel);
        
        // 插入新节点到各层链表中
        for (int i = 0; i < newLevel; i++) {
   
            // 设置新节点的前向指针
            newNode->forward[i] = update[i]->forward[i];
            // 更新插入位置的前向指针指向新节点
            update[i]->forward[i] = newNode;
        }
    }
    
    // 删除节点
    bool remove(int value) {
   
        std::vector<Node*> update(maxLevel, nullptr);
        Node* current = head;
        
        // 从最高层开始查找要删除的节点
        for (int i = currentLevel - 1; i >= 0; i--) {
   
            while (current->forward[i] && current->forward[i]->value < value) {
   
                current = current->forward[i];
            }
            update[i] = current;
        }
        
        // 获取可能要删除的节点
        current = current->forward[0];
        
        // 如果节点存在且值匹配
        if (current && current->value == value) {
   
            // 从每一层中删除该节点
            for (int i = 0; i < currentLevel; i++) {
   
                if (update[i]->forward[i] != current) {
   
                    break;
                }
                update[i]->forward[i] = current->forward[i];
            }
            
            // 删除节点
            delete current;
            
            // 更新当前最大层数
            while (currentLevel > 1 && head->forward[currentLevel - 1] == nullptr) {
   
                currentLevel--;
            }
            
            return true;
        }
        
        return false;
    }
    
    // 打印跳表内容(用于调试)
    void printSkipList() {
   
        for (int i = currentLevel - 1; i >= 0; i--) {
   
            Node* current = head->forward[i];
            std::cout << "Level " << i << ": ";
            while (current) {
   
                std::cout << current->value << " ";
                current = current->forward[i];
            }
            std::cout << std::endl;
        }
    }
};

2.3 复杂度分析

操作 时间复杂度(平均) 时间复杂度(最坏)
搜索 O(log n) O(n)
插入 O(log n) O(n)
删除 O(log n) O(n)

空间复杂度:O(n),因为跳表平均会有1/(1-p)个指针,当p=0.5时为O(2n),简化为O(n)。

跳表的随机性使得其时间复杂度分析相对复杂,但在平均情况下,跳表与平衡二叉树有类似的性能,且实现更简单。

2.4 优化技术

  1. 调整概率参数:概率p影响层数分布,通常取0.5,可根据应用特点调整。

  2. 内存优化:使用变长数组存储前向指针,仅分配必要的空间。

  3. 跳表变种

    • 后向指针:支持双向遍历
    • 带间隔信息:支持快速排名查询
    • 确定性跳表:使用确定性算法代替随机化

2.5 应用场景

  1. 有序集合实现:Redis中的Sorted Set使用跳表实现。

  2. 索引结构:作为数据库中的索引结构,支持范围查询。

  3. 替代平衡树:在需要平衡树功能但实现难度要求低的场景。

  4. 并发环境:相比平衡树,跳表的并发控制更简单。

2.6 实际应用示例

范围查询实现
// 扩展跳表,增加范围查询功能
std::vector<int> rangeQuery(SkipList& list, int startValue, int endValue) {
   
    std::vector<int> result;
    
    // 使用底层链表(0层)进行范围查询
    Node* current = list.findNodeBeforeValue(startValue)->forward[0];
    
    // 收集范围内的所有值
    while (current && current->value <= endValue) {
   
        result.push_back(current->value);
        current = current->forward[0];
    }
    
    return result;
}
实现有序计数器
// 扩展跳表节点,增加计数信息
struct CountNode {
   
    int value;
    int count;  // 元素出现次数
    std::vector<CountNode*> forward;
    
    CountNode(int val, int level, int cnt = 1) : 
        value(val), count(cnt), forward(level, nullptr) {
   }
};

// 扩展跳表函数以处理计数
void incrementCount(int value) {
   
    // 寻找value对应的节点
    Node* current = findNodeBeforeValue(value)->forward[0];
    
    // 如果存在此值,增加计数
    if (current && current->value == value) {
   
        current->count++;
    } else {
   
        // 否则插入新节点
        insert(value);
    }
}

3. 布隆过滤器(Bloom Filter)

3.1 基本原理

布隆过滤器是一种空间效率很高的概率型数据结构,用于判断一个元素是否在集合中。它的特点是:

  • 可能会出现假阳性(误判元素存在),但绝不会出现假阴性(误判元素不存在)
  • 无法删除元素(标准布隆过滤器)
  • 占用空间极小,远小于存储元素本身所需空间

布隆过滤器的核心是一个位数组和多个哈希函数。添加元素时,用多个哈希函数计算位置并将对应位置为1;查询时,检查所有哈希位置是否都为1。

3.2 C++实现

#include <iostream>
#include <vector>
#include <functional>
#include <string>
#include <cmath>

class BloomFilter {
   
private:
    std::vector<bool> bitArray;  // 位数组
    int size;                    // 位数组大小
    

http://www.kler.cn/a/594049.html

相关文章:

  • Android Jetpack Compose介绍
  • RabbitMQ八股文
  • 【软考-架构】8.3、ES-OAS-ERP-电子政务-企业信息化
  • 【机器学习】核心概念
  • MCU-芯片时钟与总线和定时器关系,举例QSPI
  • C# 语法糖
  • 京东API数据清洗与结构化存储:从JSON原始数据到MySQL实战
  • 蓝桥杯之AT24C02的页写页读
  • 【OMCI实践】【案例分享】通过OLT升级ONT未自动重启问题分析
  • LeetCode 热题 100_跳跃游戏 II(79_45_中等_C++)(贪心算法)
  • LeeCode题库第五十八题
  • 信号的捕捉(操作部分)
  • C#从入门到精通(1)
  • yarn install 出现certificate has expired报错问题
  • 算法题(102):八皇后
  • centos7/8/9安装dockerdocker-compose
  • 车载以太网网络测试-17【传输层-TCP】
  • 理想发布的下一代自动驾驶架构MindVLA是什么?
  • 【HarmonyOS Next之旅】基于ArkTS开发(三) -> 兼容JS的类Web开发(七) -> JS动画(二)
  • 进程地址空间(上)【Linux】