【问题标题】:Sorting a hash where keys contain non-alphanumeric characters对键包含非字母数字字符的哈希进行排序
【发布时间】:2016-04-27 14:44:36
【问题描述】:

我有一个像下面这样的哈希:

my %hash=( '(293 to 296)'   => 2,
           '(3118 to 3121)' => 2,
           '(330 to 333)'   => 2,
           '(2126 to 2129)' => 2,
           '(1999 to 2002)' => 2,
           '(2138 to 2141)' => 9,
           '(771 to 774)'   => 4,
           '(2016 to 2019)' => 1,
           '(888 to 891)'   => 5,
           '(3102 to 3105)' => 1,
        );

我想使用键对哈希进行排序,其中键包含括号。我试过下面的代码,

foreach $key(sort {$b <=> $a} keys %hash)
{
    print $key;
}

我得到了以下,未按数字排序:

(888 至 891)(2016 至 2019)(293 至 296)(3118 至 3121)(3102 至 3105)(330 至 333)(1999 至 2002)(2126 至 2129)(2138 至 2141)(771 至 774)

我希望得到一个按数字排序的输出,如下所示。请建议我实现以下目标的方法:

(293 to 296)
(330 to 333)
(771 to 774)
(888 to 891)
(1999 to 2002)
(2016 to 2019)
(2126 to 2129)
(2138 to 2141)
(3102 to 3105)
(3118 to 3121)                             

【问题讨论】:

  • 如果范围重叠怎么办?您应该按开始、结束还是中位数排序?
  • Ithnk 您正在寻找的是“natural sorting” - SO 上甚至还有一个标签。我已将其添加到问题中。有关示例,请参阅我自己和 @Borodin 的回复。

标签: perl sorting hash natural-sort


【解决方案1】:

'(293 to 296)' 不是数字(甚至不以数字开头),因此尝试按数字排序没有任何意义。

您可以从中提取第一个数字并对其进行排序。

($a) = ($a =~ /(\d+)/);
($b) = ($b =~ /(\d+)/);

