前端必会算法 - 最小生成树问题

270 阅读3分钟

最小生成树问题

普利姆算法(加点法)

  1. 任选一个点为起点
  2. 找到以当前点为起点的路径最短的边
  3. 如果这个边的另一端没有被连接起来,则连接
  4. 如果这个边的另一端被连接起来了,则看倒数第二短的边
  5. 重复2-4

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

  1. 先找到最短的边,连接两端的点
  2. 找到第二短的边连接两端的点,要求这条边的两端至少有一个未被连接的点
  3. 或者这个边是将两个部落进行连接
  4. 重复1-3
  • 先来一张图

    拓扑图

  • 将这张图翻译成代码

A B C D E
A 0 4 7 MAX MAX
B 4 0 8 6 MAX
C 7 8 0 5 MAX
D MAX 6 5 0 7
E MAX MAX MAX 7 0
var max = Number.MAX_SAFE_INTEGER;
var distance = [
    [0, 4, 7, max, max],
    [4, 0, 8, 6, max],
    [7, 8, 0, 5, max],
    [max, 6, 5, 0, 7],
    [max, max, max, 7, 0]
];
// 这是class, ts编译之后生成的样子,不用在意
var DisNode = /** @class */ (function () {
    function DisNode(val) {
        this.value = null;
        this.neighbor = [];
        this.value = val;
    }
    return DisNode;
}());
var A = new DisNode('A');
var B = new DisNode('B');
var C = new DisNode('C');
var D = new DisNode('D');
var E = new DisNode('E');
var pointSet = [];
pointSet.push(A, B, C, D, E);

普利姆算法

function getIndex(str) {
    for (var i = 0; i < pointSet.length; i++) {
        if (str == pointSet[i].value)
            return i;
    }
    return -1;
}
/**
 * 得到一个当前最小距离的点
 * @param {*} pointSet 点的集合
 * @param {*} distance 边的集合
 * @param {*} nowPointSet 当前已经连接的点的集合
 */
function getMinDisNode(pointSet, distance, nowPointSet) {
    var formNode = null; //线段的起点
    var minDisNode = null; // 线段的终点
    var minDis = max;
    // 根据当前已有的这些点为起点,依次判断连接其他的点的距离是多少
    for (var i = 0; i < nowPointSet.length; i++) {
        // 获取当前节点的序号
        var nowPointIdx = getIndex(nowPointSet[i].value);
        for (var j = 0; j < distance[nowPointIdx].length; j++) {
            var thisNode = pointSet[j];
            if (nowPointSet.indexOf(thisNode) < 0 && distance[nowPointIdx][j] < minDis) {
                formNode = nowPointSet[i];
                minDisNode = thisNode;
                minDis = distance[nowPointIdx][j];
            }
        }
    }
    formNode.neighbor.push(minDisNode);
    minDisNode.neighbor.push(formNode);
    return minDisNode;
}
/**
 * 普利姆算法
 * @param {*} pointSet 点集合
 * @param {*} distance 点之间的距离集合
 * @param {*} start 开始点
 */
function prim(pointSet, distance, start) {
    var nowPointSet = []; // 现在已经连接的点
    nowPointSet.push(start);
    while (true) {
        var minDisNode = getMinDisNode(pointSet, distance, nowPointSet); // 获取最短距离的点
        nowPointSet.push(minDisNode);
        if (nowPointSet.length == pointSet.length)
            break;
    }
}
prim(pointSet, distance, pointSet[2]);

克鲁斯卡尔算法

function canLink(resultList, startPoint, endPoint) {
    var beginIn = null,
        endIn = null;
    for (var i = 0; i < resultList.length; i++) {
        if (resultList[i].indexOf(startPoint) != -1) {
            beginIn = resultList[i]
        }
        if (resultList[i].indexOf(endPoint) != -1) {
            endIn = resultList[i]
        }
    }
    // 如果开始点和结束点在同一个部落数组中,则不能连接,因为已经连接了
    if (beginIn != null && endIn != null && beginIn == endIn) {
        return false
    }
    return true
}

function link(resultList, startPoint, endPoint) {
    var beginIn = null,
        endIn = null;
    for (var i = 0; i < resultList.length; i++) {
        if (resultList[i].indexOf(startPoint) != -1) {
            beginIn = resultList[i]
        }
        if (resultList[i].indexOf(endPoint) != -1) {
            endIn = resultList[i]
        }
    }
    if (beginIn == null && endIn != null) {
        // 将开始点 接入 结束点的部落数组中
        endIn.push(startPoint)
    } else if (beginIn != null && endIn == null) {
        // 将结束点 接入 开始的部落数组中
        beginIn.push(endPoint)
    } else if (beginIn == null && endIn == null) {
        // 将两个点相连
        resultList.push([startPoint, endPoint])
    } else if (beginIn != null && endIn != null && beginIn != endIn) {
        // 两个部落连接
        var allIn = beginIn.concat(endIn)
        resultList.splice(resultList.indexOf(beginIn), 1)
        resultList.splice(resultList.indexOf(endIn), 1)
        resultList.push(allIn)
    }
    startPoint.neighbor.push(endPoint)
    endPoint.neighbor.push(startPoint)
}

/**
 * 克鲁斯科尔算法
 * @param {*} pointSet 点集合
 * @param {*} distance 点距离集合
 */
function kruskal(pointSet, distance) {
    var resultList = [];
    while (true) {
        var minDis = max,
            startPoint = null,
            endPoint = null;
        for (var i = 0; i < pointSet.length; i++) {
            for (var j = 0; j < distance[i].length; j++) {
                var tempStart = pointSet[i],
                    tempEnd = pointSet[j];
                if (
                    i != j &&
                    distance[i][j] < minDis &&
                    canLink(resultList, tempStart, tempEnd)
                ) {
                    startPoint = tempStart
                    endPoint = tempEnd
                    minDis = distance[i][j]
                }
            }
        }
        link(resultList, startPoint, endPoint)
        if (resultList.length == 1 && resultList[0].length == pointSet.length) {
            break
        }
    }
}
kruskal(pointSet, distance)

写在结尾

克鲁斯卡尔算法和普利姆算法都还是比较复杂的算法,我这里实现的方法,循环次数有些多,希望看到的大佬能给予指正。

其实在前端面试中,这两种算法一般不会考代码实现,一般来说会是一道填空题,写出连接顺序。