《算法学习 4.0.Hash和树_预习材料.pdf》由会员分享,可在线阅读,更多相关《算法学习 4.0.Hash和树_预习材料.pdf(87页珍藏版)》请在taowenge.com淘文阁网|工程机械CAD图纸|机械工程制图|CAD装配图下载|SolidWorks_CaTia_CAD_UG_PROE_设计图分享下载上搜索。
1、Hash、树七月算法邹博2015年4月16日4月算法在线班2/一种NoSQL数据库 Riak4月算法在线班3/节选自七周七数据库4月算法在线班4/字典 字典,又称符号表(symbol table)、关联数组(associative array)或者映射(map),是一种用于保存键值对(keyvalue pair)的抽象数据结构。在字典中,一个键(key)可以和一个值(value)进行关联(或者说将键映射为值),这些关联的键和值就被称为键值对。字典中的每个键都是独一无二的,程序可以在字典中根据键查找与之关联的值,或者通过键来更新值,又或者根据键来删除整个键值对,等等。字典经常作为一种数据结构内置
2、在很多高级编程语言里面,但Redis所使用的C语言并没有内置这种数据结构,因此Redis构建了自己的字典实现。4月算法在线班5/字典 字典在Redis中的应用相当广泛,比如Redis的数据库就是使用字典来作为底层实现的,对数据库的增、删、查、改操作也是构建在对字典的操作之上的。除了用来表示数据库之外,字典还是哈希键的底层实现之一:当一个哈希键包含的键值对比较多,又或者键值对中的元素都是比较长的字符串时,Redis就会使用字典作为哈希键的底层实现。4月算法在线班6/字典的实现 Redis的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,而每个哈希表节点就保存了字典中的一个键值对。
3、4月算法在线班7/哈希表4月算法在线班8/哈希表 table属性是一个数组,数组中的每个元素都是一个指向dictEntry结构的指针,每个dictEntry结构保存着一个键值对。size属性记录了哈希表的大小,也即是table数组的大小,而used属性则记录了哈希表目前已有节点(键值对)的数量。sizemask属性的值总是等于size1,这个属性和哈希值一起决定一个键应该被放到table数组的哪个索引上面。4月算法在线班9/一个大小为 4 的空哈希表4月算法在线班10/哈希表节点 哈希表节点使用dictEntry 结构表示,每个dictEntry结构都保存着一个键值对。4月算法在线班11/哈希
4、表节点 key属性保存着键值对中的键,而val属性则保存着键值对中的值,其中键值对的值可以是一个指针,或者是一个uint64_t整数,又或者是一个int64_t整数。next属性是指向另一个哈希表节点的指针,这个指针可以将多个哈希值相同的键值对连接在一次,以此来解决键冲突(collision)的问题。4月算法在线班12/两个索引值相同的键k1和k04月算法在线班13/字典4月算法在线班14/字典的多态 type属性和privdata属性是针对不同类型的键值对,为创建多态字典而设置的:type属性是一个指向dictType结构的指针,每个dictType结构保存了一簇用于操作特定类型键值对的函数
5、,Redis会为用途不同的字典设置不同的类型特定函数。privdata属性则保存了需要传给那些类型特定函数的可选参数。4月算法在线班15/字典的多态4月算法在线班16/Rehash ht属性是一个包含两个项的数组,数组中的每个项都是一个dictht哈希表,一般情况下,字典只使用ht0哈希表,ht1哈希表只会在对ht0哈希表进行rehash时使用。除了ht1之外,另一个和rehash有关的属性就是rehashidx:它记录了rehash目前的进度,如果目前没有在进行rehash,那么它的值为1。4月算法在线班17/普通状态下的字典4月算法在线班18/哈希算法 当要将一个新的键值对添加到字典里面时
6、,程序需要先根据键值对的键计算出哈希值和索引值,然后再根据索引值,将包含新键值对的哈希表节点放到哈希表数组的指定索引上面。当字典被用作数据库的底层实现,或者哈希键的底层实现时,Redis使用MurmurHash2算法来计算键的哈希值。4月算法在线班19/Redis计算哈希值和索引值的方法4月算法在线班20/MurmurHashMurmurHash是一种非加密型哈希函数,适用于一般的哈希检索操作。由AustinAppleby在2008年发明,并出现了多个变种,都已经发布到了公有领域(public domain)。与其它流行的哈希函数相比,对于规律性较强的key,Murmur Hash的随机分布特
7、征表现更良好。当前的版本是MurmurHash3,能够产生出32bit或128bit哈希值。较早的MurmurHash2能产生32bit或64bit哈希值。对于大端存储和强制对齐的硬件环境有一个较慢的MurmurHash2可以用。MurmurHash2A变种增加了MerkleDamgrd构造,所以能够以增量方式调用。有两个变种产生64bit哈希值:MurmurHash64A,为64位处理器做了优化;MurmurHash64B,为32位处理器做了优化。MurmurHash2160用于产生160bit哈希值,而MurmurHash1已经不再使用。4月算法在线班21/MurmurHash4月算法在线
8、班22/djb2 this algorithm(k=33)was first reported by danbernstein many years ago in comp.lang.c.another version of this algorithm(now favored by bernstein)uses xor:hash(i)=hash(i 1)*33 stri;the magic of number 33(why it works better than many other constants,prime or not)has never been adequately expl
9、ained.这个算法(k=33)是多年前首先由dan bernstein 在comp.lang.c中提出的,这个算法的另外一个版本(bernstein的贡献)是使用异或方式:hash(i)=hash(i 1)*33 stri;魔数33尚未得到足够的解释(为何它能够比其他很多素数的效果更好)4月算法在线班23/djb2 Code4月算法在线班24/sdbm Code(65599)4月算法在线班25/sdbm的说明 This algorithm was created for sdbm(a publicdomain reimplementation of ndbm)database library
10、.it was found to do well in scrambling bits,causing better distribution of the keys and fewer splits.It also happens to be a good general hashing function with good distribution.the actual function is hash(i)=hash(i 1)*65599+stri;which is the faster version used in gawk.The magic constant 65599 was
11、picked out of thin air while experimenting with different constants,and turns out to be a prime.this is one of the algorithms used in berkeley db and elsewhere.4月算法在线班26/哈希冲突 当有两个或以上数量的键被分配到了哈希表数组的同一个索引上面时,我们称这些键发生了冲突(collision)。Redis的哈希表使用链地址法(separate chaining)来解决键冲突:每个哈希表节点都有一个next指针,多个哈希表节点可以用ne
12、xt指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表连接起来,这就解决了键冲突的问题。4月算法在线班27/冲突4月算法在线班28/Rehash rehash是在hash table的大小不能满足需求,造成过多hash碰撞后需要进行的扩容hash table的操作,其实通常的做法确实是建立一个额外的hash table,将原来的hash table中的数据在新的数据中进行重新输入,从而生成新的hash表。优先使用0号hash table,当空间不足时会调用dictExpand来扩展hash table,此时准备1号hash table用于增量的rehash使用。rehash
13、完成后把0号释放,1号保存到0号。rehashidx是下一个需要rehash的项在ht0中的索引,不需要rehash时置为1。也就是说1时,表示不进行rehash。4月算法在线班29/Rehash随着操作的不断执行,哈希表保存的键值对会逐渐地增多或者减少,为了让哈希表的负载因子(load factor)维持在一个合理的范围之内,当哈希表保存的键值对数量太多或者太少时,程序需要对哈希表的大小进行相应的扩展或者收缩。扩展和收缩哈希表的工作可以通过执行rehash(重新散列)操作来完成,Redis对字典的哈希表执行rehash的步骤如下:为字典的ht1哈希表分配空间,这个哈希表的空间大小取决于要执行
14、的操作,以及ht0当前包含的键值对数量(也即是ht0.used属性的值):如果执行的是扩展操作,那么ht1的大小为大于等于ht0.used*2的2的n次方幂的最小值;如果执行的是收缩操作,那么ht1的大小为大于等于ht0.used的2的n次方幂的最小值。将保存在ht0中的所有键值对rehash到ht1上面:rehash指的是重新计算键的哈希值和索引值,然后将键值对放置到ht1哈希表的指定位置上。当ht0包含的所有键值对都迁移到了ht1之后(ht0变为空表),释放ht0,将ht1设置为ht0,并在ht1新创建一个空白哈希表,为下一次rehash做准备。4月算法在线班30/Rehash4月算法在线
15、班31/渐进式rehash扩展或收缩哈希表需要将ht0里面的所有键值对rehash到ht1里面,但是,这个rehash动作并不是一次性、集中式地完成的,而是分多次、渐进式地完成的。这样做的原因在于,如果ht0里只保存着四个键值对,那么服务器可以在瞬间就将这些键值对全部rehash到ht1;但是,如果哈希表里保存的键值对数量不是四个,而是四百万、四千万甚至四亿个键值对,那么要一次性将这些键值对全部rehash到ht1的话,庞大的计算量可能会导致服务器在一段时间内停止服务。因此,为了避免rehash对服务器性能造成影响,服务器不是一次性将ht0里面的所有键值对全部rehash到ht1,而是分多次、
16、渐进式地将ht0里面的键值对慢慢地rehash到ht1。4月算法在线班32/哈希表渐进式rehash的详细步骤1.为ht1分配空间,让字典同时持有ht0和ht1两个哈希表。2.在字典中维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash工作正式开始。3.在rehash进行期间,每次对字典执行添加、删除、查找或者更新操作时,程序除了执行指定的操作以外,还会顺带将ht0哈希表在rehashidx索引上的所有键值对rehash到ht1,当rehash工作完成之后,程序将rehashidx属性的值增一。4.随着字典操作的不断执行,最终在某个时间点上,ht0的所有键值对都会被r
17、ehash至ht1,这时程序将rehashidx属性的值设为1,表示rehash操作已完成。渐进式rehash的好处在于它采取分而治之的方式,将rehash键值对所需的计算工作均摊到对字典的每个添加、删除、查找和更新操作上,从而避免了集中式rehash而带来的庞大计算量。4月算法在线班33/准备开始Rehash4月算法在线班34/索引0上的键值对4月算法在线班35/索引1上的键值对4月算法在线班36/索引2上的键值对4月算法在线班37/索引3上的键值对4月算法在线班38/Rehash执行完毕4月算法在线班39/渐进式Rehash的说明 因为在进行渐进式rehash的过程中,字典会同时使用ht0
18、和ht1两个哈希表,所以在渐进式rehash进行期间,字典的删除(delete)、查找(find)、更新(update)等操作会在两个哈希表上进行:比如说,要在字典里面查找一个键的话,程序会先在ht0里面进行查找,如果没找到的话,就会继续到ht1里面进行查找,诸如此类。另外,在渐进式rehash执行期间,新添加到字典的键值对一律会被保存到ht1里面,而ht0则不再进行任何添加操作:这一措施保证了ht0包含的键值对数量会只减不增,并随着rehash操作的执行而最终变成空表。4月算法在线班40/Redis数据字典总结字典被广泛用于实现Redis的各种功能,其中包括数据库和哈希键。Redis中的字典
19、使用哈希表作为底层实现,每个字典带有两个哈希表,一个用于平时使用,另一个仅在进行rehash时使用。当字典被用作数据库的底层实现,或者哈希键的底层实现时,Redis使用MurmurHash2算法来计算键的哈希值。哈希表使用链地址法来解决键冲突,被分配到同一个索引上的多个键值对会连接成一个单向链表。在对哈希表进行扩展或者收缩操作时,程序需要将现有哈希表包含的所有键值对rehash到新哈希表里面,并且这个rehash过程并不是一次性地完成的,而是渐进式地完成的。4月算法在线班41/使用Hash建立倒排索引 倒排索引源于实际应用中需要根据属性的值来查找记录。这种索引表中的每一项都包括一个属性值和具有
20、该属性值的各记录的地址。由于不是由记录来确定属性值,而是由属性值来确定记录的位置,因而称为倒排索引(inverted index)。带有倒排索引的文件称为倒排索引文件,简称倒排文件(inverted file)。4月算法在线班42/倒排列表 倒排列表记录了某个单词位于哪些文档中。一般在文档集合里会有很多文档包含某个单词,每个文档会记录文档编号(DocID),单词在这个文档中出现的次数(TF)及单词在文档中哪些位置出现过等信息,这样与一个文档相关的信息被称做倒排索引项(Posting),包含这个单词的一系列倒排索引项形成了列表结构,这就是某个单词对应的倒排列表。4月算法在线班43/倒排列表4月算
21、法在线班44/倒排索引4月算法在线班45/更新策略完全重建策略:当新增文档到达一定数量,将新增文档和原先的老文档整合,然后利用静态索引创建方法对所有文档重建索引,新索引建立完成后老索引会被遗弃。此法代价高,但是主流商业搜索引擎一般是采用此方式来维护索引的更新。再合并策略:当新增文档进入系统,解析文档,之后更新内存中维护的临时索引,文档中出现的每个单词,在其倒排表列表末尾追加倒排表列表项;一旦临时索引将指定内存消耗光,即进行一次索引合并,这里需要倒排文件里的倒排列表存放顺序已经按照索引单词字典顺序由低到高排序,这样直接顺序扫描合并即可。其缺点是:因为要生成新的倒排索引文件,所以对老索引中的很多单
22、词,尽管其在倒排列表并未发生任何变化,也需要将其从老索引中取出来并写入新索引中,这样对磁盘消耗是没必要的。原地更新策略:试图改进再合并策略,在原地合并倒排表,这需要提前分配一定的空间给未来插入,如果提前分配的空间不够了需要迁移。实际显示,其索引更新的效率比再合并策略要低。混合策略:出发点是能够结合不同索引更新策略的长处,将不同索引更新策略混合,以形成更高效的方法。4月算法在线班46/三维空间中的R索引4月算法在线班47/RMQ Range Minimum/Maximum Query:给定数组A0,N1找出给定的两个索引间的最小值的位置。通常用在需要多次询问一些区间的最值的问题中。4月算法在线班
23、48/问题分析最简单的算法是O(n)的,但是对于查询次数m很多(假设有100万次),则这个算法的时间复杂度为O(mn),显然时间效率太低。可以对数组进行预处理f(n),然后再查询g(n):记可以用线段树将查询算法优化到O(logn)(在线段树中保存线段的最值),而线段树的预处理时间复杂度为O(n),线段树整体复杂度为。不过,Sparse Table算法(ST算法)才是最好的:它可以在O(nlogn)的预处理以后实现O(1)的查询效率,即整体时间复杂度为。何为最好?4月算法在线班49/最直观的动态规划方案 用dpi,j表示数组Ai,i+1,j的最小值,ij dpi,i=Ai;dpi,j=mind
24、pi,j1,Aj i从0到n1,j从i到n1 时间复杂度O(N2)4月算法在线班50/Straight DP for Code4月算法在线班51/更好的动态规划方案 对2k的长度的子数组进行动态规划。dpi,j表示i,i+2j1区间中的最小值,即dpi,j表示从第i个数起连续2j个数中的最小值。dp(1,0)表示1,1之间的最小值,即A1 dp(1,2)表示1,4之间的最小值 dp(2,4)表示2,17之间的最小值 初值dp(i,0)=Ai dp(i,j)可以由dp(i,j1)和dp(i+2(j1),j1)导出4月算法在线班52/状态转移方程 dp(i,0)=Ai dp(i,j)=min(dp
25、(i,j1),dp(i+2(j1),j1)4月算法在线班53/ST算法预处理部分4月算法在线班54/如何使用数组dpi,j进行查询 假设要查询ALR的最小值,那么先求出一个最大的x,使得x满足2x(RL+1);于是把区间L,R分成两个(部分重叠的)长度为2x的区间:L,L+2x1和R2x+1,R;而dp(L,x)为L,L+2x1的最小值,dp(R2x+1,x)为R2x+1,x的最小值;返回其中更小的那个,就是ALR的最小值,时间复杂度是O(1)。4月算法在线班55/LCA 最近公共祖先(Lowest Common Ancestor,LCA):给定一棵树 T 和两个节点 u 和 v,找出 u 和
26、 v 离根节点最远的公共祖先。LCA(3,4)=2 LCA(3,2)=2 LCA(3,6)=4 LCA(6,10)=14月算法在线班56/问题转化 在有父指针的前提下,该问题即为寻找两个单向链表的第一个公共结点。两个单链表的第一个公共结点问题,下文不妨简称单链公共结点问题。4月算法在线班57/单链公共结点问题如果两个单向链表有公共的结点,也就是说两个链表从某一结点开始,它们的m_pNext都指向同一个结点。但由于是单向链表的结点,每个结点只有一个m_pNext,因此从第一个公共结点开始,之后它们所有结点都是重合的,不可能再出现分叉。所以,两个有公共结点而部分重合的链表,拓扑形状看起来像一个Y,
27、而不可能像X。蛮力法:在第一个链表上顺序遍历每个结点。每遍历一个结点的时候,在第二个链表上顺序遍历每个结点。如果此时两个链表上的结点是一样的,说明此时两个链表重合,于是找到了它们的公共结点。如果第一个链表的长度为m,第二个链表的长度为n,显然,该方法的时间复杂度为O(mn)。4月算法在线班58/单链公共结点问题 分析:如果第一个链表的长度为m,第二个链表的长度为n,不妨认为mn,由于两个链表从第一个公共结点到链表的尾结点是完全重合的。所以前面的(mn)个结点一定没有公共结点。算法:先分别遍历两个链表得到它们的长度m,n。在长的链表上先遍历|mn|次后,再同步遍历两个链表,直到找到相同的结点,或
28、者一直到链表结束。时间复杂度为O(m+n)。进一步的问题:如果两个链表可能有环,如何判断两个链表是否相交?以及找到两个链表的第一个公共点?快慢指针4月算法在线班59/一般LCA 在没有父指针的情况下,可以通过从根到v,u的递归查找,找到最近公共祖先。4月算法在线班60/Code4月算法在线班61/递归代码详解if(root=node1|root=node2)return root;因为函数要返回node1和node2的最近祖先,但事实上,此处返回的,并不一定是“最正”的结论。比如,node1=root,同时,node1不是node2的祖先,那么,“正”结论应该是null,但该代码返回的是nod
29、e14月算法在线班62/递归代码详解node*left=getLCA(rootleft,node1,node2);node*right=getLCA(rootright,node1,node2);第一句,会返回(node1,node2)的“潜在祖先”如果node1,node2分立在root的左、右子树中,不妨认为node1在左、node2在右,返回的是node1;如果node1,node2都在root的左子树中,将返回node1,node2的LCA如果node1,node2都在root的右子树中,将返回null 第二句,返回的情况和第一句对称,不再赘述 、的情况,可以认为返回的是(node1,
30、node2)的LCA,但的情况,不是LCA。但是,暂时不是,没关系。4月算法在线班63/递归代码详解if(left!=null&right!=null)return root;如果发现left和right都不空,说明前面的第一、二句都返回了非空结果,那必然是node1在root的一侧,node2在另外一侧。root就是它们俩的LCA。4月算法在线班64/递归代码详解else if(left!=null)return left;else if(right!=null)return right;4月算法在线班65/递归代码详解else if(left!=null)return left;else
31、if(right!=null)return right;elsereturn null;“最正”的情况:第一句的情况:node1,node2都在root的左子树中,因此,只有left为非空(第二个else if情况对称,不再赘述)return null:就是node1和node2,两个都没有在树root中,显然应该返回null4月算法在线班66/该递归代码的进一步思考 这个函数其实有个“bug”:如果node1在树中,node2不在。按照LCA的定义,应该返回null,但它返回的是node1;但这个“bug”其实无所谓。两个不在一颗树下的结点,非要算他们的共同祖先 可以通过增加标记的方法,去除
32、这个bug;同时带来的好处是,如果node1,node2在root的左孩子中,将不必调用root的右孩子。4月算法在线班67/4月算法在线班68/Tarjan算法 Tarjan算法是由Robert Tarjan在1979年发现的一种高效的离线算法,也就是说,它要首先读入所有的询问(求一次LCA叫做一次询问),然后并不一定按照原来的顺序处理这些询问,该算法的时间复杂度O(N*(N)+Q),其中,(x)不大于4,N表示问题规模,Q表示询问次数。4月算法在线班69/Tarjan概览Tarjan算法基于深度优先搜索,对于新搜索到的一个结点u,首先创建由这个结点u构成的集合setU,再对当前结点u的每一
33、个子树subTree进行搜索,每搜索完一棵子树sub,则可确定子树sub内的LCA询问都已解决。其他的LCA询问的结果必然在这个子树sub之外,这时把子树sub所形成的集合setSub与当前结点的集合setU合并成set,并将当前结点u设为这个集合set的祖先Ua。之后继续搜索下一棵子树subNext,直到当前结点u的所有子树搜索完。这时把当前结点u设为checked,同时可以处理有关当前结点u的LCA询问,如果有一个从当前结点u到结点v的询问,且v已被检查过,则由于进行的是深度优先搜索,当前结点u与v的最近公共祖先LCA一定是未checked,而这个最近公共祖先LCA包含v的子树subV一定
34、已经搜索过了,那么LCA一定是v所在集合的祖先Va。4月算法在线班70/Tarjan4月算法在线班71/Tarjan4月算法在线班72/Tarjan(2,10)(10,7)(9,10)(10,8)4月算法在线班73/处理查询的方法 邻接表 遍历P中的查询(a,b),将a插入到b的列表中,b插入到a的列表中;常见的空间检索方案。4月算法在线班74/RMQ可以转换为LCA 借助笛卡尔树(CartesianTree)笛卡尔树是一个特殊的堆,它根据一个长度为n的数组A建立。它的根是A的最小元素位置i,而左子树和右子树分别为A1i1和Ai+1n的笛卡尔树。中序遍历笛卡尔树就可以得到原数列。4月算法在线班
35、75/使用构造法求解问题 5 8 1 3 6 4 8 5 7 2 根据定义,Amn中的最小值,就是以该最小值为根的一颗子树4月算法在线班76/LCA转换为RMQ 欧拉序列:对有根树T进行DFS,将遍历到的结点按照顺序记下,将得到一个长度为2N1的序列,称之为T的欧拉序列F。通过欧拉序列,能够将一颗树映射成一个数组。4月算法在线班77/考察F的第一次出现4月算法在线班78/LCA转换成RMQ之后 将LCA问题转换为RMQ问题后,利用ST算法来求解,时间复杂度是O(nlogn)+Q*O(1),反而不及直接利用Tarjian算法求解LCA问题时间效率O(N*+Q)高。1RMQ算法的时间复杂度为,而上
36、述转换中RMQ(B,pos(u),pos(v)深度序列B数组中任意相邻两个数的差是1,正好利用“1RMQ”算法来求解。4月算法在线班79/1RMQ 算法的核心思想是分块;以L=log2N/2的块长,把B划分成M=N/L段,记录第k块的最小元素为BlockMin(k),把M块的最小值组成序列Blocks,利用分块思想,可以把询问分成两个部分:4月算法在线班80/1RMQ 求解方法:连续的BlockMin取最小值,即BlockRMQ;ST算法 两端块中某一部分取最小值,即InRMQ;这两个问题都可以O(N)内实现。4月算法在线班81/BlockRMQ BlockRMQ采用ST算法,时间复杂度为O(
37、Mlog2M)因为Mlog2M2N/log2N*log2N=O(N),所以,BlockRMQ的时间复杂度不大于O(N)4月算法在线班82/InRMQ B中任意两个相邻数为1,所以,本质上,不同的块至多有2L种;对于每一种块上的询问最多只有O(L2)种;所以,本质上不同的询问数最多有 O(2L*L2)O(N0.5log2N*log2N)O(N)完成InRMQ只需要预处理本质不同的询问,然后对应作答即可,时间复杂度为O(N)4月算法在线班83/LCA&RMQ&1RMQ 的相互转换一般RMQ问题1 1RMQ问题LCA问题ST算法Tarjan算法1 1RMQ算法欧拉序列:O(N)O(N)O(N)O(N
38、)笛卡尔树:O(N)O(N)4月算法在线班84/由于1RMQ线性时间带来的结论 因为LCA问题可以转换成生成序列的RMQ问题,并且问题规模不变,所以,LCA问题可以在O(N)的时间内解决;因为RMQ问题可以转换成LCA问题,并且问题规模不变,所以,RMQ问题可以在O(N)的时间内解决;4月算法在线班85/参考文献陈海波,王申康,RTree的查询代价模型分析及算法改进,计算机辅助设计与图形学学报J,15,2003(3)程昌秀,矢量数据多尺度空间索引方法的研究,武汉大学学报信息科学版J,34,2009(5)Eric Redmond,etc,王海鹏等 译,七周七数据库M,7th,2013http:/ Hash)http:/ 更多算法面试题在官网 http:/ 直播课程 问答社区 contact us:微博 七月问答 七月算法4月算法在线班87/感谢大家恳请大家批评指正!