【问题标题】:Look up a value in Perl based on a range根据范围在 Perl 中查找值
【发布时间】:2011-12-19 00:46:08
【问题描述】:

我有两个变量,iddate。有数百万个不同的ids,但只有几百个不同的日期。 ids 是连续的,日期随着id 而增加。像这样的:

id    date
1     1/1/2000
2     1/1/2000
3     1/1/2000
4     1/2/2000
5     1/2/2000

在 Perl 中,我需要创建一个函数,该函数将在给定 id 的情况下返回 date。我的第一个想法就是制作一个哈希表。这会起作用,但鉴于我有数百万条记录,我认为使用日期范围可能更有意义。因此,在上面的示例中,我可以存储 2 条记录,而不是存储 5 条记录:每个日期都有一条记录,最早和最晚日期对应于 id:

date       first_id  last_id
1/1/2000   1         3
1/2/2000   4         5

(在我的实际数据中,这将允许我只存储几千条记录,而不是几百万条。)

我的问题是,给定id,在给定这种结构的情况下,查找日期的最佳方法是什么?所以给定id=2,我想返回1/1/2000,因为2介于1和3之间,因此对应第一条记录。

感谢您的建议。

【问题讨论】:

  • 假设数据结构已排序,我将实现二进制搜索。
  • 如果 id 是连续的,那么 first_id 是多余的。第一个日期为 1。每隔一个日期,它比前一个日期的 last_id 多 1。
  • @cjm - 你是对的,但我没有提到日期中有一些漏洞,所以这并不总是正确的。
  • 我认为拥有数百万个键的哈希会很慢,但也许不会。也许存储较小哈希的效率增益可以忽略不计。
  • @itzy,对我来说这就是重点,我认为在您尝试之前很难说,因为性能在很大程度上取决于细节(正在使用什么 DBMS 等) ),但一种或另一种存储在某种数据库中应该有助于抽象和性能(恕我直言)。

标签: perl hashtable


【解决方案1】:

我可能会将数据放在SQLite database 中,使id 字段成为表的主键。使用DBD::SQLiteDBI

如果您首先prepare 查询包含idplaceholder 并针对id 的各种值重复执行它,则性能应该足够了。

【讨论】:

  • 注:另请参阅DBD::SQLite,它的 SQLite 内置在 DBD 模块中,无需安装其他任何东西!
【解决方案2】:

如果您要采用这样的方法,我认为在数据库级别进行查询是最有意义的。然后,以 MySQL 为例,您可以使用 BETWEEN 函数进行查询,例如 SELECT date WHERE $id BETWEEN first_id AND last_id

然后您可以在 Perl 中创建一个函数,在其中传递 id 并使用查询来检索日期。

