关于C++数据结构中的二叉树的学习笔记

这两天回顾了二叉树的上和下的算法专栏,先用这篇博客来记录我的学习笔记。


本.txt,我将再一次学习二叉树这种数据结构!


前面我们都只是讲了线性表结构而已,比如:栈和队列等数据结构。那么今天我们将来讲解一种非线性表结构---树!
树这种数据结构比线性表的数据结构要复杂得多!内容也比较多,因此专栏的王老师分为了4节课去讲解!

二叉树的存储方式主要有2种方式,分别是 链式存储 和 顺序存储

所谓的链式存储其实就是用链表list来do树这种数据结构!
所谓的顺序存储其实就是用数组arr来do树这种数据结构!


	  e
	/   \
   a     b
  / \   / \
 c   d i   f 这里的每个元素我们都叫做是节点,用来连接相邻节点之间的关系
			 我们就叫做“父子关系”
	树的一些基本概念(必须掌握):
		1-父节点:e是a和b的父亲节点
		2-子节点:a和b都是e的子节点
		3-兄弟节点:父亲节点都是同一个的结点就互称之为兄弟节点。比如:a和b的父节点都是e,因此a和b之间互称为兄弟节点
		4-根节点:没有父亲节点的结点就称之为根节点,比如e就是根节点
		5-叶子节点(叶节点):没子节点的结点就称之为叶子节点,比如c d i f都是叶子节点  

		(用生活中的例子来记忆这种数据结构的特点,非常快!)
		6-节点的高度:(类比生活中你判断一栋大楼的高度是多少,我们就从第0层开始计算,从下往上计算!)
		7-节点的深度:(类比生活中你判断水下的鱼在哪个水深(深度)处,是从水平面为0开始计算,从上往下计算!)
		8-节点的层数:(和节点深度的计算差不多,只不过层数是从1开始计算的,也是从上往下计算,比如根节点的层数就是1)


		9-树的高度:== 跟节点的高度 == 最大层数-1
		
		例子:				高度   深度    层		这个树的高度== 3
			  o		   --->  3      0     1 		
			/   \			 
		   o     o	   --->  2      1     2
		  / \   / \
		 o   o o   o   --->  1      2     3
		/ \       /
	   o   o	 o     --->  0      3     4
	

	树的结构多种多样,不过我们最常用的还是二叉树!

	二叉树(binary tree)
		:二叉树,顾名思义,就是每一个节点最多有2个“叉”,也就是有2个子节点,分别是
		左子节点 和 右子节点 。不过,二叉树并不要求每个节点都有2个子节点,有的结点只有左子节点
		而有的结点则只有右子节点。下面这3个图都是二叉树的三种常见的例子!

	二叉树的分类:
		1-普通二叉树:一般二叉树都是普通二叉树
		2-满二叉树:叶子节点全部都在最底层,并且除了叶子节点外,每个节点都有左右2个子节点
		3-完全二叉树:作为完全二叉树必须要满足2个特点:
		①除了最底层外,其他层节点个数达到最大(也即除去最底层后这个二叉树会变成满二叉树的意思!)
		②最底层的叶子节点从左边依次排列过来,并且中间没有断开过
 
		完全二叉树 和 满二叉树 是比较适合于用数组做顺序存储的!因为它们的节点数目都比较多,不会浪费数组的存储空间
		除了0下标外,其他的元素都可以按顺序的按照索引来存储。
		而其他类型的二叉树就比较适合用链表来do存储了!
 
							  o		   
							/   \
						   o     o	   
						  /     / 
						 o     o   
					   /  \
					  o    o   
						  / \   
						 o   o
							(1)	(这是普通的二叉树!)

							o
						/         \
					   o           o
					  / \         / \
					 o   o       o   o										  
			        / \  /\     / \ / \
				   o  o o  o   o  o o  o  
							(2)	(满二叉树:是满二叉树的一种特殊的case!!!)

						     o
						/         \
					   o           o
					  / \         / \
					 o   o       o   o
					/ \  /   
				   o  o o   
							(3)(完全二叉树)
						     o
						/         \
					   o           o
					  / \         / \
					 o   o       o   o
					/ \         /
				   o   o       o
				            (4) (这就是个普通的二叉树,非完全二叉树,因为虽然满足除了最底层外其他层的节点个数达到最大)
								(但是最底层的叶子节点们的节点是断开过的,并不连续!)


	

	那么下面我们就来实现一下一棵二叉树:
		我们有2种方法: 一种是基于指针or引用的二叉链式存储法(一个节点有3份字段,一份存data,另外2份分别存储指向左右子节点的指针)
					   一种是基于数组的顺序存储法!(下标为0的空间会浪费掉,用下标为1的空间来存储根节点root,然后用2*i的下标位置存储左子节点,用2*i+1的下标位置存储右子节点)

	我们先来看比较简单和直观的 链式存储法:
			所谓的二叉树的链式存储法,其实就是:一个节点有3个字段,一个存储data,另外2个是分别指向其左右节点的指针!

		比如:
		  data
		  / \
		left right
		/         \
	  data          data
	  / \			/   \
	left right   left   right
		
	我们再来看不那么直观的 数组存储法:我们会用一个数组来存储二叉树种的all的节点元素
	其中,浪费一个下标为0的数组空间,然后用下标为1的空间存储二叉树的根节点,然后用2*1 = 2的位置存储它的左子节点
	然后用2*1+1 = 3的位置来存储它的右子节点,用2*2=4的位置存储根节点的左字节点的左子节点,用2*2+1=5的位置存储根节点的右子节点
	的右子节点,然后以此类推。
	比如:
			(1)	  a
			  /     \
			 b(2)    c(3)
			/   \	  /  \
		  d(4)  e(5) f(6) g(7)
		  ......................以此类推!

	[] [a] [b]  [c]  [d]  [e]  [f]  [g] ....[] [] []  []
	0  1    2    3    4    5    6    7    8    9    10    11

	当然,这仅仅是把完全二叉树用数组的存储方式表示出来,如果你是一个普通一点的二叉树甚至是单臂树的话,这样就会非常的浪费存储空间!
	so:
	总结:
		如果是满二叉树或者是完全二叉树的话,那么用数组的存储方式来表示是完全没问题的,也无疑是最节省内存的一种方式
	,但是对于别的形式的二叉树,一般我们都是用链式存储方式来存储的!
		当然我们学习到 堆和堆排序时,你就会发现, 堆其实就是一种完全二叉树,其最常用的存储方式就是数组




	下面我们来看二叉树中非常重要的操作,也即二叉树的遍历,这也是非常常见的面试题!
	
	我们如何将所有的节点都遍历打印出来呢?
	经典的方法有3种:
		前序遍历、中序遍历和后序遍历
		其中,何为之前序中序和后序呢?其实就是表示:节点与它的左右子树节点遍历打印的先后顺序!
	
	1-前序遍历:对于树中的任意节点来说,先打印这个节点,然后再打印它的左子树,最后打印它的右子树 (本身 左 右)
	2-中序遍历:对于树中的任意节点来说,先打印它的左子树,然后再打印它本身,最后才打印它的右子树(左 本身 右)
	3-后序遍历:对于树中的任意节点来说,先打印它的左子树,然后再打印它的右子树,最后再打印这个节点本身(左 右 本身)
	
	从以上的概念中,你就可以清晰的认识到所谓的前中后序遍历是啥意思了,
	-前序就是要本身在前,左右在后
	-中序就是要本身在左右的中间
	-后序就是要本山在最后,左右在前
	(实际上呢,二叉树的前中后序遍历其实就是一个递归的过程。比如:前序遍历,其实就是先打印根节点,然后再递归地打印左子树,最后递归地打印右子树)
	(上面这个本质的思想一定要建立起来!不能有所含糊,不然你就会搞不懂怎么对二叉树进行前中后序的遍历了!)
	
	而写递归代码的关键,其实就是看你能不能写出递归的递推公式!以及递归的终止条件了。
	写递归的递推公式的关键就是,如果要解决问题a,就假设子问题b和c已经被解决了,然后再来看如何利用b和c来deal问题a
	比如下面来几个例子(帮助我理解一下这几个概念):
		前序遍历:
			a
		 /    \
		b      c
	  /  \    /  \
	 d    e  f    g
