文章目录

背景

在​​《自已做动画及编写程序搞清楚最大堆的实现原理》​​这篇文章中,我们通过动图分析编 码自行实现了最大堆的数据结构,并在文章末尾提到了最大堆的应用–优先队列。该文将通过“四字成语汉字频率统计”的实际应用,把最大堆与优先队列的原理再次进行深入剖析。

构建最大堆

  • 由于堆是一种特殊的完全二叉树,可以利用数组集合形成线性存储的数据结构。
  • 通过每个父结点与左右子结点大小的比较,使得根结点最大,且每个父结点都大于左右结点。
  • 用自行实现的优先队列进行四字成语汉字频率统计_java

代码实现
/**
* 最大堆的底层实现
*
* @author zhuhuix
* @date 2020-06-28
* @date 2020-07-01 通过数组构建最大堆
*/
public class MaxHeap<E extends Comparable<E>> {

// 存放元素的数组集合
private ArrayList<E> list;

MaxHeap() {
this.list = new ArrayList<>();
}


// 得到左孩子索引
private int getLeftChildIndex(int i) {
return (2 * i + 1);
}

// 得到右孩子索引
private int getRightChildIndex(int i) {
return (2 * i + 2);
}

// 得到父结点索引
private int getParentIndex(int i) {
if (i == 0) {
throw new IllegalArgumentException("非法索引值");
} else {
return ((i - 1) / 2);
}
}

// 通过数组创建最大堆
public void heapify(E[] arr){
this.list.addAll(Arrays.asList(arr));
for (int i=getParentIndex(this.list.size()-1);i>=0;i--){
shiftDown(i);
}
}

// 添加元素
public void add(E e) {
this.list.add(e);
/**
* 将加入的结点与父结点进行比较:
* 如果加入的结点大于父结点,则进行上浮
* 直至新结点小于或等于父结点为止
*/
shiftUp(this.list.size() - 1);
}

// 元素上浮
private void shiftUp(int i){
while (i > 0) {
E current = this.list.get(i);
E parent = this.list.get(getParentIndex(i));
// 如果父结点元素大于当前加入的元素,则进行交换
if (parent.compareTo(current) < 0) {
// 交换新加入的结点与父结点的位置
Collections.swap(this.list, i, getParentIndex(i));
} else {
break;
}
i = getParentIndex(i);
}
}


// 查找最大元素
public E findMax() {
if (this.list.size() == 0) {
return null;
}
// 最大堆中的元素永远在根结点
return this.list.get(0);
}

// 取出最大元素
public E getMax() {
if (findMax() != null) {
E e = findMax();

/**
* 取出最大元素后,需要把堆中第二大的元素放置在根结点:
* 将根结点元素与最后面的元素进行交换,
* 让最后面的元素出现在根结点,并移除最大元素
* 将根结点的元素与左右孩子结点比较,直至根结点的元素变成最大值
*/
int i = 0;
Collections.swap(this.list, i, this.list.size() - 1);
this.list.remove(this.list.size() - 1);
shiftDown(i);

return e;
} else {
return null;
}
}

// 元素下沉
private void shiftDown(int i){
// 通过循环进行当前结点与左右孩子结点的大小比较
while (getLeftChildIndex(i) < this.list.size()) {
int leftIndex = getLeftChildIndex(i);

// 通过比较左右孩子的元素哪个较大,确定当前结点与哪个孩子进行交换
int index = leftIndex;
if (getRightChildIndex(i) < this.list.size() &&
this.list.get(getRightChildIndex(i)).compareTo(this.list.get(leftIndex)) > 0) {
index = getRightChildIndex(i);
}
// 如果当前结点都大于左右孩子,则结束比较
if (this.list.get(i).compareTo(this.list.get(index)) >= 0) {
break;
}

Collections.swap(this.list, i, index);
i = index;
}
}

// 返回最大堆中的元素个数
public int getSize(){
return this.list.size();
}

// 用元素取代最大堆中的最大元素
public E replace(E e){
// 暂存最大元素
E ret= findMax();
// 将最大元素的位置放入要取代的元素
this.list.set(0,e);
// 通过下浮操作形成最大堆
shiftDown(0);
return ret;
}
}
测试
/**
* 最大堆的底层实现--测试程序
* * @author zhuhuix
* @date 2020-07-01
*/
public class MaxHeapTest {
public static void main(String[] args) {
MaxHeap<Integer> maxHeap = new MaxHeap<>();

// 将10个数字加入形成最大堆
Integer[] arrays = {19,29,4,2,27,0,38,15,12,31};
maxHeap.heapify(arrays);

// 依次从堆中取出最大值
for (int i = 0; i < arrays.length; i++) {
System.out.println("第"+(i+1)+"次取出堆目前的最大值:"+maxHeap.getMax());
}
}
}

用自行实现的优先队列进行四字成语汉字频率统计_java_02

通过最大堆实现优先队列

  • 具有优先级最高的元素最先出队。
/**
* 用最大堆的数据结构实现优先队列
*
* @author zhuhuix
* @date 2020-07-01
*/
public class PriorityQueue<E extends Comparable<E>> {
private MaxHeap<E> mhp;

PriorityQueue() {
mhp = new MaxHeap<>();
}

// 入队
public void enqueue(E e) {
mhp.add(e);
}

// 优选级最高的元素出队
public E dequeue() {
return mhp.getMax();
}

// 查看优先级最高的元素
public E getFront() {
return mhp.findMax();
}

// 队列元素个数
public int getSize() {
return mhp.getSize();
}
}

