猜动物游戏——机器学习和二叉树的应用

猜动物游戏的规则是玩家想一个动物,电脑问玩家一些问题,猜玩家想的动物,如果没猜对,就将玩家想的动物添加到数据库里。
先来看看二叉树的定义:

二叉树(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其中一个的格式,另一个也必须修改为相同的格式。一句话,怎么存储就怎么读取。我设计的数据库格式如下:

  1. 第一组数据总是根节点的数据。
  2. 每组数据的第一个数字是该节点字符串的长度。
  3. 第一个数据之后是节点字符串的内容,长度是第一个数字。
  4. 字符串之后是一个bool值,代表左孩子是否存在。如果值为true,则按相同的格式存储左孩子的内容。
  5. 后面又是一个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();
	}
}

以上就是动物游戏的全部代码。再贴上几张截图:
程序截图
修改知识库:
程序截图
知识库里添加了老虎:
程序截图
保存数据库:
保存数据库
读取保存的数据库后,程序询问上次运行添加的问题:
使用数据库
数据库文件内容:
数据库文件内容

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值