【问题标题】:Time complexity of %in% in R; is there a way to make it O(1) like with sets in Python?R 中 %in% 的时间复杂度;有没有办法让它像 Python 中的集合一样 O(1)?
【发布时间】:2017-11-15 21:11:45
【问题描述】:

R 中的%in% 运算符检查某物是否存在于其他东西中,这很明显。但我对表演很好奇。在 Python 中,搜索一个集合或字典键的项目是 O(1),因为我认为集合是哈希表。但是在 Python 中搜索列表中的项目可能是 O(n) 和 n 长度列表,因为它将逐个元素搜索。那么%in% 如何在 R 中为不同的数据类型在幕后工作?与向量相比,在 R 中的因子 dtype 中搜索某些东西似乎需要 5 倍的时间,但似乎%in% 线性搜索向量。起初我认为因子数据类型可能就像 Python 中的一个集合,因为它们都将某些东西减少到它的唯一值,但根本不是:https://www.tutorialspoint.com/r/r_data_types.htm。这是一些示例代码,因此您可以了解我对运行时的含义:

library(microbenchmark)
s <- seq(5000)
microbenchmark(1 %in% s, times = 100000)
# searching for a term further in the list takes longer
microbenchmark(4999 %in% s, times = 100000)
s <- as.factor(s)
# searching for something in a factor takes way longer than a vector
# I think because everything is converted to a character dtype
microbenchmark(4999 %in% s, times = 100000)

我的主要问题是:有没有办法在 R 中制作 %in% O(1) ?一个相关问题:Python 中是否存在与 set() 数据类型等价的(在 R 中)?

【问题讨论】:

  • 注意:%in% 的文档在这里:stat.ethz.ch/R-manual/R-devel/library/base/html/match.html 它是 match() 函数。实际上很难找到。
  • 您可以将环境用作穷人的散列,因为据称对象是内部散列的。您还可以使用hashmap,它以可矢量化的方式对事物进行哈希处理(与环境不同),但不能存储任意对象(环境可以这样做)。
  • 你想发动一场火焰战争吗?看,根据您的需要,环境可能适合它们,在这种情况下是的,R 中有一个固有的数据类型。对于这种 O(1) 效率是否有实际需要,或者这在您学习 R 时主要是教学法吗?
  • 不,我正在尝试有效地检查 R 中的一组项目中是否有某些东西。我以前在 Python 中将它用于项目。我已经循环检查它们是否在一组中,如果是则跳过它们。如果集合很大,查看列表或向量会很慢。
  • R 比 Python 有一些优势,但就像我说的,我认为 Python 赢得了这一轮。

标签: r time-complexity complexity-theory


【解决方案1】:

正如我们在 cmets 中所讨论的,在 R 中存在一种固有的类似集合的机制,尽管无可否认它有点不合时宜,而且可能并不完全符合预期。 (此 hack 的一些限制记录在 hashmap 包中。)

R 中的环境是内部散列的。这可用于存储具有随机访问(读取和写入)的任意对象。为了检查一些基准,我将生成几种类型的向量来证实您最初的担忧并展示使用环境可以带来的改进。

我们将首先生成一些类似的数据,以各种方式排序以突出您提出的问题:

library(microbenchmark)
set.seed(2)
s1 <- seq(5000)
s2 <- rev(s1) # to highlight the bias you highlighted, since the vector is sorted
s3 <- sample(s1) # to shake things up a little
s4 <- as.character(s3) # comparison with character-based named in 'l' and 'e'

l <- list()
e <- new.env(parent = emptyenv())
for (i in s4) {
  assign(i, TRUE, envir = e)
  l[[i]] <- TRUE
}
head(names(l)) # unordered
# [1] "925"  "3512" "2866" "840"  "4716" "4713"

list 在其对象中确实具有序数,这支持其对象散列的假设:

which(names(l) == "1")
# [1] 2291

环境没有这个:

e[[1]]
# Error in e[[1]] : wrong arguments for subsetting an environment

一些快速的成员资格测试:我使用了一个逻辑值,尽管这完全是武断的。除了NULL 之外的任何东西都足以满足我们的需求。我们将使用一个简单的!is.null(e[[...]]) 来测试特定的成员资格:

!is.null(e[["1"]])
# [1] TRUE
!is.null(e[["10000"]])
# [1] FALSE
!is.null(l[["1"]])
# [1] TRUE
!is.null(l[["10000"]])
# [1] FALSE

microbenchmark(
  vec1 =  1  %in% s1,
  vec2 =  1  %in% s2,
  vec3 =  1  %in% s3,
  vec4 = "1" %in% s4,
  lst = is.null(l[["1"]]),
  env = is.null(e[["1"]]),
  times = 1000
)
# Warning in microbenchmark(vec1 = 1 %in% s1, vec2 = 1 %in% s2, vec3 = 1 %in%  :
#   Could not measure a positive execution time for 6 evaluations.
# Unit: nanoseconds
#  expr   min    lq     mean median    uq     max neval
#  vec1  5835  6929 12493.25   7294  9482 3214588  1000
#  vec2  9117  9847 16660.73  10212 12764 4081050  1000
#  vec3  7294  8388 19983.63   8752 10576 3274759  1000
#  vec4 11670 12400 15423.03  12764 14223   74394  1000
#   lst 20787 21517 24561.72  21881 22975  143317  1000
#   env     0     1   461.25    365   366   18235  1000

毫不奇怪,list 表现不佳,尽管它似乎比向量做得更好(在max 的情况下,相对没有意义)。同样不足为奇的是,根据我们声称环境使用内部拥有的说法,它表现得相当好。是O(1)吗?

microbenchmark(
     samp5 = sapply(as.character(sample(5000, size =    5)), function(a) is.null(e[[a]])),
    samp50 = sapply(as.character(sample(5000, size =   50)), function(a) is.null(e[[a]])),
   samp500 = sapply(as.character(sample(5000, size =  500)), function(a) is.null(e[[a]])),
  samp5000 = sapply(as.character(sample(5000, size = 5000)), function(a) is.null(e[[a]]))
)
# Unit: microseconds
#      expr      min         lq        mean     median         uq       max neval
#     samp5   25.893    32.4565    49.58154    40.4795    58.3485   169.573   100
#    samp50  108.309   119.4310   156.45244   135.8410   167.3850   681.938   100
#   samp500  935.750  1023.2715  1265.29732  1073.9610  1172.6055  6841.985   100
#  samp5000 9410.008 10337.5520 11137.82968 10650.0765 11280.0485 15455.548   100

第一个,samp5,似乎需要更长的时间。这并不奇怪,因为有与sapply、采样和其他事情相关的开销。然而,剩余的行似乎随着样本数量的增加而扩展得很好。这表明对于一些基本的集合操作来说确实是O(1)

注意:我不得不使用整个 sapply(...) 技巧,因为与向量和列表不同,R 的环境不允许使用向量进行子集化。

e[[c("1")]]
# [1] TRUE
e[[c("1","10")]]
# Error in e[[c("1", "10")]] : 
#   wrong arguments for subsetting an environment

这是hashmap 提出(并由其修复)的声明之一。

额外的功劳:为了便于将环境作为一个集合使用,您可以使用简单的加法器和删除器:

newset <- function() new.env(parent = emptyenv())
setadd <- function(set, n) set[[n]] <- TRUE
setdel <- function(set, n) set[[n]] <- NULL
setcontains <- function(set, n) !is.null(set[[n]])
setmembers <- function(set) names(set)

e <- newset()
setcontains(e, "a")
# [1] FALSE
setadd(e, "a")
setcontains(e, "a")
# [1] TRUE
setmembers(e)
# [1] "a"
setdel(e, "a")
setcontains(e, "a")
# [1] FALSE

(Jeffrey Horner 在这里发表了一篇类似但内容更广泛的博文:http://jeffreyhorner.tumblr.com/post/114524915928/hash-table-performance-in-r-part-i。)

【讨论】:

  • 太棒了,非常彻底。但是很复杂。仍然希望 R 中有一个易于使用的 set() 等效项。我猜 R 主要是为统计而设计的,而不是真正的编程本身。
  • 但我认为在 C# 等其他一些语言中,我认为您也必须使用 hashmap。
猜你喜欢
  • 1970-01-01
  • 2020-11-01
  • 2020-12-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-05-25
  • 1970-01-01
  • 2018-03-26
相关资源
最近更新 更多