JavaScript實現《啊哈!演算法》中的系列演算法

星夢花隨0823發表於2018-11-26

《啊哈!演算法》pdf版 下載

第1章 一大波數正在靠近——排序

第1節 最快最簡單的排序——桶排序.

var testArr = [3, 5, 3, 5, 9, 7, 6]
// 桶排序---浪費空間,且只能比較自然數,時間複雜度是 O(m+n)
// 需要對資料範圍在 0~n 之間的整數進行排序
// skipArr 需要排序的資料組成的陣列
function skipSort (n, skipArr) {
  var arr = []
  var sortArr = []
  arr.length = n
  for (let i = 0; i <= n; i++) {
    arr[i] = 0
  }
  for (let j = 0; j < skipArr.length; j++) {
    arr[skipArr[j]]++
  }
  // 從小到大的排序
  // for (let t = 0; t <= n; t++) {
  //   if (arr[t]) {
  //     for (let k = 1; k <= arr[t]; k++) {
  //       sortArr.push(t)
  //     }
  //   }
  // }
  // 從大到小的排序
  for (let t = n; t > 0; t--) {
    if (arr[t]) {
      for (k = 1; k <= arr[t]; k++) {
        sortArr.push(t)
      }
    }
  }
  return sortArr
}

console.log('桶排序:', skipSort(10, testArr))
複製程式碼

第2節 鄰居好說話——氣泡排序

var testArr = [3, 5, 3, 5, 9, 7, 6]
// 氣泡排序---時間複雜度是O(N的2次方),執行效率底
// bubbleArr 需要排序的資料組成的陣列
function bubbleSort (bubbleArr) {
  var i, j, temp
  for (i = 0; i < bubbleArr.length; i++) {
    for (j = 0; j < bubbleArr.length - 1 - i; j++) {
      // 從小到大的排序
      // if (bubbleArr[j] > bubbleArr[j +1]) {
      //   temp = bubbleArr[j]
      //   bubbleArr[j] = bubbleArr[j + 1]
      //   bubbleArr[j + 1] = temp
      // }
      // 從大到小的排序
      if (bubbleArr[j] < bubbleArr[j +1]) {
        temp = bubbleArr[j]
        bubbleArr[j] = bubbleArr[j + 1]
        bubbleArr[j + 1] = temp
      }
    }
  }
  return bubbleArr
}

console.log('氣泡排序:', bubbleSort(testArr))
複製程式碼

第3節 最常用的排序——快速排序

// 快速排序---時間複雜度最差是O(N的2次方),平均時間複雜度為O(NlogN)
// quickArr 需要排序的資料組成的陣列
function quickSort (quickArr, left, right) {
  var i, j, k, temp
  if (left > right) {
    return
  }
  temp = quickArr[left]
  i = left
  j = right
  while (i !== j) {
    // 從小到大的排序
    // while (quickArr[j] >= temp && i < j) {
    //   j--
    // }
    // while (quickArr[i] <= temp && i < j) {
    //   i++
    // }
    // 從大到小的排序
    while (quickArr[j] <= temp && i < j) {
      j--
    }
    while (quickArr[i] >= temp && i < j) {
      i++
    }
    if (i < j) {
      t = quickArr[i]
      quickArr[i] = quickArr[j]
      quickArr[j] = t
    }
  }
  quickArr[left] = quickArr[i]
  quickArr[i] = temp
  quickSort(quickArr, left, i - 1)
  quickSort(quickArr, i + 1, right)
  return quickArr
}

console.log('快速排序:', quickSort(testArr, 0, testArr.length - 1))
複製程式碼

第2章 棧、佇列、連結串列

第1節 解密 QQ號——佇列

規則是這樣的:首先將第 1個數刪除,緊接著將第 2 個數放到 這串數的末尾,再將第 3 個數刪除並將第 4 個數放到這串數的末尾,再將第 5 個數刪除…… 直到剩下最後一個數,將最後一個數也刪除。按照剛才刪除的順序,把這些刪除的數連在一 起就是真實的QQ啦。

