给定n个对象,对这n个对象最基本的操作有两个:连接其中的两个对象,检测其中两个对象是否是连通的。
定义如下:
以下所讨论的问题都是围绕着这两个基本操作展开的。
首先我们假设给定的n个对象初始化工作已经完成,即给定了初始状态——初始化对象之间的连接关系。
Quick Find
首先,定义这样的数据结构id[],彼此连通的对象拥有相同的id值,最大连通分支中的每个对象id值相同,不同最大连通分支的对象id值必定不同。
id[]初始化为:id[i] = i(i:1->N),id[i]表示最大连通分支给定的一个值。
给了个形象的例子方面理解:
基于这样的数据结构的定义,以上的查找和合并的两个基本操作可以描述如下:
如果两个对象具有相同的id值,则两个对象连通,否则没有;对两个对象合并必须把其中一个对象所属的最大连通分支中所有对象的id值置为另一个对象所属的最大连通分支的统一id值,使得构成的新的最大连通分支里所有对象id值相同。
再具体一点吧,给出算法的代码实现。
显然,初始化数据结构代价是O(N),查找的代价是O(1),合并的代价是O(N)。合并的代价是巨大的,随着合并操作的进行,合并产生的代价是不可扩展了,进行N次合并的操作,代价就达到了O(N^2)。对于实际中的大N,合并的做法是不可取的。
Quick Union
同理,定义数据结构id[],初始化id[i]=i(i:1->N);id[i]的值表示对象i的父节点;对象i的根节点则是i的父节点的父节点的父节点的。。
因此,查找操作即查看两个对象的根节点是否一致;合并操作是把一个对象的根节点作为另一个对象的根节点的父节点。
渐渐感觉到有并查集的思想以及路径压缩。
感受下代码。
对比这两种算法的效率:
Quick-Union Improvements
改进一:Weighting
与Quick-Union随意地合并两个子树不同的是,改进的合并方法是有方向性的,将一个较小的子树的根合并到较大的子树的根上(较大的这个子树的根作为合并之后的树的根节点),这里较小的子树和较大的子树中的“较小”、“较大”的定义是广义的,可是是子树的规格(节点个数)、子树的高度(深度)或是某种的排序。这样的做法类似于维护一棵某种度量方向上的平衡树,保证树的形状不会像Quick-Union那样随机和不确定,使得复杂度变得可控(不可控是因为不确定导致的O(N))。
更形象地感受下当节点数很大的时候Quick-Union和Weighted Quick-Union形成树的形态的区别:
再抽象回代码的实现过程(其实“抽象”真的谈不上,看代码还更形象些):
复杂度的分析对比:
改进二:path compression
路径压缩,在计算某个节点的根节点时,直接将该节点以及由该节点指向根节点的路径中的节点的id值置为根节点的id值,即这条路径上的所有节点都以根节点作为父节点。
代码实现的过程时,路径压缩的细化量并不是将根节点作为父节点,仅仅将父节点的父节点作为父节点效果就很好了,代码实现如下:
最后,所有的算法的总结概括了。
一点点的积累,千万不要轻言说放弃,哪怕是错的,至少要坚持一段时间亲自证明是错的,或许走着走着就看到了希望。
人还是要有梦想的,万一实现了呢?