按照字符出现频率和字母顺序对字符串中的字符进行排序

4
给定一个字符串,我正在尝试计算该字符串中每个字母出现的次数,并按照其频率从高到低对它们进行排序。对于具有相似出现次数的字母,我必须按字母顺序对它们进行排序。
这是我目前所做的:
- 我创建了一个大小为26的int数组,对应于字母表中的26个字母,其各自的值表示它在句子中出现的次数。 - 我将此数组的内容推入一个包含int和char(频率和实际字母)的pair的vector v中。 - 我使用`std::sort(v.begin(), v.end());`来对这个pair的vector进行排序。
在显示频率计数时,我只使用了一个for循环,从最后一个索引开始,以从高到低的顺序显示结果。然而,对于那些具有相似频率的字母,我遇到了问题,因为我需要按字母顺序显示它们。我尝试使用内层循环,内层循环从最低索引开始,并使用条件语句检查其频率是否与外层循环相同。这似乎有效,但我的问题是我无法控制这些循环,以避免冗余输出。请看下面的示例输出,以理解我的意思:
Enter a string: hello world

Pushing the array into a vector pair v:
d = 1
e = 1
h = 1
l = 3
o = 2
r = 1
w = 1


Sorted first according to frequency then alphabetically:
l = 3
o = 2
d = 1
e = 1
h = 1
r = 1
w = 1
d = 1
e = 1
h = 1
r = 1
d = 1
e = 1
h = 1
d = 1
e = 1
d = 1
Press any key to continue . . .

正如您所见,如果不是由于错误的for循环导致的冗余输出,那么一切都将是完美的。

如果您能就我的问题提供更有效或更好的实现建议,我将非常感激,只要它们不太复杂或过于先进,因为我只是一个C++初学者。

如果您需要查看我的代码,请参阅下面:

#include <iostream>
#include <string>
#include <vector>
#include <algorithm>

using namespace std;

int main() {
    cout<<"Enter a string: ";
    string input;
    getline(cin, input);

    int letters[26]= {0};

    for (int x = 0; x < input.length(); x++) {
        if (isalpha(input[x])) {
            int c = tolower(input[x] - 'a');
            letters[c]++;
        }
    }

    cout<<"\nPushing the array into a vector pair v: \n";
    vector<pair<int, char> > v;

    for (int x = 0; x < 26; x++) {
        if (letters[x] > 0) {
            char c = x + 'a';
            cout << c << " = " << letters[x] << "\n";
            v.push_back(std::make_pair(letters[x], c));
        }
    }

    // Sort the vector of pairs.
    std::sort(v.begin(), v.end());

    // I need help here!
    cout<<"\n\nSorted first according to frequency then alphabetically: \n";
    for (int x = v.size() - 1 ; x >= 0; x--) {
        for (int y = 0; y < x; y++) {
            if (v[x].first == v[y].first) {
                cout << v[y].second<< " = " << v[y].first<<endl;
            }
        }
        cout << v[x].second<< " = " << v[x].first<<endl;
    }

    system("pause");
    return 0;
}

