我可以在R中使用一个列表作为散列吗? 如果是这样,为什么这么慢?
在使用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列表中查找不好?
增加到10万个列表长度和10万个查找只会夸大这个差异? R中的散列数据结构比本地列表()有更好的select吗?
根本的原因是R列表与命名元素不散列。 哈希查找是O(1),因为在插入期间使用哈希函数将密钥转换为整数,然后将值放在空间hash(key) % num_spots
的一个数组num_spots
long(这是一个很大的简化并避免处理碰撞的复杂性)。 查找密钥只需要对密钥进行散列查找值的位置(与O(n)数组查找相比,是O(1))。 R列表使用O(n)的名称查找。
正如德克所说,使用散列包。 这是一个巨大的局限性,它使用环境(哈希)和重写的方法来模仿哈希表。 但是一个环境不能包含其他环境,所以你不能使用哈希函数进行嵌套散列。
前一段时间,我在C / R中实现了一个可以嵌套的纯哈希表数据结构,但是当我处理其他事情的时候,我的项目就开始了。 这将是很好,虽然:-)
您可以尝试环境和/或克里斯托弗·布朗(这恰好使用环境下的环境) 哈希包。
你的代码是非常类似的,是这么慢的原因之一。 我没有优化下面代码的最大速度,只有R'ness。
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中是缓慢的。这些应用系列命令的要点是expression性。 下面的许多命令可能被放在一个循环中,如果它是一个for
循环将是诱惑。 在R尽可能多地从你的循环中。 使用应用系列命令使得这更加自然,因为该命令被devise为将一个函数的应用程序表示为某种types的列表,而不是通用循环(是的,我知道apply
可以用于多个命令)。
我是一个R的黑客,但是我是一个经验主义者,所以我会分享一些我观察到的东西,让那些对R有更深入的理论认识的人对于这个问题有所了解。
-
R使用标准stream比Perl更慢。 由于stdin和stout在Perl中的使用更为普遍,所以我认为它对这些事情做了优化。 所以在RI中,使用内置函数(例如
write.table
)来读/写文本要快得多。 -
正如其他人所说的,R中的向量操作比循环更快,而且速度更快,大多数apply()族的语法只是一个循环中的漂亮包装。
-
索引的东西工作比非索引更快。 (很明显,我知道。)data.table包支持索引dataframetypes对象。
-
我从来没有用过像@Allen这样的哈希环境(我从来没有用过哈希……据你所知)
-
你使用的一些语法是有效的,但是可以收紧。 我不认为这对速度真的很重要,但代码更可读。 我不写非常严密的代码,但我编辑了一些东西,如改变
floor(1000*runif(1))
以sample(1:1000, n, replace=T)
。 我不是故意的,我只是从头开始写的。
所以考虑到这一点,我决定testing@allen使用的哈希方法(因为它对我来说是新颖的),这是我使用索引data.table作为查找表创build的“穷人哈希”。 我不是100%确定@allen和我正在做什么,这正是你在Perl中所做的,因为我的Perl很生疏。 但是我认为下面的两个方法是一样的。 我们都从“散列”中的键中抽样第二组键,因为这可以防止散列错过。 你会想testing这些例子如何处理乱码,因为我没有多less想法。
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上,它似乎在一秒钟内运行了100K样本。
我希望上面的例子有所帮助。 如果您有任何疑问, why
@allen,@ vince和@dirk将能够回答;)
在input上面的内容之后,我意识到我没有testing@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代码!
但是一个环境不能包含另一个环境(引自Vince的回答)。
也许前一段时间是这样(我不知道),但是这个信息似乎不准确了:
> d <- new.env() > d$x <- new.env() > d$x$y = 20 > d$x$y [1] 20
所以环境现在可以制作一个非常有用的地图/字典。 也许你会错过'['运算符,在这种情况下使用哈希包。
这个来自哈希包文档的笔记也可能是有趣的:
R使用环境慢慢向本机实现散列(参见:Extract。使用$和[[已经可用一段时间了,最近的对象可以从环境中inheritance等等)访问环境,但是许多使得散列/字典还有很多缺点,比如切片操作,[。
首先,正如Vince和Dirk所说的,你在示例代码中不使用散列。 perl例子的直译就是
#!/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),他的解决scheme可能是最好的。 如果你想使用散列,考虑
keys <- sample(ls(envir = testHash), 10000, replace = TRUE) vals <- mget(keys, envir = testHash)
经过与上面相同的设置,这在我的机器上几乎是瞬时的。 要打印它们都尝试
cat(paste(keys, vals), sep="\n")
希望这有所帮助。
艾伦