树论_二叉树的定义

33 阅读7分钟

二叉树可以处理每个阶段都是两种结果的情形,如:大和小,0和1,真和假等

二叉树的定义

二叉树特点:

  • 每个节点最多有两颗子树
  • 是有序树,即左子树和右子树有次序

特殊二叉树

斜二叉树

特殊的表现为线性表

满二叉树

所有分支节点都有左子树和右子树,所有叶子节点在同一层

完全二叉树

适合用顺序结构即数组表示

叶子节点只会出现在最下面两层

若有n个节点,其深度为[log2 n]+1,[]表示取整

若从1开始编号,对于节点i,若左子树存在,左儿子序号(下标)为==i* 2==,若右子树存在,右儿子序号(下标)为==i* 2==,若节点存在双亲,则双亲为==i/2==(需要取整)

若从0开始编号,则分别为==i* 2+1==,==i* 2+2==,==(i-1)/2==

二叉树的普适性质

  1. 二叉树第i层最多有2^(i-1)个节点
  2. 深度为k的二叉树最多有2^k -1个节点(证明:等比数列求和)
  3. 二叉树的边数等于节点数-1
  4. n0:度为0的节点,n1:度为1的节点,n2:度为2的节点,则n0=n2+1(证明:n0+n1+n2-1=0* n0+1 * n1+2* n2)

二叉树的顺序存储结构

