不怕诸位笑话,本人从业前段4年数据结构和算法一直是我的软肋。这些知识在我脑子里几乎等同于空白,今天就来由浅入深的补习下这些姿势。首先来说下图
结构。
在此之前也你更应该先去了解下数组,链表,树。相对于这些结构图
会比较复杂一些。后续我也会记录我对数组,链表,树的学习笔记。
什么是图?
这个东西比较抽象,一两句话不一定能解释的明白。下面我会用一个简单的例子来解释什么是图
。下面是网上的一些解释。
图结构是一种用于表示对象之间关系的数据结构。它由一组节点(也称为顶点)和一组边组成,边连接节点表示节点之间的关系。图结构可以用来解决许多实际问题,如社交网络分析、路线规划、组织架构等
大概总结为:图是用来表示节点关系的一种数据结构。
图的最小生成树
举个例子:有A,B,C,D,E,F四个村庄,由于经费有限需要设计一个修路的方案,这条路既可以连通所有村庄而且成本是最低的。各个村庄之间的距离如下图:
下面分别采用加点法和加边法来解决这个问题
普里姆算法(加点法)
首先随便找一个村庄A
作为起点,然后寻找和他距离最近的村庄B
。将最近的这个村庄B
和自己相连接。那么现在我就已经有两个村庄A
和B
被连接,然后继续寻找距离A
和B
最近的村庄并连接。直到所有的村庄都被连接,那么任务就完成了。
下面用代码来实现:
创建村庄节点
如果A
和B
两个村庄被连接了,难么A
和B
互为邻居关系,会被添加到neighbors
中
function MyNode(value) {
this.value = value;
this.neighbors = [];
}
const a = new MyNode('A'),
b = new MyNode('B'),
c = new MyNode('C'),
d = new MyNode('D'),
e = new MyNode('E');
// 所有的村庄
const pointSet = [a, b, c, d, e];
村庄间的距离
各个村庄的距离如下表
村庄 | A | B | C | D | E |
---|---|---|---|---|---|
A | 0 | 4 | 7 | 9 | 9 |
B | 4 | 0 | 8 | 6 | 9 |
C | 7 | 8 | 0 | 6 | 9 |
D | 9 | 6 | 5 | 0 | 7 |
E | 9 | 9 | 9 | 7 | 0 |
我们可以用二维数组来表示各个村庄的距离
const distance = [
[0, 4, 7, 9, 9], // a点距离各个点(a,b,c,d,e)的距离,
[4, 0, 8, 6, 9],
[7, 8, 0, 6, 9],
[9, 6, 6, 0, 7],
[9, 9, 9, 7, 0],
];
递归遍历每个村庄
我们需要一个变量来保存已经被连接过的村庄,如果所有的村庄都被连接了那么跳出递归。
/**
* 加点法(普利姆算法)
* @param {MyNode[]} pointSet
* @param {number[][]} distance
* @param {MyNode} startPoint
*/
function prim(pointSet, distance, startPoint) {
// 已经被链接的点
const hasConnectedPointSet = [startPoint];
// 当所有的点都已经被链接时, 不需要再做任何操作
while(hasConnectedPointSet.length < pointSet.length) {
const minDistancePoint = getMinDisPiont(pointSet, distance, hasConnectedPointSet);
hasConnectedPointSet.push(minDistancePoint);
}
}
获取最小距离的村庄
我们需要从已经被连接的村庄中选取一个作为起点,距离起点最短距离(而且这个村庄没有被连接过)的村庄,作为终点。然后连接起点和终点(让他们相互称为邻居关系)。最后把终点添加到保存已经被连接过的村庄的变量里,防止重复的连接。
/**
* 获取最小的距离
* @param {MyNode[]} pointSet
* @param {number[][]} distance
* @param {MyNode[]} hasConnectedPointSet
*/
function getMinDisPiont(pointSet, distance, hasConnectedPointSet) {
/**
* 起始点
* @type {MyNode|null}
*/
let startPoint = null;
/**
* 结束点
* @description 距离{startPoint}最短的点
* @type {MyNode|null}
*/
let endPoint = null;
/**
* 记录最短距离
*/
let minDis = 9;
for (let index = 0; index < hasConnectedPointSet.length; index++) {
const curConnectedPoint = hasConnectedPointSet[index];
const curConnectedPointIdxInPointSet = pointSet.indexOf(curConnectedPoint);
for (let idx = 0; idx < distance.length; idx++) {
const curPoint = pointSet[idx];
// 当前点距离已经被链接的点的距离
const curPointFromCurConnectedPointDis = distance[curConnectedPointIdxInPointSet][idx];
if (
// 该点并未被链接, 已经连接过的还链接个毛线
!hasConnectedPointSet.includes(curPoint) &&
// 并且当前点的距离小于最小距离
curPointFromCurConnectedPointDis < minDis
) {
startPoint = curConnectedPoint;
endPoint = curPoint;
minDis = curPointFromCurConnectedPointDis;
}
}
}
if(startPoint && endPoint) {
// 链接这两个点
startPoint.neighbors.push(endPoint);
endPoint.neighbors.push(startPoint);
}
return endPoint;
}
克鲁斯卡尔算法(加边法)
同加点法一样我们也需要一个变量来保存已经被连接过的村庄,不同的是加边法可能会出现两个部落的情况,例如下图。
有可能出现A,B,C,D成一个部落,E,F,G,H成一个部落。所以我们需要用一个二维数组
hasConnectedPointGroup
来存放这些部落。然后我们要将这个两个部落合并为一个部落,如果这个部落包含了所有的村庄那么任务完成。
function kruskal(pointSet, distance){
// 被链接点的部落组
const hasConnectedPointGroup = [];
}
寻找可以连接的起点和终点
依次遍历所有村庄,寻找可以连接起点和终点,寻找距离最短且满足以下条件的村庄作可以连接。
- A和B都不在部落内
- A在部落内B不在部落内
- B在部落内C不在部落内
- A和B都不在同一个部落内
function canConnect(startPoint, endPoint, pointGroup) {
let startGroup = null;
let endGroup = null;
for (let idx = 0; idx < pointGroup.length; idx++) {
if (pointGroup[idx].includes(startPoint)) {
startGroup = pointGroup[idx];
}
if (pointGroup[idx].includes(endPoint)) {
endGroup = pointGroup[idx];
}
}
if (startGroup === null && endGroup === null) {
return true;
}
if (
(startGroup === null && endGroup !== null) ||
(endGroup === null && startGroup !== null)
) {
return true;
}
if (startGroup !== endGroup) {
return true;
}
return false;
}
连接起点和终点
在连接两个村庄时我们需要对不同条件的村庄做出不同的处理
- A和B都不在部落内时,新增一个部落将A和B放入
- A在部落内B不在部落内时,将B放入A所在的部落里
- B在部落内C不在部落内时,将A放入B所在的部落里
- A和B都不在同一个部落内时,合并两个部落
最后将A和B互相成为邻居关系
function connect(startPoint, endPoint, pointGroup) {
let startGroup = null;
let endGroup = null;
for (let idx = 0; idx < pointGroup.length; idx++) {
if (pointGroup[idx].includes(startPoint)) {
startGroup = pointGroup[idx];
}
if (pointGroup[idx].includes(endPoint)) {
endGroup = pointGroup[idx];
}
}
if (startGroup === null && endGroup === null) {
pointGroup.push([
startPoint,
endPoint
])
} else if (startGroup !== null && endGroup === null) {
startGroup.push(endPoint)
} else if(endGroup !== null && startGroup === null) {
endGroup.push(startPoint)
} else if (startGroup !== endGroup) {
// 在本例中并不会出现这种情况 因为点的链接是依次进行的
pointGroup.splice(pointGroup.indexOf(startGroup), 1);
pointGroup.splice(pointGroup.indexOf(endGroup), 1);
console.log('impossible')
pointGroup.push([
...startGroup,
...endGroup
]);
}
startPoint.neighbors.push(endPoint);
endPoint.neighbors.push(startPoint);
}
递归链接村庄
function kruskal(pointSet, distance) {
const hasConnectedPointGroup = [];
while(true) {
let minDis = n;
let beginPoint = null;
let endPoint = null;
for (let rowIdx = 0; rowIdx < distance.length; rowIdx++) {
const rowlDisses = distance[rowIdx];
for (let colIdx = 0; colIdx < rowlDisses.length; colIdx++) {
const colDis = rowlDisses[colIdx];
if (
rowIdx !== colIdx
&& colDis < minDis
&& canConnect(pointSet[rowIdx], pointSet[colIdx], hasConnectedPointGroup)
) {
beginPoint = pointSet[rowIdx];
endPoint = pointSet[colIdx];
minDis = colDis;
}
}
}
connect(beginPoint, endPoint, hasConnectedPointGroup)
if (hasConnectedPointGroup.length === 1 && hasConnectedPointGroup[0].length === pointSet.length) {
break;
}
}
}