本章中,将学习另外一种非线性数据结构——图。这是学习的最后一种数据结构,后面将学习排序和搜索算法。

第九章 图

图的相关术语

图是网络结构的抽象模型。图是一组由边连接的节点(或顶点)。学习图是重要的,因为在任何二元关系都可以用图来表示。

任何社交网络都可以用图来表示。

我们还可以用图来表示道路、航班以及通信状态

一个图 G= (V,E)由以下元素组成。

  • V:一组顶点
  • E:一组边。连接V中的顶点

由一条边连接在一起的顶点称为相邻顶点。比如,A和B 是相邻的,A和D是相邻的,A和C是相邻的,A和E是不相邻的。

一个顶点的度是其相邻顶点的数量。比如,A和其他三个顶点相连接,因此,A的度为3;E和其他两个顶点相连接,因此E的度为2.

路径是顶点v1,v2,...vk的一个连续序列,其中 vi 和 vi+1 (下标)是相邻的。以上一示意图为例,其中包含的路径A B E I 和 A C D G。

简单路径要求不包含重复的顶点。举个例子,A D G是一条简单路径。除去最后一个顶点(因为它和第一个顶点是同一个顶点),环也是简单路径,比如A D C A(最后一个顶点重新回到A)

如果途中不存在环则称该图是无环的。如果图中每两个顶点间都存在路径,则该图是连通的。

有向图和无向图

图可以是无向的(边没有方向)或是有向的(有向图)。下图就是有向图。

有向图的边有一个方向。如果图中每两个顶点间在双向上都存在路径,则该图是强连通的。例如,C和D就是强连通的。图还可以是未加权的或者加权的。加权图的边被赋予了权值。

我们可以使用图来解决计算机科学世界中的很多问题,比如搜索图中的一个特定顶点或搜索一条特定边,寻找图中的一条路径(从一个顶点到另一个顶点),寻找两个顶点之间的最短路径。

图的表示

从数据结构角度来说,我们有多种方式来表示图。在所有表示法中,不存在绝对正确的方法方式。图的正确表示法取决于解决的问题和图的类型。

邻接矩阵

图最常见的实现就是邻接矩阵。每个节点和一个整数相关联,该整数将作为数组的索引。我们用一个二维数组来表示顶点之间的连接。如果索引为i的节点和索引为 j的节点为邻,则 array[i][j] === 1,否则 array[i][j] === 0,如下图所示:

不是强连通的图(稀疏图)如果用邻接矩阵来表示,则矩阵中将会有很多0,这意味着我们浪费了计算机存储空间来表示根本不存在的边。例如,找给定顶点的相邻顶点,即使该顶点只有一个相邻的顶点,我们也不得不迭代一整行。邻接矩阵表示法不够好的另一个理由是,图中顶点的数量可能会变化,而二维数组不太灵活。

邻接表

邻接表

我们也可以使用一种叫做邻接表的动态数据来表示图。邻接表由图中每个顶点相邻顶点列表所组成。存在好几种方式来表示这种数据结构。我们可以用列表(数组)、链表,甚至是散列表或者是字典来表示相邻顶点列表。下面的示意图展示了邻接表等数据结构。

尽管邻接表可能对大多数问题来说都是更好的选择,但以上两种表示法都有用,且它们有着不同的性质(例如,要找出顶点 vw是否相邻,使用邻接矩阵会比较快)。在本章中,就会使用邻接表表示法。

关联矩阵

我们还可以用关联矩阵来表示图。在关联矩阵中,矩阵的行表示顶点,列表示边。如下图所示,我们使用二维数组来表示两者之间的连通性,如果顶点 v 是 边e的入射点,则 array[v][e] === 1,否则 array[v][e] === 0

创建 Graph 类

我们先创建类的骨架

function Graph(){
var vertices = [];
var adjList = new Dictionary();
}

我们使用一个数组来存储图中所有顶点的名字,以及一个字典来存储邻接表。字典将会使用顶点的名字作为键,邻接顶点列表作为值。 vertices数组和 adjList字典两者都是我们 Graph类的私有属性。

接着我们实现两个方法:一个用来向图中添加一个新的顶点(因为图实例化后是空的),另外一个方法用来添加顶点之间的边。我们先实现 addVertex 方法

this.addVertex = function(v){
vertices.push(v);
adjList.set(v,[]);
}

