【问题标题】:Print of vowels in a string字符串中元音的打印
【发布时间】:2022-01-02 18:12:02
【问题描述】:

我试图创建一个程序,让用户输入一个文本字符串,然后创建一个子程序,该程序将打印出该字符串中的所有元音。我的程序的问题是它只能再次打印出文本。

所以我的终端看起来像:

Enter text: Feye
Feye

而不是我打算这样做:

Enter text: Feye
eye

这是我的代码,我想知道它有什么问题。为什么它不像我在 if 语句中命令它那样打印出索引?

#include <iostream>

using namespace std;

void print_vowels (string const & text)  
{
    for (int i {}; i < text.size(); ++i)
    {
        if (text.at(i) == 'a' || 'e' || 'i' || 'o' || 'u' || 'y')
        {
            cout << text.at(i);
        }
    }
}

int main()
{
    string text {};
    
    cout << "Enter text: ";
    getline(cin,text);
    
    print_vowels(text);

    return 0;
}

我也尝试过使用text[i] 而不是text.at(i),但不幸的是我得到了相同的结果。

【问题讨论】:

标签: c++


【解决方案1】:

已经给出了一些答案。反正。我想向您展示背景和一些替代解决方案。

让我们首先检查您的尝试。你在写:

if (text.at(i) == 'a' || 'e' || 'i' || 'o' || 'u' || 'y')

并希望索引中的字母 I 将与所有提到的字母进行比较。但这不会发生。为什么?

if-statement 需要一个布尔表达式。将评估此表达式。如果结果等于 true,则执行if 之后的语句或复合语句。 OK,那我们看一下必须求值的表达式:

text.at(i) == 'a' || 'e' || 'i' || 'o' || 'u' || 'y'

由操作数和运算符组成。运算符具有属性。其中之一是优先级。一些运算符的优先级高于其他运算符。这将导致“2+3*4” = 14 而不是 =10。 “*”的优先级高于“+”。因此,首先进行乘法运算,然后进行加法运算。你也可以写“2+ (3*4)”。完全一样。

下一个属性是关联性。如果优先级相等,则关联性将决定以什么顺序评估表达式。关联性是“左”或“右”。这意味着,操作数是从左到右或从右到左计算的。

在 CPP 参考资料here 中有一个非常好的描述。请阅读这个。如果我们对此进行分析,我们将看到比较运算符== 的优先级高于逻辑or。所以,我们已经可以将表达式改写为:

(text.at(i) == 'a')     || 'e' || 'i' || 'o' || 'u' || 'y'

以下逻辑or 运算符具有相同的优先级。然后,关联性将决定评估的顺序:从左到右。所以,我们现在可以将表达式重写为

((text.at(i) == 'a')   || 'e')   || 'i' || 'o' || 'u' || 'y'     // then
(((text.at(i) == 'a')   || 'e')  || 'i' )  || 'o' || 'u' || 'y'     // then
((((text.at(i) == 'a')   || 'e')  || 'i' )  || 'o' )  || 'u' || 'y'     // then finally
(((((text.at(i) == 'a')   || 'e')  || 'i' )  || 'o' )  || 'u' )  || 'y'     

好的,接下来我们需要了解字符文字,例如“a”或“e”具有值。它们通常用 ASCII 编码。然后“a”的值为 97,“e”的值为 101,依此类推。您可以查看 ASCII 表。但是如何将这些值解释为布尔值truefalse?很简单:C++ 将 0 视为 false,将所有其他值视为 true。

此外,您需要了解布尔短路评估。请阅读here

这意味着:如果结果已经明确,则不会执行进一步的操作。例如。如果( text.at(i) == 'a'),那么,如果字母真的是'a',那么结果是真的。将不再进行任何操作。简单的。如果不是,则表达式将导致:

(((((false)  || 'e')  || 'i' )  || 'o' )  || 'u' )  || 'y'     

然后,评估将是false || ‘e’。并且“e”等于 101,结果为:false||101。这总是正确的。评价结束。不会再做任何事情了。结果要么是真的,因为字母是“a”,要么是无论如何。这被称为重言式:总是正确的。因此,所有字符都被打印出来了。

