数据结构| 树

一. 树的定义

树是一种非线性的数据结构。它是n个结点的有限集合,一棵非空树具有以下的特点:
(1)有且只有一个称为根(root)的结点。
(2)当n>1时,其余n-1个结点可以划分为m个有限集合T1、T2......Tm,且这m个有限集合不相交,我们称其中的Ti为根的子树。

当n=0时,称为空树;当n>0时,称为非空树。树的逻辑结构如图所示:


在介绍树之前,需要先了解一些有关树的概念:
树的节点:指包含一个数据元素以及指向其他结点的分支信息。
结点的度:结点子树的个数称为结点的度。例如图中B有2棵子树,则B的度数为2;F的子树为0,它的度数就是0。
树的度:树中各结点度的最大值就是树的度。
孩子与双亲:结点的子树的根称为孩子,而该结点称为双亲。
兄弟:同一个双亲的孩子之间称为兄弟。
树的层次:从根结点起,根结点为第1层,根结点的孩子结点为第2层,以此类推。
数的深度:树中所有结点的层次最大值称为树的深度。
有序树与无序树:如果树中各棵子树之间是有先后次序的称为有序树,反之则称为无序树。
森林:m棵互不相交的树构成一个森林。

二. 二叉树的相关概念

1.二叉树的定义

二叉树是由n个结点构成的另一种树结构。它的特点是每个结点最多只有两棵子树,并且二叉树是有左右之分的(左孩子和右孩子),次序不能颠倒。

在二叉树中,任何一个结点的度只可能是0、1或2。


二叉树

下面介绍两种特殊的二叉树:

1.满二叉树

每层结点都是满的二叉树称为满二叉树,即在满二叉树中每一层的结点都具有最大的结点个数。在满二叉树中每个结点的度只能为0或2,不会出现度为1的结点。


满二叉树

2.完全二叉树

对满二叉树的结点进行连续编号,约定从根结点开始,自上而下,从左到右进行编号。在一棵具有m个结点的二叉树中,若每个结点都与满二叉树的编号从1到m一一对应时,则称为完全二叉树。


完全二叉树

2. 二叉树的性质

二叉树具有以下重要的性质:
性质一:二叉树的第k层上至多有2^(k-1)个结点。
性质二:深度为k的二叉树最多有2^k-1个结点。
性质三:对于任何一棵二叉树T,如果度为0结点数为n0,度为2的结点数为n2,则有n0=n2+1。
性质四:具有n个结点的完全二叉树的深度为[log2( n)]+1([x]表示不大于x的最大整数)。

3. 二叉树的存储表示与实现

二叉树具有顺序存储和链式存储两种存储方式,其中链式存储是其最常用的方式。

1. 二叉树的顺序存储

若对一棵二叉树按照层次从上到下,从左到右进行编号,则对于完全二叉树来说每个结点的编号可以通过二叉树的性质计算得到,因此完全二叉树中的结点可以依次存放在一维数组中。


完全二叉树的顺序存储

按照同样的方法,若将非完全二叉树的结点也按照同样的方式进行编号存放在一维数组中。为了能够正确反映二叉树结点之间的逻辑关系,还需要在一维数组中空出二叉树中不存在的结点位置(我这里用^来表示)。


非完全二叉树
数组存储

可以看出如果采用顺序存储的方法,则对于许多的非完全二叉树来说构造数组是很浪费存储空间的。

2. 二叉树的链式存储

根据二叉树的定义,我们可以使用链表的方式来存储二叉树,即每个结点包括指向左孩子的指针域、数据域和指向右孩子的指针域。


链式存储

其中,data域存放某结点的数据信息;lchild与rchild分别存放指向左孩子和右孩子的指针,当左孩子或右孩子不存在时,相应指针域值为空(用符号∧或NULL表示)。利用这样的结点结构表示的二叉树的链式存储结构被称为二叉链表。


二叉链表

二叉树的链式存储的结点结构类型:
public class Node<T>{
    public Node<T> lchild;
    public T data;
    public Node<T> rchild;

    public Node(){
        lchild = null;
        data = null;
        rchild = null;
    }

    public Node(T x){
        lchild = null;
        data = x;
        rchild = null;
    }
}

3. 二叉树的基本操作的实现

class BinaryTree<T>{
    private Node<T> root;

    public BinaryTree(){}  //构造二叉树
    public BinaryTree(T x){}

    public boolean insertLeft(T x,Node<T> parent){}  //插入左结点
    public boolean insertRight(Node<T> parent){}  //插入右结点
    public boolean deleteLeft(Node<T> parent){}  //删除左结点
    public boolean deleteRight(Node<T> parent){}  //删除右结点
    public boolean search(T x){}  //在二叉树中查找数据
    public void traversal(int i){}  //按照某种方式遍历二叉树
    public int getHeight(Node<T> parent){}  //求当前二叉树的高度
} 

(1)构造二叉树

public BinaryTree(){
        root = new Node<T>();
    }  
    public BinaryTree(T x){
        this.root = new Node<T>(x);
    }

(2)插入左结点

public boolean insertLeft(T x,Node<T> parent){
        if(parent == null){
            return false;
        }
        Node<T> p = new Node<T>(x);
        if(parent.lchild == null){
            parent.lchild = p;
            return true;
        }
        return false;
    }  

(3)插入右结点

