【用TypeScript实现内存型图数据库】0x01:使用邻接表构建图并实现搜索

概述

本系列最终目的是按照Dagoba: An In-Memory Graph Database教程实现一个内存型图数据库,故本文先从最基本的图的输入、构建、搜索开始。
因为“根据输入构建图”和“在图中搜索”是图数据库的核心功能之二。

程序功能描述

功能列表

  1. 根据输入的边列表构建图
  2. 输入起始顶点和目标顶点,搜索所有连通的路径

输入格式

边列表

const edges: number[][] = [[1, 2], [1, 3], [2, 4], [2, 5], [3, 6], [3, 7], [4, 8]
    , [4, 9], [5, 10], [5, 11], [6, 12], [6, 13], [7, 14], [7, 15]];

构建图

图有很多种表示方式,其中邻接矩阵通常比较稀疏,浪费空间,所以更紧凑的邻接列表是更实用的表示方式。
由于用List结构实现邻接列表的话,检索第一层每个顶点的时间复杂度是O(n);但是如果用Map结构来实现邻接列表的话,检索第一层顶点的时间复杂度就降到了O(1)。

/**
 * 简单的图
 */
class SimpleGraph {
    /**
     * 边列表
     */
    private edges: number[][];
    /**
     * 邻接表
     * 这里为了简化操作,将原始邻接表的list数据结构换成了Map数据结构
     */
    private adjacencyMap: Map<number, number[]> = new Map();

    /**
     * 构造器
     * @param edges 边列表
     */
    constructor(edges: number[][]) {
        this.edges = edges;
        this.init();
    }

    /**
     * 初始化
     */
    init() {
        this.edges.forEach(edge => {
            // 起始顶点
            const from: number = edge[0];
            // 目标顶点
            const to: number = edge[1];
            // 如果该起始顶点邻接顶点数组未初始化则先初始化
            if (!this.adjacencyMap.has(from)) {
                this.adjacencyMap.set(from, []);
            }
            // 将连通顶点放入邻接数组
            this.adjacencyMap.get(from).push(to);
        });
    }
}

使用序列化语句输出this.adjacencyMap的结果:

console.log(JSON.stringify(Array.from(this.adjacencyMap.entries())));
// [[1,[2,3]],[2,[4,5]],[3,[6,7]],[4,[8,9]],[5,[10,11]],[6,[12,13]],[7,[14,15]]]

搜索图

图最常见的功能之一就是搜索两点之间可达的所有路径,下面就用广度优先搜索和深度优先搜索分别实现一次。

广度优先搜索

代码实现

    /**
     * 广度优先搜索
     * @param from 起始顶点 
     * @param to 目标顶点
     * @returns 从起始节点出发所有可达目标节点路径
     */
    breadthFirstSearch(from: number, to: number): number[][] {
        // 结果路径列表
        const resultPaths: number[][] = [];
        // 待遍历的路径列表,初始化时将起始节点作为第一个路径
        const toVisitPaths: number[][] = [[from]];
        // 当还有路径需要遍历时
        while (toVisitPaths.length > 0) {
            // 当前路径,从待遍历路径队列中拉出一条路径
            const curPath: number[] = toVisitPaths.shift() || [];
            // 获取该路径中最近一个遍历的节点
            const latestNode = curPath[curPath.length - 1];
            // 获取该节点可达的节点
            const nextNodes = this.adjacencyMap.get(latestNode) || []
            for (let nextNode of nextNodes) {
                // 如果当前路径已遍历过该节点,则跳过,避免环
                if (curPath.indexOf(nextNode) >= 0) {
                    continue;
                }
                // 将下一个节点压入路径,以作为下一个要遍历的节点
                curPath.push(nextNode);
                if (nextNode === to) {
                    // 如果找到了目标节点,则深拷贝路径到结果路径中,并不再加入待遍历路径列表
                    resultPaths.push(curPath.slice());
                } else {
                    // 如果还不是目标节点,则添加到待遍历路径列表
                    toVisitPaths.push(curPath.slice());
                }
                // 弹出刚刚的节点以还原路径
                curPath.pop();
            }
        }
        return resultPaths;
    }

运行结果

运行代码

// 边列表
const edges: number[][] = [[1, 2], [1, 3], [2, 4], [2, 5], [3, 6], [3, 7], [4, 8]
    , [4, 9], [5, 10], [5, 11], [6, 12], [6, 13], [7, 14], [7, 15]];
// 构建图
const simpleGraph: SimpleGraph = new SimpleGraph(edges);
// 在图中搜索路径
console.log(JSON.stringify(simpleGraph.breadthFirstSearch(1, 15)));
// 遍历顺序: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 
// 输出结果: [[1,3,7,15]]

深度优先搜索

代码实现

    /**
     * 深度优先搜索
     * @param from 起始节点
     * @param to 目标节点
     * @returns 从起始节点出发所有可达目标节点路径
     */
    depthFirstSearch(from: number, to: number): number[][] {
        // 结果路径列表
        const resultPaths: number[][] = [];
        // 待遍历的路径
        const toVisitPaths: number[][] = [[from]];
        while (toVisitPaths.length > 0) {
            // 从栈中弹出最近队列作为当前路径
            const curPath: number[] = toVisitPaths.pop() || [];
            // 最近一个遍历的节点
            const latestNode = curPath[curPath.length - 1];
            // 获取该节点可达的节点
            const nextNodes = this.adjacencyMap.get(latestNode) || [];
            for (let nextNode of nextNodes) {
                // 如果当前路径已遍历过该节点,则跳过,避免环
                if (curPath.indexOf(nextNode) >= 0) {
                    continue;
                }
                // 将下一个节点压入路径,以作为下一个要遍历的节点
                curPath.push(nextNode);
                if (nextNode === to) {
                    // 如果找到了目标节点,则深拷贝路径到结果路径中,并不再加入待遍历路径列表
                    resultPaths.push(curPath.slice());
                } else {
                    // 如果还不是目标节点,则添加到待遍历路径列表
                    toVisitPaths.push(curPath.slice());
                }
                // 弹出刚刚的节点以还原路径
                curPath.pop();
            }
        }
        return resultPaths;
    }

运行结果

// 边列表
const edges: number[][] = [[1, 2], [1, 3], [2, 4], [2, 5], [3, 6], [3, 7], [4, 8]
    , [4, 9], [5, 10], [5, 11], [6, 12], [6, 13], [7, 14], [7, 15]];
// 构建图
const simpleGraph: SimpleGraph = new SimpleGraph(edges);
// 在图中搜索路径
console.log(JSON.stringify(simpleGraph.depthFirstSearch(1, 15)));
// 遍历顺序:1 2 3 6 7 14 15 12 13 4 5 10 11 8 9 
// 输出结果:[[1,3,7,15]]

小结

可以看到深搜和广搜的区别就仅仅在于用“待遍历的路径”toVisitPaths获取下一个遍历路径curPath的方式

  • 广度优先搜索用的是shift(),即toVisitPaths当做队列,先进先出。
  • 深度优先搜索用的是pop(),即toVisitPaths当做栈,后进先出。

完整代码见文章顶部CSDN免费附件

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值