【问题标题】:Faster way to split a string into substrings of equal length将字符串拆分为等长子字符串的更快方法
【发布时间】:2016-04-17 05:45:40
【问题描述】:

我想创建一个函数来将字符串拆分为等长的子字符串n 一个字符一个字符并返回一个字符向量。

例如F('atgctgttg',n=5) 应该返回

'atgct','tgctg','gctgt','ctgtt','tgttg'

我尝试了两种不同的功能:

// [[Rcpp::export]]
CharacterVector f( const std::string str, const int n ) {
    int lim = str.length() - n + 1;
    CharacterVector result( lim );
    for ( int j = 0; j < lim; j++ )
    { 
        result[j] = str.substr( j, n );
    }
    return result;
}

// [[Rcpp::export]]
CharacterVector f1( const std::string str, const int n ) {
    const int lim = str.length();
    const int n1 = n - 1;
    CharacterVector result( lim - n1 );
    int j = 1;
    std::string tmp = str.substr( 0, n );
    result[0] = tmp;

    for ( int i = n; i < lim; i++ )
    {
        tmp.erase( 0, 1 );
        tmp.push_back( str[i] );
        result[j] = tmp;
        j++;
    }
    return result;
}

我也尝试过使用迭代器,但它并不比函数 f1 快。 请注意,Rcpp 将输入转换为参考变量。 有没有更快的方法来做到这一点?

【问题讨论】:

  • 我目前没有看到。我几乎不敢问,但f1() 真的比f()吗?那是可怕的代码...
  • 您没有指定 CharacterVector 是什么,并且通过 const&amp; 传递 std::string 比仅使用 const 更有意义。
  • 假设CharacterVectorstd::vector 的typedef,您可能希望在任何推回之前调用reserve,而不是使用所有空字符串对其进行初始化。
  • @MohamedEzzeddineMacherki:您是否测量 f1 更快,或者您是否假设?你有没有测量过你在这里一个性能问题(我觉得很难相信)?您是否尝试过const std::string &amp; str 而不是const std::string str,这可能比f2 中的任何有趣的事情都能为您带来更多的性能提升?而且,AndyG 所说的……
  • 也许std::experimental::basic_string_view 可用于您的编译器?请参阅 reference 和另一个 SO post 关于该主题。

标签: c++ r rcpp


【解决方案1】:

编译器会将您的f 函数转换为最快的代码如果您更改为通过引用复制:CharacterVector f(const std::string&amp; str, const int n)


虽然您不会看到速度提升,但您绝对可以通过取消 CharacterVector 而只使用 vector&lt;string&gt; 来简化您的流程:

const string str("atgctgttg");
const int n = 5; // Assumed positive number smaller than str.size()
const int n1 = n - 1;
vector<string> result(str.size() - n1);

transform(str.cbegin(), str.cend() - n1, result.begin(), [n](const auto& i) {return string(&i, n);});

[Live Example]


可以看到速度改进的一种方法是您是否可以使用array 而不是string

const string str("atgctgttg");
const int n1 = N - 1;
vector<array<char, N>> result(str.size() - n1);

transform(str.cbegin(), str.cend() - n1, result.begin(), [](const auto& i) {
    array<char, N> result;

    copy_n(&i, N, result.begin());
    return result;
});

[Live Example]


但到目前为止,最快(也是最好)的方法就是在原始 string 上工作,将其分解为 strings 的数组。这需要在后端做更多的工作,因为您需要使用 c 字符串而不是 std::strings。例如,我用for (auto&amp; i : result) cout &lt;&lt; string(i.data(), N) &lt;&lt; endl; 打印了我所有的vectors,但如果你没有使用vector,你可以打印如下:for (auto i = str.cbegin(); i != str.cend() - n1; ++i) printf("%.*s\n", n, &amp;*i); 显然还有更多工作,但如果你的str很大,你会发现它要快得多。

[Live example]

【讨论】:

  • 关于&,Rcpp自动管理函数的引用设置。如果你通过引用插入变量会更复杂,代码的运行时间也会增加
【解决方案2】:

我将使用的方法是在字符串的开头创建一个迭代器,然后在第一个子字符串的末尾创建一个迭代器。然后使用std::vector 使用emplace_back() 在作为子字符串的向量末尾构造一个字符串。然后递增两个迭代器,直到到达末尾。

std::vector<std::string> splitString(const std::string& str, std::size_t len)
{
    if (len >= str.size())
        return { str };
    auto it = str.begin();
    auto end = it + len;
    std::vector<std::string> strings;
    while (end != str.end())
    {
        strings.emplace_back(it, end);
        ++end;
        ++it;
    }
    // have to do this to get the last string since end == str.end()
    strings.emplace_back(it, end);
    return strings;
}

Live Example

【讨论】:

  • @MohamedEzzeddineMacherki 您的代码中的CharacterVector 是什么?
  • Charactervector是一个rcpp对象,可以在代码中转换成vector
【解决方案3】:

首先,你的函数签名有问题:

CharacterVector f( const std::string str, const int n )

您通过值传递string,在函数的每次调用中都会有一个字符串副本(除非您使用 C++11 传递可移动字符串)。最好通过 const 引用 const std::string&amp; str 传递字符串。

关于这个问题,我想到了两个可能的答案。

  1. 实际上返回输入字符串字符的副本。在这种情况下,按索引迭代字符串并在结构中插入新字符串,如代码示例 1 所示,应该很快(可能更快的是只有 1 个副本,即子字符串到结构的副本)。
  2. 返回指向真实字符串的指针结构。例如:返回包含字符串中子字符串(开始,结束)的代理对象。优点是不是字符串的副本。例如:

代码(测试:GCC 4.9.2 with C++11)

#include <iostream>
#include <vector>

struct string_ref {
    const char* start;
    const char* end;
};

// [[Rcpp::export]]
std::vector<string_ref> f(std::string&&, const int) = delete; // disallow calls with temporaries
// [[Rcpp::export]]
std::vector<string_ref> f(const std::string& str, const int n) {
    int lim = str.length() - n + 1;
    std::vector<string_ref> result(lim);
    for (int j = 0; j < lim; j++) {
        result[j] = { &str[j], &str[j + n] };
    }
    return result;
}

int main() {
    std::string input{"atgctgttg"};
    auto result = f(input, 5);
    for (const auto r : result) {
        std::cout << std::string(r.start, r.end) << std::endl;
    }
    return 0;
}

许多解析文本的库(例如:词法分析器、正则表达式引擎等)都使用此方法。对于 C++17,建议使用类型 std::string_view,以引用部分或全部字符串字符。

根据代码中的注释,您正在实现要在 R 中使用的函数(不确切知道),在这种情况下,第二个解决方案可能会带来内存访问问题(输入字符串内存需要可访问并且在使用子字符串指针时存活)。如果输入字符串是在 R 中创建并调用F,则返回指针很可能是有效的,更好的证明是测试。

问题中的代码 2 示例。第一个会更快,因为在每个循环的第二个中,都有一个字符的擦除和 push_back(在大多数 STL 实现中擦除第一个字符很可能需要字符串的所有其他字符的副本), push_back 可以在某些情况下需要扩展字符串的内存。

【讨论】:

  • 我认为在您的示例中,您可能希望使用普通的 lvaue 引用而不是 const &amp;。我不是 100% 确定,但是如果您使用临时字符串调用该函数,那么您将存储指向在函数调用后被删除的缓冲区的指针。
  • 是的,在传递临时字符串的情况下,指针指向坏内存,非常感谢,代码更新,删除带有左值参数的重载。
  • 想出点子,但我使用源 Cpp 尝试了这段代码,但它不起作用。我认为 rcpp 和 cpp 结构之间存在差异
  • 很可能是c++11支持,这段代码可以修改为c++03:不要使用auto(使用实际类型),不要使用delete(用@987654328添加正文@)。
猜你喜欢
  • 2011-04-11
  • 1970-01-01
  • 2014-01-07
  • 2011-04-15
  • 2021-08-11
  • 1970-01-01
  • 2016-03-01
相关资源
最近更新 更多