排序
公共方法
交换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
}