首先假设一个需求:
一个文件中有若干合法的身份证号,每行一个。现在需要对这些身份证号进行升序排序,要求综合出生年、月、日进行排序,其中年、月、日重要性依次递减。
很可能一个Perl 程序员看到这样的需求,马上就能作出下面的实现:
#!/usr/bin/env perl
package main;
use strict;
use warnings;
use 5.010;
print sort {my @a = split //, $a;
my @b = split //, $b;
(join "", @a[6..9]) <=> (join "", @b[6..9])
or (join "", @a[10..11]) <=> (join "", @b[10..11])
or (join "", @a[12..13]) <=> (join "", @b[12..13])}
<DATA>;
1;
__DATA__ 110000198109067427 110000198710239963 110000198105197982 110000198103242380 110000198711279963 110000198703279963 110000198005262597 110000198710159963 110000198108077237 110000198011113097 110000198710179963
诚然这样的程序可以使用规矩的思维和较短的代码完成需求,但是也带来了排序算法的低效。
看看我们做了多少次split
和join
操作吧:
每一次元素比较,这两个函数都需要被调用。假设要排序的身份证号为N 个,而perl sort 函数的时间复杂度O(NlogN)。每次比较,需要调用2 次split
和6 次join
,所以两个函数都被调用了O(NlogN) 次,如果拿掉大O
标记,这个结果会更加难看。
施瓦茨变换(Schwartzian Transform)由Perl 黑客Randal L. Schwartz 所创造,提供了一种高效排序的方法,其基本思路如下:
上面的思路因为是用自己的语言组织的,可能描述的不是很清晰,下面是一个使用施瓦茨变换高速排序的例子,对于同样的需求,返回了同样的排序结果,但是用了更少的时间。
#!/usr/bin/env perl
package main;
use strict;
use warnings;
use 5.010;
print map {$_->[0]}
sort {$a->[1] <=> $b->[1]
or $a->[2] <=> $b->[2]
or $a->[3] <=> $b->[3]}
map {@_ = split //;
[$_,
(join "", @_[6..9]),
(join "", @_[10..11]),
(join "", @_[12..13])]}
<DATA>;
1;
__DATA__ 110000198109067427 110000198710239963 110000198105197982 110000198103242380 110000198711279963 110000198703279963 110000198005262597 110000198710159963 110000198108077237 110000198011113097 110000198710179963
在排序算法的时间复杂度已经确定的情况下,我们可以从split
和join
函数的调用次数上来看为什么施瓦茨变换耗时更少:
可以看到这两个函数没有出现在排序的过程中,而是在之前将原始数据变换为列表的过程中被调用。这里假设要排序的身份证号为N 个,每个身份证需要一次变换,每次变换调用了1 次split
和3 次join
,这两个函数都被调用了O(N) 次,如果拿掉大O
标记,这个差异和上一节的传统排序方式相比将会更加明显。
最后施瓦茨变换是一种排序思路,可以用在任何语言当中。在排序算法的时间复杂度确定的情况下,使用施瓦茨变换会明显提高排序的整体速度——特别是从原始数据中提取排序元素很复杂的情况下。