3
您可以通过运行带有自定义“比较器”的排序(请参见http://en.cppreference.com/w/cpp/algorithm/sort以获取示例),以单步解决此问题。 - Oliver Charlesworth
你也可以使用 map<char, int> - Gabriel L.
@Gabriel L.,即使我使用了一个映射表,我仍然无法直接对其值进行排序,是吗?谢谢! - makki
7个回答

5
您可以通过两个步骤来大幅简化它,具体如下:
  1. First use a map to count the number of occurrences of each character in the string:

    std::unordered_map<char, unsigned int> count;
    
    for( char character : string )
        count[character]++;
    
  2. Use the values of that map as comparison criteria:

    std::sort( std::begin( string ) , std::end( string ) , 
               [&]( char lhs , char rhs )
               {
                   return count[lhs] < count[rhs];
               }
             ); 
    

这里有一个在ideone上运行的工作示例。


1
这个例子按字符出现次数正确计数,但之后没有按字母顺序排序! - caps
这段代码有一个错误,我不知道排序在哪里失败了,但是请尝试使用字符串 "AZBIPTOFTJCJJIK"。输出是 JJJITTIAZBPOFCK 注意看 "I" 字符被插入到两个非连续的位置! - Bilal Ahmed

3

如果您希望得到最高频率的最低字母,一种简单的方法是在排序前将频率存储为负值,然后在排序后再取反。但更有效的方法是更改用于排序的函数,不过这需要一定的技巧:

struct sort_helper {
   bool operator()(std::pair<int,char> lhs, std::pair<int,char> rhs) const{
     return std::make_pair(-lhs.first,lhs.second)<std::make_pair(-rhs.first,rhs.second);
   }
};
std::sort(vec.begin(),vec.end(),sort_helper());

2
// CODE BY VIJAY JANGID in C language
// Using arrays, Time complexity - ( O(N) * distinct characters ) 
// Efficient answer

#include <stdio.h>

int main() {

    int iSizeFrequencyArray= 58;
    //  122 - 65 = 57  for A to z
    int frequencyArray[iSizeFrequencyArray]; 

    int iIndex = 0;

    // Initializing frequency to zero for all
    for (iIndex = 0; iIndex < iSizeFrequencyArray; iIndex++) {
        frequencyArray[iIndex] = 0;
    }

    int iMyStringLength = 1000;
    char chMyString[iMyStringLength];

    // take input for the string
    scanf("%s", &chMyString);

    // calculating length
    int iSizeMyString;
    while(chMyString[++iSizeMyString]);

    // saving each character frequency in the freq. array
    for (iIndex = 0; iIndex < iSizeMyString; iIndex++) {
        int currentChar = chMyString[iIndex];
        frequencyArray[currentChar - 65]++;
    }

    /* // To print the frequency of each alphabet
    for (iIndex = 0; iIndex < iSizeFrequencyArray; iIndex++) {
        char currentChar = iIndex + 65;
        printf("\n%c - %d", currentChar, frequencyArray[iIndex ]);
    }
    */

    int lowestDone = 0, lowest = 0, highestSeen = 0;

    for( iIndex = 0; iIndex < iSizeFrequencyArray; iIndex++ ) {
        if(frequencyArray[iIndex] > highestSeen) {
            highestSeen = frequencyArray[iIndex];
        }
    }

    // assigning sorted values to the current array
    while (lowest != highestSeen) {

        // calculating lowest frequency
        for( iIndex = 0; iIndex < iSizeFrequencyArray; iIndex++ ) {

            if( frequencyArray[iIndex] > lowestDone &&
               frequencyArray[iIndex] < lowest) {
                lowest = frequencyArray[iIndex]; // taking lowest value
            }
        }

        // printing that frequency
        for( iIndex =0; iIndex < iSizeFrequencyArray; iIndex++ ) {

            // print that work for that times
            if(frequencyArray[iIndex] == lowest){
                char currentChar = iIndex + 65;
                int iIndex3;
                for(iIndex3 = 0; iIndex3 < lowest; iIndex3++){
                    printf("%c", currentChar);
                }
            }
        }

        // now that is done, move to next lowest
        lowestDone = lowest;

        // reset to highest value, to get the next lowest one
        lowest = highestSeen+1;

    }

    return 0;

}

解释:

  1. 首先创建一个大小为(112-65)的数组,用于存储ASCII字符A到z。
  2. 通过在每次出现时递增来存储每个字符的频率。
  3. 现在找出最高频率。
  4. 运行一个循环,条件为(lowest!= highest),其中最初的lowest = 0。
  5. 现在在每个迭代中打印出频率等于lowest的字符。它们将自动按字母顺序排列。
  6. 最后找到下一个更高的频率并打印,以此类推。
  7. 当lowest达到highest时,退出循环。

2

(代表原帖作者发布。)

感谢 Stack Overflow 社区中的众多热心人士的回复,我终于解决了我的问题。以下是我的最终代码,供有兴趣的人参考,或者给那些可能陷入同样困境的人提供未来参考:

#include <iostream>
#include <string>
#include <vector>
#include <algorithm>

using namespace std;

struct Letters
{
    Letters() : freq(0){}
    Letters(char letter,int freq) {
        this->freq = freq;
        this->letter = letter;
    }
    char letter;
    int freq;
};

bool Greater(const Letters& a, const Letters& b)
{
    if(a.freq == b.freq)
        return a.letter < b.letter;

    return a.freq > b.freq;
}

int main () {

    cout<<"Enter a string: ";
    string input;
    getline(cin, input);

    vector<Letters> count;
    int letters[26]= {0};

    for (int x = 0; x < input.length(); x++) {
        if (isalpha(input[x])) {
            int c = tolower(input[x] - 'a');
            letters[c]++;
        }
    }

    for (int x = 0; x < 26; x++) {
        if (letters[x] > 0) {
            char c = x + 'a';
            count.push_back(Letters(c, letters[x]));
        }
    }

    cout<<"\nUnsorted list..\n";
    for (int x = 0 ; x < count.size(); x++) {
        cout<<count[x].letter<< " = "<< count[x].freq<<"\n";
    }

    std::sort(count.begin(),count.end(),Greater);

    cout<<"\nSorted list according to frequency then alphabetically..\n";
    for (int x = 0 ; x < count.size(); x++) {
        cout<<count[x].letter<< " = "<< count[x].freq<<"\n";
    }

    system("pause");
    return 0;
}

示例输出:

Enter a string: hello world

Unsorted list..
d = 1
e = 1
h = 1
l = 3
o = 2
r = 1
w = 1

Sorted list according to frequency then alphabetically..
l = 3
o = 2
d = 1
e = 1
h = 1
r = 1
w = 1
Press any key to continue . . .

我基本上只是遵循@OliCharlesworth的建议,并通过这篇指南帮助实现了自定义比较器:A Function Pointer as Comparison Function

虽然我相信我的代码仍然可以更加高效,但我仍然对结果感到满意。


1
#include<stdio.h>

// CODE BY AKSHAY BHADERIYA

char iFrequencySort (char iString[]);
void vSort (int arr[], int arr1[], int len);

int
main ()
{
  int iLen, iCount;
  char iString[100], str[100];
  printf ("Enter a string : ");
  scanf ("%s", iString);

  iFrequencySort (iString);

  return 0;
}


char
iFrequencySort (char iString[])
{
  int iFreq[100] = { 0 };
  int iI, iJ, iK, iAsc, iLen1 = 0, iLen = 0;

  while (iString[++iLen]);

  int iOccurrence[94];
  int iCharacter[94];

  for (iI = 0; iI < iLen; iI++)
    {               //frequency of the characters
      iAsc = (int) iString[iI];
      iFreq[iAsc - 32]++;
    }



  for (iI = 0, iJ = 0; iI < 94; iI++)
    {               //the characters and occurrence arrays
      if (iFreq[iI] != 0)
    {
      iCharacter[iJ] = iI;
      iOccurrence[iJ] = iFreq[iI];
      iJ++;
    }
    }
  iLen1 = iJ;

  vSort (iOccurrence, iCharacter, iLen1);   //sorting both arrays

  /*letter array consists only the index of iFreq array.
     Converting it to the ASCII value of corresponding character */
  for (iI = 0; iI < iLen1; iI++)
    {
      iCharacter[iI] += 32;
    }
  iK = 0;
  for (iI = 0; iI < iLen1; iI++)
    {               //characters into original string
      for (iJ = 0; iJ < iOccurrence[iI]; iJ++)
    {
      iString[iK++] = (char) iCharacter[iI];
    }
    }
  printf ("%s", iString);
}

void
vSort (int iOccurrence[], int iCharacter[], int len)
{
  int iI, iJ, iTemp;
  for (iI = 0; iI < len - 1; iI++)
    {
      for (iJ = iI + 1; iJ < len; iJ++)
    {
      if (iOccurrence[iI] > iOccurrence[iJ])
        {
          iTemp = iOccurrence[iI];
          iOccurrence[iI] = iOccurrence[iJ];
          iOccurrence[iJ] = iTemp;

          iTemp = iCharacter[iI];
          iCharacter[iI] = iCharacter[iJ];
          iCharacter[iJ] = iTemp;
        }
    }
    }
}

1
答案已经给出并被接受。我想再提供一个标准方法来完成此任务。
通常需要先计算某些东西的数量,然后获取它们的排名、一些最高值或其他信息。
其中最常见的解决方案之一是使用所谓的关联容器,特别是使用 std::map 或更好的 std::unordered_map。这是因为我们需要一个键值,以上述描述的方式为字母和相关值,这里是该字母的计数。键是唯一的。它中不能有多个相同的字母。这当然毫无意义。
通过按其键值访问元素,关联容器非常高效。
好的,有两种。std::map 和 std::unordered_map。一种使用树以有序方式存储键,另一种使用快速哈希算法访问键值。由于我们后来对排序键不感兴趣,而是对出现次数进行排序,因此我们可以选择 std::unordred_map。作为进一步的好处,这将使用快速访问键所提到的哈希算法。
映射具有额外的巨大优势。它们具有索引运算符 [],该运算符将非常快地查找键值。如果找到,则返回与键关联的值的引用。如果未找到,则创建一个键并使用默认值(在我们的情况下为 0)初始化其值。然后,任何键的计数都很简单,只需使用 map[key]++。
但是,后来我们经常听到这样的话:但必须按计数排序。当然不行,因为计数可能具有重复值,并且映射只能包含唯一的键值。所以,不可能。
解决方案是使用第二个关联容器 std::multiset,该容器可以具有更多相同的键和自定义排序运算符,我们可以根据值进行排序。在其中,我们不是将键和值存储为 2 个元素,而是使用 std::pair 存储两个值。并且我们按照 pair 的第二部分进行排序。
我们无法在第一次使用 std::multi:set,因为我们需要唯一键(在此情况下为字母)。
上述方法给我们带来了极大的灵活性和易于使用性。我们基本上可以使用此算法计算任何内容。
例如,它可能看起来像下面的紧凑代码:
#include <iostream>
#include <string>
#include <utility>
#include <set>
#include <unordered_map>
#include <type_traits>
#include <cctype>

// ------------------------------------------------------------
// Create aliases. Save typing work and make code more readable
using Pair = std::pair<char, unsigned int>;

// Standard approach for counter
using Counter = std::unordered_map<Pair::first_type, Pair::second_type>;

// Sorted values will be stored in a multiset
struct Comp { bool operator ()(const Pair& p1, const Pair& p2) const { return (p1.second == p2.second) ? p1.first<p2.first : p1.second>p2.second; } };
using Rank = std::multiset<Pair, Comp>;
// ------------------------------------------------------------


// --------------------------------------------------------------------------------------
// Compact function to calculate the frequency of charcters and then get their rank
Rank getRank(std::string& text) {

    // Definition of our counter
    Counter counter{};

    // Iterate over all charcters in text and count their frequency
    for (const char c : text) if (std::isalpha(c)) counter[char(std::tolower(c))]++;
    
    // Return ranks,sorted by frequency and then sorted by character
    return { counter.begin(), counter.end() };
}
// --------------------------------------------------------------------------------------
// Test, driver code
int main() {
    // Get a string from the user
    if (std::string text{}; std::getline(std::cin, text))

        // Calculate rank and show result
        for (const auto& [letter, count] : getRank(text))
            std::cout << letter << " = " << count << '\n';
}

请看使用的最简语句,非常优雅。
但是,我们经常看到数组被用作关联容器。它们也有一个索引(键)和一个值。缺点可能是对于未使用的键存在微小的空间开销。此外,它们只适用于某些已知数量的东西。例如,对于26个字母。其他国家的字母表可能有更多或更少的字母。那么这种解决方案就不那么灵活了。无论如何,它也经常被使用并且可以接受。
因此,你的解决方案可能会复杂一些,但当然仍然有效。
让我再给你举个例子,获取任何容器的最高值。在这里,您将看到这样一种解决方案的灵活性。
很抱歉,但这有点先进...
#include <iostream>
#include <utility>
#include <unordered_map>
#include <queue>
#include <vector>
#include <iterator>
#include <type_traits>
#include <string>


// Helper for type trait We want to identify an iterable container ----------------------------------------------------
template <typename Container>
auto isIterableHelper(int) -> decltype (
    std::begin(std::declval<Container&>()) != std::end(std::declval<Container&>()),     // begin/end and operator !=
    ++std::declval<decltype(std::begin(std::declval<Container&>()))&>(),                // operator ++
    void(*std::begin(std::declval<Container&>())),                                      // operator*
    void(),                                                                             // Handle potential operator ,
    std::true_type{});
template <typename T>
std::false_type isIterableHelper(...);

// The type trait -----------------------------------------------------------------------------------------------------
template <typename Container>
using is_iterable = decltype(isIterableHelper<Container>(0));

// Some Alias names for later easier reading --------------------------------------------------------------------------
template <typename Container>
using ValueType = std::decay_t<decltype(*std::begin(std::declval<Container&>()))>;
template <typename Container>
using Pair = std::pair<ValueType<Container>, size_t>;
template <typename Container>
using Counter = std::unordered_map<ValueType<Container>, size_t>;
template <typename Container>
using UnderlyingContainer = std::vector<Pair<Container>>;

// Predicate Functor
template <class Container> struct LessForSecondOfPair {
    bool operator () (const Pair<Container>& p1, const Pair<Container>& p2) { return p1.second < p2.second; }
};
template <typename Container>
using MaxHeap = std::priority_queue<Pair<Container>, UnderlyingContainer<Container>, LessForSecondOfPair<Container>>;


// Function to get most frequent used number in any Container ---------------------------------------------------------
template <class Container>
auto topFrequent(const Container& data) {

    if constexpr (is_iterable<Container>::value) {

        // Count all occurences of data
        Counter<Container> counter{};
        for (const auto& d : data) counter[d]++;

        // Build a Max-Heap
        MaxHeap<Container> maxHeap(counter.begin(), counter.end());

        // Return most frequent number
        return maxHeap.top().first;
    }
    else
        return data;
}
// Test
int main() {
    std::vector testVector{ 1,2,2,3,3,3,4,4,4,4,5,5,5,5,6,6,6,6,6,7 };
    std::cout << "Most frequent is: " << topFrequent(testVector) << "\n";

    double cStyleArray[] = { 1.1, 2.2, 2.2, 3.3, 3.3, 3.3 };
    std::cout << "Most frequent is: " << topFrequent(cStyleArray) << "\n";

    std::string s{ "abbcccddddeeeeeffffffggggggg" };
    std::cout << "Most frequent is: " << topFrequent(s) << "\n";

    double value = 12.34;
    std::cout << "Most frequent is: " << topFrequent(value) << "\n";

    return 0;
}

1

按照@Manu343726的建议,使用unordered_map计数是一个好主意。然而,为了产生排序输出,需要另一步操作。

我的解决方案也是基于C++11,并使用lambda表达式。这样你既不需要定义自定义结构体,也不需要比较函数。代码几乎已经完成,我只是跳过了读取输入的部分:

#include <unordered_map>
#include <iostream>
#include <set>

int main() {
    string input = "hello world";

    unordered_map<char, unsigned int> count;
    for (char character : input)
        if (character >= 'a' && character <= 'z')
            count[character]++;

    cout << "Unsorted list:" << endl;
    for (auto const &kv : count)
        cout << kv.first << " = " << kv.second << endl;

    using myPair = pair<char, unsigned int>;
    auto comp = [](const myPair& a, const myPair& b) {
        return (a.second > b.second || a.second == b.second && a.first < b.first);
    };
    set<myPair, decltype(comp)> sorted(comp);
    for(auto const &kv : count)
        sorted.insert(kv);

    cout << "Sorted list according to frequency then alphabetically:" << endl;
    for (auto const &kv : sorted)
        cout << kv.first << " = " << kv.second << endl;

    return 0;
}

输出:

未排序列表:
r = 1
h = 1
e = 1
d = 1
o = 2
w = 1
l = 3
根据频率和字母表顺序排序的列表:
l = 3
o = 2
d = 1
e = 1
h = 1
r = 1
w = 1

注意1:不要将 unordered_map 的每个元素插入到 set 中,使用函数 std::transformstd:copy 可能更有效率,但我的代码至少较短。

注意2:与使用维护所需顺序的自定义排序 set 不同,使用一对向量并在最后一次排序可能更有效,但您的解决方案已经类似于此。 Ideone 上的代码

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