前端必會演算法 - 最小生成樹問題

JsRicardo發表於2019-12-04

最小生成樹問題

普利姆演算法(加點法)

  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)
複製程式碼

寫在結尾

克魯斯卡爾演算法和普利姆演算法都還是比較複雜的演算法,我這裡實現的方法,迴圈次數有些多,希望看到的大佬能給予指正。

其實在前端面試中,這兩種演算法一般不會考程式碼實現,一般來說會是一道填空題,寫出連線順序。

相關文章