阅读目录

一:nodejs进程进化及多进程架构原理

NodeJS是基于chrome浏览器的V8引擎构建的,它是单线程单进程模式,nodeJS的单线程指js的引擎只有一个实列。且是在主线程执行的,这样的
优点是:可以减少线程间切换的开销。并且不用考虑锁和线程池的问题。

那么nodejs是单线程吗?如果严格的来讲,node存在着多种线程。比如包括:js引擎执行的线程、定时器线程、异步http线程等等这样的。

nodejs是在主线程执行的,其他的异步IO和事件驱动相关的线程是通过libuv来实现内部的线程池和线程调度的。libuv存在着一个Event Loop,通过 Event Loop(事件循环)来切换实现类似多线程的效果。Event Loop 是维持一个执行栈和一个事件队列,在执行栈中,如果有异步IO及定时器等函数的话,就把这些异步回调函数放入到事件队列中。等执行栈执行完成后,会从事件队列中,按照一定的顺序执行事件队列中的异步回调函数。
nodeJS中的单线程是指js引擎只在唯一的主线程上运行的。其他的异步操作是有独立的线程去执行。通过libuv的Event Loop实现了类似多线程的上下文切换以及线程池的调度。线程是最小的进程,因此node也是单进程的。

理解服务器进程进化

1. 同步单进程服务器

该服务器是最早出现的,执行模型是同步的。它的服务模式是一次只能处理一个请求。其他的请求需要按照顺序依次等待处理执行。也就是说如果当前的请求正在处理的话,那么其他的请求都处于阻塞等待的状态。因此这样的服务器处理速度是不好的。

2. 同步多进程服务器

为了解决上面同步单进程服务器无法处理并发的问题,我们就出来一个同步多进程服务器,它的功能是一个请求需要一个进程来服务,也就是说如果有100个请求就需要100个进程来进行服务。那么这样就会有很大进程的开销问题了。并且相同的状态在内存中会有多种,这样就会造成资源浪费。

3. 同步多进程多线程服务器

为了解决上面多进程中资源浪费的问题,我们就引入了多进程多线程服务器模式,从我们之前一个进程处理一个请求,现在我们改成为一个线程来处理一个请求,线程相对于进程来说开销会少很多,并且线程之间还可以共享数据。并且我们还可以使用线程池来减少创建和销毁线程的开销。
但是多线程也有缺点,比如多个请求需要使用多个线程来服务,但是每个线程需要一定的内存来存放自己的堆和栈的。这样就会导致占用太多的内存。第二就是:CPU核心只能处理一件事情,系统是通过将CPU切分为时间片的方法来让线程可以均匀地使用CPU的资源的。在系统切换线程的过程中也会进行线程上下文切换,当线程数量过多时进行上下文切换会非常耗费时间的。因此在很大的并发量下,多线程还是无法做到很好的伸缩性。Apache服务器就是这样架构的。

4. 单进程单线程基于事件驱动的服务器

为了解决上面的问题,我们出现了单进程单线程基于事件驱动的模式出现了,使用单线程的优点是:避免内存开销和上下文切换的开销。
所有的请求都在单线程上执行的,其他的异步IO和事件驱动相关的线程是通过libuv中的事件循环来实现内部的线程池和线程调度的。可伸缩性比之前的都好,但是影响事件驱动服务模型性能的只有CPU的计算能力,但是只能使用单核的CPU来处理事件驱动,但是我们的计算机目前都是多核的,我们要如何使用多核CPU呢?如果我们使用多核CPU的话,那么CPU的计算能力就会得到一个很大的提升。

5. NodeJS的实现多进程架构

如上第四点,面对单线程单进程对多核使用率不好的问题,因此我们使用多进程,每个进程使用一个cpu,因此我们就可以实现多核cpu的利用。
Node提供了child_process模块和cluster模块来实现多进程以及进程的管理。也就是我们常说的 Master-Worker模式。也就是说进程分为Master(主)进程 和 worker(工作)进程。master进程负责调度或管理worker进程,那么worker进程负责具体的业务处理。在服务器层面来讲,worker可以是一个服务进程,负责出来自于客户端的请求,多个worker就相当于多个服务器,因此就构成了一个服务器群。master进程则负责创建worker,接收客户端的请求,然后分配到各个服务器上去处理,并且监控worker进程的运行状态及进行管理操作。

如下图所示:

