【问题标题】:Efficient way of sequentially detecting if certain strings occur at the beginning of other strings顺序检测某些字符串是否出现在其他字符串开头的有效方法
【发布时间】:2026-02-21 14:30:01
【问题描述】:

假设以下数据:

df <- data.frame(id   = c(1:8),
                 text = c("i like", "i like to", "oops", "i like to and", "i like it not", "victoria", "victoria secret", "victoria secret is"))

我想做的是:

  • 找出任何给定的较短字符串是否是一部分(更准确地说)是另一个字符串的开头
  • 应用最小字符长度,例如字符串必须至少有 X 个字符(例如,我们将其设置为 5)
  • 将信息添加到数据集中哪些字符串属于同一组。

我的想法是我可以根据文本响应和文本响应的长度对我的数据框进行排序,然后检查第一个字符串是否是任何后续字符串的一部分,然后我继续处理第二个字符串并检查它是否是后续的一部分,依此类推。这是一个计算的噩梦,所以我想知道是否有一种计算更有效的方法。我只是想也许首先分解成单词可能是有意义的,然后基于此进行比较? (比较完整的单词就可以了,不需要逐个字符比较)

此外,问题可能是任何更长的响应都可能是所有先前响应的一部分,这意味着需要存储的信息可能需要 n-1 列(或该长度的列表)。

简单地说:我的真实数据有大约 100.000 行。

我可以这样设想潜在的预期输出:

  id                text group_1 group_2 group_3
1  1              i like       1       1       0
2  2           i like to       1       0       0
3  3                oops       0       0       0
4  4       i like to and       1       0       0
5  5       i like it not       0       1       0
6  6            victoria       0       0       1
7  7     victoria secret       0       0       1
8  8  victoria secret is       0       0       1

注意,如果某些字符串至少有两行资格,我只需要一列。所以在这种情况下,我不想/不需要为“oops”文本添加组变量。

  • 文本 1、2、4 属于一起,因为它们都以“我喜欢”开头并且它们“按顺序”构建,即第二个文本也是第四个文本的一部分。
  • 第 1 行和第 5 行也属于同一行,因为文本 1 是文本 5 的一部分。
  • 第 6 行到第 8 行也属于一起,原因与文本 1、2、4 属于一起的原因相同(它们相互叠加)。

或者,作为第一步,我还可以使用一个输出,如果某个文本是另一个文本的一部分,它只会给我提供信息,因此在示例中,只需将 1 分配给除“oops”之外的所有文本。

【问题讨论】:

  • 这是第 8 行的错字吗?
  • 是的。感谢举报。将修复它....完成。
  • 使用prefix trees可以有效解决这类问题——似乎有一些R实现。
  • 感谢您的建议。谷歌搜索并没有在这个问题上出现很多。我找到了 triebeard 包,会试一试。

标签: r string tidyverse


【解决方案1】:

您可以通过在 C/C++ 中进行字符串比较来进行优化。我已经用 C 语言编写了一个 R 函数 match_start,如果你只有 1e+05 字符串,它应该可以帮助你完成大部分工作。

给定一个字符向量x 和一个正整数nchar_minmatch_start 返回一个长度为length(x) 的列表l,这样l[[i]] 要么是which(startWith(x, x[i])),要么是NULL——后者如果且仅当x[i]NAnchar(x[i]) 小于nchar_min

为了效率,match_start 假设x 的元素具有共同的编码,并且x 已经按升序排序。我会推荐:

  • 强制使用 enc2utf8 对非 ASCII 字符串进行 UTF-8 编码。
  • 使用 order(method = "radix") 对字符串进行排序,以便您始终使用 C 排序序列,以实现可重复性。

您可以查看?Encoding?locales?Comparison?sort 了解详情。 (一切都有些零散……)

sig <- c(x = "character", nchar_min = "integer")
bod <- '
int N = INTEGER(nchar_min)[0];
if (N < 1) {
    error("\'nchar_min\' must be positive");
}

R_xlen_t n = XLENGTH(x);
SEXP res = PROTECT(allocVector(VECSXP, n));

/* Pointer to array of pointers to strings */
const char* *tx = (const char* *) R_alloc(n, sizeof(char *));
/* Pointer to array of string lengths */
int         *nx = (int         *) R_alloc(n, sizeof(int));
/* Pointer to array of indices of matching strings */
int         *ix = (int         *) R_alloc(n, sizeof(int));

/* One loop to initialize */
SEXP el;
for (R_xlen_t i = 0; i < n; ++i) {
    el = STRING_ELT(x, i);
    if (el == NA_STRING) {
        nx[i] = -1;
    } else {
        tx[i] = CHAR(el);
        nx[i] = (int) strlen(tx[i]);
    }
}

