数据结构基础学习之(树与二叉树)

主要知识点:

  • 树的定义及常用术语
  • 树的存储表示
  • 二叉树、满二叉树和完成二叉树的定义
  • 二叉树的遍历此操作实现
  • 哈夫曼树及其编码
  • 树、森林与二叉树之间的转换

一、树

1. 概念:
  • 定义: 树是由n(n≥0)个结点组成的有限集合
  • 特点:
  1. 有且仅有一个称为根(Root)的结点;
  2. 其余的结点可分为m(m≥0)个互不相交的子集Tl,T2,…,Tm,其中每个子集本身又是一棵树,并称其为根的子树(Subree)。
  • 树的常用术语

结点(node)

  1. 由一个数据元素及关联其子树的边组成

结点路径

  1. 若树中存在一个结点序列k1,k2,…,ki,使得ki是ki+1的双亲(1≤i<j),则称该结点序列是从k1到kj的一条路径(Path)或道路。

路径的长度

  1. 指路径所经过的边(即连接两个结点的线段)的数目

结点的度(degree)

  1. 结点拥有的子树的数目

树的度

  1. 一棵树中最大的结点度数(拥有最多子树的节点,即结点的度最大值)

叶子结点(leaf)

  1. 结点的度为0的结点(没有子树的节点),也叫终端结点

分支结点

  1. 结点的度不为0的结点(有子树的节点),也叫非终端结点

孩子结点

  1. 一个结点的孩子结点是指这个结点的子树的根结点

双亲结点(parents)

  1. 一个结点有孩子结点,则这个结点称为它孩子结点的双亲结点

子孙结点

  1. 即一个结点A所有子树的结点称为该结点A的子孙结点

祖先结点

  1. 即一个结点A的祖先结点是指路径中除结点A外的的结点

兄弟结点(sibling)

  1. 同一双亲结点的孩子结点之间互成为兄弟结点

结点的层次

  1. 从根结点算起,根为第一层,它的孩子为第二层

树的深度

  1. 树中结点的最大层次数

有序树与无序树

  1. 如果将树中结点的各子树看成从左至右是有次序的(即不能互换),则称该树为有序树,否则称为无序树

森林

  1. m(m>=0)棵互不相交的树的集合

二、 二叉树

定义:

  • 二叉树(BinaryTree): 是n(n≥0)个结点的有限集, 它或者是空集(n=0),或者由一个根结点及两棵互不相交的、分别称作这个根的左子树和右子树的二叉树组成。
  • 满二叉树: 是二叉树的特殊形态,除叶节点外的所有结点都有左右子树的二叉树,称为满二叉树
  • 完全二叉树: 也是二叉树的特殊形态,
  • 单分支树:所有节点都没有左结点(或右结点)的二叉树

二叉树五种基本形态

5.4二叉树的5种基本形态.png

二叉树的性质

  • 二叉树第i层上的结点数目最多为 2^{i}(i≥0)
  • 深度为k的二叉树至多有2^{k}-1 (k≥1)个结点。
  • 在任意-棵二叉树中,若终端结点的个数为n0,度为2的结点数为n2,则n0=n2+1。
  • 具有n个结点的完全二叉树,其深度为\log_2 n+1\log_2 (n+1)
  • 对于具有n个结点的完全二叉树,若从根结点开始自上而下,从左到右开始编号,对于任意编号i(0<=i<n)的结点有:
    1. 若i=0,则结点为根结点,没有双亲,若i>0,则它的双亲结点编号为 \frac {i-1}{2}
    2. 若2i+1 >=n ,则编号i结点无左孩子,否则编号2i+1的结点就是它的左孩子
    3. 若2i+2 >=n ,则编号i结点无右孩子,否则编号2i+2的结点就是它的右孩子
  • 满二叉树和完全二叉树示意图
5.7满二叉树和完全二叉树.png

二叉树存储结构

  1. 顺序存储结构示意图
5.9二叉树顺序存储结构示意图.png
  1. 链式存储结构示意图
5.10二叉树链式存储的结点结构.png
5.11二叉树及其三叉链式存储结构.png

二叉树的遍历

  1. 前序遍历