这个方法接受顶点 v 作为参数。我们将该顶点添加到顶点列表中,并且在邻接表中,设置顶点 v 作为键对应的字典值为一个空数组。

实现 addEdge 方法

this.addEdge = function(v,w){
adjList.get(v).push(w);
adjList.get(w).push(v);
}

这个方法接受两个顶点作为参数。首先,通过将 w 加入到 v 的邻接表中,我们添加了一条自顶点 v 到顶点 w 的边。如果你想实现一个有向图,则(adjList.get(v).push(w))就足够了。但是本章中大多数的例子都是基于无向图,我们需要添加一条自w向v的边。

测试

const graph = new Graph();
const myVertices = ['A','B','C','D','E','F','G','H','I'];
for(var i = 0; i < myVertices.length; i++){
graph.addVertex(myVertices[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');

实现 Graph类的 toString 方法,便于在控制台输出图

this.toString = function(){
var s = '';
for(var i = 0; i < vertices.length; i++){
s += vertices[i] + ' -> ';
var neighbors = adjList.get(vertices[i]);
for(var j = 0; j < neighbors.length; j++){
s += neighbors[j] + ' ';
}
s += '\n';
}
return s;
}

我们为邻接表表示法构建了一个字符串,首先迭代 vertices 数组列表,将顶点的名字加入字符串中,接着取得该顶点的邻接表,同样也迭代该邻接表,将相邻顶点加入我们的字符串。邻接表迭代完成后,给我们的字符串添加一个换行符。这样就可以在控制看到一个漂亮的输出了。

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

图的遍历

和树数据结构相似,我们可以访问图的所有节点。有两种算法可以对图进行遍历:广度优先搜索(Breadth-First Search,BFS)和深度优先搜索(Depth-First Search,DFS)。图遍历可以用来寻找特定的顶点或者是寻找两个顶点之间的路径,检查图是否连通,检查图是否含有环等。

图遍历算法的思想是必须追踪每个第一次访问的节点,并且追踪有哪些节点还没有被完全探索,对于两种图遍历算法,都需要明确指出第一个被访问的节点。

完全探索一个顶点要求我们查看该顶点的每一条边。对于每一条边所连接的没有被访问过的顶点,将其标注为被发现的,并将其加入待访问的顶点。

为了保证算法的效率,务必访问每个顶点至多两次。连通图中每条边和顶点都会被访问到。

广度优先搜索算法和深度优先搜索算法基本上是相同的,只有一点不同,那就是待访问顶点列表的数据结构。

算法 数据结构 描述
深度优先搜索 通过将顶点存入栈中,顶点是沿着路径被探索的,存在新的相邻顶点就去访问
广度优先搜索 队列 通过将顶点存入队列,最先入队列的顶点先被探索

当要标注已经访问过的顶点时,我们可以用三种颜色来放映它们的状态

  • 白色:表示该顶点还没有被访问
  • 灰色:表示该顶点被访问过,但是还没有探索过
  • 黑色:表示该顶点被访问过且被完全探索过

广度优先搜索

广度优先搜索算法会从指定的第一个顶点开始遍历图,会访问其所有相邻点,就像一次访问图的一层。换句话说,就是先宽后深地访问顶点,如下图所示

以下是从顶点 v 开始的广度优先搜索算法所遵循的步骤

  1. 创建一个队列 Q
  2. 将 v 标注为被发现的(灰色),并将 v 入队列Q
  3. 如果队列Q 非空,则运行以下步骤
    1. 将 u 从 Q 中出队列
    2. 将标注 u 为被发现的(灰色)
    3. 将 u 所有未被访问过的邻点(白色)入队列
    4. 将 u 标注为已被探索的(黑色)

实现广度优先搜索算法

// 颜色辅助-广度优先搜索算法
this.initializeColor = function(){
var color = [];
for(var i = 0; i < vertices.length; i++){
color[vertices[i]] = 'white';
}
return color;
}
// 广度优先搜索算法
this.bfs = function(v,callback){
var color = this.initializeColor(),
queue = new Queue();
queue.enqueue(v);
while(!queue.isEmpty()){
var u = queue.dequeue(),
neighbors = adjList.get(u);
color[u] = 'grey';
for(var i = 0; i < neighbors.length; i++){
var w = neighbors[i];
if(color[w] === 'white'){
color[w] = 'grey';
queue.enqueue(w);
}
}
color[u] = 'black';
if(callback){
callback(u)
}
}
}

广度优先搜索和深度优先搜索多需要标注被访问过的顶点,为此,我们将使用一个辅助数组 color,由于当算法开始执行时,所有的顶点颜色都是白色,所以我们可以创建一个辅助函数 initializeColor 为这两个算法执行此初始化操作。

我们要的第一件事情是用 initializeColor 函数来将 color 数组初始化为 white ,我们还需要声明和创建一个 Queue 实例,它将会存储待访问和待探索的顶点。

bfs 方法接受一点顶点作为算法的起始点。起始顶点是必要的,我们将此顶点如队列。

如果队列为空,我们将通过出队列操作从队列中移除一个顶点,并取得一个包含其所有邻点的邻接表。该顶点将被标注为 grey,表示我们已经发现了它(但还未被完全对其的探索)。

对于 u 的每个邻点,我们取得其值,如果它还未被访问过,则将其标注了grey,并将这个顶点加入队列中,这样当从队列中出列的时候,我们可以完成对其的探索。

当完全探索该顶点和及其邻点后,我们将标注该顶点为已探索过(黑色)。

我们实现的这个 bfs 方法也接受一个回调。这个参数是可选的,如果我们传递了回调函数,会用到它。

测试

function printNode(value){
console.log('访问了顶点:' + value);
}
graph.bfs(myVertices[0],printNode);

得到下面的结果

访问了顶点:A
访问了顶点:B
访问了顶点:C
访问了顶点:D
访问了顶点:E
访问了顶点:F
访问了顶点:G
访问了顶点:H
访问了顶点:I

顶点访问顺序和之前的示意图所展示的一致。

使用 BFS 寻找最短路径

到目前为止,我们只展示了 BFS 算法的基本原理。我们可以用该算法做更多事情,而不只是输出被访问顶点的顺序。例如,考虑如何来解决下面的问题。

给定一个图G和源顶点v,找出每个顶点u,u和v之间最短的路径(以边的数量计)

对于给定顶点v,广度优化算法会访问所有与其距离为1的顶点,接着是距离为2的顶点,以此类推。所以,可以用广度优先算法来解决这个问题。我们可以修改bfs方法以返回给我们一些信息:

  • 从v到u的距离d[u]
  • 前溯点pred[u],用来推导出从v到其他每个顶点u的最短路径。

实现:

// 广度优先搜索算法优化版本
this.BFS = function(v){
var color = this.initializeColor(),
queue = new Queue(),
d = [],
pred = [];
queue.enqueue(v); for(var i = 0; i < vertices.length; i++){
d[vertices[i]] = 0;
pred[vertices[i]] = null;
}
while(!queue.isEmpty()){
var u = queue.dequeue();
neighbors = adjList.get(u);
color[u] = 'grey';
for(var i = 0; i < neighbors.length; i++){
// w相邻顶点
var w = neighbors[i];
if(color[w] === 'white'){
color[w] == 'grey';
d[w] = d[u] + 1;
pred[w] = u;
queue.enqueue(w);
}
}
color[u] = 'black';
}
return {
distance:d,
predecessors: pred
}
}

首先需要声明数组 d 来表示距离,以及 pred 数组来表示前溯点。下一步用0来初始化数组d,把pred赋值为 null。

当我们发现 顶点u的相邻点w时,则设置w的前溯点值为u。我们还通过给d[u]加1来设置顶点v和相邻点w之间的距离。

方法的最后返回一个包含d和pred的对象。

测试

var shortestPathA = graph.BFS(myVertices[0]);
console.log(shortestPathA);
// distance: [A: 0, B: 1, C: 1, D: 2, E: 2, F: 2, G: 2, H: 3, I: 3]
// predecessors: [A: null, B: "A", C: "A", D: "C", E: "B", F: "B",G: "D", , H: "D", , I: "E"]

通过前溯数组,我们可以用下面这段代码来构建从顶点A到其他顶点的路径:

var fromVertex = myVertices[0];
for(var i = 1; i < myVertices.length; i++){
var toVertex = myVertices[i],
path = new Stack();
for(var v = toVertex; v !== fromVertex;v = shortestPathA.predecessors[v]){
path.push(v);
}
path.push(fromVertex);
var s = path.pop();
while(!path.isEmpty()){
s += '-' + path.pop();
}
console.log(s);
}

使用顶点A作为源顶点。对于每个其他顶点,我们会J计算顶点A到它的路径。我们从顶点数组得到toVertex ,然后会创建一个栈来 存储路劲值。

接着,我们追溯 toVertext 到 fromVertext 的路径。变量v被赋值为前溯点的值。这样我们就可以方向追溯这条路径。将变量v添加到栈中。最后,源顶点也会被添加到栈中,以得到完整的路径。

这之后,我们创建了一个s字符串,并将源顶点赋值给它。当栈是非空的时候,我们从栈中移出一个项并将其拼接到字符串s的后面。最后在控制台上输出路径。

A-B
A-C
A-C-D
A-B-E
A-B-F
A-C-D-G
A-C-D-H
A-B-E-I

深入学习的最短路径算法

本章中的图不是加权图。如果要计算加权图中的最短路径(例如,城市A 和城市B之间的最短路径——GPS和Google Map 中用到的算法),广度优先搜索未必合适。

举个栗子,Dijkstra 算法解决了单源中最短路径问题。Bellman-Ford 算法解决了边权值为负的单源最短路径问题。A*搜索算法解决了求仅一对顶点间的最短路径问题,它用经验法则来加速搜索过程。Floyd-Warshall算法解决了求所有顶点对间的最短路径的这一问题。

图是一个广泛的主体,对最短路径及其变种问题,我们有很多的解决方案。

深度优先搜索

深度优先搜索算法将会从第一个指定的顶点开始遍历图,沿着路径直到这条路径最后一个顶点被访问了,接着原路返回并探索下一条路径。换句话说,它是先深度后广度地访问顶点,如下图所示

深度优先搜索算法不需要一个源顶点。在深度优先搜索算法中,若图中顶点v未被访问,则访问该顶点。

要访问顶点v,照下列的步骤

  1. 标注v为被发现的(灰色)
  2. 对于v的所有未访问的邻点w,访问顶点w,标注v为已被探索的(黑色)

实现

// 深度优先探索算法
this.dfs = function(callback){
var color = this.initializeColor();
for(var i = 0 ; i < vertices.length; i++){
if(color[vertices[i]] === 'white' ){
this.dfsVisit(vertices[i],color,callback);
}
}
}
this.dfsVisit =function(u,color,callback){
color[u] = 'grey';
if(callback){
callback(u);
}
var neighbors = adjList.get(u);
for(var i = 0 ;i < neighbors.length; i++){
var w = neighbors[i];
if(color[w] === 'white'){
arguments.callee(w,color,callback);
}
}
color[u] = 'black';
}

首先,我们创建了颜色数组,并用值white 为图中的每个顶点对其进行了初始化,广度优先搜索也是这么做的。接着,对于图实例中每一个未被访问过的顶点,我们调用递归函数 dfsVisit ,传递的参数为顶点、颜色数组和回调函数。

当访问u顶点时,我们标注其为被发现的grey。如果有callback 函数的话,则执行该函数输出已访问过的顶点。接下来一步是取得包含顶点u的所有邻点的列表。对于顶点u的每一个未被访问过的邻点w,我们将调用dfsVisit 函数,传递w和其他参数。最后,在该2顶点和邻点按深度访问之后,我们回退,意思是该顶点已经被完全探索了,并将其标注为black

测试

graph.dfs(printNode);
// 访问了顶点:A
// 访问了顶点:B
// 访问了顶点:E
// 访问了顶点:I
// 访问了顶点:F
// 访问了顶点:C
// 访问了顶点:D
// 访问了顶点:G
// 访问了顶点:H

下面这个示意图展示了该算法每一步的执行过程

探索深度优化算法

我们现在只是展示了深度优先搜索算法的工作原理。我们可以用该算法做更多的事情,而不是只输出被访问顶点的顺序。

对于给定的图G,我们希望深度优先探索算法遍历图G的所有节点,构建“深林”(有根树的一个集合)已经一组源顶点(根),并输出两个数组:发现时间和完成探索时间。我们可以修改 dfs 方法来返回给我们一些信息:

  • 顶点u的发现时间d[u]
  • 当顶点u被标注为黑色时,u的完成探索时间f[u]
  • 顶点u的前溯点p[u]
// 追踪发现事件和完成探索时间
var time = 0;
// 深度优先探索算法优化版本
this.DFS = function(){
var color = this.initializeColor(),
d = [],
f = [],
p = [];
time = 0; for(var i = 0; i < vertices.length; i++){
f[vertices[i]] = 0;
d[vertices[i]] = 0;
p[vertices[i]] = null;
} for(i = 0; i< vertices.length; i++){
if(color[vertices[i]] === 'white'){
this.DFSVisit(vertices[i],color,d,f,p)
}
}
return {
discovery:d,
finished:f,
predecessors:p
}
}
this.DFSVisit = function(u,color,d,f,p){
console.log('发现了'+u);
color[u] = 'grey';
d[u] = ++time;
var neighbors = adjList.get(u);
for(var i = 0; i < neighbors.length; i++){
var w = neighbors[i];
if(color[w] === 'white'){
p[w] = u;
arguments.callee(w,color,d,f,p);
}
}
color[u] = 'black';
f[u] = ++time;
console.log('探索了'+u);
}

首先我们需要一个变量来追踪发现时间和完成探索时间。时间变量不能被作为参数传递,因为非对象的变量不能作为引用传递给其他Js方法。接下来,我们声明数组d、f和p。我们需要为图的每一个顶点来初始化这些数组。在这个方法结尾返回这些值。

当一个顶点第一次被发现时,我们要追踪其发现时间。当它是由引自顶点u的边而被发现的。我们追踪它的前溯点。最后,当这个顶点被完全探索之后,我们追踪其完成时间。

深度优先算法背后的思想是什么?

边是从最近发现的u处被向外探索的。只有连接到未发现的顶点的边被探索了。当u所有的边都被探索了,该算法返回u被发现的地方去探索其他的边。这个过程持续到我们发现了虽偶有从原始顶点能够触及的顶点。如果还留有其他未被发现的顶点。我们对新的源顶点将重复这个过程。直到图中所有的顶点都被探索了。

测试

var deepPath = graph.DFS();
console.log(deepPath);
/**
发现了A
发现了B
发现了E
发现了I
探索了I
探索了E
发现了F
探索了F
探索了B
发现了C
发现了D
发现了G
探索了G
发现了H
探索了H
探索了D
探索了C
探索了A discovery: [A: 1, B: 2, C: 10, D: 11, E: 3, F: 7, G: 12, H: 14, I: 4]
finished: [A: 18, B: 9, C: 17, D: 16, E: 6, F: 8, G: 13, H: 15, I: 5]
predecessors: [A: null, B: "A", C: "A", D: "C", E: "B", G: "D", H: "D", I: "E"]
*/

小结

本章学习了几种不同的方式来表示图这一数据结构。并实现了用邻接表表示图的算法。还学习了广度优先搜索和深度优先搜索的实际应用。

书籍链接: 学习JavaScript数据结构与算法

为什么我要放弃javaScript数据结构与算法(第九章)—— 图的更多相关文章

  1. 为什么我要放弃javaScript数据结构与算法(第十一章)—— 算法模式

    本章将会学习递归.动态规划和贪心算法. 第十一章 算法模式 递归 递归是一种解决问题的方法,它解决问题的各个小部分,直到解决最初的大问题.递归通常涉及函数调用自身. 递归函数是像下面能够直接调用自身的 ...

  2. 为什么我要放弃javaScript数据结构与算法(第十章)—— 排序和搜索算法

    本章将会学习最常见的排序和搜索算法,如冒泡排序.选择排序.插入排序.归并排序.快速排序和堆排序,以及顺序排序和二叉搜索算法. 第十章 排序和搜索算法 排序算法 我们会从一个最慢的开始,接着是一些性能好 ...

  3. 为什么我要放弃javaScript数据结构与算法(第八章)—— 树

    之前介绍了一些顺序数据结构,介绍的第一个非顺序数据结构是散列表.本章才会学习另一种非顺序数据结构--树,它对于存储需要快速寻找的数据非常有用. 本章内容 树的相关术语 创建树数据结构 树的遍历 添加和 ...

  4. 为什么我要放弃javaScript数据结构与算法(第七章)—— 字典和散列表

    本章学习使用字典和散列表来存储唯一值(不重复的值)的数据结构. 集合.字典和散列表可以存储不重复的值.在集合中,我们感兴趣的是每个值本身,并把它作为主要元素.而字典和散列表中都是用 [键,值]的形式来 ...

  5. 为什么我要放弃javaScript数据结构与算法(第六章)—— 集合

    前面已经学习了数组(列表).栈.队列和链表等顺序数据结构.这一章,我们要学习集合,这是一种不允许值重复的顺序数据结构. 本章可以学习到,如何添加和移除值,如何搜索值是否存在,也可以学习如何进行并集.交 ...

  6. 为什么我要放弃javaScript数据结构与算法(第五章)—— 链表

    这一章你将会学会如何实现和使用链表这种动态的数据结构,这意味着我们可以从中任意添加或移除项,它会按需进行扩张. 本章内容 链表数据结构 向链表添加元素 从链表移除元素 使用 LinkedList 类 ...

  7. 为什么我要放弃javaScript数据结构与算法(第四章)—— 队列

    有两种结构类似于数组,但在添加和删除元素时更加可控,它们就是栈和队列. 第四章 队列 队列数据结构 队列是遵循FIFO(First In First Out,先进先出,也称为先来先服务)原则的一组有序 ...

  8. 为什么我要放弃javaScript数据结构与算法(第三章)—— 栈

    有两种结构类似于数组,但在添加和删除元素时更加可控,它们就是栈和队列. 第三章 栈 栈数据结构 栈是一种遵循后进先出(LIFO)原则的有序集合.新添加的或待删除的元素都保存在栈的同一端,称为栈顶,另一 ...

  9. 为什么我要放弃javaScript数据结构与算法(第二章)—— 数组

    第二章 数组 几乎所有的编程语言都原生支持数组类型,因为数组是最简单的内存数据结构.JavaScript里也有数组类型,虽然它的第一个版本并没有支持数组.本章将深入学习数组数据结构和它的能力. 为什么 ...

随机推荐

  1. JAVA NIO Buffer

    所谓的输入,输出,就是把数据移除或移入缓冲区.   硬件不能直接访问用户控件(JVM). 基于存储的硬件设备操控的是固定大小的数据块儿,用户请求的是任意大小的或非对齐的数据块儿.   虚拟内存:使用虚 ...

  2. touch事件中的touches、targetTouches和changedTouches详解

    touches: 当前屏幕上所有触摸点的列表; targetTouches: 当前对象上所有触摸点的列表; changedTouches: 涉及当前(引发)事件的触摸点的列表 通过一个例子来区分一下触 ...

  3. 【Hibernate框架】关联映射(一对一关联映射)

    一.整理思路: 之前,小编总结过Mybatis的关联映射,接下来,再来总结一下hibernate的相关的关联映射,直接上图: 这张图,就是小编整理总结整个Hibernate的关联映射的一个大致思路. ...

  4. MongoVUE

    MongoVUE运行界面如下:

  5. ecshop

    if($cat_id == '205'){ $smarty->display('cat1.dwt', $cache_id); }elseif($cat_id == '2'){ $smarty-& ...

  6. 三点经验:长时间运行函数需要随时发射信号报告进度,以及设置bool变量随时可以退出,每做一步操作必须及时记录和处理相关信息

    三点经验:长时间运行函数需要随时发射信号报告进度,以及设置bool变量随时可以退出,每做一步操作必须及时记录和处理相关信息 不能到最后一起处理,否则万一中间出错了,这个记录状态就全部都乱了.

  7. tiny210 u-boot 网络ping不通主机解决方案

    站在巨人的肩膀上: http://blog.csdn.net/liukun321/article/details/7438880 http://www.arm9home.net/read.php?ti ...

  8. python 打印类的属性、方法

    打印变量db的类(class):[root@fuel ~]# pythonPython 2.6.6 (r266:84292, Jan 22 2014, 09:42:36)[GCC 4.4.7 2012 ...

  9. “五年经验”年薪50W分享Java程序员掌握什么技术才不会被淘汰

    在这个IT系统动辄就是上亿流量的时代,Java作为大数据时代应用最广泛的语言,诞生了一批又一批的新技术,包括HBase.Hadoop.MQ.Netty.SpringCloud等等 . 一些独角兽公司以 ...

  10. Zabbix监控系统部署:源码安装

    1. 概述1.1 基础环境2. 部署过程2.1 创建用户组2.2 下载源码解压编译安装2.2.1 下载源码解压2.2.2 YUM安装依赖环境2.2.3 编译安装最新版curl2.2.4 更新GNU构建 ...