字符串操作 - 字符计数

Posted

技术标签:

【中文标题】字符串操作 - 字符计数【英文标题】:string manipulation - character counting 【发布时间】:2020-02-13 03:23:09 【问题描述】:

我有一个字符串S = "&|&&|&&&|&",我们应该在该字符串的两个索引之间获取'&' 的数量。 所以这里有 2 个索引 18 的输出应该是 5。这是我的蛮力风格代码:

std::size_t cnt = 0;
for(i = start; i < end; i++) 
    if (S[i] == '&')
        cnt++;

cout << cnt << endl;

我面临的问题是我的代码在编码平台中因较大的输入而超时。谁能在这里提出一个更好的方法来降低时间复杂度?

【问题讨论】:

这看起来像是来自一些在线测验/黑客网站的典型谜题。如果您的目标是学习 C++,那么您将不会在这里学到任何东西。在几乎所有情况下,就像这个一样,正确的解决方案需要了解某种数学或编程技巧。如果您不知道诀窍是什么,并尝试编写蛮力方法,那么您的程序将永远运行,并因此而失败。如果您正在尝试学习 C++,那么您将不会从无用的在线测验网站 but only from a good C++ book 中学到任何东西。 你可以使用segment trees来解决这个问题 如果你有一个查询,那么这似乎是你能得到的最好的,因为你不能真正假设任何一个位置的字符值。如果您有多个查询,那么您可以做的是预先生成一个数组,其中包含每个索引的计数,并让查询保持恒定时间。 我认为可能还有更多的条件,例如,一个字符串由两个字符&amp;|组成。或者,大多数由&amp;组成? 这个想法不是特定于 c++ 的,它通常适用于这类问题。通过生成数组,我的意思是遍历字符串并找出某个字符有多少,比如说'&amp;',在它之前的字符串中。如果第一个字符是“|”,则第一个元素(索引 0)以 0 开头或 1 如果 '&'。之后,遍历每个索引 1 【参考方案1】:

我决定尝试几种方法,包括该问题的其他两个答案提出的方法。我对输入做了几个假设,目的是为单个大字符串找到一个快速实现,该字符串只能针对单个字符搜索一次。对于将针对多个字符进行多次查询的字符串,我建议按照用户 Jefferson Rondan 的评论中的建议构建一个段树。

我使用std::chrono::steady_clock::now() 来衡量实施时间。