result: a->b->d->e->c->f->g
		中序遍历:
			1
		 /    \
	   -2      3
	  /  \    /  \
	 -3   -1 2    4    
result: -3 -> -2 -> -1 -> 2 -> 3 -> 4
从这里我们可以看出来对于二叉树的中序遍历,可以输出有序(升序)的数据序列,且时间复杂度为O(n),这是非常高效的!
因此二叉查找树也被称之为二叉排序树。
		后序遍历:
			a
		 /    \
		b      c
	  /  \    /  \
	 d    e  f    g
result: d->e->b->f->g->c->a



	前面我们学习了树、二叉树、二叉树的遍历,那么今天我们再来学习一种特殊的二叉树---二叉查找树。

	二叉查找树之最大的特点就是:支持动态数据集合的快速插入、删除、查找操作。

	我们之前说过,散列表也是支持这些操作的,并且散列表的这些操作比二叉查找树更加地高效,且时间复杂度为O(1).
	
	那么既然有了这么高效的散列表,使用二叉树的地方是不是都可以替换成散列表呢?有没有哪些地方是散列表所do不了的呢?
	有什么地方是必须要使用二叉树来do的呢?



	二叉查找树(也称之为二叉搜索树或者说二叉排序树):Binary Search Tree
		二叉查找树是二叉树中最为常用的一种类型,也叫做二叉搜索树。顾名思义呢,二叉查找树是为了实现快速查找而生的。
		只不过呢,它不仅仅是支持快速查找一个数据这一种操作的,它还支持快速的插入、删除一个数据。它是怎么做到这些的呢?

	二叉查找树要求,在树中的任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值,而右子树中的每个节点的值都大于这个节点的值。
	就比如:
		13							16							10
	  /	   \					   /                           /  \
	10      16					  10                          9    14
    / \      /					 /  \                              / \ 
  9   11   14                   9    13                           13  16
		(1)							/  \                         / 
								   11   14                      11
									(2)                          (3)  
  

