// 包含所需的标准库头文件
#include <iostream> // 用于输入输出
#include <queue> // 用于优先队列
#include <unordered_map>// 用于哈希映射
#include <vector> // 用于动态数组
#include <string> // 用于字符串处理
#include <fstream> // 用于文件操作
#include <sstream> // 用于字符串流处理
#include <direct.h> // 用于目录操作
#include <random> // 用于随机数生成
using namespace std;
// RSA加密算法的类定义
class RSA
{
private:
// 声明私有成员变量
long long p, q; // p和q是两个大素数
long long n; // n是p和q的乘积
long long e; // e是公钥指数
long long d; // d是私钥指数
// 判断一个数是否为素数的方法
bool isPrime(long long num)
{
// 如果数小于等于1,不是素数
if (num <= 1) return false;
// 检查从2到num的平方根的所有数
for (long long i = 2; i <= sqrt(num); i++)
{
// 如果num能被i整除,不是素数
if (num % i == 0) return false;
}
// 通过所有检查,是素数
return true;
}
// 生成随机素数的方法,需包含random头文件
long long generatePrime()
{
// 创建随机数生成器
// 用于获取一个硬件随机数种子,提供高质量随机性。
random_device rd;
// 使用Mersenne Twister算法,
mt19937 gen(rd());
// 定义随机数范围(100到1000)
// 构造函数uniform_int_distribution<T> dis(a, b);
// T:指生成的随机数的类型,a 和 b:随机数的范围
uniform_int_distribution<long long> dis(100, 1000);
// 生成随机数直到找到一个素数
long long num = dis(gen);
while (!isPrime(num))
{
num = dis(gen);
}
return num;
}
// 计算最大公约数的方法(欧几里德算法)
// 时间复杂度O(log(min(a,b)))
// 空间复杂度O(log(min(a,b)))(递归栈深度)
long long gcd(long long a, long long b)
{
if (b == 0) return a;
return gcd(b, a % b);
}
// 计算模反元素的方法(扩展欧几里德算法),此算法用来计算私钥
// 在数论中,模逆元是指对于给定的整数 a 和模数 m,找到一个整数 x,使得:(a*x)mod m = 1
// 时间复杂度O(log(min(a,m))),空间复杂度O(1)
long long modInverse(long long a, long long m)
{
// 保存模数 m 的初始值,之后在计算过程中可能会对 m 进行修改,但最终的结果需要使用原始的 m 来确保最终结果为正数。
long long m0 = m;
// 初始化两个变量 x 和 y。
// x 是我们最终要求的模逆元(即 a 的模逆元),初始化为 1。
// y 是用于扩展欧几里得算法的辅助变量,初始化为 0。
long long y = 0, x = 1;
// 如果模数 m 为 1,那么模逆元是无定义的,直接返回 0。
// 因为对于任何数 a,当 m = 1 时, (a * x) % 1 总是 0,而不是 1。
if (m == 1) return 0;
// 扩展欧几里德算法的核心部分
// 循环在 a > 1 时继续进行,目的是不断地将 a 和 m 替换成它们的余数,直到找到一个能够使 a = 1 的情况
// 在这个特定问题中,我们关心的是 a * x ≡ 1 (mod m),即 a * x + m * y = 1
while (a > 1)
{
long long q = a / m;
long long t = m;
m = a % m;
a = t;
t = y;
y = x - q * y;
x = t;
}
// 确保结果为正数
if (x < 0) x += m0;
return x;
}
public:
// 构造函数,创建RSA对象时自动生成密钥对
RSA()
{
generateKeys();
}
// 生成RSA密钥对的方法
void generateKeys()
{
// 生成两个不同的素数p和q
p = generatePrime();
do
{
q = generatePrime();
} while (p == q);//确保p!=q
// 计算n和欧拉函数T
n = p * q;
long long T = (p - 1) * (q - 1);
// 选择公钥指数e
e = 65537; // 使用常用的公钥指数
//e应该满足: 1 < e < T,且 e 不是 T 的因子
while (gcd(e, T) != 1 && e>=T)
{
e--;
}
// 计算私钥指数d
d = modInverse(e, T);
}
// 加密函数,将消息转换为密文
// 使用 const 修饰函数参数时,表示传入的参数在函数体内不会被修改。
// 这种用法非常常见,特别是在处理大型对象时,能够避免不必要的拷贝并保护数据不被修改。
// & 表示 message 是一个引用。引用是对原对象的别名
// 函数中对 message 的任何操作都直接作用于传入的原始对象,而不是该对象的副本。
// 如果没有使用引用传递,而是按值传递(string message),那么函数会创建 message 的副本
// 这对于大型数据(例如 string 或 vector)来说,可能会消耗不必要的内存和时间。
vector<long long> encrypt(const string& message)
{
// 初始化一个量 encrypted,用于存储加密后的每个字符
vector<long long> encrypted;
// 遍历消息中的每个字符,逐个加密。
for (char c : message)
{
// 将字符转换为数字
long long m = static_cast<long long>(c);
// result:初始化为 1,用于存储最终的加密值。
long long result = 1;
// base:计算当前字符模n的结果,用于加密计算。
long long base = m % n;
// exp:指数e,即公钥的一部分。
long long exp = e;
// 使用快速幂算法计算加密结果
// 当 exp 为 0 时,循环终止
while (exp > 0)
{
if (exp & 1)
{
result = (result * base) % n;
}
// 将 base 平方,并取模n。
base = (base * base) % n;
// 指数右移一位,准备处理下一位
exp >>= 1;
}
// 将当前字符的加密值 result 添加到 encrypted 中
encrypted.push_back(result);
}
return encrypted;
}
// 快速幂的核心是通过逐次平方和模运算减少计算量,复杂度为 O(log(e)),详细分析如下:
// 每次迭代:
// 如果 e 的当前位为 1,则 result = (result*base) % n,这是一个常数时间操作 O(1)。
// base = (base*base) % n,也是常数时间操作 O(1)。
// e 右移一位,减少问题规模一半。
// 总迭代次数:二进制表示中 e 的位数是 O(log(e))
// 因此,每次字符加密的时间复杂度是 O(log(e))
// 假设字符串的长度为 𝐿 则算法需要对每个字符执行快速幂计算。对于整个字符串,加密的总复杂度为:O(Llog(e))
// 1. 空间开销
// 临时变量:每次字符加密中,使用了一些局部变量(如 m, result, base, exp),这些变量占用 O(1) 空间。
// 加密结果存储 : 存储加密后的结果需要 O(L) 空间。
// 2. 总空间复杂度
// 总空间复杂度为 O(L),用于存储加密结果。
// 解密函数,将密文转换回原文
string decrypt(const vector<long long>& encrypted)
{
string decrypted;
// 对每个加密数字进行解密
for (long long c : encrypted)
{
long long result = 1;
long long base = c % n;
long long exp = d;
// 使用快速幂算法计算解密结果
while (exp > 0)
{
if (exp & 1)
{
result = (result * base) % n;
}
base = (base * base) % n;
exp >>= 1;
}
// 将解密后的数字转换回字符
decrypted += static_cast<char>(result);
}
return decrypted;
}
// 将公钥和私钥保存到文件
void saveKeys(const string& filename)
{
// 创建并打开一个输出文件流(ofstream),文件名由参数 filename 提供
ofstream keyFile(filename);
// 写入公钥的 e 和 n
keyFile << "Public Key (e,n): " << e << "," << n << "\n";
// 写入私钥的 d 和 n
keyFile << "Private Key (d,n): " << d << "," << n << "\n";
// 关闭文件流
keyFile.close();
}
};
// 哈夫曼树的节点类定义
class HuffmanNode
{
public:
char ch; // 存储字符
int freq; // 存储频率
HuffmanNode* left; // 左孩子指针
HuffmanNode* right; // 右孩子指针
// 构造函数,初始化节点
// char c: 初始化字符。
// int f : 初始化频率。
// ch 被初始化为 c。
// freq 被初始化为 f。
// left 和 right 被初始化为 nullptr,因为新节点默认没有子节点。
HuffmanNode(char c, int f) : ch(c), freq(f), left(nullptr), right(nullptr) {}
};
// 用于优先队列的比较结构体
// 该代码是一个仿函数(functor),用于在优先队列或其他排序容器中自定义排序规则。
struct CompareNode
{
// 重载操作符,用于比较两个节点的频率
bool operator()(HuffmanNode* l, HuffmanNode* r)
{
// 通过访问 l 和 r 节点的 freq 成员( freq 存储了节点的频率),判断 l 的频率是否大于 r 的频率。
return l->freq > r->freq; // 频率小的优先级高
}
// 如果 l 的频率大于 r 的频率,返回 true,即 r 的优先级更高,r 会排在 l 前面
};
// 哈夫曼编码类定义
class HuffmanCoding
{
private:
// 用于存储构建好的哈夫曼树的根节点。通过根节点可以访问整棵树。
HuffmanNode* root;
// 存储字符到编码的映射,
// 例如 {'a': "110", 'b': "10"},用于将原始文本编码为哈夫曼编码形式。
unordered_map<char, string> huffmanCodes;
// 存储编码到字符的映射
// 例如 { "110": 'a', "10": 'b' },用于将哈夫曼编码解码回原始文本。
unordered_map<string, char> reverseCodes;
// RSA加密对象
RSA rsa;
// 构建哈夫曼树的方法
// 该方法接收一个字符频率映射表,使用哈夫曼编码算法构建一棵哈夫曼树。
// freqMap:一个 unordered_map,存储每个字符及其出现频率,例如{ 'a': 5, 'b' : 9, 'c' : 12, 'd' : 13 }。
// 时间复杂度分析:
// 1. 初始化优先队列
// 堆是完全二叉树,其高度为 h:
// h = ⌊log2(N)⌋ + 1其中 N 是堆中元素的数量。
// 在最坏情况下,新插入的元素需要从底部移动到堆的根节点。
// 需要执行 h 次比较和交换操作。
// 每层操作的时间是 O(1),总调整时间为 O(h)。
// 因此,插入一个元素的时间复杂度为:O(logN)
// 插入优先队列需要维护堆的结构,插入一个元素的时间复杂度为:O(logN),其中 N 是优先队列中当前的元素个数。
// 假设 freqMap 的大小为 C(字符种类数),总共有 C 次插入操作:O(C⋅logC)
// 2. 构建哈夫曼树
// 每次循环,从优先队列中取出两个最小频率的节点,创建一个新节点并插入优先队列。
// 删除两个节点 : 每次删除操作的时间复杂度是 O(logN)。
// 插入一个节点 : 插入操作的时间复杂度是 O(logN)。
// 总共需要 O(logN) + O(logN) = O(logN) 时间。
// 总循环次数:
// 构建哈夫曼树需要从 C 个初始节点逐步合并为 1 个节点,因此循环次数为 C−1。
// 3. 构建过程的总复杂度:
// O((C−1)⋅logC)≈O(C⋅logC)
// 空间复杂度分析
// 优先队列最多包含 C 个节点,同时合并过程中最多 C−1 个父节点,最终优先队列中只剩一个节点。
// 因此,优先队列的空间复杂度为 O(C)。
// 哈夫曼树节点的总数为 2C−1,空间复杂度为 O(C)。
void buildHuffmanTree(const unordered_map<char, int>& freqMap)
{
// 创建一个优先队列 pq,存储哈夫曼节点(HuffmanNode*)
// 优先队列(Priority Queue) 是一种特殊的队列数据结构
// 它与普通队列的主要区别在于元素的出队顺序:优先队列中的元素不是按照插入顺序出队,而是按照优先级出队。
// 使用 CompareNode 自定义比较器,使得频率较小的节点具有更高的优先级。
priority_queue<HuffmanNode*, vector<HuffmanNode*>, CompareNode> pq;
// 将所有字符和频率添加到优先队列
// 遍历 freqMap 中的每个字符及其频率,将它们转换为 HuffmanNode 对象,并加入优先队列。
// 每个 HuffmanNode 包含字符(char)和频率(int)
for (const auto& pair : freqMap)
{
pq.push(new HuffmanNode(pair.first, pair.second));
}
// 构建哈夫曼树
// 当优先队列中剩余的节点数大于 1 时,继续构建树。
while (pq.size() > 1)
{
// 从优先队列中取出两个频率最小的节点(left 和 right)
HuffmanNode* left = pq.top(); pq.pop();
HuffmanNode* right = pq.top(); pq.pop();
// 创建一个新的父节点:
// 字符: 用特殊字符 '$' 表示(无实际意义,只是占位符)。
// 频率: 是两个子节点频率的和。
HuffmanNode* parent = new HuffmanNode('$', left->freq + right->freq);
//将 left 和 right 分别设置为父节点的左子节点和右子节点
parent->left = left;
parent->right = right;
// 将新创建的父节点加入优先队列。
pq.push(parent);
}
// 循环结束后,优先队列中只剩一个节点,这就是整棵哈夫曼树的根节点。
// 将 root 指针指向这个节点,完成哈夫曼树的构建。
root = pq.top();
}
// 生成哈夫曼编码的方法
// node: 当前递归访问的节点(HuffmanNode 类型)。它可以是根节点或子节点。
// code: 当前路径对应的编码字符串。随着递归的进行,code 会不断累加 0 或 1。
// 时间复杂度分析:
// 该函数使用递归遍历哈夫曼树中的所有节点,每个节点只会被访问一次。
// 因此,遍历所有节点的时间复杂度为 O(2C−1),即 O(C)。
// 对于路径字符串 code 的操作(如 code + "0" 和 code + "1"),涉及字符串的复制。
// 对于一棵高度为 H 的哈夫曼树,从根节点到叶子节点的路径长度为 H。
// 哈夫曼树是一棵最优二叉树,其高度 H 近似为log2(C)
// 因此,每次字符串操作的开销近似为 O(H) = O(logC)。
// 每个叶子节点都会存储一个编码字符串,操作涉及 O(C) 次。
// 因此总开销为:O(C) + O(C⋅logC) = O(ClogC)
// 空间复杂度
//1. 哈夫曼树的存储
// 整棵树的空间占用约为 (2C−1),近似为 O(C)
//2. 哈夫曼编码表的存储
// 编码表 huffmanCodes 是一个哈希表,存储 C 个字符到编码的映射:
// 每个键(字符):占用 1 字节。
// 每个值(编码字符串):长度为 H(路径长度),近似为 O(logC)。
// 总空间需求为 C×O(logC)。
// 空间复杂度的主要部分是哈夫曼编码表
void generateCodes(HuffmanNode* node, const string& code)
{
// 如果当前节点为空(nullptr),直接返回。
if (!node) return;
// 如果当前节点没有左孩子和右孩子,那么它是一个叶子节点
if (!node->left && !node->right)
{
// 将当前路径(code)作为哈夫曼编码,保存到 huffmanCodes 中,建立字符到编码的映射。
huffmanCodes[node->ch] = code;
// 将编码和字符反向存储到 reverseCodes 中,便于解码时使用。
reverseCodes[code] = node->ch;
return;
}
// 递归处理左右子树
// 如果当前节点有左子节点,则递归调用 generateCodes,将路径加上 "0"。
generateCodes(node->left, code + "0");
// 如果当前节点有右子节点,则递归调用 generateCodes,将路径加上 "1"。
generateCodes(node->right, code + "1");
}
// 保存文件的方法
// content:文件的内容,即需要写入文件的数据。
// filename:文件的名称,例如 "output.txt"。
// directory:文件所在的目录,例如 "C:\\mydir"。
void saveFile(const string& content, const string& filename,const string& directory)
{
// _mkdir:是一个 C 标准库函数,用于创建目录。
// 参数是一个 C 风格字符串,因此需要将 directory 从 string 转换为 C 风格字符串(使用.c_str())。
// 如果目录创建成功,_mkdir 返回 0。
// 如果目录已经存在,errno 会被设置为 EEXIST。
if (_mkdir(directory.c_str()) == 0 || errno == EEXIST)
{
// 如果目录成功创建,或者目录已存在,则继续写入文件。
// ofstream:用于创建并打开文件流。如果文件不存在,它会自动创建。如果文件已经存在,它会覆盖文件内容。
// 使用 directory + "\\" + filename 拼接目录和文件名,形成完整的文件路径。
// 例如,如果 directory 是 "C:\\mydir",filename 是 "output.txt",那么最终路径为 "C:\\mydir\\output.txt"。
ofstream outFile(directory + "\\" + filename);
// outFile << content; 将 content 写入文件。
outFile << content;
//outFile.close(); 关闭文件流,确保数据被正确保存。
outFile.close();
}
// 如果目录创建失败且不存在,则进入 else 部分。
else
{
// 如果目录创建失败(并且不存在),输出错误信息到标准错误流(std::cerr),提示用户无法创建目录。
cerr << "无法创建目录: " << directory << endl;
}
}
// 删除哈夫曼树的方法
// 时间复杂度:O(N),每个节点访问一次。
void deleteTree(HuffmanNode* node)
{
if (!node) return;
deleteTree(node->left); // 递归删除左子树
deleteTree(node->right); // 递归删除右子树
delete node; // 删除当前节点
}
public:
// 构造函数,将哈夫曼树的根节点指针 root 初始化为 nullptr,表示当前还没有构建哈夫曼树。
HuffmanCoding() : root(nullptr) {}
// 统计字符频率的公共方法
// 接收一个字符串 input,统计字符串中每个字符出现的次数,并返回一个 unordered_map(无序映射)。
unordered_map<char, int> countFrequency(const string& input)
{
//声明一个无序映射 freqMap,用于存储字符及其出现的频率。
unordered_map<char, int> freqMap;
// 遍历输入字符串,统计每个字符出现的次数
for (char c : input)
{
// 如果字符 c 已存在于映射中,将其对应的值加 1。
// 如果字符 c 不存在,则会自动插入键 c,并将其值初始化为 0,再加 1。
freqMap[c]++;
}
return freqMap;
}
// 从文件编码的公共方法
// filename: 输入文件的路径,包含需要编码和加密的文本。
// outputDir: 输出文件的目录路径,用于保存生成的文件。
void encodeFromFile(const string& filename, const string& outputDir)
{
// 打开输入文件
ifstream inFile(filename);
if (!inFile)
{
cerr << "无法打开文件: " << filename << endl;
return;
}
// 读取文件内容
// 使用输入流迭代器读取文件内容,将其存储为一个字符串 input。
string input((istreambuf_iterator<char>(inFile)),istreambuf_iterator<char>());
inFile.close();
// 清空之前可能存在的哈夫曼编码和反向映射,以确保生成新编码。
huffmanCodes.clear();
reverseCodes.clear();
// 构建哈夫曼树并生成编码
// 统计频率: 调用 countFrequency 方法,生成字符频率表 freqMap。
// 构建哈夫曼树: 调用 buildHuffmanTree,根据频率表构建哈夫曼树。
// 生成编码 : 调用 generateCodes 方法,生成哈夫曼编码表并存储在 huffmanCodes 和 reverseCodes 中。
auto freqMap = countFrequency(input);
buildHuffmanTree(freqMap);
generateCodes(root, "");
// 保存原始文档
saveFile(input, "document.txt", outputDir);
// 使用哈夫曼编码对文本进行编码
string encodedStr;
for (char c : input)
{
encodedStr += huffmanCodes[c];
}
//保存未加密的数据
saveFile(encodedStr, "code.dat", outputDir);
// 使用RSA加密编码后的数据
vector<long long> encrypted = rsa.encrypt(encodedStr);
// 保存加密后的数据
ofstream outFile(outputDir + "\\encryptedcode.dat", ios::binary);
for (const auto& num : encrypted)
{
outFile << num << " ";
}
outFile.close();
// 保存哈夫曼编码表
ostringstream huffmanTable;
for (const auto& pair : huffmanCodes)
{
huffmanTable << pair.first << ": " << pair.second << "\n";
}
saveFile(huffmanTable.str(), "HFM.txt", outputDir);
// 保存RSA密钥
rsa.saveKeys(outputDir + "\\key.txt");
cout << "编码和加密完成,结果保存至 " << outputDir << " 文件夹中。" << endl;
}
// 从文件解码的公共方法
void decodeFromFile(const string& outputDir)
{
// 打开加密的数据文件
ifstream codeFile(outputDir + "\\encryptedcode.dat");
if (!codeFile) {
cerr << "无法打开文件: encryptedcode.dat" << endl;
return;
}
// 读取加密的数据
vector<long long> encrypted;
long long num;
while (codeFile >> num)
{
encrypted.push_back(num);
}
codeFile.close();
// 使用RSA解密数据
string encodedStr = rsa.decrypt(encrypted);
// 使用哈夫曼编码表解码数据
string decodedStr;
string currentCode;
for (char bit : encodedStr)
{
currentCode += bit;
if (reverseCodes.count(currentCode))
{
decodedStr += reverseCodes[currentCode];
currentCode.clear();
}
}
// 保存解码后的文本
saveFile(decodedStr, "decode.txt", outputDir);
cout << "解密和解码完成,结果保存至 " << outputDir << "\\decode.txt 文件。" << endl;
}
// 显示处理结果的公共方法
void displayResults(const string& outputDir)
{
// 打开所有相关文件
ifstream docFile(outputDir + "\\document.txt"); // 原始文档
ifstream codeFile(outputDir + "\\code.dat"); // 初始数据
ifstream encryptedcodeFile(outputDir + "\\encryptedcode.dat"); // 加密后的数据
ifstream treeFile(outputDir + "\\HFM.txt"); // 哈夫曼编码表
ifstream decodeFile(outputDir + "\\decode.txt"); // 解码后的文本
ifstream keyFile(outputDir + "\\key.txt"); // RSA密钥文件
// 显示原始文本内容
cout << "\n原文内容:\n";
if (docFile)
{
cout << string((istreambuf_iterator<char>(docFile)),
istreambuf_iterator<char>()) << "\n";
}
// 显示哈夫曼编码表
cout << "\n哈夫曼编码 (码本):\n";
if (treeFile)
{
cout << string((istreambuf_iterator<char>(treeFile)),
istreambuf_iterator<char>()) << "\n";
}
// 显示RSA密钥信息
cout << "\nRSA密钥信息:\n";
if (keyFile)
{
cout << string((istreambuf_iterator<char>(keyFile)),
istreambuf_iterator<char>()) << "\n";
}
//显示初始数据内容
cout << "\n初始数据内容:\n";
if (codeFile)
{
cout << string((istreambuf_iterator<char>(codeFile)),
istreambuf_iterator<char>()) << "\n";
}
// 显示加密后的内容
cout << "\n加密后的内容:\n";
if (encryptedcodeFile)
{
cout << string((istreambuf_iterator<char>(encryptedcodeFile)),
istreambuf_iterator<char>()) << "\n";
}
// 显示解码后的文本
cout << "\n解码后的文本:\n";
if (decodeFile)
{
cout << string((istreambuf_iterator<char>(decodeFile)),
istreambuf_iterator<char>()) << "\n";
}
}
// 析构函数,清理内存
~HuffmanCoding()
{
deleteTree(root); // 删除哈夫曼树
}
};
// 主函数
int main()
{
// 创建哈夫曼编码对象
HuffmanCoding huffman;
int choice;
// 主程序循环
while (true)
{
// 显示菜单选项
cout << "\n哈夫曼编码程序\n";
cout << "1. 从文件编码\n";
cout << "2. 解码\n";
cout << "3. 显示结果\n";
cout << "4. 退出\n";
cout << "请选择操作: ";
// 获取用户输入
cin >> choice;
// 清除输入缓冲区,防止输入错误
cin.ignore(numeric_limits<streamsize>::max(), '\n');
// 根据用户选择执行相应操作
switch (choice)
{
case 1:
{
// 从文件编码的选项
string filename, outputDir;
cout << "请输入要编码的文件名: ";
getline(cin, filename);
cout << "请输入输出目录: ";
getline(cin, outputDir);
// 执行编码操作
huffman.encodeFromFile(filename, outputDir);
break;
}
case 2:
{
// 解码的选项
string outputDir;
cout << "请输入存储解码文件的目录: ";
getline(cin, outputDir);
// 执行解码操作
huffman.decodeFromFile(outputDir);
break;
}
case 3:
{
// 显示结果的选项
string outputDir;
cout << "请输入结果文件所在目录: ";
getline(cin, outputDir);
// 显示处理结果
huffman.displayResults(outputDir);
break;
}
case 4:
// 退出程序
return 0;
default:
// 处理无效输入
cout << "无效选择,请重新输入。\n";
}
}
return 0;
}能在这顿代码的基础上改为HASH吗?