高级排序
在前文中已介绍了排序函数sort的简单用法,即直接使用sort函数对一个列表中的元素进行排序。但这种用法有一个很明显的不足,即用户无法自定义排序规则,而只能以sort默认的规则进行排序。
Perl中可以通过建立排序子程序来自定义排序规则。事实上,不管是怎样的排序算法,其最终进行的都是元素的两两比较,并根据比较结果确定两个元素的先后,最终确定所有元素的先后。Perl会自动完成这些步骤,而排序子程序的作用就是说明两个元素之间的比较规则,即两个元素如何确定先后的规则。
也就是说,排序子程序规定了两个元素之间的先后顺序规则,而排序时,sort会不断调用排序子程序来确定列表中某两个未归位元素的先后顺序,最终经过很多次调用后,完成对整个列表元素的排序。
前文提到,$a和$b是sort函数的两个内置变量,而这两个内置变量其实就是用于排序子程序中,供sort进行两两比较的两个变量。每一次调用排序子程序,sort都会确定$a与$b的先后顺序。由此可以写出如下示例程序:
sub mysort{
my($a,$b)=@_;
#在这里开始比较$a与$b
}
上例中,当排序子程序被调用时,sort会自动将两个未定位的列表元素作为参数传入子程序。然后声明私有变量$a与$b并将参数数组赋值给变量列表,然后进行对这两个变量的比较。而正因为$a与$b是sort的内置变量,其在使用上就与一般变量有所区别。实际上,上例中排序子程序的第一行是由Perl自动完成的,即创建私有变量,然后赋值这一系列操作。所以排序子程序中只需要书写排序规则语句即可。下例为一个真正的排序子程序:
sub mysort{
#在这里开始比较$a与$b($a与$b已被赋值)
}
如果要使用一个排序子程序,直接将没有与号的排序子程序名写在sort与参数之间即可。例:
@n=sort mysort@n; #让sort以排序子程序的规则排序
通常情况下排序子程序都十分简短,而如果排序子程序简短到只有一行时,就可以省略定义排序子程序的过程而直接将排序子程序代码块书写在sort与参数之间。例:
@n=sort{#在这里比较$a与$b}@n;
sort是根据排序子程序返回的一个数字来对$a与$b进行比较的,数字有三种,分别为:1、-1和0。如果子程序返回-1,那么$a就会被sort排在$b之前,而相反的,如果返回1,那么$b就会被sort排在$a之前,而如果返回0,则$a与$b不限先后顺序或无法确定其先后顺序。所以排序子程序要做的就是从这三个数字中返回一个数字给sort,从而确定两个排序变量的先后顺序。所以可以写出如下排序子程序:
sub mysort{
if($a<$b){-1}elsif($a>$b){1}else{0}
} #按数字大小进行从小到大的排序
上例中,排序子程序对$a与$b进行比较,如果$a更小,则返回-1,从而使sort将$a排在前面。如果$b更小,则返回1,从而使sort将$b排在前面,而如果$a与$b一样大,则返回0,不限先后顺序。
Perl中有更为简单的方法来书写上述内容,即使用飞船操作符“<=>”。飞船操作符是一个双目操作符,可以返回三种值:如果左<右,则返回-1,如果左>右,则返回1,如果左=右,则返回0,这样的操作与上文中使用if的写法是一致的,但很明显,使用飞船操作符能大大简化排序子程序的书写。下例为一个可以实际使用的排序语句:
@n=sort{$a<=>$b}@n; #对参数列表进行递增排序
对应于比较数字的飞船操作符,Perl中也有一个用于比较字符串的操作符,即“cmp”操作符。其可以对字符串进行比较,返回的结果与飞船操作符一致。所以可以写出如下语句:
@n=sort{$a cmp $b}@n; #将参数列表中的字符串按ASCII码顺序排列
很明显,上例中的排序子程序规则就是sort函数的默认排序规则,所以上例的排序子程序可以省略。但是cmp可以用来建立更为复杂的排序子程序,最常见的如建立一个不分大小写的排序:
@n=sort{“\L$a”cmp”\L$b”}@n; #将$a与$b均全部转为小写再比较
注意,为了优化性能,$a与$b在排序子程序被调用期间就是列表元素的临时别名,所以在子程序调用期间一般不建议修改$a与$b的值,否则就会直接修改列表元素的值。
比较操作符的短视性
飞船操作符与cmp操作符具有短视性,这是其非常重要的一个性质。即操作符只管返回左右两边的比较结果而不顾$a与$b谁在左谁在右,sort接受排序子程序的返回值从而决定$a与$b的先后,即如果返回-1,那么$a一定在左,如果返回1,那么$b一定在左。利用短视性,可以调换$a与$b在操作符两边的位置,就可以得到反序,即进行递减排序的排序子程序:
sort{$b<=>$a} #递减排序子程序
这其中的原理较难理解,故此处略去论证过程。
如果要得到递减序列,还可以先得到一个递增序列,再使用reverse倒序。在较新版本的Perl中,reverse被当成sort的一个修饰符使用,在处理时会做特殊优化,所以使用reverse的写法与上文中利用短视性的写法没有明显差别。例:
reverse sort{$a<=>$b} #通过reverse返回倒序序列,即递减序列
间接排序
sort仅负责决定排序变量$a与$b的先后顺序,但在排序子程序中进行比较的却不一定只可以是$a与$b,还可以是与排序变量相关的一些其他量,如通过排序变量索引到的数组值、哈希值,以排序变量作为子程序参数从而得到的子程序返回值等,通过对这些量进行排序并返回排序结果,从而间接决定了$a与$b的先后顺序,这就是间接排序。下文以根据哈希值进行间接排序来说明这个技巧:
@key=sort{$hash{$a} cmp $hash{$b}} keys%hash;
上例是高级排序运用于哈希排序的一个典型例子。首先使用keys函数取出哈希键列表并用sort对其进行排序。在排序子程序中,$a与$b会分别成为键列表中两个哈希键的临时别名,但此时进行比较的不是哈希键,而是由哈希键取出的哈希值。根据哈希值的排序结果返回一个数字给sort,从而让sort决定$a与$b的先后顺序,从而最终得到排序后的哈希键列表。这样就可以通过哈希值的比较结果间接决定哈希键列表的顺序,从而实现了按哈希值排序。
多重排序
上文提到,如果比较操作符两边的数字或字符串相等时,就会返回0。而利用返回0这个性质,就可以将比较操作符与逻辑或操作符连用,从而在比较操作符返回0时进行短路运算,计算操作符右边表达式的值,并最终返回最后运算的那部分表达式的返回值,作为sort函数排序的依据。多重排序可以在单个规则无法确定两个排序变量的先后的情况下,再使用第二个或更多的规则对排序变量或其相关量进行排序。例:
@key=sort{
$hash{$a}cmp $hash{$b} or
$acmp $b
}keys%hash;
上例是多重排序应用于哈希排序的一个典型例子。首先取出哈希键列表并利用sort进行排序。排序子程序首先以哈希值进行间接排序,如果比较操作符返回0,即哈希值相等时,就进行短路运算,再进行哈希键的比较。而由于哈希键的唯一性,对哈希键进行比较是不可能返回0的。最终由第一个或第二个比较操作符返回比较结果,从而使sort确定哈希键列表的顺序,完成哈希排序。
uc与lc函数
uc和lc函数分别可以返回全大写或全小写的字符串,其功能类似于反斜线转义中的\U和\L。例:
print uc’aaa’; #输出AAA
print lc’AAA’; #输出aaa
sprintf函数
sprintf函数与printf函数十分相似,其在参数的用法上是一致的,均由格式字符串和替换转换符号的一个列表组成。sprintf与printf最大的区别在于,sprintf会将处理后的格式字符串作为返回值返回,而不像printf会直接输出。所以sprintf可以用于需要继续对处理后的字符串进行处理和运算的地方。例:
$_=sprintf”%d”,$_; #将转化后的格式字符串返回并重新赋值给默认变量
index与rindex函数
index函数可用于查找子字符串在主字符串中的相对位置。其会从主字符串的开头开始查找,找到第一个匹配位置后就返回该位置第一个字符所在的位置编号,然后结束查找,如果没有找到,则会返回-1。字符串位置编号类似于数组索引值,为0开始递增的整数。
index函数的参数可以为2至3个,前两个分别为主字符串和子字符串,表示从主字符串中查找该子字符串。例:
$_=index’bba’,a; #在主字符串中查找a,返回2
类似于index函数,rindex函数也可用于字符串的查找。其与index的不同之处在于起点,rindex函数将字符串末尾作为起点,不断向前查找。所以rindex找到的是子字符串在主字符串中最后一次出现的位置。例:
$_=rindex’abba’,a; #以字符串末尾作为起点向前查找,返回3
index与rindex函数都可以加上第三参数,第三参数的作用为限定查找起点。index函数会从该起点开始向后查找,直到字符串末尾,而rindex函数会从该起点开始向前查找,直到字符串开头。例:
print index’abba’,a,1; #限定起点为1,即从第一个b开始向后查找,输出3
print rindex’abba’,a,1; #从起点1开始向前查找,输出0
第三参数也可设为变量,从而用于循环查找。例:
while(1){ #无限的while循环
$_=index’abba’,’a’,$i; #起点限定为变量$i
lastif $_==-1; #当index返回-1,即未找到时跳出循环
print; #输出找到的位点
$i+=$_+1; #将起点推移至当前找到的位点后一个字符
}
substr函数
substr函数可以从一个字符串中取出某一段进行处理,其可接受2至4个参数。第一参数为需要处理的字符串,第二参数为需要取出的字符串的起点,第三参数为取出的字符串的长度。如果省略第三参数,则会从起点一直取到字符串终点。而如果第二参数使用负数,则会从主字符串的末尾向前定位。例:
substr’abba’,1; #取出abba中从起点1到结尾的全部字符串,即bba
substr’abba’,1,2; #取出起点为1长度为2字符的字符串,即bb
substr’abba’,-2; #取出字符串倒数两个字符到结尾的字符串,即ba
substr函数的第二参数也可以与index或rindex函数连用,从而将起点设为函数的返回值。例:
print substr’abba’,index(‘abba’,b),2; #在abba中查找b第一次出现的位置,并将结果返回作为substr的第二参数。取出从该起点开始长度为2字符的字符串并输出,输出bb
如果字符串存储在变量中,就可以这个字符串进行修改,而直接对一个没有保存在变量中的字符串修改是无意义的,语法上也是不允许的。修改字符串的内容有两种写法。第一种为2至3参数写法。将提取出的子字符串放在赋值式的左边,即当作被赋值的量,用新的字符串去替换提取出的那部分子字符串。用来替换的字符串不一定要与提取出的字符串等长,字符串会自行调整长度。例:
substr($_,2,2)=’c’; #提取出$_中起点为2,长度为2的子字符串,并将其替换为’c’
第二种写法为四参数写法。第四个参数为进行替换的字符串。这种写法可以将substr函数当作一种修改字符串内容的操作符使用。四参数的substr可以直接在空上下文中使用,也可以取得其返回值,返回值为修改前提取出的子字符串。同样,这种写法不要求取出的字符串与用来替换的字符串等长。例:
substr$_,2,2,’c’; #substr函数的四参数用法,作用同上例
substr函数有两个主要的使用技巧。首先,如果将其第四参数设为空字符串,就可以直接删除提取出的那部分字符串。
substr$_,2,2,”; #删除提取出的字符串
第二个技巧为:如果将其第三参数设为0,就可以直接将一个字符串添加进主字符串中。添加进的字符串的第一个字符会被添加至起点处,其余字符以及原字符串会依次向后推移。如果要将字符串添加至末尾处,将起点设为主字符串末尾的位置再加1,即该字符串的长度数值即可。例:
substr$_,2,0,’c’; #在位置2处添加字符串
substr$_,0,0,’c’; #在开头添加字符串
substr$_,length,0,’c’; #以$_的长度数值为起点,即在主字符串末尾添加字符串
樱雨楼
完成于2016.1.16
最后修改于2016.1.31