基于父子关系的高效去重算法

政采云技术团队.png

向阳1.png

💡 引言:平常在开发过程中,或多或少碰到过数据去重的场景,数据少没问题,数据大的情况下,如果没有很好的算法逻辑或者工具,效率慢且容易出错。 本文主要阐述一种基于父子关系的高效去重算法。

名词解释

血统链用一根直线把所有的父元素和子元素按照依赖关系链接起来的链条。
关系函数一个元素可以通过这个一个函数变换得到另外一个元素,那么这两个元素之间的关系可以用关系函数来表示。
父子关系函数一个元素可以通过开一个函数变换得到他的子元素。那么这两个元素之间的关系可以用父子关系函数来表示。

能解决什么问题

在对一个集合里面多个元素进行父子关系验证的过程中,把已经验证通过的合法元素以及合法的血统链,存放在 HashSet 中,在后面如果出现重复的父子元素关系校验时,可以直接查取 hashset,而不需要重新计算,极大的减少重复的计算步骤,极大的提高计算速度。

实现方案

验证父子关系

如果 A 和 B,C 之间存在某种父子关系 f,可以通过函数 f(A)=B,f(B)=C 来表示,因此 A 是 B 的父级,B 是 A 的子级,B 是 C 的父级,C 是 B 的子级。而且 A 没有父级别,A 就是根,而且在这个 ABC 集合里面就只有 A 这一个根,C 没有子级了,那么 C 就是子结点。这个时候 A,B,C 就遵循严格的父子关系了,并且家族血统链长度为 3,也就是 ABC 是一个血统链。

问题:需要验证{A,B,C}是否严格遵循父子关系? 如何验证呢,只需要验证每个元素是否都合法就行了,就拿 C 这个元素为例子,要验证他是否合法,必须要验证 C 在这个集合{A,B,C}里面是否严格遵循父子关系,我们就必须要找到 C 所在血统链的根元素,要验证这个根元素确实是存在的!并且要验证从 C 的父级元素到根元素之间的元素都是合法的(在本例中为 A 和 B,所以要满足“A 是根,并且 f(A)=B”这个条件)!接着就要验证 f(B)=C 也必须是成立的!如果以上验证都通过了,那么 C 就是合法的元素,C 在这个集合里面是严格遵循父子关系的。

由此可见,按照上面的例子,如何才能判断{A,B,C}这个集合是完整合法的呢?按照上面的验证步骤,只要所有的元素都能经过上面的步骤验证通过,而且不会出现反向依赖的情况(如 f(C)=B),那么这个集合都是合法的。所以就必须要经过如下几个验证步骤,但是验证元素的顺序是不确定,不是有序的,所以就有了如下步骤:

验证 C:
C 的父级为 B,f(B)=C
B 的父级为 A,f(A)=B
A 没有父级是根

验证 A:
C 的父级为 B,f(B)=C
A 没有父级是根,合法

验证 B:
C 的父级为 B,f(B)=C
B 的父级为 A,f(A)=B
A 没有父级是根

只要上面的步骤都验证通过,这个集合就合法了

只有这样才能保证这个血统链是合理的。如果血统链越长,那么验证计算的步骤就越多,而且每一个元素的验证过程中都会出现重复的步骤,如上表绿色的步骤,如果血统链很长而且有分支,就会出现如下情况:

A 是根,F 和 G 是子结点;D 和 E 具有共同的父级 C。这个时候,血统链就变成是两条了,分别为 ABCDF 和 ABCEG,而且血统链长度都为 5,那么这个时候要验证这个集合{A,B,C,D,E,F,G}是否满足严格的父子关系,就是要验证这两条血统是否合法。现有的最常见的解决方案就是按照上面的例子步骤逐个元素去验证,而且这些元素验证的顺序是不确定的,也就是随机的顺序的,那么显而易见,这种情况下,验证的步骤就变成是很多,要保证每一个元素都合法,就包含了所有的验证步骤。而且重复的步骤就数不胜数。

步骤:
一.验证血统 ABCDF
验证 B:
B 的父级为 A,f(A)=B

A 没有父级是根

验证 A:
A 没有父级,是根,合法

验证 F:
F 的父级为 D,f(D)=F

