【JavaScript数据结构与算法】图论基础

什么是图

  • 图结构是一种与树结构有些相似的数据结构。
  • 图论是数学的一个分支,并且,在数学的概念上,树是图的一种。
  • 它以图为研究对象,研究顶点和边组成的图形的数学理论和方法。
  • 主要研究的目的是事物之间的关系,顶点代表事物,边代表两个事物之间的关系

图的特点

  • 一组顶点:通常V(Vertex)表示顶点的集合
  • 一组边:通常用E(Edge)表示边的集合
    - 边是顶点与顶点之间的连线
    - 边可以有向的,也可以是无向的

在这里插入图片描述

欧拉七桥的问题

解决
奇点的数目不是0个就是2个
奇点,连接的边为奇数个

  相应的偶点,就是连接的边是偶数个的

图的术语

- 顶点 :表示图中的一个节点
- 边 : 表示顶点与顶点之间的连线
- 相邻顶点 : 表示一条边连接的两个顶点
- 顶点的度: 一个顶点连接的边的个数就是顶点的度
- 路径: 一个顶点到另一个顶点之间的经过的边,点顶就是其中的一个路径
- 回路:一个点沿着一条路径到达一个顶点,然后可以沿着相同的路径回到起点,这条路径就是回路
- 无向图:图中的边没有方向
- 有向图: 图中的边有方向
- 无权图:图中的边没有携带权重
- 有权图:图中的边携带了权重

图的表示

  • 邻接矩阵表示法
    让每一个节点和一个整数项关联,该整数作为数组的下标值,
    用一个二维数组表示顶点之间的连接

在这里插入图片描述

  • 邻接矩阵的问题
    - 如果图是一个稀疏图,那么矩阵中会有大量的0,这样属于是浪费了存储空间。

  • 邻接表表示法

    • 领接表由图中每个顶点以及和顶点相邻的顶点列表组成
    • 这个列表可以由 数组/链表/哈希表 存储

在这里插入图片描述

  • 邻接表的缺点
    • 计算出度比较容易,计算入度比较麻烦(出度:指向别人的边的数量,入度:指向自己的边的数量)
    • 它必须构建一个’逆邻接表“,才能有效的计算入度,但是开发中’入度’相对用的较少

图的遍历(每个顶点都要访问一遍,且不能出现重复访问)

  • 广度优先搜索(Breadth - first search)(bfc)

    先查找相邻顶点,查找完成后再查找相邻节点的相邻节点····

在这里插入图片描述

  • 深度优先搜索(Depth - first search)(dfs)

    先沿着一条路径查找到底部直到附近找不到已被探索标记的顶点,然后沿路径返回上一级顶点,再次查找有无未探索的相邻节点
    有,则继续探索,无,则继续返回,再重复查找操作,直到返回根顶点

在这里插入图片描述

探索过程:1 —— > 17 (其中:9,10,12,13,14,15,16,17是返回过程)

- 这两种遍历方法,都需要指定第一个遍历顶点

顶点的表示(设置访问状态)
- 灰色:表示该顶点被访问过了,但未被探索过。
- 白色:表示该顶点未被访问过。
- 黑色:表示该顶点被访问过且被探索过

bfs:基于队列,入队列的顶点先被探索

dfs:基于栈或者使用递归,通过将顶点存入栈,顶点是沿着路径被探索的,存在新的相邻顶点就去访问


图的实现

创建实例图

// 创建实现图
class MyGraph {
	// 顶点
    #vertices = [];
    // 边(通过map类型实现)
    #edEgs = new Map();
}

创建出来的顶点以数组的方式存储,创建的边以map类型的方式存储
当要添加顶点关系时,使用map的set方法添加顶点A 和接收与它相邻顶点的信息

如添加A节点与它相邻节点的信息:xxx.set(‘A’,[]);
数组收集所有相邻节点信息,有新的相邻节点则取出对应map中对应数组,然后添加(push)顶点信息


引入队列

因为广度优先搜索需要用到队列,先进先出,后进后出

class Queue {
    #arr = [];

    constructor() {
    }

    // 向数组后端添加数据
    enqueue() {
        for (const number of Array.prototype.slice.call(arguments)) {
            this.#arr.push(number);
        }
        return this.#arr;
    }

    // 向数组前端删除数据
    dequeue() {
        let lastNum = this.#arr[0];
        this.#arr.shift();
        return lastNum;
    }

    // 查看队列是否为空
    isEmpty() {
        return !this.#arr.length;
    }

    // 查看队列
    toString() {
        return this.#arr.join(' ');
    }

    // 查看队列元素个数
    size() {
        return this.#arr.length;
    }
}

添加顶点方法

