笔记2.1
第七章 字典
封装基本Api
function Dictionary() {
var items = {};
this.has = function (key) {
return items.hasOwnProperty(key)
}
this.set = function (key,value) {
items[key] = value;
}
this.delete = function (key) {
if(this.has(key)){
delete items[key]
return true
}
return false
}
this.get = function (key) {
return items[key]
// 如果items[key] 不存在, 好像不会报错, 而是直接返回 undefined
}
this.values = function () {
var arr = [];
for (let key in items) {
if(this.has(key)) {
arr.push(items[key])
}
}
return arr
/ 或者
return Object.values(items)
}
this.keys = function () {
var arr = [];
for (let key in items) {
if(this.has(key)) {
arr.push(key)
}
}
return arr
/ 或者
return Object.keys(items)
}
this.size = function () {
let len = 0;
for(let key in items) {
if (this.has(key)) {
len++
}
}
return len
/或者
return Object.keys(items).length
}
this.getItems = function () {
return items
}
}
其实跟上面集合的封装非常相似. 只不过 集合用的是 值 值
字典用的是 键 值
不过这么封装 和上面的 集合封装一样,
有个缺陷就是, key 都会 隐式转换成字符串,
有些 key 会互相覆盖 比如 1 和 '1',
并且引用值无法作为 key,
可以考虑用 Map
7.2 散列表 到HashTable类
散列算法的作用是尽可能快地在数据结构中找到一个值。在之前的章节中,你已经知道如果
要在数据结构中获得一个值(使用get方法),需要遍历整个数据结构来找到它。如果使用散列
函数,就知道值的具体位置,因此能够快速检索到该值。散列函数的作用是给定一个键值,然后
返回值在表中的地址。
lose lose 散列函数
其实主要看这个函数,
var loseloseHashCode = function(key) {
var hash = 0; //{1}
for(var i = 0; i < key.length; i++) { //{2}
hash += key.charCodeAt(i); //{3}
}
return hash % 37; //{4}
};
这个函数的作用是, 把传进来的key值 经过计算 变成一个数字,
让这个数字成为 新的key
这么做的第一个好处是, 能够把1 和 '1' 区分开来, 不会互相覆盖.
因为'1' 的值会转换成 charCodeAt(i) (忘了是ask码 还是 unicode码了)
不过很明显是有缺陷的, 比如 '1' 的 charCodeAt(i) 值是 47
换言之, '1' 和 数字 47 是互相覆盖的.
这称之为冲突
这个函数第{4} 行 是对 37 做了个取模,
换言之一旦不同的数据超过37个, 一定会发生互相冲突的情形.
这个函数有很多缺陷, 比如这个函数似乎只处理了 key 为 字符串的情形.
我们先把其他api 写一下
this.put = function(key, value) {
var position = loseloseHashCode(key); //{5}
console.log(position + ' - ' + key); //{6}
table[position] = value; //{7}
};
this.remove = function(key) {
table[loseloseHashCode(key)] = undefined;
};
this.get = function(key) {
return table[loseloseHashCode(key)];
};
如何解决冲突?
1. 分离链接
这样做,实际上就变成了, 根据lose 函数返回的不同值,创建不同的链表.
之前跟丁老师学习模拟 map 封装时, 用的就是这个方法.
function HashTable() {
var ValuePair = function(key, value) {
this.key = key;
this.value = value;
this.toString = function() {
return '[' + this.key + ' - ' + this.value + ']';
}
}
this.put = function(key, value) {
var position = loseloseHashCode(key);
if(table[position] == undefined) { //{1}
table[position] = new LinkedList();
}
table[position].append(new ValuePair(key, value)); //{2}
};
this.get = function(key) {
var position = loseloseHashCode(key);
if(table[position] !== undefined) { //{3}
//遍历链表来寻找键/值
var current = table[position].getHead(); //{4}
while(current.next) { //{5}
if(current.element.key === key) { //{6}
return current.element.value; //{7}
}
current = current.next; //{8}
}
//检查元素在链表第一个或最后一个节点的情况
/ 这里我们可以看到另一种循环迭代的技巧,
/ 即, 循环的第一个和最后一个可以进行特殊处理
/ 有时候我们之所以很难就是因为我们想要在循环的时候
/ 对第一个和最后一个, 也就是边界情况进行兼容 所以才会很困难.
/ 我看作者这种方式很有用
if(current.element.key === key) { //{9}
return current.element.value;
}
}
return undefined; //{10}
};
this.remove = function(key) {
var position = loseloseHashCode(key);
if(table[position] !== undefined) {
var current = table[position].getHead();
while(current.next) {
if(current.element.key === key) { //{11}
table[position].remove(current.element); //{12}
if(table[position].isEmpty()) { //{13}
table[position] = undefined; //{14}
}
return true; //{15}
}
current = current.next;
}
// 检查是否为第一个或最后一个元素
if(current.element.key === key) { //{16}
table[position].remove(current.element);
if(table[position].isEmpty()) {
table[position] = undefined;
}
return true;
}
}
return false; //{17}
};
}
2. 线性探查
另一种解决冲突的方法是线性探查。当想向表中某个位置加入一个新元素的时候,如果索引
为index的位置已经被占据了,就尝试index+1的位置。如果index+1的位置也被占据了,就尝试
index+2的位置,以此类推。
function HashTable() {
var ValuePair = function(key, value) {
this.key = key;
this.value = value;
this.toString = function() {
return '[' + this.key + ' - ' + this.value + ']';
}
}
this.put = function(key, value) {
var position = loseloseHashCode(key); // {1}
if(table[position] == undefined) { // {2}
table[position] = new ValuePair(key, value); // {3}
} else {
var index = ++position; // {4}
while(table[index] != undefined) { // {5}
index++; // {6}
}
table[index] = new ValuePair(key, value); // {7}
}
};
this.get = function(key) {
var position = loseloseHashCode(key);
if(table[position] !== undefined) { //{8}
if(table[position].key === key) { //{9}
return table[position].value; //{10}
} else {
var index = ++position;
while(table[index] === undefined ||
table[index].key !== key) { //{11}
index++;
}
if(table[index].key === key) { //{12}
return table[index].value; //{13}
}
}
}
return undefined; //{14}
};
this.remove = function(key) {
var position = loseloseHashCode(key);
if(table[position] !== undefined) { //{8}
if(table[position].key === key) { //{9}
table[index] = undefined; //{10}
} else {
var index = ++position;
while(table[index] === undefined ||
table[index].key !== key) { //{11}
index++;
}
if(table[index].key === key) { //{12}
table[index] = undefined; //{13}
}
}
}
return undefined; //{14}
};
};
个人觉得这个方法是有很大的缺陷的, 虽然没经过测试,
但很明显, 这个方法无法区分相同的key值, 比如 我put 两次 {'a' : 'b'},
按照这个代码的逻辑, 似乎不会进行覆盖.(也许应该在put 里 设置一下 key值检查)
而且如果这个方案配合 lose 函数, 感觉非常没意思. 跟不用 lose 函数基本没什么区别吧?
介绍了一个更好的 散列函数
var djb2HashCode = function(key) {
var hash = 5381; //{1}
for(var i = 0; i < key.length; i++) { //{2}
hash = hash * 33 + key.charCodeAt(i); //{3}
}
return hash % 1013; //{4}
};
其实这个函数只看 第{4} 行就能得知, 最后获得的值 必然小于1013
即, 超过1013个的数据 一定会发生冲突.
(其实, 作为一个小白, 我不太明白, 为什么一定要最后% 一个数, 难道是因为怕数字太大?)
个人觉得如果同时用好几个不同的散列函数,
进行组合标记一个key, 能够大大减少冲突.
第八章 树
8.3 二叉树和二叉搜索树
二叉树 : 每个节点 最多只有左右两个枝
二叉搜索树(BST): 是二叉树的一种,但是它只允许你在左侧节点存储(比父节点)小的值,
在右侧节点存储(比父节点)大(或者等于)的值。上一节的图中就展现了一棵二叉搜索树。
二叉搜索树封装
function BinarySearchTree() {
var Node = function(key) { //{1}
this.key = key;
this.left = null;
this.right = null;
};
var root = null; //{2}
插入
this.insert = function(key) {
var newNode = new Node(key); //{1}
if(root === null) { //{2}
root = newNode;
} else {
insertNode(root, newNode); //{3}
}
};
var insertNode = function(node, newNode) {
if(newNode.key < node.key) { //{4}
if(node.left === null) { //{5}
node.left = newNode; //{6}
} else {
insertNode(node.left, newNode); //{7}
}
} else {
if(node.right === null) { //{8}
node.right = newNode; //{9}
} else {
insertNode(node.right, newNode); //{10}
}
}
};
中序遍历
this.inOrderTraverse = function(callback) {
inOrderTraverseNode(root, callback); //{1}
};
var inOrderTraverseNode = function(node, callback) {
if(node !== null) { //{2}
inOrderTraverseNode(node.left, callback); //{3}
callback(node.key); //{4}
inOrderTraverseNode(node.right, callback); //{5}
}
};
先序遍历
this.preOrderTraverse = function(callback) {
preOrderTraverseNode(root, callback);
};
var preOrderTraverseNode = function(node, callback) {
if(node !== null) {
callback(node.key); //{1}
preOrderTraverseNode(node.left, callback); //{2}
preOrderTraverseNode(node.right, callback); //{3}
}
};
后续遍历
this.postOrderTraverse = function(callback) {
postOrderTraverseNode(root, callback);
};
var postOrderTraverseNode = function(node, callback) {
if(node !== null) {
postOrderTraverseNode(node.left, callback); //{1}
postOrderTraverseNode(node.right, callback); //{2}
callback(node.key); //{3}
}
};
}
可以看到,二叉树这里开始, 就会用上大量的递归结构.
稍微观察一下即可以发现
- 使用递归时, 尽量不要用 对象调用方式执行函数, 而是改成 传参形式.
- 使用递归时, 必然存在 if else 结构, 因为要保证一个出口
另外,可以发现的是,
上面的封装当中, 之所以这棵树是个二叉搜索树 是由 插入 函数决定的.
实际上即使是普通的二叉树, 依然可以用 上面的 三种遍历方式.
如果是插入时没有什么顺序, 那么这个树可以是无序的存储结构,
如果插入时 依照了某种顺序, 那么这个树可以是有序的存储结构.
即, 我们创建一个有序二叉树时, 没有必要只是比较一个值的大小?
搜索最大值和最小值
其实对二叉搜索树而言, 就相当于 搜索最左值和最右值
this.min = function() {
return minNode(root); //{1}
};
var minNode = function(node) {
if(node) {
while(node && node.left !== null) { //{2}
node = node.left; //{3}
}
return node.key;
}
return null; //{4}
};
this.max = function() {
return maxNode(root);
};
var maxNode = function(node) {
if(node) {
while(node && node.right !== null) { //{5}
node = node.right;
}
return node.key;
}
return null;
};
搜索特定值
this.search = function(key) {
return searchNode(root, key); //{1}
};
var searchNode = function(node, key) {
if(node === null) { //{2}
return false;
}
if(key < node.key) { //{3}
return searchNode(node.left, key); //{4}
} else if(key > node.key) { //{5}
return searchNode(node.right, key); //{6}
} else {
return true; //{7}
/或者这里可以直接返回节点 return node
}
};
移除一个节点
说实话, 这是二叉树当中遇到的最难的部分,或者说最巧的部分
真的很难想得到.
这第一个想出来的,到底是用什么思路想到的呢?
var removeNode = function(node, key) {
if(node === null) { //{2}
return null;
}
if(key < node.key) { //{3}
node.left = removeNode(node.left, key); //{4}
return node; //{5}
} else if(key > node.key) { //{6}
node.right = removeNode(node.right, key); //{7}
return node; //{8}
} else { //键等于node.key
//第一种情况——一个叶节点
if(node.left === null && node.right === null) { //{9}
node = null; //{10}
return node; //{11}
}
//第二种情况——一个只有一个子节点的节点
if(node.left === null) { //{12}
node = node.right; //{13}
return node; //{14}
} else if(node.right === null) { //{15}
node = node.left; //{16}
return node; //{17}
}
//第三种情况——一个有两个子节点的节点
var aux = findMinNode(node.right); //{18}
node.key = aux.key; //{19}
node.right = removeNode(node.right, aux.key); //{20}
return node; //{21}
}
};
/个人认为第三种情况的处理实在是太巧妙了,
/ 这绝对是对概念的透彻理解之上才能做出这种操作.
/ 说了两句废话..
var findMinNode = function(node) {
while(node && node.left !== null) {
node = node.left;
}
return node;
};
跟我以前见过的递归函数不同
以前见过的递归的返回值一般是这样的
第一种
function fn (a) {
if (flag) {
return fn(a)
} else{
return 'some'
}
}
而这里是这种感觉
第二种
function fn (a) {
if (flag) {
a.b = fn(a.b)
return a
} else{
return 'some'
}
}
我觉得这里存在一种比较大的区别, 但我无法很好的归类这是什么问题
从形式上讲, 第二种是, 我传入的值和他返回的值是同一个引用值.
从某种角度上来讲, 这么做的一个用途是, 能够保持这个引用值的整体结构.
我感觉这俩的一个还有一个区别可能是,
第一种给我的感觉是, 他提供的是一个值, 并且返回的也是一个值,
或者极有可能的情形是, 每次递归的返回值, 可能是下一个循环需要的值.
比如
return fn(a) + 1 这种情况下, 第二次返回的值,对第一次返回值是有影响的.
一种reduce 的感觉.
而第二种给我的感觉是, 他做的是一个搜索工作, 提供的是一个地址?(当然也是值),
他不需要前一次的运算并不需要后一次的运算结果参与.
一种forEach的感觉.
8.6 自平衡树
Adelson-Velskii-Landi 树(AVL 树)
AVL树是一种自平衡二叉搜索树,意思是任何
一个节点左右两侧子树的高度之差最多为1。也就是说这种树会在添加或移除节点时尽量试着成
为一棵完全树。
插入
var insertNode = function(node, element) {
if(node === null) {
node = new Node(element);
} else if(element < node.key) {
node.left = insertNode(node.left, element);
if(node.left !== null) {
// 确认是否需要平衡 {1}
}
} else if(element > node.key) {
node.right = insertNode(node.right, element);
if(node.right !== null) {
// 确认是否需要平衡 {2}
}
}
return node;
};
可以看到从形式上来讲, 这跟上面的insert的递归方式不相同, 会return 一个 node,
更接近我们刚才讲的 remove, 即, 有保持结构, 拼接的意思.
计算平衡因子
对每个节点计算右子树高度(hr)和左子树高度(hl)的差值,该值
(hr-hl)应为0、1或1。如果结果不是这三个值之一,则需要平衡该AVL树。这就是平衡因子
的概念。
计算高度的函数
var heightNode = function(node) {
if(node === null) {
return -1;
} else {
return Math.max(heightNode(node.left),
heightNode(node.right)) + 1;
}
};
我勒个去, 就这个函数, 打死我都想不出来啊!
太巧妙了.
从概念上讲, 还是能理解的,
即, 每次递归,我加一层,
左右树当中, 哪个高,就选哪一个.
但我感觉我写不出来.
这绝对是我见过的递归中, 最巧妙的一个之一了吧.
也许我想得复杂是因为, 我总是想要思考步骤,
所以跟着递归走两回 就头晕脑胀,
而这些人写递归的时候, 想得不是步骤,
而是概念的组合?
一个概念是, 每次递归加一层,
一个概念是, 左右哪个更大选哪个,
这两个进行了组合?
反正我觉得很牛逼啊
// 替换insertNode方法的行{1}
if((heightNode(node.left) - heightNode(node.right)) > 1) {
// 旋转 {3}
}
向右子树插入新节点时, 应用同样的逻辑, 代码如下:
// 替换insertNode方法的行{2}
if((heightNode(node.right) - heightNode(node.left)) > 1) {
// 旋转 {4}
}
var rotationRR = function(node) {
var tmp = node.right; // {1}
node.right = tmp.left; // {2}
tmp.left = node; // {3}
return tmp;
};
var rotationLL = function(node) {
var tmp = node.left; // {1}
node.left = tmp.right; // {2}
tmp.right = node; // {3}
return tmp;
};
var rotationLR = function(node) {
node.left = rotationRR(node.left);
return rotationLL(node);
};
var rotationRL = function(node) {
node.right = rotationLL(node.right);
return rotationRR(node);
};
插入完整版
var insertNode = function(node, element) {
if(node === null) {
node = new Node(element);
} else if(element < node.key) {
node.left = insertNode(node.left, element);
/ 其实这里是, 先插进去, 然后再进行判断,然后再调整
/ 也许存在方法是, 直接插进去的时候, 就按照平衡的规则插入?
if(node.left !== null) {
if((heightNode(node.left) - heightNode(node.right)) > 1) {/ 计算平衡因子
// 旋转 {3}
if(element < node.left.key) {
node = rotationLL(node);
} else {
node = rotationLR(node);
}
}
}
} else if(element > node.key) {
node.right = insertNode(node.right, element);
if(node.right !== null) {
if((heightNode(node.right) - heightNode(node.left)) > 1) {
// 旋转 {4}
if(element > node.right.key) {
node = rotationRR(node);
} else {
node = rotationRL(node);
}
}
}
}
return node;
};
理解倒是能理解, 但如果让我自己研究出来估计要很长很长时间.
主要是概念整理得好!
红黑树,和 堆积树
书中作为扩展来提及, 看来,这本书可能就是为了我们小白写得,
图
图是网络结构的抽象模型。图是一组由边连接的节点(或顶点)。学习图是重要的,因为任
何二元关系都可以用图来表示。
- 相邻顶点:由一条边连接在一起的顶点
- 一个顶点的度 : 相邻顶点的数量
- 路径 : 顶点v1, v2,…,vk的一个连续序列
- 简单路径 : 要求不包含重复的顶点 (环也是一个简单路径)
有向图和无向图
- 强连通: 如果图中每两个顶点间在双向上都存在路径,则该图是强连通的
(我的理解是, 两个顶点之间如果是双向的, 这两个顶点之间是强联通的, 但不能说整个图是强联通图)
(图中所有的顶点之间都是双向时, 才能说整个图是强联通图.)
(不对,百度百科里的定义不太一样: 强连通(Strongly Connected)是指一个有向图(Directed Graph)中任意两点v1、v2间存在v1到v2的路径(path)及v2到v1的路径。)
(这就表示没两点之间起码存在两条路径, 而顶点之间不一定非得是双向的) - 加权 与 未加权
9.2 图的表示
这里说的图的表示是, 用数学或者代码的形式来表示.
而非用画图的方式来表示.
9.2.1 邻接矩阵
矩阵用代码表示, 就是二维数组,
即,用二维数组来表示.
9.2.2 邻接表
存在好几种方式来表示这种数据结构。我们可以用列表(数组)、链表,甚至是
散列表或是字典来表示相邻顶点列表。
9.2.3 关联矩阵
邻接矩阵,邻接表都是用点来表示的,
而关联矩阵则是用点和边来表示的
在关联矩阵中,矩阵的行表示顶点,列表示边。如下图所
示,我们使用二维数组来表示两者之间的连通性,如果顶点v是边e的入射点,则array[v][e] === 1;
否则,array[v][e] === 0。
说实话, 这幅图配的有点莫名其妙的, 完全看不懂和 关联矩阵有毛关系?
下面是百度百科的,
9.3 创建 Graph 类
书中是打算实现 邻接表 来表示图(即上面的邻接矩阵和关联矩阵就略过去了)
function Graph() {
var vertices = []; //{1} 放顶点
var adjList = new Dictionary(); //{2} 放顶点的相邻顶点
}
this.addVertex = function(v) { // 添加顶点
vertices.push(v); //{3}
adjList.set(v, []); //{4}
};
this.addEdge = function(v, w) { // 添加顶点的相邻顶点
adjList.get(v).push(w); //{5}
adjList.get(w).push(v); //{6}
};
this.toString = function() {// 这个函数的功能是 展示 都有哪些顶点的关系
var s = '';// 实际上可以看成是如何遍历的功能
for(var i = 0; i < vertices.length; i++) { //{10}
s += vertices[i] + ' -> ';
var neighbors = adjList.get(vertices[i]); //{11}
for(var j = 0; j < neighbors.length; j++) { //{12}
s += neighbors[j] + ' ';
}
s += '\n'; //{13}
}
return s;
};
9.4 图的遍历
大名鼎鼎的 深度优先遍历和广度优先遍历在这里
这里的关键是, 访问和探索是两个行为.
访问的意思是, 访问之后的顶点会被放入 待探索的列表之中.
辅助函数
var initializeColor = function() {
var color = [];
for(var i = 0; i < vertices.length; i++) {
/初始化 让所有顶点的状态为 未访问,即,白色
color[vertices[i]] = 'white'; //{1}
}
return color;
};
广度优先遍历
版本1
this.bfs = function(v, callback) {/ v 是要遍历的起点
var color = initializeColor(), //{2}
queue = new Queue(); //{3}
queue.enqueue(v); //{4}
while(!queue.isEmpty()) { //{5}
var u = queue.dequeue(), //{6}/ 取出顶点
neighbors = adjList.get(u); //{7} / 取出该顶点的 相邻顶点数组
color[u] = 'grey'; // {8}/表示已经被访问过了
for(var i = 0; i < neighbors.length; i++) { // {9}
var w = neighbors[i]; // {10}
if(color[w] === 'white') { // {11}
color[w] = 'grey'; // {12}
queue.enqueue(w); // {13}/放进队列中
}
}
color[u] = 'black'; // {14}//表示 , 已经完成访问, 也完成 callback了
if(callback) { // {15}
callback(u);
}
}
};
版本2 : 使用BFS寻找最短路径
this.BFS = function(v) {
var color = initializeColor(),
queue = new Queue(),
d = [], //{1}
pred = []; //{2}
queue.enqueue(v);
for(var i = 0; i < vertices.length; i++) { //{3}
d[vertices[i]] = 0; //{4}
pred[vertices[i]] = null; //{5}
}
while(!queue.isEmpty()) {
var u = queue.dequeue(),
neighbors = adjList.get(u);
color[u] = 'grey';
for(i = 0; i < neighbors.length; i++) {
var w = neighbors[i];
if(color[w] === 'white') {
color[w] = 'grey';
d[w] = d[u] + 1; //{6}
pred[w] = u; //{7}
queue.enqueue(w);
}
}
color[u] = 'black';
}
return { //{8}
distances: d,
predecessors: pred
};
};
可以看到 distances 表示的是 离起始遍历点的距离,
pred 是表示 该顶点是被哪个顶点"举报的"
var myVertices = ['A','B','C','D','E','F'];
var shortestPathA = graph.BFS(myVertices[0]);
var fromVertex = myVertices[0]; //{9}
for(var i = 1; i < myVertices.length; i++) { //{10}
var toVertex = myVertices[i], //{11}
path = new Stack(); //{12}
for(var v = toVertex; v !== fromVertex; v = shortestPathA.predecessors[v]) { //{13}
path.push(v); //{14}
}
path.push(fromVertex); //{15}
var s = path.pop(); //{16}
while(!path.isEmpty()) { //{17}
s += ' - ' + path.pop(); //{18}
}
console.log(s); //{19}
}
也能看懂.
深入最短路径算法
举些例子,Dijkstra算法解决了单源最短路径问题。Bellman-Ford算法解决了边权值为负的
单源最短路径问题。A*搜索算法解决了求仅一对顶点间的最短路径问题,它用经验法则来加速搜
索过程。Floyd-Warshall算法解决了求所有顶点对间的最短路径这一问题。
与上面提到的红黑树和堆积树一样, 只是提及, 书中并不介绍
9.4.2 深度优先搜索
this.dfs = function(callback) {
var color = initializeColor(); //{1} / 状态初始化
for(var i = 0; i < vertices.length; i++) { //{2} 注意 这里 和 行 {8} 以及 行{11}
if(color[vertices[i]] === 'white') { //{3}// 之所以遍历顺序为深度优先就取决于 {2}{8}{11} 了
dfsVisit(vertices[i], color, callback); //{4}
}
}
};
var dfsVisit = function(u, color, callback) {
color[u] = 'grey'; //{5}
if(callback) { //{6}
callback(u);
}
var neighbors = adjList.get(u); //{7}
for(var i = 0; i < neighbors.length; i++) { //{8}
var w = neighbors[i]; //{9}
if(color[w] === 'white') { //{10}
dfsVisit(w, color, callback); //{11}
}
}
color[u] = 'black'; //{12}
};
实话实说, 这一部分有点失望,
他在上面实现广度优先时 用的是迭代,
而在这里实现深度优先时, 用的是 递归.
不是说不行, 而是说, 要么两个都用递归,都用迭代, 这样好做比较吧?
或者说, 上面用得是 队列, 这里应该用栈,
但用递归之后, 无法体现栈了吧?
我试着简单改了一下
this.dfs = function(callback) {
var color = initializeColor(); //{1}
for(var i = 0; i < vertices.length; i++) { //{2}
var u = vertices[i];
if(color[u] === 'white') { //{3}
color[u] = 'grey'; //{5}
if(callback) { //{6}
callback(u);
}
var neighbors = adjList.get(u); //{7}
for(var i = 0; i < neighbors.length; i++) { //{8}
var w = neighbors[i]; //{9}
if(color[w] === 'white') { //{10}
color[w] = 'grey'; //{5}
if(callback) { //{6}
callback(w);
}
color[w] = 'black';
}
}
color[u] = 'black';
}
}
};
想继续改成 用Stack , 发现没我想象的那么简单. 回头再想想吧
1. 探索深度优先算法
对于给定的图G,我们希望深度优先搜索算法遍历图G的所有节点,构建“森林”(有根树的
一个集合)以及一组源顶点(根),并输出两个数组:发现时间和完成探索时间。我们可以修改
dfs方法来返回给我们一些信息:
var time = 0; //{1}
this.DFS = function() {
var color = initializeColor(), //{2}
d = [],
f = [],
p = [];
time = 0;
for(var i = 0; i < vertices.length; i++) { //{3}
f[vertices[i]] = 0;
d[vertices[i]] = 0;
p[vertices[i]] = null;
}
for(i = 0; i < vertices.length; i++) {
if(color[vertices[i]] === 'white') {
DFSVisit(vertices[i], color, d, f, p);
}
}
return { //{4}
discovery: d,
finished: f,
predecessors: p
};
};
var DFSVisit = function(u, color, d, f, p) {
console.log('discovered ' + u);
color[u] = 'grey';
d[u] = ++time; //{5}
var neighbors = adjList.get(u);
for(var i = 0; i < neighbors.length; i++) {
var w = neighbors[i];
if(color[w] === 'white') {
p[w] = u; // {6}
DFSVisit(w, color, d, f, p);
}
}
color[u] = 'black';
f[u] = ++time; //{7}
console.log('explored ' + u);
};
到这里, 也不是不能理解.还是能看懂的.
如果广度优先遍历, 也配一下 发现时间和完成时间的话我猜应该是这样的
拓扑排序
有向无环图(DAG)。
拓扑排序 : 要编排一些任务或步骤的执行顺序时
拓扑排序只能应用于DAG(有向无环图) -- 能理解, 有向代表顺序, 无环代表不需要重复
也就是说, 深度优先遍历 能用于 按步骤, 按顺序执行的相关内容?