排序
公共方法
交換arr裏面i1索引與i2索引的數據的位置
function exchange(arr, i1, i2) {
let tmp = arr[i1];
arr[i1] = arr[i2];
arr[i2] = tmp;
}
選擇排序
選擇排序就是通過不斷遍歷數組未排序部分找到最小值,將其放在未排序部分的最前面
function selectSort(arr) {
let len = arr.length;
// 當前邊的都排好以後,最後一位一定是最大值,所以i < len - 1即可
for (let i = 0; i < len - 1; i++) {
let min = arr[i];
let minIndex = i;
// 找的是還沒有排序的數據的最小值,所以j從i + 1開始
for (let j = i + 1; j < len; j++) {
if (arr[j] < min) {
min = arr[j];
minIndex = j;
}
}
exchange(arr, i, minIndex);
}
}
冒泡排序
冒泡排序就是從數組前兩位開始,將大的那個放在後面,然後比較二三位,直到將最大的放在末尾
function bubbleSort(arr) {
let len = arr.length;
for (let i = 0; i < len - 1; i++) {
for (let j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
exchange(arr, j, j + 1);
}
}
}
}
插入排序
插入排序類似鬥地主摸牌的過程,默認第一位是已經拍好順序的,將後面的每一位依次插入到已排序部分
function insertionSort(arr) {
let len = arr.length;
for (let i = 1; i < len; i++) {
// 如果插入項本就大於最後一項,那麼不管,直接放在最後
if (arr[i] < arr[i - 1]) {
let tmp = arr[i];
// 尋找插入位置
for (let j = i; j >= 0; j--) {
if (j > 0 && arr[j - 1] > tmp) {
arr[j] = arr[j - 1];
} else {
arr[j] = tmp;
break;
}
}
}
}
}
快速排序
將數組第一個或最後一個定位基準值,通過與基準值比較,將比基準值小的放在左側,比基準值大的放在右側,然後繼續遞歸
function quickSort(arr) {
function _quickSort(arr, low, high) {
let left = low;
let right = high;
// 如果只有一項,直接返回
if (low >= high) return;
// 如果記錄high爲基準值,那麼先移動low指針
// 因爲先移動high指針的話low位置的值將被覆蓋,但是high位置的值已經被存下,被覆蓋也沒關係
let tmp = arr[high];
while (low < high) {
while (low < high && arr[low] < tmp) low++;
arr[high] = arr[low];
while (low < high && arr[high] >= tmp) high--;
arr[low] = arr[high];
}
arr[low] = tmp;
_quickSort(arr, left, low - 1);
_quickSort(arr, low + 1, right);
}
_quickSort(arr, 0, arr.length - 1);
}
查找
遍歷查找
遍歷查找是最簡單直接的查找方式,適用於任何數組,但是查找次數較大
function search(arr, target) {
for (let i = 0; i < arr.length; i++) {
searchCount++;
if (arr[i] == target) return i;
}
return false;
}
二分查找
二分查找是速度較快的查找方式,但是前提是數組是排列好的
// 二分查找 循環
function binarySearch(arr, target) {
if (arr.length == 0 || target < arr[0] || target > arr[arr.length - 1])
return false;
function _binarySearch(arr, target, start, end) {
while (start < end) {
searchCount++;
let mid = parseInt((start + end) / 2);
if (arr[mid] == target) {
return mid;
} else if (arr[mid] < target) {
start = mid + 1;
} else if (arr[mid] > target) {
end = mid - 1;
}
}
return false;
}
return _binarySearch(arr, target, 0, arr.length - 1);
}
function binarySearch2(arr, target) {
if (arr.length == 0 || target < arr[0] || target > arr[arr.length - 1])
return false;
function _binarySearch(arr, target, start, end) {
while (start < end) {
searchCount++;
let mid = parseInt((start + end) / 2);
if (arr[mid] == target) {
return mid;
} else if (arr[mid] < target) {
return _binarySearch(arr, target, mid + 1, end);
} else if (arr[mid] > target) {
// end = mid - 1;
return _binarySearch(arr, target, start, mid - 1);
}
}
return false;
}
return _binarySearch(arr, target, 0, arr.length - 1);
}
插值查找
插值查找對排列好,並且相鄰的值的大小差距差不多的數組有很快的查找速度,相比二分查找,優化了mid的取值
function interpolationSearch(arr, target) {
if (arr.length == 0 || target < arr[0] || target > arr[arr.length - 1])
return false;
function _binarySearch(arr, target, start, end) {
while (start < end) {
searchCount++;
let mid = parseInt(
((target - arr[start]) * (end - start)) /
(arr[end] - arr[start]) +
start
);
if (arr[mid] == target) {
return mid;
} else if (arr[mid] < target) {
return _binarySearch(arr, target, mid + 1, end);
} else if (arr[mid] > target) {
// end = mid - 1;
return _binarySearch(arr, target, start, mid - 1);
}
}
return false;
}
return _binarySearch(arr, target, 0, arr.length - 1);
}
樹
樹的特點
- 單根:如果一個節點A指向節點B,那麼僅可通過A找到B,不可能通過其他節點找到B
- 無環:節點的指向不能形成環
樹的術語
- 節點的度:某個節點的度等於該節點子節點的數量
- 樹的度:一棵樹中,最大的節點的度爲該樹的度
- 樹的層:從根節點開始,根爲一層,根的子節點爲二層,以此類推
- 樹的深度(高度):樹的最大層次
- 葉子節點:度爲0的節點稱爲葉子節點
- 分支節點:非葉子節點
- 子節點、父節點:如果一個節點A指向節點B,那麼A是B的父節點,B是A的子節點
- 兄弟節點:如果兩個節點有共同的父節點,那麼這兩個節點互爲兄弟節點
- 祖先節點:一個節點的祖先節點,是從根節點到該節點本身經過的所有節點
- 後代節點:如果A是B的祖先節點,那麼B是A的後代節點
獲取樹的深度
通過遞歸遍歷數的各個節點,節點的深度爲1+子節點的深度,如果子節點有過個,取最大的子節點的深度,如果節點沒有子節點,直接返回1
注意如果子節點存在,纔去遞歸子節點
function getDeep(root) {
if (!root) return 0
if (!root.left && !root.right) return 1
return 1 + Math.max(root.left ? getDeep(root.left) : 0, root.right ? getDeep(root.right) : 0)
}
樹的遍歷
樹的遍歷分爲前序遍歷,中序遍歷與後續遍歷
前序遍歷即根節點在前,然後是左子節點,右子節點,
中序遍歷即左子節點在前,然後是根節點,右子節點,
後序遍歷即左子節點在前,然後是右子節點,根節點。
function loopFront(root) {
console.log(root.value);
root.left && loopFront(root.left)
root.right && loopFront(root.right)
}
function loopMid(root) {
root.left && loopFront(root.left)
console.log(root.value);
root.right && loopFront(root.right)
}
function loopBack(root) {
root.left && loopFront(root.left)
root.right && loopFront(root.right)
console.log(root.value);
}
通過前中遍歷還原二叉樹
只有前中,中後遍歷可還原二叉樹,核心思想是遞歸
通過找出跟節點的位置找到左子樹的前中遍歷和右子樹的前中遍歷
遞歸左子樹與右子樹
// 根據兩個遍歷還原數
function restoreByFrontMid(front, mid) {
if (front.length != mid.length) return null
if (front.length == mid.length && mid.length == 0) return null
// 根節點
let root = front[0];
// 根節點在中序遍歷中的位置
let midRootIndex = mid.indexOf(root);
// 左子樹前序遍歷
let leftFront = front.substr(1, midRootIndex);
// 左子樹中序遍歷
let leftMid = mid.substr(0, midRootIndex);
// 右子樹前序遍歷
let rightFront = front.substr(midRootIndex + 1);
// 右子樹中序遍歷
let rightMid = mid.substr(midRootIndex + 1);
let node = new Node(root);
// console.log(leftFront, leftMid)
node.left = restoreByFrontMid(leftFront, leftMid)
node.right = restoreByFrontMid(rightFront, rightMid)
return node
}
樹的搜索
深度優先
原理類似前序遍歷,先搜索自己,如果子節不是要找的節點,就搜索左子節點,左子節點全部搜完纔去搜右子節點
// 深搜
function deepFirstSearch(root, target) {
console.log(root.value)
if (!root) return false;
if (root.value == target) return true;
return root.left && deepFirstSearch(root.left, target) || root.right && deepFirstSearch(root.right, target)
}
廣搜
分治法,搜索一個數組內的節點,最初是一個只包含根節點的數組,如果這個數組內的節點都不是要找的,那麼把這些節點的子節點放入一個新數組(如果子節點存在的話),重新搜索這個節點數組
// 廣搜
function rangeFirstSearch(root, target) {
if (!root) return false;
function _rangeFirstSearch(arr, target) {
let arr_ = [];
for (let i = 0; i < arr.length; i++) {
console.log(arr[i].value)
if (arr[i].value == target) return true;
if (arr[i].left) arr_.push(arr[i].left)
if (arr[i].right) arr_.push(arr[i].right)
}
if (arr_.length > 0) return _rangeFirstSearch(arr_, target)
else return false
}
return _rangeFirstSearch([root], target)
}
最小生成樹
普利姆算法
普里姆算法是以一個節點開始,遍歷它的相鄰節點,找出代價最小的節點,然後遍歷這兩個節點的相鄰接點,找出下一個代價最小節點
這個算法需要一個數組來保存已經連接的節點,並且在遍歷相鄰節點時,不考慮已遍歷節點,當以連接節點的數組長度等於總節點長度時,完成並返回任一節點
// max
const max = 10000;
// 創建各個點
let a = new Node('A');
let b = new Node('B');
let c = new Node('C');
let d = new Node('D');
let e = new Node('E');
// 點集合
let pointSet = [a,b,c,d,e];
// 邊集合
let 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]
];
function Node(val) {
this.value = val;
this.neighbor = [];
}
// 普利姆算法
function prim(pointSet, distance) {
let nodes = [pointSet[0]];
while (nodes.length < pointSet.length) {
let newPoint = getMinPoint(pointSet, distance, nodes);
nodes.push(newPoint);
}
function getMinPoint(pointSet, distance, nodes) {
let fromNode = null;
let endNode = null;
let min = max;
for (let i = 0; i < nodes.length; i++) {
let nowPonitIndex = pointSet.indexOf(nodes[i]);
for (let j = 0; j < distance[nowPonitIndex].length; j++) {
if (distance[nowPonitIndex][j] < min && !nodes.includes(pointSet[j])) {
fromNode = nodes[i];
endNode = pointSet[j];
min = distance[nowPonitIndex][j]
}
}
}
fromNode.neighbor.push(endNode)
endNode.neighbor.push(fromNode)
return endNode
}
return nodes[0]
}
克魯斯卡爾算法
克魯斯卡爾算法是一直找代價最小邊,並連接兩個節點
克魯斯卡爾算法將已經連接的點作爲一個部落(數組)放在一個部落列表裏,通過這個部落列表判斷能不能連接以及怎麼連接
克魯斯卡爾算法解決的是兩個問題
- 能不能連
- 怎麼連
關於能不能連,如果兩個節點的代價大於0,小於max,並且不在一個部落,就可以連
(一個在部落,一個不在部落,兩個在不同部落,都可以連接)
關於怎麼連接,如果兩個節點都不在部落,那麼增加一個新部落,裏面是兩個節點,並將新部落存入部落列表
如果一個在部落,一個不在部落,將不在部落的節點存入另一個節點的部落
如果兩個節點在相同部落,那麼將兩個部落合併
// max
const max = 10000;
// 創建各個點
let a = new Node('A');
let b = new Node('B');
let c = new Node('C');
let d = new Node('D');
let e = new Node('E');
// 點集合
let pointSet = [a,b,c,d,e];
// 邊集合
let 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]
];
function Node(val) {
this.value = val;
this.neighbor = [];
}
// 能不能連
function canLink(clanArr, fromPoint, endPoint) {
let fromIn = null;
let endIn = null;
for (let i = 0; i < clanArr.length; i++) {
if (clanArr[i].includes(fromPoint)) {
fromIn = clanArr[i]
}
if (clanArr[i].includes(endPoint)) {
endIn = clanArr[i]
}
}
if (fromIn && endIn && fromIn == endIn) {
return false
}
return true
}
// 連接兩個點
function link(clanArr, fromPoint, endPoint) {
fromPoint.neighbor.push(endPoint);
endPoint.neighbor.push(fromPoint);
let fromIn = null;
let endIn = null;
for (let i = 0; i < clanArr.length; i++) {
if (clanArr[i].includes(fromPoint)) {
fromIn = clanArr[i]
}
if (clanArr[i].includes(endPoint)) {
endIn = clanArr[i]
}
}
if (!fromIn && !endIn) { // 兩個點都不在任何部落
clanArr.push([fromPoint, endPoint])
} else if (fromIn && !endIn) { // fromPoint在部落,endPoint不在部落
fromIn.push(endIn)
} else if (!fromIn && endIn) { // endPoint在部落,fromPoint不在部落
endIn.push(fromPoint)
} else if (fromIn && endIn && fromIn != endIn) { // 兩個點在不同部落
fromIndex = clanArr.indexOf(fromIn);
clanArr[fromIndex] = fromIn.concat(endIn);
endIndex = clanArr.indexOf(endIn);
clanArr.splice(endIndex, 1)
}
}
// 克魯斯卡爾算法
function kruskal(pointSet, distance) {
let clanArr = [];
while (true) {
let fromPoint = null;
let endPoint = null;
let min = max;
for (let i = 0,len = distance.length; i < len; i++) {
for (let j = 0,len = distance[i].length; j < len; j++) {
if (distance[i][j] != 0 && distance[i][j] < min && canLink(clanArr, pointSet[i], pointSet[j])) {
fromPoint = pointSet[i];
endPoint = pointSet[j];
min = distance[i][j];
}
}
}
link(clanArr, fromPoint, endPoint);
if (clanArr.length == 1 && clanArr[0].length == pointSet.length) {
break
}
}
}
kruskal(pointSet, distance)
貪心算法
當遇到一個求全局最優解的問題時,如果可將全局問題切分成小的局部問題,並尋求局部最優解,同時可以證明局部最優解的累計結果就是全局最優解,則可以使用貪心算法
例:找零問題
假設你有一間小店,需要找給客戶46分錢的硬幣,你的貨櫃裏面只有面額爲25分,10分,5分,1分的硬幣,如何找零才能確保數額正確並且硬幣數最少
function zhaoling(target, arr) {
console.log(1)
if (target == 0 && arr.length == 0) {
return []
}
let result = [];
while (target > 0) {
let money = arr.filter((item) => item <= target)[0];
if (money) {
result.push(money);
target -= money
} else {
return false
}
}
return result
}
console.log(zhaoling(46, [200, 100, 50, 25, 10, 5, 1]))
// 25 10 10 1
在這個算法裏面,每次都是找小於未找金額的硬幣中最大的那個,但是這種尋求局部最優解的和未必是全局最優解
例如,找零41,零錢有[25, 20, 5, 1]貪心算法得出結果爲[25, 5, 5, 5, 1],但是這明顯不是最優解,最優解爲[20, 20, 1]
但是由於貪心算法只需要考慮眼下的事,所以貪心算法效率較高。當你不需要必須是最優解的話,貪心算法是一個不錯的選擇
但是如果必須最優解的話,可以優化
function zhaoling(target, arr) {
console.log(1)
if (target == 0 && arr.length == 0) {
return []
}
let result = [];
while (target > 0) {
let money = arr.filter((item) => item <= target)[0];
if (money) {
result.push(money);
target -= money
} else {
return false
}
}
return result
}
function perfectZhaoling(target, arr) {
if (arr.length == 0) return false;
if (target == arr[0]) return false;
arr = arr.filter((item) => item <= target)
let result = [];
result = zhaoling(target, arr);
// console.log(result)
if (result == false) return false;
for (let i = 1; i < arr.length - 1; i++) {
let subResult = zhaoling(target, arr.slice(i));
if (typeof subResult == 'object' && subResult.length < result.length) {
result = subResult
}
}
return result
}
console.log(perfectZhaoling(41, [200, 100, 50, 25, 20, 5, 1]))
此算法的原理是,先使用之前的貪心算法計算零錢爲[200, 100, 50, 25, 20, 5, 1]的解,然後去掉最大的零錢,計算[100, 50, 25, 20, 5, 1]的解,如果得到的解的長度小於之前的解,那麼就替換之前的解
動態規劃
個人理解:(自己對動態規劃的理解,可能不是很準確,僅供參考)
動態規劃就是將待求解的問題分解爲若干個子問題(階段),按順序求解子階段,前一子問題的解,爲後一子問題的求解提供了有用的信息。
與分治法不同的是,動態規劃是將每個子問題的結果緩存起來,下次遇到相同子問題直接取緩存
在我的理解中,動態規劃等於遞歸加緩存
例題1:青蛙跳臺階
一隻青蛙一次只能跳一級或者兩級臺階,那麼這隻青蛙跳上n級臺階有幾種跳法?
let arr = []
function frog(n) {
if (n <= 0) return false;
if (n <= 3) return n;
let result = frog(n - 1) + frog(n - 2);
arr[n] = result
return result
}
console.log(frog(10))
這個算法中arr即爲緩存。
例題2:變態青蛙跳臺階
一隻青蛙一次只能跳一級到n級臺階,那麼這隻青蛙跳上n級臺階有幾種跳法?
function BTfrog(n) {
if (n <= 0) return false;
if (n <= 2) return n;
let result = 0;
for (let i = 1; i < n; i++) {
result += BTfrog(i)
}
arr[n] = result
return result
}
console.log(BTfrog(10))
例題3:最長公共子序列
有時候,我們需要比較兩個字符串的相似程度,通常就是比較兩個字符串有多少相同的公共子序列,例如有兩個字符串‘asefhtbrf’,‘asmiiimhfoo’,以上兩個字符串的最長公共子序列的ashf
思路:如果這兩個字符串第一位相同,那麼就返回第一位加LCS(str1.substr(1), str2.substr(1)),如果第一位不一樣就返回LCS(str1, str2.substr(1))和LCS(str1.substr(1), str2)中比較長的一個
function LCS(str1, str2) {
if (!str1 || !str2) return '';
if (str1[0] == str2[0]) {
return str1[0] + LCS(str1.substr(1), str2.substr(1));
} else {
let s1 = LCS(str1.substr(1), str2);
let s2 = LCS(str1, str2.substr(1));
return s1 > s2 ? s1 : s2;
}
}
但是這樣會產生很大的運算量,因爲有很多重複的運算
我們可以加一個緩存來取消重複運算
let arr = []
function LCS(str1, str2) {
if (!str1 || !str2) return '';
// 先看有沒有緩存
for (let i = 0; i < arr.length; i++) {
if (arr[i].str1 == str1 && arr[i].str2 == str2) {
return arr[i].result
}
}
let s;
if (str1[0] == str2[0]) {
s = str1[0] + LCS(str1.substr(1), str2.substr(1));
} else {
let s1 = LCS(str1.substr(1), str2);
let s2 = LCS(str1, str2.substr(1));
s = s1 > s2 ? s1 : s2;
}
// 將本次計算結果緩存起來
arr.push({
str1,
str2,
result: s
})
return s
}