目录
3.在两个头文件中, 由于功能需要, 分别引用了彼此的类,导致编译错误
开发环境:Visual Studio 2019, Qt 5
结构关系
对三种数据结构的理解
1.AVL(自平衡二叉排序树)
关键点就是”如何做到自平衡”, 通过对平衡因子(左右子树高度差)的衡量, 在增/删的过程中依旧保持该树的最根本的特性(左小右大)
四种旋转场景
AVL树的删除操作的妙处
AVL树的删除操作要虽比插入复杂一点,不过思想很值得揣摩
抛开细节,如果真的找到了那个要删除的节点,问题就转化为,如何使删除完的树继续平衡呢,利用二叉排序树的特点——左子树比根小,右子树比根大 , 找到左子树中的最大值或者右子树中的最小值来替换他, 因为在局部子树的最值节点,都是在边缘地带,牵扯的鸡毛蒜皮之事都远小于拖家带口的节点
❓那么, 该选左子树中的最大值还是右子树中的最小值呢?随便都可以吗
答案是否定的, 不要因小失大, 小——删除一个节点;大——整棵自平衡二叉查找树的平衡性
我们可以将情况进一步细化
若该节点同时有左右子树. 那样我们就需要比较它左右子树的高度,选择高的那一方,取其最大/最小节点进行替代, 同时向下递归, 在左子树/右子树中删除那个最大/最小节点, 很奇妙, 一次简单的选择具有"柳暗花明又一村"的效果
若该节点至少一棵子树为空, 我们就可以直接用其左子树.右子树的根来替代
另一方面,如果当前没找到那个要删除的节点, 就需要根据与目标的大小相比较,进而选择左/右子树走下去(递归实现), 当回溯回来的时候,可能已经按上面的那种情况实现了删除, 那样就需要判断是否失衡, 此刻的左右子树高度差是否为二(即失衡), 接下来的问题就回归到树的旋转上去了.
测试效果
2.Trie树(字典树)
对于字典树,我自己的理解是,利用字符串之间的公共前缀,将重复的前缀合并在一起, 因此Trie对要处理的字符串的要求相较其他数据结构更为严格
单就比较性能的话, Trie树非常快,时间仅仅由单词长度决定, 且具有前缀模糊查找的优势(本次利用字典树已经实现了一定程度的”人性化”推荐功能)
测试效果
3.哈希散列表
图形界面方面
基本都是靠啃官方文档,在UI设计上使用了vs上的qt拓展
设计出ui,因为.ui文件结合c++进行编程比较麻烦(不是很懂), 而这个拓展点击窗体可以直接查看生成的代码
因此,可以直接运用这些代码作为头文件,对其中的类进行继承或者组合,这对于刚刚入门QT的我来说简直就是福音
部分代码
数据结构相关
avl.h
#pragma once
#include <iostream>
#include <string>
#include <fstream>
using namespace std;
typedef pair<string, string> PII;
template<typename T>
struct AVLNode {
T data;
int height;
AVLNode* lchild, * rchild;
AVLNode(T dt, AVLNode* l, AVLNode* r) :data(dt), height(0), lchild(l), rchild(r) {}
};
class AVLTree {
public:
AVLTree() {
root = nullptr;
LoadWord("./word_source/AllTestWord.txt");
}
// ~AVLTree(){
// Destory(root);
// }
void Insert(PII data) {
root = _insert(root, data);
}
void LoadWord(string wordSite); //加载词库
string Search(string word);
int GetH(AVLNode<PII>* pRoot) {
if (pRoot != nullptr) return pRoot->height;
else return 0;
}
void print() {
printTree(root);
}
void DeleteWord(string word);
AVLNode<PII>* MaxNode(AVLNode<PII>* pRoot);
AVLNode<PII>* MinNode(AVLNode<PII>* pRoot);
AVLNode<PII>* Left_Rotation(AVLNode<PII>* pRoot); // 左旋
AVLNode<PII>* Right_Rotation(AVLNode<PII>* pRoot); // 右旋
AVLNode<PII>* LR_Rotation(AVLNode<PII>* pRoot); // 先左旋后右旋
AVLNode<PII>* RL_Rotation(AVLNode<PII>* pRoot); // 先右旋后左旋
private:
AVLNode<PII>* root;
AVLNode<PII>* _insert(AVLNode<PII>* pRoot, PII data);
AVLNode<PII>* _deleteWord(AVLNode<PII>* pRoot, string word);
string _searchByEn(AVLNode<PII>* pRoot, string word); //非递归搜索
void printTree(AVLNode<PII>* pRoot);
};
avl.cpp
#include "avl.h"
void AVLTree::LoadWord(string wordSite)
{
string s;
ifstream in(wordSite);
int cnt = 1;
string word, answer;
while (getline(in, s))
{
if (cnt & 1) {
word = s;
}
else {
answer = s;
Insert({ word, answer });
}
cnt++;
}
in.close();
}
void AVLTree::printTree(AVLNode<PII>* pRoot)
{
if (pRoot != nullptr)
{
cout << pRoot->data.first << " " << pRoot->data.second << endl;
printTree(pRoot->lchild);
printTree(pRoot->rchild);
}
}
AVLNode<PII>* AVLTree::MaxNode(AVLNode<PII>* pRoot)
{
if (pRoot == nullptr) return nullptr;
while (pRoot->rchild != nullptr)
{
pRoot = pRoot->rchild;
}
return pRoot;
}
AVLNode<PII>* AVLTree::MinNode(AVLNode<PII>* pRoot)
{
if (pRoot == nullptr) return nullptr;
while (pRoot->lchild != nullptr)
{
pRoot = pRoot->lchild;
}
return pRoot;
}
AVLNode<PII>* AVLTree::Left_Rotation(AVLNode<PII>* pRoot)
{
AVLNode<PII>* pr = pRoot->rchild;
pRoot->rchild = pr->lchild;
pr->lchild = pRoot;
pRoot->height = max(GetH(pRoot->lchild), GetH(pRoot->rchild)) + 1;
pr->height = max(GetH(pr->lchild), GetH(pr->rchild)) + 1;
return pr;
}
AVLNode<PII>* AVLTree::Right_Rotation(AVLNode<PII>* pRoot)
{
AVLNode<PII>* pl = pRoot->lchild;
pRoot->lchild = pl->rchild;
pl->rchild = pRoot;
pRoot->height = max(GetH(pRoot->lchild), GetH(pRoot->rchild)) + 1;
pl->height = max(GetH(pl->lchild), GetH(pl->rchild)) + 1;
return pl;
}
AVLNode<PII>* AVLTree::LR_Rotation(AVLNode<PII>* pRoot)
{
pRoot->lchild = Left_Rotation(pRoot->lchild);
return Right_Rotation(pRoot);
}
AVLNode<PII>* AVLTree::RL_Rotation(AVLNode<PII>* pRoot)
{
pRoot->rchild = Right_Rotation(pRoot->rchild);
return Left_Rotation(pRoot);
}
// 递归插入
AVLNode<PII>* AVLTree::_insert(AVLNode<PII>* pRoot, PII data)
{
if (pRoot == nullptr) {
pRoot = new AVLNode<PII>(data, nullptr, nullptr);
//cout << data.second << "已加入" <<endl;
}
else if (pRoot->data > data) {
pRoot->lchild = _insert(pRoot->lchild, data);
// 插入数据后失衡
if (GetH(pRoot->lchild) - GetH(pRoot->rchild) == 2)
{
if (pRoot->lchild->data > data)
pRoot = Right_Rotation(pRoot);
else
pRoot = LR_Rotation(pRoot);
}
}
else if (pRoot->data < data) {
pRoot->rchild = _insert(pRoot->rchild, data);
if (GetH(pRoot->rchild) - GetH(pRoot->lchild) == 2)
{
if (data > pRoot->rchild->data)
pRoot = Left_Rotation(pRoot);
else
pRoot = RL_Rotation(pRoot);
}
}
pRoot->height = max(GetH(pRoot->lchild), GetH(pRoot->rchild)) + 1;
return pRoot;
}
string AVLTree::Search(string word) {
return _searchByEn(root, word);
}
string AVLTree::_searchByEn(AVLNode<PII>* pRoot, string word)
{
AVLNode<PII>* p = pRoot;
while (p != nullptr) {
if (p->data.first == word) {
//cout << p->data.second << endl;
return p->data.second;
}
else if (p->data.first > word)
p = p->lchild;
else
p = p->rchild;
}
return " ";
}
void AVLTree::DeleteWord(string word)
{
root = _deleteWord(root, word);
}
AVLNode<PII>* AVLTree::_deleteWord(AVLNode<PII>* pRoot, string word)
{
if (pRoot == nullptr) {
//cout << "不存在" << word << endl;
return nullptr;
}
//找到对应值
if (pRoot->data.first == word)
{
// 如果同时存在左右子树,则根据高度选择更换左子树最大节点或右子树最小节点
if (pRoot->lchild != nullptr && pRoot->rchild != nullptr)
{
if (GetH(pRoot->lchild) > GetH(pRoot->rchild))
{
AVLNode<PII>* left_max = MaxNode(pRoot->lchild); // 用左子树的最大节点替代当前节点
pRoot->data = left_max->data; // 当前分支情况left_max不可能为nullptr,可以直接覆盖data
pRoot->lchild = _deleteWord(pRoot->lchild, left_max->data.first); // 转移矛盾为删除左子树的最大节点
}
else {
AVLNode<PII>* right_min = MinNode(pRoot->rchild); // 用右子树的最小节点替代当前节点
pRoot->data = right_min->data; // 当前分支情况right_min不可能为nullptr,可以直接覆盖data
pRoot->lchild = _deleteWord(pRoot->lchild, right_min->data.first); // 转移矛盾为删除左子树的最大节点
}
}
// 至少一个子树为空
else {
AVLNode<PII>* p = pRoot;
if (pRoot->lchild != nullptr)
pRoot = pRoot->lchild;
else if (pRoot->rchild != nullptr)
pRoot = pRoot->rchild;
delete p;
//cout << "成功删除" << word << endl;
return nullptr;
}
}
else if (pRoot->data.first > word)
{
pRoot->lchild = _deleteWord(pRoot->lchild, word);
// 若是处理左子树完后失衡,则对右子树进行旋转变换
if (GetH(pRoot->rchild) - GetH(pRoot->lchild) == 2)
{
if (GetH(pRoot->rchild->lchild) > GetH(pRoot->rchild->rchild))
pRoot = RL_Rotation(pRoot);
else
pRoot = Left_Rotation(pRoot);
}
}
else if (pRoot->data.first < word)
{
pRoot->rchild = _deleteWord(pRoot->rchild, word);
// 若是处理右子树完后失衡,则对左子树进行旋转变换
if (GetH(pRoot->lchild) - GetH(pRoot->rchild) == 2)
{
if (GetH(pRoot->lchild->rchild) > GetH(pRoot->lchild->lchild))
pRoot = LR_Rotation(pRoot);
else
pRoot = Right_Rotation(pRoot);
}
}
return pRoot;
}
trie.h
#pragma once
#include <iostream>
#include <string>
#include <fstream>
using namespace std;
#define Size 26
template<typename T>
struct TrieNode {
T data; //译文
bool isEnd; //是否为词尾
TrieNode* child[Size];
TrieNode() :isEnd(false) {
for (int i = 0; i < Size; i++) {
child[i] = nullptr;
}
}
};
class TrieTree
{
public:
TrieTree() {
root = new TrieNode<string>();
_num = 0;
LoadWord("./word_source/AllTestWord.txt");
}
int GetNum() {
return _num;
}
void Insert(string key, string value);
void LoadWord(string wordSite); //加载词库
string Search(string key);
void Delete(string key);
//void FuzzySearch();
private:
int _num;
TrieNode<string>* root;
TrieNode<string>* _search(string key);
};
trie.cpp
#include "trie.h"
void TrieTree::LoadWord(string wordSite)
{
string s;
ifstream in(wordSite);
int cnt = 1;
string word, answer;
while (getline(in, s))
{
if (cnt & 1) {
word = s;
}
else {
answer = s;
Insert(word, answer);
}
cnt++;
}
in.close();
}
void TrieTree::Insert(string key, string value)
{
TrieNode<string>* p = root;
int size = key.length();
for (int i = 0; i < size; i++)
{
int index = key[i] - 'a';
if (p->child[index] == nullptr) {
p->child[index] = new TrieNode<string>();
}
p = p->child[index];
}
p->isEnd = true; //标记为词尾
p->data = value;
_num++;
//cout << "success insert \"" << value << "\"" << endl;
}
string TrieTree::Search(string key) {
TrieNode<string>* t = _search(key);
if (t != nullptr) {
return t->data;
}
else {
return " ";
}
}
TrieNode<string>* TrieTree::_search(string key)
{
TrieNode<string>* p = root;
int size = key.length();
TrieNode<string>* parent = nullptr;
TrieNode<string>* tempNode = new TrieNode<string>();
for (int i = 0; i < size; i++)
{
int index = key[i] - 'a';
if ( p->child[index] == nullptr ) //找不到该字母
{
if(i != size-1)
tempNode->data = " ";
else {
//模糊搜索低级版
int cntLike = 0; //相似的词数
for (int i = 0; i < Size; i++) {
if (p->child[i] != nullptr && p->child[i]->isEnd) {
cntLike++;
key[size - 1] = (char)(i + 'a');
tempNode->data += key + p->child[i]->data + '\n';
//cout << key << " " << p->child[i]->data << endl;
}
}
if (cntLike) {
tempNode->data = "NOT FOUND.\n[Tips:]\n" + tempNode->data;
}
else {
tempNode->data = " ";
}
}
return tempNode;
}
parent = p;
p = p->child[index];
}
// 可能不存在该单词,只是一段前缀
if (p->isEnd) {
//cout << p->data << endl;
return p;
}
else {
tempNode->data = " ";
return tempNode;
}
}
void TrieTree::Delete(string key)
{
TrieNode<string>* keySite = _search(key);
if (keySite->data != " ") {
keySite->isEnd = false;
_num--;
}
}
hashtable.h
#pragma once
#include <iostream>
#include <string>
#include <fstream>
using namespace std;
typedef pair<string, string> PII;
template<typename T>
struct HashNode {
T data;
HashNode* next;
HashNode(T dt) :data(dt), next(nullptr) {}
};
class HashTable {
public:
HashTable() {
for (int i = 0; i < MOD; i++)
{
head[i] = new HashNode<PII>(make_pair("null", "null"));
}
LoadWord("./word_source/AllTestWord.txt");
}
void LoadWord(string wordSite); //加载词库
bool Insert(PII data);
int HashCode(string key);//哈希函数,将字符串转化为一个X进制的数
string Search(string key);
bool Delete(string key);
private:
const static int MOD = 1021, X = 257; //均取质数
HashNode<PII>* head[MOD];
HashNode<PII>* _search(string key);
};
hashtable.cpp
#include "hashtable.h"
void HashTable::LoadWord(string wordSite)
{
string s;
ifstream in(wordSite);
int cnt = 1;
string word, answer;
while (getline(in, s))
{
if (cnt & 1) {
word = s;
}
else {
answer = s;
Insert({ word, answer });
}
cnt++;
}
in.close();
}
//哈希函数,将字符串转化为一个X进制的数
int HashTable::HashCode(string key) {
int hash = 0;
for (int i = 0; i < key.size(); i++)
hash = ((hash * X % MOD + (int)key[i]) % MOD + MOD) % MOD;
//qDebug() << QString::number(hash);
return hash;
}
string HashTable::Search(string key)
{
HashNode<PII>* res = _search(key);
if (res == nullptr) return " ";
else return res->data.second;
}
HashNode<PII>* HashTable::_search(string key)
{
int hashCode = HashCode(key);
HashNode<PII>* p = head[hashCode]->next;
while (p)
{
if (p->data.first == key) return p;
p = p->next;
}
return nullptr;
}
bool HashTable::Insert(PII data)
{
int hashCode = HashCode(data.first);
HashNode<PII>* res = _search(data.first);
if (_search(data.first) != nullptr) {//已存在
return false;
}
HashNode<PII>* newNode = new HashNode<PII>(data);
newNode->next = head[hashCode]->next;
head[hashCode]->next = newNode;
return true;
}
bool HashTable::Delete(string key)
{
int hashCode = HashCode(key);
HashNode<PII>* res = _search(key);
if (res == nullptr) {//不存在
return false;
}
HashNode<PII>* pre = head[hashCode], * p = head[hashCode]->next;
while (p)
{
if (p->data.first != key)
{
pre = p;
p = p->next;
}
else {
pre->next = p->next;
delete p;
p = nullptr;
break;
}
}
}
界面相关
menu.h
#pragma once
#include "ui_selectMenu.h"
#include "dictionary01.h"
#include "addWidget.h"
#include "deleteWidget.h"
#include "aboutWidget.h"
#include "avl.h"
#include "trie.h"
#include "hashtable.h"
#include <QMessageBox>
class Menu :public QWidget, public Ui::SelectMenu
{
Q_OBJECT
public:
Menu(void);
dictionary01 dict;
AddWidget add_widget;
DeleteWidget delete_widget;
AboutWidget about_widget;
public slots:
void Translate(void);
void AddWord(void);
void DeleteWord(void);
void turnToAdd(void);
void turnToDelete(void);
void turnToAVL(void);
void turnToTRIE(void);
void turnToHASH(void);
void turnToAbout(void);
private:
enum DATATYPE
{
AVL, TRIE, HASH
};
AVLTree AVL_Tree; //avl树
TrieTree TRIE_Tree; //trie树
HashTable HASH_Table; //哈希散列表
DATATYPE current_datatype; //当前选择的数据类型
};
menu.cpp
#include "menu.h"
Menu::Menu(void)
{
setupUi(this);
connect(AVLBtn, SIGNAL(clicked(void)),
this, SLOT(turnToAVL(void)));
connect(TRIEBtn, SIGNAL(clicked(void)),
this, SLOT(turnToTRIE(void)));
connect(HASHBtn, SIGNAL(clicked(void)),
this, SLOT(turnToHASH(void)));
connect(aboutBtn, SIGNAL(clicked(void)),
this, SLOT(turnToAbout(void)));
//进行翻译功能的信号连接
connect(dict.transBtn, SIGNAL(clicked(void)),
this, SLOT(Translate(void)));
//子窗口的添加与同样是子窗口的"添加界面"连接
connect(dict.addBtn, SIGNAL(clicked(void)),
this, SLOT(turnToAdd(void)));
//进行添加功能的信号连接
connect(add_widget.addBtn, SIGNAL(clicked(void)),
this, SLOT(AddWord(void)));
//子窗口的添加与同样是子窗口的"添加界面"连接
connect(dict.deleteBtn, SIGNAL(clicked(void)),
this, SLOT(turnToDelete(void)));
//进行删除功能的信号连接
connect(delete_widget.deleteBtn, SIGNAL(clicked(void)),
this, SLOT(DeleteWord(void)));
}
void Menu::Translate(void)
{
string key = dict.wordArea->text().toStdString();
string value;
if (current_datatype == AVL)
value = AVL_Tree.Search(key);
else if (current_datatype == TRIE)
value = TRIE_Tree.Search(key);
else
value = HASH_Table.Search(key);
if (value != " ") {
QString q_value = QString::fromStdString(value);
dict.translation->setText(q_value);
}
else {
dict.translation->setText("NOT FOUND.");
}
}
void Menu::turnToAdd(void)
{
add_widget.setVisible(true);
add_widget.show();
}
void Menu::AddWord(void)
{
string key = add_widget.wordArea->text().toStdString();
string value = "=>" + add_widget.ChineseArea->toPlainText().toStdString();
string tmp;
bool ifExist = true;
if (current_datatype == AVL) {
tmp = AVL_Tree.Search(key);
if (tmp == " ") {
ifExist = false;
AVL_Tree.Insert({ key, value });
}
}
else if (current_datatype == TRIE) {
tmp = TRIE_Tree.Search(key);
//单词不存在且无相似词,或不存在但有相似的词推荐
if (tmp == " " || tmp[0] == 'N') {
ifExist = false;
TRIE_Tree.Insert(key, value);
}
}
else {
if (HASH_Table.Insert({ key, value }))
ifExist = false;
else
ifExist = true;
}
if (ifExist) {
QMessageBox::about(NULL, "Tip", "The word has been existed.");
}
else {
QMessageBox::about(NULL, "Finish", "Success insert!");
}
}
void Menu::turnToDelete(void)
{
delete_widget.setVisible(true);
delete_widget.show();
}
void Menu::DeleteWord(void)
{
delete_widget.tipArea->setText("");
string key = delete_widget.wordArea->text().toStdString();
string tmp;
if (current_datatype == AVL) {
tmp = AVL_Tree.Search(key);
if (tmp != " ") {
AVL_Tree.DeleteWord(key);
QMessageBox::about(NULL, "Finish", "Success delete.");
}
else {
QMessageBox::about(NULL, "Tip", "NOT FOUND.");
}
}
else if (current_datatype == TRIE) {
tmp = TRIE_Tree.Search(key);
if (tmp[0] == 'N') {
QString tipQStr = QString::fromStdString(tmp);
delete_widget.tipArea->setText(tipQStr);
}
else if(tmp != " "){
TRIE_Tree.Delete(key);
QMessageBox::about(NULL, "Finish", "Success delete.");
}
else {
QMessageBox::about(NULL, "Tip", "NOT FOUND.");
}
}
else {
if(HASH_Table.Delete(key))
QMessageBox::about(NULL, "Finish", "Success delete.");
else
QMessageBox::about(NULL, "Tip", "NOT FOUND.");
}
}
void Menu::turnToAVL(void)
{
current_datatype = AVL; //指定当前类型
dict.setVisible(true);
dict.setWindowTitle("AVL");
this->hide();
connect(&dict, SIGNAL(ExitWin()), this, SLOT(show())); //利用子窗口自定义的信号,使父子窗口不共存
}
void Menu::turnToTRIE(void)
{
current_datatype = TRIE; //指定当前类型
dict.setVisible(true);
dict.setWindowTitle("TRIE");
this->hide();
connect(&dict, SIGNAL(ExitWin()), this, SLOT(show())); //利用子窗口自定义的信号,使父子窗口不共存
}
void Menu::turnToHASH(void)
{
current_datatype = HASH; //指定当前类型
dict.setVisible(true);
dict.setWindowTitle("HASH");
this->hide();
connect(&dict, SIGNAL(ExitWin()), this, SLOT(show())); //利用子窗口自定义的信号,使父子窗口不共存
}
void Menu::turnToAbout(void)
{
about_widget.setVisible(true);
}
最终效果
可安装(Windows64位环境下应该不会有什么问题,所有依赖的.dll相关文件均已打包在下面这个文件中了)
安装后,安装目录下有相关配置文件和程序卸载均有
桌面和任务栏均有快捷方式
主界面
Trie支持一定程度的前缀推荐
三类结构均可进行正常添加,删除,翻译的功能,且一旦有非法字符(非字母),相关按键就会锁死
若是新增单词已存在,则无效操作
当然,新增时若是释义为空或者单词有非法字符也是无法进行添加操作的
删除不存在的单词为无效操作
"关于作者"界面(可以和我交流哦hhh)
遇到的主要问题
1.链接器工具错误 LNK2005
解决方案:>>>LNK2005
原因:在.h文件中对在类外对类中的函数进行了定义.
2.C2011 class类型重定义解决办法
原因:在c++项目中同一头文件被多个文件调用, 导致其中的类被多次编译.
解决方案:在该头文件中加入#pragma once //告诉编译器只编译一次.
3.在两个头文件中, 由于功能需要, 分别引用了彼此的类,导致编译错误
原因:程序结构设计不合理, 造成高耦合性.
解决方案:改变了整个程序的结构,使最高层的类(menu)对其下辖的对象(界面/数据结构)直接操控,避免下层的类出现交织的情况.
4.Setup出现环境配置缺失的情况
为安装包配置.dll环境依赖(来自vs项目下的release目录),
※需要注意保持原有的文件结构,否则安装后也会出现类似下图的配置资源缺失的情况.