// 佇列--先進先出的資料結構

// 加密後的QQ字串是 6 3 1 7 5 8 9 2 4
const arrAfter = [6, 3, 1, 7, 5, 8, 9, 2, 4];

function getQQ(arrAfter) {
  let middleArr = Object.assign([], arrAfter);
  middleArr.unshift(0);
  let arrBefore = [];
  let head = 1;
  let tail = middleArr.length;
  while (head < tail) {
    arrBefore.push(middleArr[head]);
    head++;
    middleArr.push(middleArr[head]);
    tail++;
    head++;
  }
  return arrBefore;
}

console.log("getQQ:", getQQ(arrAfter)); //  [ 6, 1, 5, 9, 4, 7, 2, 8, 3 ]
複製程式碼

第2節 解密迴文——棧

// 棧--後進先出的資料結構
// 用來判斷迴文字串
function stackFun (stackStr) {
  var stackNewArr = [], len, mid, next, top
  len = stackStr.length
  mid = Math.floor(len / 2 - 1)
  top = 0
  for (var i = 0; i <= mid; i++) {
    stackNewArr[++top] = stackStr[i]
  }
  if (len % 2 === 0) {
    next = mid + 1
  } else {
    next = mid + 2
  }
  for (var j = next; j <= len-1; j++) {
    if (stackStr[j] !== stackNewArr[top]) {
      break
    }
    top--
  }

  if (top === 0) {
    return 'YES'
  } else {
    return 'NO'
  }
}

console.log(stackFun('abeffeba'))
複製程式碼

第3節 紙牌遊戲——小貓釣魚

// 紙牌遊戲--小貓釣魚--佇列和棧配合使用

// 初始化佇列--小哼手裡牌的相關資訊(先進先出)
const q1 = {
  head : 1,
  tail : 1,
  data: []
}

// 初始化佇列--小哈手裡牌的相關資訊(先進先出)
const q2 = {
  head : 1,
  tail : 1,
  data: []
}

// 初始化棧--桌面上牌的相關資訊(先進後出)
const s = {
  top: 0,
  data: []
}

// 初始化用來標記的陣列,用來標記哪些牌已經在桌上(桶排序)
const book = []
book.length = 10

for (let i = 1; i <= 9; i++) {
  book[i] = 0
}

// 依次向佇列中插入6個數
const arr1 = [2, 4, 1, 2, 5, 6]
const arr2 = [3, 1, 3, 5, 6, 4]

// 小哼手上的6張牌
for (let i = 1; i <= arr1.length; i++) {
  q1.data[q1.tail] = arr1[i - 1]
  q1.tail++
}

// 小哈手上的6張牌
for (let i = 1; i <= arr2.length; i++) {
  q2.data[q2.tail] = arr2[i - 1]
  q2.tail++
}

// 當佇列不為空的時候執行迴圈
while (q1.head < q1.tail && q2.head < q2.tail) {
  // 小哼出一張牌
  t = q1.data[q1.head]
  // 判斷小哼當前打出的牌是否能贏牌
  if (book[t] === 0) {  // 表明桌上沒有牌面為t的牌
    // 小哼此輪沒有贏牌
    q1.head++  // 小哼已經打出一張牌,所以要把打出的牌出隊
    s.top++
    s.data[s.top] = t  // 再把打出的牌放到桌上,即入棧
    book[t] = 1 // 標記桌上現在已經有牌面為t的牌
  } else {
    // 小哼此輪可以贏牌
    q1.head++
    q1.data[q1.tail] = t
    q1.tail++
    while (s.data[s.top] !== t) { // 把桌上可以贏得的牌依次放到手中牌的末尾
      book[s.data[s.top]] = 0 // 取消標記
      q1.data[q1.tail] = s.data[s.top] // 依次放入隊尾
      q1.tail++
      s.top--  // 棧中少了一張牌,所以棧頂要減1
    }
  }

  // 小哈出一張牌
  t = q2.data[q2.head]
  // 判斷小哈當前打出的牌是否能贏牌
  if (book[t] === 0) {  // 表明桌上沒有牌面為t的牌
    // 小哈此輪沒有贏牌
    q2.head++  // 小哈已經打出一張牌,所以要把打出的牌出隊
    s.top++
    s.data[s.top] = t  // 再把打出的牌放到桌上,即入棧
    book[t] = 1 // 標記桌上現在已經有牌面為t的牌
  } else {
    // 小哈此輪可以贏牌
    q2.head++
    q2.data[q2.tail] = t
    q2.tail++
    while (s.data[s.top] !== t) { // 把桌上可以贏得的牌依次放到手中牌的末尾
      book[s.data[s.top]] = 0 // 取消標記
      q2.data[q2.tail] = s.data[s.top] // 依次放入隊尾
      q2.tail++
      s.top--  // 棧中少了一張牌,所以棧頂要減1
    }
  }
}