【讨论】:

    【解决方案2】:

    试试这个

    在下面的脚本中,我使用模式匹配来删除带有/r 标志的( )。 它有助于保存替换中的原始数据。然后它将按数字排序。

    my %hash=( '(293 to 296)'   => 2,
               '(3118 to 3121)' => 2,
               '(330 to 333)'   => 2,
               '(2126 to 2129)' => 2,
               '(1999 to 2002)' => 2,
               '(2138 to 2141)' => 9,
               '(771 to 774)'   => 4,
               '(2016 to 2019)' => 1,
               '(888 to 891)'   => 5,
               '(3102 to 3105)' => 1,
            );
    
    
    
    foreach my $i (sort { $a=~s/\(//rg <=> $b=~s/\(//rg }  keys %hash)
    {
        print "$i\n";
    
    }
    

    【讨论】:

    • 如果你还有非数字值,这会起作用吗?
    • @Sobrique 是的,它有效。例如考虑 $a = "10 to 15"; print int($a)+14; 它给出的结果为 24。因为 int 函数。由于数字比较运算符,它的工作方式相同。如果我的解释是错误的,你能解释什么是错的吗?我从实际中得到它。你能分享一下这背后的原因吗?谢谢。
    • @mkhun @Sobrique 所以,但是,为什么这行得通?数字比较运算符&lt;=&gt; 向编译器提供解析提示(设计或意外)?并且以某种方式使用| 对非数字、非字母、不存在的字符进行非破坏性替换(/r),以一种准“自然”的方式神奇地交替排序?我想我更喜欢故意这样做,就像@Borodin 或我自己的回复一样......无论如何+1,但如果这对我来说并不神奇,我可能会更喜欢它:-)
    • @mkhun 什么是 |在 s/(|)//rg 中做什么?是否选择/或(和)进行替换?或者更神奇的东西?
    • 误报 ...s/[\(\)]//r 也同样有效。对于一些小字体、错字相关的原因,当我发布这些 cmets 时它不起作用。
    【解决方案3】:

    sort 的工作原理是将 $a$b 传递给函数,然后返回 -10+1

    最简单的 - 按第一个数字排序 - 会这样:

    sort { $a =~ s/.(\d+).*/$1/r <=> $b =~ s/.*(\d+).*/$1/r } keys %hash
    

    这会从每个键中提取第一个数值,进行比较并返回该比较值。

    当然,如果您的范围重叠,这将无法按您想要的方式工作 - 您将不得不变得更复杂 - 如果您有:

    100 到 200 150 至 180 120 到 205

    它们应该如何排序?不管怎样——你写一个在$a$b上“工作”的子程序并执行比较。这里一个有用的技巧是“标准”排序运算符 - &lt;=&gt;cmp - 返回零,因此可以使用 || 进行快捷方式。

    所以:

    sub compare_numbers {
       my @a = $a =~ m/(\d+)/g;
       my @b = $b =~ m/(\d+)/g; 
       return ( $a[0] <=> $b[0] 
             || $a[1] <=> $b[1] )
    }
    

    如果第一个比较为零,则计算第二个比较。

    或者你可以计算中间值:

    sub compare_numbers {
       my @a = $a =~ m/(\d+)/g;
       my @b = $b =~ m/(\d+)/g; 
       return ( ($a[1] - $a[0] / 2 + $a[0]) <=> ($b[1] - $b[0] / 2 + $b[0])
    }
    

    您可以以与上述类似的方式使用其中任何一个:

    sort compare_numbers keys %hash 
    

    【讨论】:

    • 感谢您的回答。但我无法理解子程序部分。它是如何工作的?我们如何实现我的输入?您的帮助将极大地帮助我理解这个过程。
    • 排序 compare_numbers 键 %hash
    • 使用s///r 删除除您想要的数字之外的所有内容有点笨拙
    • @Borodin 但很高兴有/r !!
    • @G.Cito:这是我同意的更好的增强功能之一。但请记住,它仅在 Perl v5.14 或更高版本中可用。那里有大量的 v5.8 安装
    【解决方案4】:

    您可以使用“自然”对值进行排序的 CPAN 模块之一(例如您可以使用 Sort::Naturally)。

    不过,这会隐藏正在发生的事情。所以出于教育目的,我喜欢@Sobrique@Borodin@Quentin 的解释。

    use Sort::Naturally;
    my @nsorted ;
    @nsorted = nsort ( <DATA> ) ;
    print @nsorted;
    
    __DATA__
    (293 to 296)
    (3118 to 3121)
    (330 to 333)
    (2126 to 2129)
    (1999 to 2002)
    (2138 to 2141)
    (771 to 774)
    (2016 to 2019)
    (888 to 891)
    (3102 to 3105)
    

    输出:

    (293 to 296)
    (330 to 333)
    (771 to 774)
    (888 to 891)
    (1999 to 2002)
    (2016 to 2019)
    (2126 to 2129)
    (2138 to 2141)
    (3102 to 3105)
    (3118 to 3121) 
    

    【讨论】:

    • 我不知道,但也许很多排序可以通过 naturally ... 用“技巧和优雅”来完成。
    【解决方案5】:

    问题是像(293 to 296) 这样的字符串没有数值。如果您按照应有的方式设置了use warnings 'all',您会看到多个警告,例如

    参数“(293 to 296)”在排序中不是数字

    并且每个键的计算结果都为零,因此就sort 而言,它们都是相等的

    因此,您必须从每个值中提取一个数字以用于数字排序。我会抓住每个范围的下限并按此排序。

    use strict;
    use warnings 'all';
    use feature 'say';
    
    my %hash = (
        '(293 to 296)'   => 2,
        '(3118 to 3121)' => 2,
        '(330 to 333)'   => 2,
        '(2126 to 2129)' => 2,
        '(1999 to 2002)' => 2,
        '(2138 to 2141)' => 9,
        '(771 to 774)'   => 4,
        '(2016 to 2019)' => 1,
        '(888 to 891)'   => 5,
        '(3102 to 3105)' => 1,
    );
    
    my @keys = sort {
      my ($aa, $bb) = map /(\d+)/, $a, $b;
      $aa <=> $bb;
    } keys %hash;
    
    say for @keys;
    

    输出

    (293 to 296)
    (330 to 333)
    (771 to 774)
    (888 to 891)
    (1999 to 2002)
    (2016 to 2019)
    (2126 to 2129)
    (2138 to 2141)
    (3102 to 3105)
    (3118 to 3121)
    

    使用 List::MoreUtilsList::UtilsBy 中的 nsort_by 函数可以使这更加简洁

    use List::MoreUtils 'nsort_by';
    
    say for nsort_by { /(\d+)/ and $1 } keys %hash;
    

    这段代码的输出和上面的一样

    【讨论】:

    • 我开始记得喜欢and - 非常整洁 ++ 你能解释一下m//and 是如何合作的吗?
    • OT:我希望List::UtilsBy 中的一些选择函数能移到List::UtilsSort::Naturally(见我的回答)按照它在锡上说的做,但如果 nsort 例程与 .Dual Life 模块之一一起出现在盒子里会很好。
    • and 运算符用作快捷方式。如果为 true,则返回左侧操作数,否则返回右侧操作数。在这种情况下,/(\d+)/ 应始终为 true,因此and 返回捕获的字符串。如果碰巧有一个不包含十进制数字的键值,那么 and 将评估为空字符串。使用/(\d+)/ ? $1 : 0 可能会更好,这样结果总是一个数字,但您必须决定如何处理格式错误的键
    猜你喜欢
    • 2014-04-13
    • 1970-01-01
    • 2017-07-27
    • 2016-10-28
    • 1970-01-01
    • 1970-01-01
    • 2014-08-03
    • 2021-02-22
    • 2019-03-28
    相关资源
    最近更新 更多