c语言 - 如何计算字符串中出现 n 长度的单词的频率



我这里有这段代码,可以正确格式化硬编码的句子,并找到某个字母在该字符串中出现的频率:

#include <stdio.h>
#include <string.h>
int main() {
char words[1000][100];
int x = 0, y;
char myString[10000] = "The quick Brown ? Fox ? jumps over the Lazy Dog and the !##! LAZY DOG is still sleeping";
printf("Original Text:n");
printf("%sn", myString);

// Function for uppercase letters to become lowercase and to remove special characters
for (x = 0; x <= strlen(myString); ++x) {
if (myString[x] >= 65 && myString[x] <= 90)
myString[x] = myString[x] + 32;
}
for (x = 0; myString[x] != ''; ++x) {
while (!(myString[x] >= 'a' && myString[x] <= 'z') &&
!(myString[x] >= 'A' && myString[x] <= 'Z') &&
!(myString[x] >= '0' && myString[x] <= '9') &&
!(myString[x] == '') && !(myString[x] == ' ')) {
for (y = x; myString[y] != ''; ++y) {
myString[y] = myString[y + 1];
}
myString[y] = '';
}
}

printf("nModified Text: n%sn", myString);
// Part A
int counts[26] = { 0 };
int k;
size_t myString_length = strlen(myString);
for (k = 0; k < myString_length; k++) {
char c = myString[k];
if (!isalpha(c))
continue;
counts[(int)(c - 'a')]++;
}

printf("nLettertCountn------  -----n");

for (k = 0; k < 26; ++k) {
printf("%ct%dn", k + 'a', counts[k]);
}
// Part B
int i = 0, count = 0, occurrences[10000] = { 0 };

while (myString[i] != '') {
char wordArray[100];
int j = 0;

while (myString[i] != ' ' && myString[i] != '') {
wordArray[j++] = myString[i++];
}

if (wordArray[j - 1] == ',' || wordArray[j - 1] == '.') {
wordArray[j - 1] = '';
}
wordArray[j] = '';
int status = -1;

for (j = 0; j < count; ++j) {
if (strcmp(words[j], wordArray) == 0) {
status = j;
break;
}
}

if (status != -1) {
occurrences[status] += 1;
} else {
occurrences[count] += 1;
strcpy(words[count++], wordArray);
}
++i;
}

printf("nWord LengthtOccurrencesn-----------     -----------n");

for (i = 0; i < count; ++i) {
// print each word and its occurrences
printf("%stt%dn", words[i], occurrences[i]);
}
}

B部分是我遇到问题的地方,我希望代码能够告诉我出现特定长度的单词,例如此实例:

Word length Occurrences
1           0
2           1

在这里,没有一个单词有一个字符的实例,但有一个单词有两个字符的实例。但是,我的代码输出的是给定特定单词的次数,而不是我上面想要的,如下所示:

Word Length     Occurrences
-----------     -----------
the             3
quick           1
brown           1
3
fox             1
jumps           1
over            1
lazy            2
dog             2
and             1
is              1
still           1
sleeping                1

我将如何更改它,以便它仅显示字长和频率的输出?

