数据结构(五)
一、红黑树
红黑树是数据结构中比较困难的部分,这里只简单写一下规则,具体封装代码比较复杂,不敲了。详细笔记请看大佬博客:图解红黑树
1.红黑树的五条规则
- 规则1:节点是红色或黑色的;
- 规则2:根节点是黑色的;
- 规则3:每个叶子节点都是黑色的空节点(NIL节点);
- 规则4:每个红色节点的两个子节点都是黑色的(从每个叶子到根的所有路径上不可能有两个连续的红色节点);
- 规则5:从任一节点到其每个叶子节点的所有路径都包含相同数目的黑色节点;
2.红黑树的三种变换
变色、左旋转、右旋转
(1)变色
为了重新符合红黑树的规则,需要把红色节点变为黑色,或者把黑色节点变为红色;
插入的新节点通常都是红色节点:
当插入的节点为红色的时候,大多数情况不违反红黑树的任何规则;
而插入黑色节点,必然会导致一条路径上多了一个黑色节点,这是很难调整的;
红色节点虽然可能导致红红相连的情况,但是这种情况可以通过颜色调换和旋转来调整;
(2)左旋转
以节点X为根逆时针旋转二叉搜索树,使得父节点原来的位置被自己的右子节点替代,左子节点的位置被父节点替代;
(3)右旋转
以节点X为根顺时针旋转二叉搜索树,使得父节点原来的位置被自己的左子节点替代,右子节点的位置被父节点替代;
3.插入操作的五种情况
(1)情况一
当插入的新节点N位于树的根上时,没有父节点。
这种情况下,只需要将红色节点变为黑色节点即可满足规则2 。
(2)情况二
新界点N的父节点P为黑色节点,此时不需要任何变化。
(3)情况三
节点P为红色,节点U也为红色,此时节点G必为黑色,即父红叔红祖黑。
这种情况下需要把父红叔红祖黑
=> 父黑叔黑祖红
(4)情况四
节点P是红色节点,节点U是黑色节点,并且节点N为节点P的左子节点,此时节点G一定是黑色节点,即父红叔黑祖黑。
- 先变色:将父节点P变为黑色,将祖父节点G变为红色;
- 后旋转:以祖父节点G为根进行右旋转;
(5)情况五
节点P是红色节点,节点U是黑色节点,并且节点N为节点P的右子节点,此时节点G一定是黑色节点,即父红叔黑祖黑。
在这种情况下需要:
- 先以节点P为根进行左旋转,旋转后如图b所示;
- 随后将红色节点P和黑色节点B看成一个整体的红色节点N1,将新插入的红色节点N看成红色节点P1如图c所示。此时整体就转换为了情况4。
接着按情况4处理就行了。
二、图
1.什么是图
图结构是一种与树结构有些相似的数据结构,主要特点是:
- 一组顶点:通常用 V (Vertex)表示顶点的集合;
- 一组边:通常用 E (Edge)表示边的集合;
边是顶点和顶点之间的连线;
边可以是有向的,也可以是无向的。比如A----B表示无向,A —> B 表示有向;
其他有关图的表述详见大佬博客:js实现图结构
2.封装一个图结构
图结构应该有两个属性,顶点和边
- 顶点(vertexes):用数组来存储每个顶点
- 边(edges):这里其实添加的是顶点和边的对应关系,用字典来存储,键存储顶点,值存储的是改顶点连接的其他顶点们(数组)
其中字典我们用之前封装的那个结构(当然其实用ES6自带的Map也可以)
//封装一个图结构,其中要设置两个属性
//vertexes存储顶点有哪些
//edges存储的是顶点和边集合的键值对
class Graph {
constructor() {
this.vertexes = []; //存储顶点
this.edges = new Map(); //存储顶点和边的对应关系
}
}
3.add添加顶点和边
1、添加顶点:两个属性都要添加,在edges中应该初始化一个空数组用来存储边的对应
2、添加边:传两个参数,分别是连个顶点,由于我们搞的是无向边,所以要互相指一下
//封装一个图结构,其中要设置两个属性
//vertexes存储顶点有哪些
//edges存储的是顶点和边集合的键值对
class Graph {
constructor() {
this.vertexes = []; //存储顶点
this.edges = new Map(); //存储顶点和边的对应关系
}
//添加顶点
addVertex(v) {
this.vertexes.push(v);
//添加顶点的同时要初始化存储顶点对应边的数据结构
this.edges.set(v, []); //这里使用数组存储边
}
//添加边(无向边,需要互相指一下)
addEdge(v1, v2) {
this.edges.get(v1).push(v2);
this.edges.get(v2).push(v1);
}
}
4.toString输出图的结构
以邻接表的形式输出,即输出每个顶点及该顶点对应的边关系们
代码:
//转字符串
toString() {
let result = '';
for(let i = 0; i < this.vertexes.length; i++) {
let vertex = this.vertexes[i];
result += vertex + ' => ';
let vLinks = this.edges.get(vertex);//拿到连接的顶点们
for(let i = 0; i < vLinks.length; i++) {
let edge = vLinks[i];
result += edge + ' ';
}
result += '\n';
}
return result;
}
上面这段代码并不难,如果看不懂就多捋一捋
测试代码:
//测试代码
let graph = new Graph();
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());
我们添加的这个图应该是这样一个结构:
控制台输出结果:
非常的优雅。
5.图结构的遍历
图的遍历思想与树的遍历思想一样,意味着需要将图中所有的顶点都访问一遍,并且不能有重复的访问(上面的toString方法会重复访问,比如访问A的时候访问B,然后又访问B,B里边又访问A……);
遍历有两种:广度优先遍历、深度优先遍历
两种遍历算法都需要指定第一个被访问的顶点
;
在开始之前,先介绍个我没见过的操作:
往数组里存键值对,键是字符串的形式,这我还真没见过艹
let colors = [];
colors['A'] = 'white';
colors['B'] = 'white';
console.log(colors); //[A: 'white', B: 'white']
(1)广度优先遍历(BFS)
1、用颜色作为是否已访问节点的标识
white
:未入队(未访问), grey
:在队列中(访问中), black
:已出队(完全访问)
初始化时颜色全部改成白色
initializeColor() {
let colors = [];
for(let i = 0; i < this.vertex.length; i++) {
//全部初始化为白色(未访问)
colors[this.vertex[i]] = 'white';
}
return colors;
}
2、利用队列实现
主要思路图解:
主要思路:
1、顶点先入队
2、开启循环,只要队列不为空,就继续循环
3、循环中,首先从队头取出顶点,该顶点出队说明访问完成,颜色改为黑色,输出顶点
4、本轮循环中,继续将已输出顶点的连接顶点们依次入队
5、它们入队时一定要注意,要判断是否已经在队列中或者已经出队,判断的条件很简单,只要颜色是white说明就还没入队,入队后颜色改为gery。(如果不写这个判断,那么由于顶点会有重复遍历的情况,顶点之间的相互连接导致已经在队列的顶点
和已经出队的顶点
会重复入队,这样的话就死循环了)
6、遍历结束(这里默认输出顶点就叫遍历了)
//广度优先遍历(利用队列实现),需要传入开始的顶点
bfs(initV) {
//1.初始化颜色
let colors = this.initializeColor();
//2.声明一个队列,开始顶点先入队
let queue = new Queue();
queue.enqueue(initV);
//3.开始遍历,只要队列不为空就继续遍历
while(!queue.isEmpty()) {
//3.1从队头取出一个顶点
let front = queue.dequeue();
console.log(front);
colors[front] = 'black';
//3.2出队的顶点的连接顶点依次入队,同时更改颜色
let vLinks = this.edges.get(front);
for(let i = 0; i < vLinks.length; i++) {
//这里一定要判断是否已经入队,不然会陷入死循环
if(colors[vLinks[i]] == 'white') {
queue.enqueue(vLinks[i]);
colors[vLinks[i]] = 'grey';
}
}
}
}
(2)深度优先遍历(DFS)
利用栈实现,或者利用递归实现(递归的本质也是函数调用栈)
主要思路:
1、所有顶点初始化为白色
2、调用递归函数(递归函数一定要单独写,不然每次都初始化白色,就栈溢出了)
3、递归函数中先输出,输出完后改为黑色
4、拿到邻居顶点,遍历,如果是白色(未入栈)就递归调用入栈
5、依次访问,然后回去,再访问,直到全部出栈
代码:
//深度优先遍历(利用递归实现)
dfs(initV) {
//1.把所有的顶点初始化为白色
let colors = this.initializeColor();
//2.遍历顶点
this.dfsRecursion(initV, colors);
}
//递归函数
dfsRecursion(initV, colors) {
console.log(initV); //访问并输出
colors[initV] = 'black'; //遍历完改为黑色
let vLinks = this.edges.get(initV); //拿到邻居
for (let i = 0; i < vLinks.length; i++) {
if (colors[vLinks[i]] == 'white') {
this.dfsRecursion(vLinks[i],colors);
}
}
}