/**
     * 递归的前序遍历
     * <p>
     * 1. 从根结点出发
     * 2. 先遍历完左子树
     * 3. 再遍历右子树
     * <p>
     * (注意:顺序: 中左右)
     *
     * @param treeNode
     */
    public void preOrderTraverse(BiTreeNode treeNode) {
        if (treeNode == null) return;

        //结点数据
        System.out.println(treeNode.data.toString());

        //先遍历左子树
        preOrderTraverse(treeNode.LChild);

        //然后遍历右子树
        preOrderTraverse(treeNode.RChild);
    }

    /**
     * 非递归的前序遍历
     */
    public void preOrderTraverse() {
        //获取根结点
        BiTreeNode<T> node = root;
        if (node == null) return;

        //构造一个栈,由于存储右子树结点
        LinkStack<BiTreeNode> stack = new LinkStack<>();
        stack.push(node);
        while (!stack.isEmpty()) {
            //弹出栈顶结点
            node = stack.pop();
            //访问该结点
            System.out.println(node.data.toString());
            while (node != null) {
                //如果左结点不为空,则访问
                if (node.LChild != null)
                    System.out.println(node.LChild.data);

                //如果右结点不为空,则先压入栈中
                if (node.RChild != null)
                    stack.push(node.RChild);

                //继续遍历左结点
                node = node.LChild;
            }
        }

    }
  1. 中序遍历
/**
     * 中序遍历(递归方式)
     * <p>
     * 1. 从左子树出发开始遍历
     * 2. 遍历到根结点
     * 3. 又从根结点出发,遍历右子树
     * <p>
     * (注意:顺序: 左中右)
     *
     * @param treeNode
     */
    public void inOrderTraverse(BiTreeNode treeNode) {
        if (treeNode == null) return;

        //遍历左子树
        inOrderTraverse(treeNode.LChild);

        //结点数据
        System.out.println(treeNode.data.toString());

        //遍历右子树
        inOrderTraverse(treeNode.RChild);
    }

    /**
     * 中序遍历(非递归)
     */
    public void inOrderTraverse() {
        BiTreeNode<T> node = this.root;
        if (node != null) {
            LinkStack<BiTreeNode> stack = new LinkStack<>();
            stack.push(node);
            while (!stack.isEmpty()) {
                while (stack.peek() != null)
                    stack.push(stack.peek().LChild);  //把左结点入栈,直到最左下的结点

                //弹出空结点
                stack.pop();
                if (!stack.isEmpty()) {
                    node = stack.pop();
                    //打印结点
                    System.out.print(node.data.toString());
                    //把该结点的右子结点入栈
                    stack.push(node.RChild);
                }
            }
        }
    }
  1. 后序遍历
/**
     * 后序遍历
     * <p>
     * 1. 以从左到右的方式
     * 2. 先遍历左子树
     * 3. 然后遍历右子树
     * 4. 最后遍历右子树
     * <p>
     * 顺序: 左右中
     */
    public void postOrderTraverse(BiTreeNode treeNode) {
        if (treeNode == null) return;

        postOrderTraverse(treeNode.LChild);
        postOrderTraverse(treeNode.RChild);

        System.out.println(treeNode.data.toString());
    }

    /**
     * 后序遍历(非递归)
     */
    public void postOrderTraverse() {
        //获取根结点
        BiTreeNode node = this.root;
        if (node != null) {
            LinkStack<BiTreeNode> stack = new LinkStack<>();
            //将根结点入栈
            stack.push(node);
            //设置结点访问标识
            boolean flag;
            //设置指针,指向访问过的结点
            BiTreeNode p = null;
            while (!stack.isEmpty()) {
                //将结点的左子结点入栈
                while (stack.peek() != null)
                    stack.push(stack.peek().LChild);
                //弹出空结点
                stack.pop();
                while (!stack.isEmpty()) {
                    //查看栈顶元素
                    node = stack.peek();

                    //如果该结点的右子结点为空,或已访问过,则该结点可以出栈访问
                    if (node.RChild == null || node.RChild == p) {
                        //访问结点
                        System.out.print(node.data.toString());
                        //出栈
                        stack.pop();
                        //已访问指针指向该结点
                        p = node;
                        //标识为已访问
                        flag = true;
                    } else {
                        //否则,将该结点的右子结点入栈,
                        stack.push(node.RChild);
                        // 标识该结点还没访问
                        flag = false;
                    }

                    if (!flag) {
                        break;
                    }
                }
            }
        }
    }
  1. 层次遍历
 /**
     * 层次遍历
     */
    public void levelTraverse() {
        BiTreeNode<T> node = this.root;
        if (node != null) {
            //初始化队列
            LinkQueue<BiTreeNode> queue = new LinkQueue<>();
            //根结点入队列
            queue.offer(node);

            while (!queue.isEmpty()) {
                //出队列
                node = queue.poll();
                //访问该结点
                System.out.println(node.data.toString());
                //该结点的左子结点入队列
                if (node.LChild != null) {
                    queue.offer(node.LChild);
                }
                //该结点的右子结点入队列
                if (node.RChild != null) {
                    queue.offer(node.RChild);
                }
            }
        }
    }

