猜动物游戏的规则是玩家想一个动物,电脑问玩家一些问题,猜玩家想的动物,如果没猜对,就将玩家想的动物添加到数据库里。
先来看看二叉树的定义:
二叉树(binary tree)是指树中节点的度不大于2的有序树,它是一种最简单且最重要的树。二叉树的递归定义为:二叉树是一棵空树,或者是一棵由一个根节点和两棵互不相交的,分别称作根的左子树和右子树组成的非空树;左子树和右子树又同样都是二叉树。
这个游戏里的数据库就是一个二叉树,节点对应问题或猜测,左孩子代表回答正确后的情况,右孩子代表回答错误后的情况。
一般来说,二叉树应该有获取树的高、树的度、中序遍历、先序遍历、后序遍历、层次遍历等函数,但动物游戏不需要这些功能,所以我简化了代码,只写了和动物游戏有关的基本功能。代码如下:
//
#include <iostream>
#include <fstream>
#include <Windows.h>
using namespace std;
template<class DataType>
class Node
{
public:
DataType data;
Node* parent;
Node* LeftChild;
Node* RightChild;
Node()
{
parent = nullptr;
LeftChild = nullptr;
RightChild = nullptr;
}
bool IsLeafNode()const//是否为叶节点
{
return (LeftChild == nullptr && RightChild == nullptr);
}
};
template<class type>
class BinaryTree
{
private:
void ReleaseMemory(Node<type>* pNode)
{
if (pNode->LeftChild != nullptr)
{
ReleaseMemory(pNode->LeftChild);
pNode->LeftChild = nullptr;
}
if (pNode->RightChild != nullptr)
{
ReleaseMemory(pNode->RightChild);
pNode->RightChild = nullptr;
}
if (pNode == root);//根
else if (pNode->parent->LeftChild == pNode)
pNode->parent->LeftChild = nullptr;
else
pNode->parent->RightChild = nullptr;
delete pNode;
return;
}
public:
Node<type>* root;
BinaryTree()
{
root = new Node<type>;
}
~BinaryTree()
{
ReleaseMemory(root);
}
Node<type>* InsertLeftChild(Node<type>* pParent, type Data)
{
if (pParent->LeftChild != nullptr)
pParent->LeftChild->data = Data;
else
{
Node<type>* tmp = new Node<type>;
tmp->data = Data;
pParent->LeftChild = tmp;
tmp->parent = pParent;
}
return pParent->LeftChild;
}
Node<type>* InsertRightChild(Node<type>* pParent, type Data)
{
if (pParent->RightChild != nullptr)
pParent->RightChild->data = Data;
else
{
Node<type>* tmp = new Node<type>;
tmp->data = Data;
pParent->RightChild = tmp;
tmp->parent = pParent;
}
return pParent->RightChild;
}
};
class GameOfAnimal
{
private:
BinaryTree<string> SQL;
void SaveSQL(ofstream& OutFile, Node<string>* pNode);
void ReadSQL(ifstream& InFile, Node<string>* pNode);
bool GetAnswer(string message, bool PutExit = true);
public:
GameOfAnimal();//初始化数据库
void Run()
{
Node<string>* tmp = SQL.root;
cout << "想一个动物,我将尽力猜它..."<<endl;
while (!tmp->IsLeafNode())
{
if (GetAnswer(tmp->data))
tmp = tmp->LeftChild;
else
tmp = tmp->RightChild;
}
string message = "你想的动物是";
message += tmp->data;
message += "吗?";
if (GetAnswer(message))
cout << "哈哈,我赢了!"<<endl;
else
{
string name, question,t;
cout << "好吧,算我输。你想的是什么动物呢?";
cin >> name;
cout << "请输入一个只有是和否两种回答的问题,回答“是”则动物是"
<< name << ",回答“否”则动物是" << tmp->data << ":" << endl;
cin >> question;
t = tmp->data;
tmp->data = question;
SQL.InsertLeftChild(tmp,name);
SQL.InsertRightChild(tmp, t);
cout << "哼,下一次你猜这个动物我就会了!" << endl;
}
system("pause");
system("cls");
}
};
int main()
{
DeleteMenu(GetSystemMenu(GetConsoleWindow(), FALSE), SC_CLOSE, MF_BYCOMMAND);
DrawMenuBar(GetConsoleWindow());
GameOfAnimal game;
while (1)
{
game.Run();
}
return 0;
}
void GameOfAnimal::SaveSQL(ofstream& OutFile, Node<string>* pNode)
{
OutFile << (pNode->data.size() + 1);
OutFile.write(pNode->data.data(), pNode->data.size() + 1);
OutFile << (pNode->LeftChild != nullptr) << '\0';
if (pNode->LeftChild != nullptr)
SaveSQL(OutFile, pNode->LeftChild);
OutFile << (pNode->RightChild != nullptr) << '\0';
if (pNode->RightChild != nullptr)
SaveSQL(OutFile, pNode->RightChild);
}
void GameOfAnimal::ReadSQL(ifstream& InFile, Node<string>* pNode)
{
//有bug!
int num;
InFile >> num;
char* tmp = new char[num];
InFile.read(tmp, num);
pNode->data = tmp;
delete[]tmp;
bool b;
InFile >> b;
InFile.seekg(InFile.tellg().operator+(1));
if (b)
{
SQL.InsertLeftChild(pNode, "");
ReadSQL(InFile, pNode->LeftChild);
}
InFile >> b;
InFile.seekg(InFile.tellg().operator+(1));
if (b)
{
SQL.InsertRightChild(pNode, "");
ReadSQL(InFile, pNode->RightChild);
}
}
bool GameOfAnimal::GetAnswer(string message, bool PutExit)
{
cout << message << (PutExit ? "(y/n,退出输入exit):" : "(y/n):");
string choose;
cin >> choose;
while (choose != "y" && choose != "n" && choose != "exit")
{
cout << "输入错误,请重新输入!" << endl;
cout << message << (PutExit ? "(y/n,退出输入exit):" : "(y/n):");
cin >> choose;
}
if (choose == "y")
return true;
else if (choose == "exit" && PutExit)
{
if (GetAnswer("是否保存数据库?", false))
{
string name;
cout << "输入数据库名(不带扩展名):";
cin >> name;
name += ".dat";
ofstream of(name, ios::binary | ios::out);
SaveSQL(of, SQL.root);
of.close();
}
SQL.~BinaryTree();
exit(0);
}
return false;
}
GameOfAnimal::GameOfAnimal()
{
if (GetAnswer("是否使用保存的数据库?", false))
{
string name;
cout << "输入数据库名(不带扩展名):";
cin >> name;
name += ".dat";
ifstream infile(name, ios::binary | ios::in);
if (!infile)
{
cout << "找不到数据库!" << endl;
infile.close();
}
else
{
ReadSQL(infile, SQL.root);
infile.close();
return;
}
}
SQL.root->data = "是陆生动物吗?";
Node<string>* tmp = SQL.InsertLeftChild(SQL.root, "是食肉动物吗?");
SQL.InsertLeftChild(tmp, "狼");
SQL.InsertRightChild(tmp, "绵羊");
SQL.InsertRightChild(SQL.root, "鲤鱼");
}
让我们从头开始分析:先看Node类,它表示二叉树的一个节点,成员分别是节点数据、双亲节点指针、两个孩子节点指针。默认的构造函数对指针赋空值,还有一个IsLeafNode函数 ,判断是否为叶节点(没有孩子的节点)。
再看BinaryTree类,它里面只有一个数据成员,即树根的指针root。这个类里面的节点都使用堆内存。构造函数给root分配动态内存,析构函数释放所有的内存。释放内存用的函数是ReleaseMemory,函数定义如下:
void ReleaseMemory(Node<type>* pNode)
{
if (pNode->LeftChild != nullptr)
{
ReleaseMemory(pNode->LeftChild);
pNode->LeftChild = nullptr;
}
if (pNode->RightChild != nullptr)
{
ReleaseMemory(pNode->RightChild);
pNode->RightChild = nullptr;
}
if (pNode == root);//根
else if (pNode->parent->LeftChild == pNode)
pNode->parent->LeftChild = nullptr;
else
pNode->parent->RightChild = nullptr;
delete pNode;
return;
}
此函数用到了递归调用,先释放左孩子(如果有),再释放右孩子(如果有),最后释放自己。这样,在析构函数里只需要ReleaseMemory(root);
就行了。
二叉树类还有两个函数,分别是InsertLeftChild和InsertRightChild,用来插入孩子节点,如果已经存在则修改原孩子节点的数据,返回值是插入的节点的指针。这里以插入左孩子的函数为例:
Node<type>* InsertLeftChild(Node<type>* pParent, type Data)
{
if (pParent->LeftChild != nullptr)
pParent->LeftChild->data = Data;
else
{
Node<type>* tmp = new Node<type>;
tmp->data = Data;
pParent->LeftChild = tmp;
tmp->parent = pParent;
}
return pParent->LeftChild;
}
接着,我们分析GameOfAnimal类。它有一个BinaryTree类型的数据成员,用来存储数据库,还有3个辅助函数SaveSQL,ReadSQL和GetAnswer.
为了保存机器的知识库,我添加了保存、打开数据库功能。这些功能都和文件格式有关。文件格式的概念我就不过多解释了,具体可以参见我的另一篇博客双人五子棋。一定要注意:如果修改了SaveSQL和ReadSQL其中一个的格式,另一个也必须修改为相同的格式。一句话,怎么存储就怎么读取。我设计的数据库格式如下:
- 第一组数据总是根节点的数据。
- 每组数据的第一个数字是该节点字符串的长度。
- 第一个数据之后是节点字符串的内容,长度是第一个数字。
- 字符串之后是一个bool值,代表左孩子是否存在。如果值为true,则按相同的格式存储左孩子的内容。
- 后面又是一个bool值,代表右孩子是否存在。如果值为true,则按相同的格式存储右孩子的内容。
SaveSQL函数代码如下:
void GameOfAnimal::SaveSQL(ofstream& OutFile, Node<string>* pNode)
{
OutFile << (pNode->data.size() + 1);
OutFile.write(pNode->data.data(), pNode->data.size() + 1);
OutFile << (pNode->LeftChild != nullptr) << '\0';
if (pNode->LeftChild != nullptr)
SaveSQL(OutFile, pNode->LeftChild);
OutFile << (pNode->RightChild != nullptr) << '\0';
if (pNode->RightChild != nullptr)
SaveSQL(OutFile, pNode->RightChild);
}
这个函数和ReleaseMemory函数相似,也是递归调用。而且,它和我设计的文件格式完全相同。
ReadSQL和SaveSQL正好相反,但读取格式和文件格式也一样,也是递归调用。代码如下:
void GameOfAnimal::ReadSQL(ifstream& InFile, Node<string>* pNode)
{
int num;
InFile >> num;
char* tmp = new char[num];
InFile.read(tmp, num);
pNode->data = tmp;
delete[]tmp;
bool b;
InFile >> b;
InFile.seekg(InFile.tellg().operator+(1));//文件指针后移一格
if (b)
{
SQL.InsertLeftChild(pNode, "");
ReadSQL(InFile, pNode->LeftChild);
}
InFile >> b;
InFile.seekg(InFile.tellg().operator+(1));
if (b)
{
SQL.InsertRightChild(pNode, "");
ReadSQL(InFile, pNode->RightChild);
}
}
GetAnswer就简单了,只是获取一个是、否或退出的回答。代码如下:
bool GameOfAnimal::GetAnswer(string message)
{
cout << message << "(y/n):";
string choose;
cin >> choose;
while (choose != "y" && choose != "n" && choose != "exit")
{
cout << "输入错误,请重新输入!" << endl;
cout << message << "(y/n):";
cin >> choose;
}
if (choose == "y")
return true;
return false;
}
在构造函数里,我们要初始化数据库。首先要询问是否使用保存的数据库,如果使用则调用ReadSQL函数,不使用则使用默认的数据库(只有3种动物)。
GameOfAnimal::GameOfAnimal()
{
if (GetAnswer("是否使用保存的数据库?"))
{
string name;
cout << "输入数据库名(不带扩展名):";
cin >> name;
name += ".dat";
ifstream infile(name, ios::binary | ios::in);
if (!infile)
{
cout << "找不到数据库!" << endl;
infile.close();
}
else
{
ReadSQL(infile, SQL.root);
infile.close();
return;
}
}
SQL.root->data = "是陆生动物吗?";
Node<string>* tmp = SQL.InsertLeftChild(SQL.root, "是食肉动物吗?");
SQL.InsertLeftChild(tmp, "狼");
SQL.InsertRightChild(tmp, "绵羊");
SQL.InsertRightChild(SQL.root, "鲤鱼");
}
下一个Run函数是最主要的函数,也是机器学习部分算法的核心。首先,从根节点开始输出问题,得到肯定回答就进入左孩子节点,否则进入右孩子节点。如果到了叶节点,也就是答案,则询问用户是不是数据库里的动物。如果是,游戏结束;如果不是,情况就复杂了,需要在原节点出插入问题,再创建两个子节点。当用户选择退出时,计算机再询问是否保存数据库。如果保存则调用SaveSQL函数。关于机器学习部分的算法,我举了个例子。比如,二叉树原来是这样的:
计算机提出了问题1,用户回答为n,计算机猜是动物2,但用户想的不是动物2,而是动物3,这时候,用户需要输入动物3的名称,并输入问题2。对于问题2,回答为y则为动物3,回答为n为动物2(反过来也行)。现在的二叉树变成了这样的:
该函数代码如下:
void GameOfAnimal::Run()
{
do
{
Node<string>* tmp = SQL.root;
cout << "想一个动物,我将尽力猜它..." << endl;
while (!tmp->IsLeafNode())
{
if (GetAnswer(tmp->data))
tmp = tmp->LeftChild;
else
tmp = tmp->RightChild;
}
string message = "你想的动物是";
message += tmp->data;
message += "吗?";
if (GetAnswer(message))
cout << "哈哈,我赢了!" << endl;
else
{
string name, question, t;
cout << "好吧,算我输。你想的是什么动物呢?";
cin >> name;
cout << "请输入一个只有是和否两种回答的问题,回答“是”则动物是"
<< name << ",回答“否”则动物是" << tmp->data << ":" << endl;
cin >> question;
t = tmp->data;
tmp->data = question;
SQL.InsertLeftChild(tmp, name);
SQL.InsertRightChild(tmp, t);
cout << "哼,下一次你猜这个动物我就会了!" << endl;
}
system("pause");
system("cls");
} while (GetAnswer("是否继续?"));
if (GetAnswer("是否保存数据库?"))
{
string name;
cout << "输入数据库名(不带扩展名):";
cin >> name;
name += ".dat";
ofstream of(name, ios::binary | ios::out);
SaveSQL(of, SQL.root);
of.close();
}
}
以上就是动物游戏的全部代码。再贴上几张截图:
修改知识库:
知识库里添加了老虎:
保存数据库:
读取保存的数据库后,程序询问上次运行添加的问题:
数据库文件内容: