目录
结构关系
对三种数据结构的理解
1.AVL(自平衡二叉排序树)
四种旋转场景
AVL树的删除操作的妙处
测试效果
2.Trie树(字典树)
测试效果
3.哈希散列表
图形界面方面
部分代码
数据结构相关
avl.h
avl.cpp
trie.h
trie.cpp
hashtable.h
hashtable.cpp
界面相关
menu.h
menu.cpp
最终效果
遇到的主要问题
1.链接器工具错误 LNK2005
2.C2011 class类型重定义解决办法
3.在两个头文件中, 由于功能需要, 分别引用了彼此的类,导致编译错误
4.Setup出现环境配置缺失的情况
已开源,详情可见我的gitee仓库
开发环境: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目录),
※需要注意保持原有的文件结构,否则安装后也会出现类似下图的配置资源缺失的情况.