如何从仅出现一次的列表中获取值?

时间:2014-04-11 08:12:23

标签: regex perl

我在互联网上看到了这个问题。获取列表中仅存在一次的唯一数字,而列表中存在两次其他数字。数据很大并且包含大约一百万个未分类的数字,并且可能包含随机顺序的负数,其中所有数字都出现两次,除了一个只出现一次的数字。

my @array = (1,1,2,3,3,4,4)

输出:

2

列表中只有两个不重复。我尝试了我的解决方案。

my $unique;
$unique ^= $_ for(@array);
say $unique;

它不会对负数工作但速度很快。

我尝试了一个哈希,其中key是数字,value是它在列表中出现的次数。反转哈希值,然后打印值为1作为键,因为所有其他数字的值都是2,因为它们出现两次。哈希解决方案很慢,输入数百万的大数字,但适用于负数。

我尝试了将整个列表与制表符合并然后使用

的正则表达方式
my $combined = join " ", @array;
$combined !~ (\d+).*$1;
say $1;

但我只得到列表的最后一个数字

有快速的方法吗?有没有使用正则表达式的想法?

编辑:重新标记标题以获得更好的答案

3 个答案:

答案 0 :(得分:4)

处理此问题的标准方法是将其全部放入哈希值。

use v5.10;
use strict;
use warnings;

my @nums = (2..500_000, 500_002..1_000_000, 0..1_000_001);

my %count;
for (@nums) {
    $count{$_}++
}

for (keys %count) {
    say $_ if $count{$_} == 1;
}

但是,它很慢。

然后我想也许我可以避免不得不遍历哈希来找到单身......

my @nums = (2..500_000, 500_002..1_000_000, 0..1_000_001);
my %uniqs;
my %dups;
for (@nums) {
    if( $uniqs{$_} ) {
        delete $uniqs{$_};
        $dups{$_} = 1;
    }
    elsif( !$dups{$_} ) {
        $uniqs{$_} = 1;
    }
}

print join ", ", keys %uniqs;

但那甚至更慢。

这是我提出的最快的事情,大约需要一半的时间。

use v5.10;
use strict;
use warnings;

my @nums = (2..500_000, 500_002..1_000_000, 0..1_000_001);
@nums = sort @nums;
say $nums[0] if $nums[0] != $nums[1];
for (1..$#nums-1) {
    my($prev, $this, $next) = @nums[$_-1, $_, $_+1];
    say $this if $prev != $this && $next != $this;
}
say $nums[-1] if $nums[-1] != $nums[-2];

通过对列表进行排序,您可以遍历它并检查给定条目的邻居是否重复。必须要小心第一个和最后一个元素。我把他们的检查放在循环之外,以避免每次迭代都运行一个特殊情况。

因为sort是O(nlogn),随着数字列表变大,这个解决方案最终会慢于基于散列的解决方案,但在此之前你可能会耗尽内存。

最后,如果此列表很大,您应该考虑将其存储在数据库的磁盘上。然后,您可以避免使用内存,让数据库有效地完成工作。

答案 1 :(得分:4)

这似乎很快:

use v5.10; use strict; use warnings;

sub there_can_be_only_one {
    my @counts;
    $counts[ $_>=0 ? 2*$_ : (-2*$_)-1 ]++ for @{$_[0]};
    $counts[ $_>=0 ? 2*$_ : (-2*$_)-1 ]==1 and return $_ for @{$_[0]};
    return;
}

my @array = (1,1,-4,-4,2,3,-1,3,4,-1,4);
say there_can_be_only_one(\@array);

它基本上是散列技术的变体,但使用数组而不是散列。因为我们需要处理负数,所以我们不能在@counts数组中不加修改地使用它们。当然,负索引在Perl中起作用,但它们会覆盖我们的正数索引数据。失败。

所以我们使用类似于两个补码的东西。我们将数组中的正数存储为2*$_,将负数存储为(-2*$_)-1。那就是:

Integer:   ... -3  -2  -1   0   1   2   3 ...
Stored as: ...  5   3   1   0   2   4   6 ...

因为这个解决方案不依赖于对列表进行排序,而只是对它进行两次传递(好吧,平均来说,一次半通过),它在 O(n)处执行与Schwern的 O(n log n)解决方案形成鲜明对比。因此,对于较大的列表(几百万个整数)应该明显更快。这是我(相当低功耗)上网本的快速比较:

use v5.10; use strict; use warnings;
use Benchmark qw(timethese);
use Time::Limit '60';

sub tobyink {
    my @counts;
    $counts[ $_>=0 ? 2*$_ : (-2*$_)-1 ]++ for @{$_[0]};
    $counts[ $_>=0 ? 2*$_ : (-2*$_)-1 ]==1 and return $_ for @{$_[0]};
    return;
}

sub schwern {
    my @nums = sort @{$_[0]};
    return $nums[0] if $nums[0] != $nums[1];
    for (1..$#nums-1) {
         my($prev, $this, $next) = @nums[$_-1, $_, $_+1];
         return $this if $prev != $this && $next != $this;
    }
    return $nums[-1] if $nums[-1] != $nums[-2];
}

my @input = (
    1..2_000_000,  # 1_000_001 only appears once
    1..1_000_000, 1_000_002..2_000_000,
);

timethese(1, {
    tobyink  => sub { tobyink(\@input) },
    schwern  => sub { schwern(\@input) },
});