下面是我根据理论知识手动敲出来的二叉查找树的代码:

MyTreeNode.hpp:

#ifndef MYTREENODE 
#define MYTREENODE
#include
using namespace std;
template
class MyTreeNode//无prevFather节点的二叉搜索树!
{
public:
	typedef  MyTreeNode Node;
	T data;
	Node* left;
	Node* right;
public:
	//构造函数
	MyTreeNode() :data(0), left(nullptr), right(nullptr) {}
	MyTreeNode(T d) :data(d), left(nullptr), right(nullptr) {}
	MyTreeNode(T d, Node* l, Node* r) :data(d), left(l), right(r) {}
	//拷贝构造函数
	MyTreeNode(const Node*& node)
	{
		this->data = node->data;
		this->left = new Node(node->left->data,node->left->left,node->right->right);
		this->right = new Node(node->right->data, node->right->left, node->right->right);
	}
	//拷贝赋值函数
	Node*& operator=(const Node*& node)
	{
		Node* tempNode = new Node;
		tempNode->data = node->data;
		tempNode->left = new Node(node->left->data, node->left->left, node->right->right);
		tempNode->right = new Node(node->right->data, node->right->left, node->right->right);
		return tempNode;
	}
};

#endif // !MYTREENODE 

MyBinSearchTree.hpp:

#ifndef MYBINSEARCHTREE 
#define MYBINSEARCHTREE
#include
#include"MyTreeNode.hpp"
using namespace std;                                                                                                                
template

class MyBinSearchTree//无prevFather节点的二叉搜索树!
{
public:
	typedef  MyTreeNode Node;
	Node* root;
public:
	//构造函数
	MyBinSearchTree() :root(nullptr) {}
	MyBinSearchTree(T rootData)
	{
		this->root = new Node;//对于树的根节点,当你创建一颗树的时候就必须要new一个root了!
		this->root->data = rootData;
	}
	//创建一个二叉查找树的函数
	void createMyTree(T val, Node* &temp);
	//二叉查找树的find查找的函数
	MyTreeNode* find(T val);
	//二叉查找树的insert插入节点的函数
	bool insert(T val);
	void findPreFatherOfP(MyTreeNode* & pPrev, MyTreeNode* &p, T val);
	//二叉查找树的delete节点的函数
	bool deleteNode(T val);
	//前序遍历
	void preOrder(Node* tree);
	//中序遍历
	void middleOrder(Node* tree);
	//后序遍历
	void lastOrder(Node* tree);

};