二:node中child_process模块实现多进程

nodejs 是单进程的,因此无法使用多核cpu,node提供了child_process模块来实现子进程。从而会实现一个广义上的多进程模式,通过child_process模块,可以实现一个主进程,多个子进程模式,主进程叫做master进程,子进程叫做worker(工作)进程,在子进程中不仅可以调用其他node程序,我们还可以调用非node程序及shell命令等。执行完子进程后,我们可以以流或回调形式返回给主进程。

child_process提供了4个方法,用于创建子进程,这四个方法分别为 spawn, execFile, exec 和 fork. 所有的方法都是异步的。

该如上4个方法的区别是什么?

spawn: 子进程中执行的是非node程序,提供一组参数后,执行的结果以流的形式返回。
execFile: 子进程中执行的是非node程序, 提供一组参数后,执行的结果以回调的形式返回。
exec: 子进程执行的是非node程序,提供一串shell命令,执行结果后以回调的形式返回,它与 execFile不同的是,exec可以直接执行一串
shell命令。

fork: 子进程执行的是node程序,提供一组参数后,执行的结果以流的形式返回,它与spawn不同的是,fork生成的子进程只能执行node应用。

2.1 execFile 和 exec

该两个方法的相同点和不同点如下:

相同点:执行的都是非node应用,且执行的结果以回调函数的形式返回。
不同点:execFile执行的是一个应用,exec执行的是一段shell命令。

比如来说:echo是Unix系统的一个自带命令,我们可以直接在命令行中执行如下命令:

echo hello world

如下所示:

如上可以看到,我们在命令行中会打印 hello world. 因此这个我们可以使用 exec 来实现。

1)通过exec来实现:

exec执行shell命令代码如下:

const cp = require('child_process');
console.log(cp);
cp.exec('echo hello world', function(err, res) {
console.log(res);
});

执行如下图所示:

如上我们可以看到,我们的 child_process模块有如下属性:

{ ChildProcess: [Function: ChildProcess],
fork: [Function: fork],
_forkChild: [Function: _forkChild],
exec: [Function: exec],
execFile: [Function: execFile],
spawn: [Function: spawn],
spawnSync: [Function: spawnSync],
execFileSync: [Function: execFileSync],
execSync: [Function: execSync] }

执行如上exec命令后,结果输出为 hello world.

2) 通过execFile实现

const cp = require('child_process');
cp.execFile('echo', ['hello', 'world'], function(err, res) {
console.log(res);
});

如上结果也是为 "hello world".

2.2 spawn

spawn是用于执行非node应用的,并且是不能直接执行shell。spawn执行的结果是以流的形式输出的,通过流的方式可以节约内存的。

2.3 fork

在node中提供了fork方法,通过使用fork方法在单独的进程中执行node程序,通过使用fork新建worker进程,上下文都复制主进程。并且通过父子之间的通信,子进程接收父进程的信息,并执行子进程后结果信息返回给父进程。降低了大数据运行的压力。

现在我们来理解下使用fork()方法来创建子进程,fork()方法只需要指定要执行的javascript文件模块,即可创建Node的子进程。下面我们是简单的hello world的demo,master进程根据cpu的数量来创建出相应数量的worker进程,worker进程利用进程ID来标记。

|------ 项目
| |--- master.js
| |--- worker.js
| |--- package.json
| |--- node_modules

如上是我们的简单项目结构,其中 worker.js 代码如下:

console.log('Worker-' + process.pid + ': Hello world.');

master.js 代码如下:

const childProcess = require('child_process');
const cpuNum = require('os').cpus().length; for (let i = 0; i < cpuNum; ++i) {
childProcess.fork('./worker.js');
} console.log('Master: xxxx');

然后我们进入项目中的根目录,执行 node master.js 命令即可看到打印信息如下:

如上图可以看到,我们的master创建了4个worker进程后输出 hello world信息。如上就是根据cpu的数量创建了4个工作进程。

三:父子进程间如何通信?

如上创建了4个worker进程后,现在我们需要考虑的是如何实现 master进程与worker进程通信的问题。

在NodeJS中父子进程之间通信可以通过 on('message') 和 send()方法来实现通信,on('message') 是监听message事件的。
当该进程收到其他进程发送的消息时候,便会触发message事件。send()方法则是用于向其他进程发送消息的。

具体如何做呢?

