【数据结构】英文单词词频统计与检索系统

目录

一、题目描述

输入格式

输出格式

输入样例1

输出样例1

输入样例2

输出样例2

提示

[1. 语言与库限制](#1. 语言与库限制)

[2. 各策略ASL计算方法](#2. 各策略ASL计算方法)

通用计算流程(三种策略统一)

[3. 比较次数统计口径](#3. 比较次数统计口径)

[4. Normalize函数](#4. Normalize函数)

[5. Time33哈希函数](#5. Time33哈希函数)

[6. 关键注意事项](#6. 关键注意事项)

[7. 常见问题](#7. 常见问题)

二、英文单词词频统计与检索系统的C++代码完整实现

三、C++代码详细解释

(一)全局常量定义(满足题目强制要求)

(二)核心工具函数

[1. 单词标准化函数 Normalize(题目强制要求)](#1. 单词标准化函数 Normalize(题目强制要求))

[2. Time33 哈希函数 myHash(题目强制要求,重命名避免关键字冲突)](#2. Time33 哈希函数 myHash(题目强制要求,重命名避免关键字冲突))

(三)数据结构定义

[1. 线性表节点(策略 1/2 专用)](#1. 线性表节点(策略 1/2 专用))

[2. 哈希表节点(策略 3 专用,链地址法解决冲突)](#2. 哈希表节点(策略 3 专用,链地址法解决冲突))

[3. 哈希表结构体(策略 3 专用)](#3. 哈希表结构体(策略 3 专用))

[(四)查找 / 排序算法(策略 1/2 核心)](#(四)查找 / 排序算法(策略 1/2 核心))

[1. 策略 1:顺序查找 linearSearch](#1. 策略 1:顺序查找 linearSearch)

[2. 策略 2:快速排序(手写,按字典序)](#2. 策略 2:快速排序(手写,按字典序))

[(1)交换节点函数 swapLinearNode](#(1)交换节点函数 swapLinearNode)

[(2)快排分区函数 partition](#(2)快排分区函数 partition)

[(3)快排主函数 quickSort](#(3)快排主函数 quickSort)

[3. 策略 2:二分查找 binarySearch](#3. 策略 2:二分查找 binarySearch)

(五)主函数(核心执行流程)

[1. 初始化阶段](#1. 初始化阶段)

[2. 读取并处理输入文本](#2. 读取并处理输入文本)

[3. 计算 Unique Words 和 ASL(平均查找长度)](#3. 计算 Unique Words 和 ASL(平均查找长度))

[4. 输出结果 + 内存释放](#4. 输出结果 + 内存释放)

(六)关键细节与注意事项

(七)核心知识点总结

四、总结


一、题目描述

  1. 本题要求实现一个英文单词词频统计与检索系统,使用三种不同的数据结构策略存储和检索单词,并计算每种策略的平均查找长度(ASL),以对比它们的查找效率。

本文的C++完整代码、英文文本和测试数据的下载地址:英文单词词频统计与检索系统资源-CSDN下载

你需要根据输入的策略编号,使用对应的数据结构完成词频统计:

  • 策略1:线性表 + 顺序查找
  • 策略2:线性表 + 快速排序 + 二分查找
  • 策略3:哈希表(链地址法,链表法解决冲突)

输入格式

第一行 :一个整数 S(1 ≤ S ≤ 3),表示使用的策略编号。

第二行起 :英文文本内容,读取直到遇到字符 #(遇到 # 立刻结束,# 不参与分词/统计,# 之后内容忽略)。

分词与规范化规则

  • 定义:由连续的英文字母(A-Z, a-z)构成的序列视为一个单词。
  • 分隔符:任何非英文字母字符(空格、标点、数字、换行符等)均视为分隔符。
  • 结束标记 :字符 # 仅用于结束输入;读取过程中遇到 # 立刻停止读取。# 不作为分隔符参与分词,可以单独占一行表示输入结束。

输出格式

程序输出三行信息:

bash 复制代码
Total Words: X
Unique Words: Y
ASL: Z.ZZ
  • Total Words:文本中所有单词的总数(含重复)
  • Unique Words:去重后的单词总数
  • ASL:平均查找长度(保留2位小数)

ASL(Average Search Length,平均查找长度)

定义:在数据存储结构构建完成后,查找其中任意一个不同单词所需的平均比较次数。

计算公式

其中:

  • N = 不同词数(Unique Words)
  • = 查找第 i 个不同单词所需的比较次数

重要说明

  1. ⚠️ ASL统计的是查找成功时的比较次数(假设要查的单词一定存在)
  2. ⚠️ 计算时不考虑词频权重,即每个不同单词被查找的概率相等(等概率假设)
  3. ⚠️ 插入/构建过程中的比较次数不计入ASL,ASL只反映"数据结构建好后的查找性能"

输入样例1

bash 复制代码
3
The quick brown fox jumps over the lazy dog.
Quick brown foxes jump over lazy dogs!
The fox and the dog are friends.
#

输出样例1

bash 复制代码
Total Words: 23
Unique Words: 14
ASL: 1.00

输入样例2

bash 复制代码
2
Software engineering requires careful testing.
Testing software ensures quality software.
Quality engineering practices matter greatly.
Data-driven decisions improve software quality.
#

输出样例2

bash 复制代码
Total Words: 21
Unique Words: 14
ASL: 3.21

提示

1. 语言与库限制

  • 语言:C++
  • 允许std::string(用于读取输入与 normalize),C/C++基础库(<iostream> <string> <cstring> <cctype> 等)
  • 禁止 :STL容器(vector map set unordered_map 等)和库排序函数(必须手写数据结构和排序算法)

2. 各策略ASL计算方法

通用计算流程(三种策略统一)

步骤1:构建阶段

  • 读取文本,根据策略构建相应的数据结构
  • 统计总词数(Total Words)和不同词数(Unique Words)
  • ⚠️ 此阶段的比较次数不计入ASL

步骤2:ASL统计阶段

  • 遍历数据结构中的每个不同单词
  • 对每个不同单词,重新模拟一次查找过程
  • 记录每次查找的比较次数
  • 计算

3. 比较次数统计口径

策略 比较次数定义 计算方式
策略1 查找第i个词需要i次比较 对每个词执行顺序查找累加
策略2 每访问mid并比较记1次 对每个词执行二分查找累加
策略3 从链表头到命中节点的节点数 遍历哈希表,累加节点位置

⚠️ 统一原则:所有策略都基于"等概率查找假设",即每个不同单词被查询的概率相同。

4. Normalize函数

cpp 复制代码
// 单词规范化函数(使用此标准实现)
// 功能:从token的pos位置开始提取下一个单词(仅字母,转小写)
// 参数:token-输入串,pos-当前位置(会被更新),word-输出单词
// 返回:1=成功提取单词,0=token处理完毕
int Normalize(const char *token, int &pos, char *word) {
 // 跳过非字母字符
while (token[pos] && !isalpha(token[pos]))
 pos++;

if (!token[pos])
return 0; // token结束

 // 提取连续字母并转小写
int len = 0;
while (token[pos] && isalpha(token[pos])) {
 word[len++] = tolower(token[pos++]);
 }
 word[len] = '\0';
return 1;
}

5. Time33哈希函数

cpp 复制代码
unsigned int hash(const char *str) {
unsigned int h = 0;
while (*str) {
 h = (h << 5) + h + *str;  // h = h*33 + c
 str++;
 }
return h % HASH_SIZE;  // HASH_SIZE必须为1009
}

链表节点定义参考

cpp 复制代码
struct HashNode {
char word[50];      // 单词
int count;          // 词频
 HashNode* next;     // 指向下一个节点(链表法)
};

struct HashTable {
 HashNode* buckets[1009];  // 哈希表大小固定为1009
int unique_count;          // 不同词数
};

6. 关键注意事项

  • 策略2排序:必须按照单词的字典序(Lexicographical Order)排序
  • 规则:从第一个字符开始逐位比较 ASCII 码。
  • 如果首字符相同,则比较第二个字符,依此类推。
  • 示例:apple < applye < y),cat < catch(短串排前面)。
  • 必须严格满足此顺序,否则二分查找结果不正确。
  • 策略3哈希表:数组大小 M 必须为 1009
  • 策略3链地址法:使用链表解决哈希冲突,链表节点需要动态分配(使用 new),且必须使用头插法
  • 所有策略基于等概率查找假设计算ASL
  • ASL必须保留2位小数
  • normalize后为空的token应跳过,不计入总词数

7. 常见问题

Q1: 为什么建议使用提供的Normalize函数?

A: 为保证评测一致性。如果normalize实现略有差异(如空串处理方式不同),会导致词频统计结果不同,影响PTA评测准确性。

Q2: 我可以自己实现normalize吗?

A: 可以,但必须完全符合上述规范。建议直接使用提供的标准实现,避免因实现差异导致测试失败。

Q3: Normalize后变成空串怎么办?

A: 提供的Normalize函数会自动跳过非字母字符。如果token完全由非字母字符组成(如"123"),Normalize会返回0表示处理完毕,此时不应计入总词数。

Q4: 测试样例中的分词结果是什么?

A: 示例:

  • "Test,World!""test""world" (两个单词)
  • "Data-Structure""data""structure" (两个单词)
  • "User's""user""s" (两个单词)
  • "123" → 空串(跳过,不计数)

Q5: 策略2必须用快速排序吗?可以用其他排序算法吗?

A: 可以使用其他O(N log N)的排序算法(如归并排序、堆排序),但禁止使用:

  • 库函数(std::sort等)
  • O(N²)的排序(冒泡、选择、插入排序在大数据量下会比较慢)
    快速排序是最常用的选择,建议掌握。

Q6: 如何验证我的ASL计算是否正确?

A: 三步验证法:

  1. 小规模手工验证:用3个单词测试,手工计算ASL对比
  2. 理论公式验证
  • 策略1:ASL应该精确等于(N+1)/2
  • 策略2:ASL应该接近log₂(N+1)-1(误差<15%)
  • 策略3:ASL应该接近1+α/2(α=N/1009,误差<25%)

二、英文单词词频统计与检索系统的C++代码完整实现

cpp 复制代码
#include <iostream>
#include <cstring>
#include <cctype>
#include <string>
using namespace std;

// 全局常量定义(严格遵循题目要求)
const int MAX_LINEAR_SIZE = 10000;  // 线性表最大容量(足够处理常规输入)
const int HASH_SIZE = 1009;         // 哈希表大小(题目强制要求1009)
const int MAX_WORD_LEN = 50;        // 单词最大长度

// ===================== 题目要求的标准化函数 =====================
int Normalize(const char *token, int &pos, char *word) {
    // 跳过所有非字母字符
    while (token[pos] && !isalpha(token[pos]))
        pos++;

    if (!token[pos])  // 字符串处理完毕
        return 0;

    // 提取连续字母并转换为小写
    int len = 0;
    while (token[pos] && isalpha(token[pos])) {
        word[len++] = tolower(token[pos++]);
    }
    word[len] = '\0';  // 字符串结束符
    return 1;
}

// ===================== 题目要求的Time33哈希函数(修改1:重命名为myHash) =====================
unsigned int myHash(const char *str) {  // 原函数名hash → myHash
    unsigned int h = 0;
    while (*str) {
        h = (h << 5) + h + *str;  // 等价于 h = h*33 + 当前字符ASCII
        str++;
    }
    return h % HASH_SIZE;  // 模1009得到哈希桶索引
}

// ===================== 数据结构定义 =====================
// 策略1/2的线性表节点(存储单词+词频)
struct LinearNode {
    char word[MAX_WORD_LEN];
    int count;
};

// 策略3的哈希表节点(链地址法)
struct HashNode {
    char word[MAX_WORD_LEN];
    int count;
    HashNode* next;
    // 构造函数:初始化单词,词频默认1,指针置空
    HashNode(const char* w) : count(1), next(nullptr) {
        strcpy(word, w);
    }
};

// 策略3的哈希表结构体
struct HashTable {
    HashNode* buckets[HASH_SIZE];  // 哈希桶数组
    int unique_count;              // 不同单词数
    // 构造函数:初始化桶为空,不同词数为0
    HashTable() : unique_count(0) {
        memset(buckets, 0, sizeof(buckets));
    }
};

// ===================== 策略1:顺序查找函数 =====================
// 返回值:是否找到目标单词;输出参数cmp_count:查找的比较次数
bool linearSearch(LinearNode* list, int size, const char* word, int& cmp_count) {
    cmp_count = 0;
    for (int i = 0; i < size; i++) {
        cmp_count++;  // 每一次比较计数+1
        if (strcmp(list[i].word, word) == 0) {
            return true;
        }
    }
    return false;
}

// ===================== 策略2:快速排序(手写,字典序) =====================
// 交换两个线性表节点
void swapLinearNode(LinearNode& a, LinearNode& b) {
    LinearNode temp = a;
    a = b;
    b = temp;
}

// 快速排序分区函数(按单词字典序)
int partition(LinearNode* list, int low, int high) {
    char pivot[MAX_WORD_LEN];
    strcpy(pivot, list[high].word);  // 选最后一个元素作为基准
    int i = low - 1;  // 小于基准的区域边界

    for (int j = low; j < high; j++) {
        // 字典序:list[j].word < pivot 则交换
        if (strcmp(list[j].word, pivot) < 0) {
            i++;
            swapLinearNode(list[i], list[j]);
        }
    }
    swapLinearNode(list[i+1], list[high]);  // 基准归位
    return i + 1;
}

// 快速排序主函数
void quickSort(LinearNode* list, int low, int high) {
    if (low < high) {
        int pi = partition(list, low, high);
        quickSort(list, low, pi - 1);   // 递归排序左半区
        quickSort(list, pi + 1, high);  // 递归排序右半区
    }
}

// ===================== 策略2:二分查找函数 =====================
// 返回值:是否找到目标单词;输出参数cmp_count:查找的比较次数
bool binarySearch(LinearNode* list, int size, const char* word, int& cmp_count) {
    cmp_count = 0;
    int low = 0, high = size - 1;
    while (low <= high) {
        cmp_count++;  // 每一次比较mid位置计数+1
        int mid = (low + high) / 2;
        int cmp_res = strcmp(list[mid].word, word);
        
        if (cmp_res == 0) {
            return true;  // 找到目标
        } else if (cmp_res > 0) {
            high = mid - 1;  // 目标在左半区
        } else {
            low = mid + 1;   // 目标在右半区
        }
    }
    return false;
}

// ===================== 主函数(核心逻辑) =====================
int main() {
    int strategy;
    cin >> strategy;
    cin.ignore();  // 忽略输入策略后的换行符

    // 初始化各策略的存储结构
    LinearNode linearList[MAX_LINEAR_SIZE] = {0};  // 策略1/2的线性表
    int linearSize = 0;                            // 线性表当前有效长度
    HashTable hashTable;                           // 策略3的哈希表
    int totalWords = 0;                            // 总词数(含重复)

    // ===================== 读取输入文本(直到#) =====================
    string line;
    while (getline(cin, line)) {
        // 检查是否包含结束符#,截断#后的内容
        size_t sharpPos = line.find('#');
        bool isEnd = (sharpPos != string::npos);
        if (isEnd) {
            line = line.substr(0, sharpPos);
        }

        // 处理当前行的所有单词
        const char* token = line.c_str();
        int pos = 0;
        char word[MAX_WORD_LEN];
        while (Normalize(token, pos, word)) {
            totalWords++;  // 总词数+1

            // ===================== 策略1/2:构建线性表 =====================
            if (strategy == 1 || strategy == 2) {
                int cmp;
                bool found = linearSearch(linearList, linearSize, word, cmp);
                if (found) {
                    // 单词已存在,词频+1
                    for (int i = 0; i < linearSize; i++) {
                        if (strcmp(linearList[i].word, word) == 0) {
                            linearList[i].count++;
                            break;
                        }
                    }
                } else {
                    // 单词不存在,添加到线性表尾部
                    strcpy(linearList[linearSize].word, word);
                    linearList[linearSize].count = 1;
                    linearSize++;
                }
            }
            // ===================== 策略3:构建哈希表 =====================
            else if (strategy == 3) {
                unsigned int idx = myHash(word);  // 修改2:调用重命名后的myHash
                HashNode* p = hashTable.buckets[idx];
                bool found = false;

                // 遍历链表查找单词
                while (p != nullptr) {
                    if (strcmp(p->word, word) == 0) {
                        p->count++;
                        found = true;
                        break;
                    }
                    p = p->next;
                }

                // 单词不存在,头插法添加新节点
                if (!found) {
                    HashNode* newNode = new HashNode(word);
                    newNode->next = hashTable.buckets[idx];
                    hashTable.buckets[idx] = newNode;
                    hashTable.unique_count++;
                }
            }

            // 遇到#则终止所有输入处理
            if (isEnd) {
                goto endInput;
            }
        }

        // 行内包含#,终止循环
        if (isEnd) {
            break;
        }
    }
endInput:  // 输入结束标记

    // ===================== 计算Unique Words和ASL =====================
    int uniqueWords = 0;
    double ASL = 0.0;
    int totalCmp = 0;  // 所有不同单词的查找比较次数总和

    if (strategy == 1) {
        // 策略1:线性表+顺序查找
        uniqueWords = linearSize;
        for (int i = 0; i < linearSize; i++) {
            int cmp;
            linearSearch(linearList, linearSize, linearList[i].word, cmp);
            totalCmp += cmp;
        }
        ASL = (double)totalCmp / uniqueWords;
    }
    else if (strategy == 2) {
        // 策略2:快速排序+二分查找
        uniqueWords = linearSize;
        quickSort(linearList, 0, linearSize - 1);  // 先排序
        for (int i = 0; i < linearSize; i++) {
            int cmp;
            binarySearch(linearList, linearSize, linearList[i].word, cmp);
            totalCmp += cmp;
        }
        ASL = (double)totalCmp / uniqueWords;
    }
    else if (strategy == 3) {
        // 策略3:哈希表(链地址法)
        uniqueWords = hashTable.unique_count;
        // 遍历所有哈希桶,统计每个节点的查找次数(链表位置)
        for (int i = 0; i < HASH_SIZE; i++) {
            HashNode* p = hashTable.buckets[i];
            int pos = 1;  // 链表中位置(头节点为1次比较)
            while (p != nullptr) {
                totalCmp += pos;
                pos++;
                p = p->next;
            }
        }
        ASL = (double)totalCmp / uniqueWords;
    }

    // ===================== 输出结果(严格按格式) =====================
    printf("Total Words: %d\n", totalWords);
    printf("Unique Words: %d\n", uniqueWords);
    printf("ASL: %.2f\n", ASL);

    // ===================== 释放哈希表内存(防止内存泄漏) =====================
    if (strategy == 3) {
        for (int i = 0; i < HASH_SIZE; i++) {
            HashNode* p = hashTable.buckets[i];
            while (p != nullptr) {
                HashNode* temp = p;
                p = p->next;
                delete temp;
            }
            hashTable.buckets[i] = nullptr;
        }
    }

    return 0;
}

三、C++代码详细解释

  1. 读取文本输入(以#为结束符),将文本中的单词标准化(提取连续字母、转为小写);
  2. 支持三种不同的查找 / 统计策略,统计文本的总词数不同单词数(Unique Words)平均查找长度(ASL)
  3. 输出统计结果,并针对哈希表策略做内存释放,避免内存泄漏。

(一)全局常量定义(满足题目强制要求)

cpp 复制代码
const int MAX_LINEAR_SIZE = 10000;  // 线性表最大容量(适配常规文本输入)
const int HASH_SIZE = 1009;         // 哈希表桶数(题目强制要求1009)
const int MAX_WORD_LEN = 50;        // 单个单词最大长度(避免字符串越界)
  • 常量命名全大写 + 下划线,符合 C++ 工程规范;
  • HASH_SIZE=1009是题目强制要求(1009 是质数,能减少哈希冲突);
  • MAX_LINEAR_SIZE足够大,避免线性表溢出。

(二)核心工具函数

1. 单词标准化函数 Normalize(题目强制要求)
cpp 复制代码
int Normalize(const char *token, int &pos, char *word) {
    // 跳过所有非字母字符(比如标点、数字、空格)
    while (token[pos] && !isalpha(token[pos]))
        pos++;

    if (!token[pos])  // 字符串处理完毕,无单词可提取
        return 0;

    // 提取连续字母并转为小写
    int len = 0;
    while (token[pos] && isalpha(token[pos])) {
        word[len++] = tolower(token[pos++]);
    }
    word[len] = '\0';  // 手动添加字符串结束符(C风格字符串必需)
    return 1;
}
  • 功能:从输入字符串中提取 "纯字母单词",并标准化为小写;
  • 参数说明
    • token:待处理的原始字符串(比如一行文本);
    • pos:当前处理位置(引用传参,因为要修改全局位置);
    • word:输出参数,存储标准化后的单词;
  • 返回值1表示提取到有效单词,0表示无单词可提取;
  • 关键逻辑:先跳过非字母字符 → 提取连续字母并转小写 → 补全字符串结束符。
2. Time33 哈希函数 myHash(题目强制要求,重命名避免关键字冲突)
cpp 复制代码
unsigned int myHash(const char *str) {  // 原函数名hash→myHash(hash是C++关键字)
    unsigned int h = 0;
    while (*str) {
        h = (h << 5) + h + *str;  // 等价于 h = h*33 + 当前字符ASCII(Time33核心)
        str++;
    }
    return h % HASH_SIZE;  // 模1009得到哈希桶索引(保证索引在0~1008之间)
}
  • 功能 :将字符串映射为0~1008的整数(哈希索引);
  • Time33 算法原理 :通过h = h*33 + 字符ASCII计算哈希值,位运算h<<5 +hh*33的优化(左移 5 位 =×32,加 h=×33);
  • 为什么模 1009:题目强制要求,且 1009 是质数,能最大化减少哈希冲突。

(三)数据结构定义

1. 线性表节点(策略 1/2 专用)
cpp 复制代码
struct LinearNode {
    char word[MAX_WORD_LEN];  // 存储标准化后的单词
    int count;                // 单词出现次数(词频)
};
  • 用于策略 1(顺序查找)和策略 2(排序 + 二分查找),线性表由LinearNode数组实现。
2. 哈希表节点(策略 3 专用,链地址法解决冲突)
cpp 复制代码
struct HashNode {
    char word[MAX_WORD_LEN];
    int count;
    HashNode* next;  // 链表指针(链地址法核心)
    // 构造函数:初始化单词,词频默认1,指针置空
    HashNode(const char* w) : count(1), next(nullptr) {
        strcpy(word, w);  // 拷贝单词到节点
    }
};
  • 链地址法:每个哈希桶对应一个链表,冲突的单词存入同一链表;
  • 构造函数简化节点创建,避免手动初始化。
3. 哈希表结构体(策略 3 专用)
cpp 复制代码
struct HashTable {
    HashNode* buckets[HASH_SIZE];  // 哈希桶数组(1009个桶)
    int unique_count;              // 不同单词总数(避免重复统计)
    // 构造函数:初始化桶为空,不同词数为0
    HashTable() : unique_count(0) {
        memset(buckets, 0, sizeof(buckets));  // 所有桶指针置空
    }
};
  • buckets:每个元素是HashNode*,指向对应桶的链表头;
  • unique_count:记录哈希表中不同单词的数量(简化后续统计)。

(四)查找 / 排序算法(策略 1/2 核心)

1. 策略 1:顺序查找 linearSearch
cpp 复制代码
bool linearSearch(LinearNode* list, int size, const char* word, int& cmp_count) {
    cmp_count = 0;
    for (int i = 0; i < size; i++) {
        cmp_count++;  // 每一次字符串比较,计数+1(统计查找次数)
        if (strcmp(list[i].word, word) == 0) {
            return true;  // 找到目标单词
        }
    }
    return false;  // 未找到
}
  • 功能:遍历线性表,查找目标单词;
  • 参数说明
    • list:线性表数组;
    • size:线性表有效长度;
    • word:目标单词;
    • cmp_count:输出参数,记录查找过程中的比较次数;
  • 返回值true找到,false未找到;
  • 关键strcmp是 C 风格字符串比较函数,返回 0 表示两个字符串相等。
2. 策略 2:快速排序(手写,按字典序)
(1)交换节点函数 swapLinearNode
cpp 复制代码
void swapLinearNode(LinearNode& a, LinearNode& b) {
    LinearNode temp = a;
    a = b;
    b = temp;
}
  • 交换两个线性表节点的所有数据(单词 + 词频)。
(2)快排分区函数 partition
cpp 复制代码
int partition(LinearNode* list, int low, int high) {
    char pivot[MAX_WORD_LEN];
    strcpy(pivot, list[high].word);  // 选最后一个元素作为基准
    int i = low - 1;  // 小于基准的区域边界

    for (int j = low; j < high; j++) {
        // 字典序比较:list[j].word < pivot 则交换
        if (strcmp(list[j].word, pivot) < 0) {
            i++;
            swapLinearNode(list[i], list[j]);
        }
    }
    swapLinearNode(list[i+1], list[high]);  // 基准元素归位
    return i + 1;  // 返回基准元素的索引
}
  • 核心逻辑:以最后一个元素为基准,将线性表划分为 "小于基准" 和 "大于基准" 两部分;
  • 字典序判断strcmp(a, b) < 0 表示a的字典序小于b
(3)快排主函数 quickSort
cpp 复制代码
void quickSort(LinearNode* list, int low, int high) {
    if (low < high) {
        int pi = partition(list, low, high);
        quickSort(list, low, pi - 1);   // 递归排序左半区
        quickSort(list, pi + 1, high);  // 递归排序右半区
    }
}
  • 递归实现快速排序,最终线性表按单词字典序升序排列。
3. 策略 2:二分查找 binarySearch
cpp 复制代码
bool binarySearch(LinearNode* list, int size, const char* word, int& cmp_count) {
    cmp_count = 0;
    int low = 0, high = size - 1;
    while (low <= high) {
        cmp_count++;  // 每一次折半比较,计数+1
        int mid = (low + high) / 2;
        int cmp_res = strcmp(list[mid].word, word);
        
        if (cmp_res == 0) {
            return true;  // 找到目标
        } else if (cmp_res > 0) {
            high = mid - 1;  // 目标在左半区
        } else {
            low = mid + 1;   // 目标在右半区
        }
    }
    return false;  // 未找到
}
  • 前提:线性表必须已按字典序排序;
  • 核心逻辑:折半缩小查找范围,每次比较后排除一半元素,效率远高于顺序查找。

(五)主函数(核心执行流程)

1. 初始化阶段
cpp 复制代码
int strategy;
cin >> strategy;
cin.ignore();  // 忽略输入策略后的换行符(避免干扰后续文本读取)

// 初始化存储结构
LinearNode linearList[MAX_LINEAR_SIZE] = {0};  // 策略1/2的线性表
int linearSize = 0;                            // 线性表当前有效长度
HashTable hashTable;                           // 策略3的哈希表
int totalWords = 0;                            // 总词数(含重复)
  • 输入策略(1/2/3),cin.ignore()处理换行符是关键(否则getline会读取空行);
  • 初始化线性表、哈希表、统计变量。
2. 读取并处理输入文本
cpp 复制代码
string line;
while (getline(cin, line)) {
    // 检查是否包含结束符#,截断#后的内容
    size_t sharpPos = line.find('#');
    bool isEnd = (sharpPos != string::npos);
    if (isEnd) {
        line = line.substr(0, sharpPos);
    }

    // 处理当前行的所有单词
    const char* token = line.c_str();
    int pos = 0;
    char word[MAX_WORD_LEN];
    while (Normalize(token, pos, word)) {
        totalWords++;  // 总词数+1

        // 策略1/2:线性表处理
        if (strategy == 1 || strategy == 2) {
            int cmp;
            bool found = linearSearch(linearList, linearSize, word, cmp);
            if (found) {
                // 单词已存在,词频+1
                for (int i = 0; i < linearSize; i++) {
                    if (strcmp(linearList[i].word, word) == 0) {
                        linearList[i].count++;
                        break;
                    }
                }
            } else {
                // 单词不存在,添加到线性表尾部
                strcpy(linearList[linearSize].word, word);
                linearList[linearSize].count = 1;
                linearSize++;
            }
        }
        // 策略3:哈希表处理
        else if (strategy == 3) {
            unsigned int idx = myHash(word);
            HashNode* p = hashTable.buckets[idx];
            bool found = false;

            // 遍历链表查找单词
            while (p != nullptr) {
                if (strcmp(p->word, word) == 0) {
                    p->count++;
                    found = true;
                    break;
                }
                p = p->next;
            }

            // 单词不存在,头插法添加新节点
            if (!found) {
                HashNode* newNode = new HashNode(word);
                newNode->next = hashTable.buckets[idx];
                hashTable.buckets[idx] = newNode;
                hashTable.unique_count++;
            }
        }

        // 遇到#则终止所有输入处理
        if (isEnd) {
            goto endInput;
        }
    }

    // 行内包含#,终止循环
    if (isEnd) {
        break;
    }
}
endInput:  // 输入结束标记
  • 核心逻辑
    1. 逐行读取文本,遇到#则截断并终止输入;
    2. 调用Normalize提取标准化单词,每提取一个单词,总词数totalWords+1
    3. 按策略处理单词:
      • 策略 1/2 :用linearSearch查找单词 → 存在则词频 + 1 → 不存在则添加到线性表尾部;
      • 策略 3 :计算哈希索引 → 遍历对应桶的链表查找单词 → 存在则词频 + 1 → 不存在则头插法添加新节点(头插法效率更高),并更新unique_count
3. 计算 Unique Words 和 ASL(平均查找长度)
cpp 复制代码
int uniqueWords = 0;
double ASL = 0.0;
int totalCmp = 0;  // 所有不同单词的查找比较次数总和

if (strategy == 1) {
    // 策略1:顺序查找
    uniqueWords = linearSize;
    for (int i = 0; i < linearSize; i++) {
        int cmp;
        linearSearch(linearList, linearSize, linearList[i].word, cmp);
        totalCmp += cmp;
    }
    ASL = (double)totalCmp / uniqueWords;
}
else if (strategy == 2) {
    // 策略2:快速排序+二分查找
    uniqueWords = linearSize;
    quickSort(linearList, 0, linearSize - 1);  // 先排序
    for (int i = 0; i < linearSize; i++) {
        int cmp;
        binarySearch(linearList, linearSize, linearList[i].word, cmp);
        totalCmp += cmp;
    }
    ASL = (double)totalCmp / uniqueWords;
}
else if (strategy == 3) {
    // 策略3:哈希表
    uniqueWords = hashTable.unique_count;
    // 遍历所有哈希桶,统计每个节点的查找次数(链表位置)
    for (int i = 0; i < HASH_SIZE; i++) {
        HashNode* p = hashTable.buckets[i];
        int pos = 1;  // 链表中位置(头节点为1次比较)
        while (p != nullptr) {
            totalCmp += pos;
            pos++;
            p = p->next;
        }
    }
    ASL = (double)totalCmp / uniqueWords;
}
  • Unique Words(不同单词数)
    • 策略 1/2:等于线性表的有效长度linearSize
    • 策略 3:等于哈希表的unique_count(插入时已统计);
  • ASL(平均查找长度)ASL = 所有不同单词的查找比较次数总和 / 不同单词数
    • 策略 1:对每个单词做顺序查找,累加比较次数;
    • 策略 2:先排序,再对每个单词做二分查找,累加比较次数;
    • 策略 3:哈希表的 ASL 统计逻辑是 "链表中第 n 个节点需要 n 次比较"(头节点 1 次,第二个 2 次...)。
4. 输出结果 + 内存释放
cpp 复制代码
// 输出结果(严格按格式)
printf("Total Words: %d\n", totalWords);
printf("Unique Words: %d\n", uniqueWords);
printf("ASL: %.2f\n", ASL);

// 释放哈希表内存(防止内存泄漏)
if (strategy == 3) {
    for (int i = 0; i < HASH_SIZE; i++) {
        HashNode* p = hashTable.buckets[i];
        while (p != nullptr) {
            HashNode* temp = p;
            p = p->next;
            delete temp;
        }
        hashTable.buckets[i] = nullptr;
    }
}
  • 输出格式严格:Total WordsUnique WordsASL保留两位小数;
  • 策略 3 必须释放哈希表节点内存(否则会造成内存泄漏):遍历每个桶的链表,逐个delete节点。

(六)关键细节与注意事项

  1. C 风格字符串处理 :全程使用strcpy(拷贝)、strcmp(比较),且手动补全\0,避免字符串越界;
  2. 内存管理 :策略 3 的哈希表节点是动态分配的(new HashNode),必须手动delete释放;
  3. 引用传参NormalizeposlinearSearch/binarySearchcmp_count用引用传参,确保修改能作用到外部变量;
  4. 结束符处理#可能出现在行中间,需截断并立即终止输入(goto endInput);
  5. 哈希冲突解决:策略 3 用链地址法(每个桶对应链表),是哈希表冲突解决的经典方案。

(七)核心知识点总结

模块 核心知识点
字符串处理 C 风格字符串、strcpy/strcmptolower/isalpha
查找算法 顺序查找、二分查找(前提:有序)
排序算法 快速排序(手写、字典序)
哈希表 Time33 哈希函数、链地址法解决冲突、质数桶数减少冲突
性能分析 平均查找长度(ASL)的计算逻辑
内存管理 动态分配 / 释放节点(避免内存泄漏)

四、总结

本文实现了一个英文单词词频统计与检索系统,支持三种数据结构策略:线性表顺序查找、线性表快速排序后二分查找、哈希表链地址法。系统读取输入文本直到遇到#结束,对单词进行标准化处理(转为小写、提取连续字母),统计总词数和不同词数,并计算各策略的平均查找长度(ASL)。关键实现包括手写快速排序、二分查找和哈希表冲突处理,最终输出统计结果并确保内存安全释放。该系统有效对比了不同数据结构的查找效率差异。

相关推荐
独自破碎E2 小时前
【迭代+动态规划】把数字翻译成字符串
算法·动态规划
sunfove2 小时前
从信息熵到决策边界:决策树算法的第一性原理与深度解析
算法·决策树·机器学习
Niuguangshuo2 小时前
CLIP:连接图像与文本的 AI 核心工具
人工智能·神经网络·算法
sali-tec2 小时前
C# 基于OpenCv的视觉工作流-章13-边缘提取
人工智能·opencv·算法·计算机视觉
Elnaij2 小时前
从C++开始的编程生活(17)——多态
开发语言·c++
Frank_refuel2 小时前
C++之多态详解
开发语言·c++
kaikaile19952 小时前
基于MATLAB的PSO-ELM(粒子群优化极限学习机)算法实现
深度学习·算法·matlab
YuTaoShao2 小时前
【LeetCode 每日一题】1895. 最大的幻方——(解法二)前缀和优化
linux·算法·leetcode
Elnaij2 小时前
从C++开始的编程生活(18)——二叉搜索树基础
开发语言·c++