//二叉查找树的find查找的函数
template
MyTreeNode* MyBinSearchTree::find(T val)
{
	/*
		1-若要查找的值val等于根节点的值那么就返回
		2-若val大于根节点的值那么就递归查找右子树!
		3-若val小于根节点的值那么就递归查找左子树!
	*/
	Node* temp = this->root;
	while (temp != nullptr)
	{
		if (val == temp->data) {
			return temp;
		}
		if (val < temp->data) {
			temp = temp->left;
			continue;
		}
		if (val > temp->data) {
			temp = temp->right;
			continue;
		}
	}
	return nullptr;//if都找不到就返回nullptr!
}
template
void MyBinSearchTree::findPreFatherOfP(MyTreeNode* &pPrev, MyTreeNode* &p,T val)
{
	while (p != nullptr && p->data != val)
	{
		pPrev = p;
		if (val < p->data) {
			p = p->left;
		}
		else if (val > p->data) {
			p = p->right;
		}
	}
}
//二叉查找树的delete节点的函数
template
bool MyBinSearchTree::deleteNode(T val)
{
	Node* node = MyBinSearchTree::find(val);
	if (node == nullptr) {
		return false;
		//你 在这一棵二叉查找树中连找都找不到这个元素当然删除失败啦!
	}
	//这个元素存在的大case下你才能删除啊对吧?
	Node* p = this->root;
	Node* pPrev = nullptr;
	if (node->left == nullptr && node->right == nullptr)
	{//小case1:若值等于给定值的这个节点既没有左子树也没有右子树
		findPreFatherOfP(pPrev, p, val);
		//删除元素
		if (p->data < pPrev->data) {
			pPrev->left = nullptr;
			return true;
			cout << "删除成功!" << endl;
		}
		else if (p->data > pPrev->data) {
			pPrev->right = nullptr;
			return true;
			cout << "删除成功!" << endl;
		}
	}
	if (node->left != nullptr && node->right == nullptr)
	{//小case2:若值等于给定值的这个节点有左子树但没有右子树
		findPreFatherOfP(pPrev, p, val);
		//删除元素
		Node* thisdeleteNode = p;
		p = p->left;
		if (p->data < pPrev->data) {
			pPrev->left = p;
		}
		else if (p->data > pPrev->data) {
			pPrev->right = p;
		}
		thisdeleteNode = nullptr;
		cout << "删除成功!" << endl;
		return true;
	}
	if (node->left == nullptr && node->right != nullptr)
	{	//小case3:若值等于给定值的这个节点没有左子树但有右子树
		findPreFatherOfP(pPrev, p, val);
		//删除元素
		Node* thisdeleteNode = p;
		p = p->right;
		if (p->data < pPrev->data) {
			pPrev->left = p;
		}
		else if (p->data > pPrev->data) {
			pPrev->right = p;
		}
		thisdeleteNode = nullptr;
		cout << "删除成功!" << endl;
		return true;
	}

	if (node->left != nullptr && node->right != nullptr) 
	{//小case4:若值等于给定值的这个节点既有左子树也有右子树
		findPreFatherOfP(pPrev, p, val);
		Node* minP = p->right;//保存找待删除节点的右子树中值等于最小值的当前节点
		Node* minPP = p;//保存找待删除节点的右子树中值等于最小值的父亲节点
		//左子树不为空的case下!
		if (minP->left != nullptr) {
			while (minP->left != nullptr) {
				minPP = minP;
				minP = minP->left;
			}
			p->data = minP->data;//只需要把待删除节点的值赋值为其右子树中的最小值
			minPP->left = minP->right;
			//我不管你当前最小值是否还存在右子树,
			//我都把他的父亲节点的左子树赋值为该最小值节点的右子树
			//(当然,左子树肯定是不存在的,因为最小了,就必须不存在左子树了!否则就不是二叉查找树了啊!)
			minP = nullptr;
			cout << "删除成功!" << endl;
			return true;
		}//左子树为空的case下!
		else {
			if (minP->right != nullptr) {
				p->data = minP->data;//只需要把待删除节点的值赋值为其右子树中的最小值
				minPP->right = minP->right;
				minP = nullptr;
				cout << "删除成功!" << endl;
				return true;
			}
			else {
				p->data = minP->data;
				minPP->right = nullptr;
				minP = nullptr;
				cout << "删除成功!" << endl;
				return true;
			}
		}

	}

	return false;
}

