【问题标题】:How to get the value from the list that appears only once?如何从只出现一次的列表中获取值?
【发布时间】:2014-04-14 13:37:55
【问题描述】:

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

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

输出:

2

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

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

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

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

我尝试了一种将整个列表与选项卡组合的正则表达式方法,然后使用了

my $combined = join " ", @array;
$combined !~ (\d+).*$1;
say $1;

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

有什么快速的方法吗?任何想法使用正则表达式?

编辑:改写标题以获得更好的答案

【问题讨论】:

  • 感谢 Lee Duhem 的编辑。
  • 是否保证列表单调递增?
  • @SzG 不,列表未排序,可以包含负数和正数。

标签: regex perl


【解决方案1】:

这似乎很快:

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%。

【讨论】:

  • 使用数组会严重影响内存。将1_000_000_000, 1_000_000_000 添加到输入中,它会花费 30 秒来分配内存。等待发生的 DOS 攻击。此外,由于允许算法在找到数字后立即退出,因此您实际上是在测试它们检查数字的顺序。如果schwern 按数字排序,它的运行速度与tobyink 一样快。 there_can_be_only_one 仍然是最快且内存最少的。
  • 确实。我假设这些数字大致是连续的。如果它们是稀疏的,那么散列可以更好地使用内存。
【解决方案2】:

处理这种情况的标准方法是将其全部放入哈希中。

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),随着数字列表变得越来越大,这个解决方案最终会比基于哈希的解决方案慢,但在此之前你可能会耗尽内存。

最后,如果这个列表很大,您应该考虑将它存储在数据库中的磁盘上。这样就可以避免内存耗尽,让数据库高效地完成工作。

【讨论】:

  • 我认为你必须使用 spaceship 运算符进行排序。谢谢回复。使用 XOR 很快。
  • @Wordzilla 你说得对,默认排序是 ASCIIbetical,我没有考虑过。现在想来,排序标准并不重要。对我们而言,重要的是相同的条目将被排序相同。默认排序是最快的。
  • 因为您是彻底的,所以最终搜索需要 2 N 次比较。如果我们利用完整信息,则可以进行二分搜索,从而进行 O(Log N) 比较。编写一个也利用此信息的排序例程会很有趣。快速排序可以在枢轴上将列表一分为二,并完全忽略具有偶数个元素的“一半”。
【解决方案3】:

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

实际上,如果你想对负数进行异或运算,你只需要将它们字符串化:

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)

表明对字符串进行异或运算比将数学转换为正数进行异或运算快 15%。

推论 - 排序列表怎么样?

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

如果我们使用额外信息,即在一堆双胞胎中只有一个单胞胎,我们可以通过成对比较来快速简化搜索,从而将我们的比较减少 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)

【讨论】:

  • 谢谢!我没有想过将数字串起来并对其进行异或运算。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-01-24
  • 1970-01-01
  • 1970-01-01
  • 2016-06-10
相关资源
最近更新 更多