17、编写一个名为 has – no – e 的子程序,如果给定的单词中没有字母“e”,则返回 True。修改此程序,只打印没有字母“e”的单词,并计算列表中没有字母“e”的单词的百分比。假设单词列表存储在 ‘words.txt’ 文件中,每行一个单词。
以下是对应代码:
sub has-no-e (Str $word) {
not defined index $word, "e";
}
my $total-count = 0;
my $count-no-e = 0;
for 'words.txt'.IO.lines -> $line {
$total-count++;
if has-no-e $line {
$count-no-e++;
say $line;
}
}
say "=" x 24;
say "Total word count: $total-count";
say "Words without 'e': $count-no-e";
printf "Percentage of words without 'e': %.2f %%", 100 * $count-no-e / $total-count;
18、编写一个名为 uses – only 的子程序,它接受一个单词和一个字母字符串,如果该单词仅包含字母列表中的字母,则返回 True。此外,能否用字母 acefhlo 组成除 “Hoe alfalfa?” 之外的句子?
子程序代码如下:
sub uses-only (Str $word, Str $available) {
for 0..$word.chars - 1 -> $idx {
my $letter = substr $word, $idx, 1;
return False unless defined index $available, $letter;
}
True;
}
可以用字母 acefhlo 组成其他句子,例如 “Call a chef.”。
19、编写一个程序,找出有三个连续双字母的单词。例如,“committee”(c – o – m – m – i – t – t – e – e),要是没有中间的“i”就符合要求;“Mississippi”(M – i – s – s – i – s – s – i – p – p – i),要是去掉那些“i”也符合要求。
程序找出的有三个连续双字母的单词是“bookkeeper”、“bookkeepers”、“bookkeeping”和“bookkeepings”。
可以使用以下两种程序来找出这些单词:
使用循环技术的程序:
sub is_triple_double (Str $word) {
# Tests if a word contains three consecutive double letters.
my $i = 0;
my $count = 0;
while $i < $word.chars - 1 {
if substr($word, $i, 1) eq substr($word, $i + 1, 1) {
$count++;
return True if $count == 3;
$i += 2;
} else {
$count = 0;
$i++;
}
}
return False;
}
for 'words.txt'.IO.lines -> $word {
say $word if is_triple_double $word;
}
使用正则表达式的程序:
for 'words.txt'.IO.lines -> $word {
say $word if $word ~~ /(.) $0 (.) $1 (.) $2/;
}
也可以在操作系统命令行提示符下使用单行命令:
$ perl6 -ne '.say if /(.) $0 (.) $1 (.) $2/' words.txt
20、这是另一个《汽车脱口秀》谜题:前几天我在高速公路上开车,碰巧注意到了我的里程表。和大多数里程表一样,它显示六位数字,且只显示整数英里数。例如,如果我的车行驶了300000英里,我会看到3 – 0 – 0 – 0 – 0 – 0。那天我看到的情况非常有趣。我注意到最后四位数字是回文数,即它们正读和反读都一样。例如,5 – 4 – 4 – 5是回文数,所以我的里程表可能显示3 – 1 – 5 – 4 – 4 – 5。一英里后,最后五位数字是回文数。例如,它可能显示3 – 6 – 5 – 4 – 5 – 6。再一英里后,六位数字中的中间四位是回文数。还有更绝的,再一英里后,所有六位数字都是回文数!问题是,我第一次看里程表时上面显示的数字是多少?编写一个程序来测试所有六位数字,并打印出任何满足这些要求的数字。
以下是解决该回文里程表谜题的可能程序:
sub is-palindrome ($number, $start, $len) {
# Checks if the relevant substring is a palindrome
my $substring = substr $number, $start, $len;
return $substring eq flip $substring;
}
sub check ($num) {
# Checks whether the integer num has the properties described
return (
is-palindrome($num, 2, 4) and
is-palindrome($num + 1, 1, 5) and
is-palindrome($num + 2, 1, 4) and
is-palindrome($num + 3, 0, 6)
);
}
say 'The following are the possible odometer readings:';
for 1e5 .. 1e6 - 4 -> $number {
say $number if check $number;
}
另一种方法是使用正则表达式来判断是否为回文数:
sub check ($num) {
# Checks whether the integer num has the properties described
$num ~~ /^..(.)(.)$1$0/ and
$num + 1 ~~ /^.(.)(.).$1$0/ and
$num + 2 ~~ /^.(.)(.)$1$0/ and
$num + 3 ~~ /^(.)(.)(.)$2$1$0/;
}
say 'The following are the possible odometer readings:';
for 1e5 .. 1e6 - 4 -> $number {
say $number if check $number;
}
第二段代码更短,但执行速度较慢,在我的电脑上执行时间几乎是第一段的两倍。如果需要多次或频繁运行程序,第一种更快的方法可能更好;如果只是一次性计算,可能会更喜欢第二种版本。具体选择由你决定。
21、编写一个名为 nested_sum 的子程序,该子程序接受一个由整数数组组成的数组,并将所有嵌套数组中的元素相加。
以下是几种实现方式:
使用嵌套循环:
perl
my @AoA = [[1, 2], [3], [4, 5, 6]];
sub nested_sum (@array) {
my $sum;
for @array -> $item {
for $item.flat -> $nested_item {
$sum += $nested_item;
}
}
return $sum;
}
say nested_sum @AoA; # -> 21
使用“|”操作符:
perl
my @AoA = [[1, 2], [3], [4, 5, 6]];
sub nested_sum (@array) {
my $sum;
for @array -> $item {
for |$item -> $nested_item {
$sum += $nested_item;
}
}
return $sum;
}
say nested_sum @AoA; # -> 21
使用 map 和归约操作符:
perl
my @AoA = [[1, 2], [3], [4, 5, 6]];
sub nested_sum (@array) {
return [+] map {|$_}, @array;
}
say nested_sum @AoA; # -> 21
递归实现(处理未知嵌套深度):
perl
my @AoA = [[1,2], [3], [4,5,6], [3, [7,6, [3,2]]]];
sub nested_sum ($input) {
my $sum = 0;
for |$input -> $item {
if $item.WHAT ~~ Int {
$sum += $item;
} else {
$sum += nested_sum $item;
}
}
return $sum;
}
say nested_sum @AoA; # -> 42
22、编写一个名为
cumul-sum
的子程序,它接受一个数字列表并返回累积和;也就是说,返回一个新列表,其中第
i
个元素是原始列表中前
i
个元素的和。
cumul-sum
i
i
sub cumul-sum (@array) {
my @cumulative;
my $partial_sum = 0;
for @array -> $element {
$partial_sum += $element;
push @cumulative, $partial_sum;
}
return @cumulative;
}
23、编写一个名为 middle 的子程序,它接受一个列表并返回一个新列表,该新列表包含除第一个和最后一个元素之外的所有元素。
sub middle (@array) { return @array[1..*-2] }
24、编写一个名为chop-it的子程序,该子程序接受一个数组,通过移除数组的第一个和最后一个元素来修改它,并且不返回有意义的值。
以下是两种实现方式:
方式一:使用
shift
和
pop
函数。
sub chop-it (@array) {
shift @array;
pop @array;
return;
}
方式二:使用切片。
sub chop-it (@array) {
@array = @array[1..*-2];
return;
}
25、编写一个名为is_sorted的子程序,它接受一个数字列表(或数组)作为参数,如果列表按升序排序则返回True,否则返回False。
以下是几种实现方式:
迭代比较方式:
sub is_sorted (@array) {
my $previous = @array[0];
for @array -> $current {
return False if $current < $previous;
$previous = $current;
}
return True;
}
与排序后列表比较方式:
sub is_sorted (@array) {
return @array eqv @array.sort;
}
使用功能编程和归约超运算符方式:
sub is_sorted (@array) {
return [<=] @array;
}
26、如果能通过重新排列一个单词的字母来拼出另一个单词,那么这两个单词就是变位词。编写一个名为 is – anagram 的子程序,它接受两个字符串,如果它们是变位词则返回 True。
以下是两种实现方式:
第一种:
sub is-anagram(Str $word1, Str $word2) {
return False if $word1.chars != $word2.chars;
return False if $word1.comb.sort ne $word2.comb.sort;
True;
}
第二种(更简洁但可能效率稍低):
sub is-anagram(Str $word1, Str $word2) {
return $word1.comb.sort eq $word2.comb.sort;
}
27、编写一个名为has_duplicates的子程序,它接受一个列表或数组,如果其中有任何元素出现不止一次,则返回True。该子程序不应修改原始输入。
以下是几种实现该功能的子程序代码:
使用
repeated
函数:
sub has_duplicates(@array) {
?@array.repeated
}
先排序再比较相邻元素:
sub has_duplicates(@array) {
my @sorted = sort @array;
for 1..@sorted.end -> $i {
return True if @sorted[$i] eq @sorted[$i - 1];
}
return False;
}
排序后记录前一个元素进行比较:
sub has_duplicates(@array) {
my @sorted = sort @array;
my $previous = shift @sorted;
for @sorted -> $item {
return True if $item eq $previous;
$previous = $item;
}
return False;
}
使用
unique
函数比较元素数量:
sub has_duplicates(@array) {
@array.unique.elems != @array.elems;
}
使用哈希表:
sub has_duplicates(@array) {
my %seen;
for @array -> $elmt {
return True if %seen{$elmt}:exists;
%seen{$elmt} = 1;
}
return False;
}
28、如果你的班级有23名学生,那么其中两人同一天生日的概率是多少?你可以通过生成23个随机生日样本并检查是否有重复来估算这个概率。提示:你可以使用rand和int函数生成随机生日。
通过模拟可知,23人的样本中,至少两人同一天生日的概率约为50%。
29、编写一个子程序,读取文件words.txt并构建一个列表,列表中的每个元素对应一个单词。编写此函数的两个版本,一个使用push方法,另一个使用unshift方法。哪个版本运行时间更长?为什么?
unshift版本运行时间更长。因为
unshift
是在数组开头插入元素,Perl需要多次移动数据来重新组织整个数组;而
push
是在数组末尾插入元素,内部操作相对较少。
30、编写一个名为 bisect 的函数,该函数接受一个排序列表和一个目标值,并返回目标值是否在列表中的信息。
以下是实现该功能的代码:
sub bisect (@word_list, Str $word) {
my $index = (@word_list.elems / 2).Int;
return False if $index == 0 and @word_list[$index] ne $word;
my $found = @word_list[$index];
if $word lt $found {
# search the first half
return bisect @word_list[0..$index-1], $word;
} elsif $word gt $found {
# search the second half
return bisect @word_list[$index+1..*-1], $word;
}
True; # if we get there, we've found the word
}
你可以使用如下方式调用这个函数:
for <a f w e q ab ce> -> $search {
if bisect [<a b d c e f g>], $search {
say "found $search";
} else {
say "did not find $search";
}
}
31、如果两个单词互为倒序,则称它们为“倒序对”。编写一个程序,找出words.txt文件中所有的倒序对。
以下是几种实现方式:
方式一:使用二分查找
sub bisect (Str $word, @word_list) {
# see the code in the previous exercise
}
my @array = 'words.txt'.IO.lines;
for @array -> $word {
my $reverse = $word.flip;
my $res = bisect $reverse, @array;
say "$word and $reverse form a reverse pair" if $res >= 0;
}
say now - INIT now;
方式二:使用grep和二分查找
say "$_ and $_.flip() form a reverse pair" for @array.grep( { bisect( .flip, @array ) >= 0 } );
方式三:使用哈希查找
my %hash = map { $_ => 1}, 'words.txt'.IO.lines;
for %hash.keys -> $word {
my $reverse = $word.flip;
say "$word and $reverse form a reverse pair" if %hash{$reverse}:exists;
}
say now - INIT now;
32、如果从两个单词中交替取字母能组成一个新单词,那么这两个单词就“互锁”。编写一个程序,在words.txt文件中找出所有互锁的单词对。提示:不要枚举所有的单词对,因为数量太多了!
以下是解决该问题的程序:
use lib ".";
use BisectSearch;
my @array = 'words.txt'.IO.lines;
for @array -> $word {
my ($word1, $word2) = intersplit($word);
say "$word: $word1, $word2" if bisect($word1, @array) >= 0 and bisect($word2, @array) >= 0;
}
sub intersplit (Str $word) {
my @letters = $word.comb;
my $evens = join '', map {@letters[$_] if $_ %% 2}, @letters.keys;
my $odds = join '', map {@letters[$_] if $_ % 2}, @letters.keys;
return ($evens, $odds);
}
该程序的思路是,对于单词列表中的每个单词,将其“互拆”为一个包含偶数位字母的字符串和一个包含奇数位字母的字符串,然后检查这些子字符串是否属于该列表。此外,还可以对
intersplit
子程序进行优化,使用一个接受两个参数(一个奇数位字母和一个偶数位字母)的尖块来实现:
sub intersplit (Str $word) {
my (@evens, @odds);
for $word.comb -> $even, $odd {
push @evens, $even;
push @odds, $odd;
}
return @evens.join, @odds.join;
}
33、对阿克曼函数进行记忆化处理,看看记忆化是否能让该子程序对更大的参数进行求值。提示:不能。
对阿克曼函数进行记忆化处理的代码如下:
my %ack-memo;
sub mem-ack(Int $m, Int $n) {
return $n + 1 if $m == 0;
return mem-ack($m - 1, 1) if $n == 0;
%ack-memo{"$m;$n"} = mem-ack($m - 1, mem-ack($m, $n - 1)) unless %ack-memo{"$m;$n"}:exists;
return %ack-memo{"$m;$n"};
}
但实际上,即便使用了记忆化,也不建议尝试对
$m
大于 3 的值运行该函数。因为如果遇到已经计算过的数对对应的阿克曼值,意味着进入了无限循环,所以记忆化阿克曼函数并没有实际意义,无法用更大的参数求值。
34、编写一个名为has – duplicates的函数,该函数接受一个列表作为参数,使用哈希表实现,如果列表中任何对象出现不止一次,则返回True,否则返回False。
可以使用以下代码实现:
sub has-duplicates(@array) {
my %seen;
for @array -> $elmt {
return True if %seen{$elmt}:exists;
%seen{$elmt} = 1;
}
return False;
}