二叉树的建立

  1. 由前序遍历和中序遍历,或后序遍历和中序遍历推导建立二叉树
/**
     * 二叉树的建立
     *
     * @param preOrder 前序遍历的序列
     * @param inOrder  中序遍历的序列
     * @param preIndex 前序遍历开始位置
     * @param inIndex  中序遍历开始位置
     * @param count    结点数
     */
    public LinkBiTree(String preOrder, String inOrder, int preIndex, int inIndex, int count) {
        if (count > 0) {
            //获取前序遍历的序列的根结点
            char r = preOrder.charAt(preIndex);
            //记录根结点在中序遍历中的位置
            int i = 0;
            for (; i < count; i++) {
                if (r == inOrder.charAt(i + inIndex)) {
                    break;
                }
            }
            root = new BiTreeNode(r);
            root.LChild = new LinkBiTree(preOrder, inOrder, preIndex + 1, inIndex, i).root;
            root.RChild = new LinkBiTree(preOrder, inOrder, preIndex + i + 1, inIndex + i + 1, count - i - 1).root;

        }
    }
  1. 由标明空子树的前序遍历建立二叉树
 /**
     * 由标明的空子树建立二叉树
     *
     * @param preOrder
     */
    private static int preIndex = 0;

    public LinkBiTree(String preOrder) {
        //获取前序遍历中的根结点
        char c = preOrder.charAt(preIndex++);
        //如果字符不为#
        if ('#' != c) {
            //创建根结点
            root = new BiTreeNode(c);
            //创建左子树
            root.LChild = new LinkBiTree(preOrder).root;
            //创建右子树
            root.RChild = new LinkBiTree(preOrder).root;
        } else {
            root = null;
        }
    }
  1. 由完全二叉树顺序存储序列建立二叉树
/**
     * 使用完全二叉树的顺序存储结构建立二叉链式存储结构
     *
     * @param sqBiTree 序列
     * @param index    根结点标识
     */
    public LinkBiTree(String sqBiTree, int index) {
        if (index < sqBiTree.length()) {
            root = new BiTreeNode(sqBiTree.charAt(index));
            //建立左右子树
            root.LChild = new LinkBiTree(sqBiTree, 2 * index + 1).root;
            root.RChild = new LinkBiTree(sqBiTree, 2 * index + 2).root;
        }
    }

三、哈夫曼树及哈夫曼编码

1. 基本概念:

  • 树的路径长度: 是从树根结点到树中每一结点的路径长度之和。在结点数目相同的二叉树中,完全二叉树的路径长度最短。
  • 结点的权:在一些应用中,赋予树中结点的一个有某种意义的实数。
  • 结点的带权路径长度:结点到树根之间的路径长度与该结点上权的乘积。
  • 树的带权路径长度(Weighted Path Length of Tree):定义为树中所有叶结点的带权路径长度之和
  • 公式: wpl=\sum_{k=1}^n W_kL_k
  • W_k: 第k个结点的权值
  • L_k :根结点到第k个结点的路径长度
  • 最优二叉树:二叉树带权路径长度值最小,它就是一棵最优二叉树或哈夫曼树
  • 赫夫曼树中不存在度为1的结点(赫夫曼树的每一分支结点都是由两棵子树合并产生的新结点)

