diff --git a/zh/12.md b/zh/12.md index c7426a53cfa93c07326a8180e6872ad2b1c19dd4..3930f5c87a153b343f1c1a8a25ae50d264100b57 100644 --- a/zh/12.md +++ b/zh/12.md @@ -182,11 +182,11 @@ private Edge nextOutEdge, nextInEdge; 如果图非常稠密(图中的边可能很多),并且以下操作很频繁:判断顶点之间是否存在边和获取边的权重,那么我们可以使用邻接矩阵。我们给顶点0~顶点|V|-1(|V|代表顶点集V的大小),然后初始化一个|V|*|V|的矩阵,如果顶点i和顶点j之间有边,那么矩阵中的元素(i,j)的值设为1,反之设为0。如果是有权重的边,则把元素(i,j)的值设为权重,如果没有边也可以把值设为某个特殊值(这种情况可以由图12.3扩展而来)。无向图的矩阵是对称的。图12.7展示了一个无权有向图和一个无权无向图和他们的邻接矩阵。 -无权图的邻接矩阵有一些有趣的特性。比如说图12.7的第一张图,首先我们把矩阵与自己相乘的结果定义为:![expr1](http://latex.codecogs.com/gif.latex?(X+%5ccdot+X)_%7bij%7d%3d%5csum_%7b0+%5cleq+k%3c%7cV%7c%7dX_%7bik%7d+%5ccdot+X_%7bkj%7d) 。然后把上述矩阵和自己相乘,得到结果: +无权图的邻接矩阵有一些有趣的特性。比如说图12.7的第一张图,首先我们把矩阵与自己相乘的结果定义为:![expr1](http://latex.codecogs.com/gif.latex?(X%20\cdot%20X)_{ij}=\sum_{0%20\leq%20k<|V|}X_{ik}%20\cdot%20X_{kj}) 。然后把上述矩阵和自己相乘,得到结果: ![12.multi-result](https://github.com/yuanrw/cs61b-textbook-zh/blob/ch12/zh/img/12.multi-result.png) -通过分析这个结果,可以看到![expr2](http://latex.codecogs.com/gif.latex?(X+%5ccdot+X)_%7bij%7d)的值等于满足顶点i和顶点k之间有一条边(![expr3](http://latex.codecogs.com/gif.latex?M_{ik}=1)),顶点k和顶点j之间也有一条边(![expr4](http://latex.codecogs.com/gif.latex?M_{kj}=1))的顶点k的数量。而对于其他的节点,![expr5](http://latex.codecogs.com/gif.latex?M_{ik})和![expr6](http://latex.codecogs.com/gif.latex?M_{kj})中至少有一个为0。所以显然,![expr7](http://latex.codecogs.com/gif.latex?M_{ij}^2)代表顶点i,j两点间长度为2的路的条数。同样,![expr8](http://latex.codecogs.com/gif.latex?M_{ij}^3)代表顶点i,j两点间长度为3的路的条数。如果用布尔代数来计算(布尔代数中0+1=1+1=1),那么当两个顶点满足:顶点间有至少一条长度为2的路径,它在矩阵中的值就为1。 +通过分析这个结果,可以看到![expr2](http://latex.codecogs.com/gif.latex?(X%20\cdot%20X)_{ij})的值等于满足顶点i和顶点k之间有一条边(![expr3](http://latex.codecogs.com/gif.latex?M_{ik}=1)),顶点k和顶点j之间也有一条边(![expr4](http://latex.codecogs.com/gif.latex?M_{kj}=1))的顶点k的数量。而对于其他的节点,![expr5](http://latex.codecogs.com/gif.latex?M_{ik})和![expr6](http://latex.codecogs.com/gif.latex?M_{kj})中至少有一个为0。所以显然,![expr7](http://latex.codecogs.com/gif.latex?M_{ij}^2)代表顶点i,j两点间长度为2的路的条数。同样,![expr8](http://latex.codecogs.com/gif.latex?M_{ij}^3)代表顶点i,j两点间长度为3的路的条数。如果用布尔代数来计算(布尔代数中0+1=1+1=1),那么当两个顶点满足:顶点间有至少一条长度为2的路径,它在矩阵中的值就为1。 稀疏的图(边的数量远远小于![expr9](http://latex.codecogs.com/gif.latex?V^2)的图)不适合用邻接矩阵来存储。当你想动态地添加和删除顶点时,它们也会出问题。 @@ -262,4 +262,233 @@ REMOVE HIGHEST PRIORITY ITEM 是删除并返回队列中的第一个(最近添 有向图的*拓扑排序*是指把顶点按照特定规则排序:如果从顶点v出发到顶点w是可达的,那么w就排在v后面,如果我们把图想象成顶点的某种有序关系,那么拓扑排序就是一种线性的排序。循环有向图不存在拓扑排序。例如,UNIX系统的make程序使用了拓扑排序,用来查找执行命令的顺序,然后更新每个文件,以便在后续命令中使用。 -执行拓扑排序的时候,需要维护每个顶点在当前未处理的顶点集中的入度。以下版本使用一个数组来保存入度。拓扑排序的算法的当前实现: \ No newline at end of file +执行拓扑排序的时候,需要维护每个顶点在当前未处理的顶点集中的入度。以下版本使用一个数组来保存入度。拓扑排序的算法的当前实现: +```java +/** An array of the vertices in G in topologically sorted order. * Assumes G is acyclic. */ +static int[] topologicalSort(Digraph G) { +int[] count = new int[G.numVertices()]; int[] result = new int[G.numVertices()]; int k; +for (int v = 0; v < G.numVertices(); v += 1) count[v] = G.inDegree(v); +Graph-traversal schema replacement for topological sorting; return result; +} +``` + +模板中有下列可替换的地方: + +COLLECTION OF VERTICES 可以是顶点的集合、multiset、列表或序列(栈、队列等)。 + +INITIAL COLLECTION 是所有v的集合,将count[v]初始化为0。 + +REMOVE HIGHEST PRIORITY ITEM 可以删除任意元素。 + +MARKED 和 MARK 可以是不重要的操作。(比如永远返回false,不做任何操作)。 + +VISIT(v) 将顶点v设为结果集中的下一个非空元素,并且把与顶点v的所有边(v,w)相邻的顶点的入度count[w]值减一。 + +NEEDS PROCESSING 如果count[w]==0,则返回true。 + +图12.8展示了该算法。 + +### 12.3.5 最小生成树 + +现在讨论一个连通加权无向图。*最小(权值)生成树*(简称MST)是一种树,它是给定图的子图,包含给定图的所有顶点,并且权的总和最小。比如,我们有一些城市,现在想以电话线作为路径把这些城市连接起来,并且让电话线的成本达到最小。城市就是顶点,城市之间的连接就是边。寻找最小连接集合的过程就是寻找最小生成树(可能不止一个)。为此,我们利用了最小连通树的一个有用定理。 + +![12.8](https://github.com/yuanrw/cs61b-textbook-zh/blob/ch12/zh/img/12.8.png) + +图12.8:拓扑排序的输入(左上图)和处理的三个阶段。灰色的是已处理并且放入结果集的节点。第一个处理的节点是图的边缘上的一个节点。字母下标代表节点入度。按照图中的处理。最终的结果是:A, C, F, D, B, E, G,H(这只是其中一种可能)。 + +**定理:**如果连通图G的顶点被分成两个不相交的非空集,![v0](http://latex.codecogs.com/gif.latex?V_0)和![v1](http://latex.codecogs.com/gif.latex?V_1),那么G的任意最小连通树一定包含这样一条边:边所连接的两个节点分别属于![v0](http://latex.codecogs.com/gif.latex?V_0)和![v1](http://latex.codecogs.com/gif.latex?V_1)(即横跨![v0](http://latex.codecogs.com/gif.latex?V_0)和![v1](http://latex.codecogs.com/gif.latex?V_1)),并且边的权值是横跨两个图的所有边中最小的(可能不止一条)。 + +**证明。**可以很方便地用反证法证明。假设最小生成树T不包含横跨![v0](http://latex.codecogs.com/gif.latex?V_0)和![v1](http://latex.codecogs.com/gif.latex?V_1)且权值最小的边。现在给T添加一条横跨![v0](http://latex.codecogs.com/gif.latex?V_0)和![v1](http://latex.codecogs.com/gif.latex?V_1)且权值最小的边e,得到图![T'](http://latex.codecogs.com/gif.latex?T')(必须有这样一条边,否则T就不是连通的)。因为T是一棵树,添加了这条新的边之后,会产出一个包含e的环(因为连接e的两个节点之间肯定是有路径的)。这个环中包含的另一条横跨![v0](http://latex.codecogs.com/gif.latex?V_0)和![v1](http://latex.codecogs.com/gif.latex?V_1)边![e'](http://latex.codecogs.com/gif.latex?e'),假设e的权值比![e'](http://latex.codecogs.com/gif.latex?e')小,把![e'](http://latex.codecogs.com/gif.latex?e')从![t'](http://latex.codecogs.com/gif.latex?T')中移除,得到一颗新的树,但是由于我们用![e'](http://latex.codecogs.com/gif.latex?e')替换了e,这棵新树的边权值之和小于T的边权值之和,和假设矛盾。得出T不包含从![v0](http://latex.codecogs.com/gif.latex?V_0)到![v1](http://latex.codecogs.com/gif.latex?V_1)的最小权值边的假设是错的。(证明) + +利用这个定理,我们把已经选中的要作为树的边所连接的顶点放入已处理(已标记)集合![v0](http://latex.codecogs.com/gif.latex?V_0),剩下的所有顶点放入集合![v1](http://latex.codecogs.com/gif.latex?V_1)。根据上述定理,我们可以安全地将标记顶点到未标记顶点的任何最小权值边添加到树中。 + +这就是*普里姆算法*(Prim’s algorithm)。这次我们给每个节点引入两个额外信息,dist[v](权重集)和parent[v](顶点集)。在算法的每个点上,未处理顶点(仍然在树边缘上的顶点)的dist值是它与已处理顶点之间的最小距离(权值),parent的值是达到该最小距离的已处理顶点。 + +```java +/** For all vertices v in G, set PARENT[v] to be the parent of v in +* a MST of G. For each v in G, DIST[v] may be altered arbitrarily. +* Assumes that G is connected. WEIGHT[e] is the weight of edge e. */ +static void MST(Graph G, int[] weight, int[] parent, int[] dist) { +for (int v = 0; v < G.numVertices(); v += 1) { dist[v] = ∞; + parent[v] = -1; + } +Let r be an arbitrary vertex in G; dist[r] = 0; +Graph-traversal schema replacement for MST; } + +``` + +图遍历模板的适当“设置”如下。 + +COLLECTION OF VERTICES 是把顶点的dist值从小到大排列的优先队列。 + +INITIAL COLLECTION 包含图G的所有顶点。 + +REMOVE HIGHEST PRIORITY ITEM 删除优先队列中的第一项。 + +VISIT(v):对于每个边缘(v, w)和权重n,如果顶点w未处理且和dist [w] > n,则将dist [w] 值设为n,将parent[w]设为v。 + +NEEDS PROCESSING(v) 永远返回false。 + +图12.9演示了该算法的实际应用。 + +#### 12.3.6 单源最短路径 + +假设有一加权图(有向图或无向图),我们想找到从某个节点到它的每个可达节点的最短路径。最简洁的算法叫做*最短路径树*。这是一个生成树(不一定是最小生成树),它以所需节点为根,从根到树中任意其它节点的路径也是整个图中总权重最小的路径。 + +最常见的是迪杰斯特拉算法,它看起来和Prim的MST算法基本一样。PARENT和DIST数据和之前一样。但是在Prim的算法中,DIST代表从未标记顶点到已标记顶点的最短距离,而在迪杰斯特拉算法中,DIST代表从起始节点开始到顶点所有已知的最短路径的长度。 + +```java +/** For all vertices v in G reachable from START, set PARENT[v] +* to be the parent of v in a shortest-path tree from START in G. For +* all vertices in this tree, DIST[v] is set to the distance from START * WEIGHT[e] are non-negative edge weights. Assumes that vertex +* STARTisinG.*/ +static void shortestPaths(Graph G, int start, int[] weight, int[] parent, double[] dist) +{ +for (int v = 0; v < G.numVertices(); v += 1) { +dist[v] = ∞; + parent[v] = -1; + } +dist[start] = 0; +Graph-traversal schema replacement for shortest-path tree; } + +``` + +我们把算法模板做如下替换: + +COLLECTION OF VERTICES 是把顶点的dist值从小到大排列的优先队列。 + +INITIAL COLLECTION 包含图G的所有顶点。 + +![12.9](https://github.com/yuanrw/cs61b-textbook-zh/blob/ch12/zh/img/12.9.png) + +图12.9:最小生成树的Prim算法。顶点r是A。节点中的数字是dist值。虚线表示parent值;它们在最后形成一个最小生成树。白色的节点位于边缘。最后两个步骤(不会改变parent指针)合并为一个步骤。 + +REMOVE HIGHEST PRIORITY ITEM 移除优先队列中的第一项。 + +MARKED 和 MARK可以是不重要的操作(返回false,什么都不做)。 + +VISIT(v):每条边(v, w)和重量n,如果dist [w] > n + dist [v],集dist [w] n + dist [v]和设置父[w]诉重排边缘。 + +NEEDS PROCESSING(v) 永远返回false。 + +图12.10演示了迪杰斯特拉算法的实际应用。 + +由于迪杰斯特拉算法和Prim算法的结构很相似,它们的时间复杂度也是相似的。每个节点只会访问一次(从优先队列中删除一个节点),每访问一条边最多只会重排一次优先队列。因此,如果V是图G的顶点数,E是边数,那算法运行时间的上界就是![expr10](http://latex.codecogs.com/gif.latex?O((V+E)%20\cdot%20lgv))。 + +#### 12.3.7 A星算法 + +Dijkstra算法可以有效地从图中的一个起点找到所有最短路径。但是,如果只想得到从一个起点到一个终点的一条最短路径。我们也可以修改访问迪杰斯特拉算法的步: + +VISIT(v):[单个终点]如果v是终点则退出方法。否则,对于v的每条边(v, w),权值为n,如果dist[w] > n + dist[v],则将dist[w]设为n + dist[v],并将parent[w]设为v。根据需要重新排列。 + +这样就避免了计算距离起点更远的终点,但是Dijkstra算法还是可能做大量不必要的操作。 + +例如,假设你想找到一条从丹佛到纽约市的最短道路。虽然可以保证当从优先队列中选择纽约市时,停止算法。但是在算法考虑曼哈顿的一条街道之前,它已经找到了从丹佛到西海岸(除了阿拉斯加)、墨西哥和加拿大西部各省的几乎所有目的地的最短路径——这些方向都是错的! + +我们可以通过改变节点的顺序来修改算法——一个偏向于我们预期目标的节点。通过必要的调整后得到的算法称为A星算法: + +![12.10](https://github.com/yuanrw/cs61b-textbook-zh/blob/ch12/zh/img/12.10.png) + +图12.10:Dijkstra的最短路径算法。起始节点为A。节点中的编号表示到目前找到的该节点到A的最小距离(dist)。虚线箭头表示父指针;它们的最终值是形成一颗最短路径树。最后三步已合并为了一步。 + +```java +/** For all vertices v in G along a shortest path from START to END, * set PARENT[v] to be the predecessor of v in the path, and set +* DIST[v] is set to the distance from START. WEIGHT[e] are +* non-negative edge weights. H[v] is a consistent heuristic +* estimate of the distance from v to END. Assumes that vertex START +* is in G, and that END is in G and reachable from START. */ +static void shortestPath(Graph G, int start, int end, int[] weight, int[] h, +int[] parent, double dist[]) +{ +for (int v = 0; v < G.numVertices(); v += 1) { +dist[v] = ∞; + parent[v] = -1; + } +dist[start] = 0; +Graph-traversal schema replacement for A* search; } +``` + +A星搜索算法的模板和Dijkstra算法相同,只是把步骤VISTT替换为上面的单个终点的版本。 + +COLLECTION OF VERTICES [A* search]是把顶点按dist(v)+h[v]值排序的优先队列。 + +换句话说,不同之处在于,我们需要假设通往终点的最短路径经过当前节点,然后估算出最短路径的大小。Dijkstra算法本质上是相同的,只不过h[v] = 0。 + +想要得到最优且正确的行为,我们需要h的做一些限制,即启发式距离估计。如注释所示,h是不变的。所以它必须是可容许的:h[v]不能比从v到终点的实际最短路径长度还要大。其次,如果(v, w)是一条边,那么 + +![expr11](http://latex.codecogs.com/gif.latex?h[v]%20\leq%20weight[(v,w)]+h[w])) + +这是我们熟悉的三角形不等式的一个版本:三角形任意一条边的长度必须小于或等于另外两条边的长度之和。满足这些条件的时候,A星算法是最优的,因为没有其它使用相同的启发式信息(h)的算法可以访问更少的节点(如果有多个权重相同的路径则需要其他限制)。 + +现在重新考虑从丹佛出发的路线规划,我们可以使用到纽约直线距离作为我们的启发式信息,因为这个距离满足三角不等式,且比两点之间的任意组合都要小。然而在实际的运用中,普遍的做法是对数据进行大量的预处理,这样查询的时候就不需要进行完整的搜索,从而加快操作速度。 + +#### 12.3.8 克鲁斯克尔算法 + +图遍历模板并不是唯一可行的方法,我们将考虑一种“经典”方法来形成最小生成树,称为*克鲁斯克尔算法*。该算法依赖于*并查集*。任何时候,这个结构都包含一个顶点分区:一个包含所有顶点的不相交顶点集的集合。最开始,每个集合只有单独的一个节点。做法是每次增加一条边,来构成最小生成树。我们选择一条权重最小的跨越两个不同顶点集的边,把它增加到最小生成树中,然后把这两个顶点集合并成一个集合,重复此操作,直到所有顶点集合成一个(集合中必须包含所有的顶点)。每个集合都是一组顶点,这些顶点被我们添加到最小生成树中的边连接起来,每两个顶点都是可达的。当只有一个集合时,就说明所有的顶点都是可达的,那我们就得到了能够连接整棵树的边。根据§12.3.5中的定理,如果我们每次添加的边都是连接两个不相交顶点集的最小加权边,那么该边总是最小生成树的一部分,因此最终得到的肯定是一颗最小生成树。图12.11展示了这个算法。 + +对于这个程序,假设我们有一种数据结构—并查集(UnionFind),用于存储顶点集。我们需要它进行两个操作:S.sameSet(v, w)返回顶点v和w是否在S中的同一个顶点集中,以及S.union(v, w),它将包含顶点v和w的集合合并成一个集合。还有一个边集用来存储最终结果。 + +![12.11](/Users/yrw/Library/Application Support/typora-user-images/image-20190630122605425.png) + +图12.11:克鲁斯克尔算法。顶点中的数字表示集合:标记相同数字的顶点位于同一集合中。虚线表示边已经被添加到最小生成树中。这和图12.9中的最小生成树不同。 + +```java +/** Return a subset of edges of G forming a minimum spanning tree for G. * G must be a connected undirected graph. WEIGHT gives edge weights. */ +EdgeSet MST(Graph G, int[] weight) { + UnionFind S; + EdgeSet E; +// Initialize S to {{v} | v is a vertex of G}; S = new UnionFind(G.numVertices()); +E = new EdgeSet(); +For each edge (v,w) in G in order of increasing weight { if (! S.sameSet(v, w)) { + Add (v,w) to E; + S.union(v, w); + } +} +return E; } + +``` + +union-find位是比较复杂的部分。你可能认为每个sameSet操作的时间复杂度最坏情况下是Θ(nlgn)(在每个到N设置每个大小到N)。有一个更好的方法。假设(在这个问题中)集合包含从0到N - 1的整数。任何时候,最多有N个不相交的集合;我们从每个集合中选出一个数字,用该数字(一个数字0到N−1)作为集合id。对于某个数字代表的非空集合,可以通过顶点的集合id是否相同来判断两个顶点是否在同一个集合中。一种实现方式是将每个不相交集表示为一课顶点树,子节点指向父节点(您可能还记得我说过这样的数据结构会用用武之地)。每棵树的根都是集合id,可以通过父指针找到。例如,以下集合 + +{{1,2,4,6,7},{0,3,5},{8,9,10}} + +可以表示成下面这样的森林 + +![12.insert1](https://github.com/yuanrw/cs61b-textbook-zh/blob/ch12/zh/img/12.insert1.png) + +我们用一个整数数组 parent 来存储,其中parent[v]的值为v的父节点id,如果v没有父节点(即v是集合id),parent[v]=-1。union操作很简单:执行S.union(v,w),找到包含v和w的树的根(通过父指针向上找),然后把其中一个根变成另一个根的孩子。例如,执行S.union(6,0),先找到6的集合id(1)和0的集合id(3),然后把3的父指针指向1: + +![12.insert2](https://github.com/yuanrw/cs61b-textbook-zh/blob/ch12/zh/img/12.insert2.png) + +为了优化算法,我们应该把较小的树(一般是指高度)指向较大的树。 + +这个时候我们加入一个小插曲。在遍历了从顶点6到顶点1和从顶点0到顶点3的路径之后,我们把路径中经过的每个节点的父指针直接指向节点1,由此重新组织树(实际上是“记录”找到集合id的操作结果)。因此,在为6和0找到集合id并进行统一之后,我们得到下面这颗更平坦的树: + +![12.insert3](https://github.com/yuanrw/cs61b-textbook-zh/blob/ch12/zh/img/12.insert3.png) + +这种重排称为*路径压缩*,它会使后续关于顶点6、4和0的访问比原来快很多。事实证明,这个技巧(包括把浅树指向一颗深的树形成一个集合),使包含N个元素的多个集合中的任意一个集合上的任意序列M中的union和sameSet操作时间复杂度为O(α(M, N) M)。在这里,α(M, N)是一个逆*阿克曼*的函数。具体来说,α(M, N)被定义为当i最小时使A(i,⌊M/N⌋) > lgN,且 + +A(1,j) = ![2^j](http://latex.codecogs.com/gif.latex?2^j),当![expr12](http://latex.codecogs.com/gif.latex?j%20\geq%201), +A(i,1) = A(i−1,2),当![expr13](http://latex.codecogs.com/gif.latex?i%20\geq%202), +A(i,j) = A(i − 1,A(i,j − 1)),当![expr14](http://latex.codecogs.com/gif.latex?i,j%20\geq%202) + +好吧,这些相当复杂,但我只想说,A增长地非常快,所以α增长很缓慢,而且无论如何都小于等于4。简而言之,M的操作(任何组合中的union和sameSets)的*平摊成本*大致为常数。因此,Kruskal算法所需的时间主要由边的排序时间决定,对于边的数量E,趋近于O(elg E)。对于连通图等于O(elgv),其中V是顶点的数量。 + +### 练习 + +**12.1。**一只鹦鹉和一只魔鬼发现自己身处迷宫中,迷宫中蜿蜒曲折的小通道连接着许多房间,其中有一个是迷宫出口。魔鬼发现鹦鹉特别美味。对于鹦鹉来说(对魔鬼来说就是它未来的猎物)不幸的是,魔鬼跑得比鹦鹉快两倍,并且魔鬼有特殊能力可以找到通往出口的最短路径。对鹦鹉来说幸运的是,他异常敏锐的感官能随时准确地感应到魔鬼的位置,而且他对迷宫的结构了如指掌。如果他能比魔鬼先到达出口或魔鬼行走的路径上的任意房间(必须之前,不能是同时),它就能抓住魔鬼。魔鬼并不聪明,它总是走最短的路径,即使是鹦鹉已经前面等着它,他也不会改变路线。例如,在下面的迷宫中,鹦鹉(起点在S点)经过6个时间单位就能到阴影房间用餐,魔鬼(起点在B点)经过7个时间单位能到。连接通道上的数字表示距离(房间内的数字只是标签)。斯纳克的速度是0.5单位/小时,波罗戈夫的速度是1单位/小时。 + +![12.exec](https://github.com/yuanrw/cs61b-textbook-zh/blob/ch12/zh/img/12.exec.png) + +编写一个程序,读取上述迷宫,根据不同的情况打印以下两条信息之一:Snark eats或Borogove escapes。把你的代码写在Chase类中(参见cs61b / hw / hw7中的模板)。 + +输入如下: + +* 一个正整数N≥3,表示房间数。你可以认为N < 1024。房间的编号从0到N - 1。0号房间总是出口。最开始魔鬼在1号房间,鹦鹉在2号房间。 +* 边的序列,每条边由两个房间号(房间号的顺序任意)和一个距离整数组成。 + +如果同时有两条最短路径,魔鬼总是选择房间号小的那条。 + +迷宫的输入格式如下: + +![12.exec2](https://github.com/yuanrw/cs61b-textbook-zh/blob/ch12/zh/img/12.exec2.png) \ No newline at end of file diff --git a/zh/img/12.10.png b/zh/img/12.10.png new file mode 100644 index 0000000000000000000000000000000000000000..78c7e84df0ce35d91b01a0443b928abf0c56d7fa Binary files /dev/null and b/zh/img/12.10.png differ diff --git a/zh/img/12.11.png b/zh/img/12.11.png new file mode 100644 index 0000000000000000000000000000000000000000..0daaf84912642017dce58b44c533266ff11a61fe Binary files /dev/null and b/zh/img/12.11.png differ diff --git a/zh/img/12.6.png b/zh/img/12.6.png index 33fee54bab2209652e9b80e8392b36ef4ae5df7c..bd6e94f8f1c8f402ab43912cb881cdd64bb20e61 100644 Binary files a/zh/img/12.6.png and b/zh/img/12.6.png differ diff --git a/zh/img/12.8.png b/zh/img/12.8.png new file mode 100644 index 0000000000000000000000000000000000000000..16ffcb055c5807ac74fa5d4bcc9dea8c11282b34 Binary files /dev/null and b/zh/img/12.8.png differ diff --git a/zh/img/12.9.png b/zh/img/12.9.png new file mode 100644 index 0000000000000000000000000000000000000000..489ceca3753e15990835eb881f9bc53ce855070e Binary files /dev/null and b/zh/img/12.9.png differ diff --git a/zh/img/12.exec.png b/zh/img/12.exec.png new file mode 100644 index 0000000000000000000000000000000000000000..4801600dd4bb488d3e4d6c85adcb02cef339c41c Binary files /dev/null and b/zh/img/12.exec.png differ diff --git a/zh/img/12.exec2.png b/zh/img/12.exec2.png new file mode 100644 index 0000000000000000000000000000000000000000..f4deab648c0de9a914a7629111e8295cada5b6fb Binary files /dev/null and b/zh/img/12.exec2.png differ diff --git a/zh/img/12.insert1.png b/zh/img/12.insert1.png new file mode 100644 index 0000000000000000000000000000000000000000..a14342c70ff807374c4030f8f9ffd6926117be84 Binary files /dev/null and b/zh/img/12.insert1.png differ diff --git a/zh/img/12.insert2.png b/zh/img/12.insert2.png new file mode 100644 index 0000000000000000000000000000000000000000..3987f469f1b8d038fe18928cee79c9b27848b00a Binary files /dev/null and b/zh/img/12.insert2.png differ diff --git a/zh/img/12.insert3.png b/zh/img/12.insert3.png new file mode 100644 index 0000000000000000000000000000000000000000..b8c4c0da90bf15c9e29caa94b688b4d1853e7789 Binary files /dev/null and b/zh/img/12.insert3.png differ