public boolean insertRight(Node<T> parent){
        if(parent == null){
            return false;
        }
        Node<T> p = new Node<T>(x);
        if(parent.rchild == null){
            parent.rchild = p;
            return true;
        }
        return false;
    }  

(4)删除左结点

public boolean deleteLeft(Node<T> parent){
        if(parent == null){
            return false;
        }
        parent.lchild = null;
        return true;
    }  

(5)删除右结点

 public boolean deleteRight(Node<T> parent){
         if(parent == null){
            return false;
        }
        parent.rchild = null;
        return true;
    }  

**(6)求二叉树的高度

public int getHeight(Node<T> parent){
        int lh,rh,max;
        if(parent != null){
            lh = getHeight(parent.lchild);
            rh = getHeight(parent.rchild);
            max = lh > rh ? lh : rh;
            return max+1;
        }
        esle{
            return 0;
        }
    }  

3. 遍历二叉树

遍历二叉树是指按照某种规律对二叉树的每个结点进行访问,使得每个结点仅被访问一次的操作。二叉树的遍历不同于线性表的遍历,对于二叉树来说,每个结点有两棵子树,因而需要一种规律,使得二叉树的结点能够排列在一个线性队列上,从而便于遍历。如果用D、L、R遍历根结点,遍历左子树和遍历右子树,如果限定先左后右的次序,就会有3中遍历方案:DLR(先序遍历)、LDR(中序遍历)和LRD(后序遍历)。

下面来分别介绍这3种遍历方式。

1. 先序遍历二叉树

先序遍历的递归定义是:如果二叉树为空,则不执行操作。反之则先访问根结点,然后先序遍历左子树,最后先序遍历右子树。

先序遍历二叉树的递归算法如下:

public void preorder(Node<T> node){
    if(node == null){
        return;
    }
    System.out.println(node.data);
    preorder(node.lchild);
    preorder(node.rchild);
}

2. 中序遍历二叉树

中序遍历的递归定义是:如果二叉树为空,则不执行操作。反之则先中序遍历左子树,然后访问根结点,最后中序遍历右子树。

中序遍历二叉树的递归算法如下:

public void inorder(Node<T> node){
    if(node == null){
        return ;
    }
    inorder(node.lchild);
    System.out.println(node.data);
    inorder(node.rchild);
}

3. 后序遍历二叉树

后序遍历的递归定义是:如果二叉树为空,则不执行操作。反之则先后序遍历左子树,然后后序遍历右子,最后树访问根结点。

后序遍历二叉树的递归算法如下:

public void postorder(Node<T> node){
    if(node == null){
        return ;
    }
    postorder(node.lchild);
    postorder(node.rchild);
    System.out.println(node.data);
}

4.层次遍历二叉树

层次遍历输出二叉树的结点可以利用队列来实现,即先定义一个队列queue用来存放结点的信息。从根结点出发,依次把每一层的结点入队,当一层结点入队完毕之后,将队头元素出队,输出该结点,然后判断结点是否存在左右孩子,如果存在,则将左右孩子入队。重复执行以上操作直到队空为止。

层次遍历输出二叉树的算法如下:

public void levelorder(){
    Node<T>[] queue = (Node<T>)new Node[maxSize];
    int front,rear;
    front = -1;
    rear = 0;
    queue[rear] = root;
    if(this.root == null){
        return;
    }
    while(front != rear){
        front++;
        System.out.println(queue[front].data);
        if(queue[front].lchild != null){
            rear++;
            queue[rear] = queue[front].lchild;
        }
        if(queue[front].rchild != null){
            rear++;
            queue[rear] = queue[front].rchild;
        }
    }
}

四.线索二叉树

一个具有n个结点的二叉树如果采用二叉链表存储结构,在2n个指针域中只有n-1个指针域用来存储结点孩子的引用,而另外n+1个指针域存放的都是null。因此,可以利用某结点空的左指针域指出该结点在某种遍历序列中直接前驱结点的存储地址,利用结点空的右指针域指出该结点爱某种遍历序列中直接后继结点的存储地址。而对于那些非空的指针域,则仍然存放指向结点左、右孩子的指针。这样,就得到一颗线索二叉树。

序列可以由不同的遍历方法得到,因此,线索树有先序线索二叉树、中序线索二叉树和后序线索二叉树,把二叉树改造成线索二叉树的过程称为线索化。

关于如何区别某结点的指针域存放的是指针还是线索,一般通过为每个结点增设两个标志位ltag和rtag来进行实现。


ltag
rtag

线索二叉树存储结构如下:
| itag | lchild | data | rchild | rtag

最优二叉树——哈夫曼树

1.哈夫曼树的定义

在介绍哈夫曼树之前,先介绍几个与哈弗曼树有关的定义。
(1)路径与路径长度:路径是指在树中,从一个结点到另一个结点所走过的路程。路径长度是一个结点到另一个结点的分支数目。树的路径长度是指从树的树根到每一个结点的路径长度的和。
(2)树的带权路径长度:在一棵树中,如果其结点上附带有一个权值,通常把该结点的路径长度与该结点上的权值之积称为该结点的带权路径长度。关于权的定义可以看这里。我们用WPL来表示带权路径长度。

求带权路径长度的例子

哈弗曼树就是带权路径长度最小的树,也称为最优二叉树。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容