时间复杂度是衡量算法执行效率的一种标准。但是,时间复杂度并不能跟性能划等号。在真实的软件开发中,即便在不降低时间复杂度的情况下,也可以通过一些优化手段,提升代码的执行效率。对于实际的软件开发来说,即便是像 10%、20% 这样微小的性能提升,也是非常可观的。
并行计算是一个工程上的实现思路,尽管跟算法关系不大,但是,在实际的软件开发中,它确实可以非常巧妙地提高程序的运行效率,是一种非常好用的性能优化手段。
特别是,当要处理的数据规模达到一定程度之后,无法通过继续优化算法,来提高执行效率 的时候就需要在实现的思路上做文章,利用更多的硬件资源,来加快执行的效率。所以,在很多超大规模数据处理中,并行处理的思想,应用非常广泛,比如 MapReduce 实际上就是一种并行计算框架。
第一种是对归并排序并行化处理。可以将8GB 的待排序的数据划分成 16 个小的数据集合,每个集合包含 500MB 的数据。用 16 个线程,并行地对这 16 个 500MB 的数据集合进行排序。分别排序完成之后,再将这 16 个有序集合合并。
第二种是对快速排序并行化处理。通过扫描一遍数据,找到数据所处的范围区间。把这个区间从小到大划分成 16 个小区间。将 8GB 的数据划分到对应的区间中。针对这 16 个小区间的数据,启动 16 个线程,并行地进行排序。等到 16 个线程都执行结束之后,得到的数据就是有序数据了。
这两种处理思路都是分治的思想,对数据进行分片,然后并行处理。区别在于,第一种处理思路是先随意地对数据分片,排序之后再合并。第二种处理思路是先对数据按照大小划分区间,然后再排序,排完序就不需要再处理了。
我们知道,散列表是一种非常适合快速查找的数据结构。
可以将数据随机分割成 k 份(比如 16 份),每份中的数据只有原来的 1/k,然后针对这 k 个小数据集合分别构建散列表。这样,当某个小散列表的装载因子过大的时候,可以单独对这个散列表进行扩容,而其他散列表不需要进行扩容。
还是刚才那个例子,假设现在有 2GB 的数据,我们放到 16 个散列表中,每个散列表中的数据大约是 150MB。当某个散列表需要扩容的时候,我们只需要额外增加 150*0.5=75MB 的内存(假设还是扩容到原来的 1.5 倍)。不管从扩容的执行效率还是内存的利用率上,这种多个小散列表的处理方法,都要比大散列表高效。
要查找某个数据的时候,只需要通过 16 个线程,并行地在这 16 个散列表中查找数据。
往散列表中添加数据的时候,可以选择将这个新数据放入装载因子最小的那个散列表中,这样有助于减少散列冲突。
在文本中查找某个关键词可以通过字符串匹配算法KMP、BM、RK、BF 等来实现。
对于很长的超级大的文本,如何并行处理加快匹配速度呢?
可以把大的文本,分割成 k 个小文本。然后启动 k 个线程,并行地在这 k个小文本中查找关键词,再在每个小文本的结尾和开始各取关键词的长度重新查找一遍即可。
假设关键词的长度是 m。在每个小文本的结尾和开始各取 m 个字符串。前一个小文本的末尾 m 个字符和后一个小文本的开头 m 个字符,组成一个长度是 2m 的字符串。再拿关键词,在这个长度为 2m 的字符串中再重新查找一遍。
对于广度优先搜索算法,也可以将其改造成并行算法。
广度优先搜索是一种逐层搜索的搜索策略。基于当前这一层顶点,可以启动多个线程,并行地搜索下一层的顶点。只需增加至使用两个队列来完成扩展顶点的工作。
假设这两个队列分别是队列 A 和队列 B。多线程并行处理队列 A 中的顶点,并将扩展得到的顶点存储在队列 B 中。等队列 A 中的顶点都扩展完成之后,队列 A 被清空,再并行地扩展队列 B 中的顶点,并将扩展出来的顶点存储在队列 A。这样两个队列循环使用,就可以实现并行广度优先搜索算法。
假设n 个任务之间又有一定的依赖关系,如何根据依赖关系找出可以并行执行的任务?
答:将任务之间的依赖关系存储为有向图,然后用拓扑排序中的Kahn算法的思想来执行任务。
首先,初始化统计每个顶点(任务)的入度。
然后,将所有入度为0的顶点(任务)加入到队列A中,启动线程池开始执行队列A中的任务, 每处理完一个任务则将这个任务对应的顶点从队列中移除,并把这个顶点可达的顶点的入度都减 1,发现某个顶点入度为0时,将该顶点添加到队列B中。
待线程池执行结束,队列A为空时,再启动线程池开始执行队列B中的任务,两个队列循环使用,重复上面的步骤,直到另一个队列也为空时,结束循环。
此时所有任务跑完。