master进程中可以调用 child_process的fork()方法后会得到一个子进程的实列,通过该实列我们可以监听到来自子进程的消息或向子进程发送消息。而worker进程则通过process对象接口来监听父进程的消息或向父进程发送消息。现在我们把master.js 代码改成如下:

const childProcess = require('child_process');
const worker = childProcess.fork('./worker.js'); // 主进程向子进程发送消息
worker.send('Hello World'); // 监听子进程发送过来的消息
worker.on('message', (msg) => {
console.log('Received message from worker:' + msg);
});

worker.js 代码如下:

// 接收主进程发来的消息
process.on('message', (msg) => {
console.log('Received message from master:' + msg);
// 子进程向主进程发送消息
process.send('Hi master.');
});

我们继续在命令中执行 node master.js 命令后,看到如下信息被打印了

3.2 Master实现对Worker的请求进行分发

如上只是简单的父进程和子进程进行通信的demo实列,现在我们继续来看一个更复杂一点的demo。我们知道master进程最主要是创建子进程,及对子进程进行管理和分配,而子进程最主要做的事情是处理具体的请求及业务。

进程通信除了使用到上面的send()方法,发送一些普通对象以外,我们还可以发送句柄,什么是句柄呢,句柄是一种引用,可以用来标识资源。
比如通过句柄可以标识一个socket对象等。我们可以利用该句柄实现请求的分发。

现在我们通过master进程来创建一个TCP服务器来监听一些特定的端口,master进程会收到客户端的请求,我们会得到一个socket对象,通过这个socket对象就可以和客户端进行通信,从而我们可以处理客户端的请求。

比如如下demo实列,master创建TCP服务器并且监听8989端口,收到该请求后会将请求分发给worker处理,worker收到master发来的socket以后,通过socket对客户端的响应。

|------ 项目
| |--- master.js
| |--- worker.js
| |--- tcp_client.js
| |--- package.json
| |--- node_modules

master.js 代码如下:

const childProcess = require('child_process');
const net = require('net'); // 获取cpu的数量
const cpuNum = require('os').cpus().length; let workers = [];
let cur = 0; for (let i = 0; i < cpuNum; ++i) {
workers.push(childProcess.fork('./worker.js'));
console.log('worker process-' + workers[i].pid);
} // 创建TCP服务器
const tcpServer = net.createServer(); /*
服务器收到请求后分发给工作进程去处理
*/
tcpServer.on('connection', (socket) => {
workers[cur].send('socket', socket);
cur = Number.parseInt((cur + 1) % cpuNum);
}); tcpServer.listen(8989, () => {
console.log('Tcp Server: 127.0.0.8989');
});

worker.js 代码如下:

// 接收主进程发来的消息
process.on('message', (msg, socket) => {
if (msg === 'socket' && socket) {
// 利用setTimeout 模拟异步请求
setTimeout(() => {
socket.end('Request handled by worker-' + process.pid);
},100);
}
});

tcp.client.js 代码如下:

const net = require('net');
const maxConnectCount = 10; for (let i = 0; i < maxConnectCount; ++i) {
net.createConnection({
port: 8989,
host: '127.0.0.1'
}).on('data', (d) => {
console.log(d.toString());
})
}

如上代码,tcp_client.js 负责创建10个本地请求,master.js 首先根据cpu的数量,创建多个worker进程,然后创建一个tcp服务器,使用connection来监听net中 createConnection 方法创建事件,当有事件来的时候,就使用worker子进程依次进行分发事件,最后我们通过worker.js 来使用 process中message事件对事件进行监听。如果收到消息的话,就打印消息出来,比如如下代码:

// 接收主进程发来的消息
process.on('message', (msg, socket) => {
if (msg === 'socket' && socket) {
// 利用setTimeout 模拟异步请求
setTimeout(() => {
socket.end('Request handled by worker-' + process.pid);
},100);
}
});

为了查看效果,我们可以在项目的根目录下 运行 命令 node master.js 启动服务器,然后我们打开另一个命令行,执行 node tcp_client.js 启动客户端,然后我们会看到我们的10个请求被分发到不同的服务器上进行处理,如下所示:

3.3 Worker监听同一个端口

