我可以将列表用作R中的哈希吗?如果是这样,为什么会这么慢?

问题描述 投票:39回答:7

使用R之前,我使用了很多Perl。在Perl中,我经常使用哈希,在Perl中,哈希的查找通常被认为是快速的。

例如,以下代码将用最多10000个键/值对填充哈希,其中键是随机字母,值是随机整数。然后,它在该哈希中进行10000次随机查找。

#!/usr/bin/perl -w
use strict;

my @letters = ('a'..'z');

print @letters . "\n";
my %testHash;

for(my $i = 0; $i < 10000; $i++) {
    my $r1 = int(rand(26));
    my $r2 = int(rand(26));
    my $r3 = int(rand(26));
    my $key = $letters[$r1] . $letters[$r2] . $letters[$r3];
    my $value = int(rand(1000));
    $testHash{$key} = $value;
}

my @keyArray = keys(%testHash);
my $keyLen = scalar @keyArray;

for(my $j = 0; $j < 10000; $j++) {
    my $key = $keyArray[int(rand($keyLen))];
    my $lookupValue = $testHash{$key};
    print "key " .  $key . " Lookup $lookupValue \n";
}

现在,我越来越希望在R中具有类似于哈希的数据结构。以下是等效的R代码:

testHash <- list()

for(i in 1:10000) {
  key.tmp = paste(letters[floor(26*runif(3))], sep="")
  key <- capture.output(cat(key.tmp, sep=""))
  value <- floor(1000*runif(1))
  testHash[[key]] <- value
}

keyArray <- attributes(testHash)$names
keyLen = length(keyArray);

for(j in 1:10000) {
  key <- keyArray[floor(keyLen*runif(1))]
  lookupValue = testHash[[key]]
  print(paste("key", key, "Lookup", lookupValue))
}

代码似乎在做等效的事情。但是,Perl的速度要快得多:

>time ./perlHashTest.pl
real    0m4.346s
user    **0m0.110s**
sys 0m0.100s

与R相比:

time R CMD BATCH RHashTest.R

real    0m8.210s
user    **0m7.630s**
sys 0m0.200s

什么解释了差异? R列表中的查找是否不好?

增加到100,000个列表长度和100,000个查询只会夸大差异?与本地list()相比,R中的哈希数据结构有更好的替代方法吗?

r perl hash
7个回答
36
投票
big简化并避免了处理冲突的复杂性)。键的查找只需要对键进行哈希处理即可找到值的位置(O(1),而不是O(n)数组查找)。 R列表使用名称查找为O(n)。

正如Dirk所说,请使用哈希包。这样做的一个巨大限制是它使用环境(经过哈希处理)和[方法的覆盖来模仿哈希表。但是一个环境不能包含另一个环境,因此您不能使用hash函数嵌套嵌套的哈希。

前一段时间,我曾致力于在C / R中实现一个可嵌套的纯哈希表数据结构,但是当我从事其他工作时,它就一直存在于我的项目中。最好有:-)

19
投票

11
投票
n <- 10000 keys <- matrix( sample(letters, 3*n, replace = TRUE), nrow = 3 ) keys <- apply(keys, 2, paste0, collapse = '') value <- floor(1000*runif(n)) testHash <- as.list(value) names(testHash) <- keys keys <- sample(names(testHash), n, replace = TRUE) lookupValue = testHash[keys] print(data.frame('key', keys, 'lookup', unlist(lookupValue)))

在我的机器上,除了打印外几乎可以立即运行。您的代码以与您报告的相同的速度运行。它在做什么吗?您可以将n设置为10,然后查看输出和testHash,看看是否就此。

关于语法的注意:上面的apply只是一个循环,R的速度很慢。这些应用族命令的要点是表达性。后面的许多命令可能已与apply放在一个循环中,如果是for循环,那将是一个诱惑。在R中,请尽可能多地走出循环。使用apply family命令使之更为自然,因为该命令旨在代表某个函数对某种类型的列表的应用,而不是通用循环(是的,我知道apply可以用于多个命令)。