if (q2.head === q2.tail) {
  console.log('小哼贏')
  console.log('小哼當前手中的牌是:')
  for (let i = q1.head; i <= q1.tail-1; i++) {
    console.log(q1.data[i])
  }
  if (s.top > 0) { // 如果桌上有牌則依次輸出桌上的牌
    console.log('桌上的牌是:')
    for (let i = 1; i <= s.top; i++) {
      console.log(s.data[i])
    }
  } else {
    console.log('桌上已經沒有牌了')
  }
} else {
  console.log('小哈贏')
  console.log('小哈當前手中的牌是:')
  for (let i = q2.head; i <= q2.tail-1; i++) {
    console.log(q2.data[i])
  }
  if (s.top > 0) { // 如果桌上有牌則依次輸出桌上的牌
    console.log('桌上的牌是:')
    for (let i = 1; i <= s.top; i++) {
      console.log(s.data[i])
    }
  } else {
    console.log('桌上已經沒有牌了')
  }
}
複製程式碼

第4節 連結串列

function Node(element) {
    this.element = element
    this.next = null
    this.prev = null
}

function LinkList() {
    this.head = new Node('head')
    this.tail = new Node('tail')
    this.head.next = this.tail
    this.tail.prev = this.head
}

LinkList.prototype = {
    find: function (item) {
        var currNode = this.head
        while ((currNode !== null) && (currNode.element !== item)) {
            currNode = currNode.next
        }
        return currNode
    },
    findFromTail: function (item) {
        var currNode = this.tail
        while ((currNode !== null) && (currNode.element !== item)) {
            currNode = currNode.prev
        }
        return currNode
    },
    insert: function (newElement, item) {
        var newNode = new Node(newElement)
        var currNode = this.findFromTail(item)
        if (currNode !== null) {
            if (currNode.next === null) {
                currNode.next = newNode
                newNode.prev = currNode
            } else {
                currNode.next.prev = newNode
                newNode.next = currNode.next
                newNode.prev = currNode
                currNode.next = newNode
            }
        } else {
            this.tail.prev.next = newNode
            newNode.prev = this.tail.prev
            newNode.next = this.tail
            this.tail.prev = newNode
        }

    },
    findPrevious: function (item) {
        var currNode = this.head
        while ((currNode.next !== null) && (currNode.next.element !== item)) {
            currNode = currNode.next
        }
        return currNode

    },
    remove: function (item) {
        // var prevNode = this.findPrevious(item)
        // if (prevNode.next !== null) {
        //     prevNode.next = prevNode.next.next
        // }
        var currNode = this.find(item)
        if (currNode.next === null) {
            currNode.prev.next = null
        } else {
            currNode.prev.next = currNode.next
            currNode.next.prev = currNode.prev
        }
    },
    edit: function (item, newElement) {
        var currNode = this.find(item)
        currNode.element = newElement
    },
    display: function () {
        var currNode = this.head
        while (currNode !== null) {
            console.log(currNode.element)
            currNode = currNode.next
        }
    }
}

