美文网首页
数据结构与算法基础七:线索二叉树与赫夫曼树

数据结构与算法基础七:线索二叉树与赫夫曼树

作者: Trigger_o | 来源:发表于2021-05-27 20:07 被阅读0次

    一:线索二叉树

    二叉树遍历实际是将复杂的非线性结构转换为线性结构,一个n个节点的二叉链表,一共2n个指针域,n+1个分支,也就是说只有n+1个指针域是有值的,另外n-1个是空的;
    线索二叉树指的就是,利用这些空的指针域,存储上这个节点的前驱或者后继节点,前驱和后继依据遍历得到.
    相当于把遍历的信息附加在链表上.这样的二叉链表叫做线索链表.


    浪费的指针域

    如下图,中序遍历这个二叉树,把空着的指针指向前驱或者后继节点


    填充后继
    填充前驱

    但是现在又有一个问题,我们不知道一个节点,它的指针域存的到底是孩子还是前驱/后继,因此需要作出区分.
    增加ltag和rtag做标记,ltag为0表示左孩子,为1表示前驱,rtag表示右孩子或者后继.


    增加两个tag
    增加两个ta

    swift实现:

    class Node{
        
        var name : String
        weak var leftChild : Node?
        weak var rightChild : Node?
        var ltag = 0
        var rtag = 0
        
        init(_ dataName:String) {
            name = dataName
        }
        
        deinit {
            print("node:\(name) release")
        }
    }
    
    

    线索链表已经是一个双向链表,在swift中指针不能再直接引用了,否则无法释放,因此需要weak引用;
    ltag和rtag初始化为0比较方便;

     var nodes = [Node]()
      var pre : Node?
    
    func createBinaryTree(_ list:[String]) -> Node?{
            if(list.count == 0){
                return nil
            }
            var index = 0
            
            func createTree() -> Node?{
                let str = list[index]
                var tree : Node?
                index += 1;
                if index < list.count && str != "#"{
                    tree = Node.init(str)
                    nodes.append(tree!)
                    tree!.leftChild = createTree()
                    tree!.rightChild = createTree()
                }
                return tree
            }
                    
            return createTree()
        }
    

    设置一个全局的指针pre,用于标记当前处理的那个节点;
    初始化一个数组nodes用于真正的节点存储;
    根据前序遍历生成二叉树,每个节点存储在nodes中.

    func binaryTreeThread(_ root : Node?){
            guard let node = root else {
                return
            }
            if node.leftChild == nil{
                node.ltag = 1
                node.leftChild = pre
            }
            if pre?.rightChild == nil{
                pre?.rtag = 1
                pre?.rightChild = node
            }
            pre = node
            if node.ltag == 0{
                binaryTreeThread(node.leftChild)
            }
            if node.rtag == 0{
                binaryTreeThread(node.rightChild)
            }
            
        }
    

    实现线索化,如果leftChild是空的,则把leftChild设置为前一个节点pre,也就是前驱;如果前一个节点的rightChild是空的,就把rightChild设置为当前的节点,也就是后继,然后更新pre;最后,为了防止循环无法停止,需要在tag为0的情况下进行递归.

    func binaryTreeForEach(_ root : Node?){
            guard let node = root else {
                return
            }
            print(node.name + "前驱是\(node.ltag == 1 ? node.leftChild!.name : "")"  + "后继是\(node.rtag == 1 ? node.rightChild!.name : "")")
            if node.ltag == 0{
                binaryTreeForEach(node.leftChild)
            }
            if node.rtag == 0{
                binaryTreeForEach(node.rightChild)
            }
        }
    

    现在遍历方法也要修改,不然会无限循环;
    在遍历的时候输出前驱和后继

            let node = createBinaryTree(["A","B","#","D","#","#","C","#","#"])
            binaryTreeThread(node)
            binaryTreeForEach(node)
            nodes.removeAll()
            pre = nil
    

    最后调用方法,然后置空,全部释放.


    输出结果

    现在我们添加一个头结点,让头结点的lchild指向中序遍历的第一个节点N1,rchild指向中序遍历的最后一个节点N2;
    并且N1的lchild指向头结点,也就是头结点是N1的前驱,N2的rchild指向头结点,也就是头结点是N2的后继.
    这样一来,从N1开始,顺序访问后继一直到头结点,就可以遍历整个二叉树,反过来从N2开始顺序访问前驱也可以.

    二:赫夫曼树

    首先看一个例子,学生的考试成绩需要按照分段筛选到不同的评优等级;
    假如学生的成绩际分布是这样的:


    成绩分布

    那么下面这段代码虽然合理但是显然其效率是有优化空间的.


    条件筛选
    树形结构

    我们调整一下,把分布概率更大的节点放在前面,显然效率会提高.


    调整后的树

    1.定义
    有这样两个二叉树(图1-1),我们给叶节点加上权重,权重根据业务有具体的意义;
    从一个节点到另一个节点叫做路径,每间隔一个节点,路径长度增加1;
    从根节点到每一个节点的路径长度之和, 叫做树的路径长度;下图中左边的树,根节点到其他节点一共8条路径,路径长度是1+1+2+2+3+3+4+4 = 20; 右边是1+2+3+3+2+1+2+2 = 16.
    节点的路径长度乘上该节点的权,就叫带权路径长度;
    而树的带权路径长度是所有的叶节点的带权路径长度之和;左边是1x5+2x15+3x40+4x30+4x10 = 315;右边是3x5+3x15+2x40+2x30+2x10 = 220.
    带权路径长度(WPL)最短的二叉树树,就叫做赫夫曼树.

    图1-1

    2.构造赫夫曼树
    首先将权值最小的两个节点组合成一个子树N1,注意权值小的设置为左孩子,现在N1的权值是15;
    然后将N1和权值倒数第三小的节点组成子树N2,同样权值小的设置为左,也就是15(N1)和15(B)比较,这里左右都行,现在N2的权值是30;

    N1和N2
    重复上面的过程,当放置到D时,N4的权值是60,大于C的40,此时C是左孩子,然后只剩一个根节点,设置为T;
    赫夫曼树完成

    3.赫夫曼编码
    现在要发送一段文字"BADCADFEED",一共出现六个字母ABCDEF,进行编码

    编码表
    假设六个字母的权值是A 27,B 8,C 15, D 15, E 30, F 5;如下图左边的树.
    接着把这个树的左孩子改成权值0,右孩子改成权值1;如下图右边的树.
    树和改造后的树
    现在每个字母根据路径就得到了唯一的编码;
    新的编码表
    结果对比
    并且它还要一个好处,就是不会出现包含其他编码字串的情况出现;
    解析的时候也通过赫夫曼树来解析,只要一个个数字顺着路径走就可以了.
    解析

    定义:设需要编码的字符集为{d1,d2...dn},各个字符出现的频率是{w1,w2....wn},以d1,d2...dn为叶节点,以w1,w2...wn作为节点的权值来构造哈赫夫曼树,规定该树的左分支为0,右分支为1,则字符的路径对应的编码就是赫夫曼编码.

    相关文章

      网友评论

          本文标题:数据结构与算法基础七:线索二叉树与赫夫曼树

          本文链接:https://www.haomeiwen.com/subject/wxsdsltx.html