数据结构知识点(二)——哈夫曼树、字典树

7、哈夫曼树

7.1、哈夫曼树的概述

  哈夫曼树,也称最优二叉树,它是n个带权叶子结点构成的所有二叉树中,带权路径长度最小的二叉树。

  所谓树的带权路径长度,就是树中所有的叶节点的权值乘上其到根结点的路径长度。

  权值越大的结点离根结点越近的二叉树才是最优二叉树。

  树的带权路径路径长度(WPL)是从树根到每一结点的路径长度之和,记为WPL=(W1*L1+W2*L2+W3*L3+...+Wn*Ln),N个权值Wi(i=1,2,...n)构成一棵有N个叶结点的二叉树,相应的叶结点的路径长度为Li(i=1,2,...n)。

(a)WPL = 9*2 + 4*2 + 2*2 + 5*2 = 40

(b)WPL = 9 + 4*2 + 2*3 + 5*3 = 38

(c)WPL = 9 + 5*2 + 4*3 + 2*3 = 37

所以(c)是最优二叉树。

7.2、构建哈夫曼树的流程分析

  给出一组数字,对这组数字进行排序,排序方式如下:

  (1)取出根节点权值最小的两颗二叉树

  (2)组成一颗新的二叉树,前面取出来的两颗二叉树是新二叉树的两个子树

  (3)新二叉树的根节点的权值是前两个二叉树的根节点的权值之和

如图:


 


 


 


 


 


 


 

7.3、哈夫曼树的代码实现

(1)定义哈夫曼树数据结构:

public class Node<E> {
    E data;
    double weight;
    Node leftChild;
    Node rightChild;

    public Node(E data, double weight) {
        super();
        this.data = data;
        this.weight = weight;
    }

    public String toString() {
        return "Node[data=" + data + ", weight=" + weight + "]";
    }
}

(2)根据树的结点形成哈夫曼树:

    /**
     * 构造哈夫曼树
     *
     * @param nodes
     *            节点集合
     * @return 构造出来的哈夫曼树的根节点
     */
    private static Node createTree(List<Node> nodes) {
        // 只要nodes数组中还有2个以上的节点
        while (nodes.size() > 1) {
            quickSort(nodes);
            //获取权值最小的两个节点
            Node left = nodes.get(nodes.size()-1);
            Node right = nodes.get(nodes.size()-2);

            //生成新节点,新节点的权值为两个子节点的权值之和
            Node parent = new Node(null, left.weight + right.weight);

            //让新节点作为两个权值最小节点的父节点
            parent.leftChild = left;
            parent.rightChild = right;

            //删除权值最小的两个节点
            nodes.remove(nodes.size()-1);
            nodes.remove(nodes.size()-1);

            //将新节点加入到集合中
            nodes.add(parent);
        }

        return nodes.get(0);
    }

    /**
     * 将指定集合中的i和j索引处的元素交换
     *
     * @param nodes
     * @param i
     * @param j
     */
    private static void swap(List<Node> nodes, int i, int j) {
        Node tmp;
        tmp = nodes.get(i);
        nodes.set(i, nodes.get(j));
        nodes.set(j, tmp);
    }

    /**
     * 实现快速排序算法,用于对节点进行排序
     *
     * @param nodes
     * @param start
     * @param end
     */
    private static void subSort(List<Node> nodes, int start, int end) {
        if (start < end) {
            // 以第一个元素作为分界值
            Node base = nodes.get(start);
            // i从左边搜索,搜索大于分界值的元素的索引
            int i = start;
            // j从右边开始搜索,搜索小于分界值的元素的索引
            int j = end + 1;
            while (true) {
                // 找到大于分界值的元素的索引,或者i已经到了end处
                while (i < end && nodes.get(++i).weight >= base.weight)
                    ;
                // 找到小于分界值的元素的索引,或者j已经到了start处
                while (j > start && nodes.get(--j).weight <= base.weight)
                    ;

                if (i < j) {
                    swap(nodes, i, j);
                } else {
                    break;
                }
            }

            swap(nodes, start, j);

            //递归左边子序列
            subSort(nodes, start, j - 1);
            //递归右边子序列
            subSort(nodes, j + 1, end);
        }
    }

    public static void quickSort(List<Node> nodes){
        subSort(nodes, 0, nodes.size()-1);
    }

    //广度优先遍历
    public static List<Node> breadthFirst(Node root){
        Queue<Node> queue = new ArrayDeque<Node>();
        List<Node> list = new ArrayList<Node>();

        if(root!=null){
            //将根元素加入“队列”
            queue.offer(root);
        }

        while(!queue.isEmpty()){
            //将该队列的“队尾”元素加入到list中
            list.add(queue.peek());
            Node p = queue.poll();

            //如果左子节点不为null,将它加入到队列
            if(p.leftChild != null){
                queue.offer(p.leftChild);
            }

            //如果右子节点不为null,将它加入到队列
            if(p.rightChild != null){
                queue.offer(p.rightChild);
            }
        }

        return list;
    }

(3)测试方法:

public static void main(String[] args) {
        List<Node> nodes = new ArrayList<Node>();

        nodes.add(new Node("A", 40.0));
        nodes.add(new Node("B", 8.0));
        nodes.add(new Node("C", 10.0));
        nodes.add(new Node("D", 30.0));
        nodes.add(new Node("E", 10.0));
        nodes.add(new Node("F", 2.0));

        Node root = HuffmanTree.createTree(nodes);

        System.out.println(breadthFirst(root));

    }

