【数据结构】树-二叉树及其应用


前言

什么是二叉树?二叉树就是只两个叉的树,这么理解是对的,但是还差点意思,二叉树指的是任何节点的度不大于2的树。怪抽象的,简单的来说,就是任何节点都只能有至多两个子节点的树叫二叉树。下面我们来看看几种特殊的二叉树。

1. 斜树

斜树就是所有节点都斜向一边的树,比如左斜树就是所有节点都只有左子树的树,右斜树就是所有节点都只有右子树的树,如下:
在这里插入图片描述
在这里插入图片描述
是不是觉得这和线性表顺序表没啥区别,确实如此。

2. 满二叉树

满二叉树就是所有节点要么拥有两个子树,要么没有子树(叶子),且作为的那些叶子的节点必须在同一深度,且在最下一层,就像下面这棵树:
在这里插入图片描述

3. 完全二叉树

完全二叉树就是可以缺失部分叶子的满二叉树,但是这个缺失有个条件,那就是在叶子所在的那一深度上,要从右往左缺失,只要不符合这个条件,统统不属于完全二叉树
在这里插入图片描述

一、树、森林与二叉树的转换

1. 树转换为二叉树

这就是我们之前讲的树的兄弟孩子表示法,左子树为第一个孩子,右子树为兄弟,转换如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.森林转换为二叉树

首先,先将每一棵树用兄弟孩子表示法转换成二叉树,每个根节点的右子树都是空着的,这时候再将其他树,作为根节点的右子树,如下图:
在这里插入图片描述
在这里插入图片描述

二、二叉树的性质

性质1. 二叉树的第i层至多有2i-1个节点

要证明这个性质很简单,因为二叉树限定了任一节点的子树至多有两个,那么第i层的节点数最多是上一层结点数的两倍,想让该层结点数最多就要让上一层节点数也最多,这不就是满二叉树嘛,而满二叉树的第i层的结点数恰好就是2i-1

性质2. 深度为k的满二叉树至多有2k-1个结点

想要结点数最多那就是满二叉树嘛,直接计算深度为k的满二叉树的结点数,恰好就是2k-1。

性质3. 任何一颗二叉树如果其终端结点数为n0,度为2的结点数为n2,则n0 = n2 + 1

任意一棵树他的总结点数n = n0 + n1 + n2,从下往上看,每个结点对应一个分支处理根结点,所有总分支为n - 1,从上往下看,度为2的结点有两个分支,度为1的结点有一个,度为0的没有,所有总分支数为n1 + n2,所有n - 1 = n1 + n2
联立结点数和分支数的式子,就得到了n0 = n2 + 1。

性质4. 具有n个结点的完全二叉树的深度为 ⌊ l o g 2 ( n + 1 ) ⌋ \lfloor log_2(n+1) \rfloor log2(n+1)⌋ + 1

假设这个二叉树的深度为k,同深度的满二叉树的结点数为2k + 1,k - 1深度的二叉树的结点数为2k-1 + 1,由完全二叉树的定义我们可以知道2k-1 + 1 < n ≤ 2k + 1,化简得 ⌊ l o g 2 ( n + 1 ) ⌋ \lfloor log_2(n+1) \rfloor log2(n+1)⌋ + 1

性质5. 如果一个有n个结点的完全二叉树,结点编号i如图所示,则有如下性质:

1)如果i = 1,则结点是二叉树的根无双亲;如果i > 1,则其双亲是 ⌊ i / 2 ⌋ \lfloor i / 2 \rfloor i/2
2)如果2i > n,则结点无左孩子,否则其左孩子是结点2i
3)如果2i + 1 > n,则结点无右孩子,否则其右孩子是结点2i + 1
在这里插入图片描述

三、二叉树存储及其操作

(一)普通二叉树

顺序存储结构

顺序存储结构,在存储非完全二叉树时会十分浪费空间,这里我们只讲他的存储思路,实现代码十分简单定义一个数组就好。我们知道树是可以编号的,他对应的号码和满二叉树的的一样,基于此,我们只需要把数据存到相应的位置就好
在这里插入图片描述

链式存储结构

1. 二叉链表

由于限定了结点的度数,这个时候孩子表示法就变得轻松多了

typedef struct BiTNode{
	TElemType data;
	struct BiTNode *lchild, *rchild;
}*BiTree;
2. 三叉链表

我们也可以设置一个指向双亲的指针(双亲孩子表示法),这样我们无论是上溯还是下寻都方便很多

typedef struct TriTNode{
	TElemType data;
	struct TriTNode *lchild, *parent, *rchild;
}*TriTree;

操作

1. 构造二叉树

构造一个二叉树用递归的方法最好理解了,首先设置一个停止递归的条件,然后给当前结点赋值后递归构造该结点的左右子节点

Status CreatBiTree(BiTree &T){
	cin >> ch;
	if(ch == '#') return OK;
	else{
		if(!(T = new BiTNode)) exit(OVERFLOW);
		T -> data = ch;
		CreatBiTree(T -> lchild);
		CreatBiTree(T -> rchild);
	}
	return ok;
}
2. 复制二叉树

赋值二叉树的思路和构造二叉树一致,用递归来遍历整棵树,同时将值给取出来

int copy(BiTree T, BiTree &NewT){
	if(T == NULL){
		NewT = NULL;
		return 0;
	}else{
		NewT = new BiTNode;
		NewT -> data = T -> data;
		copy(T -> lchild, NewT -> lchild)
		copy(T -> rchild, NewT -> rchild)
	}
	
}
3. 遍历二叉树

遍历树有三种方法,分别为线序遍历、中序遍历、后序遍历,分别代表先遍历根后左右子树、先遍历左子树接着遍历根最后遍历右子树、先遍历左右子树最后根,不同的方法遍历出来的顺序非常不一样,但是对应代码不过是改变了三条语句的顺序

