【问题标题】:Best way to remove white spaces from std::string从 std::string 中删除空格的最佳方法
【发布时间】:2015-08-12 08:08:50
【问题描述】:

我已将文本文件内容加载到 std::string。我想从加载的字符串中删除空格。

  1. 需要使用以下哪种方法以获得更好的性能?
  2. 以下哪种方法是最佳实践?
  3. 还有其他更好的方法来实现这一点吗?

方法一:
在循环语句中使用 string.find() 扫描字符串并使用 string.erase() 删除空格;

方法二:
在循环语句中使用 string.find() 扫描字符串,并使用 string.append() 将非空白字符复制到新的 string() 变量中;

方法三:
在循环语句中使用 string.find() 扫描字符串,并使用 string.replace() 将非空白字符复制到新的 string(size_t n, char c) 变量中;

方法四:
分配一个 char*(使用 malloc[源字符串的大小])
在循环语句中使用 string.find() 扫描字符串,然后使用 strcpy 将非空白字符复制到 char* 变量,然后使用 strcat();
最后将 char* 复制到新字符串
免费字符*

【问题讨论】:

  • 你能改变文件的加载方式吗?
  • 您最好先确定存在性能问题,然后再关注性能。
  • @m.s.谢谢。这是最好的方法,当我从磁盘读取文件时,我可以使用方法 4 malloc(file_size)。但是我正在使用的 API 读取远程文件并返回字符串类型。我需要编辑问题吗?
  • 只需在您的问题中添加此信息,因为它限制了一些解决方案

标签: c++ c++11 c++14


【解决方案1】:

编辑:升级为区域感知特征。感谢 user657267 !

标准算法很简洁!

s.erase(std::remove_if(
    begin(s), end(s),
    [l = std::locale{}](auto ch) { return std::isspace(ch, l); }
), end(s));

Live on Coliru

那是就地修改,如果你需要保留原始字符串:

std::string s2;
s2.reserve(s.size());
std::remove_copy_if(
    begin(s), end(s),
    std::back_inserter(s2),
    [l = std::locale{}](auto ch) { return std::isspace(ch, l); }
);

Live on Coliru

【讨论】:

  • 我认为这是非常自我记录的代码,但性能可能是个问题。 AFAIK remove_if 使用数据转移来消除差距。
  • 对于未到位的情况,我们有remove_copy_if
  • @ddriver 标准算法当然是一个很好的第一次尝试。实施者应该知道他们的东西。
  • @ddriver 我不明白为什么会这样,但是我不是库实现者;)
  • @Quentin 假设你是对的。使用单独分配的字符串,需要每个非空白字符的一份副本。正确写为 remove_if,每个非空白字符只需要移动一次。
【解决方案2】:

恕我直言,您可以使用方法 2 获得最佳性能,但在附加之前,您需要调用 std::string::reserve 方法将新字符串的容量设置为初始字符串的大小。这需要在追加时防止不必要的重新分配。