【讨论】:

    【解决方案3】:

    使用 [semi] 稀疏数组。性能应该没问题。您正在查看每百万条记录使用几兆字节的内存。如果在存储之前将日期转换为整数纪元,那就更好了。

    use Time::Local;
    
    my @date_by_id;
    while (<FILE>) {
      chomp;
    
      my ($id, $date) = split /\s+/;
      my ($mon, $mday, $year) = split /\//, $date;
    
      $mon--;
      $year -= 1900;
    
      $date_by_id[$id] = timelocal 0, 0, 0,  
        $mday, $mon, $year;
    }
    

    性能应该足够好,以至于您不需要将其包装在函数中。只需在需要的地方使用$date_by_id[&lt;ID&gt;],记住它可以是undef

    【讨论】:

      【解决方案4】:

      尝试实现弗兰克的想法:

      鉴于

      sub getDateForId {
        use integer;
        my ($id, $data) = @_;
        my $lo = 0;
        my $sz = scalar @$data;
        my $hi = $sz - 1;
        while ( $lo <= $hi ) {
          my $mi = ($lo + $hi) / 2;
          if ($data->[$mi]->[0] < $id) {
            $lo = $mi + 1;
          } elsif ($data->[$mi]->[0] > $id) {
            $hi = $mi - 1;
          } else {
            return $data->[$mi]->[1];
          }
        }
        # $lo > $hi: $id belongs to $hi range
        if ($hi < 0) {
          return sprintf "** id %d < first id %d **", $id, $data->[0]->[0];
        } elsif ($lo >= $sz) {
          return sprintf "** id %d > last  id %d **", $id, $data->[$sz-1]->[0];
        } else {
          return sprintf "%s (<== lo %d hi %d)", $data->[$hi]->[1], $lo, $hi;
        }
      }
      

      和数据

      my @data = (
          [2, '1/1/2000' ]
        , [4, '1/2/2000' ]
        , [5, '1/3/2000' ]
        , [8, '1/4/2000' ]
      );
      

      ,测试

      for my $id (0..9) {
        printf "%d => %s\n", $id, getDateForId( $id, \@data );
      }
      

      打印

      0 => ** id 0 < first id 2 **
      1 => ** id 1 < first id 2 **
      2 => 1/1/2000
      3 => 1/1/2000 (<== lo 1 hi 0)
      4 => 1/2/2000
      5 => 1/3/2000
      6 => 1/3/2000 (<== lo 3 hi 2)
      7 => 1/3/2000 (<== lo 3 hi 2)
      8 => 1/4/2000
      9 => ** id 9 > last  id 8 **
      

      【讨论】:

        【解决方案5】:

        正如其他人所说,您可能想尝试使用数据库。另一种可能性:使用更复杂的数据结构。

        例如,如果您的哈希表是按日期排列的,那么您可以将哈希中的每个条目作为一个 reference 到一个 id 数组。

        用你的例子:

        $hash{1/1/2000} = [ 1, 2, 3];
        $hash{1/2/2000} = [ 4, 5 ];
        

        这样,如果您找到一个日期,您可以快速找到该日期的所有 ID。对键进行排序将允许您找到一系列日期。如果您以更可排序的格式存储日期,则尤其如此。例如,YYYYMMDD 格式或标准 Unix 日期/时间格式。

        例如:

        $hash{20000101} = [ 1, 2, 3];
        $hash{20000102} = [ 4, 5];
        

        你说有几百个日期,所以排序你的日期会很快。

        您熟悉数组哈希之类的东西吗?您可以查看 Mark's very short tutorial about referencesperldsc 的 Perl 文档,它实际上向您展示了如何设置数组的哈希值。

        现在,通过 id 查找日期...

        想象一个更复杂的结构。第一级将有两个元素DATESIDS。然后,您可以让 IDS 部分成为对 ID 散列的引用,并且 DATES 键的结构与上述相同。不过,您必须使这两个结构保持同步...

        $dataHash->{DATES}->{20020101}->[0] = 1;
        $dataHash->{DATES}->{20020101}->[2] = 2;
        $dataHash->{DATES}->{20020101}->[3] = 3;
        $dateHash->{IDS}->{1} = 20020101;
        $dateHash->{IDS}->{2} = 20020101;
        $dateHash->{IDS}->{3} = 20020101;
        

        嗯...这越来越复杂了。也许你应该看看object oriented programming 上的 Perl 教程。

        在没有任何测试的情况下写下我脑海中的东西:

        package DataStruct;
        
        sub new {
           my $class = shift;
        
           my $self = {};
           bless $self, $class;
        
          my $self->_Id;
          my $self->_Date;
        
          return $self;
        }
        
        sub _Id {
           my $self = shift;
           my $id   = shift;
           my $date = shift;
        
           $self->{IDS} = {} if not exists $self->{IDS};
        
           if (defined $id and defined $date) {
              $self->{IDS}->{$id} = $date;
           }
        
           if (defined ($id) {
              return $self->{IDS}->{$id};
           else {
               return keys %{self->{IDS}};
           }
        }
        
        sub _Date {
           my $self = shift;
           my $date = shift;
           my $id   = shift;
        
           $self->{DATES} = {} if not exists $self->{DATES};
        
           if (defined $date and defined $id) {
              $self->{DATES}->{$date} = [] if not defined $self->{DATES}->{$date};
              push @{$self->{DATES}->{$date}}, $id;
           };
        
           if ($date) {
               return @{$self->{DATES}->{$date}};
           }
           else {
               return keys %{$self->{DATES};
           }
        }
        
        sub Define {
            my $self = shift;
            my $id   = shift;
            my $date = shift;
        
            $self->_Id($id, $date);
            $self->_Date($date, $id);
        
            return $self->_Date($date);
        }
        
        sub FetchId {
            my $self = shift;
            my $id   = shift;
        
            return $self->_Id($id);
        }
        
        sub FetchDate {
            my $self = shift;
            my $id   = shift;
        
            return $self->_Date;
        }
        

        在上面,你创建了你的初始数据结构:

        my $struct = DataStruct->new;
        

        现在,要添加日期和 ID,您可以调用:

        $struct->Define($id, $date);
        

        这将依次调用$struct-&gt;_Id($id, $date);$struct-&gt;_Date($date, $Id);。由于这些以下划线开头,它们是 private 并且只能由其他 DataStruct 方法调用。你主要使用 $struct-Set 来放你的数据。

        要获取特定日期(或整个日期范围),请使用 $dataStruct-&gt;FetchDate($date) 方法,要获取特定 ID,请使用 $dataStruct-&gt;FetchId($id);

        现在,DataStruct 包将用于保持 ID 哈希和日期哈希彼此同步,并将复杂性排除在程序的主要部分之外。

        这里有你需要的一切!您所要做的就是修复我的许多错误,并且可能有一些例程将M/D/Y 样式日期转换为YYYYMMDDstyle 日期或转换为标准日期/时间内部存储结构。这样,您不必担心在调用这些例程之前确定日期。哦,您可能还需要某种错误处理。如果我给你一个错误的日期或身份证号码怎么办?

        正如其他人所说,即使您使用 SQLite 之类的人造数据库结构,也最好使用数据库结构。

        但是,我想让您知道 Perl 实际上非常有能力创建一些非常集成的数据结构,这些数据结构可以在这种情况下提供帮助。

        根据您提出问题的方式,我假设您确实不熟悉创建这些复杂的数据结构。如果没有,Perl 本身就内置了一些出色的tutorials。而且,命令perldoc(与 Perl 一起安装)可以提取所有 Perl 文档。试试perldoc perlreftut,看看它是否能调出 Mark 的参考教程。

        一旦您开始接触更复杂的数据结构,您将学会使用面向对象编程来简化它们的处理。同样,在 Perl 中内置了一些优秀的教程(或者你可以去Perldoc webpage)。

        如果您已经知道这一切,我深表歉意。但是,至少您有存储和使用数据的基础。

        【讨论】:

        • 谢谢,这真的很有帮助。
        猜你喜欢
        • 2012-10-03
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-10-07
        • 1970-01-01
        • 2019-01-03
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多