成语汉字频率统计案例

统计四字成语文件中的汉字出现频率的前5位
  • 通过最大堆的数据结构实现优先队列,优先队列具有入队、出队、查看最先出队元素的功能;
  • 建立一个汉字频率的类,并实现比较大小的方法:出现频率越低认为优先级越高。
  • 编写优先队列测试类,设置汉字出现的次数,验证汉字出现频率越低的最先出队。
  • 编写成语汉字统计主程序:
    – 读取四字成语的文件,形成字符串;
    – 将字符串转换成汉字数组,通过HashMap统计汉字及其出现频率;
    – 将HashMap中每个汉字与出现频率,依次放入优先队列中,若后续放入的汉字频率高于队列优先级最高的汉字频率,则进行替换。
项目结构

用自行实现的优先队列进行四字成语汉字频率统计_结点_03

汉字频率的类
/**
* 使用优先队列统计成语列表中频率出现最高的前五个字--汉字频率的类
*
* @author zhuhuix
* @date 2020-07-01
*/
public class CharFrequency implements Comparable<CharFrequency> {

private char c;
private int frequency;

CharFrequency(char c, int frequency) {
this.c = c;
this.frequency = frequency;
}

// 频率低则顺序靠前,优先级最高
@Override
public int compareTo(CharFrequency o) {
if (this.frequency < o.frequency) {
return 1;
} else if (this.frequency > o.frequency) {
return -1;
} else {
return 0;
}
}

public char getC() {
return c;
}

public void setC(char c) {
this.c = c;
}

public int getFrequency() {
return frequency;
}

public void setFrequency(int frequency) {
this.frequency = frequency;
}

@Override
public String toString() {
return c +", 出现频率=" + frequency;
}
}
优先队列测试类
/**
* 优先队列测试类--频率越低,优先级最高
*
* @author zhuhuix
* @date 2020-07-01
*/
public class PriorityQueueTest {
public static void main(String[] args) {

PriorityQueue<CharFrequency> priorityQueue = new PriorityQueue<>();

// 将汉字及出现频率放入优先队列
priorityQueue.enqueue(new CharFrequency('二',2));
priorityQueue.enqueue(new CharFrequency('五',5));
priorityQueue.enqueue(new CharFrequency('三',3));
priorityQueue.enqueue(new CharFrequency('四',4));
priorityQueue.enqueue(new CharFrequency('一',1));

// 测试输出
System.out.println(priorityQueue.dequeue());
System.out.println(priorityQueue.dequeue());
System.out.println(priorityQueue.dequeue());
System.out.println(priorityQueue.dequeue());
System.out.println(priorityQueue.dequeue());

}
}

用自行实现的优先队列进行四字成语汉字频率统计_优先队列_04

成语汉字统计主程序
/**
* 使用优先队列统计成语列表中频率出现最高的前五个汉字
*
* @author zhuhuix
* @date 2020-07-01
*/
public class IdiomFreqCount {
public final static int count = 5;

public static void main(String[] args) throws IOException {

// 读取硬盘上的四字成语文件载入字符串对象中
InputStreamReader inputStreamReader = new InputStreamReader(new FileInputStream("c:\\四字成语.txt"), "GBK");
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
StringBuilder stringBuilder = new StringBuilder();
String idiom;
int len = 0;
while ((idiom = bufferedReader.readLine()) != null) {
stringBuilder.append(idiom);
len++;
}
bufferedReader.close();
System.out.println("总共成语" + len + "个");

// 将字符串对象转换成数组
char[] array = stringBuilder.toString().toCharArray();

// 遍历数组中的字符,将字符做为key,出现频率做为value,放入HashMap

HashMap<Character, Integer> hashMap = new HashMap<>();
for (int i = 0; i < array.length; i++) {
if (!hashMap.containsKey(array[i])) {
hashMap.put(array[i], 1);
} else {
// 如果字符已存在,则出现次数加1
hashMap.put(array[i], hashMap.get(array[i]) + 1);
}
}
System.out.println("总共出现汉字" + hashMap.keySet().size() + "个");

// 根据HashMap中的键与值,构建CharFrequency对象,按CharFrequency的顺序放入优先队列中
PriorityQueue<CharFrequency> priorityQueue = new PriorityQueue<>();
for (char key : hashMap.keySet()) {
// 如果队列中不满统计的字符个数,则直接将对象放入队列中
if (priorityQueue.getSize() < count) {
priorityQueue.enqueue(new CharFrequency(key, hashMap.get(key)));
} else {
// 如果当前字符的出现频率大于优先队列中排在最前面字符的出现频率,则队列出队并则将该字放入队列中
// 按此循环,可以使汉字频率出现高的逐渐替换频率出现低的
if (hashMap.get(key) > priorityQueue.getFront().getFrequency()) {
priorityQueue.repalcePriority(new CharFrequency(key, hashMap.get(key)));
}
}
}

// 依次取出最大堆中的前五个元素,则为出现频率最高的五个字
int index = Math.min(priorityQueue.getSize(), count);
for (int i = 0; i < index; i++) {
System.out.println(priorityQueue.dequeue().toString());
}
}

}
  • 输入文件:
  • 用自行实现的优先队列进行四字成语汉字频率统计_java_05

  • 输出结果:
  • 用自行实现的优先队列进行四字成语汉字频率统计_结点_06