蓝桥杯——Huffuman树

蓝桥杯——Huffuman树

  • 0.前言
  • 1.解题思路
    • 1.1原题
    • 1.2思路一(`LinkedList`)
      • 1.2.1方案概要
      • 1.2.2代码
      • 1.2.3细节问题
    • 1.3思路二(`PriorityQueue`)
      • 1.3.1方案概要
      • 1.3.2代码
      • 1.3.3细节问题
  • 2.总结

0.前言

不得不说,蓝桥杯VIP的题目质量确实高很多,也很锻炼人。这道Huffuman树的题目也是一个很有意思的题目。蓝桥杯VIP的题目可以在整个网站免费看到并评测不限制语言。C语言网,所有的测评数据可以去GitHub上获取,比如Blue_Bridge_Cup。

1.解题思路

1.1原题

蓝桥杯——Huffuman树_第1张图片

1.2思路一(LinkedList

这是我最开始想到的可行方法,考虑我们需要做哪些事情,首先是获取最小的两个数,然后把它们剔除,再把这两个数的合作为一个新的元素加入。因为涉及元素的变动,常规的数组不太能满足要求,考虑队列。常规使用的队列一般为ArrayList,它采用的是数据结构中的顺序存储,优点是随机查找效率高,但增删改效率低,对于这样需要大量增加删除的需求不太满足。另一个是LinkedList,它采用的是单链表存储,适合对表内容有大量改动的情况。因此考虑使用LinkedList来存储这些元素。

1.2.1方案概要

确定了底层使用LinkedList存储之后,大致实现方案为,元素存储在integers列表中,使用两个变量存储最小值firstMin和第二小secondMin的值,和为costcost的和为total。遍历integers,不断和firstMin以及secondMin比较,最终获取最小值和第二小值,剔除这两个元素,把cost添加进integers中,并把数值加入到total中。如此循环直至integers中元素只有一个。

1.2.2代码

import java.util.LinkedList;
import java.util.Scanner;

public class Main {
    public static void main(String[] agrs) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        //只有一个元素则直接返回
        if (n == 1) {
            System.out.println(sc.nextInt());
        } else {
            //最小值
            int firstMin;
            //第二小值
            int secondMin;
            //最小值和第二小值的和
            int cost;
            //花费的总和
            int total = 0;
            //存储的元素
            LinkedList<Integer> integers = new LinkedList<>();
            //元素赋值
            for (int i = 0; i < n; i++) {
                integers.add(sc.nextInt());
            }
            //循环直至integers的元素只有一个
            while (integers.size() != 1) {
                //默认最小值
                firstMin = n * 1000;
                //默认第二小值
                secondMin = n * 1000;
                //遍历integers
                for (Integer integer : integers) {
                    //如果比第二小的值小
                    if (integer < secondMin) {
                        //且比最小值小
                        if (integer < firstMin) {
                            //原最小值为第二小值
                            secondMin = firstMin;
                            //当前元素为最小值
                            firstMin = integer;
                        } else {
                            //在secondMin和secondMin之间,当前元素为secondMin
                            secondMin = integer;
                        }
                    }
                }
                //花费
                cost = firstMin + secondMin;
                //总和
                total += cost;
                //剔除
                integers.removeFirstOccurrence(firstMin);
                integers.removeFirstOccurrence(secondMin);
                //添加
                integers.add(cost);
            }
            System.out.println(total);
        }
        sc.close();
    }
}

1.2.3细节问题

  • firstMinsecondMin初始值均为n*1000的原因是,首先因为后面比较的时候,都是先与secondMin比较的,所以必须保证secondMin不能大于firstMin。如果选择前两个数值中的最小值为firstMin,另一个为secondMin。容易出现的问题是,默认遍历是从第一个元素开始的,那么第一个元素肯定是小于secondMin的,会导致secondMinfirstMin在不应该相等的情况下相等。之所以必须要从第一个开始,是由LinkedList的结构决定的。我们说过顺序存储的优势在于随机查找,比如按照下标查找就是随机查找,但链式存储就不适合这种根据下标的随机查找,如果使用根据下标的随机查找,每次都需要在从列表第0号位置开始一个个的找,效率很低。所以必须采用foreach这种基于迭代器的遍历方式,所谓迭代器在我的理解就是lazy操作,每次都找到当前元素的下一位即可,这种遍历的效率对于不适合随机查找的LinkedList非常高。另外虽然可以通过元素获取到下标,然后把第一个元素跳过,问题在于,万一后面有一个和第一个元素相等的元素,此时根据元素获取下标的时候也会把它跳过。最后只能选择最大值来保证firstMinsecondMin肯定会被正常替换掉。初始每个元素最大值为1000,共有n个元素,那么再相加之后,最大值为n*1000
  • 剔除时候采用removeFirstOccurrence()LinkedList的移除元素的方式有很多,采用整个方法就是考虑重复元素的问题,这样可以只剔除一个元素。