而且,我们的朋友,启用所有警告或静态代码分析的编译器,会发现这个问题并通知我们。


我在另一个答案中看到了类似std::cout &lt;&lt; 97 || 101 || 105 || 111 || 117 || 121; 的示例,我的猜测是作者并不完全理解它的含义。这里插入运算符&lt;&lt; 的优先级高于逻辑或运算符||`。您也可以将其重写为:

(((((std::cout << 97)  ||  101 )  || 105 )  || 111 )   || 117 )   || 121;

如您所见。首先,我们将数字 97 写入std::cout 插入器运算符将始终返回对调用它的流的引用。所以,接下来我们有:

((((std::cout  ||  101 )  || 105 )  || 111 )   || 117 )   || 121;

现在,std::cout 用于布尔表达式。你猜怎么着? std::cout有一个布尔运算符(参见here)。如果一切正常,这将返回true。并且由于可以向std::cout 写入内容,因此将启动短路评估,并且不会执行进一步的操作。结果语句与在一行中写入“true;”相同。输出只是布尔表达式求值的副作用。但这只是一个旁注。


我们从中学到了什么。我们应该真正理解我们正在编写的表达式。而且,一个非常重要的建议:使用括号,即使是多余的。始终使用许多括号来明确您的意思。如果您使用括号,您会自己识别错误。

是不是很惊讶,要分析这么简单的一句话,需要多少解释?但现在我们了解并可以着手解决。


推荐解决方案

为了将一个字母与所有元音'a'、'e'、'I'、'o'、'u'等进行比较,我们需要将字母与所有元音'a进行比较'、'e'、'I'、'o'、'u' 等等。是的。就是这样:

if (   (text.at(i) == 'a') || (text.at(i) == 'e') || (text.at(i) == 'i') || (text.at(i) == 'o') || (text.at(i) == 'u')  ) . . .

顺便说一句,字母“y”不被视为元音。反正。你可以搜索你想要的。


此外,还有许多不同的解决方案。

有些比其他更快,有些或更直观,有些需要更少的打字工作等等。

其他答案中显示的 2 个解决方案,使用 std::setfindstd::arraystd::find 不是最快的解决方案。

std::set 的情况下,std::unordered_set 连同它的count 函数会更有效和更快,因为std::unordered_set 使用快速算法进行搜索。而std::find也可以通过简单的计算来跑赢。这个我会在最后展示。

如果您更喜欢更易读的东西(并且可能了解其他语言),您可以模拟in-like 操作,例如:

if (5, in{ 1, 2, 3, 4, 5 }) . . .

但是为此,您需要做一些非常先进的事情。我只是想在这里向你展示什么是可能的。正如我所说,有许多不同的可能解决方案。对于上述语句,我们需要为 std::initializer_list 额外定义一些包装类并重载其逗号运算符。正如我所说的,一些沉重的东西:

template<class T>
struct in {
    in(const std::initializer_list<T>& il) : ref(il) {}
    const std::initializer_list<T>& ref;
};

template<class T>
bool operator,(const T& lhs, const is_in<T>& rhs) {
    return std::find(rhs.ref.begin(), rhs.ref.end(), lhs) != rhs.ref.end();
}

暂时忘记它。


现在,最后但并非最不重要的是,alpha ASCII 字母的最快解决方案。 if-语句:

if ( 0x02208222 >> (text.at[i] & 0x1f)) & 1) . . .

会给你你需要的。我们这里只有一个快速的按位,然后是按位移位,然后是按位与。这可以被评估为超快。

但是如何破解呢?

这种技术非常古老,在古代使用,当时人们在他们的 8 位微控制器上使用汇编程序。我们依赖 ASCII 来表示字母和发明者,他们在定义代码时做出了一些明智的决定。

请看下图。 如果您查看大写字母和小写字母,您会发现只有前 5 位对于编码任何 apha 字母很重要。这很清楚,因为使用 5 位,您可以对 0..31 之间的值进行编码。而我们在西方语言中只有 26 个字母。位号 5(第六位,从 0 开始计数)决定情况。设置的位数 5 表示小写。您可以在表格中清楚地看到这一点。正因为如此,小写字母和大写字母之间的差值始终为 32。在二进制中,设置了位数为 5(第六位,计数从 0 开始)的数字。而且,如果我们想忽略这种情况,那么我们使用0001 1111b 进行位与(掩码),即0x1fhex,我们得到一个从 1 到 26 的数字等价于字母 'A' - 'Z ' 对于任何情况。那是text.at[i] &amp; 0x1f的部分。啊哈。

下一个聪明的想法是对我们想要在位位置搜索的字母进行编码。例如。我们取一个 32 位无符号整数值。在这个值中,我们根据字母的数字来设置位。例如,如果我们搜索“a”,那么我们会看到“a”有一个关联的数字 1,我们将位编号设置为 1(第 2 位,从 0 开始计数)。如果我们另外寻找一个“e”,我们在我们的值中设置位号 5(第六位,从 0 开始计数)。对于我们想要搜索的任何一个字母字符,我们在我们的 32 位 unsigned int 值中设置相应的位。

对您要搜索的所有字母进行此操作后,我们会得到结果数字 0x02208222 hex。啊哈。现在我们知道了,这是从这里来的。如果我们现在想检查某个字母,那么我们将这个数字中的位向右移动很多位置,正如其对应的字母等效所指示的那样。如果我们在可能的最低位置找到一个设置位,那么我们就有一个肯定的匹配。

复杂,但很酷。超快速地找到所有可能的字母集。

但我承认,你需要考虑一下。 . .


所以,我希望你能给你一个可以理解的解释。如果有问题,请提出。


.


基准测试


为了比较不同的解决方案,我添加了一个基准。结果是显着的。我用 10 亿个随机测试用例运行了 3 次测试。

结果排名:

1. ASCII approach       100%
2. Unordered_set        395%
3. Standard comparison  522%
4. constexpr array     1046% 
5. set                 1638%

基准的实施:

#include <array>
#include <iostream>
#include <random>
#include <algorithm>
#include <chrono>
#include <unordered_set>
#include <set>

constexpr size_t TestDataSize = 1'000'000'000u;
constexpr size_t NumberOfTestRuns = 3u;
using TestData = std::array<char, TestDataSize>;

const char SourceCharacter[]{"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"};

TestData testData;


bool isVowel_ASCII(const char c) {
    return (0x02208222 >> (c & 0x1f)) & 1;
}

static constexpr std::array<char, 12> vowels{ 'A', 'E', 'I', 'O', 'U', 'Y', 'a', 'e', 'i', 'o', 'u', 'y' };
bool isVowel_ConstexprArray(const char c) {
    return (std::end(vowels) != std::find(begin(vowels),
        end(vowels), c));
}

const std::unordered_set<char> vowel_UnorderedSet{ 'A', 'E', 'I', 'O', 'U', 'Y', 'a', 'e', 'i', 'o', 'u', 'y' };
bool isVowel_UnorderedSet(const char c) {
    return vowel_UnorderedSet.count(c);
}


const std::set<char> vowel_Set{ 'A', 'E', 'I', 'O', 'U', 'Y', 'a', 'e', 'i', 'o', 'u', 'y' };
bool isVowel_Set(const char c) {
    return (vowel_Set.find(c) != vowel_Set.end());;
}

bool isVowel_Standard(const char c) {
    return (c == 'a') || (c == 'e') || (c == 'i') || (c == 'o') || (c == 'u') || (c == 'y') ||
        (c == 'A') || (c == 'E') || (c == 'I') || (c == 'O') || (c == 'U') || (c == 'Y') ;
}

int main() {

    std::random_device rd;
    std::mt19937 generator(rd());
    std::uniform_int_distribution<> intRange(0, sizeof(SourceCharacter)-1);


    size_t counter_ASCII{};
    size_t counter_ConstexprArray{};
    size_t counter_UnorderedSet{};
    size_t counter_Set{};
    size_t counter_Standard{};

    auto start = std::chrono::system_clock::now();
    auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now() - start);


    for (size_t testRun{}; testRun < NumberOfTestRuns; ++testRun) {

        std::cout << "\n\n\nTest run " << (testRun + 1) << "\n\n";

        std::generate(testData.begin(), testData.end(), [&]() {return SourceCharacter[intRange(generator)]; });

        counter_ASCII = 0u;
        counter_ConstexprArray = 0u;
        counter_UnorderedSet = 0u;
        counter_Set = 0u;
        counter_Standard = 0;

        // --------------------------------------------------------------------------------
        start = std::chrono::system_clock::now();

        for (size_t testCase{}; testCase < TestDataSize; ++testCase) {
            if (isVowel_ASCII(testData[testCase])) ++counter_ASCII;
        }

        elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now() - start);
        std::cout << "Test 'ASCII` solution. Resulting vowel count:           " << counter_ASCII << "\t Duration: " << elapsed.count() << " ms\n";

        // --------------------------------------------------------------------------------
        start = std::chrono::system_clock::now();

        for (size_t testCase{}; testCase < TestDataSize; ++testCase) {
            if (isVowel_UnorderedSet(testData[testCase])) ++counter_UnorderedSet;
        }

        elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now() - start);
        std::cout << "Test 'unordered_set' solution. Resulting vowel count:   " << counter_UnorderedSet << "\t Duration: " << elapsed.count() << " ms\n";

        // --------------------------------------------------------------------------------
        start = std::chrono::system_clock::now();

        for (size_t testCase{}; testCase < TestDataSize; ++testCase) {
            if (isVowel_Standard(testData[testCase])) ++counter_Standard;
        }

        elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now() - start);
        std::cout << "Test 'standard' solution. Resulting vowel count:        " << counter_Standard << "\t Duration: " << elapsed.count() << " ms\n";


        // --------------------------------------------------------------------------------
        start = std::chrono::system_clock::now();

        for (size_t testCase{}; testCase < TestDataSize; ++testCase) {
            if (isVowel_ConstexprArray(testData[testCase])) ++counter_ConstexprArray;
        }

        elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now() - start);
        std::cout << "Test 'Constexpr array` solution. Resulting vowel count: " << counter_ConstexprArray << "\t Duration: " << elapsed.count() << " ms\n";


        // --------------------------------------------------------------------------------
        start = std::chrono::system_clock::now();

        for (size_t testCase{}; testCase < TestDataSize; ++testCase) {
            if (isVowel_Set(testData[testCase])) ++counter_Set;
        }

        elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now() - start);
        std::cout << "Test 'set' solution. Resulting vowel count:             " << counter_Set << "\t Duration: " << elapsed.count() << " ms\n";

    }
}

