看了 Loiane Groner 著的《学习JavaScript数据结构与算法》一书,自己写篇博客对着敲敲代码:
全文包含十个部分,分别是:数组、栈、队列、链表、集合、字典与散列表、树、图、排序和搜索算法、算法补充知识。
知识点其他部分参考:
学习JavaScript数据结构和算法(部分一)
学习JavaScript数据结构和算法(部分二)
学习JavaScript数据结构和算法(部分三)
学习JavaScript数据结构和算法(部分四)
8、图
这一章的话,学过图论就不难,概念都是书上的。
8.1、创建图类:
function Graph() {
this.vertices = []; //存储图中所有点的名字
this.adjList = new Dictionary(); //存储邻接表(使用顶点名字作为键,邻接顶点作为值)
this.addVertex = addVertex; //向图中添加新的顶点
this.addEdge = addEdge; //向图中添加新的边
this.toString = toString; //方便输出
}
function addVertex(v){
this.vertices.push(v);
this.adjList.set(v,[]);
}
function addEdge(v,w){
this.adjList.get(v).push(w);
this.adjList.get(w).push(v); //无向图则两者都需要,有向图可酌情选一边
}
function toString(){
let s = '';
for(let i=0; i<this.vertices.length; i++){
s += this.vertices[i] + '->';
let neighbors = this.adjList.get(this.vertices[i]);
for(let j=0; j<neighbors.length; j++){
s += neighbors[j] + ' ';
}
s += '\n';
}
return s;
}
//测试
var graph = new Graph();
var myVertices = ['A','B','C','D','E','F','G','H','I'];
myVertices.forEach((i) => { graph.addVertex(i); });
graph.addEdge('A', 'B');
graph.addEdge('A', 'C');
graph.addEdge('A', 'D');
graph.addEdge('C', 'D');
graph.addEdge('C', 'G');
graph.addEdge('D', 'G');
graph.addEdge('D', 'H');
graph.addEdge('B', 'E');
graph.addEdge('B', 'F');
graph.addEdge('E', 'I');
console.log(graph.toString());
/*输出:
A->B C D
B->A E F
C->A D G
D->A C G H
E->B I
F->B
G->C D
H->D
I->E
*/
8.2、图的遍历:
我们可以访问图中的所有节点。有两种算法可以对图进行遍历:广度优先搜索(BFS)和深度优先搜索(DFS)。图遍历可以用来寻找特定的顶点或寻找两个顶点之间的路径,检查图是否连通,检查图是否有环等。
遍历思想:必须追踪每个第一次访问的节点,并且追踪有哪些节点还没有被完全探索。对于两种图遍历算法,都需要明确指出第一个被访问的顶点。完全探索一个顶点要求我们查看该顶点的每一条边。对于每一条边所连接的没有被访问过的顶点,将其标注为被发现的,并将其加进待访问顶点列表中。
算法 | 数据结构 | 描述 |
---|---|---|
深度优先搜索 | 栈 | 通过将顶点存入栈中,顶点是沿着路径被探索的,存在新的相邻顶点就去访问 |
广度优先搜索 | 队列 | 通过将顶点存入队列中,最先入队列的顶点先被探索 |
* 广度优先搜索算法会从指定的第一个顶点开始遍历图,先访问其所有的相邻点,就像一次访
问图的一层。换句话说,就是先宽后深地访问顶点,如下图所示:
- 深度优先搜索算法将会从第一个指定的顶点开始遍历图,沿着路径直到这条路径最后一个顶点被访问了,接着原路回退并探索下一条路径。换句话说,它是先深度后广度地访问顶点,如下
图所示:
关于二者的应用,可以在牛客网上多刷点题,自然就懂了,其中深度优先遍历需要注意回溯,广度优先遍历需要注意辅助空间。广度优先遍历常用来求最短路经,深度优先遍历用来寻找符合要求的线路。
9、排序算法
关于javascript的排序算法,我专门整理了一篇博客,传送门:十大排序算法—javaScript详解
10、算法
10.1 搜索算法
顺序搜索很简单,for循环就行。我们用的比较多的是二分搜索:
这个算法要求被搜索的数据结构已排序。以下是该算法遵循的步骤。
(1) 选择数组的中间值。
(2) 如果选中值是待搜索值,那么算法执行完毕(值找到了)。
(3) 如果待搜索值比选中值要小,则返回步骤1并在选中值左边的子数组中寻找。
(4) 如果待搜索值比选中值要大,则返回步骤1并在选种值右边的子数组中寻找。
以下是其实现:
function binarySearch(array,item){
let low = 0,
high = array.length-1;
mid,element;
while(low <= high){
mid = Math.floor((low+high)/2);
element = array[mid];
if(element < item){
low = mid + 1;
}else if(element > item){
high = mid-1;
}else{
return mid;
}
}
return -1; //没找到,返回-1
}
二分法应用广泛,很多排序算法就应用二分加递归节省时间,如快排、归并等。递归算法用的太多,都很熟悉,就不单独说了,为何用递归呢?更快吗?递归并不比普通版本更快,反倒更慢。但要知道,递归更容易理解,更容易解决问题,并且它所需的代码量更少。
10.2 动态规划
动态规划(Dynamic Programming,DP)是一种将复杂问题分解成更小的子问题来解决的优化技术。要注意动态规划和分而治之(归并排序和快速排序算法中用到的那种)是不同的方法。分而治之方法是把问题分解成相互独立的子问题,然后组合它们的答案,而动态规划则是将问题分解成相互依赖的子问题。
用动态规划解决问题时,要遵循三个重要步骤:
(1) 定义子问题;
(2) 实现要反复执行而解决子问题的部分(这一步要参考前一节讨论的递归的步骤);
(3) 识别并求解出边界条件。
能用动态规划解决的一些著名的问题如下。
* 背包问题:给出一组项目,各自有值和容量,目标是找出总值最大的项目的集合。这个
问题的限制是,总容量必须小于等于“背包”的容量。
* 最长公共子序列:找出一组序列的最长公共子序列(可由另一序列删除元素但不改变余
下元素的顺序而得到)。
* 矩阵链相乘:给出一系列矩阵,目标是找到这些矩阵相乘的最高效办法(计算次数尽可
能少)。相乘操作不会进行,解决方案是找到这些矩阵各自相乘的顺序。
* 硬币找零:给出面额为d1…dn的一定数量的硬币和要找零的钱数,找出有多少种找零的
方法。
* 图的全源最短路径:对所有顶点对(u, v),找出从顶点u到顶点v的最短路径。
10.3 贪心算法
贪心算法遵循一种近似解决问题的技术,期盼通过每个阶段的局部最优选择(当前最好的解),从而达到全局的最优(全局最优解)。它不像动态规划那样计算更大的格局。比起动态规划算法而言,贪心算法更简单、更快。然而,如我们所见,它并不总是得到最优答案。但是综合来看,它相对执行时间来说,输出了一个可以接受的解。例如0-1背包问题等。
算法原理讲了反而容易糊涂,只能依靠多刷题,自己总结规律,然后对题目培养敏锐度,知道什么时候该用什么算法。