假设

    程序提示用户输入字符串大小、搜索字符、开始索引和结束索引。 输入格式正确(开始 字符串是从 ' ''~' 之间均匀分布的 ascii 字符随机生成的。 字符串对象中的基础数据连续存储在内存中。

方法

    朴素的 for 循环:索引变量递增,并使用索引逐个字符地对字符串进行索引。 迭代器循环:使用字符串迭代器,在每次迭代时取消引用,并与搜索字符进行比较。 底层数据指针:找到指向字符串底层字符数组的指针,并循环递增。将取消引用的指针与搜索字符进行比较。 索引映射(由 GyuHyeon Choi 建议):将 max printable ascii character 元素的 int 类型数组初始化为 0,并且对于在遍历数组时遇到的每个字符,相应的索引都会增加 1 .最后,取消引用搜索字符的索引以查找找到了多少个该字符。 只需使用 std::count(Atul Sharma 建议):只需使用构建计数功能。 将基础数据重新转换为指向更大数据类型的指针并进行迭代:保存string 数据的基础const char* const 指针被重新解释为指向更广泛数据类型的指针(在本例中为指向类型uint64_t的指针)。然后,每个取消引用的 uint64_t 都与由搜索字符组成的掩码进行异或运算,uint64_t 的每个字节都用0xff 掩码。这减少了遍历整个数组所需的指针增量。

结果

对于一个 1,000,000,000 大小的字符串,从索引 5 到 999999995 进行搜索,每个方法的结果如下:

    简单的 for 循环:843 毫秒 迭代器循环:818 毫秒 基础数据指针:750 毫秒 索引映射(由 GyuHyeon Choi 建议):929 ms 只需使用 std::count(Atul Sharma 建议):819 毫秒 将基础数据重铸为指向更大数据类型的指针并进行迭代:664 毫秒

讨论

表现最好的实现是我自己的数据指针重铸,它完成的时间比天真的解决方案所用时间的 75% 多一点。最快的“简单”解决方案是对底层数据结构的指针迭代。这种方法的优点是易于实施、理解和维护。索引映射方法尽管被宣传为比简单解决方案快 2 倍,但在我的基准测试中并没有看到这样的加速。 std::count 方法与手动指针迭代差不多快,而且实现起来更简单。如果速度真的很重要,请考虑重铸底层指针。否则,请坚持使用std::count


代码

#include <algorithm>
#include <iostream>
#include <random>
#include <string>
#include <functional>
#include <typeinfo>
#include <chrono>

int main(int argc, char** argv)

    std::random_device device;
    std::mt19937 generator(device());
    std::uniform_int_distribution<short> short_distribution(' ', '~');
    auto next_short = std::bind(short_distribution, generator);

    std::string random_string = "";
    size_t string_size;
    size_t start_search_index;
    size_t end_search_index;
    char search_char;
    std::cout << "String size: ";
    std::cin >> string_size;
    std::cout << "Search char: ";
    std::cin >> search_char;
    std::cout << "Start search index: ";
    std::cin >> start_search_index;
    std::cout << "End search index: ";
    std::cin >> end_search_index;

    if (!(start_search_index <= end_search_index && end_search_index <= string_size))
    
        std::cout << "Requires start_search <= end_search <= string_size\n";
        return 0;
    

    for (size_t i = 0; i < string_size; i++)
    
        random_string += static_cast<char>(next_short());
    

    // naive implementation
    size_t count = 0;
    auto start_time = std::chrono::steady_clock::now();
    for (size_t i = start_search_index; i < end_search_index; i++)
    
        if (random_string[i] == search_char)
            count++;
    
    auto end_time = std::chrono::steady_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end_time - start_time);
    std::cout << "Naive implementation. Found: " << count << "\n";
    std::cout << "Elapsed time: " << duration.count() << "us.\n\n";

    // Iterator solution
    count = 0;
    start_time = std::chrono::steady_clock::now();
    for (auto it = random_string.begin() + start_search_index, end = random_string.begin() + end_search_index;
        it != end;
        it++)
    
        if (*it == search_char)
            count++;
    
    end_time = std::chrono::steady_clock::now();
    duration = std::chrono::duration_cast<std::chrono::microseconds>(end_time - start_time);
    std::cout << "Iterator solution. Found: " << count << "\n";
    std::cout << "Elapsed time: " << duration.count() << "us.\n\n";

    // Iterate on data
    count = 0;
    start_time = std::chrono::steady_clock::now();
    for (auto it = random_string.data() + start_search_index,
        end = random_string.data() + end_search_index;
        it != end; it++)
    
        if (*it == search_char)
            count++;
    
    end_time = std::chrono::steady_clock::now();
    duration = std::chrono::duration_cast<std::chrono::microseconds>(end_time - start_time);
    std::cout << "Iterate on underlying data solution. Found: " << count << "\n";
    std::cout << "Elapsed time: " << duration.count() << "us.\n\n";

    // use index mapping
    count = 0;
    size_t count_array['~'] 0 ;
    start_time = std::chrono::steady_clock::now();
    for (size_t i = start_search_index; i < end_search_index; i++)
    
        count_array[random_string.at(i)]++;
    
    end_time = std::chrono::steady_clock::now();
    duration = std::chrono::duration_cast<std::chrono::microseconds>(end_time - start_time);
    count = count_array[search_char];
    std::cout << "Using index mapping. Found: " << count << "\n";
    std::cout << "Elapsed time: " << duration.count() << "us.\n\n";

    // using std::count
    count = 0;
    start_time = std::chrono::steady_clock::now();
    count = std::count(random_string.begin() + start_search_index
        , random_string.begin() + end_search_index
        , search_char);
    end_time = std::chrono::steady_clock::now();
    duration = std::chrono::duration_cast<std::chrono::microseconds>(end_time - start_time);
    std::cout << "Using std::count. Found: " << count << "\n";
    std::cout << "Elapsed time: " << duration.count() << "us.\n\n";

    // Iterate on larger type than underlying char
    count = end_search_index - start_search_index;
    start_time = std::chrono::steady_clock::now();
    // Iterate through underlying data until the address is modulo 4
    
        auto it = random_string.data() + start_search_index;
        auto end = random_string.data() + end_search_index;

        // iterate until we reach a pointer that is divisible by 8
        for (; (reinterpret_cast<std::uintptr_t>(it) & 0x07) && it != end; it++)
        
            if (*it != search_char)
                count--;
        

        // iterate on 8-byte sized chunks until we reach the last full chunk that is 8-byte aligned
        auto chunk_it = reinterpret_cast<const uint64_t* const>(it);
        auto chunk_end = reinterpret_cast<const uint64_t* const>((reinterpret_cast<std::uintptr_t>(end)) & ~0x07);
        uint64_t search_xor_mask = 0;
        for (size_t i = 0; i < 64; i+=8)
        
            search_xor_mask |= (static_cast<uint64_t>(search_char) << i);
        
        constexpr uint64_t all_ones = 0xff;
        for (; chunk_it != chunk_end; chunk_it++)
        
            auto chunk = (*chunk_it ^ search_xor_mask);
            if (chunk & (all_ones << 56))
                count--;
            if (chunk & (all_ones << 48))
                count--;
            if (chunk & (all_ones << 40))
                count--;
            if (chunk & (all_ones << 32))
                count--;
            if (chunk & (all_ones << 24))
                count--;
            if (chunk & (all_ones << 16))
                count--;
            if (chunk & (all_ones <<  8))
                count--;
            if (chunk & (all_ones <<  0))
                count--;
        

        // iterate on the remainder of the bytes, should be no more than 7, tops
        it = reinterpret_cast<decltype(it)>(chunk_it);
        for (; it != end; it++)
        
            if (*it != search_char)
                count--;
        
    
    end_time = std::chrono::steady_clock::now();
    duration = std::chrono::duration_cast<std::chrono::microseconds>(end_time - start_time);
    std::cout << "Iterate on underlying data with larger step sizes. Found: " << count << "\n";
    std::cout << "Elapsed time: " << duration.count() << "us.\n\n";


