算法与数据结构(4)----树

 1 二叉树

1.1 二叉树的定义

二叉树是每个结点最多有两个子树的树结构。它有五种基本形态:二叉树可以是空集;根可以有空的左子树或右子树;或者左、右子树皆为空。

1.2一些基本概念

结点的度:结点拥有的子树的数目

叶子结点:度为0的结点

分支结点:度不为0的结点

树的度:树中结点的最大的度

层次:根结点的层次为1,其余结点的层次等于该结点的双亲结点的层次加1

树的高度:树中结点的最大层次

1.3 满二叉树 完全二叉树

高度为h,并且由2h-1个结点组成的二叉树,称为满二叉树

完全二叉树

定义:一棵二叉树中,只有最下面两层结点的度可以小于2,并且最下层的叶结点集中在靠左的若干位置上,这样的二叉树称为完全二叉树。

特点:叶子结点只能出现在最下层和次下层,且最下层的叶子结点集中在树的左部。显然,一棵满二叉树必定是一棵完全二叉树,而完全二叉树未必是满二叉树。

1.4 二叉树的性质

性质1:二叉树第i层上的结点数目最多为2i-1(i>=1)

性质2:深度为k的二叉树至多有2k-1个结点(k>=1)

性质3:包含n个结点的二叉树的高度至少为(log2n)+1

性质4:在任意一棵二叉树中,若终端结点的个数为n0,度为2的结点数为n2,则n0=n2+1

 性质5:对一颗具有n个结点的完全二叉树的结点自上向下、自左向右从1开始连续编号,那么对任一结点i,有

(1) 如果i=1,则结点i是二叉树的根,无双亲,如果i》1,则其双亲是i/2向上取整。

(2) 如果2i>n,则结点i无左孩子,为叶子结点;如果2i<=n,则其左孩子是2i。

(3)如果2i+1>n,则结点i无右孩子;如果2i+1<=n,则其右孩子是2i+1.

 2 二叉树的存储结构

2二叉树的存储结构可以采用顺序存储结构和链表存储结构

2.1顺序存储结构

---- 依据二叉树的性质,完全二叉树满二叉树采用顺序存储比较合适,树中结点的序号可以唯一地反映出结点之间的逻辑关系,这样既能够最大可能地节省存储空间,又可以利用数组元素的下标值确定结点在二叉树中的位置,以及结点之间的关系。

---- 一棵完全二叉树(满二叉树)如下图所示:

                       

将这棵二叉树存入到数组中,相应的下标对应其同样的位置,如下图所示:

                

但是对于一般的非完全二叉树来说,如果仍然按照从上到下、从左到右的次序存储在一维数组中,则数组下标之间不能准确反映树中结点间的逻辑关系,可以在非完全二叉树中添加一些并不存在的空结点使之变成完全二叉树,(把不存在的结点设置为“^”)不过这样做有可能会造成空间的浪费,如下图所示,然后再用一维数组顺序存储二叉树。

         

             

缺点是:有可能对存储空间造成极大的浪费,在最坏的情况下,一棵深度为k的右斜树,它只有k个结点,却需要2^k-1个结点存储空间。这显然是对存储空间的严重浪费,所以顺序存储结构一般只用于完全二叉树或满二叉树

2.2 链式存储结构

二叉树的链式存储结构是指用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。

---- 二叉树的每个结点最多有两个孩子,因此,每个结点除了存储自身的数据外,还应设置两个指针分别指向左、右孩子结点。

结点结构如下图所示:

        

其中data是数据域,lchild和rchild都是指针域,分别存放指向左孩子和右孩子的指针。由上图所示的结点构成的链表称作二叉链表。当没有孩子结点时,相应的指针域置为空。

 3 二叉树的遍历

二叉树的遍历(traversing binary tree)是指从根结点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次。

二叉树的遍历主要有四种:前序遍历、中序遍历、后序遍历和层序遍历。

1)、前序遍历

先访问根结点,然后遍历左子树,最后遍历右子树。

代码:

//顺序存储
public void preOrderTraverse(int index) {  
    if (datas[index] == null)  
        return;  
    System.out.print(datas[index] + " ");  
    preOrderTraverse(index*2);  
    preOrderTraverse(index*2+1);  
} 

//链式存储
 public void preOrderTraverse(Node node) {  
    if (node == null)  
        return;  
    System.out.print(node.data + " ");  
    preOrderTraverse(node.leftChild);  
    preOrderTraverse(node.rightChild);  
} 

2)、中序遍历

先遍历左子树,然后遍历根结点,最后遍历右子树。

//链式存储
 public void inOrderTraverse(Node node) {  
    if (node == null)  
        return;  
    inOrderTraverse(node.leftChild);
    System.out.print(node.data + " ");  
    inOrderTraverse(node.rightChild);  
} 

3)、后序遍历

先遍历左子树,然后遍历右子树,最后遍历根结点。

//链式存储
 public void postOrderTraverse(Node node) {  
    if (node == null)  
        return;  
    postOrderTraverse(node.leftChild);
    postOrderTraverse(node.rightChild);  
    System.out.print(node.data + " ");  
} 

4)、层序遍历

从上到下逐层遍历,在同一层中,按从左到右的顺序遍历。

注意:

  • 已知前序遍历和中序遍历,可以唯一确定一棵二叉树。
  • 已知后序遍历和中序遍历,可以唯一确定一棵二叉树。
  • 已知前序遍历和后序遍历,不能确定一棵二叉树。

 4 二叉树的变形

二叉树的变形介绍 二叉排序树 平衡二叉树 哈夫曼树及哈夫曼编码 线索二叉树 堆排序, B树以及红黑树会在一篇转载博客介绍

4.1 线索二叉树


  空的左孩子指针指向该结点的前驱;空的右孩子指针指向该结点的后继。这种附加的指针值称为线索,带线索的二叉树称为线索二叉树。 

    在不同的遍历次序下,二叉树中的每个结点一般有不同的前驱和后继。因此,线索二叉树又分为前序线索二叉树中序线索二叉树后序线索二叉树3种。 

    根据二叉树的特性,n个结点的二叉树,采用链式存储结构时,有n+1个空链域,可以利用这些空链域存放指向结点的直接前驱和直接后继结点的指针。为此做如下规定:当结点的左指针为空(即无左子树)时,令该指针指向按某种方式遍历二叉树时得到的该结点的前驱结点;当结点的右指针为空(即无右子树)时,令该指针指向按某种方式遍历二叉树时得到的该结点的后继结点;为了避免混淆,还需要增加两个标志位来区分指针指向的是其孩子还是前驱及后继。

4.2二叉查找树

二叉排序树或者是一棵空树,或者是具有下列性质的二叉树:
(1)若左子树不空,则左子树上所有结点的值均小于或等于它的根节点的值;
(2)若右子树不空,则右子树上所有结点的值均大于或等于它的根结点的值;
(3)左、右子树也分别为二叉排序树;

4.3 平衡二叉树

平衡二叉树(Balanced BinaryTree)又被称为AVL树。它具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。

平衡二叉树一般是一个有序树,它具有二叉树的所有性质,其遍历操作和二叉树的遍历操作相同。但是由于其对二叉树施加了额外限制,因而其添加、删除操作都必须保证平衡二叉树的性子被保持。

平衡二叉树中引入了一个概念:平衡二叉树节点的平衡因子,它指的是该节点的两个子树,即左子树和右子树的高度差,即用左子树的高度减去右子树的高度,如果该节点的某个子树不存在,则该子树的高度为0,如果高度差的绝对值超过1就要根据情况进行调整。

4.4 赫夫曼树及赫夫曼编码

哈夫曼树是一种带权路径长度最短的二叉树,也称为最优二叉树。以下图为例进行说明:


它们的带权路径长度分别为:

图a:WPL=5*2+7*2+2*2+13*2=54

图b:WPL=5*3+2*3+7*2+13*1=48

由此说明哈夫曼树是带树路径最短的树,也称最优二叉树。

