JavaScript实现图结构

一、图论

1.1.图的简介

什么是图?

  • 图结构是一种与树结构有些相似的数据结构;
  • 图论是数学的一个分支,并且,在数学中,树是图的一种;
  • 图论以图为研究对象,研究顶点组成的图形的数学理论和方法;
  • 主要的研究目的为:事物之间的联系顶点代表事物代表两个事物间的关系

图的特点:

  • 一组顶点:通常用 V (Vertex)表示顶点的集合;
  • 一组边:通常用 E (Edge)表示边的集合;
    • 边是顶点和顶点之间的连线;
    • 边可以是有向的,也可以是无向的。比如A----B表示无向,A ---> B 表示有向;

图的常用术语:

  • 顶点:表示图中的一个节点

  • 边:表示顶点和顶点给之间的连线

  • 相邻顶点:由一条边连接在一起的顶点称为相邻顶点

  • 度:一个顶点的相邻顶点的数量

  • 路径:

    • 简单路径:简单路径要求不包含重复的顶点;
    • 回路:第一个顶点和最后一个顶点相同的路径称为回路;
  • 无向图:图中的所有边都是没有方向的;

  • 有向图:图中的所有边都是方向的;

  • 无权图:无权图中的边没有任何权重意义;

  • 带权图:带权图中的边有一定的权重含义;

1.2.图的表示

邻接矩阵

表示图的常用方式为:邻接矩阵

  • 可以使用二维数组来表示邻接矩阵;

  • 邻接矩阵让每个节点和一个整数相关联,该整数作为数组的下标值

  • 使用一个二维数组来表示顶点之间的连接

如上图所示:

  • 二维数组中的0表示没有连线,1表示有连线;
  • 如:A[ 0 ] [ 3 ] = 1,表示 A 和 C 之间有连接;
  • 邻接矩阵的对角线上的值都为0,表示A - A ,B - B,等自回路都没有连接(自己与自己之间没有连接);
  • 若为无向图,则邻接矩阵应为对角线上元素全为0的对称矩阵;

邻接矩阵的问题:

  • 如果图是一个稀疏图,那么邻接矩阵中将存在大量的 0,造成存储空间的浪费;
邻接表

另外一种表示图的常用方式为:邻接表

  • 邻接表由图中每个顶点以及和顶点相邻的顶点列表组成;
  • 这个列表可用多种方式存储,比如:数组/链表/字典(哈希表)等都可以;

如上图所示:

  • 图中可清楚看到A与B、C、D相邻,假如要表示这些与A顶点相邻的顶点(边),可以通过将它们作为A的值(value)存入到对应的数组/链表/字典中。
  • 之后,通过键(key)A可以十分方便地取出对应的数据;

邻接表的问题:

  • 邻接表可以简单地得出出度,即某一顶点指向其他顶点的个数;
  • 但是,邻接表计算入度(指向某一顶点的其他顶点的个数称为该顶点的入度)十分困难。此时需要构造逆邻接表才能有效计算入度;

二、封装图结构

在实现过程中采用邻接表的方式来表示边,使用字典类来存储邻接表。

2.1.添加字典类和队列类

首先需要引入之前实现的,之后会用到的字典类和队列类:

    //封装字典类
function Dictionary(){
//字典属性
this.items = {} //字典操作方法
//一.在字典中添加键值对
Dictionary.prototype.set = function(key, value){
this.items[key] = value
} //二.判断字典中是否有某个key
Dictionary.prototype.has = function(key){
return this.items.hasOwnProperty(key)
} //三.从字典中移除元素
Dictionary.prototype.remove = function(key){
//1.判断字典中是否有这个key
if(!this.has(key)) return false //2.从字典中删除key
delete this.items[key]
return true
} //四.根据key获取value
Dictionary.prototype.get = function(key){
return this.has(key) ? this.items[key] : undefined
} //五.获取所有keys
Dictionary.prototype.keys = function(){
return Object.keys(this.items)
} //六.size方法
Dictionary.prototype.keys = function(){
return this.keys().length
} //七.clear方法
Dictionary.prototype.clear = function(){
this.items = {}
}
} // 基于数组封装队列类
function Queue() {
// 属性
this.items = []
// 方法
// 1.将元素加入到队列中
Queue.prototype.enqueue = element => {
this.items.push(element)
} // 2.从队列中删除前端元素
Queue.prototype.dequeue = () => {
return this.items.shift()
} // 3.查看前端的元素
Queue.prototype.front = () => {
return this.items[0]
} // 4.查看队列是否为空
Queue.prototype.isEmpty = () => {
return this.items.length == 0;
} // 5.查看队列中元素的个数
Queue.prototype.size = () => {
return this.items.length
} // 6.toString方法
Queue.prototype.toString = () => {
let resultString = ''
for (let i of this.items){
resultString += i + ' '
}
return resultString
}
}

2.2.创建图类

先创建图类Graph,并添加基本属性,再实现图类的常用方法:

    //封装图类
function Graph (){
//属性:顶点(数组)/边(字典)
this.vertexes = [] //顶点
this.edges = new Dictionary() //边
}

2.3.添加顶点与边

如图所示:

创建一个数组对象vertexes存储图的顶点;创建一个字典对象edges存储图的边,其中key为顶点,value为存储key顶点相邻顶点的数组。

代码实现:

      //添加方法
//一.添加顶点
Graph.prototype.addVertex = function(v){
this.vertexes.push(v)
this.edges.set(v, []) //将边添加到字典中,新增的顶点作为键,对应的值为一个存储边的空数组
}
//二.添加边
Graph.prototype.addEdge = function(v1, v2){//传入两个顶点为它们添加边
this.edges.get(v1).push(v2)//取出字典对象edges中存储边的数组,并添加关联顶点
this.edges.get(v2).push(v1)//表示的是无向表,故要添加互相指向的两条边
}

2.4.转换为字符串输出

为图类Graph添加toString方法,实现以邻接表的形式输出图中各顶点。

代码实现:

      //三.实现toString方法:转换为邻接表形式
Graph.prototype.toString = function (){
//1.定义字符串,保存最终结果
let resultString = "" //2.遍历所有的顶点以及顶点对应的边
for (let i = 0; i < this.vertexes.length; i++) {//遍历所有顶点
resultString += this.vertexes[i] + '-->'
let vEdges = this.edges.get(this.vertexes[i])
for (let j = 0; j < vEdges.length; j++) {//遍历字典中每个顶点对应的数组
resultString += vEdges[j] + ' ';
}
resultString += '\n'
}
return resultString
}

测试代码:

   //测试代码
//1.创建图结构
let graph = new Graph() //2.添加顶点
let myVertexes = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I']
for (let i = 0; i < myVertexes.length; i++) {
graph.addVertex(myVertexes[i])
} //3.添加边
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') //4.输出结果
console.log(graph.toString());

测试结果:

2.5.图的遍历

图的遍历思想:

  • 图的遍历思想与树的遍历思想一样,意味着需要将图中所有的顶点都访问一遍,并且不能有重复的访问(上面的toString方法会重复访问);

遍历图的两种算法:

  • 广度优先搜索(Breadth - First Search,简称BFS);
  • 深度优先搜索(Depth - First Search,简称DFS);
  • 两种遍历算法都需要指定第一个被访问的顶点

为了记录顶点是否被访问过,使用三种颜色来表示它们的状态

  • 白色:表示该顶点还没有被访问过;
  • 灰色:表示该顶点被访问过,但其相邻顶点并未完全被访问过;
  • 黑色:表示该顶点被访问过,且其所有相邻顶点都被访问过;

首先封装initializeColor方法将图中的所有顶点初始化为白色,代码实现如下:

      //四.初始化状态颜色
Graph.prototype.initializeColor = function(){
let colors = []
for (let i = 0; i < this.vertexes.length; i++) {
colors[this.vertexes[i]] = 'white';
}
return colors
}
广度优先搜索

广度优先搜索算法的思路:

  • 广度优先搜索算法会从指定的第一个顶点开始遍历图,先访问其所有的相邻顶点,就像一次访问图的一层;
  • 也可以说是先宽后深地遍历图中的各个顶点;

实现思路:

基于队列可以简单地实现广度优先搜索算法:

  • 首先创建一个队列Q(尾部进,首部出);
  • 调用封装的initializeColor方法将所有顶点初始化为白色;
  • 指定第一个顶点A,将A标注为灰色(被访问过的节点),并将A放入队列Q中;
  • 循环遍历队列中的元素,只要队列Q非空,就执行以下操作:
    • 先将灰色的A从Q的首部取出;
    • 取出A后,将A的所有未被访问过(白色)的相邻顶点依次从队列Q的尾部加入队列,并变为灰色。以此保证,灰色的相邻顶点不重复加入队列;
    • A的全部相邻节点加入Q后,A变为黑色,在下一次循环中被移除Q外;

代码实现:

      //五.实现广度搜索(BFS)
//传入指定的第一个顶点和处理结果的函数
Graph.prototype.bfs = function(initV, handler){
//1.初始化颜色
let colors = this.initializeColor() //2.创建队列
let que = new Queue() //3.将顶点加入到队列中
que.enqueue(initV) //4.循环从队列中取出元素,队列为空才停止
while(!que.isEmpty()){
//4.1.从队列首部取出一个顶点
let v = que.dequeue() //4.2.从字典对象edges中获取和该顶点相邻的其他顶点组成的数组
let vNeighbours = this.edges.get(v) //4.3.将v的颜色变为灰色
colors[v] = 'gray' //4.4.遍历v所有相邻的顶点vNeighbours,并且加入队列中
for (let i = 0; i < vNeighbours.length; i++) {
const a = vNeighbours[i];
//判断相邻顶点是否被探测过,被探测过则不加入队列中;并且加入队列后变为灰色,表示被探测过
if (colors[a] == 'white') {
colors[a] = 'gray'
que.enqueue(a)
}
} //4.5.处理顶点v
handler(v) //4.6.顶点v所有白色的相邻顶点都加入队列后,将顶点v设置为黑色。此时黑色顶点v位于队列最前面,进入下一次while循环时会被取出
colors[v] = 'black'
}
}

过程详解:

下为指定的第一个顶点为A时的遍历过程:

  • 如 a 图所示,将在字典edges中取出的与A相邻的且未被访问过的白色顶点B、C、D放入队列que中并变为灰色,随后将A变为黑色并移出队列;
  • 接着,如图 b 所示,将在字典edges中取出的与B相邻的且未被访问过的白色顶点E、F放入队列que中并变为灰色,随后将B变为黑色并移出队列;

  • 如 c 图所示,将在字典edges中取出的与C相邻的且未被访问过的白色顶点G(A,D也相邻不过已变为灰色,所以不加入队列)放入队列que中并变为灰色,随后将C变为黑色并移出队列;
  • 接着,如图 d 所示,将在字典edges中取出的与D相邻的且未被访问过的白色顶点H放入队列que中并变为灰色,随后将D变为黑色并移出队列。

如此循环直到队列中元素为0,即所有顶点都变黑并移出队列后才停止,此时图中顶点已被全部遍历。

测试代码:

    //测试代码
//1.创建图结构
let graph = new Graph() //2.添加顶点
let myVertexes = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I']
for (let i = 0; i < myVertexes.length; i++) {
graph.addVertex(myVertexes[i])
} //3.添加边
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') //4.测试bfs遍历方法
let result = ""
graph.bfs(graph.vertexes[0], function(v){
result += v + "-"
})
console.log(result);

测试结果:

可见,安装了广度优先搜索的顺序不重复地遍历了所有顶点。

深度优先搜索

广度优先算法的思路:

  • 深度优先搜索算法将会从指定的第一个顶点开始遍历图,沿着一条路径遍历直到该路径的最后一个顶点都被访问过为止;
  • 接着沿原来路径回退并探索下一条路径,即先深后宽地遍历图中的各个顶点;

实现思路:

  • 可以使用结构来实现深度优先搜索算法;
  • 深度优先搜索算法的遍历顺序与二叉搜索树中的先序遍历较为相似,同样可以使用递归来实现(递归的本质就是函数栈的调用)。

