前端基礎算法

排序

公共方法

交換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);
}

樹的特點

  1. 單根:如果一個節點A指向節點B,那麼僅可通過A找到B,不可能通過其他節點找到B
  2. 無環:節點的指向不能形成環

樹的術語

  1. 節點的度:某個節點的度等於該節點子節點的數量
  2. 樹的度:一棵樹中,最大的節點的度爲該樹的度
  3. 樹的層:從根節點開始,根爲一層,根的子節點爲二層,以此類推
  4. 樹的深度(高度):樹的最大層次
  5. 葉子節點:度爲0的節點稱爲葉子節點
  6. 分支節點:非葉子節點
  7. 子節點、父節點:如果一個節點A指向節點B,那麼A是B的父節點,B是A的子節點
  8. 兄弟節點:如果兩個節點有共同的父節點,那麼這兩個節點互爲兄弟節點
  9. 祖先節點:一個節點的祖先節點,是從根節點到該節點本身經過的所有節點
  10. 後代節點:如果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]
}

克魯斯卡爾算法

克魯斯卡爾算法是一直找代價最小邊,並連接兩個節點
克魯斯卡爾算法將已經連接的點作爲一個部落(數組)放在一個部落列表裏,通過這個部落列表判斷能不能連接以及怎麼連接
克魯斯卡爾算法解決的是兩個問題

  1. 能不能連
  2. 怎麼連
    關於能不能連,如果兩個節點的代價大於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
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章