/* Another loop to compare strings */
for (R_xlen_t i = 0, m = 1; i < n; ++i, m = 1) {
    if (nx[i] < N) {
        continue;
    }
    ix[0] = (int) i + 1;
    for (R_xlen_t j = i + 1; j < n; ++j, ++m) {
        if (nx[j] < nx[i] || memcmp(tx[i], tx[j], nx[i])) {
            break;
        }
        ix[m] = (int) j + 1;
    }
    el = PROTECT(allocVector(INTSXP, m));
    memcpy(INTEGER(el), ix, m * sizeof(int));
    SET_VECTOR_ELT(res, i, el);
    UNPROTECT(1);
}
UNPROTECT(1);
return res;
'
match_start <- inline::cfunction(sig, bod, language = "C")
df$text_utf8 <- enc2utf8(df$text)
o <- order(df$text_utf8, method = "radix")
l <- match_start(df$text_utf8[o], nchar_min = 5L)
df$id_match[o] <- lapply(l, function(i) if (!is.null(i)) df$id[o][i])
df
  id               text          text_utf8   id_match
1  1             i like             i like 1, 5, 2, 4
2  2          i like to          i like to       2, 4
3  3               oops               oops       NULL
4  4      i like to and      i like to and          4
5  5      i like it not      i like it not          5
6  6           victoria           victoria    6, 7, 8
7  7    victoria secret    victoria secret       7, 8
8  8 victoria secret is victoria secret is          8

match_start 不是最佳,因为存储所有索引向量的效率低下,而您只需要一个递归列表或树。我可能会担心树的深度,如果你认为完全嵌套你的字符串是一个真正的可能性。 (编辑: R 通过options(expressions=) 支持高达5e+05 的树深度,所以递归可能还是没问题的。)

【讨论】:

  • 不完全确定非 UTF 字符是什么,但我有很多“外来”字符,例如在印地语。使用我原始帖子的 cmets 中建议的这种前缀行军方法会破坏这些。所以处理这些字符确实是有益的。
  • 所以,响应的标记似乎有效(刚刚检查过),现在我需要比较不同的方法以确定哪种方法最适合我的情况。但是已经感谢您投入时间来帮助我!
  • Kevin Ushey 写了一篇不错的编码介绍here。我发现它很有帮助。
  • @deschen Sys.which("make") 说什么? make 在你的 PATH 中吗?如果您使用的是 Windows,那么也许您需要配置 Rtools?见here
  • @deschen 我会说enc2utf8 的行为符合预期。 ?Encoding 指出 ASCII 字符串从不标记编码。重要的是使用非 ASCII 字符的字符串被标记为 UTF-8。
【解决方案2】:

一些想法(必须在大数据集上检查运行时)。

  • gt5 是具有所需长度的字符串,应该是直截了当的。
  • occur 是多次出现的字符串。你可以忽略它,但我想知道它也可能很好。可能很贵。
  • gr 是按最大公分母出现的行数分组。仅当 text 按字符串长度排序时才有效(严格排序,或者像您的情况一样,按组集排序)。使用agrep 进行模糊匹配。 (我不知道它在大型数据集上的性能如何)。此外,如果您真的只关心第一个的 100% 匹配,例如字符串的 8 个字符,您可以将 agrep 替换为 grep(substr(x,1,8),substr(text,1,8))。 (句子开头的定义)
library(dplyr)

df %>% 
  mutate(gt5=nchar(text)>5, 
         occur=rowSums(sapply(unique(unlist(strsplit(text, " "))), function(x) 
           grepl(x,text)))>1, 
         gr=sapply(text, function(x) max(grep(substr(x,1,8),substr(text,1,8)))) )
  id               text   gt5 occur gr
1  1             i like  TRUE  TRUE  4
2  2          i like to  TRUE  TRUE  4
3  3               oops FALSE FALSE  3
4  4      i like to and  TRUE  TRUE  4
5  5      not i like is  TRUE  TRUE  5
6  6           victoria  TRUE  TRUE  8
7  7    victoria secret  TRUE  TRUE  8
8  8 victoria secret is  TRUE  TRUE  8

注意:当只允许一组时,分配 1 - 5 或 1 - 4 是一个决策问题。另一方面,允许多个组可能会增加组的数量。

【讨论】:

  • 非常感谢。需要稍微消化一下,因为我还不了解分配组的逻辑,例如为什么在您的情况下,第 1 行和第 5 行被组合在一起,而第 2 和第 4 行(理想情况下,它将第 1、2、4 行组合在一起。但不可否认,1 和 5 也是有效的选项,正如您在上面的注释中所说,分配几个组会增加很多复杂性。
  • @deschen 是的,这应该只是作为讨论的开始。随意拆开它:)您对“句子开头”的定义也可能会有所调整。到目前为止,这种使用 grep 的方法的好处是它只需要一次扫描文本。顺便说一句,如果您从 grep 中删除 max,您将获得所有组关系。
  • 我试图解构你的代码,并认为我现在理解了。但是,该方法不会只从一开始就查看文本。例如。如果我将第 5 个值更改为“我不喜欢是”,它仍会与第 1 行一起标记。
  • @deschen 在这种情况下,agrep 应替换为 grep。见编辑。
最近更新 更多