//二叉查找树的insert插入节点的函数
template
bool MyBinSearchTree::insert(T val)
{
	if (MyBinSearchTree::find(val) != nullptr) {
		cout << "我的二叉查找树不允许存在重复的"<root;
	while (temp != nullptr)
	{
		if (val < temp->data && temp->left==nullptr) {
			temp->left = new Node(val);
			return true;
		}
		if (val > temp->data && temp->right == nullptr) {
			temp->right = new Node(val);
			return true;
		}
		if (val < temp->data && temp->left != nullptr) {
			temp = temp->left;
			continue;
		}
		if (val > temp->data && temp->right != nullptr) {
			temp = temp->right;
			continue;
		}
	}
	return false;
}

//前序遍历
template
void MyBinSearchTree::preOrder(Node* tree)//中左右
{
	Node* temp = tree;
	if (temp != nullptr) {
		cout << temp->data << endl;
	}
	if (temp->left != nullptr) {
		MyBinSearchTree::preOrder(temp->left);
	}
	if (temp->right != nullptr) {
		MyBinSearchTree::preOrder(temp->right);
	}
}
//中序遍历
template
void MyBinSearchTree::middleOrder(Node* tree)//左中右
{
	Node* temp = tree;
	if (temp->left != nullptr) {
		MyBinSearchTree::middleOrder(temp->left);
	}
	if (temp != nullptr) {
		cout << temp->data << endl;
	}
	if (temp->right != nullptr) {
		MyBinSearchTree::middleOrder(temp->right);
	}
}
//后序遍历
template
void MyBinSearchTree::lastOrder(Node* tree)//左右中
{
	Node* temp = tree;
	if (temp->left != nullptr) {
		MyBinSearchTree::lastOrder(temp->left);
	}
	if (temp->right != nullptr) {
		MyBinSearchTree::lastOrder(temp->right);
	}
	if (temp != nullptr) {
		cout << temp->data << endl;
	}
}
template
void MyBinSearchTree::createMyTree(T val, Node* &temp)
{
	/* 注意!这里的形参表中的temp必须要使用& 取引用的方式来do(也即使用引用传递的方式来do)
	   否则你没法改变这课树的形状,你插入了也白插入!
	   这个问题就和一个非常简单的C++的函数swap问题一样
		void swap(int val1,int  val2)
		{
			int temp = val1;
			val1 = val2;
			val2 = temp;
		}
		//外部
		int a=2,b=3;
		swap(a,b);
		//你这样只会改变临时变量val1和val2的值,而外部的传入
		的a和b的值并不会发生改变,因为这是值传递而已!
		
		你要想改变里面的值你就必须要用指针传递or引用传递!
	*/
	if (temp == nullptr)
	{
		cout << "插入成功!" << endl;
		temp = new Node(val);
	}
	else
	{
		if (temp->data == val)
		{
			cout << "我不允许二叉查找(搜索)树中存在从重复元素!" << endl;
		}
		else if (val < temp->data) {
			createMyTree(val, temp->left);
		}
		else if (val > root->data) {
			createMyTree(val, temp->right);
		}
	}
}
#endif // !MYBINSEARCHTREE 

MyBinTree.cpp:

#include
#include"MyTreeNode.hpp"
#include"MyBinSearchTree.hpp"
using namespace std;
void createMyTree(MyBinSearchTree* &myTree)
{
	//创建一个二叉树
	int val = 0;
	while (1)
	{
		cout << "请输入你要插入的节点的val值:(-999 to quit!)" << endl;
		cin >> val;
		if (val == -999) {
			cout << "结束创建二叉树!" << endl;
			break;
		}
		myTree->createMyTree(val, myTree->root);
	}
}
void testMyTree()
{
	int v = 0;
	cout << "请输入你所要创建的二叉搜索树的root根节点的val值:" << endl;
	cin >> v;
	MyBinSearchTree* myTree = new MyBinSearchTree(v);
	//创建一个二叉树
	createMyTree(myTree);
	//前序遍历
	cout << "前序遍历:" << endl;
	myTree->preOrder(myTree->root);
	//中序遍历
	cout << "中序遍历:" << endl;
	myTree->middleOrder(myTree->root);
	//后序遍历
    cout << "后序遍历:" << endl;
	myTree->lastOrder(myTree->root);
	//查找val值
	cout << "查找操作:" << endl;
	MyTreeNode* t = myTree->find(-6);
	if (t) {
		cout << "查找元素:" << t->data << "成功!"<insert(5);
	if (ret) {
		cout << "插入5成功!" << endl;
	}
	else cout << "插入5失败!" << endl;
	//用前序遍历来show 一下当前的二叉查找树中的all元素!
	myTree->preOrder(myTree->root);
	bool ret2 = myTree->insert(6);
	if (ret2){
		cout << "插入6成功!" << endl;
	}
	else cout << "插入6失败!" << endl;
	//用前序遍历来show 一下当前的二叉查找树中的all元素!
	myTree->preOrder(myTree->root);

	//删除值为val的节点
	cout << "删除操作:" << endl;
	bool retDeleteResult = myTree->deleteNode(18);
	if (retDeleteResult) 
		cout << "删除成功!" << endl;
	else
		cout << "删除失败!" << endl;
	//用前序遍历来show 一下当前的二叉查找树中的all元素!
	myTree->preOrder(myTree->root);
}
int main(void)
{
	
	testMyTree();
	system("pause");
	return 0;
}

你可能感兴趣的:(C++数据结构,数据结构,c++,算法)