D 的父级为 C,f(C)=D
C 的父级为 B,f(B)=C
B 的父级为 A,f(A)=B
A 没有父级是根

验证 D:
D 的父级为 C,f(C)=D

C 的父级为 B,f(B)=C
B 的父级为 A,f(A)=B
A 没有父级是根

验证 C:
C 的父级为 B,f(B)=C

B 的父级为 A,f(A)=B
A 没有父级是根

二.验证血统 ABCEG
验证 E:
E 的父级为 C,f(C)=E

C 的父级为 B,f(B)=C
B 的父级为 A,f(A)=B
A 没有父级是根

验证 G:
G 的父级为 E,f(E)=G

E 的父级为 C,f(C)=E
C 的父级为 B,f(B)=C
B 的父级为 A,f(A)=B
A 没有父级是根

由上可见,这两个血统链的验证步骤一共有 24 个步骤,绿色部分步骤是重复两次或以上的。现在只是一个简单的 7 个元素的集合,每增加一个元素的验证,计算步骤就成指数式的增长,如果这个元素集合个数不是 7 个,而是上千万个以上呢,如果关系函数 f(x)比较复杂,而且元素集合里面的血统链重复而繁多,血统链长度又很长,那么这个验证父子关系的过程就会存在数不胜数的重复步骤。就好像以上标绿色的步骤。那么该如何优化这些重复的计算步骤呢?就是这次要解决的问题。

详细方案

其实以上绿色部分的步骤是可以只计算一次,而不需要重复计算的,把计算过的结果都存在 hashset 里,还是以上面的数据为例子,在验证这 7 个元素的父子关系的过程中,应该对验证过程中的中间数据使用 hashset 缓存起来,hashset 的特性是不可重复,自动去重,并且查找性能是所有数据结构里面最快的一种。如果在验证过程中,把各个步骤计算出来的中间数据放进去 hashset 里面,在验证的后续步骤里面就可以从 hashset 里面查取经过验证的中间数据,就可以大大减少重复验证的步骤。那么计算的步骤应该是怎样的呢?

每一个元素验证的流程图如下:

例如对 C 元素的验证,验证流程改造如下,绿色部分就是记忆算法,把计算的中间数据放到 hashset 中。 图 1:

因此验证集合血统关系的步骤就变得简洁了,如下所示,不管验证元素的顺序是怎样,都能保持验证过的节点只会计算一次,直接去掉了所有重复计算步骤。

一.验证血统 ABCDF 的步骤
验证 A:
A 为根节点,合法,
把 A 放到 hashSet 里面

验证 B:
B 的父级元素为 A ,f(A)=B.
查找到 A 是存在 hashset 里面,B 是合法的。
并把 B 放到 hashset 里面。

验证 C:
C 的父级元素为 B,f(B)=C
查找到 B 是存在 hashset 里面,C 是合法的。
并把 C 放到 hashset 里面。

验证 D:
D 的父级元素为 C,f(C)=D
查找到 C 是存在 hashset 里面,D 是合法的。
并把 D 放到 hashset 里面。

验证 F:
F 的父级元素为 D,f(D)=F
查找到 D 是存在 hashset 里面,F 是合法的。
并把 F 放到 hashset 里面。

二.验证血统 ABCEG 的步骤
验证 E:
E 的父级元素为 C,f(C)=E
查找到 C 是存在 hashset 里面,E 是合法的。
并把 E 放到 hashset 里面。

验证 G:
G 的父级元素为 E,f(E)=G
查找到 E 是存在 hashset 里面,G 是合法的。
并把 G 放到 hashset 里面。

只要上面的步骤都验证通过,集合{A,B,C,D,E,F,G}就是严格遵循父子关系了。常规的算法就需要 24 个步骤,但是有了记忆算法,验证的步骤一下子降到 13 个。减少了近乎一半的计算次数,而且随着集合的元素个数的增加,记忆算法的效果就越明显。例如上千万个元素,上一亿各元素。

再回到图 1,每次验证一个元素的时候,就通过 hashset 把曾经算过的符合父子关系的元素保存下来,可以减少后续的计算的重复步骤,只需要一次查询就可判断其合法性。在任何血统链之间没有共同部分的情况下,而且平均血统链长度为 m 的情况下:

