使用类似IPv4的数字和点对哈希键进行排序

时间:2018-04-15 17:51:11

标签: perl sorting

假设我的IPvN是一个格式与IPv4几乎相同的序列,唯一的区别是可以有任意数量的点或数字。我想按如下方式对这些序列进行排序。

  • IPv4在IPv5之前,依此类推。
  • 对于相同数量的小数点,按从左到右的垂直位置的数字升序排序。
    8.8.8.8
    20.0.0.0
    1.2.3.4.5


问题

1。如何让它变得更聪明?

我用粗糙的方法来合成逻辑条件表达式。我知道这样做是非常低效和浪费内存,特别是当只有少数异常长的IPvN时,人类可以一眼就解决但我拖着完全冗余和无用的代码尾部,如... or $a->[1000] <=> $b->[1000]

2。如何在单个"Schwartzian transform"中实现满足多个条件的排序?

如下所示,我对双层进行了叠加的Schwartzian变换。

3。为什么会出现此错误?

(我输出的第一行。)

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
}

3 个答案:

答案 0 :(得分:6)

我解决这个问题的方法是使用Guttman Rosler变换(GRT)。 GRT是Schwartzian变换的变体,其中sort块是空的(即使用标准词典字符串比较)。这样更有效,因为sort可以完全在C代码中运行,而无需回调到Perl代码O(n * log n)次(对于每次比较)。在您的情况下,这更为重要,因为您的代码会反复调用eval,这很慢。

GRT的棘手部分是如何构建你的密钥

  1. 他们比较正确,
  2. 他们允许您随后提取原始数据。
  3. pack / unpack在这里经常有用。

    以下是解决问题的方法:

    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}})后跟对应于点分十进制数字的字节。

    按字典顺序进行比较时,此字符串将根据您的标准进行排序:

    • 首先比较长度字段(因为它首先出现在字符串中)。 Big-endian整数按升序排序,因此首先排序数字成分较少的字符串(即较少的点)。
    • 对于具有相同长度字段的字符串(即相同数量的数字组件),比较打包内容(每个数字组件已编码为一个字节),这又会产生所需的顺序。

    最后,我们通过应用逆操作"\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;