树的定义:树是n(n>=0)个结点的有限集合,n=0时,称为空树。在任意一棵非空树中应满足:①有且仅有一个特定的称为根的结点。②当n大于1时,其余结点可以分为m(m>0)个互不相交的有限集合,其中每一个集合本身又是一棵树,并且称为根节点的子树。树是一种递归的数据结构。
二叉树的定义:二叉树是n(n>=0)个结点的有限集合:①n=0时,为空二叉树。②n>0时,由一个根结点和两个互不相交的被称为根的左子树和右子树组成。右子树和左子树分别是一棵二叉树。二叉树是有序树,若将其左右子树颠倒,则成为另一棵不同的二叉树。
二叉树和度为2的有序树的区别:①度为2的树至少有三个结点,而二叉树可以为空。②度为2的有序树的孩子结点的左右次序是相对的,而二叉树的孩子结点的左右次序是绝对的。
二叉树的遍历是指通过一定的顺序访问二叉树的所有结点。遍历方法一般有四种:先序遍历、中序遍历、后序遍历以及层次遍历,其中前三种一般用深度优先搜索(DFS)实现,而层次遍历一般用广度优先搜索(BFS)实现。另外前三种也可以通过递归实现。
先序:
void preOrder1(BinTree *root) //递归先序遍历
{
if(root!=NULL)
{
cout<<root->data<<" ";
preOrder1(root->lchild);
preOrder1(root->rchild);
}
}
中序:
void inOrder1(BinTree *root) //递归中序遍历
{
if(root!=NULL)
{
inOrder1(root->lchild);
cout<<root->data<<" ";
inOrder1(root->rchild);
}
}
后序:
void postOrder1(BinTree *root) //递归后序遍历
{
if(root!=NULL)
{
postOrder1(root->lchild);
postOrder1(root->rchild);
cout<<root->data<<" ";
}
}
先序、中序、后序是通过栈实现非递归。
层次遍历是通过队列实现
void preOrder2(BinTree *root) //非递归先序遍历
{
stack<BinTree*> s;
BinTree *p=root;
while(p!=NULL||!s.empty())
{
while(p!=NULL)
{
cout<<p->data<<" ";
s.push(p);
p=p->lchild;
}
if(!s.empty())
{
p=s.top();
s.pop();
p=p->rchild;
}
}
}
void inOrder2(BinTree *root) //非递归中序遍历
{
stack<BinTree*> s;
BinTree *p=root;
while(p!=NULL||!s.empty())
{
while(p!=NULL)
{
s.push(p);
p=p->lchild;
}
if(!s.empty())
{
p=s.top();
cout<<p->data<<" ";
s.pop();
p=p->rchild;
}
}
}
这里有两种思路。
第一种是要保证根结点在左孩子和右孩子访问之后才能访问,因此对于任一结点P,先将其入栈。如果P不存在左孩子和右孩子,则可以直接访问它;或者P存在左孩子或者右孩子,但是其左孩子和右孩子都已被访问过了,则同样可以直接访问该结点。若非上述两种情况,则将P的右孩子和左孩子依次入栈,这样就保证了每次取栈顶元素的时候,左孩子在右孩子前面被访问,左孩子和右孩子都在根结点前面被访问。
void postOrder3(BinTree *root) //非递归后序遍历
{
stack<BinTree*> s;
BinTree *cur; //当前结点
BinTree *pre=NULL; //前一次访问的结点
s.push(root);
while(!s.empty())
{
cur=s.top();
if((cur->lchild==NULL&&cur->rchild==NULL)||
(pre!=NULL&&(pre==cur->lchild||pre==cur->rchild)))
{
cout<<cur->data<<" "; //如果当前结点没有孩子结点或者孩子节点都已被访问过
s.pop();
pre=cur;
}
else
{
if(cur->rchild!=NULL)
s.push(cur->rchild);
if(cur->lchild!=NULL)
s.push(cur->lchild);
}
}
}
第二种思路是对于任一结点P,将其入栈,然后沿其左子树一直往下搜索,直到搜索到没有左孩子的结点,此时该结点出现在栈顶,但是此时不能将其出栈并访问,因此其右孩子还为被访问。所以接下来按照相同的规则对其右子树进行相同的处理,当访问完其右孩子时,该结点又出现在栈顶,此时可以将其出栈并访问。这样就保证了正确的访问顺序。可以看出,在这个过程中,每个结点都两次出现在栈顶,只有在第二次出现在栈顶时,才能访问它。因此需要多设置一个变量标识该结点是否是第一次出现在栈顶。
void postOrder2(BinTree *root) //非递归后序遍历
{
stack<BTNode*> s;
BinTree *p=root;
BTNode *temp;
while(p!=NULL||!s.empty())
{
while(p!=NULL) //沿左子树一直往下搜索,直至出现没有左子树的结点
{
BTNode *btn=(BTNode *)malloc(sizeof(BTNode));
btn->btnode=p;
btn->isFirst=true;
s.push(btn);
p=p->lchild;
}
if(!s.empty())
{
temp=s.top();
s.pop();
if(temp->isFirst==true) //表示是第一次出现在栈顶
{
temp->isFirst=false;
s.push(temp);
p=temp->btnode->rchild;
}
else //第二次出现在栈顶
{
cout<<temp->btnode->data<<" ";
p=NULL;
}
}
}
}
void LevelorderTraversal ( BinTree BT )
{
Queue Q;
BinTree T;
if ( !BT ) return; /* 若是空树则直接返回 */
Q = CreatQueue(); /* 创建空队列Q */
AddQ( Q, BT );
while ( !IsEmpty(Q) ) {
T = DeleteQ( Q );
printf("%d ", T->Data); /* 访问取出队列的结点 */
if ( T->Left ) AddQ( Q, T->Left );
if ( T->Right ) AddQ( Q, T->Right );
}
}
二叉树还原是指根据二叉树的遍历序列构造一棵二叉树。还原二叉树有一个结论:中序序列可以与先序序列、后序序列、层次序列中的任意一个来构建唯一的二叉树,而后三者两两搭配或是三个一起都无法构建唯一一棵二叉树。原因是,先序、后序、层次均是提供根节点,作用是相同的,都必须由中序序列来区分出左右子树。
//先序 中序建树 返回根节点 输出后序
Node preIn(int preL,int preH, int inL,int inH)
{
if(preL>preH)return NULL; //先序序列为空,到达递归边界
Node root = new node;
root->data = pre[preL];
int k=inL;
for(k=inL;k<=inH;k++)
{
if(in[k]==pre[preL])
{
break;
}
}
//左子树先序是[preL+1,preL+k-inL],中序是[inL,k-1]
root->left = preIn(preL+1,preL+k-inL,inL,k-1);
//右子树先序是[preL+k-inL+1,preH],中序是[k+1,inH].
root->right = preIn(preL+k-inL+1,preH,k+1,inH);
return root;
}
int num=0;
void postPrint(Node root)
{
if(root==NULL)return;
postPrint(root->left);
postPrint(root->right);
post[num++]=root->data;
return;
}
//后序 中序建树 返回根节点 输出先序
Node postIn(int inL,int inH,int postL,int postH)
{
if(postL>postH)return NULL;
Node root = new node;
root->data = post[postH];
int k= inL;
for(k = inL;k<=inH;k++)
{
if(in[k]==post[postH])
{
break;
}
}
//左子树 中序[inL,k-1], 后序[postl,postL+k-inL-1]
root->left = postIn(inL,k-1,postL,postL+k-inL-1);
//右子树 中序[k+1,inH] , 后序[postL+k-inL,postH-1]
root->right = postIn(k+1,inH,postL+k-inL,postH-1);
return root;
}
void prePrint(Node root) //num initiate 0
{
if(root==NULL)return;
pre[num++]=root->data;
prePrint(root->left);
prePrint(root->right);
return;
}
//层级 中序建树 返回根节点
Node levelIn(vector<int>layer,int inL,int inH)
{
if(layer.size()==0)return NULL;
Node root = new node;
root->data = layer[0];
int k;
for(k=inL;k<=inH;k++)
{
if(in[k]==layer[0])
{
break;
}
}
vector<int>layerLeft;
vector<int>layerRight;
for(int i=1;i<layer.size();i++)
{
bool isLeft = false;
for(int j=inL;j<k;j++)
{
if(layer[i]==in[j])
{
isLeft = true;
break;
}
}
if(isLeft)
{
layerLeft.push_back(layer[i]);
}
else
{
layerRight.push_back(layer[i]);
}
}
root->left = levelIn(layerLeft,inL,k-1);
root->right = levelIn(layerRight,k+1,inH);
return root;
}
二叉树的应用主要有 二叉查找树(BST),平衡二叉树(AVL),哈夫曼树,堆和并查集。
二叉查找树(Binary Search Tree),是一种特殊的二叉树,又称为排序二叉树、二叉搜索树、二叉排序树。其递归定义如下:①要么二叉查找树是一棵空树。②要么二叉查找树由根结点、左子树、右子树组成,其中左子树和右子树均是二叉查找树,且左子树上的所有结点的数据域均小于或等于根结点的数据域,右子树上的所有结点的数据域均大于根结点的数据域。
其中需要注意的是,即便是一组相同的数字,如果插入它们的顺序不同,最后生成的二叉查找树也不相同。
另外,二叉查找树有一个实用的性质:对二叉查找树进行中序遍历,遍历的结果是有序的。
平衡二叉树是由前苏联两位数学家提出,也称为AVL树。AVL树仍是一棵二叉查找树,只是在其基础上增加了“平衡”的要求。所谓平衡是指,对AVL树的任意结点来说,其左子树和右子树的高度之差的绝对值不超过1,其中左右子树的高度差称为该结点的平衡因子。平衡因子可以借助子树的高度间接求出。
AVL树插入时需要调整结点以满足平衡,具体调整情况如下表(BF表示平衡因子):
树形 | 判定条件 | 调整方法 |
---|---|---|
LL | BF(root)=2,BF(root->lchild)=1 | 对root进行右旋 |
LR | BF(root)=2,BF(root->lchild)=-1 | 先对root->lchild进行左旋,再对root进行右旋 |
RR | BF(root)=-2,BF(root->rchild)=-1 | 对root进行左旋 |
RL | BF(root)=-2,BF(root->rchild)=1 | 先对root->rchild进行右旋,再对root进行左旋 |
并查集是一种维护集合的数据结构,支持以下两个操作:①合并,合并两个集合。②查找:判断两个元素是否在一个集合。
并查集的实现,是通过一个数组实现的,即int father[N]; 其中father[i]表示元素i的父亲结点,而父亲结点本身也是这个集合内的元素。如果father[i]==i,则说明元素i是该集合的根结点,但对同一个集合来说,只能存在一个根结点,且将其作为所属集合的标识。
并查集的一个性质,同一集合中一定不会产生环,即并查集产生的每一个集合都是一棵树。
堆是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子结点的值,那么称这样的堆为大顶堆,这时每个结点的值都是以它为根结点的子树的最大值;如果父亲结点的值小于或等于孩子结点的值,那么称这样的堆为小顶堆,这时每个结点的值都是以它为根结点的子树的最小值。
从树根结点到任意结点的路径长度(经过的边数)与该结点上权值的乘积称为该结点的带权路径长度。树中所有叶结点的带权路径长度之和称为该树的带权路径长度(WPL)。
在含有N个带权叶子节点的二叉树中,其中带权路径长度最小的二叉树称为哈夫曼树,也称为最优二叉树。