(编译时优化)

结果:

Test run 1

Test 'ASCII` solution. Resulting vowel count:           226384481        Duration: 920 ms
Test 'unordered_set' solution. Resulting vowel count:   226384481        Duration: 3630 ms
Test 'standard' solution. Resulting vowel count:        226384481        Duration: 4780 ms
Test 'Constexpr array` solution. Resulting vowel count: 226384481        Duration: 9620 ms
Test 'set' solution. Resulting vowel count:             226384481        Duration: 15060 ms



Test run 2

Test 'ASCII` solution. Resulting vowel count:           226421607        Duration: 920 ms
Test 'unordered_set' solution. Resulting vowel count:   226421607        Duration: 3630 ms
Test 'standard' solution. Resulting vowel count:        226421607        Duration: 4800 ms
Test 'Constexpr array` solution. Resulting vowel count: 226421607        Duration: 9650 ms
Test 'set' solution. Resulting vowel count:             226421607        Duration: 15060 ms



Test run 3

Test 'ASCII` solution. Resulting vowel count:           226416438        Duration: 920 ms
Test 'unordered_set' solution. Resulting vowel count:   226416438        Duration: 3630 ms
Test 'standard' solution. Resulting vowel count:        226416438        Duration: 4800 ms
Test 'Constexpr array` solution. Resulting vowel count: 226416438        Duration: 9620 ms
Test 'set' solution. Resulting vowel count:             226416438        Duration: 15070 ms

