在一个字符串中迭代单词的最有效方法

4

如果我想要遍历一个字符串中以空格分隔的单词,那么显而易见的解决方案就是:

std::istringstream s(myString);

std::string word;
while (s >> word)
    do things

然而,这种方法效率很低。在初始化字符串流时,整个字符串都被复制了一遍,然后每次从中提取一个单词时,该单词都会逐个字符地复制到 word 变量中(几乎等于再次复制整个字符串)。有没有一种方法可以在不手动迭代每个字符的情况下改善这种情况呢?


1
没有所谓的“最有效的方式”。 - Slava
你需要整个字符串吗?如果不需要,你可以直接从一开始读取单词。 - NathanOliver
“手动迭代每个字符”有什么问题吗?这可能是istringstream ::operator>>所做的(在将结果复制到“word”参数之上)。 - barak manos
@Slava 我同意,那是我措辞不当。我想我是指“比这种方式更有效率”。 - user1000039
3
你应该最先关注的是代码是否好、易读、易于维护和能够正常工作,而不是担心“低效”。只有当程序比某个要求慢时,才需要进行测量、基准测试和分析以找出热点和瓶颈,然后从那里开始着手解决。 - Some programmer dude
4个回答

5
在大多数情况下,复制只占总成本的一小部分,因此拥有干净、高可读性的代码变得更加重要。在极少数情况下,当时间分析器告诉你复制创建了瓶颈时,你可以借助标准库迭代字符串中的字符。
一种方法是使用std::string::find_first_of和std::string::find_first_not_of成员函数进行迭代,例如:
const std::string s = "quick \t\t brown \t fox jumps over the\nlazy dog";
const std::string ws = " \t\r\n";
std::size_t pos = 0;
while (pos != s.size()) {
    std::size_t from = s.find_first_not_of(ws, pos);
    if (from == std::string::npos) {
        break;
    }
    std::size_t to = s.find_first_of(ws, from+1);
    if (to == std::string::npos) {
        to = s.size();
    }
    // If you want an individual word, copy it with substr.
    // The code below simply prints it character-by-character:
    std::cout << "'";
    for (std::size_t i = from ; i != to ; i++) {
        std::cout << s[i];
    }
    std::cout << "'" << std::endl;
    pos = to;
}

演示。

不幸的是,代码变得更难阅读,所以应该避免这种改变,或者至少推迟到需要时再进行。


我有一种感觉,STL 的本地化部分可能隐藏着一种简单的方法,但如果没有的话,字符串流绝对是正确的选择。不过还是谢谢你的演示。 - user1000039
可以使用Boost字符串算法更轻松地完成这个任务(可能也更有效)。请参见我的答案。 - zett42

1
使用boost字符串算法,我们可以按如下方式编写代码。 循环不涉及任何字符串的复制。
#include <string>
#include <iostream>
#include <boost/algorithm/string.hpp>

int main()
{
    std::string s = "stack over   flow";

    auto it = boost::make_split_iterator( s, boost::token_finder( 
                          boost::is_any_of( " " ), boost::algorithm::token_compress_on ) );
    decltype( it ) end;

    for( ; it != end; ++it ) 
    {
        std::cout << "word: '" << *it << "'\n";
    }

    return 0;
}

使其具有C++11的特性

由于迭代器对现在来说已经很老派了,因此我们可以使用boost.range来定义一些通用的辅助函数。这些函数最终允许我们使用range-for循环遍历单词:

#include <string>
#include <iostream>
#include <boost/algorithm/string.hpp>
#include <boost/range/iterator_range_core.hpp>

template< typename Range >
using SplitRange = boost::iterator_range< boost::split_iterator< typename Range::const_iterator > >;

template< typename Range, typename Finder >
SplitRange< Range > make_split_range( const Range& rng, const Finder& finder )
{
    auto first = boost::make_split_iterator( rng, finder );
    decltype( first ) last;
    return {  first, last };
}

template< typename Range, typename Predicate >
SplitRange< Range > make_token_range( const Range& rng, const Predicate& pred )
{
    return make_split_range( rng, boost::token_finder( pred, boost::algorithm::token_compress_on ) );
}

int main()
{
    std::string str = "stack \tover\r\n  flow";

    for( const auto& substr : make_token_range( str, boost::is_any_of( " \t\r\n" ) ) )
    {
        std::cout << "word: '" << substr << "'\n";
    }

    return 0;
}

演示:

http://coliru.stacked-crooked.com/a/2f4b3d34086cc6ec


0

如果你想让它尽可能快,你需要回到老牌的C函数strtok()(或其线程安全的伴侣strtok_r()):

const char* kWhiteSpace = " \t\v\n\r";    //whatever you call white space

char* token = std::strtok(myString.data(), kWhiteSpace);
while(token) {
    //do things with token
    token = std::strtok(nullptr, kWhiteSpace));
}

请注意,这将覆盖myString的内容:它通过用终止空字节替换每个标记后的第一个分隔符字符,并依次返回标记的起始指针来工作。毕竟,这是一个传统的C函数。

然而,这个弱点也是它的优势:它不执行任何复制,也不分配任何动态内存(这可能是您示例代码中最耗时的事情)。因此,您不会找到比strtok()更快的本地C++方法。


根据C++参考文档,对通过调用data获取的数组进行修改会导致未定义的行为,因此可能需要将其复制到临时数组中。我还建议避免使用非可重入的strtok,而是更喜欢使用strtok_r。 - Sergey Kalinichenko
1
这仅涉及到string::data()的const重载。如果您有一个高级的C++17编译器,那么还有一个非const重载,它返回一个指向非const数组的指针,您可以对其进行修改。 - zett42
@dasblinkenlight 没错。我正在使用非const重载,因为我将生成的char*传递给std::strtok(),它要求作为第一个参数的是一个非const指针。所以,没有UB。显然,我同意您更喜欢strtok_r(),但是a)它似乎不可通过std::命名空间使用(当然这不会阻止我实际导入和使用C函数),并且b)如果您没有多个线程,则不必要(是的,有些实际软件设计为使用多进程而非多线程或者多线程根本毫无意义)。 - cmaster - reinstate monica

-1

字符串分割怎么样?您可以查看post获取更多信息。

在这篇文章中,有一个详细的答案关于如何将字符串分割成标记。在这个答案中,您可以查看第二种使用迭代器和复制算法的方法。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接