如何在不使用循环的情况下简洁地找到字符串中的所有数字

How can I concisely find all digits in a string without using a loop?

本文关键字:字符串 数字 情况下 循环 简洁      更新时间:2023-10-16

我想获取std::string中的所有数字,但不要使用循环(我自己;我调用的代码使用什么,我不介意(。请求的另一种视图是:从字符串中删除所有非数字,只留下数字。我知道我可以使用这样的代码找到字符串中的所有数字:

std::string get_digits(std::string input) {
    std::string::size_type next_digit(0u);
    for (std::string::size_type pos(0u);
         input.npos != (pos = input.find_first_of("0123456789"));
         ++pos) {
        input[next_digit++] = input[pos];
    }
    input.resize(next_digit);
    return input;
}

但是,此函数使用循环。 std::string不提供find_all()功能或其他东西!理想情况下,字符串是就地操作的(上面的代码移动它,但它很容易更改为引用(。

当有多种选择时,我承诺会发布分析结果,说明不同方法在一些冗长的文本上的效果如何。

一种方法是使用std::copy_if(或std::remove_if(:

std::string get_digits(std::string input) {
    std::string result;
    std::copy_if(
        input.begin(), 
        input.end(), 
        std::back_inserter(result), 
        [](char c) { return '0' <= c && c <= '9'; });
    return result;
}

显然这在内部使用循环,但你说你不在乎这个......

编辑:与std::remove_if

std::string get_digits_remove(std::string input) {
    auto itErase = std::remove_if(
        input.begin(), 
        input.end(), 
        [](char c) { return !('0' <= c && c <= '9'); });
    input.erase(itErase, input.end());
    return input;
}

虽然我主要希望有 5 个快速答案(没有实现,叹息(,但答案和评论导致了一些我自己没有想到的有趣方法。我个人的期望是,这些答案将有效地导致:

  • 如果您想快速,请使用

    input.erase(std::remove_if(input.begin(), input.end(),
                               [](unsigned char c){ return !std::isdigit(c); }),
                input.end());
    
  • 如果要简洁,请使用

    text = std::regex_replace(text, std::regex(R"(D)"), "");
    

相反,有许多我什至没有考虑过的方法:

  • 使用递归函数!

  • 使用似乎需要额外工作的std::partition()(保留将被丢弃的字符(并更改顺序。

  • 使用似乎需要更多工作但不会更改顺序的std::stable_partition()

  • 使用 std::sort() 并提取带有相关字符的子字符串,尽管我不知道如何使该子字符串保留原始字符序列。仅仅使用稳定版本并不完全适合它。

将不同的方法放在一起,并在如何对字符进行分类方面使用许多变体,导致总共 17 个版本大致相同的操作(代码在 github 上(。大多数版本使用std::remove_if()std::string::erase(),但在数字分类上有所不同。

  1. remove_if()[](char c){ return d.find(c) == d.npos; }).
  2. remove_if() [](char c){ return std::find(d.begin(), d.end(), c) == d.end(); }
  3. remove_if()[](char c){ return !std::binary_search(d.begin(), d.end()); }
  4. remove_if() [](char c){ return '0' <= c && c <= '9'; }
  5. remove_if() [](unsigned char c){ return !std::isdigit(c); }(char作为unsigned char传递,以避免在c是负值char时出现未定义的行为(
  6. remove_if() std::not1(std::ptr_fun(std::static_cast<int(*)(int)>(&std::isdigit)))(强制转换对于确定正确的过载是必要的:std::isdigit()恰好过载(。
  7. remove_if()[&](char c){ return !hash.count(c); }
  8. remove_if() [&](char c){ return filter[c]; }(代码初始化实际上使用循环(
  9. remove_if() [&](char c){ return std::isidigit(c, locale); }
  10. remove_if() [&](char c){ return ctype.is(std::ctype_base::digit, c); }
  11. str.erase(std::parition(str.begin(), str.end(), [](unsigned char c){ return !std::isdigit(c); }), str.end())
  12. str.erase(std::stable_parition(str.begin(), str.end(), [](unsigned char c){ return !std::isdigit(c); }), str.end())
  13. 其中一个答案中描述的"排序方法">
  14. 答案之一中描述的copy_if()方法
  15. 递归方法在其中一个答案中描述
  16. text = std::regex_replace(text, std::regex(R"(D)"), "");(我没有设法让它在 icc 上工作(
  17. 像 16 但已经构建了正则表达式

我已经在MacOS笔记本电脑上运行了基准测试。由于这样的结果很容易用 Google Chars 绘制图表,因此这里是结果的图表(尽管删除了使用正则表达式的版本,因为这些版本会导致图表缩放,以至于有趣的位并不真正可见(。表格形式的基准测试结果:

    test                          clang   gcc     icc
 1  use_remove_if_str_find        22525   26846  24815
 2  use_remove_if_find            31787   23498  25379
 3  use_remove_if_binary_search   26709   27507  37016
 4  use_remove_if_compare          2375    2263   1847
 5  use_remove_if_ctype            1956    2209   2218
 6  use_remove_if_ctype_ptr_fun    1895    2304   2236
 7  use_remove_if_hash            79775   60554  81363
 8  use_remove_if_table            1967    2319   2769
 9  use_remove_if_locale_naive    17884   61096  21301
10  use_remove_if_locale           2801    5184   2776
11  use_partition                  1987    2260   2183
12  use_stable_partition           7134    4085  13094
13  use_sort                      59906  100581  67072
14  use_copy_if                    3615    2845   3654
15  use_recursive                  2524    2482   2560
16  regex_build                  758951  531641 
17  regex_prebuild               775450  519263

我会从一个很好的原始函数开始,它组成了你要使用的std算法:

template<class Container, class Test>
void erase_remove_if( Container&& c, Test&& test ) {
  using std::begin; using std::end;
  auto it = std::remove_if( begin(c), end(c), std::forward<Test>(test) );
  c.erase( it, end(c) );
}

然后我们写保存数字:

std::string save_digits( std::string s ) {
  erase_remove_if( s,
    [](char c){
      if (c > '9') return true;
      return c < '0';
    }
  );
  return s;
}
您可以使用std::partition

地执行此操作:

std::string get_digits(std::string& input)
{
    auto split =
        std::partition( std::begin(input), std::end(input), [](char c){return ::isdigit(c);} );
    size_t len = std::distance( std::begin(input), split );
    input.resize( len );
    return input;
}

std::partition不保证订单,因此如果订单很重要,请使用std::stable_partition

// terrible no-loop solution
void getDigs(const char* inp, char* dig)
{
    if (!*inp)
        return;
    if (*inp>='0' && *inp<='9')
    {
        *dig=*inp; 
        dig++;
        *dig=0;
    }
    getDigs(inp+1,dig);
}

也许简单的答案就足够了?

std::string only_the_digits(std::string s)
{
    s.erase(std::remove_if(s.begin(), s.end(),
                           [](char c) { return !::isdigit(c); }), s.end());
    return s;
}

这种方法的缺点是它无条件地创建输入数据的副本。如果有很多数字,那没关系,因为我们正在重用该对象。或者,您可以使此函数仅就地修改字符串 ( void strip_non_digits(std::string &) .(

但是,如果只有几个数字并且您希望保持输入不变,那么您可能更愿意创建一个新的(小(输出对象而不是复制输入。这可以通过输入字符串的引用视图来完成,例如,由基础 TS 提供,并使用 copy_if

std::string only_the_digits(std::experimental::string_view sv)
{
    std::string result;
    std::copy_if(sv.begin(), sv.end(), std::back_inserter(::isdigit));
    return result;
}

4 个步骤没有循环解决方案(但有错误检查,超过 4 个语句(:

1( 对字符串进行排序,使用合适的排序(递增顺序( ...现在所有数字将在一起,

合并

2( 使用 std::string.find_first_of(( 查找第一个数字的索引 (请务必检查找到的数字(

3( 使用 std::string.find_last_of(( 查找最后一个数字的索引 (请务必检查找到的数字(

4( 使用 std::string::substr(( 和前面的 2 个索引来提取数字

我认为这几乎是我所能得到的简洁。

std::string get_digits(std::string input)
{
    input.erase(std::stable_partition(
                               std::begin(input),
                               std::end(input),
                               ::isdigit),
                std::end(input));
    return input;
}

特征:

  1. 按值传递接收器参数以利用 C++11 中的复制 elision
  2. 保留数字顺序。
  3. 无用户代码 - 仅使用同行评审的 stl 函数。出现错误的几率 - 零。

这将是 stl 风格的基于迭代器的方法:

template<class InIter, class OutIter>
OutIter collect_digits(InIter first, InIter last, OutIter first_out)
{
    return std::copy_if(first, last, first_out, ::isdigit);
}

这有许多优点:

  1. 输入可以是任何可迭代的字符范围,而不仅仅是字符串
  2. 可以通过返回输出迭代器来链接
  3. 允许目标容器/迭代器(包括ostream_iterator(
  4. 只要有一点爱,它就可以处理 Unicode 字符等

有趣的例子:

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <iterator>
template<class InIter, class OutIter>
OutIter collect_digits(InIter first, InIter last, OutIter first_out)
{
    return std::copy_if(first, last, first_out, ::isdigit);
}
using namespace std;
int main()
{
    char chunk1[] = "abc123bca";
    string chunk2 { "def456fed" };
    vector<char> chunk3 = { 'g', 'h', 'i', '7', '8', '9', 'i', 'h', 'g' };
    string result;
    auto pos = collect_digits(begin(chunk1), end(chunk1), back_inserter(result));
    pos = collect_digits(begin(chunk2), end(chunk2), pos);
    collect_digits(begin(chunk3), end(chunk3), pos);
    cout << "first collect: " << result << endl;
    cout << "second collect: ";
    collect_digits(begin(chunk3),
                   end(chunk3),
                   collect_digits(begin(chunk2),
                                  end(chunk2),
                                  collect_digits(begin(chunk1),
                                                 end(chunk1),
                                                 ostream_iterator<char>(cout))));
    cout << endl;
    return 0;
}

只要#include <regex>在它之前,或者你以其他方式包含它,我就会使用这个单行宏:

#define DIGITS_IN_STRING(a) std::regex_replace(a, std::regex(R"([D])"), "")