树(2)---二叉树

二叉树

二叉树的定义

二叉树是另一种树形结构,其特点是每个结点至多只有两棵子树,(即二叉树中不存在度大于2的结点。且,二叉树左右有序,不能颠倒。

二叉树也是递归的形式定义:

1)为空二叉树,n=0;

2)由一个根结点和两个不相交的左子树和右子树组成。左右子树分别又是两棵二叉树。

特殊的二叉树

1)满二叉树:一棵高度为h,并且含有2^h-1个结点的二叉树称为满二叉树,即树中的每一层都含有最多的结点。除叶子结点外每个结点度数都为2。

2)完全二叉树:一个高度为h,有n个结点的二叉树,当且仅当其每一个结点都与高度为h的满二叉书中编号为1~n的结点一一对应。(即比满二叉树少几个叶子结点)


3)二叉排序树:左子树上结点关键字小于结点,右子树上结点关键字大于结点,且子树也为二叉排序树。

4)平衡二叉树:树上任一结点的左右子树深度不超过1。


二叉树的存储结构:

顺序存储结构:

1)数组方式存储(自上向下从左往右依次存储,也就是1,2,4,8……的顺序)

2)0位不存元素,从pq[1]开始

3)元素0表示空结点

4)高度为log2^N+1(N为数组长N从1开始) 


链表方式存储:

结点至少包含三个域:

数据域data、左指针域lchild、右指针域rchild;

数据域分为:key键和data值,key是每个结点的门牌号,唯一存在;

以及为了方便添加一个int域,存储每个结点的子结点数。


二叉树的遍历

先序遍历:

二叉树为空结束,否则:

1)访问根结点

2)先序遍历左子树

3)先序遍历右子树

中序遍历:

二叉树为空结束,否则:

1)中序遍历左子树

2)访问根结点

3)中序遍历右子树

后序遍历:

二叉树为空结束,否则:

1)后序遍历左子树

2)后序遍历右子树

3)访问根结点

遍历的顺序是指访问根结点的先后顺序。


二叉树API:
class BTree{
BTree()构建二叉树
void TLRtrave()先序遍历
void LTRtrave()中序遍历
void LRTtrave()后序遍历
int defth()二叉树深度
int leafnum()叶子数
int size()结点数
void clear()清空
void put(int key,string item)插入元素
void delete_1(int key)删除元素
}; 

代码:

顺序存储结构:

class BTree
{
public:
	BTree(){ T.push_back("null"); }//元素0表示该结点为空
	void put(int n,string item)//(插入元素,数组位序)
	{//从上往下从左往右按层存储(即每层按顺序存储进去)
		if (n > N + 1 || n<1 || T[n / 2] == "0")exit(0);
		if (n == N + 1){ T.push_back(item); N++; }
		else T[n] = item;
	}
	void TLRtrave(){ if (N>0) TLRtrave(1); cout << endl; }//先序遍历
	void LTRtrave(){ if (N>0) LTRtrave(1); cout << endl; }//中序遍历
	void LRTtrave(){ if (N>0) LRTtrave(1); cout << endl; }//后序遍历
	int defth();//二叉树深度
	int leafnum(){ return N>0 && T[1] != "0" ? leafnum(1) : 0; }//叶子数
	int size();//结点数
	void clear();//清空树
	void delete_1(int t);//删除第t个结点
private:
	void TLRtrave(int t);
	void LTRtrave(int t);
	void LRTtrave(int t);
	int leafnum(int t);
	void visit(int t){ cout << T[t] << " "; }//工具函数,打印数据