var nums = new LinkList()
nums.insert('1', 'head')
nums.insert('2', '1')
nums.insert('3', '2')
nums.insert('4', '3')
nums.insert('10', '11')
nums.display()    // head 1 2 3 4 10 tail
複製程式碼

第3章 列舉!很暴力

第1節 坑爹的奧數

// XXX + XXX = XXX,將數字1-9分別填入9個X中,每個數字只能使用一次使得等式成立,例如173 + 286 = 459
// 演算法邏輯如下:
// num陣列表示9個位置的對應的數字
const num = []
num.length = 10

const book = []
book.length = 10

var i , total = 0, sum

for (num[1] = 1; num[1] <= 9; num[1]++) {
    for (num[2] = 1; num[2] <= 9; num[2]++) {
        for (num[3] = 1; num[3] <= 9; num[3]++) {
            for (num[4] = 1; num[4] <= 9; num[4]++) {
                for (num[5] = 1; num[5] <= 9; num[5]++) {
                    for (num[6] = 1; num[6] <= 9; num[6]++) {
                        for (num[7] = 1; num[7] <= 9; num[7]++) {
                            for (num[8] = 1; num[8] <= 9; num[8]++) {
                                for (num[9] = 1; num[9] <= 9; num[9]++) {
                                    // 初始化book陣列
                                    for (i = 1; i <= 9; i++) {
                                        book[i] = 0
                                    }
                                    // 如果某個數出現過就標記一下
                                    for (i = 1; i <= 9; i++) {
                                        book[num[i]] = 1
                                    }
                                    // 統計共出現了多少個不同的數
                                    sum = 0
                                    for (i = 1; i <= 9; i++) {
                                        sum += book[i]
                                    }
                                    // 如果正好出現了9個不同的數,~並且滿足等式條件,則輸出
                                    if (sum === 9 && (num[1] * 100 + num[2] * 10 + num[3] +
                                        num[4] * 100 + num[5] * 10 + num[6] ===
                                        num[7] * 100 + num[8] * 10 + num[9])) {
                                        total++
                                        console.log(num[1] + ',' + num[2] + ',' + num[3] + ',' + num[4] + ',' + num[5] + ',' + num[6] + ',' + num[7] + ',' + num[8] + ',' + num[9])
                                        console.log( '' + num[1] + num[2] + num[3] + '+' + num[4] + num[5] + num[6] + '=' + num[7] + num[8] + num[9])
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

console.log('total:', total / 2)   // 168
複製程式碼

第6章 最短路徑

第5節 只有五行的演算法——Floyd-Warshall

JavaScript實現《啊哈!演算法》中的系列演算法

  • 上圖中有4個城市8條公路,公路上的數字表示這條公路的長短。請注意這些公路是單向的。我們現在需要求任意兩個城市之間的最短路程,也就是求任意兩個點之間的最短路徑。這個問題也被稱為“多源最短路徑”問題。
// 最短路徑--動態規劃的思想
// Floyd-Warshall演算法是解決任意兩點間的最短路徑的一種演算法。通常可以在任何圖中使用,包括有向圖、帶負權邊的圖。  演算法的時間複雜度為O(N^3)
// 該演算法不能解決帶有“負權迴路”(或者叫“負權環”)的圖。因為這型別的圖沒有最短路徑

var k, i, j, t1, t2, t3
var inf = 999999
var n = 4 // 表示4個點
var m = 8 // 表示4個點之間所有邊之和
var e = [[], [], [], [], []]

// 初始化--預設點與點之間都有邊,且距離為inf(無窮大)
for (i = 1; i <= n ; i++) {
  for (j = 1; j <= n; j++) {
    if (i === j) {
      e[i][j] = 0 // 表示 始點和終點相同的時候,距離為0
    } else {
      e[i][j] = inf // 表示 從i點到j點的距離為inf(無窮大)
    }
  }
}

// 給部分點與點之間設定邊以及距離
e[1][2] = 2 // 表示 從1點 到 2點 的距離為 2
e[1][3] = 6 // 表示 從1點 到 3點 的距離為 6
e[1][4] = 4 // 表示 從1點 到 4點 的距離為 4
e[2][3] = 3 // 表示 從2點 到 3點 的距離為 3
e[3][1] = 7 // 表示 從3點 到 1點 的距離為 7
e[3][4] = 1 // 表示 從3點 到 4點 的距離為 1
e[4][1] = 5 // 表示 從4點 到 1點 的距離為 5
e[4][3] = 12 // 表示 從4點 到 3點 的距離為 12

// 最短路徑--演算法核心語句
for (k = 1; k <= n; k++) {
  for (i = 1; i <= n; i++) {
    for (j = 1; j <= n; j++) {
      if ((e[i][k] < inf) && (e[k][j] < inf) && (e[i][j] > e[i][k] + e[k][j])) {
        e[i][j] = e[i][k] + e[k][j]
      }
    }
  }
}

// 輸出結果
for (i = 1; i<= n; i++) {
  for (j = 1; j <= n; j++) {
    console.log('從' + i + '點到' + j + '點的最短距離為' + e[i][j])
    console.log('-----------------------------------------------')
  }
}
複製程式碼

第7章 神奇的樹

第2節 二叉樹

  • 二叉樹的性質
    • 在二叉樹的第i層上最多有2^(i-1)個節點(i>=1)
    • 深度為k的二叉樹最多有2^k-1個節點(k>=1)
    • 對任何一棵二叉樹T,如果其終端節點數為n0,度為2的節點數為n2,則n0=n2+1
      • 一棵深度為k且有2^k-1個節點的二叉樹稱為滿二叉樹
      • 深度為k的,有n個節點的二叉樹,當且僅當其每一個節點都與深度為k的滿二叉樹中編號從1至n的節點一一對應時,稱之為完全二叉樹
  • 二叉樹的遍歷
    • 二叉樹的遍歷指的是按照某種順序,依次訪問二叉樹的每個節點,有且訪問一次
    • 二叉樹的遍歷有以下三種
      • 前序遍歷,從根節點,到左子樹,再到右子樹,簡稱根左右
      • 中序遍歷,從左節點,到根節點,再到右子樹,簡稱左根右
      • 後序遍歷,從左子樹,到右子樹,再到根節點,簡稱左右跟
  • 完全二叉樹的特性
    • 具有n個節點的完全二叉樹的深度為Math.floor(log2 n)+1
    • 如果對一棵有n個節點的完全二叉樹(其深度為Math.floor(log2 n)+1)的節點按層序編號(從第1層到第Math.floor(log2 n)+1,每層從左到右),則對任一節點(1<=i<=n)有:
      • 如果i=1,則節點i是二叉樹的根,無雙親;如果i>1,則其雙親parent(i)是節點Math.floor(i/2)
      • 如果2i>n,則節點i無左孩子(節點i為葉子節點);否則其左孩子LChild(i)是節點2i
      • 如果2i+1>n,則節點i無右孩子;否則其右孩子RChild(i)是節點2i+1
  • 綜上:
    • 完全二叉樹:若設二叉樹的深度為h,除第h層外,其它各層 (1~h-1) 的結點數都達到最大個數,第h層所有的結點都連續集中在最左邊,這就是完全二叉樹。
    • 滿二叉樹的任意節點,要麼度為0,要麼度為2.
// 定義一個節點類
function Node (data, left, right) {
  this.data = data
  this.left = left
  this.right = right
  this.show = function () {
    return this.data
  }
}

// 定義一個二叉查詢樹類BST
function BST () {
  this.root = null
  this.insert = insert
  this.preOrder = preOrder
  this.inOrder = inOrder
  this.postOrder = postOrder
}

// 插入節點
function insert (data) {
  // 建立一個節點儲存資料
  var node = new Node(data, null, null)
  // 下面將節點node插入到樹中
  // 如果樹是空的,就將節點設為根節點
  if (!this.root) {
    this.root = node
  } else {
    //樹不為空
    // 判斷插在父節點的左邊還是右邊
    // 所以先要儲存一下父節點
    // var parent = this.root;
    var current = this.root
    var parent
    // 如果要插入的節點鍵值小於父節點鍵值,則插在父節點左邊,
    // 前提是父節點的左邊為空,否則要將父節點往下移一層,
    // 然後再做判斷
    while (true) {
      // data小於父節點的鍵值
      parent = current
      if (data < parent.data) {
        // 將父節點往左下移(插入左邊)
        // parent = parent.left;
        current = current.left
        // 如果節點為空,則直接插入
        if (!current) {
          // !!!此處特別注意,如果就這樣把parent賦值為node,也僅僅只是parent指向node,
          // 而並沒有加到父元素的左邊!!!根本沒有加到樹中去。所以要先記住父元素,再把當前元素加入進去
          parent.left = node
          break
        }
      } else {
        // 將父節點往右下移(插入右邊)
        current = current.right
        if (!current) {
          parent.right = node
          break
        }
      }
    }
  }
}

// 先序遍歷(根左右)
function  preOrder (node) {
  if (node) {
    // console.log(node.show())
    DLR_arr.push(node.show())
    if (!node.left && !node.right && DLR_arr.length === 5) {
      console.log("前序遍歷:", DLR_arr);
    }
    this.preOrder(node.left)
    this.preOrder(node.right)
  }
}

// 中序遍歷(左根右)
function  inOrder (node) {
  if (node) {
    this.inOrder(node.left)
    // console.log(node.show())
    LDR_arr.push(node.show())
    if (!node.left && !node.right && LDR_arr.length === 5) {
      console.log("中序遍歷:", LDR_arr);
    }
    this.inOrder(node.right)
  }
}

// 後序遍歷(左右根)
function postOrder (node) {
  if (node) {
    this.postOrder(node.left)
    this.postOrder(node.right)
    // console.log(node.show())
    LRD_arr.push(node.show())
    if (LRD_arr.length === 5) {
      console.log("後序遍歷:", LRD_arr);
    }
  }
}

// 例項化一個BST樹
var tree = new BST()
// 新增節點
tree.insert(30)
tree.insert(14)
tree.insert(35)
tree.insert(12)
tree.insert(17)

var DLR_arr = []
var LDR_arr = []
var LRD_arr = []

// 先序遍歷
tree.preOrder(tree.root)
// 中序遍歷
tree.inOrder(tree.root)
// 後序遍歷
tree.postOrder(tree.root)
複製程式碼

第3節 堆——神奇的優先佇列

  • 一種特殊的二叉樹
  • 最小堆:所有父節點都比子節點要小
  • 最大堆:所有父節點都比子節點要大
  • 應用:優先佇列:支援插入元素和尋找最大(小)值元素的資料結構
// Base
var h = [0, 99, 5, 36, 7, 22, 17, 46, 12, 2, 19, 25, 28, 1, 92] // 用來存放堆的陣列

var n = 14 // 用來存放堆中元素的個數,也就是堆的大小

var sum = n

// 交換函式,用來交換堆中的兩個元素的值
function swap (x, y) {
    let t = h[x]
    h[x] = h[y]
    h[y] = t
}

// 向下調整函式
function siftDown (i) {
    let t
    let flag = 0
    while (i * 2 <= n && flag === 0) {
        if (h[i] > h[i * 2]) {
            t = i * 2
        } else {
            t = i
        }
        if (h[t] > h[i * 2 + 1]) {
            t = i * 2 + 1
        }
        if (t !== i) {
            swap(i , t)
            i = t
        } else {
            flag = 1
        }
    }
}

// 建立堆函式
function create () {
    let i
    for (i = Math.floor(n / 2); i >= 1; i--) {
        siftDown(i)
    }
}

// 刪除最小元素
function deleteMin () {
    let t
    t = h[1]
    h[1] = h[n]
    n--
    siftDown(1)
    return t
}

create()

for (let j = 1; j <= sum ; j++) {
    console.log(deleteMin())  // 1, 2, 5, 7, 12, 17, 19, 22, 25, 28, 36, 46, 92, 99
}

// More
var h = [0, 99, 5, 36, 7, 22, 17, 46, 12, 2, 19, 25, 28, 1, 92] // 用來存放堆的陣列

var n = 14 // 用來存放堆中元素的個數,也就是堆的大小

function HeapSort (h, n ,n) {
    this.h = h
    this.n = n
    this.num = n
}

HeapSort.prototype = {
    swap: function (x, y) {
        let t = this.h[x]
        this.h[x] = this.h[y]
        this.h[y] = t
    },
    siftDown: function (i) {
        let t
        let flag = 0
        while (i * 2 <= this.n && flag === 0) {
            if (this.h[i] > this.h[i * 2]) {
                t = i * 2
            } else {
                t = i
            }
            if (this.h[t] > this.h[i * 2 + 1]) {
                t = i * 2 + 1
            }
            if (t !== i) {
                this.swap(i , t)
                i = t
            } else {
                flag = 1
            }
        }
    },
    create: function () {
        let i
        for (i = Math.floor(this.n / 2); i >= 1; i--) {
            this.siftDown(i)
        }
    },
    deleteMin: function () {
        let t
        t = this.h[1]
        this.h[1] = this.h[this.n]
        this.n--
        this.siftDown(1)
        return t
    }
}

var newHeap = new HeapSort(h, n, n)

newHeap.create()

for (let j = 1; j <= newHeap.num ; j++) {
    console.log(newHeap.deleteMin())  // 1, 2, 5, 7, 12, 17, 19, 22, 25, 28, 36, 46, 92, 99
} 
複製程式碼

第4節 擒賊先擒王——並查集

// 存放每個節點的根節點
var f = []

// 記錄節點的個數
var n = 10

// 有關聯的節點關係
var m = []
m.length = 10
m[1] = [1, 2]
m[2] = [3, 4]
m[3] = [5, 2]
m[4] = [4, 6]
m[5] = [2, 6]
m[6] = [8, 7]
m[7] = [9, 7]
m[8] = [1, 6]
m[9] = [2, 4]

// 記錄獨立根節點的個數
var sum = 0

// 初始化 陣列裡 存的是 自己陣列下標的編號
function init () {
  for (let i = 1; i <= n; i++) {
    f[i] = i
  }
}

// 找祖宗的遞迴函式,不停地去找祖宗,直到找到祖宗為止,
// 其實就是去找犯罪團伙的最高領導人,“擒賊先擒王”原則
function getF (v) {
  if (f[v] === v) {
    return v
  } else {
    // 這裡是路徑壓縮,每次在函式返回的時候,順帶把路上遇到的人的“BOSS”改為最後
    // 找到的祖宗編號,也就是犯罪團伙的最高領導人編號。這樣可以提高今後找到犯罪團伙的最高領導人
    //(其實就是樹的祖先)的速度
    f[v] = getF(f[v])
    return f[v]
  }
}
// 合併兩子集合的函式
function merge (v, u) {
  let t1, t2
  t1 = getF(v)
  t2 = getF(u)
  if (t1 !== t2) {  // 判斷兩個節點是否在同一個集合中,即是否為同一個祖先
    f[t2] = t1
    // “靠左”原則左邊變成右邊的BOSS。即把右邊的集合,作為左邊集合的子集合。
    // 經過路徑壓縮後,將f[u]的根的值也賦值為v的祖先f[t1]
  }
}

// 初始化
init()
// 開始合併犯罪團伙
for (let i = 1; i < m.length; i++) {
  merge(m[i][0], m[i][1])
}
// 求出獨立的根節點,即掃描出有多少個獨立的犯罪團伙
for (let i = 1; i <= n; i++) {
  if (f[i] === i) {
    sum++
  }
}

console.log(sum)  // 3
複製程式碼

相關文章