前端小白初识`图`结构

71 阅读6分钟

不怕诸位笑话,本人从业前段4年数据结构和算法一直是我的软肋。这些知识在我脑子里几乎等同于空白,今天就来由浅入深的补习下这些姿势。首先来说下结构。

在此之前也你更应该先去了解下数组,链表,树。相对于这些结构会比较复杂一些。后续我也会记录我对数组,链表,树的学习笔记。

什么是图?

这个东西比较抽象,一两句话不一定能解释的明白。下面我会用一个简单的例子来解释什么是。下面是网上的一些解释。

图结构是一种用于表示对象之间关系的数据结构。它由一组节点(也称为顶点)和一组边组成,边连接节点表示节点之间的关系。图结构可以用来解决许多实际问题,如社交网络分析、路线规划、组织架构等

大概总结为:图是用来表示节点关系的一种数据结构。

图的最小生成树

举个例子:有A,B,C,D,E,F四个村庄,由于经费有限需要设计一个修路的方案,这条路既可以连通所有村庄而且成本是最低的。各个村庄之间的距离如下图:

image.png 下面分别采用加点法和加边法来解决这个问题

普里姆算法(加点法)

首先随便找一个村庄A作为起点,然后寻找和他距离最近的村庄B。将最近的这个村庄B和自己相连接。那么现在我就已经有两个村庄AB被连接,然后继续寻找距离AB最近的村庄并连接。直到所有的村庄都被连接,那么任务就完成了。

下面用代码来实现:

创建村庄节点

如果AB两个村庄被连接了,难么AB互为邻居关系,会被添加到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];

村庄间的距离

各个村庄的距离如下表

村庄ABCDE
A04799
B40869
C78069
D96507
E99970

我们可以用二维数组来表示各个村庄的距离

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;
}

克鲁斯卡尔算法(加边法)

同加点法一样我们也需要一个变量来保存已经被连接过的村庄,不同的是加边法可能会出现两个部落的情况,例如下图。

image.png 有可能出现A,B,C,D成一个部落,E,F,G,H成一个部落。所以我们需要用一个二维数组hasConnectedPointGroup来存放这些部落。然后我们要将这个两个部落合并为一个部落,如果这个部落包含了所有的村庄那么任务完成。

function kruskal(pointSet, distance){
   // 被链接点的部落组
   const hasConnectedPointGroup = [];
}

寻找可以连接的起点和终点

依次遍历所有村庄,寻找可以连接起点和终点,寻找距离最短且满足以下条件的村庄作可以连接。

  1. A和B都不在部落内
  2. A在部落内B不在部落内
  3. B在部落内C不在部落内
  4. 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;
}

连接起点和终点

在连接两个村庄时我们需要对不同条件的村庄做出不同的处理

  1. A和B都不在部落内时,新增一个部落将A和B放入
  2. A在部落内B不在部落内时,将B放入A所在的部落里
  3. B在部落内C不在部落内时,将A放入B所在的部落里
  4. 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;
    }
  }
}