	vector<string> T;//动态数组(除去0位)
	int N = 0;//动态数组长
};
void BTree::TLRtrave(int t)//先序遍历(数组首元素位序)
{
	visit(t);
	if (t * 2 <= N)TLRtrave(t * 2);
	if (t * 2 + 1 <= N)TLRtrave(t * 2 + 1);
}
void BTree::LTRtrave(int t)//中序遍历(数组首元素)
{
	if (t * 2 <= N)LTRtrave(t * 2);
	visit(t);
	if (t * 2 + 1 <= N)LTRtrave(t * 2 + 1);
}
void BTree::LRTtrave(int t)//后序遍历(数组首元素)
{
	if (t * 2 <= N)LRTtrave(t * 2);
	if (t * 2 + 1 <= N)LRTtrave(t * 2 + 1);
	visit(t);
}
int BTree::defth()
{
	int t = N;
	while (true)
	{
		if (T[t] != "0")break;
		t--;
	}
	return log2(t) + 1;
}
int BTree::leafnum(int t)
{
	if (2 * t > N)return 1;
	else if (T[2 * t] == "0" &&
		(2 * t + 1 > N || T[2 * t + 1] == "0"))return 1;
	else
	{
		int l = 0, r = 0;
		if (2 * t <= N&&T[2 * t] != "0")l = leafnum(2 * t);
		if (2 * t + 1 <= N&&T[2 * t + 1] != "0")r = leafnum(2 * t + 1);
		return l + r;
	}
}
int BTree::size()
{
	int t = 0;
	for (int i = 1; i <= N; i++)
		if (T[i] != "0")t++;
	return t;
}
void BTree::clear()
{
	for (int i = 1; i <= N; i++)
		T.pop_back();
	N = 0;
}
void BTree::delete_1(int t)
{
	if (2 * t <= N)delete_1(2 * t);
	if (2 * t + 1 <= N)delete_1(2 * t + 1);
	T[t] = "0";
}
顺序存储比较简单,需要注意的有以下几点:

1.put函数是按层存入的,因为使用的是动态数组所以没法跳过前面的项直接存储后面的元素,所以要注意,测试时将空项值都设为0.

2.是在leafnum函数中,边界较多,需要考虑:

1)叶子没有左右孩子,

2)或者孩子值都为0,

3)亦或者左孩子为0,右孩子为空

三种情况下都为叶子结点

3.是defth函数,计算深度时,应该排除末尾为值0的元素。


链式存储结构:

class Node
{
	friend class BTree;
	int key;//键(不重复)
	string data;//值域
	Node* lchild;//左指针域
	Node* rchild;//右指针域
	int N;//以该结点为根的子结点总数
public:
	Node(int key, string item, int N)
		:key(key), data(item), N(N){}
};
class BTree
{
public:
	void put(int key, string item)
	{
		root = put(root, key, item);
	}
	void TLRtrave(){ TLRtrave(root); }//先序遍历
	void LTRtrave(){ LTRtrave(root); }//中序遍历
	void LRTtrave(){ LRTtrave(root); }//后序遍历
	int defth(){ return defth(root); }//二叉树深度
	int leafnum(){ return leafnum(root); }//叶子数
	int size(){ return size(root); }//结点数
	void clear(){ clear(root); root = NULL; }//清空
	void delete_1(int key)//删除键为key的元素
	{
		root = delete_1(root, key);
	}
private:
	Node* root = NULL;//根结点
	//工具函数
	Node* put(Node* x, int key, string item);
	void visit(Node* x)//打印元素
	{
		cout << x->key << ":" << x->data << endl;
	}
	void TLRtrave(Node* root);
	void LTRtrave(Node* root);
	void LRTtrave(Node* root);
	int defth(Node* root);
	int leafnum(Node* x);
	int size(Node* x){ return !x ? 0 : x->N; }
	void clear(Node* x);
	Node* delete_1(Node* x, int key);
};
Node* BTree::put(Node* x, int key, string item)//插入
{
	//如果key存在于以X为根结点的子树中则更新它的data
	//否则插入他的子树中,左子树key小于他,右子树key大于他
	if (!x)return new Node(key, item, 1);
	if (key<x->key)     x->lchild = put(x->lchild, key, item);
	else if (key>x->key)x->rchild = put(x->rchild, key, item);
	else x->data = item;
	x->N = size(x->lchild) + size(x->rchild) + 1;
	return x;
}
void BTree::TLRtrave(Node* x)
{
	if (x)
	{
		visit(x);
		TLRtrave(x->lchild);
		TLRtrave(x->rchild);
	}
}
void BTree::LTRtrave(Node* x)
{
	if (x)
	{
		LTRtrave(x->lchild);
		visit(x);
		LTRtrave(x->rchild);
	}
}
void BTree::LRTtrave(Node* x)
{
	if (x)
	{
		LRTtrave(x->lchild);
		LRTtrave(x->rchild);
		visit(x);
	}
}
int BTree::defth(Node* x)
{
	int l, r;
	if (x)
	{
		l = defth(x->lchild);
		r = defth(x->rchild);
		return l > r ? l + 1 : r + 1;
	}
	else return 0;
}
int BTree::leafnum(Node* x)
{
	
	if (!x->lchild&&!x->rchild)return 1;
	else
	{
		int l = 0, r = 0;
		if(x->lchild)l = leafnum(x->lchild);
		if(x->rchild)r = leafnum(x->rchild);
		return l + r;
	}
}
void BTree::clear(Node* x)
{
	if (x)
	{
		clear(x->lchild);
		clear(x->rchild);
		delete x;
	}
}
Node* BTree::delete_1(Node* x, int key)
{
	if (!x)return NULL;
	if (key < x->key)x->lchild = delete_1(x->lchild, key);
	else if (key > x->key)x->rchild = delete_1(x->rchild, key);
	else
	{
		visit(x);
		clear(x);
		x = NULL;
	}
	return x;
}
链式结构的基本点都是在递归的运用上,几乎每一个操作函数都用到了递归,我想这就应该是二叉树如此便洁的原因吧,通过堆栈的利用,将链表插入的灵活性和有序数组查找的高效性结合到一起。

