如何按照哈希值中的一个值对哈希引用数组进行排序?

6

首先,请原谅我生疏的Perl语言。我正在尝试修改Bugzilla的“whine.pl”,以生成按严重程度排序的漏洞列表。

因此,它会给我一个哈希引用数组。每个哈希包含有关特定漏洞的大量信息(id、受让人、严重程度等)。我想按严重性对数组进行排序。最好的方法是什么?

我想到了几种可能性。其中之一是创建五个数组(每个严重程度一个),然后循环遍历该数组,并将哈希引用推入相应的严重程度级别数组中。完成后,我可以重新组装它们,并用排序后的数组替换原始数组。

我的朋友提出的另一种方法是将存储在哈希中的严重程度级别(以文本形式存储)分配给一些数字,并进行比较。也许像这样:

sub getVal {
    my $entry = $_[0];
    %lookup = ( "critical" => 0, ... );
    return $lookup(entry("bug_severity"));
}
@sorted = sort { getVal($a) <=> getVal($b) } @unsorted;

顺便说一下,你没有一个哈希数组,而是一个匿名哈希的引用数组。 - Sinan Ünür
谢谢,Sinan。我已经修正了标题。 - Allan Anderson
1
@grahzny:感谢你引发了一场很棒的讨论。今天一直都挺安静的 :) - Ether
1
现在,我正在查看 whine.pl 的源代码,它似乎是从 SQL 数据库中获取这些内容的。使用适当的 SELECT 查询从数据库中获取已经排序好的 bug 不是更好吗? - Sinan Ünür
你说得完全正确,Sinan;但是,它使用的查询是存储在Bugzilla数据库中的用户创建的“保存搜索”。我没有看到在UI中实现这一点的方法。也许我们可以将其添加到Bugzilla中,但我想先从使用这个辅助脚本开始。 - Allan Anderson
4个回答

7
为了避免不必要地多次调用getVal函数,您可以使用“装饰,排序,去饰”的方法。装饰是为了获取实际排序所需的信息:
my @decorated = map { [ $_, getVal($_) ] } @unsorted;

然后对装饰好的列表进行排序:

my @sortedDecorate = sort { $a->[1] <=> $b->[1] } @decorated;

然后获取原始信息(去除装饰):
my @sorted = map { $_->[0] } @sortedDecorate;

或者更加Perl风格的做法:
@sorted = map { $_->[0] }
          sort { $a->[1] <=> $b->[1] }
          map { [ $_, getVal($_) ] } @unsorted;

有趣的想法。我喜欢。(但不要用最后一种方式,Perl已经够难理解了!) - tster
4
这确实是施瓦茨变换。虽然这个名字是以我为名,但并非我所起。 - Randal Schwartz
我记得你在我参加的Perl课程中提到过这件事,Randal。令我感兴趣的是,社区选择采用这个术语而不是一般的decorate-sort-undecorate。 :) - jamessan
@Sinan,我不是说不要这样做。我是说不要把它变成一行代码,因为这会让它难以理解。将其分成三行易于理解和阅读的代码比一行代码更有效率。 - tster

