如何在Perl中从数组中删除重复项?

173

我在 Perl 中有一个数组:

my @my_array = ("one","two","three","two","three");

如何从数组中删除重复项?

11个回答

179

您可以像 perlfaq4 中所演示的那样进行操作:

sub uniq {
    my %seen;
    grep !$seen{$_}++, @_;
}

my @array = qw(one two three two three);
my @filtered = uniq(@array);

print "@filtered\n";

输出:

one two three

如果您想使用模块,请尝试来自List::MoreUtilsuniq函数。


31
好的,我会尽力进行翻译。以下是需要翻译的内容:请不要在示例中使用 $a 或 $b,因为它们是 sort() 的魔术全局变量。 - szabgab
2
@ephemient 是的,但如果你在这个函数中添加排序,那么它会胜过$::a和$::b,不是吗? - vol7ron
2
如果是这样的话,那么sort使用非局部变量是一个极其糟糕的设计决策。@szabgab - Brian Vandenberg
7
@BrianVandenberg 欢迎来到1987年的世界,当时创建了这个东西,几乎100%兼容Perl,因此它无法被淘汰。 - szabgab
20
“sub uniq { my %seen; grep !$seen{$}++, @ }”是一种更好的实现方式,因为它不需要额外的开销就可以保留顺序。或者更好的办法是使用List::MoreUtils中的函数。” - ikegami
显示剩余4条评论

128
Perl文档自带一个很好的FAQ集合。你提出的问题经常被问到:
% perldoc -q duplicate

以下是从上述命令输出中复制并粘贴的答案:


在 /usr/local/lib/perl5/5.10.0/pods/perlfaq4.pod 中找到

如何从列表或数组中删除重复元素?(由 brian d foy 提供)

使用哈希表。当您想到“唯一”或“重复”这些词时,请思考“哈希键”。

如果您不关心元素的顺序,您可以创建哈希表,然后提取键。重要的不是如何创建哈希表,而是使用“keys”获取唯一元素。

   my %hash   = map { $_, 1 } @array;
   # or a hash slice: @hash{ @array } = ();
   # or a foreach: $hash{$_} = 1 foreach ( @array );

   my @unique = keys %hash;

如果你想使用一个模块,可以尝试使用"List::MoreUtils"中的"uniq"函数。在列表上下文中,它返回唯一的元素,并保留其在列表中的顺序。在标量上下文中,它返回唯一元素的数量。

   use List::MoreUtils qw(uniq);

   my @unique = uniq( 1, 2, 3, 4, 4, 5, 6, 5, 7 ); # 1,2,3,4,5,6,7
   my $unique = uniq( 1, 2, 3, 4, 4, 5, 6, 5, 7 ); # 7

你也可以遍历每个元素并跳过已经看过的元素。使用哈希表进行跟踪。第一次循环遇到一个元素时,在 %Seen 中没有该元素的键。"next" 语句创建了该键,并立即使用其值(即 "undef"),因此循环继续执行 "push" 并增加该键的值。下一次循环遇到相同的元素时,它的键存在于哈希表中,并且该键的值为 true(因为它不是 0 或 "undef"),因此 "next" 跳过该迭代,循环进入下一个元素。

   my @unique = ();
   my %seen   = ();

   foreach my $elem ( @array )
   {
     next if $seen{ $elem }++;
     push @unique, $elem;
   }

您可以使用grep更简洁地编写此内容,它执行相同的操作。

   my %seen = ();
   my @unique = grep { ! $seen{ $_ }++ } @array;

1
http://perldoc.perl.org/perlfaq4.html#How-can-I-remove-duplicate-elements-from-a-list-or-array%3F - szabgab
19
约翰正在我的答案中窃取我的声誉! - brian d foy
5
我认为如果你实际查找问题,应该得到额外的加分。 - Brad Gilbert
2
我喜欢最佳答案是95%的复制粘贴和3个OC句子。为了非常清楚,这确实是最佳答案;我只是觉得这个事实很有趣。 - Parthian Shot

71

从 CPAN 安装 List::MoreUtils

然后在你的代码中使用:

use strict;
use warnings;
use List::MoreUtils qw(uniq);

my @dup_list = qw(1 1 1 2 3 4 4);

my @uniq_list = uniq(@dup_list);

