第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
- 上圖中有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的節點一一對應時,稱之為
完全二叉樹
- 一棵深度為k且有2^k-1個節點的二叉樹稱為
- 二叉樹的遍歷
- 二叉樹的遍歷指的是按照某種順序,依次訪問二叉樹的每個節點,有且訪問一次
- 二叉樹的遍歷有以下三種
- 前序遍歷,從根節點,到左子樹,再到右子樹,簡稱根左右
- 中序遍歷,從左節點,到根節點,再到右子樹,簡稱左根右
- 後序遍歷,從左子樹,到右子樹,再到根節點,簡稱左右跟
- 完全二叉樹的特性
- 具有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
複製程式碼