外部排序

Definition


我们熟悉的排序算法,例如:冒泡排序快速排序等,都是在内存中进行的排序算法。但是当我们需要对于一个非常大的文件进行排序时,这个文件可能包含有数十万条记录,我们无法将这样庞大的文件复制到内存中进行排序。这时,我们需要将排序的记录存储在外存上(例如磁盘和磁带),然后一部分一部分读取进入内存进行排序之后,再存放回外存,这样的排序涉及到了多次的内存和外存之间的数据交换,这样的排序方法就是外部排序。

General Method


通常我们采用**归并排序**来进行外部排序,有两个独立的阶段:

  • 内部排序阶段:将 n n n 个记录分为若干个 长度为 h h h 的子文件,依次读入内存中进行内部排序,使得这些子文件内部都是有序的,然后存回内存,这些子文件称为归并段或顺串
  • 外部排序阶段:对这些归并段逐趟归并,最终合并成完整的包含 n n n 个记录的有序文件
外部排序阶段 example

例如这里有一个包含 2000 2000 2000 个记录的文件,每个磁盘块包含 250 250 250 个记录,总共 8 8 8 个磁盘块。对这 8 8 8 个磁盘块进行二路归并:每次从磁盘读入两个磁盘块,到内存两个缓冲区中,对这两个磁盘块进行 M e r g e \bold {Merge} Merge 操作:

  • 读入两个归并段 R 1 , R 2 R_{1},R_{2} R1,R2 到两个输入缓冲区中
  • 进行二路归并,归并出来的对象顺序输出到输出缓冲区中
  • 若输出缓冲区对象存满(这是经常发生的,因为我们通常会把内存等分为两个输入缓冲区和一个输出缓冲区,当然第一趟归并段较小,通常不会发生),将其内对象顺序写到输出归并段 R 1 ′ R_{1}' R1,清空这个输出缓冲区
  • 若输入缓冲区对象取空(同样通常不会出现在第一趟中),则从对应的归并段中读取下一块,继续参加归并
  • 重复上述过程,直到最后一趟中对两个 1000 1000 1000 记录的归并段归并合成完整的有序文件

整个过程两个阶段的时间复杂度之和为:
t E S = r t I S + d t I O + S ( n − 1 ) t m g (0) t_{ES}=rt_{IS}+dt_{IO}+S(n-1)t_{mg}\tag{0} tES=rtIS+dtIO+S(n1)tmg(0)
其中:

  • r t I S rt_{IS} rtIS:对所有初始归并段进行内部排序的时间, r r r 时初始归并段的个数, t I S t_{IS} tIS 即 Internal Sort Time 内部排序时间
  • d t I O dt_{IO} dtIO d d d 是访问外存的次数, t I O t_{IO} tIO 是每个块的存取时间,即总的 IO 时间
  • S ( n − 1 ) t m g S(n-1)t_{mg} S(n1)tmg:内部归并总时间, S S S 是归并趟数, n n n 是每趟参加 2 路归并的记录个数, t m g t_{mg} tmg 是每做一次内部排序时,取得关键字最小的记录的时间,即进行比较获得输出到输出缓冲区的下一个记录所花费的时间

而显然,磁盘存取时间时显著大于内部排序和内部归并的时间,因此要提高外部排序的速度,需要减少访问外存的次数。外存上信息的读写是以“物理块”为单位的,例如例子中,总共有 8 8 8 个物理块,则一趟需要进行 8 8 8 次读和 8 8 8 次写,总共 16 16 16 次,三趟归并加上对初始归并段进行内部排序需要进行的读写,总共 16 × 4 = 64 16\times4=64 16×4=64 次读写,则例子的2路平衡归并排序的总时间为:
t E S = 8 × t I S + 64 × t I O + 3 × 1999 t m g (1) t_{ES}=8\times t_{IS}+64\times t_{IO}+3\times1999t_{mg}\tag{1} tES=8×tIS+64×tIO+3×1999tmg(1)

如果对上述例子进行 4 4 4 路归并排序,则只需要两趟即可,这时总共的读写次数就是 ( 2 + 1 ) × 16 = 48 (2+1)\times16=48 (2+1)×16=48 次。也就是说,我们可以通过增大归并路数,减少归并趟数,从而减少外存读取次数,显著改善外部排序时间。同理,减少归并的段数,即增大初始归并段的大小,也可以减少归并趟数,缩短外部排序时间。

Moreover


多路归并和败者树

增加归并路数可以减少归并趟数,但是同时也使得内部归并的时间增加(即每次需要从更多的路数中挑选出关键字最小的记录)。当路数为 m m m 时,挑选记录需要的比较次数为 m − 1 m-1 m1 次。则每趟 n n n 个元素需要进行 ( n − 1 ) ( m − 1 ) (n-1)(m-1) (n1)(m1) 次比较, S S S 趟归并总共需要的比较次数为:
S ( n − 1 ) ( m − 1 ) = [ log ⁡ m r ] ( n − 1 ) ( m − 1 ) = [ log ⁡ r ] ( n − 1 ) m − 1 log ⁡ m (2) S(n-1)(m-1)=[\log_{m}r](n-1)(m-1)=[\log r](n-1)\frac{m-1}{\log m}\tag{2} S(n1)(m1)=[logmr](n1)(m1)=[logr](n1)logmm1(2)
其中, [ log ⁡ r ] ( n − 1 ) [\log r](n-1) [logr](n1) 是常数,而 ( m − 1 ) / log ⁡ m (m-1)/\log m (m1)/logm 随着 m m m 的增大而增长,这将降低减少外存访问次数带来的效益,因此我们需要改进内部排序算法。

败者树

为了使得内部归并不受到 m m m 增大的影响,引入败者树。是一种树形选择排序的变体,可以看成一棵完全二叉树,结点存储的是每次比较的败者,也就是说,我们保存了上次比较的结果,因为每次我们选出一个记录之后,在新的一组候选记录中,其中 m − 1 m-1 m1 个记录是和上次相同的,也就是上一轮的败者,这样当我们用一棵完全二叉树的每个结点记录这些败者,这时新的记录只需要向上进行比较,最终只需经过 log ⁡ m \log m logm 次比较即可找到胜者,即关键字最小的记录,输出到缓冲区。这时,内部排序的比较时间为:
S ( n − 1 ) [ log ⁡ m ] = [ log ⁡ r ] m ( n − 1 ) [ log ⁡ m ] = [ log ⁡ r ] ( n − 1 ) (3) S(n-1)[\log m]=[\log_{r}]m(n-1)[\log m]=[\log r](n-1)\tag{3} S(n1)[logm]=[logr]m(n1)[logm]=[logr](n1)(3)
这时比较次数和 m m m 无关。此时只要内存允许,通过增大路数可以显著改善外部排序的时间。不过,如果内存固定,增大路数自然就会减少每一路对应的输入缓冲区的大小,这样,需要更加频繁的外存读取,反而会增大读写外存的次数

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值