第 15 题:谈谈你对回流和重绘的理解?

Python 实现十大经典排序算法 artipub

  返回  

树与二叉树

2021/7/21 13:45:36 浏览:

 

目录

1.树的定义

2.树的基本概念

2.1结点

2.2度

2.3高度和深度

2.4森林

3.树的抽象数据类型及存储结构

3.1抽象数据类型

3.2存储结构

4.二叉树

4.1二叉树的定义

4.2二叉树的性质

5.二叉树的实现

6.二叉树的遍历

6.1层次遍历

6.2先序遍历

6.3中序遍历

6.4后序遍历



       我们知道,对于向量vector和列表list属于线性结构。vector实质是由动态数组进行封装,而list内部结构是一个链表。vector的查找效率非常高,最快可以做到llogn级别(二分查找),而插入和删除效率低,都需要线性的时间。然而列表list恰恰相反,插入删除效率高,查找的效率低。有没有一种数据结构兼有二者优点呢?

     树这种结构恰好满足这种要求。

1.树的定义

       在数学中,树结构被定义为一种特殊的图,也就是说它可以定义为一组元素的二元关系。元素与元素之间存在某种关系,我们就引入一条边,没有则不引入。所有能够定义二元关系的元素称为顶点,二元关系称为边。树是n(n>=0)个节点的有限集合,选取其中一个结点作为根结点,其他的m个结点又是一个互不相交的有限集,又可以单独作为一棵树。其实,我们可以把树这种结构看成一个二维链表。如图:

       我们取m1作为根节点,m1和m2,m3有二元关系,所以他们之间存在一条边。同理可得m3~m6.但是,要构成一棵树,要满足根结点外的结点不存在交集,也就是无法构成环,形成回路。如:

2.树的基本概念

2.1结点

根节点:没有双亲结点的结点。如图1-1m1

内结点:除根结点以外的结点。如m2~m6

双亲结点:孩子结点的直接祖先。m4,m5的直接祖先是m2,m6直接祖先是m3.所以m2是m4,m5的 双亲,同理可得m3。

孩子:结点的子树的根节点称为该节点的孩子结点。m1结点的孩子结点为m2和m3

兄弟:由同一个双亲产生的孩子结点互为兄弟结点。例如,m2与m3互为兄弟结点,他们的双亲结点为m1。
子孙:有同一个根节点的子树的任意结点称为子孙。

2.2度

        一个树的结点包含一个数据元素及若干个指向其子树的分支。该结点拥有的子树的个数称为该结点的度。

2.3高度和深度

        结点的层次从根结点开始定义,根为第一层。根的孩子为第二层。树中结点的最大层为数的深度。树的高度和树的深度定义角度不太一样,树的深度自上而下定义,树的高度自下而上定义,但是,本质上描述树的层次时区别不大。

2.4森林

森林是m棵互不相交的树的集合。

3.树的抽象数据类型及存储结构

3.1抽象数据类型

            结点                             功能
         root()  根结点
         parent()父结点
        firstChild()长子
       nextSibing()兄弟
       insert(i,e)将e作为第i个孩子插入
      remove(i)删除第i个孩子
      traverse()遍历

3.2存储结构

3.2.1双亲表示法

        对于一棵树而言,我们可以构造一张表记录每个元素的信息。这张表里面每个结点除了知道自己的结点信息以外还知道双亲的位置。

rankdataparent
0R-1
1A0
2B0
3C0
4D1
5E1
6F3
7G6
8H6
9I6
#define MAX_TREE_SIZE  100
typedef int TElemType;
typedef struct PTNode
{
TElemType data;
int parent;
} PTNode;
typedef struct
{
PTNode nodes[MAX_TREE_SIZE];
int r,n;
}PTree;

缺点:这样的存储结构,根据parent指针很容易找到双亲结点,时间复杂度为O(1),当parent值为-1时,找到根结点。但是如果想要知道孩子结点是什么,需要遍历整个结构。

3.2.2孩子表示法

方案一:

对于一棵树,可能有多个孩子,我们考虑在一个结点有多个指针域指向孩子结点。如图所示:

       这种方法缺点是结点空间浪费比较大。毕竟像E,G,H,I,J这样的结点,没有孩子,但是分配的的时候必须分配空间。

方案二:

      这种方案可以节省空指针带来的空间浪费如图:

但是这种结构在运算结点的度的时候效率比较低,需要遍历。

方案三

第三种方案集合了方案一方案二的优点,既能节省空间,也能使结构较为完整。孩子结点作为一个单链表。如图:

 3.2.3孩子兄弟表示法

        任意一棵树,它结点第一个孩子如果存在就是唯一的。如果孩子的右兄弟存在的话,也是唯一的。所以,我们设置两个指针,分别指向孩子结点和右兄弟结点。

 这样做其实就是将一颗数转换成了一棵二叉树。

4.二叉树

4.1二叉树的定义

        二叉树是n个结点构成的集合。该树的结点数的度数不超过2。所以,每个结点最多有两个孩子。我们以左右孩子加以区分。
完全二叉树:一棵树如果按照层次进行排序。如果编号为i的结点,与满二叉树中的i结点的位置相同,则称为完全二叉树。

满二叉树:一颗二叉树,所有分支结点都存在左右子树。则成为满二叉树。满二叉树是一种特殊的完全二叉树。

斜树:只含有左子树或只含有右子树的二叉树.

4.2二叉树的性质

      1.一个深度为k的二叉树,该数第k层的结点数不超过2^(k-1)(k>=1)。

      2.一深度为k的二叉树,至多有2^k-1个结点。(k>=1)

      3.对于一颗二叉树,叶子结点个数为n0,度为2的结点数为n2.则度为0点数n0=n2+1; 

5.二叉树的实现

       一颗二叉树的结点需要记录如图下所示的信息。

       结点parent ,lChild,rChild的引用分别指向双亲和左右孩子结点。结点中也需要存储一些信息如高度height.在左式堆中的npl指标。在红黑树中的color指标。

定义如下:

#define  BinNodePosi(T) BinNode<T>* //结点的位置
template <typename> struct BinNode
{
BinNodePosi(T) parent,lChild,rChild;//父亲,孩子
T data; int height;int size();//高度,子树的规模
BinNodePosi(T) insertAslC(T const&);//作为左孩子插入新的结点
BinNodePosi(T) insertAsrC(T const &);//作为右孩子插入新的结点
BinNodePosi(T) succ();//当前结点的直接后继
template<typename VST> void travLevel(VST &);//子树的层次遍历
template<typename VST> void travPre(VST &);//子树的先序遍历
template<typename VST> void travIn(VST &);//子树的中序遍历
template<typename VST> void travPost(VST &);//子树的后序遍历
}

template <typename T> BinNodePosi(T) BinNode<T>::insertAslC(T const &)
{
return lChild=new BinNode(e,this);//新建一个接点,作为赋值给lChild;
}
template <typename T> BinNodePosi(T) BinNode<T>::insertAsrlC(T const &)
{
return rChild=new BinNode(e,this);//新建结点,作为右孩子赋给rChild;
}
template<typename T>
  int  BinNode<T> ::size()
{
int s=1;
if(lChild) s+=lChild->size();
if(rChild) s+=rChild->size();
return s;
}

我们可以看到,二叉树的插入操作先建立一个结点BinNode(e,this),e作为新插入的元素。this指向当前的结点的父节点。通过赋值,rChild=new BinNode(e,this);rChild指向新结点。

 BinNode<T>::size()接口,以递归的方式记录结点数。所以必须遍历所有结点。时间复杂度为O(n)

6.二叉树的遍历

        对于一颗二叉树而言,遍历算法极为重要。我们介绍他四种遍历算法。访问的结点无非就只有三种。根结点,左孩子和右孩子。按照根结点访问的顺序,有先序中序和后序三种遍历。

6.1层次遍历

    层次遍历从根结点开始,纵向按照深度方向,横向从左向右依次遍历结点。

template<typename T> 
template<typename VST>
void BinNode<T>::travLevel(VST& visit)
{
queue<BinNodePosi(T)> q;//辅助一个队列
q.enqueue(this);//将根结点入队
while(!q.empty())
{
BinNodePosi<T> x=q.dequeue();将根结点出队
visit(x->data);//访问根结点
if(HasLChild(*x)) q.enqueue(x->lChild);//如果有左孩子,左孩子入队
if(HasRChild(*x)) q.enqueue(x->rChild);//如果有右孩子,右孩子入队
}
}

6.2先序遍历

先访问根结点,其次访问左右孩子。