我们之前已经实现了句柄可以发送普通对象及socket对象外,我们还可以通过句柄的方式发送一个server对象。我们在master进程中创建一个TCP服务器,将服务器对象直接发送给worker进程,让worker进程去监听端口并处理请求。因此master进程和worker进程就会监听了相同的端口了。当我们的客户端发送请求时候,我们的master进程和worker进程都可以监听到,我们知道我们的master进程它是不会处理具体的业务的。
因此需要使用worker进程去处理具体的事情了。因此请求都会被worker进程处理了。

那么在这种模式下,主进程和worker进程都可以监听到相同的端口,当网络请求到来的时候,会进行抢占式调度,只有一个worker进程会抢到链接然后进行服务,由于是抢占式调度,可以理解为谁先来谁先处理的模式,因此就不能保证每个worker进程都能负载均衡的问题。下面是一个demo如下:

master.js 代码如下:

const childProcess = require('child_process');
const net = require('net'); // 获取cpu的数量
const cpuNum = require('os').cpus().length; let workers = [];
let cur = 0; for (let i = 0; i < cpuNum; ++i) {
workers.push(childProcess.fork('./worker.js'));
console.log('worker process-' + workers[i].pid);
} // 创建TCP服务器
const tcpServer = net.createServer(); tcpServer.listen(8989, () => {
console.log('Tcp Server: 127.0.0.8989');
// 监听端口后将服务器句柄发送给worker进程
for (let i = 0; i < cpuNum; ++i) {
workers[i].send('tcpServer', tcpServer);
}
// 关闭master线程的端口监听
tcpServer.close();
});

worker.js 代码如下:

// 接收主进程发来的消息
process.on('message', (msg, tcpServer) => {
if (msg === 'tcpServer' && tcpServer) {
tcpServer.on('connection', (socket) => {
setTimeout(() => {
socket.end('Request handled by worker-' + process.pid);
}, 100);
})
}
});

tcp_client.js 代码如下:

const net = require('net');
const maxConnectCount = 10; for (let i = 0; i < maxConnectCount; ++i) {
net.createConnection({
port: 8989,
host: '127.0.0.1'
}).on('data', (d) => {
console.log(d.toString());
})
}

如上代码,我们运行 node master.js 代码后,运行结果如下所示:

然后我们进行 运行 node tcp_client.js 命令后,运行结果如下所示:

如上我们可以看到 进程id为 37660 调度的比较多。

3.4 实现进程重启

worker进程可能会因为其他的原因导致异常而退出,为了提高集群的稳定性,我们的master进程需要监听每个worker进程的存活状态,当我们的任何一个worker进程退出之后,master进程能监听到并且能够重启新的子进程。在我们的Node中,子进程退出时候,我们可以在父进程中使用exit事件就能监听到。如果触发了该事件,就可以断定为子进程已经退出了,因此我们就可以在该事件内部做出对应的处理,比如说重启子进程等操作。

下面是我们上面监听同一个端口模式下的代码demo,但是我们增加了进程重启的功能。进程重启时,我们的master进程需要重新传递tcpServer对象给新的worker进程。但是master进程是不能被关闭的。否则的话,句柄将为空,无法正常传递。

master.js 代码如下:

const childProcess = require('child_process');
const net = require('net'); // 获取cpu的数量
const cpuNum = require('os').cpus().length; let workers = [];
let cur = 0; for (let i = 0; i < cpuNum; ++i) {
workers.push(childProcess.fork('./worker.js'));
console.log('worker process-' + workers[i].pid);
} // 创建TCP服务器
const tcpServer = net.createServer(); /*
服务器收到请求后分发给工作进程去处理
*/
tcpServer.on('connection', (socket) => {
workers[cur].send('socket', socket);
cur = Number.parseInt((cur + 1) % cpuNum);
}); tcpServer.listen(8989, () => {
console.log('Tcp Server: 127.0.0.8989');
// 监听端口后将服务器句柄发送给worker进程
for (let i = 0; i < cpuNum; ++i) {
workers[i].send('tcpServer', tcpServer);
// 监听工作进程退出事件
workers[i].on('exit', ((i) => {
return () => {
console.log('worker-' + workers[i].pid + ' exited');
workers[i] = childProcess.fork('./worker.js');
console.log('Create worker-' + workers[i].pid);
workers[i].send('tcpServer', tcpServer);
}
})(i));
}
// 不能关闭master线程的,否则的话,句柄将为空,无法正常传递。
// tcpServer.close();
});

worker.js 代码如下:

