初衷
想要学习算法大概是感觉到面对复杂业务,以及想阅读源码时,感到力不从心。源码中贯穿着优秀的算法思想,一个优雅的实现,在我看来需要想破脑袋才能理解,而如果有了算法理解,其实是自然而然的事情。所以决定学一学算法。
算法图解
在这里,向大家推荐一本书,算法图解。正如这本书副标题所写:像小说一样有趣的算法入门书。整本书讲解算法之前 通常从实际应用中引出问题,像探案一样一步一步道出 真谛。书中绝无长篇大论以及枯燥的公式,书中穿插了400多个示意图,生动的介绍算法执行过程,可以说对算法初学者简直太友好了。
好了,下面介绍一下算法图解每章节的内容,以及js实现。
注意: 下面分章节介绍,是在也算不上介绍,更多的是js实现,对于算法思想和真正的细节,还是希望大家读原书。我顶多算抛砖引玉,轻喷
二分查找
二分查找用于有序列表的查找,一个简单的猜大小的例子:
我随便想一个 1-100的数字,你的目标是以最小的次数猜到这个数字,你每次猜测后,我会说 小了、大了、或者对了。
最快的做法是:
- 猜50。
- 小了,但是排除了一半的数字,然后猜 75
- 大了,那余下的数字又排除了一半!然后猜63
- 大了,那就猜 57
- 大就猜 53, 小则猜 60
- 。。。 总之 范围在一步步的减小。最多7步肯定能猜对。
这就是二分查找了。
二分查找的实现,有2种,一种是使用while循环,一种是使用递归。我把两种实现都写出来。
// while
function binary_search1(list, key) {
let low = 0;
let high = list.length - 1;
let mid = -1;
while(low <= high) {
mid = parseInt(low + (high - low) /2);
if (key === list[mid]) {
return mid;
} else if (key > list[mid]) {
low = mid + 1;
} else if (key < list[mid]) {
high = mid - 1;
} else {
return -1;
}
}
return -1;
};
// recursive
function binary_search2(list, low, high, key) {
if (low > high) {
return -1;
}
let mid = parseInt((low + (high - low)/2));
if (key === list[mid]) {
return mid;
}
if (key > list[mid]) {
low = mid + 1;
return binary_search2(list, low, high, key)
}
if (key < list[mid]) {
high = mid - 1;
return binary_search2(list, low, high, key);
}
}
选择排序
第二章讲了两种基本数据结构: 数组和链表。然后在此基础上 讲解了选择排序。
选择排序思想是,遍历列表,找出最大的元素放到另一个列表中。再次这样做,找出第二大元素放到另一个列表中。
实现可参考:
ps. 排序类可以用leetcode#912验证
function selectSort(arr) {
const res = [];
while (arr.length) {
res.push(...arr.splice(getSmallest(arr), 1))
}
return res;
}
function getSmallest(arr) {
let smallestValue = Infinity;
let smallestIndex = -1;
arr.forEach((item, index) => {
if (item <= smallestValue) {
smallestValue = item;
smallestIndex = index;
}
});
return smallestIndex;
}
递归
第三章讲解了 递归算法。递归算法我们大抵知道,需要找出基线条件 和 递归条件,符合递归条件的时候调用自己,基线条件则函数不再调用自己。 而算法图解则是用插图的方式详细讲解了 递归每一步计算机都发生了什么,并且解释了编程的一个重要概念--调用栈(call stack)
。
很多问题解决都使用了递归概念,图解举了一个小例子: 阶乘.
阶乘公式是f(n) = n*(n-1)*...*1 (x>=1); f(0)=1
阶乘的实现很简单:
function factorial(n) {
if(n < 0) {
return -1;
}
if (n === 0) {
return 1;
}
return n * factorial(n -1);
}
快速排序
快速排序是一个很经典的问题了。快排的思想是分而治之。
- 选择一个基准值。
- 将数组分成两个子数组,分大于和小于基准值
- 对这2个子数组进行快速排序。
基于这个思想,额外申请空间,实现一个特别容易理解的快排。
function quickSort1(arr) {
if ( arr.length <= 1) {
return arr;
}
const low = 0;
const high = arr.length - 1;
const base = parseInt(low + (high - low)/2);
const baseValue = arr[base];
const left = [];
const right = [];
const mid = [];
arr.forEach((item, index) => {
if (item < baseValue) {
left.push(item);
} else if (item > baseValue) {
right.push(item);
} else if (item === baseValue) {
mid.push(item);
}
});
return quickSort1(left).concat(mid).concat(quickSort1(right));
}
这个实现缺点是需要额外申请空间。我们知道快排关键是分区: 也就是将数组移动成 基准值左边均小于基准值,基准值右边均大于基准值。所以分区的关键是 找到基准值在数组中的位置。然后对两边的数组再进行快排。
分区过程是利用左边和右边两个指针。
- 左边元素小于 基准值 则左边指针向右移动。右边元素如果大于基准值,则右边元素向左移动。
- 当两个指针都停止移动时,交互两个元素的值。
- 当左右指针相同时,退出循环,此时的位置就是基准值应该在数组中的位置。
- 对位置左右两侧的数组重复上述过程排序
不是太好理解,在b站找视频看吧,或者看看算法4.
实现:
function quickSort3(arr, low, high) {
if (low < 0 || high < 0 || low >= high || !arr.length) {
return arr;
}
let pivot = arr[low];
let i = low, j = high;
while(i < j) {
while (j > i && arr[j] > pivot) {
j--;
}
while( i < j && arr[i] < pivot) {
i ++;
}
if (i < j && arr[i] === arr[j]) {
i ++;
} else if ( i < j) {
const temp = arr[j];
arr[j] = arr[i];
arr[i] = temp;
}
}
quickSort3(arr, low, i - 1);
quickSort3(arr, j + 1, high);
return arr;
}
广度优先搜索
广度优先是由于图的查找,可以解决2类问题:
- 是否有路径
- 最短路径
是否有路径
一个现实生活中的demo。
你需要找芒果销售商,将芒果卖给他。你需要在朋友中查找芒果销售商,你可以这么做:
- 创建一个朋友名单
- 依次检查每个人,看他是否为芒果销售商
- 如果你的朋友没有,那就在朋友中的朋友找
- 直到找到销售商或者把朋友找完
解决这个问题,首先需要创建一个图,表明你朋友和朋友的朋友之间的关系。
具体实现如下
function is_seller(name) {
return /m$/.test(name);
}
function createGraph() {
return {
you: ['alice', 'bob', 'claire'],
bob: ['anuj', 'peggy'],
alice: ['peggy'],
claire: ['thom', 'jonny'],
anuj: [],
peggy: [],
thom: [],
jonny: []
};
}
export default function breadth_first_search(name) {
const graph = createGraph();
// one queue
const searchQueue = [];
const searched = [];
searchQueue.push(...graph[name]);
let person;
while(searchQueue.length) {
person = searchQueue.shift();
// not search this person
if (!searched.includes(person)) {
searched.push(person);
if (is_seller(person)) {
console.log('the person is', person);
return true;
} else {
searchQueue.push(...graph[person]);
}
}
}
return false;
};
迪克斯特拉算法
迪克斯特拉算法用于解决加权图中前往x的最短路径。
迪克斯特拉算法步骤是
- 找出 ’最便宜‘的节点
- 更新该节点邻居的开销
- 重复这个过程,直到对每个节点都这样做了。
- 计算最终路径
以下图为例,利用一个迪克斯特拉算法查找从 起点到终点的最短路径:
要编写解决这个问题的代码,需要三个散列表
- 构建各个节点的图
- 表明每个节点代价的object
- 表明每个节点parent的object
参考实现
function find_lowest_cost_node(costs, processed) {
let lowest_cost = Infinity;
let lowest_node = '';
for (let node in costs) {
if ( lowest_cost >= costs[node] && !processed.includes(node)) {
lowest_cost = costs[node];
lowest_node = node;
}
}
return lowest_node;
}
function dikstra(graph, costs, parent) {
const processed = [];
let node = find_lowest_cost_node(costs, processed);
while(node && !processed.includes(node)) {
Object.keys(graph[node]).forEach(item => {
// if item cost less than costs ,than update the costs and parent
if (costs[node] + graph[node][item] < costs[item]) {
costs[item] = costs[node] + graph[node][item];
parent[item] = node;
}
});
processed.push(node);
// update node
node = find_lowest_cost_node(costs, processed);
}
// find the best path through parent
const paths = ['fin'];
node = 'fin';
while(parent[node]) {
paths.unshift(parent[node]);
node = parent[node];
};
// console.log(paths);
console.log('the start to fin path is:', paths.join('-'), 'the costs is: ', costs['fin']);
};
let graph = {
start: {
a: 5,
b: 2
},
a: {
c: 4,
d: 2
},
b: {
a: 8,
d: 7
},
c: {
fin: 3,
d: 6
},
d: {
fin: 1
},
fin: {
}
};
let costs = {
a: 5,
b: 2,
c: Infinity,
d: Infinity,
fin: Infinity
};
let parent = {
a: 'start',
b: 'start',
c: '',
d: '',
fin: ''
};
dikstra(graph, costs, parent);
动态规划
第9章讲了动态规划,其实我现在也没有完全搞懂动态规划。书中对于动态规划的讲解主要是 图解每一步,对于 状态转移方程 如何推导,确实没有怎么讲。所以对于这一节内容,我也只是停留在实现,更深入的还需要看其他资料。
写一个 最长公共子序列作为参考吧
function getTemp(arr, i, j) {
if (i < 0 || j < 0) {
return 0;
}
return arr[i][j];
}
const longestCommonSubsequence = function(text1, text2) {
if (!text1.length || !text2.length) {
return 0;
}
const len1 = text1.length;
const len2 = text2.length;
let res = [];
for (let i = 0; i < len1; i ++) {
res[i] = [];
for(let j = 0; j < len2; j ++) {
if (text2[j] === text1[i]) {
res[i][j] = getTemp(res, i - 1, j - 1) + 1;
} else {
res[i][j] = Math.max(getTemp(res, i - 1, j), getTemp(res, i, j - 1));
}
}
}
return res[len1 -1][len2-1];
};
console.log(longestCommonSubsequence('abcde', 'ace'));