示例输出

String size: 1000000000
Search char: &
Start search index: 5
End search index: 999999995
Naive implementation. Found: 10527454
Elapsed time: 843179us.

Iterator solution. Found: 10527454
Elapsed time: 817762us.

Iterate on underlying data solution. Found: 10527454
Elapsed time: 749513us.

Using index mapping. Found: 10527454
Elapsed time: 928560us.

Using std::count. Found: 10527454
Elapsed time: 819412us.

Iterate on underlying data with larger step sizes. Found: 10527454
Elapsed time: 664338us.

【讨论】:

Native for with if 在每个循环中的语句比数组访问快吗?认真的吗? @GyuHyeonChoi 根据我的基准测试,是的。如果您能找到我的代码不如您的快的原因,我很高兴听到,因为我自己也是新手。 我编辑了你的代码。请重新对其进行基准测试。顺便说一句,为您的最后一个解决方案投票...!【参考方案2】:
int cnt[125];  // ASCII '&' = 46, '|' = 124
cnt['&'] = 0;
for(int i = start; i < end; i++) 
    cnt[S.at(i)]++;

cout << cnt['&'] << endl;

if 比较昂贵,因为它比较和分支。这样就更好了。

【讨论】:

这真的比原来的代码快吗? 是的,我测量了几次经过的时间,结果快了 2 倍以上。【参考方案3】:

您可以使用算法标准 C++ 库中的std::count。 只需包含标题&lt;algorithm&gt;

std::string s"&|&&|&&&|&";
// https://en.cppreference.com/w/cpp/algorithm/count
auto const count = std::count(s.begin() + 1  // starting index
                             ,s.begin() + 8  // one pass end index 
                             ,'&');

【讨论】:

作者要求提供性能更好的解决方案。只要std::count 将目标作为参数,它就需要进行比较...该函数在内部执行作者已经执行的操作。您的解决方案比作者已经完成的解决方案慢了大约两倍。

以上是关于字符串操作 - 字符计数的主要内容,如果未能解决你的问题,请参考以下文章

C 语言字符串操作 ( strlen 与 sizeof 函数 | 计算 字符串长度 与 内存块大小 )

如何使用java打印字符串中每个字符的计数

Python字符串基本操作

常见字符串操作

字符串简单操作

python基础之 列表元组操作 字符串操作 字典操作 集合操作 文件操作 字符编码与转码