基于递归实现深度优先搜索算法:定义dfs方法用于调用递归方法dfsVisit,定义dfsVisit方法用于递归访问图中的各个顶点。

在dfs方法中:

  • 首先,调用initializeColor方法将所有顶点初始化为白色;
  • 然后,调用dfsVisit方法遍历图的顶点;

在dfsVisit方法中:

  • 首先,将传入的指定节点v标注为灰色
  • 接着,处理顶点V;
  • 然后,访问V的相邻顶点;
  • 最后,将顶点v标注为黑色;

代码实现:

      //六.实现深度搜索(DFS)
Graph.prototype.dfs = function(initV, handler){
//1.初始化顶点颜色
let colors = this.initializeColor() //2.从某个顶点开始依次递归访问
this.dfsVisit(initV, colors, handler)
} //为了方便递归调用,封装访问顶点的函数,传入三个参数分别表示:指定的第一个顶点、颜色、处理函数
Graph.prototype.dfsVisit = function(v, colors, handler){
//1.将颜色设置为灰色
colors[v] = 'gray' //2.处理v顶点
handler(v) //3.访问V的相邻顶点
let vNeighbours = this.edges.get(v)
for (let i = 0; i < vNeighbours.length; i++) {
let a = vNeighbours[i];
//判断相邻顶点是否为白色,若为白色,递归调用函数继续访问
if (colors[a] == 'white') {
this.dfsVisit(a, colors, handler)
} } //4.将v设置为黑色
colors[v] = 'black'
}

过程详解:

这里主要解释一下代码中的第3步操作:访问指定顶点的相邻顶点。

  • 以指定顶点A为例,先从储存顶点及其对应相邻顶点的字典对象edges中取出由顶点A的相邻顶点组成的数组:

  • 第一步:A顶点变为灰色,随后进入第一个for循环,遍历A白色的相邻顶点:B、C、D;在该for循环的第1次循环中(执行B),B顶点满足:colors == "white",触发递归,重新调用该方法;
  • 第二步:B顶点变为灰色,随后进入第二个for循环,遍历B白色的相邻顶点:E、F;在该for循环的第1次循环中(执行E),E顶点满足:colors == "white",触发递归,重新调用该方法;
  • 第三步:E顶点变为灰色,随后进入第三个for循环,遍历E白色的相邻顶点:I;在该for循环的第1次循环中(执行I),I顶点满足:colors == "white",触发递归,重新调用该方法;
  • 第四步:I顶点变为灰色,随后进入第四个for循环,由于顶点I的相邻顶点E不满足:colors == "white",停止递归调用。过程如下图所示:

  • 第五步:递归结束后一路向上返回,首先回到第三个for循环中继续执行其中的第2、3...次循环,每次循环的执行过程与上面的同理,直到递归再次结束后,再返回到第二个for循环中继续执行其中的第2、3...次循环....以此类推直到将图的所有顶点访问完为止。

下图为遍历图中各顶点的完整过程:

  • 发现表示访问了该顶点,状态变为灰色
  • 探索表示既访问了该顶点,也访问了该顶点的全部相邻顶点,状态变为黑色
  • 由于在顶点变为灰色后就调用了处理函数handler,所以handler方法的输出顺序为发现顶点的顺序即:A、B、E、I、F、C、D、G、H 。

测试代码:

    //测试代码
//1.创建图结构
let graph = new Graph() //2.添加顶点
let myVertexes = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I']
for (let i = 0; i < myVertexes.length; i++) {
graph.addVertex(myVertexes[i])
} //3.添加边
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') //4.测试dfs遍历顶点
let result = ""
graph.dfs(graph.vertexes[0], function(v){
result += v + "-"
})
console.log(result);

测试结果:

2.6.完整实现

    //封装图结构
