【问题标题】:How to efficiently sort the characters in a string in R?如何有效地对R中的字符串中的字符进行排序?
【发布时间】:2020-02-05 19:55:50
【问题描述】:

如何有效地对向量中每个字符串的字符进行排序?例如,给定一个字符串向量:

set.seed(1)
strings <- c(do.call(paste0, replicate(4, sample(LETTERS, 10000, TRUE), FALSE)),
do.call(paste0, replicate(3, sample(LETTERS, 10000, TRUE), FALSE)),
do.call(paste0, replicate(2, sample(LETTERS, 10000, TRUE), FALSE)))

我写了一个函数,它将每个字符串拆分成一个向量,对向量进行排序,然后折叠输出:

sort_cat <- function(strings){
  tmp <- strsplit(strings, split="")
  tmp <- lapply(tmp, sort)
  tmp <- lapply(tmp, paste0, collapse = "")
  tmp <- unlist(tmp)
  return(tmp)
}
sorted_strings <- sort_cat(strings)

但是,我需要应用它的字符串向量很长,而且这个函数太慢了。有人对如何提高性能有任何建议吗?

【问题讨论】:

  • 查看 stringi 包 - 它提供了相对于基础的加速。 Rich Scriven 的回答提供了更多细节:stackoverflow.com/questions/5904797/…
  • letters 并不总是像您的示例中那样长度为 3,是吗?
  • 不,字符串的长度可能会有所不同。
  • 我认为在strsplit() 中添加fixed = TRUE 可能会提高性能,因为它不涉及使用正则表达式。

标签: r string performance sorting


【解决方案1】:

使用stringi 重新实现可实现大约 4 倍的加速。我还编辑了sort_cat 以在strsplit 中使用fixed = TRUE,这使得它有点快了。感谢 Carl 提出的单循环建议,它让我们的速度更快了一点。

sort_cat <- function(strings){
  tmp <- strsplit(strings, split="", fixed = TRUE)
  tmp <- lapply(tmp, sort)
  tmp <- lapply(tmp, paste0, collapse = "")
  tmp <- unlist(tmp)
  return(tmp)
}

library(stringi)
sort_stringi = function(s) {
  s = stri_split_boundaries(s, type = "character")
  s = lapply(s, stri_sort)
  s = lapply(s, stri_join, collapse = "")
  unlist(s)
}

sort_stringi_loop = function(s) {
  s = stri_split_boundaries(s, type = "character")
  for (i in seq_along(s)) {
    s[[i]] = stri_join(stri_sort(s[[i]]), collapse = "")
  }
  unlist(s)
}

bench::mark(
  sort_cat(strings),
  sort_stringi(strings),
  sort_stringi_loop(strings)
)
# # A tibble: 3 x 13
#   expression                    min median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc total_time result memory
#   <bch:expr>                 <bch:> <bch:>     <dbl> <bch:byt>    <dbl> <int> <dbl>   <bch:tm> <list> <list>
# 1 sort_cat(strings)          23.01s 23.01s    0.0435    31.2MB     2.17     1    50     23.01s <chr ~ <Rpro~
# 2 sort_stringi(strings)       6.16s  6.16s    0.162     30.5MB     2.11     1    13      6.16s <chr ~ <Rpro~
# 3 sort_stringi_loop(strings)  5.75s  5.75s    0.174     15.3MB     1.74     1    10      5.75s <chr ~ <Rpro~
# # ... with 2 more variables: time <list>, gc <list>

这种方法也可以并行使用。如果您想更快地进行,分析代码以查看哪些操作实际上花费的时间最长将是一个很好的下一步。

【讨论】:

  • 我认为这最终会比应用更快,并且如果长度不同,则不依赖于删除空值。不过,可能会建议一个包含在 unlist 中的循环?
  • 单循环提高了一点速度,谢谢!
  • 是的,伙计。不过,这仍然困扰着我。我觉得我错过了一个非常明显和更简单的方法来完成这件事......
  • 我的意思是,编写一个 RCPP 函数可能会很容易做到这一点,而且速度很快。但是在 R 中工作,我认为我们基本上只能完成这些步骤。
  • 这就是我的想法:C++