// 添加顶点方法
    addVertices(v) {
        // 判断添加顶点已存在???
        if (this.#vertices.indexOf(v) !== -1) {
            throw new Error(`${v} existing!`);
        }

        // 添加顶点
        this.#vertices.push(v);
        // 添加顶点到map
        this.#edEgs.set(v, []);

    }

当添加顶点时,就在对应存储顶点信息的数组中添加顶点,同时在map(边)中添加对应顶点的相邻顶点信息

console.log('-----------添加顶点---------');
myGraph.addVertices('A');
myGraph.addVertices('B');
myGraph.addVertices('C');
myGraph.addVertices('D');
myGraph.addVertices('E');
myGraph.addVertices('F');
myGraph.addVertices('G');
myGraph.addVertices('H');
myGraph.addVertices('I');

添加边

// 添加边
    addEdEg(v1, v2) {
        // 判断越界
        this.#crossingTheLineJudge(v1, v2);

        this.#edEgs.get(v1).push(v2);
        this.#edEgs.get(v2).push(v1);
    }

向map中对应key值的value(数组)中添加相连节点信息,注意两个顶点的相邻顶点信息都要添加

console.log('-----------建立顶点之间的边---------');
myGraph.addEdEg('A', 'B');
myGraph.addEdEg('A', 'C');
myGraph.addEdEg('A', 'D');
myGraph.addEdEg('C', 'D');
myGraph.addEdEg('C', 'G');
myGraph.addEdEg('D', 'G');
myGraph.addEdEg('D', 'H');
myGraph.addEdEg('B', 'E');
myGraph.addEdEg('B', 'F');
myGraph.addEdEg('E', 'I');

结构

在这里插入图片描述


toString方法

 // 打印方法
    toString() {
        let str = '',
            i = 0,
            j;

		//循环打印顶点信息
        do {
            str += this.#vertices[i] + ' = >';

            j = 0;
			//取出对应相邻顶点列表
            let mapArray = this.#edEgs.get(this.#vertices[i]);
			
			//循环打印相邻顶点列表
            do {
                str += mapArray[j];
            } while (++j < mapArray.length);

            str += '\n';
        } while (++i < this.#vertices.length);

        return str;
    }

实现思路:先循环打印顶点信息数组中的顶点,然后嵌套循环打印对应顶点的相邻顶点信息

测试

console.log('-----------字符串打印输出---------');
let data = myGraph.toString();
console.log(data);

-----------字符串打印输出---------
A = >BCD
B = >AEF
C = >ADG
D = >ACGH
E = >BI
F = >B
G = >CD
H = >D
I = >E

图的遍历

广度优先搜索

  • 颜色初始化方法

    // 初始化颜色标记
    #initColor() {
        // 创建一个颜色标记数组
        let colors = [], i = 0;
        do {
            // 添加顶点默认标记
            colors[this.#vertices[i]] = 'white';
        } while (++i < this.#vertices.length)
        return colors;
    }
  • 搜索
// 广度优先搜索
    breadthFirstSearch(currentVer, callback) {
        // 判断越界
        this.#crossingTheLineJudge(currentVer);

        // 1 初始化颜色标记
        let verColor = this.#initColor();

        // 2 创建队列
        let myQueue = new Queue();

        // 3 入队列
        myQueue.enqueue(currentVer);

        // 4 循环出队列---当队列中无数据是终止循环
        do {
            // 5 出队列获得出队列元素值
            let toQueue = myQueue.dequeue();
            // 6 获取出队列元素的相邻顶点
            let adjacentVertices = this.#edEgs.get(toQueue);

            // 改变访问状态 - - 方便终止出队列循环
            verColor[toQueue] = 'gray';

            let i = 0;

            // 这个循环的作用是---获得此时出队列的顶点的相邻顶点
            do {
                // 判断顶点颜色状态是不是白色---未访问状态
                if (verColor[adjacentVertices[i]] === 'white') {

                    // 为访问状态则入队列 -- 并改变颜色状态
                    verColor[adjacentVertices[i]] = 'gray';
                    // 相邻顶点入队列
                    myQueue.enqueue(adjacentVertices[i]);
                }

            } while (++i < adjacentVertices.length)

            // 使用回调函数获取所遍历得的值 -- 访问   注意访问后颜色变为黑色
            callback(toQueue);
            verColor[toQueue] = 'black';
        } while (!myQueue.isEmpty())
    }

实现思路,从指定顶点开始遍历,将此顶点入队列,再出队列遍历顶点的相邻顶点信息,若相邻顶点未被访问(颜色值为白色)则入队列,然后继续出队列(循环实现,终止条件是队列为空时),然后再遍历这个顶点的相邻顶点信息,未访问就入队列,直到所有入队列的顶点出队列完,就停止循环。在此过程中将出队列的顶点打印。注意访问后颜色的改变

测试

console.log('-----------广度优先搜索---------');
// 第一个参数表示从那个顶点开始遍历,第二个参数用于接收遍历所拿到的顶点
let breadthFirstSearchData = [];
myGraph.breadthFirstSearch('D', (item) => {
    breadthFirstSearchData.push(item);
});

console.log(breadthFirstSearchData);

-----------广度优先搜索---------
[
  'D', 'A', 'C',
  'G', 'H', 'B',
  'E', 'F', 'I'
]

在这里插入图片描述


深度优先搜索

    // 深度优先搜素
    depthFirstSearch(currentVer, callback) {
        // 判断
        this.#crossingTheLineJudge(currentVer);
        // 初始化顶点
        let verColor = this.#initColor();
        // 调用递归方法
        this.#recursive(currentVer, verColor, callback);
    }

    // 深度搜索递归
    #recursive(current, colors, callback) {
        // 返回当前顶点
        callback(current);

        // 改变访问状态
        colors[current] = 'black';

        // 获取当前顶点的相邻顶点
        let ldin = this.#edEgs.get(current);

        let i = 0;

        // 循环遍历出当前顶点的相邻顶点
        do {
            // 相邻顶点未访问,则添加访问,并开始递归
            if (colors[ldin[i]] === 'white') {

                colors[ldin[i]] = 'gray';

                this.#recursive(ldin[i], colors, callback);
            }

        } while (++i < ldin.length)

    }

实现思路:递归访问
从初始顶点开始,先初始化访问状态,然后传入顶点信息,颜色信息,回调到递归方法
先打印访问到的顶点
然后改变访问状态为已打印
获得顶点的相邻顶点信息
遍历相邻顶点信息
若相邻顶点状态为未访问,则改变状态为访问未打印,再递归

测试

console.log('-----------深度优先搜索---------');
let depthFirstSearchData = [];
myGraph.depthFirstSearch('I', (item) => {
    depthFirstSearchData.push(item);
});
console.log(depthFirstSearchData);

-----------深度优先搜索---------
[
  'I', 'E', 'B',
  'A', 'C', 'D',
  'G', 'H', 'F'
]

在这里插入图片描述


越界判断

判断创建边广度搜索深度搜索时初始顶点没有的方法

  //特殊判断
    #crossingTheLineJudge() {
        [...arguments].forEach((item) => {

            let index = this.#vertices.indexOf(item);

            // 在遍历搜索中,起始顶点不存在则报错
            if (index === -1) {
                throw new Error(`Vertices don't exist is "${item}"`);
            }

        })
    }