function Graph (){
//属性:顶点(数组)/边(字典)
this.vertexes = [] //顶点
this.edges = new Dictionary() //边 //方法
//添加方法
//一.添加顶点
Graph.prototype.addVertex = function(v){
this.vertexes.push(v)
this.edges.set(v, []) //将边添加到字典中,新增的顶点作为键,对应的值为一个存储边的空数组
}
//二.添加边
Graph.prototype.addEdge = function(v1, v2){//传入两个顶点为它们添加边
this.edges.get(v1).push(v2)//取出字典对象edges中存储边的数组,并添加关联顶点
this.edges.get(v2).push(v1)//表示的是无向表,故要添加互相指向的两条边
} //三.实现toString方法:转换为邻接表形式
Graph.prototype.toString = function (){
//1.定义字符串,保存最终结果
let resultString = "" //2.遍历所有的顶点以及顶点对应的边
for (let i = 0; i < this.vertexes.length; i++) {//遍历所有顶点
resultString += this.vertexes[i] + '-->'
let vEdges = this.edges.get(this.vertexes[i])
for (let j = 0; j < vEdges.length; j++) {//遍历字典中每个顶点对应的数组
resultString += vEdges[j] + ' ';
}
resultString += '\n'
}
return resultString
} //四.初始化状态颜色
Graph.prototype.initializeColor = function(){
let colors = []
for (let i = 0; i < this.vertexes.length; i++) {
colors[this.vertexes[i]] = 'white';
}
return colors
} //五.实现广度搜索(BFS)
//传入指定的第一个顶点和处理结果的函数
Graph.prototype.bfs = function(initV, handler){
//1.初始化颜色
let colors = this.initializeColor() //2.创建队列
let que = new Queue() //3.将顶点加入到队列中
que.enqueue(initV) //4.循环从队列中取出元素
while(!que.isEmpty()){
//4.1.从队列中取出一个顶点
let v = que.dequeue() //4.2.获取和顶点相相邻的其他顶点
let vNeighbours = this.edges.get(v) //4.3.将v的颜色变为灰色
colors[v] = 'gray' //4.4.遍历v所有相邻的顶点vNeighbours,并且加入队列中
for (let i = 0; i < vNeighbours.length; i++) {
const a = vNeighbours[i];
//判断相邻顶点是否被探测过,被探测过则不加入队列中;并且加入队列后变为灰色,表示被探测过
if (colors[a] == 'white') {
colors[a] = 'gray'
que.enqueue(a)
}
} //4.5.处理顶点v
handler(v) //4.6.顶点v所有白色的相邻顶点都加入队列后,将顶点v设置为黑色。此时黑色顶点v位于队列最前面,进入下一次while循环时会被取出
colors[v] = 'black'
}
} //六.实现深度搜索(DFS)
Graph.prototype.dfs = function(initV, handler){
//1.初始化顶点颜色
let colors = this.initializeColor() //2.从某个顶点开始依次递归访问
this.dfsVisit(initV, colors, handler)
} //为了方便递归调用,封装访问顶点的函数,传入三个参数分别表示:指定的第一个顶点、颜色、处理函数
Graph.prototype.dfsVisit = function(v, colors, handler){
//1.将颜色设置为灰色
colors[v] = 'gray' //2.处理v顶点
handler(v) //3.访问v相连的其他顶点
let vNeighbours = this.edges.get(v)
for (let i = 0; i < vNeighbours.length; i++) {
let a = vNeighbours[i];
//判断相邻顶点是否为白色,若为白色,递归调用函数继续访问
if (colors[a] == 'white') {
this.dfsVisit(a, colors, handler)
} } //4.将v设置为黑色
colors[v] = 'black'
}
}

参考资料:JavaScript数据结构与算法

