海量数据处理--外排序算法

外排序算法是指能够处理极大量数据的排序算法。通常来说,外排序处理的数据不能一次装入内存,只能放在读写较慢的外存储器(通常是硬盘)上。通常采用“排序-归并”的策略,将原本的大文件,拆分为若干个小文件,小文件可以读入内存中进行排序,然后使用归并操作。

因此,外排序通常分为两个基本步骤:

将大文件切分为若干个个小文件,并分别使用内存排好序
使用K路归并算法将若干个排好序的小文件合并到一个大文件中
接下来我们更具体的讨论两个步骤:

第一步:文件拆分:

根据内存的大小,尽可能多的分批次的将数据 Load 到内存中,并使用系统自带的内存排序函数(或者自己写个快速排序算法),将其排好序,并输出到一个个小文件中。比如一个文件有1T,内存有1G,那么我们就这个大文件中的内容按照 1G 的大小,分批次的导入内存,排序之后输出得到 1024 个 1G 的小文件

第二步:K路归并排序

在完成了大文件的拆分,并对拆分出来的小文件分别进行了排序之后,就使用K路归并算法合并排序好的文件。K路归并算法使用的是数据结构堆(Heap)来完成的,使用 Java 或者 C++ 的同学可以直接用语言自带的 PriorityQueue(C++中叫priority_queue)来代替。我们将 K 个文件中的第一个元素加入到堆里,假设数据是从小到大排序的话,那么这个堆是一个最小堆(Min Heap)。每次从堆中选出最小的元素,输出到目标结果文件中,然后如果这个元素来自第 x 个文件,则从第 x 个文件中继续读入一个新的数进来放到堆里,并重复上述操作,直到所有元素都被输出到目标结果文件中。

看到这里,你可能会发现问题,就是在归并的过程中,一个个从文件中读入数据,一个个输出到目标文件中操作很慢,如何优化?

优化

确实,如果我们每个文件只读入1个元素并放入堆里的话,总共只用到了 1024 个元素,这很小,没有充分的利用好内存。另外,单个读入和单个输出的方式也不是磁盘的高效使用方式。因此我们可以为输入和输出都分别加入一个缓冲(Buffer)。假如一个元素有10个字节大小的话,1024 个元素一共 10K,1G的内存可以支持约 100K 组这样的数据,那么我们就为每个文件设置一个 100K 大小的 Buffer,每次需要从某个文件中读数据,都将这个 Buffer 装满。当然 Buffer 中的数据都用完的时候,再批量的从文件中读入。输出同理,设置一个 Buffer 来避免单个输出带来的效率缓慢。

在了解了外排序的过程之后,你是不是已经迫不及待地想要动手实现一下其中的一些算法了呢。实现合并 K 个有序数组的算法吧。

//这个类只要是标记这个数在哪个数组的那个位置
class Element {
    public int row, col, val;
    Element(int row, int col, int val) {
        this.row = row;
        this.col = col;
        this.val = val;
    }
}

public class Solution {
                //定义堆的比较规则
    private Comparator<Element> ElementComparator = new Comparator<Element>() {
        public int compare(Element left, Element right) {
            return left.val - right.val;
        }
    };
    
                     //主方法 二维矩阵的每一行做一个数组
    public int[] mergekSortedArrays(int[][] arrays) {
        if (arrays == null) {
            return new int[0];
        }
        //记录共多少个元素
        int total_size = 0;
        Queue<Element> Q = new PriorityQueue<Element>(
            arrays.length, ElementComparator);
            //初始化 加入每个数组的第一个元素以及他的位置
        for (int i = 0; i < arrays.length; i++) {
            if (arrays[i].length > 0) {
                Element elem = new Element(i, 0, arrays[i][0]);
                Q.add(elem);
                total_size += arrays[i].length;
            }
        }
        
        int[] result = new int[total_size];
        int index = 0;
        while (!Q.isEmpty()) {
            Element elem = Q.poll();
            result[index++] = elem.val;
            if (elem.col + 1 < arrays[elem.row].length) {
                elem.col += 1;
                elem.val = arrays[elem.row][elem.col];
                Q.add(elem);
            }
        }
        
        return result;
    }
}

在一起完成了合并k个有序数组的任务之后,相信对于合并k个有序链表的问题,你一定能独自解决了。代码我就不贴了 ,自行百度一下。