原始的算法步骤 r 跟元素集合个数 n 的关系为:r = (1+m) * (m) *(n/m) / 2

而记忆算法的步骤 r 跟元素集合个数 n 的关系为: r = (m + 2 * m-1)* (n/m)

如果一个集合里面的包含有 100 个元素,在任何血统链之间没有共同部分的情况下,而且平均血统链长度为 5。
使用原始的算法(包含重复步骤)来计算,一共需要验证的步骤就多达 6520 /2=300
使用记忆算法,一共需要验证的步骤最多为(5+2*5-1)*20 = 280
记忆算法减少的步骤为 300-280=20
记忆算法让计算耗时减少了 20/300=6%

血统链长度为 20
使用原始的算法(包含重复步骤)来计算,一共需要验证的步骤就多达 21205/2=1050
使用记忆算法,一共需要验证的步骤最多为(20+2*20-1)*5 = 295
记忆算法减少的步骤为 1050-295=755
记忆算法让计算耗时减少了 755/1050 = 71%

如果一个集合里面包含有 1000 个元素, 在任何血统链之间没有共同部分的情况下,而且平均血统链长度为 5
使用原始的算法(包含重复步骤)来计算,一共需要验证的步骤就多达 65200/2=3000
使用记忆算法,一共需要验证的步骤最多为(5+2*5-1)*200 = 2800
记忆算法减少的步骤为 3000-2800=200
记忆算法提速了 200/3000=6%

血统链长度为 20
使用原始的算法(包含重复步骤)来计算,一共需要验证的步骤就多达 212050/2=10500
使用记忆算法,一共需要验证的步骤最多为(20+2*20-1)*50 = 2950
记忆算法减少的步骤为 10500-2950=7550
记忆算法让计算耗时减少了 7550/10500 = 71%

血统链长度为 200
使用原始的算法(包含重复步骤)来计算,一共需要验证的步骤就多达 2012005/2=100500
使用记忆算法,一共需要验证的步骤最多为(200+2*200-1)*5 = 2995
记忆算法减少的步骤为 100500-2995=97505
记忆算法让计算耗时减少了 97505/100500 = 97%
可见集合中的元素个数越多,血统链平均长度越长,记忆算法提升计算效率就越明显。

替代方案

第一种替代方案,不使用 hashset,使用 hashmap,用 hashmap 的 key 存放元素,用 value 存放后续进行其他运算所需要的复杂数据,大大增强这个记忆算法的可扩展性,也就是存放的数据可以更多样化了,不只是可存放这个元素本身。

第二种替代方案,使用分布式缓存,或者是本地文件,存储这些经过验证后的元素,但是,需要自己重新实现一套去重算法保证落地到本地文件和分布式缓存中的元素是唯一的。还要自己实现一套高效的查询算法。快速查询这个元素是否已经存在于本地文件中或者是分布式缓存中。

第三种替代方案,就是不使用记忆算法,把集合中每一个元素都变成是一个对象来处理。每次验证完毕一个元素后,就在这元素上面打标,表示这元素是否为合法的元素。

推荐阅读

分布式一致性

Lucene 查询原理解析

Unsafe 魔法类应用体现 (LockSupport)

数据中台建设实践(二)- 数据治理之数据质量

招贤纳士

政采云技术团队(Zero),一个富有激情、创造力和执行力的团队,Base 在风景如画的杭州。团队现有300多名研发小伙伴,既有来自阿里、华为、网易的“老”兵,也有来自浙大、中科大、杭电等校的新人。团队在日常业务开发之外,还分别在云原生、区块链、人工智能、低代码平台、中间件、大数据、物料体系、工程平台、性能体验、可视化等领域进行技术探索和实践,推动并落地了一系列的内部技术产品,持续探索技术的新边界。此外,团队还纷纷投身社区建设,目前已经是 google flutter、scikit-learn、Apache Dubbo、Apache Rocketmq、Apache Pulsar、CNCF Dapr、Apache DolphinScheduler、alibaba Seata 等众多优秀开源社区的贡献者。如果你想改变一直被事折腾,希望开始折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊……如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的技术团队的成长过程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 zcy-tc@cai-inc.com

微信公众号

文章同步发布,政采云技术团队公众号,欢迎关注

政采云技术团队.png