结果:

[Node[data=null, weight=100.0], Node[data=A, weight=40.0], Node[data=null, weight=60.0], Node[data=null, weight=30.0], Node[data=D, weight=30.0], Node[data=C, weight=10.0], Node[data=null, weight=20.0], Node[data=null, weight=10.0], Node[data=E, weight=10.0], Node[data=F, weight=2.0], Node[data=B, weight=8.0]]

7.3、哈夫曼编码

  哈夫曼(Huffman)编码算法是基于二叉树构建编码压缩结构的,算法根据文本字符出现的频率,重新对字符进行编码。

以“we will we will r u”进行压缩:

(1)统计每个字符出现的频率

 

(2)按出现频率高低将其放入一个优先级队列中,从左到右依次为频率逐渐增加。

 

 

(3)创建哈夫曼树

 

 

(4)把二叉树分支中左边的支路编码为0,右边分支表示为1

 

(5)依次遍历这颗二叉树得到所有字符的编码,可以得到下面这张编码表:

 

 

8、字典树

  字典树,又称Trie树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来节约存储空间,最大限度地减少无谓的字符串比较,查询效率比哈希表高。 

 

 

字典树的具体代码实现:

(1)定义Trie树数据结构:

   public class TrieNode {
    Map<Character, TrieNode> childdren;
    boolean wordEnd;

    public TrieNode() {
        childdren = new HashMap<Character, TrieNode>();
        wordEnd = false;
    }

(2)实现字典树:

//字典树的java实现
public class Trie {
    private TrieNode root;

    public Trie() {
        root = new TrieNode();
        root.wordEnd = false;
    }
    //字典树的插入方法
    public void insert(String word) {
        TrieNode node = root;
        for (int i = 0; i < word.length(); i++) {
            Character c = new Character(word.charAt(i));
            if (!node.childdren.containsKey(c)) {
                node.childdren.put(c, new TrieNode());
            }
            node = node.childdren.get(c);
        }
        node.wordEnd = true;
    }
    //字典树的搜索方法
    public boolean search(String word) {
        TrieNode node = root;
        boolean found = true;
        for (int i = 0; i < word.length(); i++) {
            Character c = new Character(word.charAt(i));
            if (!node.childdren.containsKey(c)) {
                return false;
            }
            node = node.childdren.get(c);
        }
        return found && node.wordEnd;
    }
    //查找是否前缀存在
    public boolean startsWith(String prefix) {
        TrieNode node = root;
        boolean found = true;
        for (int i = 0; i < prefix.length(); i++) {
            Character c = new Character(prefix.charAt(i));
            if (!node.childdren.containsKey(c)) {
                return false;
            }
            node = node.childdren.get(c);
        }
        return found;
    }

}

 

9、B树

  B树,全称平衡多路查找树,B-tree有多条路,即父节点有多个子节点。

  如图:

 

 

 说明:

  1. B树的阶:节点的最多子节点个数。比如2-3树的阶是3,2-3-4树的阶是4。
  2. B树的搜索:从根结点开始,对结点内的关键字(有序)序列进行二分查找;如果命中则结束,否则进入查询关键字所属范围的儿子节点;以此重复下去,直到所对应的儿子指针为空,或已经是叶子节点。
  3. 关键字集合分布在整棵树中,即叶子节点和非叶子节点都存放数据。
  4. 搜索有可能在非叶子节点结束。
  5. 搜索性能等价于在关键字全集内做一次二分查找。

 

10、B+树

  B+树是B树的变体,也是一种多路搜索树。

  如图:

   B+树的说明:

  1. B+树的搜索与B树基本相同,区别是B+树只有到达叶子节点才命中(B树可以在非叶子节点命中),其性能也等价于在关键字全集做一次二分查找。
  2. 所有关键字都出现在叶子节点的链表中(数据只能在叶子节点上),且链表中的关键字(数据)是有序的。
  3. 不可能在非叶子节点命中。
  4. 非叶子节点相当于是叶子节点的索引,叶子节点相当于存储数据的数据层。
  5. B+树更适合文件索引系统。

 

11、B*树

  是B+树的变体,在B+树的非根和非叶子结点再增加指向兄弟的指针;

  如图:

 

  

B*树的说明:
1、B*树定义了非叶子节点关键字个数至少是(2/3)M,即块的使用率是2/3,二B+树的块是最低使用率是1/22、空间使用率:
B+树的分裂:当一个结点满时,分配一个新的结点,并将原结点中1/2的数据复制到新结点,最后在父结点中增加新结点的指针;B+树的分裂只影响原结点和父结点,而不会影响兄弟结点,所以它不需要指向兄弟的指针; B*树的分裂:当一个结点满时,如果它的下一个兄弟结点未满,那么将一部分数据移到兄弟结点中,再在原结点插入关键字,最后修改父结点中兄弟结点的关键字(因为兄弟结点的关键字范围改变了);如果兄弟也满了,则在原结点与兄弟结点之间增加新结点,并各复制1/3的数据到新结点,最后在父结点增加新结点的指针; 所以,B*树分配新结点的概率比B+树要低,空间使用率更高;
posted @ 2020-01-05 12:29  jet-software  阅读(1046)  评论(0编辑  收藏  举报