【解决方案2】:

您可以肯定地通过最小化循环数量来减少时间,并通过使用parallel 包进一步这样做...我的方法是拆分字符串一次,然后在循环中排序和粘贴:

sort_cat <- function(strings){
    tmp <- strsplit(strings, split="")
    tmp <- lapply(tmp, sort)
    tmp <- lapply(tmp, paste0, collapse = "")
    tmp <- unlist(tmp)
    return(tmp)
}

sort_cat2 <- function(strings){
    unlist(mcMap(function(i){
        stri_join(sort(i), collapse = "")
    }, stri_split_regex(strings, "|", omit_empty = TRUE, simplify = F), mc.cores = 8L))
}

> microbenchmark::microbenchmark(
+     old = sort_cat(strings[1:500000]),
+     new = sort_cat2(strings[1:500000]),
+     times = 1
+ )
Unit: seconds
 expr        min         lq       mean     median         uq        max neval
  old 9.62673395 9.62673395 9.62673395 9.62673395 9.62673395 9.62673395     1
  new 5.10547437 5.10547437 5.10547437 5.10547437 5.10547437 5.10547437     1

剃须 4 秒左右,但还是没那么快……

编辑

好的,在这里使用apply.. 策略:

1) 提取字母而不是分割边界 2)用结果创建一个矩阵 3)逐行迭代 4) 排序 5) 加入

您避免了多个循环和取消列出.... IGNORE: ?caveat 是如果字符串长度不同,您需要删除 apply 中的任何空或 NA,例如 i[!is.na(i) &amp;&amp; nchar(i) &gt; 0]

sort_cat3 <- function(strings){
    apply(stri_extract_all_regex(strings, "\\p{L}", simplify = TRUE), 1, function(i){
        stri_join(stri_sort(i), collapse = "")
    })
}

> microbenchmark::microbenchmark(
+     old = sort_cat(strings[1:500000]),
+     mapping = sort_cat2(strings[1:500000]),
+     applying = sort_cat3(strings[1:500000]),
+     times = 1
+ )
Unit: seconds
     expr         min          lq        mean      median          uq         max neval
      old 10.35101934 10.35101934 10.35101934 10.35101934 10.35101934 10.35101934     1
  mapping  5.12771799  5.12771799  5.12771799  5.12771799  5.12771799  5.12771799     1
 applying  3.97775326  3.97775326  3.97775326  3.97775326  3.97775326  3.97775326     1

从 10.3 秒到 3.98 秒

【讨论】:

  • 并行运行原始函数的加速比是多少?
  • 下降了 50% 以上。 tmp &lt;- strsplit(strings, split="") unlist(mclapply(tmp, function(i){ paste0(sort(i), collapse = "") }))
  • @Gregor 确实如此。刚刚经过测试,似乎可以?
  • 酷,只是检查:)
  • 不,一点也不……我自己也有同样的问题……这意味着省略我在答案中关于删除 NA/empty 的注释……不需要它。 stringi 是迄今为止我最喜欢的包...
【解决方案3】:

这个版本稍微快一点

sort_cat2=function(strings){
A=matrix(unlist(strsplit(strings,split="")),ncol=3,byrow=TRUE)
B=t(apply(A,1,sort))
paste0(B[,1],B[,2],B[,3])
}

但我认为它可能会被优化

【讨论】:

  • 只有在所有字符串的长度相同时才有效。不过,又好又快!
猜你喜欢
  • 2021-09-07
  • 2016-07-29
  • 2011-01-23
  • 1970-01-01
  • 2012-04-12
  • 1970-01-01
  • 2015-08-13
  • 2017-02-08
  • 2011-08-19
相关资源
最近更新 更多