正如预期的那样。

  • ASCII 解决方案是最快的。它就是为此目的而设计的。但它仅适用于 alpha ASCII 字母,不那么兼容,不容易理解,也不那么可读。 . .
  • std::unordered_set 速度极快,易于阅读和理解,因此是惯用的正确方法
  • 比较所有元音的幼稚方法令人惊讶。永远不要低估一个优化的编译器。 . .

@OP 请使用推荐的解决方案

【讨论】:

  • ASCII 算法的一个缺点是它还会将 !, %, ), /, 5, 9 检测为元音
  • '&& ((c & 0xc0) == 0x40)' 可以添加,但会减慢速度...
【解决方案2】:

这是一个简单的解决方案:

#include <iostream>
#include <string>
#include <string_view>
#include <array>
#include <iterator>
#include <algorithm>


static constexpr std::array<char, 12> vowels { 'A', 'E', 'I', 'O', 'U', 'Y',
                                               'a', 'e', 'i', 'o', 'u', 'y' };

bool is_vowel( const char ch )
{
    return ( std::end( vowels ) != std::find( begin( vowels ),
                                              end( vowels ), ch ) );
}

bool is_consonant( const char ch )
{
    if ( ( ch >= 'A' && ch <= 'Z' ) ||
         ( ch >= 'a' && ch <= 'z' ) )
    {
        return ( std::end( vowels ) == std::find( begin( vowels ),
                                                  end( vowels ), ch ) );
    }

    return false;

}