2. 构造哈夫曼树

  • 步骤:(假设带权值的叶子结点为 {E10,B15,A5,C40,D30})
  1. 先把这些叶子结点按权值从小到大排序,组成有序序列:A5, E10, B15, D30, C40
  2. 取两权值最小的结点,作为新结点N1的左右孩子,注意:权值小的结点作为左孩子;新结点的权值为这两个结点权值的和;即5+10=15;
  3. 把新结点N1 加入有序序列:N_115 B15, D30, C40
  4. 重复步骤2,把N1和B结点作为新结点N2的左右孩子, 权值为:15+15=30
  5. 重复步骤3,有序序列:N_230` D30, C40
  6. 重复步骤2,把N2和D结点作为新结点N3的左右孩子, 权值为:30+30=60
  7. 重复步骤3,有序序列: C40, N_360`
  8. 重复步骤2,把C和N3结点作为新结点T的左右孩子, 权值为:40+60=100
  9. 因为结点T是二叉树的根结点,所以完成的哈夫曼树的构造
  • 完成哈夫曼树图
  1. 带权路径长度为: WPL=401 + 302+153+104+5*4= 205
5.4哈夫曼树构造过程.png
  • 构造哈夫曼树总结:
  1. 根据给定的n个权值{w1,w2,w3...,wn}构成n棵二叉树的集合F={T1,T2,...,Tn},其中每棵二叉树Ti中只有一个带权值的根结点,其左右子树为空
  2. 在集合F中选取权值最小的树,作为左右子树(权值较小的树作为左子树)构造一棵新的二叉树,且新二叉树的权值为其左右子树权值的和
  3. 在F中删除这两棵树,同时使用新的二叉树加入F中
  4. 重复步骤2,3,直到F中只含一棵树为止,则可以得到哈夫曼树

3. 哈夫曼树编码

  • 定义哈夫曼树左分支代表0, 右分支代表1

  • 从根结点到叶子结点所经过的路径分支组成的0和1序列,就是哈夫曼树编码

  • 哈夫曼树编码示意图

5.25哈夫曼树及编码.png

四、树、森林与二叉树的转换

1. 树转换为二叉树
  1. 加线。所有兄弟结点之间加一条线

  2. 去线。对树的每个结点,只保留它与第一个孩子结点的连线,删除其它孩子结点连线

  3. 层次调整。以树的根结点为轴心,顺时针旋转一定的角度,注意:第一个孩子结点作为二叉树结点的左孩子结点,兄弟结点转换过来的孩子结点作为右孩子结点。

  4. 转换示意图

5.29树转换二叉树.png

2. 森林转换二叉树

  1. 将森林中的每棵树转换为二叉树
  2. 第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树的根结点的右孩子,用连接起来
  3. 重复步骤2,直到所有二叉树连接起来,就得到森林转换过来的二叉树
  4. 示意图
5.31森林转换为二叉树.png
  • 二叉树转换为树
  1. 加线。若某结点是其双亲结点的左孩子,则将该结点沿着右分支向下的所有结点与该结点的双亲结点用线连接
  2. 删线。 将树中所有双亲结点与右孩子结点的连线删除
  3. 层次调整。以树的根结点为轴心,逆时针旋转一定的角度
  4. 示意图
5.30二叉树转换为树.png
  • 二叉树转换为森林
  1. 从根结点开始, 若右孩子存在,则把右孩子结点的连线删除,得分离的二叉树后,看其右孩子是否存在,存在则删除,直到所有右孩子连线都删除为止
  2. 再将分离后的二叉树转换为树,
  3. 示意图
5.32二叉树转换森林.png

五、树的存储结构

表示法

  1. 双亲表示法


    5.33双亲链表存储结构.png
  2. 孩子表示法


    5.34孩子链表存储结构.png
  3. 双亲孩子表示法


    5.35双亲孩子链表存储结构.png
  4. 孩子兄弟表示法(应用最广泛)


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

推荐阅读更多精彩内容