树和二叉树

1.树的定义和基本术语

  • 数是个结点的有限集。在任意一棵非空树中
    • 有且仅有一个特定的称为的结点
    • 当n>1时,其余结点可分为m(m>0)个互不相交的有限继T1,T2,…,Tm,其中每一个集合本身又是一棵树,并且称为根的子树。
  • 结点的度结点拥有的子树数称为结点的度度为0的结点称为叶子或终端结点
  • 数的度:各结点的度的最大值
  • 数的深度或高度:数中结点的最大层次
  • 有序树和无序树:如果将树中结点的各子树看成从左至右是有次序的,则称该数为有序树,否则称为无序树
  • 森林:m课互不相交的树的集合

2.二叉树

  • 二叉树是另一种树型结构,它的特点是每个结点至多只有两个子树,并且,二叉树的子树有左右之分,其次序不能任意颠倒
  • 二叉树的五种基本形态
    在这里插入图片描述
  • 二叉树的性质
    • 在二叉树的第i层至多有2i-1个结点
    • 深度为k的二叉树至多有2k-1个结点
    • 对任何一棵二叉树T,如果其叶子结点数为n0,度为2的结点数为n2,则n0=n2+1
      证明:设度为1的结点数为n1
      首先计算结点数:得到n = n0+n1+n2
      再计算边数,边数等于结点数-1:n-1 = 2n2+n1
      两个式子联立得到n0=n2+1
  • 满二叉树:一棵深度为k且有2k-1个节点的二叉树称为满二叉树
  • 完全二叉树:深度为k的,有n个结点的二叉树,当且仅当每个结点都与深度为k的满二叉树中编号从1至n的结点一一对应时,称之为完全二叉树。
  • 完全二叉树的性质:
    • 具有n个节点的完全二叉树的深度为⌊log2n⌋+1
      证明:
      设完全二叉树的深度为k
      则结点的范围是2k-1 <= n < 2k
      则k-1 < log2n <k
      得到 k = ⌊log2n⌋+1
    • 如果对一棵有n个结点的完全二叉树的结点按层序编号,则对任一结点有:
      (1)如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲PARENT(i)是结点⌊i/2⌋。
      (2)如果2i>n,则结点i无左孩子;否则其左孩子为2i
      (3)如果2i+1>n,则结点i无右孩子;否则其右孩子为2i+1
      在这里插入图片描述
  • 三种二叉树的示例
    在这里插入图片描述
  • 二叉树的顺序存储结构
    只适合完全二叉树, 否则会造成大量的空间浪费。
    在这里插入图片描述
#define MAX_TREE_SIZE 100
typedef TElemType SqBiTree[MAX_TREE_SIZE];  
SqlBiTree bt;
  • 二叉树的二叉链表存储表示
typdef struct BiTNode
{
	TElemType data;
	struct BiTNode *lchild,*rchild; 
}BiTNode,*BiTree;

3.遍历二叉树和搜索二叉树

(1)遍历二叉树

  • 三种遍历二叉树的方法
    在这里插入图片描述
  • 先序遍历二叉树
void PreOrderTraverse(BiTree T)
{
	//递归终点
	if(!T)  return;
 	printf(T->data);
	PreOrderTraverse(T->lchild);
	PreOrderTraverse(T->rchild);
}
  • 中序遍历二叉树
void PreOrderTraverse(BiTree T)
{
	//递归终点
	if(!T)  return;
	PreOrderTraverse(T->lchild);
	printf(T->data);
	PreOrderTraverse(T->rchild);
}
  • 后序遍历二叉树
void PreOrderTraverse(BiTree T)
{
	//递归终点
	if(!T)  return;
	PreOrderTraverse(T->lchild);
	PreOrderTraverse(T->rchild);
	printf(T->data);
}
  • 遍历二叉树的算法的基本操作是访问结点,则不论按哪一种次序进行遍历,对含n个结点的二叉树,其时间复杂度为O(n)。所需辅助空间为遍历过程中栈的最大容量,即树的深度,最坏情况下为n,则空间复杂度为O(n)

(2)线索二叉树

在这里插入图片描述

  • 以上述结构构成的二叉链表作为二叉树的存储结构,叫做线索链表,其中指向结点前驱和后继指针,叫做线索。加上线索的二叉树称之为线索二叉树。对二叉树以某种次序遍历使其变为线索二叉树的过程叫做线索化
  • 在线索树上进行遍历,只要先找到序列中的第一个结点,然后依次找结点后继直至后继为空时而止。
  • 在中序线索二叉树上遍历二叉树,虽时间复杂度亦为O(n),但常数因子要比上节讨论的算法要小,且不需要设栈。因此,若在某程序中所有二叉树需经常遍历或者查找结点在遍历所得线性序列中的前驱和后继,应采用线索链表作存储结构
  • 二叉树的二叉线索存储表示