全部代码

// import {Queue} from './队列/demo';

class Queue {
    #arr = [];

    constructor() {
    }

    // 向数组后端添加数据
    enqueue() {
        for (const number of Array.prototype.slice.call(arguments)) {
            this.#arr.push(number);
        }
        return this.#arr;
    }

    // 向数组前端删除数据
    dequeue() {
        let lastNum = this.#arr[0];
        this.#arr.shift();
        return lastNum;
    }

    // 查看队列是否为空
    isEmpty() {
        return !this.#arr.length;
    }

    // 查看队列
    toString() {
        return this.#arr.join(' ');
    }

    // 查看队列元素个数
    size() {
        return this.#arr.length;
    }
}

// 创建实现图
class MyGraph {
    // 顶点
    #vertices = [];
    // 边(通过map类型实现)
    #edEgs = new Map();

    // 添加顶点方法
    addVertices(v) {
        // 判断添加顶点已存在???
        if (this.#vertices.indexOf(v) !== -1) {
            throw new Error(`${v} existing!`);
        }

        // 添加顶点
        this.#vertices.push(v);
        // 添加顶点到map
        this.#edEgs.set(v, []);

    }

    // 添加边
    addEdEg(v1, v2) {
        // 判断
        this.#crossingTheLineJudge(v1, v2);

        this.#edEgs.get(v1).push(v2);
        this.#edEgs.get(v2).push(v1);
    }