在写代码过程中遇见的问题有:

1.键值的使用,准确来说链式结构代码写的是一棵有序树,通过键值将树的左右子树分为两半,这样做不仅为插入、删除带来了便利,更为后面写查找树打下基础。

2.依然是leafnum的边界问题,在调用递归前,必须先确定左右孩子是否存在。

3.是根结点root,构造树之前我们必须将他设为NULL,才能与插入函数向对应。同样的clear函数结束后,我们还是需要root=NULL,否则插入函数将会跳过构造结点,直接进行key的比较。

4.是delete_1函数,删除结点的同时,删除了结点上的左右子树,但是我想应该还会有一个删除函数支持只删除单个结点,不过知识点还涉及到了后面查找树的一些功能,所以就留到下一节在写。

测试用例:

int main()
{
	BTree p;
	int m, n;
	string item;
	cout << "***********************************" << endl;
	cout << "0.打开菜单.       1.插入新元素." << endl;
	cout << "2.先序遍历.       3.中序遍历." << endl;
	cout << "4.后序遍历.       5.二叉树的深度." << endl;
	cout << "6.二叉树的叶子数. 7.求结点数." << endl;
	cout << "8.清空二叉树.     9.删除元素." << endl;
	cout << "***********************************\n" << endl;
	while (cin >> m)
	{
		switch (m)
		{
		case 0:
			cout << "***********************************" << endl;
			cout << "0.打开菜单.       1.插入新元素." << endl;
			cout << "2.先序遍历.       3.中序遍历." << endl;
			cout << "4.后序遍历.       5.二叉树的深度." << endl;
			cout << "6.二叉树的叶子数. 7.求结点数." << endl;
			cout << "8.清空二叉树.     9.删除元素." << endl;
			cout << "***********************************\n" << endl;
			break;
		case 1:
			cout << "输入插入元素键和值:" << endl;
			cin >> n >> item;
			p.put(n, item);
			break;
		case 2:
			cout << "先序遍历:" << endl;
			p.TLRtrave();
			break;
		case 3:
			cout << "中序遍历:" << endl;
			p.LTRtrave();
			break;
		case 4:
			cout << "后序遍历:" << endl;
			p.LRTtrave();
			break;
		case 5:
			cout << "二叉树深度:" << p.defth() << endl;
			break;
		case 6:
			cout << "二叉树叶子数:" << p.leafnum() << endl;
			break;
		case 7:
			cout << "结点数:" << p.size() << endl;
			break;
		case 8:
			cout << "清空." << endl;
			p.clear();
			break;
		case 9:
			cout << "输入删除元素键:" << endl;
			cin >> n;
			p.delete_1(n);
			break;

		}
	}
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值