在 C++ 中的(奇怪的)字符串中查找单词

Finding words in a (weird) string in C++

这个程序在技术上有什么错误?预期结果为 6,因为这是字符串中存在的单词总数。

#include <iostream>
using namespace std; 

int main()
{
    string str = "  Let's   count     the      number     of    words  ";
    int word = 0;
    for (int i = 0; str[i] != '[=10=]';)
    {
        if ((str[i] == 32 && str[i + 1] == 32) || (str[i] == 32 && str[i - 1] == 32))
        {
            ++i;
        }
        else if ((str[i] == 32 && str[i - 1] != 32) || (str[i] == 32 && str[i + 1] != 32))
        {
            word++;
        }
        ++i;
    }
    cout << "No. of words: " << word << endl;
    return 0;
}

我的错误结果:

No. of words: 0

此外,如果我尝试将字符串中的空格甚至字符串本身更改为一组全新的间隔单词,请说:

string str = "   Hello world   ";
string str = "Hello    world! How   are you?   ";

我仍然得到不正确的结果,但与 0 不同。我是 C++ 编程的新手,这些奇怪的行为让我做噩梦。这很常见吗?我可以做些什么来纠正这个问题?

如果您能按照我编写的方式突出显示或更正我的程序,那么这对我理解错误会很有帮助,而且会很快,而不必在此时了解一些新命令。因为,正如我所说,我是 C/C++ 的初学者。

感谢您的宝贵时间!

I'm new to C++ programming and these kinds of strange behaviors are giving me nightmares. Is this common?

是的,这很常见。您编写了一大堆堆积如山的逻辑,但您没有工具来了解它的行为方式。

What I can do to get this corrected?

您可以从两个方向进行此操作:

  1. 对此进行调试以提高您对其运作方式的理解:

    • 预先确定你希望它为一些输入做什么,在每一行
    • 在调试器中单步执行它以查看它的实际作用
    • 想想为什么它没有达到您的预期

    有时候问题是你的代码没有正确实现你的算法,有时候算法本身就坏了,通常两者兼而有之。通过这两个工作会给你一些洞察力。

  2. 首先编写更容易理解的代码(等价地,编写易于推理的算法)。

    这取决于您对某些事情是否易于推理有一些直觉,这是您从迭代步骤 1 中发展而来的。

... instead of having to know some new commands at this point.

好吧,无论如何你都需要学习使用调试器,所以现在是开始的最佳时机。

我们当然可以改进现有代码,尽管我更愿意修复逻辑。一般来说,我鼓励您将现有的 if 条件抽象为小函数,但问题是它们目前似乎没有任何意义。

那么,我们如何定义一个词呢?

您的代码表明它至少有一个非 space 字符在 之后 space。 (顺便说一句,绝对比 32 更喜欢 ' ',而且 std::isspace 比任何一个都好。)

但是您的代码的隐含定义是有问题的,因为:

  • 每个长于一个字符的单词都有第一个字符和最后一个字符,您将计算每个字符
  • 你不能检查第一个字符前面是否有任何东西,而不会越界
  • 最后一个字符后跟空终止符,但你不认为它是白色的space

让我们选择一个不同的定义,它不需要阅读 str[i-1],也不需要你当前代码出错的棘手遍历。

我声称一个词是非白色space字符的连续子串,并且词被白色space字符的连续子串分隔。因此,我们可以编写伪代码来处理这些术语,而不是查看每对连续字符:

    for (current = str.begin(); current != str.end(); ) {
        // skip any leading whitespace
        current = find_next_non_whitespace(str, current);
        if (current != str.end()) {
            // we found a word
            ++words;
            current = find_next_whitespace(str, current);
        }
    }
            

注意。当我谈到将您的代码抽象成小函数时,我的意思是像 find_next_non_whitespace 这样的东西 - 它们应该易于实现,易于测试,并且有一个能告诉您一些事情的名称。

当我说你现有的条件似乎没有意义时,是因为更换

if ((str[i] == 32 && str[i + 1] == 32) || (str[i] == 32 && str[i - 1] == 32))

与,比如说,

if (two_consecutive_spaces(str, i))

提示的问题多于回答的问题。为什么有两个连续的 space 的特殊情况?它与只有一个 space 不同吗?如果我们有两个单词,它们之间只有一个 space,实际会发生什么?为什么在这种情况下我们前进了两个字符,但在单词分支上只前进了一个?

代码无法轻易映射回可解释的逻辑这一事实是一个不好的迹象——即使它有效(我们知道它无效),我们对它的理解还不够深入,无法改变,扩展或重构它。

我想你有一些方法可以做到。看看这段代码。与你的非常相似:

string s = "  Let's   count     the      number     of    words  ";

int word = 0;

for (auto i = 0; s[i] != '[=10=]'; i++) {
    if (i == 0) {
        if (s[i] != ' ') {
            ++word;
        }
        continue;
    }

    if (s[i - 1] == ' ' && s[i] != ' ') {
        ++word;
    }
}

cout << "No of Words: " << word << endl;

这个想法是逐个字符地遍历字符串读取。所以我们做一些逻辑:

  • 如果我们在第一个字符串字符中并且它等于' ',则转到下一个循环迭代
  • 如果我们在第一个字符串字符中并且它不同于' ',意味着我们正在开始一个单词,所以计算它并跳转到下一个循环迭代。
  • 如果我们到达第二个if,意味着我们不在第一个位置,所以尝试访问i - 1应该是有效的。然后我们只检查前一个字符是否为空白 space 而当前字符不是。这意味着我们正在开始一个新词。所以计算它并跳转到下一个循环迭代。

另一种更简单的方法是使用字符串流:

string s = "  Let's   count     the      number     of    words  ";
stringstream ss(s);
string sub;
int word = 0;
while (ss >> sub) {
    ++word;
}
cout << "No of Words: " << word << endl;

通过这种方式,您基本上是从字符串中逐字提取。