1.3思路二(PriorityQueue

认真考虑一下整个题,其实本质上就是不断寻找一个数组中最小的两个元素的过程。我们当然可以使用不断排序的方式然后获取前两位,但那样效率太低,不够优雅,而且我们只需要选择最小的两个值就可以了,不需要全部元素都有序,换句话说,如果最小的元素被剔除了,我再获取一个最小的元素,这样我就获取到了最小的两个元素。如果对数据结构还有印象的话,应该记得一个叫堆排序的方式,以小根堆为例,逻辑结构是一棵完全二叉树,它只需要保证父节点不大于子节点即可,左右节点大小顺序不管。这样也就保证堆顶肯定是最小值,每次取堆顶数的时候数据复杂度为 O ( 1 ) O(1) O(1),为了保持完全二叉树的结构,可以把最后一个元素,暂时放到堆顶,然后根据该元素的大小,调整到合适位置。之所以不能直接把剔除前左右孩子节点的最小值作为根节点,考虑的是,要保持完全二叉树的结构不变,只能重新定位。
比如下图这样的二叉树。
蓝桥杯——Huffuman树_第2张图片
插入也是一样的道理,时间复杂度都是 O ( log ⁡ 2 N ) O(\log_2{N}) O(log2N),毕竟是二叉树可以近似理解为二分法。

1.3.1方案概要

队列queue存储元素,使用两个变量存储最小值firstMin和第二小secondMin的值,和为costcost的和为total。对queue进行两次remove()操作,默认返回堆顶的元素,赋值为firstMinsecondMin。接着求和为cost,total+=cost,queue.add(cost)即可,如此循环直至queue中只有一个元素

1.3.2代码

import java.util.PriorityQueue;
import java.util.Scanner;

public class Main {
    public static void main(String[] agrs) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        if (n == 1) {
            System.out.println(sc.nextInt());
        } else {
            //优先队列,元素为n个
            PriorityQueue<Integer> queue = new PriorityQueue<>(n);
            //赋值
            for (int i = 0; i < n; i++) {
                queue.add(sc.nextInt());
            }
            int firstMin;
            int secondMin;
            int cost;
            int total = 0;
            while (queue.size() != 1) {
                //返回并剔除首个元素
                firstMin = queue.remove();
                //返回并剔除首个元素
                secondMin = queue.remove();
                cost = firstMin + secondMin;
                //新增元素
                queue.offer(cost);
                total += cost;
            }
            System.out.println(total);
        }
        sc.close();
    }
}

1.3.3细节问题

  • 一定要赋值为PriorityQueue queue = new PriorityQueue<>(n);,这样才可以使用一些ProvorityQueue中的特定 方法,不再局限与Queue接口中给定的方法.
  • offer()add()区别,在PriorityQueue中没有区别,add()的源码如下
    /**
     * Inserts the specified element into this priority queue.
     *
     * @return {@code true} (as specified by {@link Collection#add})
     * @throws ClassCastException if the specified element cannot be
     *         compared with elements currently in this priority queue
     *         according to the priority queue's ordering
     * @throws NullPointerException if the specified element is null
     */
    public boolean add(E e) {
        return offer(e);
    }
  • 其余一些细节在思路一中有的说过了,可以自行查阅。具体关键ProvorityQueue的一些详解,可查阅深入Java集合系列之五:PriorityQueue。

2.总结

先对比一下运行时间和内存消耗

方案 耗时 内存
方案一 301 16612
方案二 251 16588

可明显看出方案二这种方式的效率非常高,原因就在于,它只是局部有序,而且采用了二叉树这种存储方式,无需过多遍历,使得增删改查的效率大大提升。
这次我又感受到了数据结构的厉害,数据结构真的很重要。

你可能感兴趣的:(蓝桥杯,二叉树,数据结构,队列,java)