以下是有关代码的一些备注:

  • 第一个循环重新计算每次迭代的字符串长度:for (x = 0; x <= strlen(myString); ++x)。由于您在循环中修改字符串,因此编译器很难确定字符串长度不会更改,因此经典优化可能不起作用。 使用与下一个循环相同的测试:

    for (x = 0; myString[x] != ''; ++x)
    
  • 大写的测试不是很好读,因为您对字母AZ的 ASCII 值进行硬编码,您应该编写:

    if (myString[x] >= 'A' && myString[x] <= 'Z')
    myString[x] += 'a' - 'A';
    

    或使用<ctype.h>中的宏:

    unsigned char c = myString[x];
    if (isupper(c))
    myString[x] = tolower(c);
    

    或同等且可能更有效:

    myString[x] = tolower((unsigned char)myString[x]);
    
  • 在第二个循环中,删除既不是字母、数字也不是空格的字符。 你有一个冗余的嵌套while循环和第三个嵌套循环,用于移动每个删除的字节的数组的其余部分:这种方法具有三次时间复杂度,O(N3),效率非常低。您应该改用线性时间操作的双指方法:

    for (x = y = 0; myString[x] != ''; ++x) {
    unsigned char c = myString[x];
    if (!isalnum(c) && c != ' ') {
    myString[y++] = c;
    }
    }
    myString[y] = '';
    
  • 请注意,此循环会删除所有标点符号,而不是将其替换为空格:这可能会将单词粘合在一起,例如"a fine,good man"->"a finegood man"

  • 在第三个循环中,您使用charc作为isalpha(c)的参数。应包含使用此头文件中声明的任何函数的<ctype.h><ctype.h>中的函数和宏仅针对unsigned char类型的所有值和特殊负值EOF定义。如果类型char在您的平台上签名,则如果字符串具有负字符,isalpha(c)将具有未定义的行为。 在您的特定情况下,您过滤了不是 ASCII 字母、数字或空格的字符,因此这应该不是问题,但始终使用unsigned charfor character 参数来isalpha()和等效函数是一个好习惯。

  • 另请注意,此计数阶段可以合并到前面的循环中。

  • 要计算单词的出现次数,数组occurrences应具有与words数组相同的元素数 1000。您不检查边界,因此如果有超过 1000 个不同的单词和/或这些单词中的任何一个包含 100 个字符或更多,则具有未定义的行为。

  • 在下一个循环中,从字符串中提取单词,在嵌套循环主体内递增i。您还会在外部循环的末尾递增i,从而跳过最后一个 null 终止符。测试while (myString[i] != '')将测试超出字符串末尾的字节,这是不正确的,并且可能是未定义的行为。

  • 为避免在此循环中计算空单词,如果不在字符串末尾,则应在复制单词之前跳过空格序列。

  • 根据问题,计算单个单词不是B部分应该做的事情,而是应该计算单词长度的频率。您可以在第一个循环中执行此操作,方法是跟踪当前单词的长度,并在找到分隔符时增加单词长度频率数组。

  • 请注意,不需要修改字符串来计算字母频率或单词长度出现次数。

  • 建议为每个任务编写一个单独的函数。

这是一个修改版本:

#include <ctype.h>
#include <stdio.h>
#define MAX_LENGTH 100
// Function to lowercase letters and remove special characters
void clean_string(char *str) {
int x, y;
printf("Original Text:n");
printf("%sn", str);
for (x = y = 0; str[x] != ''; x++) {
unsigned char c = str[x];
c = tolower(c);
if (isalnum(c) || c == ' ') {
str[y++] = c;
}
}
str[y] = '';
printf("nModified Text:n%sn", str);
}
// Part A: count letter frequencies
void count_letters(const char *str) {
int letter_count['z' - 'a' + 1] = { 0 };
for (int i = 0; str[i] != ''; i++) {
unsigned char c = str[i];
if (c >= 'a' && c <= 'z') {
letter_count[c - 'a'] += 1;
} else
if (c >= 'A' && c <= 'Z') {
letter_count[c - 'A'] += 1;
}
}
printf("nLettertCount"
"n------t-----n");
for (int c = 'a'; c <= 'z'; c++) {
printf("%ct%dn", c, letter_count[c - 'a']);
}
}
// Part B: count word lengths frequencies
void count_word_lengths(const char *str) {
int length_count[MAX_LENGTH + 1] = { 0 };
for (int i = 0, len = -1;; i++) {
unsigned char c = str[i];
// counting words as sequences of letters or digits
if (isalnum(c)) {
len++;
} else {
if (len >= 0 && len <= MAX_LENGTH) {
length_count[len] += 1;
len = -1;
}
}
if (c == '')
break;
}
printf("nWord LengthtOccurrences"
"n-----------t-----------n");
for (int len = 0; len <= MAX_LENGTH; len++) {
if (length_count[len]) {
printf("%-11dt%dn", len, length_count[len]);
}
}
}
int main() {
char myString[] = "The quick Brown ? Fox ? jumps over the Lazy Dog and the !##! LAZY DOG is still sleeping";
// Uncomment if modifying the string is required
//clean_string(myString);
count_letters(myString);
count_word_lengths(myString);
return 0;
}

输出:

Letter  Count
------  -----
a       3
b       1
c       1
d       3
e       6
f       1
g       3
h       3
i       4
j       1
k       1
l       5
m       1
n       3
o       5
p       2
q       1
r       2
s       4
t       4
u       2
v       1
w       1
x       1
y       2
z       2
Word Length     Occurrences
-----------     -----------
1               1
2               7
3               3
4               4
7               1

