【问题标题】:Most frequent character in range范围内出现频率最高的字符
【发布时间】:2013-01-08 07:16:37
【问题描述】:

我有一个长度为n 的字符串s。用于查找i..j 范围内最常见字符的最有效数据结构/算法是什么?

字符串不会随着时间而改变,我只需要重复查询s[i]s[i + 1]、...、s[j] 中出现频率最高的字符。

【问题讨论】:

  • 字符串的预期长度和您感兴趣的间隔是多少
  • @Ivaylo 看起来是可变的。
  • n 的大小约为 10^6,我也需要大约 10^6 个查询。
  • ASCII、其他单字节字符集还是 Unicode?​​span>

标签: c++ character complexity-theory frequency mode


【解决方案1】:

最快的是使用unordered_map或类似的:

pair<char, int> fast(const string& s) {
    unordered_map<char, int> result;

    for(const auto i : s) ++result[i];
    return *max_element(cbegin(result), cend(result), [](const auto& lhs, const auto& rhs) { return lhs.second < rhs.second; });
}

最轻的,在记忆方面,需要一个可以排序的非常量输入,以便可以使用find_first_not_of 或类似的:

pair<char, int> light(string& s) {
    pair<char, int> result;
    int start = 0;

    sort(begin(s), end(s));
    for(auto finish = s.find_first_not_of(s.front()); finish != string::npos; start = finish, finish = s.find_first_not_of(s[start], start)) if(const int second = finish - start; second > result.second) result = make_pair(s[start], second);
    if(const int second = size(s) - start; second > result.second) result = make_pair(s[start], second);
    return result;
}

需要注意的是,这两个函数都有一个非空字符串的前提条件。此外,如果字符串中的字符数最多并列,则两个函数都将返回按字典顺序排列的字符数最多的字符。

Live Example