typedef enum PointerTag{Link,Thread};
typedef struct BiThrNode
{
	TElemType data;
	struct BiThrNode *lchild,*rchild;
	PointerTag LTag,RTag;
}BiThrNode,*BiThrTree;
  • 为方便起见,仿照线性表的存储结构,在二叉树的线索链表上也添加一个头结点,并令其lchild域的指针指向二叉树的根结点*,其rchild域的指针指向中序遍历时访问的最后一个结点;反之,令二叉树中序序列中的第一个结点的lchild域指针和最后一个结点rchild域的指针均指向头结点**。这好比为二叉树建立了一个双向线索链表,即可从第一个结点起顺后继进行遍历,也可以从最后一个结点起顺前驱进行遍历
  • 二叉树的线索化
BiThrNode* pre;

void InThreading(BiThrTree p)
{
	if(!p)  return;
	InThreading(p->lchild);
	if(!p->lchild)
	{
		p->LTag = Thread;
		p->lchild = pre;
	}
	if(!pre->rchild)
	{
		pre->RTag = Thread;
		pre->rchild = p;
	}
	pre = p;
	InThreading(p->rchild);
}

bool InOrderThreading(BiThrTree &Thrt,BiThrTree T)
{
	Thrt = (BiThrTree)malloc(sizeof(BiThrNode));
	if(!Thrt)  return false;
	//建立头结点
	Thrt->LTag = Link; 
	Thrt->RTag = Thread;
	Thrt->rchild = Thrt  //右指针回指
	if(!T) Thrt->lchild = Thrt;  //若二叉树为空,则左指针回指
	else
	{
		Thrt->lchild=T; pre = Thrt;
		InThreadinng(T);
		//最后一个结点线索化
		pre->rchild = Thrt;
		pre->RTag = Thread;
		Thrt->rchild = pre;
	}
	return OK;
}
  • 线索二叉树遍历
bool InOrderTraverse_Thr(BiThrTree T)
{
	BiThrNode* p = T->lchild;
	while(p!=T)
	{
		while(p->LTag==Link) 
			p = p->lchild;
		printf(p->data);
		while(p->RTag==Thread && p->rchild!=T)
		{	
			p = p->rchild;
			printf(p->data);
		}
		p = p->rchild;
	}
	return true;
}

4.树和森林

(1)树的存储结构

  • 双亲表示法:假设以一组连续空间存储树的结点,同时在每个结点中附设一个指示器指示其双亲结点在数组中的位置(缺点:这种表示法,求结点的孩子时需要遍历整个结构)
#define MAX_TREE_SIZE 100
typedef struct PTNode
{
	TElemType data;
	int parent;
}PTNode;
typedef struct
{
	PNode nodes[MAX_TREE_SIZE];
	int r,n;   //根的位置和结点数
}PTree;
  • 孩子表示法:由于树中每个结点可能有多棵子树, 则可用多重链表,即每个结点有多个指针域,其中每个指针指向一棵子树的根结点(又不利于找双亲的操作)