使用strtok_r()并简化计数.
它是同级strtok()不是线程安全的。在为什么 strtok() 被认为是不安全的?

此外,strtok_r()通过在字符串内插入字符来切碎输入字符串。如果要保留原始字符串的副本,则必须制作原始字符串的副本并将其传递给strtok_r()

还有一个问题。strtok_r()还不是C标准的一部分,但POSIX-2008列出了它。GNUglibc实现它,但是要访问这个函数,我们需要在源文件包含任何内容之前#define _POSIX_C_SOURCE

还有strdup()strndup()复制输入字符串,它们为您分配内存。使用完后,您必须释放该字符串内存。strndup()是在POSIX-2008中添加的,因此我们在源代码中声明200809L使用它。

使用新标准编写新代码总是更好的。 建议至少POSIX 200809LC standard 2011

#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#define MAX_STR_LEN     1024
#define MAX_WORD_LEN    128
#define WORD_DELIMS     " nt"
int is_word (const char* str, const size_t slen) {
int word = 0;
for (size_t ci = 0; ci < slen;)
if (isalnum (str[ci++])) {
word = 1;
break;
}
return word;
}
void get_word_stat (const char* str, int word_stat[]) {
char *copy = strndup (str, MAX_STR_LEN); // limiting copy
if (!copy) { // copying failed
printf ("Error duplicating input stringn");
exit (1);
}
for (char *token, *rmdStr = copy; (token = strtok_r (NULL, WORD_DELIMS, &rmdStr)); /* empty */) {
size_t token_len = strlen (token);
if (token_len > (MAX_WORD_LEN - 1)) {
printf ("Error: Increase MAX_WORD_LEN(%d) to handle words of length %lun", MAX_WORD_LEN, token_len);
exit (2);
}
if (is_word (token, token_len))
++word_stat[token_len];
else
printf ("[%s] not a wordn", token);
}
free (copy);
}
int main () {
char str [MAX_STR_LEN] = "The quick Brown ? Fox ? jumps over the Lazy Dog and the !##! LAZY DOG is still sleeping";
printf ("Original Text: [%s]n", str);
int word_stat[MAX_WORD_LEN] = {0};
get_word_stat (str, word_stat);
printf ("nWordLength   Occurrencesn");
for (int si = 1; si < MAX_WORD_LEN; ++si) {
if (word_stat[si])
printf ("%dtt%dn", si, word_stat[si]);
}
return 0;
}

每当您对某事发生的频率感兴趣时,您都希望使用频率数组,其中包含处理整个可能发生范围所需的元素数量。您希望跟踪字长的频率,因此您需要一个大小调整为跟踪最长字的数组。(非医学未删节词典中的最长单词为 29 个字符,最长医学单词为 45 个字符)

所以这里一个包含 29 个元素的简单整数数组就可以了(除非你想考虑医学词,否则使用 45)。如果您想考虑无意义的单词,请适当调整大小,例如"Supercalifragilisticexpialidocious",34个字符。根据合理预期的最大出现次数选择类型。使用将出现次数限制为INT_MAX的签名int(2147483647)。使用unsigned将使限制加倍,或者使用uint64_t获得完整的 64 位范围。

工作原理

你如何使用一个简单的数组来跟踪字长的出现?很简单,声明一个足够大小的数组并将所有元素初始化为零。现在你要做的就是阅读一个单词,使用,例如size_t len = strlen(word);获取长度,然后递增yourarray[len] += 1;

假设单词有 10 个字符,您将在yourarray[10]中添加一个。因此,数组索引对应于字长。当您获取所有单词的长度并递增相应的数组索引时,要获得结果,您只需遍历数组并在索引(单词长度)处输出值(出现次数)。如果您有两个单词,每个单词包含 10 个字符,则yourarray[10]将包含 2(对于对应于不同单词长度字符数的每个其他索引,依此类推)。

选择如何分隔单词时的注意事项

选择将空格分隔的单词字符串拆分为单个单词的方法时,您需要知道原始字符串是否可变。例如,如果您选择用strtok()分隔单词,它将修改原始字符串。在您的情况下,由于您的单词存储在数组或char中,这很好,但是如果您有这样的字符串文字怎么办:

char *mystring =  "The quick Brown ? Fox ? jumps over the Lazy Dog ";

在这种情况下,当strtok()尝试修改只读内存保存mystring的区域时,将mystring传递给strtok()SEGFAULT(忽略Microsoft对字符串文本的非标准处理)