JavaScript实现图结构的更多相关文章

  1. JavaScript数据结构——图的实现

    在计算机科学中,图是一种网络结构的抽象模型,它是一组由边连接的顶点组成.一个图G = (V, E)由以下元素组成: V:一组顶点 E:一组边,连接V中的顶点 下图表示了一个图的结构: 在介绍如何用Ja ...

  2. JavaScript实现栈结构(Stack)

    JavaScript实现栈结构(Stack) 一.前言 1.1.什么是数据结构? 数据结构就是在计算机中,存储和组织数据的方式. 例如:图书管理,怎样摆放图书才能既能放很多书,也方便取? 主要需要考虑 ...

  3. Twproject Gantt – 开源的 JavaScript 甘特图组件

    Twproject Gantt 是一款基于 jQuery 开发的甘特图组件,也可以创建其它图表,例如任务树(Task Trees).内置编辑.缩放和 CSS 皮肤等功能.更重要的是,它是免费开源的. ...

  4. 图结构练习——判断给定图是否存在合法拓扑序列(dfs算法(第一个代码),邻接矩阵(前两个代码),邻接表(第三个代码))

    sdut 2140 图结构练习——判断给定图是否存在合法拓扑序列 Time Limit: 1000ms   Memory limit: 65536K  有疑问?点这里^_^ 题目描述  给定一个有向图 ...

  5. 图结构练习——最短路径(floyd算法(弗洛伊德))

    图结构练习——最短路径 Time Limit: 1000ms   Memory limit: 65536K  有疑问?点这里^_^ 题目描述  给定一个带权无向图,求节点1到节点n的最短路径.   输 ...

  6. 图结构练习——最短路径(dijkstra算法(迪杰斯拉特))

      图结构练习——最短路径 Time Limit: 1000ms   Memory limit: 65536K  有疑问?点这里^_^ 题目描述  给定一个带权无向图,求节点1到节点n的最短路径.   ...

  7. 图结构练习——最小生成树(kruskal算法(克鲁斯卡尔))

    图结构练习——最小生成树 Time Limit: 1000ms   Memory limit: 65536K  有疑问?点这里^_^ 题目描述  有n个城市,其中有些城市之间可以修建公路,修建不同的公 ...

  8. 图结构练习——最小生成树(prim算法(普里姆))

      图结构练习——最小生成树 Time Limit: 1000ms   Memory limit: 65536K  有疑问?点这里^_^ 题目描述  有n个城市,其中有些城市之间可以修建公路,修建不同 ...

  9. C++图结构的图结构操作示例

    示例代码: /* By qianshou 2013/10/5明天就要开学了~哎~ */ #include<iostream> using namespace std; /********* ...

随机推荐

  1. git工作中常用操作总结

    这篇文章主要记录下工作中常用的git操作.主要是对之前文章记录的问题做个总结,这个其实在idea中操作更加简单 别名配置 在敲git 命令时,其实可以使用别名,比如 commit可以配置为ci 下面是 ...

  2. .NET Core 3.x之下的配置框架

    一.配置框架的核心类库 首先我们使用.NET Core的配置框架需要安装额外的NuGet扩展包,下面是列举最常用的几个扩展包以及所对应的配置功能 NuGet Package Description M ...

  3. python之迭代器 生成器 枚举 常用内置函数 递归

    迭代器 迭代器对象:有__next__()方法的对象是迭代器对象,迭代器对象依赖__next__()方法进行依次取值 with open('text.txt','rb',) as f: res = f ...

  4. shell编程中星号(asterisk "*")的坑

    今天分享一个有关shell编程中由通配符引起的问题. 1. 问题代码 cat test.logs 4567890 * ##*************************************## ...

  5. 18 JpaRepository和JpaSpecificationExecutor

    继承JpaRepository后的方法列表 JpaRepository findAll() List<T> findAll(Sort) List<T> findAll(Iter ...

  6. 安装mysql.so

    1.----   cd /usr/local/src/php-5.5.34/ext/mysql/2.----  /usr/local/php5/bin/phpize3.---- ./configure ...

  7. openwrt 上的 upnp wifi 音频推送 gmediarender

    首先是必须启用的模块 Libraries ---> <*> libupnp Sound ---> <*> alsa-utils<*> madplay-a ...

  8. golang.org/x/sys/unix: unrecognized

    安装的过程中报错 : package golang.org/x/sys/unix: unrecognized import path "golang.org/x/sys/unix" ...

  9. Spring中的JdbcTemplate的使用

    一.jdbcTemplate的作用 它就是用于和数据库交互的,实现对表的crud.与dbutils相似 二.JdbcTemplate的使用 <dependency> <groupId ...

  10. css 实现九宫格

    1.自己写了一个,写完对比了下别人写的发现自己写的太low.故就不写自己太差劲的了. 别人写的我总结优化了一下,如果不用写内容去掉position,content简单也是可以的. <!DOCTY ...