蓝桥杯——Huffuman树
0.前言
不得不说,蓝桥杯VIP的题目质量确实高很多,也很锻炼人。这道Huffuman树的题目也是一个很有意思的题目。蓝桥杯VIP的题目可以在整个网站免费看到并评测不限制语言。C语言网,所有的测评数据可以去GitHub上获取,比如Blue_Bridge_Cup。
1.解题思路
1.1原题
1.2思路一(LinkedList
)
这是我最开始想到的可行方法,考虑我们需要做哪些事情,首先是获取最小的两个数,然后把它们剔除,再把这两个数的合作为一个新的元素加入。因为涉及元素的变动,常规的数组不太能满足要求,考虑队列。常规使用的队列一般为ArrayList
,它采用的是数据结构中的顺序存储,优点是随机查找效率高,但增删改效率低,对于这样需要大量增加删除的需求不太满足。另一个是LinkedList
,它采用的是单链表存储,适合对表内容有大量改动的情况。因此考虑使用LinkedList
来存储这些元素。
1.2.1方案概要
确定了底层使用LinkedList
存储之后,大致实现方案为,元素存储在integers
列表中,使用两个变量存储最小值firstMin
和第二小secondMin
的值,和为cost
,cost
的和为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细节问题
firstMin
和secondMin
初始值均为n*1000
的原因是,首先因为后面比较的时候,都是先与secondMin
比较的,所以必须保证secondMin
不能大于firstMin
。如果选择前两个数值中的最小值为firstMin
,另一个为secondMin
。容易出现的问题是,默认遍历是从第一个元素开始的,那么第一个元素肯定是小于secondMin
的,会导致secondMin
和firstMin
在不应该相等的情况下相等。之所以必须要从第一个开始,是由LinkedList
的结构决定的。我们说过顺序存储的优势在于随机查找,比如按照下标查找就是随机查找,但链式存储就不适合这种根据下标的随机查找,如果使用根据下标的随机查找,每次都需要在从列表第0号位置开始一个个的找,效率很低。所以必须采用foreach
这种基于迭代器的遍历方式,所谓迭代器在我的理解就是lazy操作,每次都找到当前元素的下一位即可,这种遍历的效率对于不适合随机查找的LinkedList
非常高。另外虽然可以通过元素获取到下标,然后把第一个元素跳过,问题在于,万一后面有一个和第一个元素相等的元素,此时根据元素获取下标的时候也会把它跳过。最后只能选择最大值来保证firstMin
和secondMin
肯定会被正常替换掉。初始每个元素最大值为1000,共有n个元素,那么再相加之后,最大值为n*1000
。- 剔除时候采用
removeFirstOccurrence()
,LinkedList
的移除元素的方式有很多,采用整个方法就是考虑重复元素的问题,这样可以只剔除一个元素。
1.3思路二(PriorityQueue
)
认真考虑一下整个题,其实本质上就是不断寻找一个数组中最小的两个元素的过程。我们当然可以使用不断排序的方式然后获取前两位,但那样效率太低,不够优雅,而且我们只需要选择最小的两个值就可以了,不需要全部元素都有序,换句话说,如果最小的元素被剔除了,我再获取一个最小的元素,这样我就获取到了最小的两个元素。如果对数据结构还有印象的话,应该记得一个叫堆排序的方式,以小根堆为例,逻辑结构是一棵完全二叉树,它只需要保证父节点不大于子节点即可,左右节点大小顺序不管。这样也就保证堆顶肯定是最小值,每次取堆顶数的时候数据复杂度为
O
(
1
)
O(1)
O(1),为了保持完全二叉树的结构,可以把最后一个元素,暂时放到堆顶,然后根据该元素的大小,调整到合适位置。之所以不能直接把剔除前左右孩子节点的最小值作为根节点,考虑的是,要保持完全二叉树的结构不变,只能重新定位。
比如下图这样的二叉树。
插入也是一样的道理,时间复杂度都是
O
(
log
2
N
)
O(\log_2{N})
O(log2N),毕竟是二叉树可以近似理解为二分法。
1.3.1方案概要
队列queue
存储元素,使用两个变量存储最小值firstMin
和第二小secondMin
的值,和为cost
,cost
的和为total
。对queue
进行两次remove()
操作,默认返回堆顶的元素,赋值为firstMin
和secondMin
。接着求和为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<Integer> 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 |
可明显看出方案二这种方式的效率非常高,原因就在于,它只是局部有序,而且采用了二叉树这种存储方式,无需过多遍历,使得增删改查的效率大大提升。
这次我又感受到了数据结构的厉害,数据结构真的很重要。