当然,您可以创建mystring的副本并将字符串文本放在可变内存中,然后在副本上调用strtok()。或者,可以使用不修改mystring的方法(例如使用sscanf()和偏移量来分析单词,或者使用交替调用strcspn()strspn()来查找和跳过空格,或者只是使用开始结束指针来处理字符串括号单词并在指针之间复制字符。完全取决于你。

例如,使用带有偏移量的sscanf()来处理字符串,使用每次读取期间消耗的字符数从开头更新偏移量:

char *mystring =  "The quick Brown ? Fox ? jumps over the Lazy Dog "
"and the !##! LAZY DOG is still sleeping",
*p = mystring,         /* pointer to mystring to parse */
buf[MAXLEN] = "";      /* temporary buffer to hold each word */
int nchar = 0,              /* characters consumed by sscanf */
offset = 0;             /* offset from beginning of mystring */

/* loop over each word in mystring using sscanf and offset */
while (sscanf (p + offset, "%s%n", buf, &nchar) == 1) {
size_t len = strlen (buf);    /* length of word */

offset += nchar;              /* update offset with nchar */

/* do other stuff here */
}

测试单词是否为阿尔法数字

您可以循环遍历每个字符,从每个字符的ctype.h调用isalnum()宏。或者,您可以让strspn()为您完成此操作,给定单词可以包含的字符列表。例如,仅对于数字和字母字符,您可以使用一个简单的常量,然后在循环中调用strspn()来确定该单词是否仅由您将在单词中接受的字符组成,例如

#define ACCEPT "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
...
/* use strspn to test that word is valid (alphanum) or get next word */
if (strspn (buf, ACCEPT) != len) {
fprintf (stderr, "  error: rejecting "%s"n", buf); /* optional */
continue;
}
...

这两种方式都不比另一种更正确,这实际上是一个方便性和可读性的问题。使用库提供的函数还可以提供一点信心,即它的编写方式将允许编译器完全优化编译的代码。

一个简短的例子

将上述想法放在一个简短的示例中,该示例将使用sscanf()解析mystring中的单词,然后使用简单的整数数组跟踪所有 alphanum 单词(最多 31 个字符,并输出任何被拒绝的单词)的出现,以保持长度的频率,您可以做到:

#include <stdio.h>
#include <string.h>
#define MAXLEN      32    /* if you need a constant, #define one (or more) */
#define ACCEPT "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
int main (void) {

char *mystring =  "The quick Brown ? Fox ? jumps over the Lazy Dog "
"and the !##! LAZY DOG is still sleeping",
*p = mystring,         /* pointer to mystring to parse */
buf[MAXLEN] = "";      /* temporary buffer to hold each word */
int nchar = 0,              /* characters consumed by sscanf */
offset = 0,             /* offset from beginning of mystring */
lenfreq[MAXLEN] = {0};  /* frequency array for word length */

/* loop over each word in mystring using sscanf and offset */
while (sscanf (p + offset, "%s%n", buf, &nchar) == 1) {
size_t len = strlen (buf);    /* length of word */

offset += nchar;              /* update offset with nchar */

/* use strspn to test that word is valid (alphanum) or get next word */
if (strspn (buf, ACCEPT) != len) {
fprintf (stderr, "  error: rejecting "%s"n", buf); /* optional */
continue;
}

lenfreq[len] += 1;      /* update frequency array of lengths */
}

/* output original string */
printf ("nOriginal Text:nn%snn", mystring);

/* output length frequency array */
puts ("word length     Occurrencesn"
"-----------     -----------");
for (size_t i = 0; i < MAXLEN; i++) {
if (lenfreq[i])
printf ("%2zu%14s%dn", i, " ", lenfreq[i]);
}
}

示例使用/输出

编译和运行该程序将产生:

$ ./bin/wordlen-freq
error: rejecting "?"
error: rejecting "?"
error: rejecting "!##!"
Original Text:
The quick Brown ? Fox ? jumps over the Lazy Dog and the !##! LAZY DOG is still sleeping
word length     Occurrences
-----------     -----------
2              1
3              7
4              3
5              4
8              1

(注意:即使没有出现,您也可以通过删除打印条件if (lenfreq[i])输出从031的所有长度 - 由您决定)

仔细看看,如果你有问题,请告诉我。

最新更新