Perl 陣列中的獨特值
List::MoreUtils
雖然視情況而定,但最簡單的方法多半是使 用 List::MoreUtils CPAN 模組中的 uniq 函式。
use List::MoreUtils qw(uniq); my @words = qw(foo bar baz foo zorg baz); my @unique_words = uniq @words;
A full example is this:
以下是較完整的範例:
use strict; use warnings; use 5.010; use List::MoreUtils qw(uniq); use Data::Dumper qw(Dumper); my @words = qw(foo bar baz foo zorg baz); my @unique_words = uniq @words; say Dumper \@unique_words;
輸出的結果為:
$VAR1 = [ 'foo', 'bar', 'baz', 'zorg' ];
此模組另外也提供了 distinct 函式,但與 uniq 作用完全相同,只是 名稱不同。
而此模組必需先自 CPAN 安裝之後才能使用。
手作 uniq 函式
如果因故無法安裝上述模組,或認為載入此模組太耗資源,那麼以下的算式基本上也可以達到 一樣的功能:
my @unique = do { my %seen; grep { !$seen{$_}++ } @data };
不過,對於不熟悉的人來說,這列算式可能會看得一頭霧水,所以最好還是定義成函式 uniq, 並且在源碼各處直接使用函式:
use strict; use warnings; use 5.010; use Data::Dumper qw(Dumper); my @words = qw(foo bar baz foo zorg baz); my @unique = uniq( @words ); say Dumper \@unique_words; sub uniq { my %seen; return grep { !$seen{$_}++ } @_; }
抽絲剝繭
即然把範例程式丟出來了,便不能不解釋一下。先來看看較簡易的版本:
my @unique; my %seen; foreach my $value (@words) { if (! $seen{$value}) { push @unique, $value; $seen{$value} = 1; } }
此例中,我們採用普通的 foreach 迴圈,一一走過原陣列各值。其中另外引入了小幫手 %seen 雜湊。雜湊有個不錯的特性:每個雜湊鍵都是 獨特的。
一開始時,雜湊是空的,在迴圈內碰到第一個 "foo" 時,$seen{"foo"}尚未存在, 因此其值為 undef,在 Perl 語言中表意為布林偽值。在範例程式用以表示「尚未 看過 "foo"」這樣的意義。接著程式將它推入另一個陣列 @unique陣列,此陣列的 用處,便是收藏所有獨特值。
同時,$seen{"foo"} 的值也被賦為 1。不過,任何能被 Perl 語言當成是布林真值的值,都可以使用。
於是,如果之後又碰到曾經出現過的值,那麼它就會出現在 %seen 雜湊鍵中,並且對應到一個布林真值, 也就是說 if 中的條件就會變成失敗,之後的 push 就不會執行,這個曾經出現的值就不會 再次被推入要回傳的陣列當中。
精練簡化
首先,我們將賦值算式 $seen{$value} = 1; 改為「後遞增」算式 $seen{$value}++。這並不會 改變整體的行為,因為所有正數都會被當成是布林真值,但這麼做,可以讓我們把此運算放在 if 的測試條件當中。 此處的重點在於它為「後遞增」算符,而不是「前遞增」算符,也就是讓遞增運算在取值之後發生。這個運算式第一次被取值時 會是真,而之後都會是偽。
my @unique; my %seen; foreach my $value (@data) { if (! $seen{$value}++ ) { push @unique, $value; } }
這樣稍微簡短了一些,但還可以再改進。
以 grep 來過濾重複出現的值
在 Perl 中的 grep 函式,形式上比 Unix grep 指令更為通用。
它基本上就是個濾器。 在其右方,要提供一個陣列與一個程式區塊,並在其區塊中填上一條運算式。grep函式會一一走過陣列中的內容, 放入 $_ 變數,也就是 Perl 的預設純量變數, 然後執行區塊。如果區塊的執行結果為真,則此值就會通過濾器,跑到左邊;如果區塊執行結果是偽,那就會被擋下來。
於此,我們被能造出了如下的運算式:
my %seen; my @unique = grep { !$seen{$_}++ } @words;
用 'do' 或 'sub' 包裝一下
最後還有一件事,是把那些述式用 do 區塊給包起來:
my @unique = do { my %seen; grep { !$seen{$_}++ } @words };
或是,包成函式,並取個好名字:
sub uniq { my %seen; return grep { !$seen{$_}++ } @_; }
手作函式,第二回合
Parakash Kailasa 薦議了一版更加簡短的 uniq 函式程式碼,限用於 perl 5.14 以上版本, 並不保證值的次序與原陣列相同。
my @unique = keys { map { $_ => 1 } @data };
函式的形式:
my @unique = uniq(@data); sub uniq { keys { map { $_ => 1 } @_ } };
讓我們來拆解看看:
map 的語法與 grep十分類似:都需要一個程式區塊與一個陣列(或是串列)。陣列內容會被一一迭代過, 執行區塊,並將區塊執行結果傳向左側。
In our case, for every value in the array it will pass the value itself followed by the number 1. Remember =>, aka. fat comma, is just a comma. Assuming @data has ('a', 'b', 'a') in it, this expression will return ('a', 1, 'b', 1, 'a', 1).
在此例中,每一個陣列的值,都會變成它自已跟上數字 1。記好了,=> 這 個符號,又稱「肥逗號」,就是個「逗號」。若 @data 的內容為 ('a', 'b', 'c'),那最後運算下來 會變成 ('a', 1, 'b', 1, 'a', 1)。
map { $_ => 1 } @data
如果,將這樣的運算結果被存予雜湊中,那麼雜湊鍵就會與原資料相同,而雜湊值全都是 1。試看看:
use strict; use warnings; use Data::Dumper; my @data = qw(a b a); my %h = map { $_ => 1 } @data; print Dumper \%h;
輸出結果為:
$VAR1 = { 'a' => 1, 'b' => 1 };
或是,如果把上述結果用大括號包起來的話,就可以得到匿名雜湊參照了。
{ map { $_ => 1 } @data }
接著把各部件組合起來看看結果:
use strict; use warnings; use Data::Dumper; my @data = qw(a b a); my $hr = { map { $_ => 1 } @data }; print Dumper $hr;
這段程式的輸出與前一程式相同,只是在雜湊內容的鍵值次序可能略有不同。
最後,在 perl 5.14 以上的版本,keys 函式可以直接處理雜湊參照。於是我們可以寫:
my @unique = keys { map { $_ => 1 } @data };
便能取得 @data 中的所有獨特值。
習題
輸入以下檔案後,輸入其內容的獨特值:
input.txt:
foo Bar bar first second Foo foo another foo
輸出應為:
foo Bar bar first second Foo another
習題二
同樣是濾掉重複的單字,但這次字母大小寫視為相同。
輸出應為:
foo Bar first second another

Published on 2013-04-24