假设我的IPvN是一个格式与IPv4几乎相同的序列,唯一的区别是可以有任意数量的点或数字。我想按如下方式对这些序列进行排序。
8.8.8.8
20.0.0.0
1.2.3.4.5
我用粗糙的方法来合成逻辑条件表达式。我知道这样做是非常低效和浪费内存,特别是当只有少数异常长的IPvN时,人类可以一眼就解决但我拖着完全冗余和无用的代码尾部,如... or $a->[1000] <=> $b->[1000]
。
如下所示,我对双层进行了叠加的Schwartzian变换。
(我输出的第一行。)
Use of uninitialized value in numeric comparison (<=>) at (eval 4) line 1.
但是,如果是
'8.8.8.6.0' => 6
而非
<{1}}中的'8.8.8.8.0' => 6
,不会有任何警告。
我只知道这个错误与
有关
%IP_counter
。
'8.8.8.8' => 3
use strict;
use warnings;
use feature 'say';
use List::Util qw(max);
my %IP_counter =
(
'1.1.1.1.1' => 1,
'127.0.0.1' => 2,
'8.8.8.8' => 3,
'20.0.0.1' => 4,
'999999999999999999999999999999999999999999999999.9'
=> 5,
'8.8.8.8.0' => 6,
'888888888888888888888888888888888888888888888888.8'
=> 7,
);
# Find the max length
# Abbr. "c_n_d_m" -> "count numbers divided (max)".
my @c_n_d;
my $c_n_d_m = 0;
foreach (keys %IP_counter) {
@c_n_d = split /\./, $_;
$c_n_d_m = @c_n_d if $c_n_d_m < @c_n_d
}
# ?Q1. Question 1 arises here
# Concatenation of hardcode
my @m = (1..$c_n_d_m);
my $cond;
foreach my $i (0..@m-1) {$cond .= '$a->['.$m[$i].'] <=> $b->['.$m[$i].'] or '}
$cond = substr $cond, 0, rindex($cond, ' or ');
# ?Q2. Question 2 arises here
# Sort keys by numeral
# Do a double "Schwartzian transform"
my @sort_by_num =
# Sort by number of dots:
map { $_ }
sort { (split /\./, $a) <=> (split /\./, $b) }
map { [ $_, /(\d+)/g ] -> [@_] }
# Sort by (regarded as) shift floats:
map { $_->[0] }
sort { eval $cond }
map { [ $_, /(\d+)/g ] }
keys %IP_counter;
# Print the result
my $width = max map {length $_} keys %IP_counter;
foreach my $IP (@sort_by_num) {
printf "%-*s => $IP_counter{$IP}\n", $width, $IP
}
答案 0 :(得分:6)
我解决这个问题的方法是使用Guttman Rosler变换(GRT)。 GRT是Schwartzian变换的变体,其中sort
块是空的(即使用标准词典字符串比较)。这样更有效,因为sort
可以完全在C代码中运行,而无需回调到Perl代码O(n * log n)次(对于每次比较)。在您的情况下,这更为重要,因为您的代码会反复调用eval
,这很慢。
GRT的棘手部分是如何构建你的密钥
以下是解决问题的方法:
use strict;
use warnings;
my @values = qw(
139.8.0.2
127.0.0.11
1.1.1.1.1
217.0.22.3
8.8.8.8
127.0.0.1
139.12.0.2
);
my @sorted =
map join('.', unpack 'J>/C'),
sort
map pack('J>/C', split /\./),
@values;
print "$_\n" for @sorted;
输出:
8.8.8.8
127.0.0.1
127.0.0.11
139.8.0.2
139.12.0.2
217.0.22.3
1.1.1.1.1
我们的想法是获取每个字符串,将其拆分为.
,然后使用格式字符串J>/C
打包数字。这告诉pack
打包一系列字符(真正的八位字节),其代码点以无符号整数(C
)给出,但以UV格式(/
)加上前缀({ {1}}),big endian(J
)。 >
是Perl在内部用于无符号整数的C类型;它通常是4个字节(对于32位机器)或8个字节(对于64位机器)宽,并且UV
不能产生比这种类型更适合的字段。
因此,例如split
,'139.8.0.2'
生成split
,其中139, 8, 0, 2
变为pack
,即长度值4编码为大端8字节整数({ {1}})后跟对应于点分十进制数字的字节。
按字典顺序进行比较时,此字符串将根据您的标准进行排序:
最后,我们通过应用逆操作"\x00\x00\x00\x00\x00\x00\x00\x04\x8b\x08\x00\x02"
和"\x00\x00\x00\x00\x00\x00\x00\x04"
撤消split
/ pack
生成的编码。
也就是说,在Schwartzian变换中使用多个标准的一般方法是从第一个join
生成多个字段:
unpack
答案 1 :(得分:3)
这显示了IP地址的排序,并忽略了代码的其余部分
use strict;
use warnings 'all';
use feature 'say';
my @ips = qw/
139.8.0.2
127.0.0.11
1.1.1.1.1
217.0.22.3
8.8.8.8
127.0.0.1
139.12.0.2
/;
my @sorted = map { $_->[0] }
sort { $a->[1] <=> $b->[1] or $a->[2] cmp $b->[2] }
map { [ $_, tr/.//, join ('', map chr, /\d+/g) ] } @ips;
say for @sorted;
8.8.8.8
127.0.0.1
127.0.0.11
139.8.0.2
139.12.0.2
217.0.22.3
1.1.1.1.1
答案 2 :(得分:-1)
使用CPAN中的Sort::Naturally
。为什么重新发明轮子?
#!/usr/bin/env perl
# always use these two
use strict;
use warnings;
# use autodie to automatically die on open errors
use autodie;
use Sort::Naturally;
my %IP_counter =
(
'1.1.1.1.1' => 1,
'127.0.0.1' => 2,
'8.8.8.8' => 3,
'20.0.0.1' => 4,
'999999999999999999999999999999999999999999999999.9'
=> 5,
'8.8.8.8.0' => 6,
'888888888888888888888888888888888888888888888888.8'
=> 7,
);
my @sorted_keys = nsort( keys %IP_counter );
print "$_\n" for @sorted_keys;