【讨论】:

    【解决方案3】:

    为了便于阅读,我会选择boost string algorithm library

    #include <boost/algorithm/string.hpp>
    std::string str1="Hello  Dolly,   Hello World!"
    boost::erase_all(str1, " ");
    

    编辑: 所有空白字符的示例:

    std::string str1="Hello  Dolly,\n   Hello\t World!\t";
    boost::find_format_all(str1, boost::token_finder(::isspace), boost::const_formatter(""));
    

    EDIT2: 我运行了一些基准测试,看看这种方法与 Quentin 的答案相比如何。我使用isspace 的语言环境感知和非语言环境感知版本运行了 100 个样本。以微秒为单位的平均时间为:

    | method                            | avg. time (μs) |
    |-----------------------------------|----------------|
    | boost::find_format_all w/locale   | 2.02429        |
    | boost::find_format_all w/o locale | 0.578105588    |
    | std::remove_if w/locale           | 1.197737742    |
    | std::remove_if w/o locale         | 0.190661227    |
    

    【讨论】:

    • 反过来……并非所有的空格都是空白(“空格键”)。特别是,换行符和制表符也是空格。
    • @ArneVogel @ddriver 我添加了一个更通用的示例,它将替换::isspace 定义的所有空白字符。显然,这可以用一个区域感知版本来代替。
    【解决方案4】:

    不清楚您所说的“删除空格”到底是什么意思 - 它是打算使文本不可读并破坏源代码,还是您的意思是“任何多余的空格”?

    由于有一个答案建议使用第三方库,我将使用 Qt 中的一种方法,该方法将仅删除“多余的空白”:

    QString s("Test\n new line,   multiple spacebars \t tab!");
    
    qDebug() << s;
    qDebug() << s.simplified();
    

    输出:

    "Test
     new line,   multiple spacebars      tab!"
    
    "Test new line, multiple spacebars tab!"
    

    大多数其他答案都集中在删除所有空格键上,但严格来说,还有其他字符被归类为空格,例如制表符或换行符。

    查看simplified() 方法的代码,我会说它相当有效:

    QString QString::simplified() const
    {
        if (d->size == 0)
            return *this;
        QString result(d->size, Qt::Uninitialized);
        const QChar *from = (const QChar*) d->data;
        const QChar *fromend = (const QChar*) from+d->size;
        int outc=0;
        QChar *to   = (QChar*) result.d->data;
        for (;;) {
            while (from!=fromend && from->isSpace())
                from++;
            while (from!=fromend && !from->isSpace())
                to[outc++] = *from++;
            if (from!=fromend)
                to[outc++] = QLatin1Char(' ');
            else
                break;
        }
        if (outc > 0 && to[outc-1] == QLatin1Char(' '))
            outc--;
        result.truncate(outc);
        return result;
    }
    

    新字符串在没有初始化的情况下进行预分配,因此避免了任何初始化和任何重新分配,然后类似于方法2(请记住在不保留空间的情况下追加会导致重新分配很多且缓慢),仅将有用的数据复制到结果字符串,最后,它被修剪得更短以消除任何浪费的内存。

    你可以按照这个逻辑为std::string高效地实现它

    直接取自Qt source code

    【讨论】:

      【解决方案5】:

      方法 5:使用库覆盖所有边缘情况(包括当前语言环境),当且仅当分析表明这是一个问题时才进行调整。

      #include <algorithm>
      #include <functional>
      #include <iostream>
      #include <locale>
      #include <string>
      
      template<typename CharT, typename Traits, typename Allocator>
      std::basic_string<CharT, Traits, Allocator>
      strip_whitespace(std::basic_string<CharT, Traits, Allocator> str)
      {
        str.erase(
          remove_if(str.begin(), str.end(), bind(
            std::isspace<CharT>, std::placeholders::_1, std::locale{}
          )),
          str.end()
        );
        return str;
      }
      
      int main()
      {
        std::string str{"A string with \n whitespace"};
        std::cout << str << '\n' << strip_whitespace(str) << '\n';
      }
      

      【讨论】:

        【解决方案6】:

        为了提高性能,最好一次读取一个平台字的字符串(在 64 位平台上为 8 个字节),然后从读取的寄存器中提取每个字符,测试它是否有空格,如果它不是空格,将其添加到接下来要写入的平台字宽寄存器中。当要写入的寄存器已满时(尽可能多地存储到寄存器中的字符),将其写入预先分配的输出缓冲区。对于单字节字符串,逐字符扫描速度要慢 8 倍。

        最后,字符串尾部包含的字符可能比占用完整平台单词的字符少。然后需要一个计数器来了解最后处理的平台字中有多少字符。

        【讨论】:

        • 这是一个微优化。您应该依靠编译器来完成它。您应该仅在分析之后才使用此 *if ever)。
        • @bolov,我分析了一个逐字节复制的案例。它确实比一次复制 64 位值要慢得多。假设问题认为这个字符串处理是一个瓶颈,那么优化是有意义的。而8倍的瓶颈提升也不是“微”。
        • 我并不是说它不好或更快。只是考虑寄存器大小和公司是一个微优化,不管你得到什么加速。只是说当你开始编码时你不应该跳到这个。您应该知道并理解这一点,但只有在分析之后才考虑它。我并不是说这是一个不好的答案或建议,只是想澄清一下。
        • 我无法对此进行测试。我很惊讶一个简单的 for 没有优化...... 8 倍加速似乎可疑......
        • ...虽然你可能是对的。 s[i] 和 s[i+1] 之间存在依赖关系,因此编译器可能必须保持访问顺序......
        猜你喜欢
        • 2012-12-23
        • 2021-05-21
        • 1970-01-01
        • 1970-01-01
        • 2020-04-20
        • 1970-01-01
        • 2019-01-23
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多