6
List::MoreUtils没有随Perl一起捆绑发布,这会影响使用它的项目的可移植性 :( (至少我是不会用它的)。 - yPhil
3
“@Ranguard: @dup_list 应该放在 uniq 函数中,而不是放在 @dups 中。” - incutonez
@yassinphilip CPAN 是使 Perl 强大和伟大的因素之一。如果您只基于核心模块编写项目,那么您将对代码设置巨大限制,并可能编写质量不佳的代码,试图做一些模块更好地完成的工作,只是为了避免使用它们。此外,仅使用核心模块并不能保证任何东西,因为不同的 Perl 版本可以从分发中添加或删除核心模块,因此可移植性仍然取决于此。 - Francisco Zarabozo
1
从 Perl v5.26.0 开始,List::Util 已经有了 uniq,因此不再需要使用 MoreUtils。 - Sundeep

24

我通常使用的方法是:

my %unique = ();
foreach my $item (@myarray)
{
    $unique{$item} ++;
}
my @myuniquearray = keys %unique;

如果您使用哈希表并将项目添加到哈希表中,则还可以知道列表中每个项目出现的次数。


3
这种方法的不足之处在于,如果您需要保留原始顺序,则无法实现。 - Nathan Fellman
最好使用slices而不是foreach循环:@unique{@myarray}=() - Onlyjob

11

可以用一个简单的Perl一行命令完成。

my @in=qw(1 3 4  6 2 4  3 2 6  3 2 3 4 4 3 2 5 5 32 3); #Sample data 
my @out=keys %{{ map{$_=>1}@in}}; # Perform PFM
print join ' ', sort{$a<=>$b} @out;# Print data back out sorted and in order.

PFM块的作用如下:

@in中的数据被输入到map中。 map构建一个匿名哈希表。从哈希表中提取keys并输入到@out中。


9

方法一:使用哈希表

逻辑:哈希表只能有唯一的键,因此遍历数组,将任何值分配给数组的每个元素,并将元素作为哈希表的键。返回哈希表的键,即为您的唯一数组。

my @unique = keys {map {$_ => 1} @array};

方法二:为了可重用性而扩展方法一

如果我们需要在代码中多次使用此功能,则最好制作一个子程序。

sub get_unique {
    my %seen;
    grep !$seen{$_}++, @_;
}
my @unique = get_unique(@array);

方法三:使用模块List::MoreUtils

use List::MoreUtils qw(uniq);
my @unique = uniq(@array);

8
变量@array是包含重复元素的列表。
%seen=();
@unique = grep { ! $seen{$_} ++ } @array;

4

那个最后的翻译已经很不错了,我只是想稍微修改一下:

my @arr;
my @uniqarr;

foreach my $var ( @arr ){
  if ( ! grep( /$var/, @uniqarr ) ){
     push( @uniqarr, $var );
  }
}

我认为这可能是最易读的做法。


1

之前的回答已经总结了可能实现此任务的方式。

然而,我建议对于那些不关心重复计数但关心顺序的人进行修改。

my @record = qw( yeah I mean uh right right uh yeah so well right I maybe );
my %record;
print grep !$record{$_} && ++$record{$_}, @record;

请注意,先前建议的grep !$seen {$_} ++...在否定之前会增加$seen {$_},因此无论是否已经存在于%seen中,都会发生增量。然而,上述方法在$record{$_}为真时短路,使得只听过一次的内容从%record中删除。

你也可以使用以下荒谬的方法,利用自动创建和哈希键的存在:

...
grep !(exists $record{$_} || undef $record{$_}), @record;

然而,这可能会导致一些混淆。

如果您不关心顺序或重复计数,则可以使用哈希切片和我刚提到的技巧进行另一种hack:

...
undef @record{@record};
keys %record; # your record, now probably scrambled but at least deduped

对于那些进行比较的人:sub uniq{ my %seen; undef @seen{@_}; keys %seen; } 真是太好了。 - stevesliva

0

试一下这个,看起来uniq函数需要一个经过排序的列表才能正常工作。

use strict;

# Helper function to remove duplicates in a list.
sub uniq {
  my %seen;
  grep !$seen{$_}++, @_;
}

my @teststrings = ("one", "two", "three", "one");

my @filtered = uniq @teststrings;
print "uniq: @filtered\n";
my @sorted = sort @teststrings;
print "sort: @sorted\n";
my @sortedfiltered = uniq sort @teststrings;
print "uniq sort : @sortedfiltered\n";

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