【讨论】:

    【解决方案2】:

    正如所建议的,最省时的算法是将每个字符的频率存储在一个数组中。但是请注意,如果您只是用字符索引数组,您可能会调用未定义的行为。即,如果您正在处理包含 0x00-0x7F 范围之外的代码点的文本,例如使用 UTF-8 编码的文本,则最多可能会出现分段违规,最坏的情况是堆栈数据损坏:

    char frequncies [256] = {};
    frequencies ['á'] = 9; // Oops. If our implementation represents char using a
                           // signed eight-bit integer, we just referenced memory
                           // outside of our array bounds!
    

    正确解决此问题的解决方案如下所示:

    template <typename charT>
    charT most_frequent (const basic_string <charT>& str)
    {
        constexpr auto charT_max = numeric_limits <charT>::max ();
        constexpr auto charT_min = numeric_limits <charT>::lowest ();
        size_t frequencies [charT_max - charT_min + 1] = {};
    
        for (auto c : str)
            ++frequencies [c - charT_min];
    
        charT most_frequent;
        size_t count = 0;
        for (charT c = charT_min; c < charT_max; ++c)
            if (frequencies [c - charT_min] > count)
            {
                most_frequent = c;
                count = frequencies [c - charT_min];
            }
    
        // We have to check charT_max outside of the loop,
        // as otherwise it will probably never terminate
        if (frequencies [charT_max - charT_min] > count)
            return charT_max;
    
        return most_frequent;
    }
    

    如果您想多次迭代同一个字符串,请将上述算法(如construct_array)修改为使用std::array &lt;size_t, numeric_limits &lt;charT&gt;::max () - numeric_limits &lt;charT&gt;::lowest () + 1&gt;。然后在第一个 for 循环之后返回该数组而不是最大字符,并省略算法中找到最频繁字符的部分。在您的顶级代码中构造一个 std::map &lt;std::string, std::array &lt;...&gt;&gt; 并将返回的数组存储在其中。然后将查找最常见字符的代码移到该顶级代码中并使用缓存的计数数组:

    char most_frequent (string s)
    {
        static map <string, array <...>> cache;
    
        if (cache.count (s) == 0)
            map [s] = construct_array (s);
        // find the most frequent character, as above, replacing `frequencies`
        // with map [s], then return it
    }
    

    现在,这只适用于整个字符串。如果你想重复处理相对较小的子字符串,你应该使用第一个版本。否则,我会说您最好的选择可能是执行第二种解决方案,但将字符串划分为可管理的块;这样,您可以从缓存中获取大部分信息,而只需重新计算迭代器所在的块中的频率。

    【讨论】:

      【解决方案3】:

      您需要在空间和时间复杂度方面指定您的算法要求。

      如果您坚持O(1) 空间复杂度,只需排序(例如,如果没有可用的自然比较运算符,则使用位的字典顺序)并计算最高元素的出现次数将为您提供O(N log N) 时间复杂度。

      如果您坚持 O(N) 时间复杂度,请使用 @Luchian Grigore 的解决方案,它也采用 O(N) 空间复杂度(好吧,O(K) 用于 K-字母字母表)。

      【讨论】:

      • 好点,默认情况下,我认为“最高效”是“最省时”。
      【解决方案4】:

      如果您希望在区间上获得有效的结果,您可以在序列的每个索引处构建一个积分分布向量。然后通过在 j+1 和 i 处减去积分分布,您可以从 s[i],s[i+1],...,s[j] 获得区间内的分布。

      以下是 Python 中的一些伪代码。我假设你的字符是字符,因此有 256 个分布条目。

      def buildIntegralDistributions(s):
          IDs=[]        # integral distribution
          D=[0]*256
          IDs.append(D[:])
          for x in s:
              D[ord(x)]+=1
              IDs.append(D[:])
          return IDs
      
      def getIntervalDistribution(IDs, i,j):
          D=[0]*256        
          for k in range(256):
              D[k]=IDs[j][k]-IDs[i][k]
          return D
      
      s='abababbbb'
      IDs=buildIntegralDistributions(s)
      Dij=getIntervalDistribution(IDs, 2,4)
      
      >>> s[2:4]
      'ab'
      >>> Dij[ord('a')]  # how many 'a'-s in s[2:4]?
      1
      >>> Dij[ord('b')]  # how many 'b'-s in s[2:4]?
      1
      

      【讨论】:

      • 我相信我在回答中已经解释了同样的想法
      • 我想过,但这不会有效,除非有一种非常快速的方法来减去分布。谢谢。
      • @user2007674:这个想法的适用性当然取决于你的间隔长度。如果您的间隔比字母表中的字符数短,那么从头开始重新计算分布会更有效。
      • @Ivaylo:是的,是一样的想法!
      【解决方案5】:

      假设字符串是常量,不同的ij 将被传递给查询发生。

      如果你想尽量减少处理时间,你可以做一个

      struct occurences{
          char c;
          std::list<int> positions;
      };
      

      并为每个字符保留一个std::list&lt;occurences&gt;。为了快速搜索,您可以保持positions 有序。

      如果你想最小化内存,你可以保持一个递增的整数并循环通过i .. j

      【讨论】:

        【解决方案6】:
        string="something"
        arrCount[string.length()];
        

        在每次访问字符串调用freq()之后

        freq(char accessedChar){
        arrCount[string.indexOf(x)]+=1
        }
        

        要获得最频繁的字符调用string.charAt(arrCount.max())

        【讨论】:

          【解决方案7】:

          对数组进行一次迭代,并为每个位置记住每个字符在该位置之前出现了多少次。所以是这样的:

          "abcdabc"
          

          对于索引 0:

          count['a'] = 1
          count['b'] = 0
          etc...
          

          对于索引 1:

          ....
          count['a'] = 1
          count['b'] = 1
          count['c'] = 0
          etc...
          

          对于索引 2:

          ....
          count['a'] = 1
          count['b'] = 1
          count['c'] = 1
          ....
          

          等等。对于索引 6:

          ....
          count['a'] = 2
          count['b'] = 2
          count['c'] = 2
          count['d'] = 1
          ... all others are 0
          

          计算完这个数组后,您可以得到给定字母在恒定时间内在区间 (i, j) 中的出现次数 - 只需计算 count[j] - count[i-1](此处注意 i = 0!)。

          因此,对于每个查询,您必须遍历所有字母而不是间隔中的所有字符,因此您最多只能遍历 128 个字符,而不是遍历 10^6 个字符(假设您只有 ASCII 符号)。

          一个缺点 - 您需要更多内存,具体取决于您使用的字母表的大小。

          【讨论】:

          • A drawback - you need more memory, depending on the size of the alphabet you are using. 对 10^6 大小为 10^6 的字符串的年度轻描淡写...
          • 这是有道理的,我的字符串中只有小写字母字符。但是,对于 10^6 长度的字符串,这将需要大约 2600 万次操作。这相当快,但我想知道是否有更快的解决方案。
          • @crush 的大小不是 10^6,而是字母表的大小,最多 128,我猜在常见情况下最多 52。
          • @user2007674 我想不出更快的解决方案,抱歉:(
          • 你有一个多维数组:arr[index][char]。可以有 10^6 个索引。
          【解决方案8】:

          保存每个字符出现次数的数组。您在遍历字符串一次时增加相应的值。这样做时,您可以记住数组中的当前最大值;或者,在最后的数组中查找最大值。

          伪代码

          arr = [0]
          for ( char in string )
             arr[char]++
          mostFrequent = highest(arr)
          

          【讨论】:

          • @IvayloStrandjev 添加答案:)
          • @IvayloStrandjev 我看不出这样更有效率 - 它基本上是完全相同的答案。
          • 他正在创建一个多维数组。
          • @LuchianGrigore 我可能误解了你的答案,但在我看来你的答案的复杂性是 O(NQ) 而我有 O(N + LQ) 其中L 是字母表的大小,Q - 查询数。
          • 我认为这里真正的问题是 OP 没有定义高效。在计算或存储方面是否需要高效?
          猜你喜欢
          • 2011-05-07
          • 1970-01-01
          • 2013-12-20
          • 1970-01-01
          • 1970-01-01
          • 2019-07-16
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多