数据结构——二叉树总结

数据结构—二叉树总结

  • 写在前面
  • 二叉树遍历
    • 递归实现先、中、后序遍历
    • 非递归遍历
      • 先序非递归
      • 中序非递归
      • 后序非递归
      • 层次遍历
  • 二叉树还原
    • 先序中序建树
    • 后序中序建树
    • 层次中序建树
  • 二叉树应用
    • 二叉查找树
    • 平衡二叉树(AVL树)
    • 并查集
    • 哈夫曼树
  • 参考资料


写在前面

  1. 树的定义:树是n(n>=0)个结点的有限集合,n=0时,称为空树。在任意一棵非空树中应满足:①有且仅有一个特定的称为的结点。②当n大于1时,其余结点可以分为m(m>0)个互不相交的有限集合,其中每一个集合本身又是一棵树,并且称为根节点的子树。树是一种递归的数据结构。

  2. 二叉树的定义:二叉树是n(n>=0)个结点的有限集合:①n=0时,为空二叉树。②n>0时,由一个根结点和两个互不相交的被称为根的左子树和右子树组成。右子树和左子树分别是一棵二叉树。二叉树是有序树,若将其左右子树颠倒,则成为另一棵不同的二叉树。

  3. 二叉树和度为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树仍是一棵二叉查找树,只是在其基础上增加了“平衡”的要求。所谓平衡是指,对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个带权叶子节点的二叉树中,其中带权路径长度最小的二叉树称为哈夫曼树,也称为最优二叉树


参考资料

  • 《数据结构考研复习指导》 王道论坛组编 电子工业出版社
  • (二叉树的非递归遍历,作者:海子)https://www.cnblogs.com/dolphin0520/archive/2011/08/25/2153720.html
  • 《算法笔记》 胡凡 曾磊主编 机械工业出版社

你可能感兴趣的:(数据结构DS)