因此,我们可以利用哈夫曼树对数据进行压缩,哈夫曼编码就是哈替曼的一个应用。利用哈夫曼树求得的用于通信的二进制编码称为哈夫曼编码,树中从根节点到每个叶子节点都有一条路径,对路径上各分支用数字“0”、“1”进行编码(一般约定左子树为0,右子树为1),取每条路径上的“0”、“1”组成的序列即为对应叶子节点对应的编码,即哈夫曼编码

4.5 堆排序

堆分为最大堆和最小堆,其实就是完全二叉树。最大堆要求节点的元素都要不小于其孩子,最小堆要求节点元素都不大于其左右孩子,两者对左右孩子的大小关系不做任何要求,其实很好理解。有了上面的定义,我们可以得知,处于最大堆的根节点的元素一定是这个堆中的最大值。

其实我们的堆排序算法就是抓住了堆的这一特点,每次都取堆顶的元素,将其放在序列最后面,然后将剩余的元素重新调整为最大堆,依次类推,最终得到排序的序列

 4 树和森林

定义什么的不介绍 很明显 说一下存储结构与森林的基本操作

4.1 树和森林存储结构

树的双亲表示法 


一般采用顺序存储结构实现。用一组地址连续的存储单元来存放树的结点,每个结点有两个域:

 data-----存放结点的信息;

parent-----存放该结点双亲结点的位置。

特点:求结点的双亲很容易,但求结点的孩子需要遍历整个向量。   

树的孩子表示法

这是树的链式存储结构。每个结点的孩子用单链表存储,称为孩子链表。

  n个结点可以有n个孩子链表(叶结点的孩子链表为空表)。

  n个孩子链表的头指针用一个向量表示。

   它的特点是:与双亲相反,求孩子易,求双亲难。

树的双亲孩子表示法

将双亲和孩子表示法结合到一起,这样他们的优点就都有了。

树的孩子兄弟表示法

孩子兄弟链表表示法也是树的一种链式存储结构。用二叉链表作为树的存储结构,每个结点的左链域指向该结点的第一个孩子,右链域指向下一个兄弟结点。

    由于结点中的两个指针指示的分别为“孩子”和“兄弟”,故称为“孩子-兄弟链表”。这种结构也称为二叉链表。  

特点:双亲只管长子,长子连接兄弟。

4.2 森林和二叉树的转换

树与二叉树均可用二叉链表作为存储结构,因此给定一棵树,用二叉链表存储,可唯一对应一棵二叉树,反之亦然。

(1) 在树中各兄弟(堂兄弟除外)之间加一根连线。

 (2) 对于任一结点,只保留它与最左孩子的连线外,删去它与其余孩子之间的连线。

 (3) 以树根为轴心,将整棵树按顺时钟方向旋转约45°。

    特点:根无右子树

树和森林都可转换成二叉树,但树转换成二叉树后根结点无右分支,而森林转换后的二叉树,其根结点有右分支。


(1) 将森林中的每一棵树转换成等价的二叉树。

 (2) 保留第一棵二叉树,自第二棵二叉树始,依次将后一棵二叉树的根结点作为前一棵二叉树根结点的右孩子,当所有的二叉树依此相连后,所得到的二叉树就是由森林转化成的二叉树。

 (3) 以树根为轴心,将整棵树按顺时钟方向旋转约45°。

4.3 森林的遍历

森林的深度优先遍历通常也有两种方式:前序遍历和后序遍历。

(1) 前序遍历森林

若森林非空,则:

访问森林中第一棵树的根结点;

前序遍历第一棵树中根结点的各子树所构成的森林

前序遍历去掉第一棵树外其它树构成的森林。

(2) 后序遍历森林

若森林非空,则:

后序遍历森林中第一棵树中根结点的各子树所构成的森林;

访问第一棵树的根结点;

后序遍历去掉第一棵树外其它树构成的森林。

当用二叉链表作为树和森林的存储结构时,树和森林的前序遍历和后序遍历可用二叉树的前序遍历和中序遍历算法来实现。 


















猜你喜欢

转载自blog.csdn.net/qq_40182703/article/details/80200986