typedef struct CTNode
{
	int child;
	struct CTNode* next;
}*ChildPtr;
typedef struct
{
	TElemType data;
	ChildPtr firstchild;  //孩子链表头指针
}CTBox;
typedef struct
{
	CTBox nodes[MAX_TREE_SIZE];
	int n,r; 
}CTree;
  • 孩子兄弟表示法:又称二叉树表示法,或二叉链表表示法。即以二叉链表作树的存储结构。链表中结点的两个域分别指向该结点的第一个孩子结点和下一个兄弟结点(最合适的存储方法,便于实现各种个树的操作
typedef struct CSNode
{	
	ElemType data;
	struct CSNode* firstchild,*nextsibling;
}CSNode,*CSTree;
  • 静态表示法
typedef struct
{
	ElemType data;  
	vector<int> child;  //存放子结点的索引
}Tree[nmax];

(2)森林与二叉树的转换

  • 给定一树,可以找唯一的一个二叉树与之对应,从物理结构来看,它们的二叉链表是相同的,只是解释不同而已。
    在这里插入图片描述
  • 若把森林中第二棵树的根结点看成是第一棵树的根结点的兄弟,同样可导出森林和二叉树的关系
    在这里插入图片描述

(3)数和森林的遍历

  • 先根遍历树和后根遍历树,分别对应二叉树中的前序遍历和中序遍历

5.树与等价问题(并查集)

  • 等价关系和等价类的定义:
    在这里插入图片描述
  • 等价关系是现实世界中广泛存在的一种关系,许多应用问题可以归结为按给定的等价关系划分某集合为等价类,通常称这类问题为等价问题
  • 等价类划分的方法:
    在这里插入图片描述
  • 使用树型结构解决等价类的划分:
    以森林F = (T1,T2,…,Tn)表示集合S,森林中每一棵树表示S中的一个子集,树中每个结点表示子集中的一个成员。每个结点中含有一个指向其双亲的指针。实现并操作的时候,只要将一颗子集树的根指向另一棵子集树的根,即使用双亲表示法解决
  • 结构
typedef struct PTNode
{
	TElemType data;
	int parent;
}PTNode;
typedef struct
{
	PNode nodes[MAX_TREE_SIZE];
	int r,n;   //根的位置和结点数
}PTree;
typedef PTree MFSet;
  • 查找函数
int find_mfset(MFSet S,int i)
{
	if(i<1 || i>s.n)  return -1;
	int j = i;
	while(S.nodes[j].parent>0)
	{
		j = S.nodes[j].parent;
	}  
	return j;
}
  • 归并操作
//i,j都是根结点
bool merge_mfset(MFSet& S,int i,int j)
{
	if(i<1 || i>s.n || j<1 || j>s.n)  return false;
	s.nodes[i].parent  = j;
	return true;
}
  • 由于查找操作的时间复杂度是O(d),即形成的树的深度,所以深度应该是越小越好,应该对归并算法做出改进,每次让深度小的指向深度大的。为此应该相应地修改存储结构,令根结点的parent域存储子集中所含成员数目的负值(存负值是为了防止和其他节点的parent值弄混)
bool better_merge_mfset(MFSet& S,int i,int j)
{
	if(i<1 || i>s.n || j<1 || j>s.n)  return false;
	//如果si的结点数较少
	if(S.nodes[i].parent>S.nodes[j].parent)
	{
		S.node[j].parent += S.nodes[i].parent;
		S.node[i].parent = j;
	}
	else
	{
		S.node[i].parent += S.nodes[j].parent;
		S.node[j].parent = i;
	}
	return true;
}
  • 可以证明改进后的算法算出的集合树,其深度不超过⌊log2n⌋+1,即形成的是最多是完全二叉树。
  • 由此,利用修改后的算法算等价问题的时间复杂度为O(nlog2n)
  • 在实际运算中,我们又发现随着子集逐对合并,数的深度也越来越大,为了进一步减少确定元素的集合的时间,我们可以再改进查找算法,即当所查元素i不再树的第二层时,在算法中增加一个“压缩路径的功能”,即将所有从根到元素i路径上的元素都变成树根的孩子
int better_find_mfset(MFSet S,int i)
{
	if(i<1 || i>s.n)  return -1;
	int j = i;
	while(S.nodes[j].parent>0)
	{
		j = S.nodes[j].parent;
	}  
	for(int k=i;k!=j;k=t)
	{
		int t = S.node[k].parent;
		S.node[k].parent = j;
	}
	return j;
}

6.赫夫曼树及其应用

(1)最优二叉树(赫夫曼树)

  • 路径:从树中一个结点到另一个结点之间的分支构成这两个结点之间的路径
  • 路径长度:路径上的分支数目称作路径长度数的路径长度是从树根到每一结点的路径长度之和。
  • 带权路径长度:结点的带权路径长度为从该结点到树根之间的路径长度与结点上权的乘积。数的带权路径长度为树中所有叶子结点的带权路径长度之和,通常记做WPL。
  • 假设有n个权值,试构造一棵有n个叶子结点的二叉树,每个叶子结点带权为wi,则其中带权路径长度WPL最小的二叉树称做最优二叉树或赫夫曼树
  • 赫夫曼树要解决的问题就是条件分支问题,每个分支都有一个权值,即分支概率,赫夫曼树就是让概率大的放在前面判断,概率小的最后判断。
  • 构造哈夫曼树的方法:
    在这里插入图片描述

(2)赫夫曼编码

  • 目前,进行快速远距离通信的主要手段是电报,即将需传送的文字转换由二进制的字符组成的字符串。例如,假设需传送的电文为’A B A C C D A’,它只有4种字符,只需两个字符的串便可分辨。可以将A,B,C,D的编码分别为00,01,10和11
  • 当然,传送电文时,肯定希望总长尽可能地短,如果对每个字符设计长度不等的编码,且让电文中出现次数较多的字符采用尽可能短的编码,则传送电文的总长度便可减少。要注意的是,若要设计长短不等的编码,则必须是任一字符的编码都不是另一个字符的编码的前缀,这种编码称为前缀编码
  • 用二叉树设前缀编码:只需要约定左分支表示字符0,右分支表示字符1,得到的必为二进制前缀编码
  • 由于赫夫曼树中没有度为1的结点, 则一棵有n个叶子结点的赫夫曼树共有2n-1个结点,可以存储在一个大小为2n-1的一维数组中。如何选定结点结构?由于在构成赫夫曼树之后,为求编码需从叶子结点出发走一条从叶子到根的路径;而为译码需从根出发走一条从根到叶子的路径。则对每个结点而言,即需知双亲的信息,又需知孩子结点的信息。由此,设定下述存储结构
typedef struct
{
	unsigned weight;
	unsigned parent, lchild, rchild;
}HTNode,*HuffmanTree;
typedef char* *HuffmanCode;
  • 赫夫曼编码
void HuffmanCoding(HuffmanTree& HT, HuffmanCode& HC, int w[], int n)
{
	//求赫夫曼树
	if (n <= 1) return;
	int m = 2 * n - 1;  //结点数
	HT = (HuffmanTree)malloc(sizeof(HTNode) * m);
	int i = 0;
	for (; i < n; ++i)
		HT[i] = { w[i] , 0, 0, 0 };
	for (; i < m; ++i)
		HT[i] = { 0,0,0,0 };
	for (i = n; i < m; ++i)
	{
		int s1 = 0,s2 = 0;
		//在叶节点中选择parent为0且weight最小的两个结点
		Select(HT, i, s1, s2);
		HT[s1].parent = i;
		HT[s2].parent = i;
		HT[i].weight = HT[s1].weight + HT[s2].weight;
		HT[i].lchild = s1;
		HT[i].rchild = s2;
	}
	
	//求赫夫曼编码
	HC = (HuffmanCode)malloc(sizeof(char*) * n);
	stack<char> s[100];
	for (i = 0; i < n; i++)
	{
		int k = i;
		int pre = -1;  //存储上次结点
		while (HT[k].parent != 0)
		{
			if (pre != -1)
			{
				if (HT[k].lchild == pre)
					s[i].push('0');
				else
					s[i].push('1');
			}
			pre = k;
			k = HT[k].parent;
		}
		//根结点
		if (HT[k].lchild == pre)
			s[i].push('0');
		else
			s[i].push('1');
		//存入到HC中
		int pc = 0;
		char tmp[100] = "";
		while (!s[i].empty())
		{
			tmp[pc]= s[i].top();
			++pc;
			s[i].pop();
		}
		tmp[pc] = '\0';
		HC[i] = (char*)malloc(sizeof(char) * n);
		strcpy(HC[i],tmp);
	}
}

7.回溯法与树的遍历

  • 在程序设计中,有相当一类求一组解、或求全部解或求最优解的问题,它不是根据某种确定的计算法则,而是利用试探和回溯的搜索技术求解。回溯法也是设计递归过程的一种重要方法,它的求解过程实质上是一个先序遍历一棵“状态树”的过程
  • 求含n个元素的幂集(一个集合的幂集是由该集合的子集为元素组成的集合,包括空集)
    求幂集的过程可以看做是对每个元素进行取舍之后形成的状态树的叶子结点
    在这里插入图片描述
void GetPowerSet(int i,List A,List& B)
{
	if(i>ListLength(A)) Output(B);
	else
	{
		int x =0;
		GetElem(A,i,x);
		k = ListLength(B);
		ListInsert(B,k+1,x);
		GetPowerSet(i+1,A,B);
		ListDelete(B,k+1,x);
		GetPowerSet(i+1,A,B);
	}
}
  • 4皇后问题:四个皇后不能在同一行同一列同一对角线
    在这里插入图片描述
bool suit(int i, int j,int a[])
{
	for (int row = 1; row <= i-1; ++row)
	{
		if (a[row] == j || abs(row-i) == abs(a[row]-j))
		{
			return false;
		}
	}
	return true;
}
void Trial(int i,int a[], int n)
{
	if (i > n) 
	{  //输出棋盘的当前布局   
		for (int i = 1; i <= n; ++i)
		{
			cout << "(" <<i <<","<< a[i] <<")"<<" ";
		}
		cout << endl;
		return;
	}
	else
	{
		for (int j = 1; j <= n; ++j)
		{
			if (suit(i, j, a))
			{
				a[i] = j;
				Trial(i + 1, a, n);
				a[i] = 0;
			}
		}
	}
}

8.数的计数

  • 重要结论:
    在这里插入图片描述
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值