4
您可以使用Schwartzian Transform
my @sorted = map  { $_->[1] }
             sort { $a->[0] <=> $b->[0] }
             map  { [ $lookup{$_->{bug_severity}, $_ ] } 
             @unsorted;

解释:

map  { [ $lookup{$_->{bug_severity}, $_ ] } @unsorted;

将每个 bug 映射到一个数组引用,其第一个元素是查找表中的数字 bug 严重性。使用 Schwartzian Transform,你只需要在 @unsorted 中每个 bug 查找一次数值

然后,

sort { $a->[0] <=> $b->[0] }

按第一个元素对数组进行排序。最后,

@sorted = map  { $_->[1] }

sort返回的数组中提取原始缺陷。

getval只进行哈希查找时,实际上不需要它。

用于自动生成有效排序器的 CPAN 模块 Sort::Maker 非常出色:

use strict; use warnings;

use Sort::Maker;

my @bugs = (
    { name => 'bar', bug_severity => 'severe' },
    { name => 'baz', bug_severity => 'noncritical' },
    { name => 'foo', bug_severity => 'critical' },
);

my $sorter = make_sorter('ST',
    name      => 'severity_sorter',
    init_code => 'my %lookup = (
                     critical => 0,
                     severe => 1,
                     noncritical => -1 );',
    number    => [ code => '$lookup{$_->{bug_severity}}' ],
);

use Data::Dumper;
print Dumper $_ for severity_sorter( @bugs );

输出:

$VAR1 = {
          'name' => 'baz',
          'bug_severity' => 'noncritical'
        };
$VAR1 = {
          'name' => 'foo',
          'bug_severity' => 'critical'
        };
$VAR1 = {
          'name' => 'bar',
          'bug_severity' => 'severe'
        };

请注意,使用简单方法时需要查找的次数取决于@unsorted中的元素数量。我们可以使用以下简单程序进行计数:

#!/usr/bin/perl

use strict;
use warnings;

my ($n_elements) = @ARGV;

my @keys = qw(a b c);
my %lookup = map { $keys[$_-1] => $_ } 1 .. @keys;

my @unsorted = map { $keys[rand 3] } 1 .. $n_elements;

my $n_lookups;

my @sorted = sort {
    $n_lookups += 2;
    $lookup{$a} <=> $lookup{$b}
} @unsorted;

print "It took $n_lookups lookups to sort $n_elements elements\n";

输出:

C:\Temp> tzt 10
对10个元素进行排序需要38次查找
C:\Temp> tzt 100 对100个元素进行排序需要978次查找
C:\Temp> tzt 1000 对1000个元素进行排序需要10916次查找
C:\Temp> tzt 10000 对10000个元素进行排序需要113000次查找

因此,人们需要更多的信息来决定是使用朴素排序还是使用Schwartzian变换作为适当的解决方案。

这里有一个简单的基准测试,似乎支持@Ether的观点:

#!/usr/bin/perl

use strict;
use warnings;

use Benchmark qw( cmpthese );

my ($n_elements) = @ARGV;

my @keys = qw(foo bar baz);
my %lookup = map { $keys[$_] => $_ } 0 .. $#keys;

my @unsorted = map { {v => $keys[rand 3]} } 1 .. $n_elements;

cmpthese(-1, {
    naive => sub {
        my @sorted = sort {
            $lookup{$a->{v}} <=> $lookup{$b->{v}}
        } @unsorted;
    },
    schwartzian => sub {
        my @sorted = map  { $_->[1] }
                     sort { $a->[0] <=> $b->[0] }
                     map  { [$lookup{$_->{v}}, $_] }
                     @unsorted;
    }
});

输出:

C:\Temp> tzt 10
               率 schwartzian       naive
schwartzian 18842/s          --        -29%
naive       26357/s         40%          --
C:\Temp> tzt 100 率 naive schwartzian naive 1365/s -- -11% schwartzian 1532/s 12% --
C:\Temp> tzt 1000 率 naive schwartzian naive 121/s -- -11% schwartzian 135/s 12% --

1
Jamessan已经发布了这个内容,如果不付出大量的努力,几乎无法理解。 - tster
2
又一个解释得很好的例子 :) 感谢您提供详细信息。我有很多东西可以尝试。 - Allan Anderson

3

我很喜欢你提出的解决方案:

my %sevs = (critical => 0, high => 1, ...);
my @sorted = sort { $sevs{$a->{bug_severity}} <=> $sevs{$b->{bug_severity}} } @unsorted

1
谢谢,tster;听到我走在正确的道路上很不错,看到它以不同的方式表达也很有用。 - Allan Anderson
其他的解决方案都很有教育意义;我认为这个简单的方案是我需求的最佳选择。 - Allan Anderson

0
您可以使用查找表来确定Bugzilla严重性的排序,例如(使用示例数据说明):
use strict; use warnings;
use Data::Dumper;

my @bugInfo = (
                { id => 1,
                  assignee => 'Bob',
                  severity => 'HIGH'
                },
                { id => 2,
                  assignee => 'Anna',
                  severity => 'LOW'
                },
                { id => 3,
                  assignee => 'Carl',
                  severity => 'EXTREME'
                },
              );
my %severity_ordering = (
    EXTREME => 0,
    HIGH => 1,
    MEDIUM => 2,
    LOW => 3,
);
sub byseverity
{
    $severity_ordering{$a->{severity}} <=> $severity_ordering{$b->{severity}}
}

my @sortedBugs = sort byseverity @bugInfo;
print Dumper(\@sortedBugs);

产生:

$VAR1 = [
          {
            'assignee' => 'Carl',
            'id' => 3,
            'severity' => 'EXTREME'
          },
          {
            'assignee' => 'Bob',
            'id' => 1,
            'severity' => 'HIGH'
          },
          {
            'assignee' => 'Anna',
            'id' => 2,
            'severity' => 'LOW'
          }
        ];

这基本上就是你在问题中发布的内容(哦,我没有仔细阅读它),以及tster所说的。所以,是的,我同意这是最好的解决方案。 :) - Ether
1
我很感激你把我的模糊想法具体化,谢谢你提供详细的示例,这节省了我一些“啊,Perl又是怎么做的?”的时间。 - Allan Anderson
表格中每个条目都有一个查找,但是1.查找表非常短(例如bugzilla只有约5个条目),而且2.在Schwartzian变换中,您必须多次处理输入数据中的每个条目,在这种情况下,大致上具有相当的费用。除非我错过了什么,否则只有在输入数据相对较小且与用于确定排序顺序的表格相比较小时,变换才会付出回报,并且您还必须考虑代码的复杂性(简单的代码比复杂的代码更容易调试)。 - Ether
@Sinan,好的,说得对。然而,使用转换的真正收益在于执行排序方法n log(n)次的计算成本是否超过了执行两个映射调用所需的时间。UnixReview文章中的示例涉及拆分;在OP的示例中,数据已经很好地可解析为干净的数据结构。因此,在这种情况下,我认为即使要处理大量错误列表,转换也不会给我们带来任何好处。 - Ether

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接