__END__
Benchmark: timing 1 iterations of schwern, tobyink...
schwern: 11 wallclock secs ( 8.72 usr +  0.92 sys =  9.64 CPU) @  0.10/s (n=1)
         (warning: too few iterations for a reliable count)
tobyink:  5 wallclock secs ( 5.01 usr +  0.08 sys =  5.09 CPU) @  0.20/s (n=1)
         (warning: too few iterations for a reliable count)

更新:在我的初步回答中,我错过了没有数字会出现两次以上的详细信息。我假设一些数字有可能出现三次或更多次。使用这个额外的细节,我们可以更快:

sub there_can_be_only_one {
    my $tmp;
    $tmp ^= $_>=0 ? 2*$_ : (-2*$_)-1 for @{$_[0]};
    $tmp%2 ? ($tmp+1)/-2 : $tmp/2;
}

say there_can_be_only_one(\@array);

这比我最初的答案快了大约30%。

答案 2 :(得分:2)

它不适用于负数但速度很快。

实际上,如果你想让xor处理负数,你只需要对它们进行字符串化:

my @array = (-10..-7,-5..10,-10..10);

my $unique;
$unique ^= "$_" for @array;
say $unique;

输出

-6

做一些快速基准测试:

Benchmark: timing 100 iterations of schwern, there_can_be_only_one, tobyink, xor_string...
   schwern: 323 wallclock secs (312.42 usr +  7.08 sys = 319.51 CPU) @  0.31/s (n=100)
there_can_be_only_one: 114 wallclock secs (113.49 usr +  0.02 sys = 113.51 CPU) @  0.88/s (n=100)
   tobyink: 177 wallclock secs (176.76 usr +  0.14 sys = 176.90 CPU) @  0.57/s (n=100)
xor_string: 98 wallclock secs (97.05 usr +  0.00 sys = 97.05 CPU) @  1.03/s (n=100)

显示xor-ing字符串的速度比将数学平移与正数相差15%。

推论 - 排序列表怎么样?

Schwern的解决方案带来了一个有趣的推论。他对列表进行了排序,然后搜索了所有独特的元素。

如果我们使用额外的信息,即在一对双重按钮中只有1个单例,我们可以通过进行成对比较来快速简化搜索,从而将我们的比较减少到4倍。

但是,我们可以通过二进制搜索做得更好。如果我们在已知匹配对之间的屏障上分隔列表,那么剩下的两个列表中的任何一个都包含我们的单例。我做了一些这个解决方案的基准测试,它比其他任何事情都快了几个数量级(当然):

use strict;
use warnings;
use Benchmark qw(timethese);

sub binary_search {
    my $nums = $_[0];

    my $min = 0;
    my $max = $#$nums;
    while ($min < $max) {
        my $half = ($max - $min) / 2; # should  always be an integer
        my ($prev, $this, $next) = ($min+$half-1) .. ($min+$half+1);

        if ($nums->[$prev] == $nums->[$this]) {
            if ($half % 2) {         # 0 0 1 1 2 2 3 ( half = 3 )
                $min = $next;
            } else {                 # 0 1 1 2 2 ( half = 2 )
                $max = $prev - 1;
            }
        } elsif ($nums->[$this] == $nums->[$next]) { 
            if ($half % 2) {         # 0 1 1 2 2 3 3 ( half = 3 )
                $max = $prev;
            } else {                 # 0 0 1 1 2 ( half = 2 )
                $min = $next + 1;          
            }
        } else {
            $max = $min = $this;
        }
    }

    return $nums->[$min];
}

sub xor_string {
    my $tmp;
    $tmp ^= "$_" for @{$_[0]};
}

sub brute {
    my $nums = $_[0];

    return $nums->[0] if $nums->[0] != $nums->[1];
    for (1..$#$nums-1) {
        my($prev, $this, $next) = @$nums[$_-1, $_, $_+1];
        return $this if $prev != $this && $next != $this;
    }
    return $nums->[-1] if $nums->[-1] != $nums->[-2];
}

sub pairwise_search {
    my $nums = $_[0];
    for (my $i = 0; $i <= $#$nums; $i += 2) {
        if ($nums->[$i] != $nums->[$i+1]) {
            return $nums->[$i];
        }
    }
}

# Note: this test data is very specific and is intended to take near the maximum
# number of steps for a binary search while shortcutting halfway for brute force
# and pairwise
my @input = sort {$a <=> $b} (0..500_003, 500_005..1_000_000, 0..1_000_000);
#my @input = sort {$a <=> $b} (0..499_996, 499_998..1_000_000, 0..1_000_000);

timethese(1000, {
    brute  => sub { brute(\@input) },
    pairwise  => sub { pairwise_search(\@input) },
    xor_string => sub { xor_string(\@input) },
    binary => sub { binary_search(\@input) },
});

结果:

Benchmark: timing 1000 iterations of binary, brute, pairwise, xor_string...
    binary:  0 wallclock secs ( 0.02 usr +  0.00 sys =  0.02 CPU) @ 62500.00/s (n=1000)
            (warning: too few iterations for a reliable count)
     brute: 472 wallclock secs (469.92 usr +  0.05 sys = 469.97 CPU) @  2.13/s (n=1000)
  pairwise: 216 wallclock secs (214.74 usr +  0.00 sys = 214.74 CPU) @  4.66/s (n=1000)
xor_string: 223 wallclock secs (221.74 usr +  0.06 sys = 221.80 CPU) @  4.51/s (n=1000)