void print_vowels( const std::string_view text )
{
    for ( const char ch : text )
    {
        if ( is_vowel( ch ) ) { std::cout << ch; }
    }
}

void print_consonants( const std::string_view text )
{
    for ( const char ch : text )
    {
        if ( is_consonant( ch ) ) { std::cout << ch; }
    }
}


int main( )
{
    std::string text { };

    std::cout << "Enter text: ";
    std::getline( std::cin, text );

    std::cout << "vowels: ";
    print_vowels( text );

    std::cout << "\nconsonants: ";
    print_consonants( text );
}

另外,请注意at 成员函数会影响性能,因为它还会在运行时检查超出范围的索引。所以在这种情况下最好使用[] 运算符或基于范围的for-loop

示例输入/输出:

Enter text: Feye
vowels: eye
consonants: F

另一个示例:

Enter text: Apple
vowels: Ae
consonants: ppl

另一个:

Enter text: Or#an.ge(-=
vowels: Oae
consonants: rng

了解std::find here

【讨论】:

  • 我也在想,如果我想创建另一个子程序来输出输入单词中的常量怎么办?我需要对字母表中的每个字母(元音除外)做同样的事情吗?没有更快的方法吗?就像遍历整个字母表,如果它不是元音,你输入一个常量。
  • 谢谢您的回复
  • @Mohamed Alkalam 我不太明白您所说的 subprogram 是什么意思。你是说函数吗?
  • @digito_evo 他只是指另一个程序来识别常量
  • 是的,一个函数。我的坏
【解决方案3】:

考虑使用基于范围的 for 循环,此处为示例:

#include <iostream>
#include <string>
#include <set>

// dont use : using namespace std; this will bite you in larger projects.

// make small functions that you can reuse/make code more readable
bool is_vowel(const char c)
{
    // create a set of vowels.
    // by making the set static it is only initialized once on the first call
    // by using a set the final code will contain less if/then checks
    static const std::set<char> vowels{ 'a','e','i','o','u', 'A','E','I','O','U' };

    // return if character is found in set
    return (vowels.find(c) != vowels.end());
    // C+20 return vowels.contains(c);
}

