CS224W笔记-第十一课

CS224W笔记-第十一课:PageRank

第11课的重点是相对传统的连接分析,核心是PageRank算法。全讲座分3个部分,

  • 2000年时期的Web的一个连接概览;
  • PageRank算法的核心思想和具体工程实现中的关键点;
  • PageRank算法的一个变体。

除了第一部分,关于PageRank的部分可以用讲义的最后一页做一个非常好的总结,如下:

PageRank Summary整个PageRank的算法可以从一个图的随机游走的角度来理解,根据闪跳(Teleport)的目的地不同,分成了课程里介绍的3种。具体下面笔记里总结。

90年代的Web概览

本课的第一部分讲了一篇2000年左右的论文,对当时的Web的情况做了一个快照研究。

这里的研究把Web看成了一个有向图。图中的节点是一个网页,边是超链接,边的方向是从一个包含超链接的网页指向超链接的网页。当然,这里面的网页的定义需要简化,对于当时的Web,其中不包括动态生成的网页和那些防火墙内无法被访问到的网页,以及一些爬虫无法触及的Web内容。

依托于通过超链接构建的有向同构图,这个研究关注的问题是:

  1. Web是如何连接的?
  2. 当时的Web是怎样的一副图景?

强连通元件(SCC)

Web是一个有向图,而有向图里面只有两种类型的元件:

  • 强连通元件(SCC):SCC里的节点都两两可达, I n ( A ) = O u t ( A ) In(A) = Out(A) In(A)=Out(A)
  • 有向无环图(DAG):其中,如果 u u u可以有通路连接 v v v,则没有通路从 v v v连到 u u u

SCC的定义:

一个节点集 S S S,其中的任意两个节点可以有链路到达。而没有更大的集合具有这种特性。

90年代的Web概览

而有向图则可以被简化成以SCC为节点的DAG。基于这种思想,1999年Broder等人对当时的Web进行了链接的分析。通过使用网络爬虫爬取了2亿多个URL以及15亿条链接所组成的网络。

研究的核心问题是:Web是如何简化成SCC,以及由此形成的DAG是什么样的。