// 接收主进程发来的消息
process.on('message', (msg, tcpServer) => {
if (msg === 'tcpServer' && tcpServer) {
tcpServer.on('connection', (socket) => {
setTimeout(() => {
socket.end('Request handled by worker-' + process.pid);
}, 100);
})
}
});

tcp_client.js 代码如下:

const net = require('net');
const maxConnectCount = 10; for (let i = 0; i < maxConnectCount; ++i) {
net.createConnection({
port: 8989,
host: '127.0.0.1'
}).on('data', (d) => {
console.log(d.toString());
})
}

当我们在命令中 运行 node master.js  和 node tcp_client.js 执行后,如下图所示:

然后我们进入我们的电脑后台(我这边是mac电脑),进入活动监视器页面,结束某一个进程,如下图所示:

结束完成后,我们再来看下我们的 node master.js 命令可以看到,先打印 某某工作进程被退出了,然后某某工作进程被创建了,如下图所示

然后我们再到我们的 活动监视器可以看到新的 进程号被加进来了,如下图所示:

四:理解cluster集群

如上我们了解了使用 child_process实现node集群操作,现在我们来学习使用cluster模块实现多进程服务充分利用我们的cpu资源以外,还能够帮我们更好地进行进程管理。我们使用cluster模块来实现我们上面同样的功能,代码如下:

master.js 代码如下:

const cluster = require('cluster');
if (cluster.isMaster) {
const cpuNum = require('os').cpus().length;
for (let i = 0; i < cpuNum; ++i) {
cluster.fork();
} // 创建进程完成后输出信息
cluster.on('online', (worker) => {
console.log('Create worker-' + worker.process.pid);
}); // 监听子进程退出后重启事件
cluster.on('exit', (worker, code, signal) => {
console.log('[Master] worker ' + worker.process.pid + ' died with code:' + code + ', and' + signal);
cluster.fork(); // 重启子进程
});
} else {
const net = require('net');
net.createServer().on('connection', (socket) => {
setTimeout(() => {
socket.end('Request handled by worker-' + process.pid);
}, 10)
}).listen(8989)
}

如上代码,我们可以使用 cluster.isMaster 来判断是主进程还是子进程,如果是主进程的话,我们使用cluster创建了和cpu数量相同的worker进程,并且通过监听 cluster中的online事件来判断worker是否创建成功。并且使用了 cluster监听了 exit事件,当worker进程退出后,会触发master进程中cluster的online事件来判断worker是否创建成功。如下图我们在命令行中运行命令:

如下所示:

我们现在同样的道理,我们去 活动监视器去吧 47575这个端口号结束掉。在看看我们的命令行如下所示:

从上图我们也可以看到 47575 进程结束掉,并且47898进程重启了。如上代码使用 cluster模块实现了child_process集群的操作。