    // 打印方法
    toString() {
        let str = '',
            i = 0,
            j;

        do {
            str += this.#vertices[i] + ' = >';

            j = 0;

            let mapArray = this.#edEgs.get(this.#vertices[i]);

            do {
                str += mapArray[j];
            } while (++j < mapArray.length);

            str += '\n';
        } while (++i < this.#vertices.length);

        return str;
    }

    // 初始化颜色标记
    #initColor() {
        // 创建一个颜色标记数组
        let colors = [], i = 0;
        do {
            // 添加顶点默认标记
            colors[this.#vertices[i]] = 'white';
        } while (++i < this.#vertices.length)
        return colors;
    }

    // 图的遍历
    // 广度优先搜索
    breadthFirstSearch(currentVer, callback) {
        // 判断
        this.#crossingTheLineJudge(currentVer);

        // 1 初始化颜色标记
        let verColor = this.#initColor();

        // 2 创建队列
        let myQueue = new Queue();

        // 3 入队列
        myQueue.enqueue(currentVer);

        // 4 循环出队列---当队列中无数据是终止循环
        do {
            // 5 出队列获得出队列元素值
            let toQueue = myQueue.dequeue();
            // 6 获取出队列元素的相邻顶点
            let adjacentVertices = this.#edEgs.get(toQueue);

            // 改变访问状态 - - 方便终止出队列循环
            verColor[toQueue] = 'gray';

            let i = 0;

            // 这个循环的作用是---获得此时出队列的顶点的相邻顶点
            do {
                // 判断顶点颜色状态是不是白色---未访问状态
                if (verColor[adjacentVertices[i]] === 'white') {

                    // 为访问状态则入队列 -- 并改变颜色状态
                    verColor[adjacentVertices[i]] = 'gray';
                    // 相邻顶点入队列
                    myQueue.enqueue(adjacentVertices[i]);
                }

            } while (++i < adjacentVertices.length)

            // 使用回调函数获取所遍历得的值 -- 访问   注意访问后颜色变为黑色
            callback(toQueue);
            verColor[toQueue] = 'black';
        } while (!myQueue.isEmpty())
        // console.log(verColor)

    }

    // 深度优先搜素
    depthFirstSearch(currentVer, callback) {
        // 判断
        this.#crossingTheLineJudge(currentVer);

        // 初始化顶点
        let verColor = this.#initColor();
        // 调用递归方法
        this.#recursive(currentVer, verColor, callback);
    }

    // 深度搜索递归
    #recursive(current, colors, callback) {
        // 返回当前顶点
        callback(current);

        // 改变访问状态
        colors[current] = 'black';

        // 获取当前顶点的相邻顶点
        let ldin = this.#edEgs.get(current);

        let i = 0;

        // 循环遍历出当前顶点的相邻顶点
        do {
            // 相邻顶点未访问,则添加访问,并开始递归
            if (colors[ldin[i]] === 'white') {

                colors[ldin[i]] = 'gray';

                this.#recursive(ldin[i], colors, callback);
            }

        } while (++i < ldin.length)

    }

    //特殊判断
    #crossingTheLineJudge() {
        [...arguments].forEach((item) => {

            let index = this.#vertices.indexOf(item);

            // 在遍历搜索中,起始顶点不存在则报错
            if (index === -1) {
                throw new Error(`Vertices don't exist is "${item}"`);
            }

        })
    }
}

测试


let myGraph = new MyGraph();
console.log('-----------添加顶点---------');
myGraph.addVertices('A');
myGraph.addVertices('B');
myGraph.addVertices('C');
myGraph.addVertices('D');
myGraph.addVertices('E');
myGraph.addVertices('F');
myGraph.addVertices('G');
myGraph.addVertices('H');
myGraph.addVertices('I');
console.log('-----------建立顶点之间的边---------');
myGraph.addEdEg('A', 'B');
myGraph.addEdEg('A', 'C');
myGraph.addEdEg('A', 'D');
myGraph.addEdEg('C', 'D');
myGraph.addEdEg('C', 'G');
myGraph.addEdEg('D', 'G');
myGraph.addEdEg('D', 'H');
myGraph.addEdEg('B', 'E');
myGraph.addEdEg('B', 'F');
myGraph.addEdEg('E', 'I');

console.log('-----------字符串打印输出---------');
let data = myGraph.toString();
console.log(data);

console.log('-----------广度优先搜索---------');
// 第一个参数表示从那个顶点开始遍历,第二个参数用于接收遍历所拿到的顶点
let breadthFirstSearchData = [];
myGraph.breadthFirstSearch('D', (item) => {
    breadthFirstSearchData.push(item);
});

console.log(breadthFirstSearchData);

console.log('-----------深度优先搜索---------');
let depthFirstSearchData = [];
myGraph.depthFirstSearch('I', (item) => {
    depthFirstSearchData.push(item);
});
console.log(depthFirstSearchData);

-----------添加顶点---------
-----------建立顶点之间的边---------
-----------字符串打印输出---------
A = >BCD
B = >AEF
C = >ADG
D = >ACGH
E = >BI
F = >B
G = >CD
H = >D
I = >E

-----------广度优先搜索---------
[
  'D', 'A', 'C',
  'G', 'H', 'B',
  'E', 'F', 'I'
]
-----------深度优先搜索---------
[
  'I', 'E', 'B',
  'A', 'C', 'D',
  'G', 'H', 'F'
]
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值