10
投票

    R使用标准版似乎慢得多流比Perl多。自从stdin和烈性黑啤酒更常用于我认为Perl有优化这些事情是怎么做的。所以在R我发现使用内置命令可以更快地读取/写入文本功能(例如write.table)。
  • 正如其他人所说,R中的运算比循环...和w.r.t.速度,大多数apply()家庭语法只是一个漂亮的包装一个循环。

  • 索引的东西比非索引。 (很明显,我知道。)data.table包支持数据帧类型对象的索引。

  • 我从没用过哈希像@Allen这样的环境说明了(据我所知,我从未吸入过哈希...)

  • 您使用的某些语法有效,但是可以加强。我认为这对速度没有任何实质性影响,但是代码更具可读性。我写的代码不是很紧,但是我做了一些编辑,例如将floor(1000*runif(1))更改为sample(1:1000, n, replace=T)。我并不是要书呆子,我只是按照从头开始的方式写了它。

  • 因此,考虑到这一点,我决定将@allen使用的哈希方法(因为这对我来说是新奇的)针对我使用索引的data.table作为查找表创建的“穷人的哈希”进行测试。我不是100%肯定@allen和我在做什么与您在Perl中所做的完全一样,因为我的Perl非常生锈。但是我

    认为下面的两种方法做同样的事情。我们都从“散列”中的键中采样了第二组键,因为这可以防止散列遗漏。您将要测试这些示例如何处理哈希重复项,因为我没有考虑太多。

    require(data.table) dtTest <- function(n) { makeDraw <- function(x) paste(sample(letters, 3, replace=T), collapse="") key <- sapply(1:n, makeDraw) value <- sample(1:1000, n, replace=T) myDataTable <- data.table(key, value, key='key') newKeys <- sample(as.character(myDataTable$key), n, replace = TRUE) lookupValues <- myDataTable[newKeys] strings <- paste("key", lookupValues$key, "Lookup", lookupValues$value ) write.table(strings, file="tmpout", quote=F, row.names=F, col.names=F ) }

    hashTest <- function(n) {
    
      testHash <- new.env(hash = TRUE, size = n)
    
      for(i in 1:n) {
        key <- paste(sample(letters, 3, replace = TRUE), collapse = "")
        assign(key, floor(1000*runif(1)), envir = testHash)
      }
    
      keyArray <- ls(envir = testHash)
      keyLen <- length(keyArray)
    
      keys <- sample(ls(envir = testHash), n, replace = TRUE)
      vals <- mget(keys, envir = testHash)
    
      strings <- paste("key", keys, "Lookup", vals )
      write.table(strings, file="tmpout", quote=F, row.names=F, col.names=F )
    
      }
    

    如果我使用100,000次抽奖运行每种方法,则会得到类似以下内容:

    > system.time(  dtTest(1e5))
       user  system elapsed 
      2.750   0.030   2.881 
    > system.time(hashTest(1e5))
       user  system elapsed 
      3.670   0.030   3.861 
    

    请记住,这仍然比Perl代码慢得多,在我的PC上,Perl代码似乎在一秒钟内就可以运行100K样本。

    希望以上示例对您有所帮助。如果您对why有任何疑问,也许@ allen,@ vince和@dirk可以回答;)

    输入上述内容后,我意识到我没有测试@john所做的事情。所以,该死的,让我们全部做3。我将@john的代码更改为使用write.table(),这是他的代码:

    johnsCode <- function(n){ keys = sapply(character(n), function(x) paste(letters[ceiling(26*runif(3))], collapse='')) value <- floor(1000*runif(n)) testHash <- as.list(value) names(testHash) <- keys keys <- names(testHash)[ceiling(n*runif(n))] lookupValue = testHash[keys] strings <- paste("key", keys, "Lookup", lookupValue ) write.table(strings, file="tmpout", quote=F, row.names=F, col.names=F ) }

    和运行时间:

    > system.time(johnsCode(1e5))
       user  system elapsed 
      2.440   0.040   2.544 
    

    然后就可以了。 @john编写严格/快速的R代码!


  • 6
    投票
    但是一个环境不能包含另一个环境(引自文斯的回答)。
    也许以前是这样的(我不知道),但是此信息似乎不再准确了:

    > d <- new.env() > d$x <- new.env() > d$x$y = 20 > d$x$y [1] 20

    因此,环境现在可以提供功能强大的地图/字典。也许您会错过'['运算符,请在这种情况下使用哈希包。

    此哈希包文档中的注释也可能引起您的兴趣:

    R正在缓慢地使用 环境,(请参阅摘录。使用$和[[具有 已有一段时间了,最​​近对象可以从 环境等。但是许多构成哈希/字典的功能 仍然缺少诸如切片操作之类的[。


    4
    投票
    #!/usr/bin/Rscript testHash <- new.env(hash = TRUE, size = 10000L) for(i in 1:10000) { key <- paste(sample(letters, 3, replace = TRUE), collapse = "") assign(key, floor(1000*runif(1)), envir = testHash) } keyArray <- ls(envir = testHash) keyLen <- length(keyArray) for(j in 1:10000) { key <- keyArray[sample(keyLen, 1)] lookupValue <- get(key, envir = testHash) cat(paste("key", key, "Lookup", lookupValue, "\n")) }

    它在我的机器上运行非常快,主要是设置。 (尝试一下并发布时间。)

    但是,正如约翰所说,真正的问题是您必须考虑R中的向量(例如perl中的map),而他的解决方案可能是最好的。如果您确实想使用哈希,请考虑

    keys <- sample(ls(envir = testHash), 10000, replace = TRUE) vals <- mget(keys, envir = testHash)

    经过与上述相同的设置,这在我的机器上几乎是即时的。要全部打印,请尝试

    cat(paste(keys, vals), sep="\n")
    

    希望这会有所帮助。

    艾伦    

    0
    投票
    我使用setkey的data.table包具有更好的性能。如果您不熟悉data.table和setkey,则可以从这里开始:https://cran.r-project.org/web/packages/data.table/vignettes/datatable-keys-fast-subset.html

    我意识到最初的问题涉及10,000种事物,但几天前Google便将我引导到这里。我尝试使用哈希程序包,并且遇到了很多困难。然后,我发现了这篇博客文章,该文章表明构建哈希可能需要花费数小时才能完成1000万以上的工作,这与我的经验相吻合:https://appsilon.com/fast-data-lookups-in-r-dplyr-vs-data-table/?utm_campaign=News&utm_medium=Community&utm_source=DataCamp.com

    © www.soinside.com 2019 - 2024. All rights reserved.