计算SCC有一个计算问题,即如何才能获得包括节点 v v v的SCC,相应地就是如何找到Web里的SCC。这里的计算方法是基于一个发现,包含 v v v的SCC是 v v v可达的节点和可到达 v v v的两个节点集合的交集。:
S C C   w i t h   v = O u t ( v ) ∩ I n ( v ) = O u t ( v , G ) ∩ O u t ( v , G ′ ) SCC \ with \ v= Out(v) \cap In(v) = Out(v, G) \cap Out(v, G') SCC with v=Out(v)In(v)=Out(v,G)Out(v,G)
其中, G G G是原有向图, G ′ G' G是把 G G G里所有边方向变换的图,也就相当于是把 I n ( v ) In(v) In(v)变成了 O u t ( v ) Out(v) Out(v)

在后续的Web概览分析中,随机选择一个起点 v v v,然后BFS计算通过它可以访问到的节点集合 O u t ( v ) Out(v) Out(v)和可以访问到它的节点 I n ( v ) In(v) In(v)。得到了下面的几个结论:

1. 可触达性(Reachability):

通过对 S C C SCC SCC的计算,可以得到每个节点可触达的节点数,排序后可以得到下图所示的可触达性的结果。
Reachability图中的横坐标是选择的初始起节点的比例,按Jure的说法,是把节点按照可触达节点的数量进行了排序。图中的纵坐标是log后的可触达节点的数量。

可以看到,当时的Web页面的可触达的页面有一个明显的跨越。50%的节点可触达的节点不到1000,而另外的50%的节点则是可以触达亿级别的节点。

2. Web的SCC:
根据以上的统计,可以得到网页(节点)可触达和可被触达的统计,并得到Web里的SCC的情况。

  • O u t ( v ) = 100 M , 占 50 % 左 右 的 节 点 Out(v) = 100M,占50\%左右的节点 Out(v)=100M50%
  • I n ( v ) = 100 M , 占 50 % 左 右 的 节 点 In(v) = 100M,占50\%左右的节点 In(v)=100M50%
  • 最 大 的 S C C = 56 M , 占 28 % 左 右 的 节 点 最大的SCC = 56M,占28\%左右的节点 SCC=56M28%

3. Web概览
上述的数据让研究者勾勒出了90年代的Web的概览,如下图所示:
90Web
在90年代的Web就是中间一个巨大的SCC,包括5千6百万个网页,相互都有路径可达;外加两个单方向的大组件,各自包括大概4千4百万个网页。除此以外,就是一些突触,包括一些小规模的单方向的网页。

这个Web链接的概览对于当时的互联网而言是非常有意义的。对于当时野蛮生长的Web来收,这是第一次完整的刻画,也为后来很多的基于链接关系进行搜索的算法的产生带来了灵感。最出名的是PageRank和HITS。二者的基本思想差不多,只是HITS的老师没有创业,而PageRank则产生了谷歌。

PageRank

问题:PageRank和HITS需要解决的问题是一样的,都是如何去衡量网页的重要性。这是当时整个Web搜索的一个难题。通过关键字匹配进行信息检索的算法在给出候选网页后,无法更好地对结果进行排序,从而让各种垃圾网页返回给搜索的用户。

基本思路

  1. 网页的重要性可以通过指向它的链接(即投票机制)来衡量,毕竟别人认可你才会通过超链接来指向你,指向网页的链接越多,重要性相对越高;
  2. 来自重要网页的链接,它的重要性也更高。

基本公式
定义网页 j j j的排序值(重要性) r j r_j rj如下:
r j = ∑ i → j r i d i r_j = \displaystyle \sum_{i \to j} \frac{r_i}{d_i} rj=ijdiri
其中, i i i是网页 j j j的邻居,并有链接指向它; d i d_i di是网页 i i i的出度值。

基本版PageRank

基本版PageRank的计算需要先构建一个随机邻接矩阵 M M M,方式如下:

  • 对于包括 N N N个网页的链接图,构建 N × N N \times N N×N的邻接矩阵,每行每列都表示一个网页;
  • 邻接矩阵的每个节点的值 M ( i , j ) M_{(i,j)} M(i,j)对应着 j j j列的平均出度 1 / d j 1/d_j 1/dj,如果从网页 j j j有一个链接到网页 i i i,否则为 M ( i , j ) = 0 M_{(i,j)} = 0 M(i,j)=0
  • 根据随机邻接矩阵的定义,每一列的和都是1.

再定义每个网页 i i i具有重要性指标 r i r_i ri,且约束所有网页重要性 r r r的总和为1,即 ∑ i r i = 1 \sum_{i} r_i = 1 iri=1。按照上面的定义,并结合上面的基本公式的计算,则可以得到网页重要性值 r r r和随机邻接矩阵 M M M的关系如下: r = M ⋅ r r= M \cdot r r=Mr

熟悉矩阵计算的同学看到上的公式就可能会发现,这个公式和特征值的定义公式 ( A x = λ x ) (Ax=\lambda x) (Ax=λx)是非常像的。因为 M M M是一个方阵,即可分解出特征值和特征向量,所以上面的重要性 r r r就是随机邻接矩阵 M M M的特征值为 λ = 1 \lambda = 1 λ=1的特征向量。

虽然直接对 M M M进行特征分解就能获得需要的 r r r,但是常规方法的计算复杂度是 O ( N 3 ) O(N^3) O(N3),对于Web的规模而言,这个计算量就无法承受了。不过基于 λ = 1 \lambda=1 λ=1这个优良的特征值的性质,可以看到 r = ( M ( M ( M . . . ( M ⋅ r ) ) ) ) r= (M(M(M...(M\cdot r)))) r=(M(M(M...(Mr)))),因此 r r r就是 M ⋅ r M \cdot r Mr的极限值。这样的话就可以用迭代循环的方法来得到 r r r

课程视频的顺序和给出的PPT的顺序在这里略微不同,Jure先从随机游走的角度解释了重要性 r r r就是在Web上进行无限随机游走的静态分布。也解释了上面的极限计算的原理,和随机游走的理解,以及为什么下面介绍的Power Iteration算法的初始值设置的原因。这里会打破这个顺序,先讲Power Iteration和实际计算PageRank的方法。然后再仔细讲讲Web随机游走。

Power Iteration算法

给定一个Web图,其中包括 N N N个节点(页面),页面里的超链接构成了节点间的边,并按照出度值构建了随机邻接矩阵构建 M \mathsf{M} M

Power Iteration算法分三步计算网页的重要性 r r r

  1. 初始化: r 0 = [ 1 / N , 1 / N , … , 1 / N ] T \mathsf{r}^0 = [1/N, 1/N, \dots, 1/N]^{\rm{T}} r0=[1/N,1/N,,1/N]T
  2. 迭代计算: r ( t + 1 ) = M ⋅ r ( t ) \mathsf{r}^{(t+1)} = \mathsf{M} \cdot \mathsf{r}^{(t)} r(t+1)=Mr(t)
  3. 终止条件: ∣ r ( t + 1 ) − r ( t ) ∣ 1 < ε |\mathsf{r}^{(t+1)} - \mathsf{r}^{(t)}|_1 \lt \varepsilon r(t+1)r(t)1<ε

在满足终止条件或者最大迭代次数后,获得的 r r r就是每个网页的重要性值。这样的话,计算量就近似是线性的。

PageRank的实际计算方法
上述的Power Iteration算法在实际的Web计算的时候会存在两种情况,造成重要性计算的失效,从而不能完成对网页实际排序的任务。

  • 问题1: 如上面的Web概览图里所示,有大量的网页会存在有进无出的情况,即死路。在随机邻接矩阵 M M M里,这些网页节点的列的所有值都是0。
  • 问题2: 蜘蛛陷阱问题,即所有的出度都在一组网页内。这种情况下,所有的重要性都会最终传导到这些节点里。
    Dead endSpider trap
    上面两个例子演示了,存在死路节点,会让所有节点的 r r r最终都变成了0;而那些蜘蛛节点则会把整个网络的重要性都汇聚到自己这里,最终只有它的 r r r成1,别人都是0。

随机游走的概念和在PageRank里的应用

这两种情况对应的原因是一致的,就是重要性沿着超链接传播到这里,然后就消失或者困住了。如果用链接投票的概念来理解这个问就不是很直观,这种情况下使用随机游走的概念就好理解了。

随机游走

  1. 对于某个Web(包括 N N N个网页),我们考虑有一个网络浏览者,它从一个网页开始,沿着网页里的超链接跳到(游走)其他的网页上去。随机的含义是指当从网页跳出的时候,它会从所有的超链接里面等概率得随机地选择一个。
  2. 在某个时刻 t t t,这个浏览者处在某个网页 i i i,当下一刻 t + 1 t+1 t+1,它通过超链接跳转到网页 j j j
  3. 一直重复上述过程,不停止。

随机游走概率分布
那么设定 p ( t ) ∈ R N p(t) \in \R^{N} p(t)RN为时刻 t t t的概率向量,其中第 i i i个元素是浏览者在此时刻处于网页 i i i的概率,概率的总和为 1 1 1。那么对于随机游走的情况下, p ( t + 1 ) p(t+1) p(t+1)的值会是: p ( t + 1 ) = M ⋅ p ( t ) p(t+1) = M \cdot p(t) p(t+1)=Mp(t)

因为, p ( t + 1 ) p(t+1) p(t+1)的概率是由 p ( t ) p(t) p(t)和随机邻接矩阵 M M M共同决定的。这和公式 r j = ∑ i → j r i d i r_j = \displaystyle \sum_{i \to j} \frac{r_i}{d_i} rj=ijdiri是一样的。

假如, 在某个Web里,随机游走可以达到一个状态, p ( t + 1 ) = M ⋅ p ( t ) = p ( t ) p(t+1) = M \cdot p(t) = p(t) p(t+1)=Mp(t)=p(t)。这个时候就可以认为 p ( t ) p(t) p(t)是此Web里的随机游走的稳态概率分布。从这个公式可以看出,上面PageRank的基本公式 r = M ⋅ r r = M \cdot r r=Mr 里的 r r r就是随机游走的稳态概率分布。

用随机游走对上述2个问题进行理解

  1. 对于死路节点的问题,这相当于浏览者来到了这个网页后,没法再继续游走了。虽然它停留在了这里,但是对于整个 p ( t ) p(t) p(t)来说,就不可能达到 p ( t + 1 ) = M ⋅ p ( t ) = p ( t ) p(t+1) = M \cdot p(t) = p(t) p(t+1)=Mp(t)=p(t)的状态,也就相当于这个概率分布无法收敛到一个固定值。
  2. 对于陷阱的问题,浏览者就被限制到了这个地方,在 p ( t + 1 ) p(t+1) p(t+1)的时刻,它处在陷阱的概率就是确定的1,所以这也不是我们想要达到的稳态概率分布。

从随机游走的思路来解决上述问题
从随机游走的角度理解了问题的根源,那么就可从随机游走的角度来解决这两个问题。解决方案很简单,即让浏览者在任何网页上都有一定的概率直接瞬移(Teleport)到网络里的任意一个网页。这也就相当于我们浏览网站的时候,不是按照里面的超链接来浏览,而是选择一个新网站来浏览。

有了这个瞬移的方法,随机游走就不会被限定到某个网页里,从而让 p ( t + 1 ) p(t+1) p(t+1)可以继续收敛到某个稳态概率分布。

实际PageRank的算法

根据上述的跳出的思想,基础的PageRank的算法修改成如下的方式:
r j = ∑ i → j β r i d i + ( 1 − β ) 1 N (1-单节点计算) \tag{1-单节点计算} r_j = \displaystyle \sum_{i \to j} \beta \dfrac{r_i}{d_i} + (1-\beta) \dfrac{1}{N} rj=ijβdiri+(1β)N1(1-)
r = A ∗ r , a n d   A = β M + ( 1 − β ) [ 1 N ] N × N (2-矩阵计算) \tag{2-矩阵计算} r = A * r, and \ A = \beta M + (1-\beta) {\genfrac [ ] {1pt}{1}1{N}}_{N \times N} r=Ar,and A=βM+(1β)[N1]N×N(2-)

这里的 β \beta β是一个是否不跳出的概率。即在每个网页上,浏览者都有 1 − β 1-\beta 1β的可能性不按照出链接来游走,而是随机的跳到整个Web的任何一个网页上,这样就带走了 1 − β 1-\beta 1β的重要性的值,平均分配给所有节点。

这个从公示1里可以看出,每个节点接收来自直接领居的重要性,但是被打了折扣,被扣除的重要性会从整个Web的跳出重要性里获取一份均值。

这个方法如果改进成可迭代的矩阵模式就是公式(2),要注意的是这里公式1和2是在一定条件下才完全等价。因为公式(2)里面每个随机邻接矩阵的元素都被加了 1 − β N \dfrac{1-\beta}{N} N1β,在和 r r r相乘后相加,变成了 ( 1 − β N ) ∑ j r j (\dfrac{1-\beta}{N})\sum_j r_j (N1β)jrj。如果我们的Web里面没有死路,那么 r r r的和是 1 1 1,就和公式(1)一样了。但是由于死路节点的存在, r r r的和会小于 1 1 1,这样两个公式就不等价了。

矩阵 A A A计算的问题
使用上面所说的Power Iteration的方法,可以对 r = A × r r = A \times r r=A×r进行迭代计算出PageRank的值。但是在实际的计算中会遇到无法计算的问题。

核心的问题是把 ( 1 − β ) [ 1 N ] N × N (1-\beta) {\genfrac [ ] {1pt}{1}1{N}}_{N \times N} (1β)[N1]N×N加入到随机邻接矩阵后,原本非常稀疏的随机邻接矩阵 M M M变成了稠密的矩阵 A A A。这样的矩阵对内存的要求是异常的大,以至于在MapReduce这样的分布式技术出现前是无法处理的。所以谷歌就把矩阵 A A A的迭代计算修改回了公式1的方式,变成了: r = β M ⋅ r + ( 1 − β ) [ 1 N ] N (3) \tag{3} r = \beta M \cdot r + (1-\beta) {\genfrac [ ] {1pt}{1}1{N}}_{N} r=βMr+(1β)[N1]N(3)
这里的计算就可以用稀疏矩阵的乘法来对 M M M r r r进行存储和运算,而 ( 1 − β ) [ 1 N ] N (1-\beta) {\genfrac [ ] {1pt}{1}1{N}}_{N} (1β)[N1]N是一个 N N N维的向量,所需的存储很小。课堂上Jure说是计算这个版本刺激了谷歌开发出了MapReduce,可见计算量依然很大。

完整的PageRank算法

根据公式(3),完整的PageRank算法如下:
输入:

  • 有向图 G G G,有 N N N个节点,其中可以包括死路节点和陷阱节点。
  • 参数 β \beta β,一般取 0.8 ∽ 0.9 0.8 \backsim 0.9 0.80.9间的一个值,通常为0.85。

输出:PageRank向量 r n e w r^{new} rnew

  • 初始设置 r j o l d = 1 N r_j^{old} = \dfrac{1}{N} rjold=N1
  • 递归循环,直到 ∑ j ∣ r j n e w − r j o l d ∣ < ε \sum_j |r_j^{new}-r_j^{old}|< \varepsilon jrjnewrjold<ε
    • 对于每个网页 j ,   ∀ j ′ n e w = ∑ i → j β r i o l d d i j, \ \forall j^{\prime new} = \sum_{i \to j} \beta\dfrac{r_i^{old}}{d_i} j, jnew=ijβdiriold,如果网页 j j j没有入度,则 r j ′ n e w = 0 r_j^{\prime new}=0 rjnew=0
    • 然后, ∀ j n e w = j ′ n e w + 1 − S N \forall j^{new}=j^{\prime new} + \dfrac{1-S}{N} jnew=jnew+N1S,其中, S = ∑ j j ′ n e w S=\sum_j j^{\prime new} S=jjnew
    • r o l d = r n e w r^{old} = r^{new} rold=rnew

这里的算法和上面的公式(3)还有所区别。就是并没有对每个网页加上 1 − β N \dfrac{1-\beta}{N} N1β,而是 1 − S N \dfrac{1-S}{N} N1S。这样计算就直接避免了死路节点带来的 r r r和小于1 的问题。

下面会采用课程里的一个例子图来按上面的不同的公式来计算PageRank的值进行理解。
样例图
对应的随机邻接矩阵如下:
M = 0. 0. 0. 0.5 0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 0.5 0.333 0.5 0.5 0.5 0.5 1. 1. 0. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.333 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.5 0.5 0.5 0.5 0. 0. 0. 0. 0. 0. 0.333 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. M = \begin{array}{cc} 0. & 0.& 0.& 0.5& 0.& 0. & 0.& 0.& 0.& 0.& 0. \\ 0.& 0.& 1.& 0.5& 0.333 & 0.5 & 0.5& 0.5& 0.5& 1.& 1. \\ 0.& 1.& 0.& 0.& 0.& 0. & 0.& 0.& 0.& 0.& 0. \\ 0.& 0.& 0.& 0.& 0.333 & 0. & 0.& 0.& 0.& 0.& 0. \\ 0.& 0.& 0.& 0.& 0.& 0.5 & 0.5& 0.5& 0.5& 0.& 0. \\ 0.& 0.& 0.& 0.& 0.333 & 0. & 0.& 0.& 0.& 0.& 0. \\ 0.& 0.& 0.& 0.& 0.& 0. & 0.& 0.& 0.& 0.& 0. \\ 0.& 0.& 0.& 0.& 0.& 0. & 0.& 0.& 0.& 0.& 0. \\ 0.& 0.& 0.& 0.& 0.& 0. & 0.& 0.& 0.& 0.& 0. \\ 0.& 0.& 0.& 0.& 0.& 0. & 0.& 0.& 0.& 0.& 0. \\ 0.& 0.& 0.& 0.& 0.& 0. & 0.& 0.& 0.& 0.& 0. \\\end{array} M=0.0.0.0.0.0.0.0.0.0.0.0.0.1.0.0.0.0.0.0.0.0.0.1.0.0.0.0.0.0.0.0.0.0.50.50.0.0.0.0.0.0.0.0.0.0.3330.0.3330.0.3330.0.0.0.0.0.0.50.0.0.50.0.0.0.0.0.0.0.50.0.0.50.0.0.0.0.0.0.0.50.0.0.50.0.0.0.0.0.0.0.50.0.0.50.0.0.0.0.0.0.1.0.0.0.0.0.0.0.0.0.0.1.0.0.0.0.0.0.0.0.0.

这里面节点A是一个死路,它的随机邻接矩阵的向量是全0。使用Google Matrix,即公式(2),计算PageRank就会发生PageRank泄漏的情况。计算模拟(迭代50次, β \beta β取0.8)后得到的PageRank是 [ 0.01212338 , 0.14942966 , 0.12985629 , 0.01260778 , 0.02068081 , 0.01260778 , 0.00694528 , 0.00694528 , 0.00694528 , 0.00694528 , 0.00694528 ] [0.01212338, 0.14942966, 0.12985629, 0.01260778, 0.02068081, 0.01260778, 0.00694528, 0.00694528, 0.00694528, 0.00694528, 0.00694528] [0.01212338,0.14942966,0.12985629,0.01260778,0.02068081,0.01260778,0.00694528,0.00694528,0.00694528,0.00694528,0.00694528],总和是 0.372 0.372 0.372,出现了明显的泄漏情况。所以需要在每轮迭代的时候,把PageRank的和扩展到1,再次计算。使用这个方法,得到的新PageRank是 [ 0.03258693 , 0.40189786 , 0.34880602 , 0.03388896 , 0.05558877 , 0.03388896 , 0.0186685 , 0.0186685 , 0.0186685 , 0.0186685 , 0.0186685 ] [0.03258693, 0.40189786, 0.34880602, 0.03388896, 0.05558877, 0.03388896, 0.0186685, 0.0186685, 0.0186685, 0.0186685 , 0.0186685 ] [0.03258693,0.40189786,0.34880602,0.03388896,0.05558877,0.03388896,0.0186685,0.0186685,0.0186685,0.0186685,0.0186685]

使用公式3进行计算,只需要进行一次矩阵乘法,剩下的都是向量计算。得到的PageRank是 [ 0.03551728 , 0.39001296 , 0.33644825 , 0.03688094 , 0.06043515 , 0.03688094 , 0.02076489 , 0.02076489 , 0.02076489 , 0.02076489 , 0.02076489 ] [0.03551728, 0.39001296, 0.33644825, 0.03688094, 0.06043515, 0.03688094, 0.02076489, 0.02076489, 0.02076489, 0.02076489, 0.02076489] [0.03551728,0.39001296,0.33644825,0.03688094,0.06043515,0.03688094,0.02076489,0.02076489,0.02076489,0.02076489,0.02076489],同时和为1。

到这里就结束了PageRank的核心思路和算法的介绍。课程的最后是对两种随机游走场景的介绍。

重新开始的随机游走和个性化PageRank

重新开始的随机游走(Random Walk with Restart)
这个算法针对的是二分图的场景。目的是针对某个节点,计算出和其他节点和它的关系。它的基本思想也很简单,就是针对某个查询节点,其他节点和它的相似性可以通过从它开始的随机游走所经过的节点的次数来衡量。但是在游走过程中,经过每个节点时,都有一定个概率 α \alpha α会跳回初识的查询节点,重新开始游走。在一定的游走次数后,经过的其他节点的计数就可以作为和查询节点的相似度。算法的代码和例子如下图所示:
重新开始的随机游走
这个算法虽然简单,但是却能充分利用图的结构信息。而且可以很容的并行执行,不需要做矩阵运算,所以很实用。

个性化PageRank
上面的重新开始的随机游走和PageRank所代表的随机游走的区别是,PageRank里,游走着闪跳的目标节点是图里的所有节点,而重新开始的随机游走总是要闪跳会初始的查询节点。

而个性化PageRank则是在这两种情况的中间,即游走闪跳的目标是图里面的一部分节点,通过这个方法可以计算一批节点和其他节点的相似度。

至此第十一课的内容就全部结束了。对最后的三种算法的总结已经在笔记的最开始部分,这里不再重复。

从开始写第十一课到结束,前后有2个多月,也是很辛苦,中间工作繁忙,家里事情也很多。但是PageRank是当年的一个非常重要且常见的图算法,所以一定要搞懂。下面继续第12和13课。将的都是图结构里的事件传播。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值