从归并排序到大数据中的归并

从归并排序到大数据中的归并

归并排序在实际场景中应用广泛,很大程度上是因为它不仅仅只是“排序”。归并排序的核心价值在于其分而治之的思想,这种思想完美贴合分布式的设计理念。正所谓“三个臭皮匠,顶个诸葛亮”,将一个复杂的任务进行拆分,每台机器都可以较小的时间和空间资源去处理单个子任务。而且在子任务可以并行处理的情况下,复杂任务最终完成的时间还要更短。

归并排序

既然由归并排序延伸而来,还是要先介绍一下归并排序的原理和性能分析。

void merge(vector<int>& a, vector<int>& aux, int lo, int mid, int hi) {
  int i = lo;
  int j = mid + 1;
  for (int k = lo; k <= hi; ++k) {
    aux[k] = a[k];  // 注意不能用assign,这里赋值是部分操作,aux大小不变
  }
  for (int k = lo; k <= hi; ++k) {
    if (i > mid) {
      a[k] = aux[j++];  // 左边已用完
    } else if (j > hi) {
      a[k] = aux[i++];  // 右边已用完
    } else if (aux[i] <= aux[j]) {
      a[k] = aux[i++];  // 左边 <= 右边(稳定排序)
    } else {
      a[k] = aux[j++];  // 右边 > 左边
    }
  }
}

/**
 * @overload
 */
void sort(vector<int>& a, vector<int>& aux, int lo, int hi) {
  if (lo >= hi) {  // 这里只要知道lo=hi=0就知道应该带等号
    return;
  }
  int mid = lo + (hi - lo) / 2;
  sort(a, aux, lo, mid);  // 注意中间元素包含在左区间
  sort(a, aux, mid + 1, hi);
   if (a[mid] <= a[mid + 1]) { // 子数组已经有序
     return; 
   }
  merge(a, aux, lo, mid, hi);
}

void sort(vector<int>& a) {
  int len = a.size();
  auto aux = a;  // auxiliary 辅助数组
  sort(a, aux, 0, len - 1);
} 
  • 空间复杂度:由于需要使用一个辅助数组aux,所以空间复杂度为 O ( N ) O(N) O(N)

  • 时间复杂度:粗略理解的话,自顶向下划分时对半划分,所以划分得到子数组二叉树的高度就是 l o g 2 N log_2N log2N,子数组的比较次数又是线性的 O ( N ) O(N) O(N),所以总排序时间就是 O ( N l o g 2 N ) O(Nlog_2N) O(Nlog2N)。详细分析可以参考《算法4》2.2.4 排序算法的复杂度。

虽然归并排序的理论时间复杂度看着和快排差不多,但是比较次数比快排多,移动次数也更多。并且在大多数实际数据中,快排可以先进行随机打散来避免最坏的 O ( N 2 ) O(N^2) O(N2)。这些都来自于数学概率,你完全可以相信。

内部排序和外部排序

我们熟知的排序算法一般都属于内部排序,即快速排序、堆排序、冒泡排序、插入排序等,归并排序也属于内部排序。那么为什么还要有外部排序呢?其实内部和外部的区别在于存储的介质,内部排序只使用内存,外部排序还需使用磁盘。显然,外部排序使用磁盘的目的就是为了存储更多的数据,所以外部排序基本等同于大数据的排序。那么,外部排序的工作原理是什么呢?这就要与归并联系起来了。

大数据中的归并

既然内存放不下就放到磁盘,如果单个磁盘也放不下就拆分放到多个磁盘中,在面对大数据时我们不可避免要与磁盘打交道。大数据的排序过程(外部排序)通常如下:

  1. 拆分。首先我们将大数据集分割成多个小文件,每个文件的大小都适合加载到内存中。

  2. 内部排序。然后我们使用某种内部排序算法(如快速排序、堆排序等)对每个小文件进行排序,排序得到的有序子数组再写回磁盘。

  3. 归并。N个有序子数组我们就设置N个游标,一开始统一指向位置为0的元素,然后比较得到当前的最小元素,写入到最终文件中。然后我们将产生这个最小元素的有序数组的游标向后移动,读取下一个元素,再次与其他游标指向的元素进行比较。以此类推,直到所有游标都到末尾,最终文件里就是全局排好序的数据。

在大数据中的归并同样也是分而治之的思想,先拆分成子文件,方便读入内存进行内部排序,最后再进行归并。现在我们应该可以初步感受到归并排序的这种分治思想,在大数据的世界里,很多单机处理不了的问题都可以按照这个思路来解决。

归并的优化

我们刚才提到的归并是最简单直接的思路,假如拆分的子文件为N个,每次我们都要 O ( N ) O(N) O(N)的时间得到当前最小的元素,子文件很多的时候这个效率并不高。相信聪明的小伙伴看到最小的元素就已经有了优化的思路,这不就是优先队列/堆的用武之地嘛。我们在内存建立一个大小为N的最小堆,每次我们从堆顶取出最小元素,时间复杂度为 O ( l o g 2 N ) O(log_2N) O(log2N),每次我们往堆里放入元素,时间复杂度也为 O ( l o g 2 N ) O(log_2N) O(log2N)

在大多数情况下,标准的归并已经足够高效,并且实现起来相对简单。而在需要处理大量子文件的时候,使用堆进行多路归并可能是一个更好的选择。


如今互联网上各类文章满天飞,但是大部分要不是寥寥数语,让人过目即忘;要不是过多细枝末节又没有实操,让人不知所云。我将从个人学习和工作经历出发,给大家带来深入浅出的技术解析。我的文章力求简短精悍,尽量结合实战,以便大家在碎片时间即可充分吸收,后续还能学以致用。

欢迎大家关注我的微信公众号,所有文章第一时间更新~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值