void print_vowels(const std::string word)
{
    std::cout << "the vowels for word " << word << " are : ";

    // this is a range based for loop
    // it will loop over all items in a collection (something with begin() and end() methods.
    // it has one big benefit, it will not go out of bounds (indices can)
    for (const char c : word)
    {
        // check with the function written earlier
        // this makes code much more readable.
        if (is_vowel(c)) std::cout << c;
    }

    std::cout << "\n";
}

int main()
{
    print_vowels("banana");
    print_vowels("himalaya");
    print_vowels("pinguin");

    return 0;
}

【讨论】:

  • 谢谢!你介意回答为什么“使用命名空间标准;”吗?不推荐?我只使用它,所以我不需要每次都输入“std::”
  • 习惯于输入命名空间learncpp.com/cpp-tutorial/… :) 这个链接也应该解释很多:stackoverflow.com/questions/1452721/…。我的建议:永远不要试图节省打字工作。键入你只做一次(甚至不是最多的编码工作),读回代码更频繁(由你或其他需要维护代码的人)
  • @PepijnKramer 您可以考虑更好地使用std::unordered_setcount 函数。恕我直言,这将是惯用的正确方法。您也可以查看我的答案以获得更快的解决方案。但是,与往常一样,有许多不同的可能解决方案,您的解决方案很好。速度不会有明显差异。 . .
  • @ArminMontigny 我通常首先考虑可读性(伴随着多年的代码维护),然后担心速度优化。当然我会考虑所选数据结构的性能。这次我只是没有考虑 unordered_set ,所以感谢提醒:)
  • @PepijnKramer。一些额外的信息。我在底部添加了一个基准。非常有趣的是,天真的方法有多快。我真的很惊讶。 . .
【解决方案4】:

如果您运行以下行:

cout << 'a' || 'e' || 'i' || 'o' || 'u' || 'y' ;

你会得到 'a' 的输出。 上一行基本上是这样执行的:

cout << 97 || 101 || 105 || 111 || 117 || 121 ; // this gives 97 

您的代码执行这样的原因是因为每个字符都有一个相应的 ASCII 值,并且通过这一行,您只是尝试对一堆整数或数值(ASCII值)在引擎盖下。所以你不能指望这条线像它应该的那样工作。相反,您必须单独检查以下条件:

if (text[i] == 'a' || text[i] == 'e' || text[i] == 'i' || text[i] == 'o' || text[i] == 'u' || text[i] == 'y') 
{ 
cout << text[i]; 
}

如上所述,您可以通过以下方式检查某个字符的 ASCII 值:

char c = 'a';
cout << "ASCII Value of " << c << " is " << int(c);

该程序基本上比较整数(ASCII 值),并据此判断它是否是元音。只要我们在两个或多个整数之间进行 OR 运算,我们总是会得到 true 值,因为这些整数本质上是非零值。在两个或多个非零值之间执行 OR 运算将始终返回 true

我建议使用[] 运算符以获得更好的性能,因此请使用text[i] 而不是text.at(i)

【讨论】:

  • 我也在想,如果我想创建另一个子程序来输出输入单词中的常量怎么办?我需要对字母表中的每个字母(元音除外)做同样的事情吗?没有更快的方法吗?就像遍历整个字母表,如果它不是元音,你输入一个常量。
  • 当然,聪明的方法是选择循环,否则需要2077才能完成编写程序
  • @MohamedAlkalam:你可以写一个函数bool isVowel(char c),然后写bool isConsonant(char c) { return std::isalpha(c) &amp;&amp; ! isVowel(c);
  • 只是猜测,可能是您没有完全了解您的陈述cout &lt;&lt; 97 || 101 || 105 || 111 || 117 || 121 ;的所有含义,我在回答中对此进行了深入分析。不过你的回答也绝对正确,非常好!
猜你喜欢
  • 2017-01-18
  • 2019-12-09
  • 2014-06-17
  • 1970-01-01
  • 2020-02-13
  • 1970-01-01
  • 1970-01-01
  • 2013-11-27
  • 1970-01-01
相关资源
最近更新 更多