template<typename T,typename VST>
 void traverse(BinNodePosi<T> x,VST & visit)
{
if(!x) return;//递归基
visit(x->data);//先访问树根结点的数据
traverse(x->lChild,visit);//递归到左孩子
traverse(x->rChild,visit);//递归到右孩子
} 

         对于上述代码我们看到是利用递归的形式,也很容易想到,毕竟二叉树的定义上看类似于递归结构。但是,对于不同数据而言,对遍历时间复杂度描述差异会比较明显。递归本身是运用了运行时栈的结构。我们其实可以更精确的控制,借助一个栈结构,控制数据读入。以迭代的方式达到目的。

template<typename T, typename VST>
  void travPre_I1(BinNodePosi<T> x,VST& visit)
{
stack <BinNodePosi<T>> s;//辅助一个栈
if(x)
{
s.push(x);//根结点入栈
}
while(!s.empty())
{
x=s.pop(); 
visit(x->data);
if(HasRChild(*x)) s.push(x->rChild);//右孩子先入栈
if(HasLChils(*x)) s.push(x->lChild);//左孩子后入栈
}
}

如图所示:

        对于第一个元素a先入栈,出栈后,访问该结点。如果有右孩子,右孩子先入栈,其次左孩子入栈。然后出栈。按照出栈的顺序依次遍历了a,b,c,d,e,f结点。 

6.3中序遍历

对于中序遍历同样也有递归的思路。

template<typename T,tyepname VST>
void traverse(BinNodePosi<T> x,VST &visit)
{
if(!x) return ;
traverse(x->lChild,visit);
visit(x->data);
traverse(x->rChild,visit);
}

中序遍历,只需要讲visit()函数交换位置即可。我们也提供一种利用辅助栈的形式。代码如下:

template<typename T>
static void golongLeftBranch(BinNodePosi<T> X,stack <BinNodePosi<T>>& s)
{while(x){s.push(x);x=x->lChild;}}//反复入栈
template<typename T,typename v> 
void travIn_I1(BinNodePosi<T> x,v& visit)
{
stack <BinNodePosi<T>> s;//辅助一个栈
while(true)
{
golongLeftBrabch(x,s);
if(s.empty()) break;//栈为空,处理完毕
x=s.pop();//出栈,接下来访问当前结点
visit(x->data);
x=x->rChild;//转向右子树
}
}

       从图中我们看到,中序遍历最先访问左侧链的a结点,如果a左子树为空,访问右子树。最先访问左孩子,需要不断地入栈。当左子树为空时,出栈访问当前结点,并转向右子树。右子树访问完毕,需要开始回溯。直至全部访问完毕。

6.4后序遍历

同样后序遍历也有递归和将递归转化为迭代的方式。

递归:

template<typename T,typename VST>
 void traverse(BinNodePosi<T> x,VST& visit)
{
if(!x)return;
traverse(x->lChild,visit);
traverse(x->rChild,visit);
visit(x->data);
}

迭代:

       从图中我们可以看到,查找第一个遍历点是先从根结点出发,如果有左孩子,优先把权力给左孩子,如果没有就把权力给右孩子。如果两个都没有,那这就是我们要找的优先遍历的结点。该结点也是一个叶子结点。其次的遍历路径类似于一个藤攀树,盘绕遍历。

代码:

template<typename T> 
static void gotoLeftmostLeaf(stack <BinNodePosi<T>> &s)
{
while(BinNodePosi<T> x=s.top())
  if(HasLChild(*x))//尽可能先向左
{
if(HasRChild(*x))//如果有右孩子,则优先入栈。出栈时后访问
s.push(x->rChild);
s.push(x->lChild);//然后再转向左孩子
}
else//没左孩子转向右孩子
{
s.push(x->rChild);
}
s.pop();//返回最后入栈的空结点。
}
template<typename T, typename VST>
void travPost_I(BinNode<T> x,VST& visit)
{
stack<BinNodePosi<T>> s;//辅助一个栈
if(x) s.push(x);//根结点先入栈
while(!s.empty())
{
if(s.top()!=x->parent)//
gotoLeftmostLeaf(s);
x=s.top();
visit(x->data);
}
}

联系我们

如果您对我们的服务有兴趣,请及时和我们联系!

服务热线:18288888888
座机:18288888888
传真:
邮箱:888888@qq.com
地址:郑州市文化路红专路93号