记录一下Compressed Sparse Row(CSR)压缩邻接表实现

无向图邻接表的压缩存储实现

无向图中每条边在普通邻接表存储时会被记录两次(A→B和B→A)。通过压缩存储,我们可以减少存储空间并提高效率。下面介绍一种称为CSR(Compressed Sparse Row)的压缩邻接表实现。

基本思路

CSR格式使用三个数组来表示图:

  1. edges:存储所有边的目标顶点
  2. indices:存储每个顶点的边起始位置
  3. degrees:存储每个顶点的度(可选)

代码实现

下面是一个完整的C++实现:

#include <vector>
#include <iostream>

class CompressedGraph {
private:
    int numVertices;                // 顶点数量
    std::vector<int> edges;         // 存储所有边的目标节点
    std::vector<int> indices;       // 每个顶点的边起始索引
    
public:
    // 构造函数
    CompressedGraph(int n) : numVertices(n) {
        indices.resize(n + 1, 0);  // n+1是因为需要一个额外元素表示结束位置
    }
    
    // 从边列表构建压缩邻接表
    void buildFromEdges(const std::vector<std::pair<int, int>>& edgeList) {
        // 计算每个顶点的度
        std::vector<int> degrees(numVertices, 0);
        for (const auto& edge : edgeList) {
            degrees[edge.first]++;
            if (edge.first != edge.second) // 排除自环
                degrees[edge.second]++;
        }
        
        // 计算索引数组
        indices[0] = 0;
        for (int i = 0; i < numVertices; i++) {
            indices[i + 1] = indices[i] + degrees[i];
        }
        
        // 重置度数组用于后续处理
        std::vector<int> currentDegrees(numVertices, 0);
        
        // 分配边数组空间
        edges.resize(indices[numVertices], 0);
        
        // 填充边数组
        for (const auto& edge : edgeList) {
            int u = edge.first;
            int v = edge.second;
            
            // 添加边 u -> v
            edges[indices[u] + currentDegrees[u]++] = v;
            
            // 添加边 v -> u (如果不是自环)
            if (u != v) {
                edges[indices[v] + currentDegrees[v]++] = u;
            }
        }
    }
    
    // 检查两个顶点之间是否有边
    bool hasEdge(int u, int v) const {
        for (int i = indices[u]; i < indices[u + 1]; i++) {
            if (edges[i] == v) return true;
        }
        return false;
    }
    
    // 获取顶点的所有邻居
    std::vector<int> getNeighbors(int vertex) const {
        std::vector<int> neighbors;
        for (int i = indices[vertex]; i < indices[vertex + 1]; i++) {
            neighbors.push_back(edges[i]);
        }
        return neighbors;
    }
    
    // 打印图的结构
    void print() const {
        for (int i = 0; i < numVertices; i++) {
            std::cout << "顶点 " << i << " 的邻居: ";
            for (int j = indices[i]; j < indices[i + 1]; j++) {
                std::cout << edges[j] << " ";
            }
            std::cout << std::endl;
        }
    }
    
    // 获取图的边数
    int getEdgeCount() const {
        return edges.size() / 2;  // 每条边存两次,所以除以2
    }
};

使用示例

#include "CompressedGraph.h"
#include <iostream>

int main() {
    // 创建一个有5个顶点的图
    CompressedGraph graph(5);
    
    // 定义边列表 (假设顶点编号从0开始)
    std::vector<std::pair<int, int>> edges = {
        {0, 1}, {0, 3}, {1, 2}, {1, 3}, {2, 4}, {3, 4}
    };
    
    // 构建图
    graph.buildFromEdges(edges);
    
    // 打印图结构
    std::cout << "图的压缩邻接表表示:\n";
    graph.print();
    
    // 测试边查询
    std::cout << "\n顶点0和1之间是否有边: " << (graph.hasEdge(0, 1) ? "是" : "否") << std::endl;
    std::cout << "顶点0和2之间是否有边: " << (graph.hasEdge(0, 2) ? "是" : "否") << std::endl;
    
    // 获取顶点的邻居
    std::cout << "\n顶点1的所有邻居: ";
    auto neighbors = graph.getNeighbors(1);
    for (int neighbor : neighbors) {
        std::cout << neighbor << " ";
    }
    std::cout << std::endl;
    
    // 显示边数
    std::cout << "\n图中边的数量: " << graph.getEdgeCount() << std::endl;
    
    return 0;
}

工作原理说明

  1. 空间优化:虽然我们仍然存储每条边两次,但数据结构更紧凑,没有链表节点的开销

  2. indices数组:提供了O(1)时间复杂度访问一个顶点的邻居列表

    • indices[i]:表示顶点i的第一个邻居在edges数组中的位置
    • indices[i+1]-1:表示顶点i的最后一个邻居在edges数组中的位置
  3. 图示例

    对于图:0 -- 1 -- 2
           |     |
           3 -- 4
    

    压缩存储为:

    • edges = [1,3, 0,2,3, 1,4, 0,1,4, 2,3]
    • indices = [0, 2, 5, 7, 10, 12]

    表示:

    • 顶点0的邻居:edges[0]到edges[1],即1和3
    • 顶点1的邻居:edges[2]到edges[4],即0,2,3
    • 依此类推...

这种实现方式特别适用于大型稀疏图,提供了高效的存储和查询操作。

posted @ 2025-04-17 17:05  十八Eigh18n  阅读(93)  评论(0)    收藏  举报