从上到下,从左到右将二叉树的节点放在数组中,适合完全二叉树,节点与其双亲,儿子的下标对应关系与完全二叉树相同:[[2 二叉树的定义和性质#完全二叉树]]

二叉树的链式存储结构

用二叉链表储存,每个节点含一个data域,两个指针域,分别是左儿子left,右儿子right,根据需要可增加双亲域parent

typdef int DataType

typedef struct treenode_{
	DataType data;
	struct treenode_ *left,*right;
}TreeNode;

左儿子或右儿子不存在就设置为NULL

二叉树的遍历

三种遍历经过的路线一样,只是每个节点的访问时机不同 一般来说默认先遍历左子树再遍历右子树,但根据实际情况不同可以先遍历右子树再遍历左子树

先序遍历(递归

先访问根节点,再递归遍历左子树,再递归遍历右子树

递归算法:

void PreOrderTraversal(TreeNode* t){
	if(t){
		cout<<t->data;
		PreOrderTraversal(t->left);
		PreOrderTraversal(t->right);
	}
}

中序遍历(递归

先递归遍历左子树,再访问根节点,再递归遍历右子树

递归算法:

void InOrderTraversal(TreeNode* t){
	if(t){
		PreOrderTraversal(t->left);
		cout<<t->data;
		PreOrderTraversal(t->right);
	}
}

后序遍历(递归

先递归遍历左子树,再递归遍历右子树,再访问根节点

递归算法:

void PostOrderTraversal(TreeNode* t){
	if(t){
		PreOrderTraversal(t->left);
		PreOrderTraversal(t->right);
		cout<<t->data;
	}
}

中序遍历(非递归

基本思想:利用指示==指针p==和==stack==模拟中序遍历,基于指示指针或stack不为空的大循环,先让指示指针p沿着左链走到底,边走边将指示过的节点push,然后若stack不为空就pop并且访问,再让p指向该pop出来的节点的右儿子

void InOrderTraversal(TreeNode* t){
	TreeNode* p=t;    //指示指针
	stack<TreeNode*> s;    //堆栈
	while(p||!s.empty()){    //大循环
		while(p){    //让p沿着左链走到底,并push
			s.push(p);
			p=p->left;
		}
		if(!s.empty()){    //stack不为空就pop并访问
			p=s.top();
			s.pop();
			cout<<p->data;    //pop并访问
			p=p->right;    //访问右儿子
		}
	}
}

迁移:中序遍历是==出栈==时访问,根据先序遍历访问节点的时机不同,先序遍历非递归算法是在==入栈==时访问

先序遍历(非递归

void InOrderTraversal(TreeNode* t){
	TreeNode* p=t;    //指示指针
	stack<TreeNode*> s;    //创建堆栈
	while(p||!s.empty()){    //大循环
		while(p){    //让p沿着左链走到底,并push
			s.push(p);
			cout<<p->data;    //push并访问
			p=p->left;
		}
		if(!s.empty()){    //stack不为空就pop并访问
			p=s.top();
			s.pop();
			p=p->right;    //访问右儿子
		}
	}
}

层序遍历

前三种都是==Depth First Search==,层序遍历是==Breadth First Search==

基本思想:利用queue模拟层序遍历,每次迭代,先pop,再将pop出来的节点的所有子节点push,当队列为空停止迭代,进入迭代前先将根节点push

void LevelOrderTraversal(TreeNode* t){
	queue<TreeNode*> q;    //创建队列
	q.push(t);
	TreeNode* p;    //用于接收pop出来的节点
	q.push(t);    //先将根节点入队
	while(!q.empty()){
		p=q.front();
		q.pop();
		cout<<p->data;    //出队并访问
		if(p->left)q.push(p->left);    //将左儿子入队
		if(p->right)q.push(p->right);    //将右儿子入队
	}
}

推广:若要获得每层最左边的节点(假设将每层的最左边的节点储存在vector中),可用size记录queue中全部是同一层的节点时的节点个数,然后通过size次循环把queue中的节点pop并push其子节点

void LevelOrderTraversal(TreeNode* t){
	queue<TreeNode*> q;    //创建队列
	q.push(t);
	TreeNode* p;    //用于接收pop出来的节点
	q.push(t);    //先将根节点入队
	vector<TreeNode*> v;
	while(!q.empty()){
		int size=q.size();
		v.push_back(q.front());    //此时队列中的第一个节点就是最左节点
		while(size--){
			p=q.front();
			q.pop();
			cout<<p->data;    //出队并访问
			if(p->left)q.push(p->left);    //将左儿子入队
			if(p->right)q.push(p->right);    //将右儿子入队
		}
	}
}

再推广:若要获得每层最右边的节点,就先push右子树,再push左子树

推导遍历结果

根据中序遍历和先序遍历或中序遍历和后序遍历和推导出二叉树或另一种遍历

前提:必须有中序遍历

基本思想:递归处理两个遍历序列相对应的一段

先假定遍历序列都储存在数组中

根据先序和中序推二叉树

  1. 关键参数:

    对于待处理的两段相对应的序列段,用==preL==记录先序遍历待处理序列段的左端(起始)下标,用==inL==记录中序遍历待处理序列段的左端(起始)下标,用==len==记录待处理序列段的长度

    可推出先序遍历待处理序列右端下标为==preL+n-1==,记录为==preR==,中序遍历待处理序列右端下标为==inL+n-1==,记录为==inR==

  2. 找中序遍历中的根节点:

    对于先序遍历序列段的第一个元素(左端),该元素就是根节点,在中序遍历序列段中通过遍历找到这个元素并记录该元素的下标,记录为==i==)

  3. 找左子树和右子树的先序遍历序列段和中序遍历序列段:

    对于中序遍历,根据根节点的下标i和inL和len可以确定左子树序列长度为==i-inL==,记录为==leftLen==,右子树序列长度为==inR-i==,记录为==rightLen==

    可推出中序遍历左子树的待处理序列左端为==inL==,右子树的待处理序列左端为==i+1==

    对于先序遍历,可推出先序遍历左子树的待处理序列左端为==preL+1==,右子树的待处理序列左端为==PreL+leftLen+1==

  4. 递归处理左子树序列段和右子树序列段,边界情况(len == 0)时返回NULL

目的是建树,所以在每次找到根节点后建一个节点,赋为对应的data

TreeNode* solveSequence(preL,inL,len){
	if(len==0)return NULL;    //边界情况
	int preR=preL+len-1;
	int inR=inR+len-1;
	DataType root=preOrder[preL];
	TreeNode* t=new TreeNode;    //建根节点
	t->data=root;
	int i;
	for(i=inL;i<=inR;i++){
		if(inOrder[i]==root)break;
	}
	int leftLen=i-inL;
	int rightLen=inR-i;
	t->left=solveSequence(preL+1,inL,leftLen);
	t->right=solveSequence(preL+leftLen+1,i+1,rightLen);
	return t;
}

根据先序和中序推后序

假设将得到的后序遍历序列储存在数组postOrder[]中

和根据先序遍历和中序遍历推二叉树不同的是,推后序遍历只需要将找到的root放到后序对应的序列段的右端(最后一个元素)

void solveSequence(preL,inL,postL,len){
	if(len==0)return;    //边界情况
	int preR=preL+len-1;
	int inR=inR+len-1;
	int postR=postL+len-1;
	DataType root=preOrder[preL];
	postOrder[postR]=root;
	int i;
	for(i=inL;i<=inR;i++){
		if(inOrder[i]==root)break;
	}
	int leftLen=i-inL;
	int rightLen=inR-i;
	t->left=solveSequence(preL+1,inL,postL,leftLen);
	t->right=solveSequence(preL+leftLen+1,i+1,postL+leftLen+1,rightLen);
	return t;
}

推广

由后序遍历和中序遍历推二叉树或先序遍历思路相同,只是根节点在后序遍历的右端