Status PreOrderTraverse(BiTree T){
	if(T == NULL) return OK;
	else{
		visit(T);	// 访问根节点
		PreOrderTraverse((T -> lchild);	// 遍历左子树
		PreOrderTraverse((T -> rchild);	// 遍历右子树
	}
}
4. 中序遍历二叉树(用栈代替递归)

首先初始化一个栈来存放还未遍历到的根节点,当T指指向空或者栈非空的情况下,判断T是否为空,如果不是就需要让他进栈去判断他的左子树,如果为空就说明遍历左子树已经到头了,轮到根,然后将他的值赋值为右子树,接着遍历

Status InOrdertraverse(BiTree T){
	InitStack(S);
	while(T || !StackEmpty(s)){
		if(T){
			push(S, T);
			T -> lchild;
		}else{
			pop(S, T);
			cout << T -> data;
			T = T -> rchild;
		}
	}
	return OK;
}
5. 层次遍历二叉树(用队列代替递归)

根据层次来遍历整棵树,只要队列不为空,就说明还有需要遍历的结点,将他取出来读出数据后,再将他的左右子树入队等待遍历

void LevelOrder(BiTree T){
	SqQueue *qu;
	InitQueue(qu);
	enQueue(qu, T);
	while(!EmptyQueue(qu)){
		deQueeu(qu, T);
		cout << T -> data;
		if(T -> lchild != NULL) enQueue(T -> lchild);
		if(T -> rchild != NULL) enQueue(T -> rchild);
	}
}
6. 获取树的深度
int Depth(BiTree T){
	if(T == NULL) return 0;
	else{
		int m = 0, n = 0;
		m = Depth(T -> lchild);
		n = Depth(T -> rchild);
		return m > n ? (m+1) : (n+1);
	}
}
7. 获取树的结点数
int NodeCount(BiTree T){
	if(T == NULL) return 0;
	return NodeCount(T -> lchild) + NodeCount(T -> rchild) + 1;
}
8. 获取树的叶子数
int LesfCount(BiTree T){
	if(T == NULL) return 0;
	if(T -> lchild == NULL && T -> rchild == NULL) return 1;
	else return LeafCount(T -> lchild) + LeafCount(T -> rchild);
}

(二)线索二叉树

一个有n个结点的二叉树有2n个指针,但只有n-1条分支,因而有n+1个指针被浪费了,我们要把它作为线索利用起来指向他的前驱和后继,为了区分他指向的是孩子还是线索,听到这里你是不是觉得这是什么垃圾结构,为了利用空闲位置还让我倒贴两倍的位置进去。如果线索二叉树止步于此,那它确实是一个垃圾结构,但是线索二叉树的优点并不在于能节省空间,而在于能节省时间,我们之前对树做一个遍历,常常用到递归,这十分的消耗内存和空间,而线索二叉树可以帮我们摆脱递归,让时间复杂度变成O(n)。

存储结构

typedef enum{Link, Thread} PointerTag;
typedef struct BiThrNode{
	TElemType data;
	BiThrNode *lchild; *rchild;
	PointerTag LTag, RTag;
}*BiThrTree;

操作

1. 二叉树线索化

通过中序遍历的方式线索化一个二叉树,首先线索化他的左子树,线索化完之后对当前结点进行处理,如果他没有左子树,就将他的前驱赋值给左孩子,如果他的前驱没有右子树就将他赋值给他的前驱的右孩子

BiThrTree pre;	// 指向当前访问结点的上一结点
void InThreading(BiThrTree T){
	if(T){
		InThreading(T -> lchild);
		if(!p -> lchild){
			p -> LTag = Thread;
			p -> lchild = pre;
		}
		if(!pre -> rchild){
			pre -> RTag = Thread;
			pre -> rchild = p;
		}
		pre = p;
		InThreading(T -> rchild)
	}
}
2. 中序遍历线索二叉树
Status InOrderTraverse_Thr(BiThrTree T){
	BiThrTree p;
	p = T -> lchild;
	while(p != T){
		while(p -> LTag == Link) p = p -> lchild;
		cout << p -> data;
		while(p -> RTag == Thread && p -> rchild != T){
			p = p-> rchild;
			cout << p -> data;
		}
		p = p -> rchild;
	}
	return OK;
}

四、哈夫曼树

如果一个二叉树的结点是带权的,那么我们要如何让整棵树的权值最小呢?答案很简单,只需要将权值高的放在前面就好了,哈夫曼树就是这个原理,可以用它来解决远距离通信时数据数据压缩的问题

1. 哈夫曼树的存储(顺序存储)

typedef struct HTNode{
	int weight;
	int parent, lchild, rchild;
}*HuffmanTree;

void CreatHuffmanTree(HuffmanTree HT, int n){
	// 创建带权结点
	if(n <= 1) return;
	m = 2*n - 1;
	HT = new HTNode[m+1];
	
	// 初始化结点
	for(int i = 1; i < m; ++i){
		HT[i].parent = 0;
		HT[i].lchild = 0;
		HT[i].rchild = 0;
	}
	
	//输入权重
	for(int i = 1; i < n; ++i){
		cin >> HT[i].weight;
	}

	//合并结点并把他们存在后n-1个位置
	for(int i = n+1; i <= m; ++i){
		// 选取权重最小的两个结点
		select(HT, i-1, s1, s2);
		HT[s1].parent = i;
		HT[s2].parent = i;
		HT[i].lchild = s1;
		HT[i].rchild = s2;
		HT[i].weight = HT[s1].weight + HT[s2].weight;
	}
}

跳转系列文章目录


  • 18
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值