有关更多的cluster中的API可以看这篇文章(http://wiki.jikexueyuan.com/project/nodejs/cluster.html)

我们在下一篇文章会深入学习使用cluster的应用场景demo。 基本原理先到这里。

注:我也是在看资料学习的。

浅谈NodeJS多进程服务架构基本原理的更多相关文章

  1. 浅谈大型web系统架构

    动态应用,是相对于网站静态内容而言,是指以c/c++.php.Java.perl..net等服务器端语言开发的网络应用软件,比如论坛.网络相册.交友.BLOG等常见应用.动态应用系统通常与数据库系统. ...

  2. 转:浅谈大型web系统架构

    浅谈大型web系统架构 动态应用,是相对于网站静态内容而言,是指以c/c++.php.Java.perl..net等服务器端语言开发的网络应用软件,比如论坛.网络相册.交友.BLOG等常见应用.动态应 ...

  3. AngularJS进阶(二十五)requirejs + angular + angular-route 浅谈HTML5单页面架构

    requirejs + angular + angular-route 浅谈HTML5单页面架构 众所周知,现在移动Webapp越来越多,例如天猫.京东.国美这些都是很好的例子.而在Webapp中,又 ...

  4. 五分钟DBA:浅谈伪分布式数据库架构

    [IT168 技术]12月25日消息,2010互联网行业技术研讨峰会今日在上海华东理工大学召开.本次峰会以“互联网行业应用最佳实践”为主题,定位于互联网架构设计.应用开发.应用运维管理,同时,峰会邀请 ...

  5. 浅谈HTML5单页面架构(二)——backbone + requirejs + zepto + underscore

    本文转载自:http://www.cnblogs.com/kenkofox/p/4648472.html 上一篇<浅谈HTML5单页面架构(一)--requirejs + angular + a ...

  6. 【ZZ】浅谈大型web系统架构 | 菜鸟教程

    浅谈大型web系统架构 http://www.runoob.com/w3cnote/large-scale-web-system-architecture.html

  7. 浅谈HTML5单页面架构(一)——requirejs + angular + angular-route

    心血来潮,打算结合实际开发的经验,浅谈一下HTML5单页面App或网页的架构. 众所周知,现在移动Webapp越来越多,例如天猫.京东.国美这些都是很好的例子.而在Webapp中,又要数单页面架构体验 ...

  8. 浅谈全区全服架构的SNS游戏后台

    版权声明:本文由梁本志原创文章,转载请注明出处: 文章原文链接:https://www.qcloud.com/community/article/198 来源:腾云阁 https://www.qclo ...

  9. 直播技术:从性能参数到业务大数据,浅谈直播CDN服务监控

    线上服务的有效监控和数据收集,一直是后端服务离不开的话题.直播作为一种经典的分布式系统,监控以及数据收集更是必不可少的工作.如何对海量的服务集群有效的监控和保活,又如何抓取集群中的碎片数据中来优化服务 ...

随机推荐

  1. Cisco packet tracer下dhcp的配置的vlan的应用

    话不多说,先上拓扑图. pc0和pc1分别接在三层交换机的F0/1.F0/2接口,ser接在F0/24接口,用ser用作dhcp的服务器. 0x01:配置server0 先配置server的IP地址. ...

  2. .NET Core IdentityServer4实战 第六章-Consent授权页

    在identityServer4中登陆页面只要是成功了,就会注册一个Cookie在服务器资源上,像现在大部分的网站第三方授权,都是经过一个页面,然后选需要的功能,IdentityServer4也给我们 ...

  3. Nagios安装配置教程(一)基本了解

    一.Nagios简介 Nagios是一款开源的电脑系统和网络监视工具,能有效监控Windows.Linux和Unix的主机状态, 交换机路由器等网络设置,打印机等.在系统或服务状态异常时发出邮件或短信 ...

  4. CSU 1811: Tree Intersection(线段树启发式合并||map启发式合并)

    http://acm.csu.edu.cn/csuoj/problemset/problem?pid=1811 题意:给出一棵树,每一个结点有一个颜色,然后依次删除树边,问每次删除树边之后,分开的两个 ...

  5. 7.秋招复习简单整理之请你讲讲 Statement 和 PreparedStatement 的区别?哪个性能更好?

    Statement和PreparedStatement都是数据库用于执行SQL语句的句柄,但是PreparedStatement代表一个预编译的SQL. 以下是PreparedStatement和St ...

  6. 基于缓存或zookeeper的分布式锁实现

    缓存锁  我们常常将缓存作为分布式锁的解决方案,但是却不能单纯的判断某个 key 是否存在 来作为锁的获得依据,因为无论是 exists 和 get 命名都不是线程安全的,都无法保证只有一个线程可以获 ...

  7. Android 开发你需要了解的那些事

    本文微信公众号「AndroidTraveler」首发. 背景 最近部门有新入职员工,作为规划技术路线的导师,这边给新员工安排了学习路线. 除了基本的学习路线之外,每次沟通,我都留了一个小问题,让小伙伴 ...

  8. codeblocks出现'to_string' was not declared in this scope 的问题,用g++11编译环境

    在将数字转化为字符串时使用to_string()竟然出现了'to_string' was not declared in this scope,我头文件用的万能头文件肯定没问题,而这个函数在其他的CB ...

  9. Java开发面试题汇总 -- 精选版(附答案)

    最近事情太多,没太时间写公众号.今天抽空再整理整理面试中的那点事吧,帮助那些正在找工作或想跳槽找工作的兄弟姐妹们. 前面我己写过多篇推文,相信关注此公众号的伙伴们已经了解掌握了不少.从目前流行的开发技 ...

  10. Prim算法与Kruskal(没有代码)

    两个最小生成树算法, 都有一个共同的思想: 这棵树是一点一点长大的; 并且每次生长, 都是贪心的. 不同之处是: Kruscal算法是以边为中心的, 每次找最小的并且有用的边添加到树上; Prim算法 ...