现在我们来尝试解决一个前面提到过的问题:

求两个超大文件中 URLs 的交集。

问题的具体描述如下:

给定A、B两个文件,各存放50亿个URLs,每个 URL 各占 64 字节,内存限制是 4G,让你找出A、B文件共同的 URLs?

和之前讲过的题目一样,遇到问题首先还是要看有没有条件需要澄清,这里主要是一个问题:这两个文件各自是否已经没有重复?
对于这个问题,通常面试官会先让你假设没有重复,然后再来看有重复的情况怎么处理。那我们就先来看没有重复的情况。

方法1:文件拆分 Sharding(也可以叫 Partitioning)

首先能想到的最简单的方法,肯定就是要把文件从拆分。50亿,每个 URLs 64 字节,也就是 320G 大小的文件。很显然我们不能直接全部 Load 到内存中去处理。这种内存不够的问题,通常我们的解决方法都可以是使用 hash function 来将大文件拆分为若干个小文件。比如按照hashfunc(url) % 200进行拆分的话,可以拆分成为,200 个小文件 —— 也就是如果 hashfunc(url) % 200 = 1 就把这个 url 放到 1 号文件里。每个小文件理想状况下,大小约是 1.6 G,完全可以 Load 到内存里。

这种方法的好处在于,因为我们的目标是要去重,那么那些A和B中重复的 URLs,会被hashfunc(url) % 200映射到同一个文件中。这样在这个小文件中,来自 A 和 B 的 URls 在理想状况下一共 3.2G,可以全部导入内存进入重复判断筛选出有重复的 URLs。

前面说的是理想情况,那么特殊情况下,如果 hashfunc(url) % 200 的结果比较集中,就有可能会造成不同的 URLs 在同一个文件中扎堆的情况,应该如何处理呢?
这种情况下,有一些文件的大小可能会超过 4G。对于这种情况,处理的办法是进行二次拆分,把这些仍然比较大的小文件,用一个新的 hashfunc 进行拆分:hashfunc’(url) % X。这里再拆成多少个文件,可以根据文件的实际大小来定。如果二次拆分之后还是存在很大的文件,就进行三次拆分。直到每个小文件都小于 4G。

方法2:BloomFilter:

既然是内存空间太少的问题,我们前面讲过了一个主要用于内存过少的情况的数据结构:BloomFilter。我们可以使用一个 4G 的 Bloom Filter,它大概包含 320 亿 个 bit。把 A 文件的 50亿 个 URLs 丢入 BF 中,然后查询 B 文件的 每个 URL 是否在 BF 里。这种方法的缺点在于,320 亿个 bit 的 BF 里存 50 亿个 URLs 实在是太满了(要考虑到BF可能会用4个哈希函数),错误率会很高。因此仍然还需需要方法1中的文件拆分来分批处理。

方法3:外排序算法:

将A,B文件分别拆分为80个小文件,每个小文件4G。每个文件在拆分的时候,每4G的数据在内存中做快速排序并将有序的URLs输出到小文件中。
用多路归并算法,将这160个小文件进行归并,在归并的过程中,即可知道哪些是重复的 URLs。只需将重复的 URLs 记录下来即可。

那么,如果A,B各自有重复的URLs怎么处理呢?

当 A, B 各自有重复的 URLs 的时候,比如最坏情况下,A里的50亿个URLs 全部一样。B里也是。这样采用方法1这种比较容易想到的 Sharding 方法,是不奏效的,因为所有 URLs 的 hashcode 都一样,就算换不同的 hashfunc 也一样。这种情况下,需要先对两个文件进行单独的去重,方法是每 4G 的数据,放到内存中用简单的哈希表进行去重。这样,在最坏情况下,总共 320G 的数据里,一个 URLs 最多重复 80次,则不会出现太严重的扎堆情况了。算法上唯一需要稍微改动的地方是,由于 A 存在多个重复的 URLs,所以当和 B 的 URLs 被sharding 到同一个文件里的时候,需要标记一下这个 URLs 来自哪个文件,这样才能知道是否在A和B中同时出现过。
另外,使用外排序的方法,是无需对两个文件进行单独去重的步骤的。

发布了11 篇原创文章 · 获赞 3 · 访问量 160

猜你喜欢

转载自blog.csdn.net/qq_38574975/article/details/103605136