目录
优先队列
- 普通的队列是一种先进先出的数据结构,元素在队列尾追加,而从队列头删除
- 在优先队列中,元素被赋予优先级,当访问元素时具有最高优先级的元素最先出队
- 优先队列具有最高级先出(fistin,largestout)的行为特征
堆
定义和基本操作
- 二叉堆是满足一些特殊的二叉树
- 二叉堆是一颗完全二叉树(叶子节点要么在最后一层且满足左连续,要么在倒数第二层且满足右连续)
- 堆中任一节点的值总是大于等于其孩子节点值(最大堆:这种优先级可以自己来指定)
- 实际可以不用使用创建一个节点的方式,可以使用数组的形式来表示二叉堆,下标从0开始时: left(i) = 2 * i + 1, right(i) = 2 * i + 2, parent(i) = Mth.floor((i - 1) / 2)
- 可以利用堆来进行堆排序,升序利用大根堆,降序利用小根堆。
利用数组定义一个最大堆结构
public class MaxHeap<E extends Comparable<E>> {
private ArrayList<E> data;
public MaxHeap(){
data = new ArrayList<>();
}
public MaxHeap(int capacity){
data = new ArrayList<>(capacity);
}
//返回堆中数组元素的个数
public int size(){
return data.size();
}
//判断堆中是否为空
public boolean isEmpty(){
return data.isEmpty();
}
//得到父节点的索引值
public int parent(int index){
//表示的已经为堆的根节点了,再没有任何根节点
if(index <= 0){
throw new RuntimeException("index 值不合法");
}
return (index - 1) / 2;
}
//得到左孩子
public int leftChild(int index){
return index * 2 + 1;
}
//得到右孩子
public int rightChild(int index){
return index * 2 + 2;
}
}
往堆中添加元素-ShiftUP操作
添加元素(刚开始就相当于添加在了数组的最后一个位置):Shift Up(上浮动,不断和父亲节点比较然后往上调整)往最小堆中添加一个元素无需向下调整(只有向上调整的过程)
//添加节点,刚开始添加在最后一个位置
public void add(E e){
data.add(e);
//然后浮动元素:传递一个位置即可
shiftUp(data.size() - 1);
}
private void shiftUp(int k){
//注意:两个&&条件的先后次序
while(k > 0 && data.get(parent(k)).compareTo(data.get(k)) < 0){
//利用集合类给我们提供好的方法
//编码技巧:对于静态方法,我们可以先导入这个类,然后直接使用swap()方法即可
Collections.swap(data, k, parent(k));
k = parent(k);//将返回的结果重新赋值为parent(k),一直下去
}
}
取出堆中最大元素-ShiftDown(下沉操作)
- 把堆顶元素和最后一个元素进行交换,删除最后一个元素(也就是返回)
- 向下调整堆(和两个孩子的最大值进行比较,如果小的话,就需要交换,下沉)
//取出堆中最大元素
public E extractMax(){
//找到最大元素(就是根节点的值)
//交换根和最后一个叶子节点
//删除交换后的根
//不断的shiftDown
E ret = findMax();
Collections.swap(data, 0, data.size() - 1);
data.remove(data.size() - 1);
shifDown(0);
return ret;
}
public E findMax(){
if(data.size() == 0){
throw new RuntimeException("this is a empty heap");
}
return data.get(0);
}
private void shifDown(int k){
//当leftChild越界了的话就直接停止下沉,因为rightChild的值会比leftChild的值大
while(leftChild(k) < data.size()){
int j = leftChild(k);
//如果右孩子还没有越界的话,表明还有右孩子并且右孩子的值比左孩子的值要大
//过滤出左右孩子谁是最大的
if(j + 1 < data.size() && data.get(j+1).compareTo(data.get(j)) >0){
j ++;
}
//下沉结束,什么都不用再操作了
if(data.get(k).compareTo(data.get(j)) >= 0){
break;
}
//交换值并且重新赋值k的值
Collections.swap(data, k ,j);
k = j;
}
}
堆升序排序过程
首先把数组的n个数建成(Heapify操作:见下面内容)一棵大小为的大根堆,堆顶是整个所有元素中的最大值,把堆顶元素和堆的最后一个位置的元素进行交换,然后把最大值脱离出整个堆结构,放在数组的最后位置,作为数组的有序部分保存下来,接下来把大小为n-1的堆从上往下进行大根堆的调整(向下调整),调整出n-1个数中的最大值放在堆顶的位置,然后再把堆顶的值和整个堆中最后一个位置的值交换,同样作为整个数组的有序部分脱离出整个堆, 堆的大小从n-1变成了n-2,重复上面的过程,不断从堆顶往下调整,每次堆的大小都会减1, 数组的有序部分也会依次增加,当堆的大小变为1的时候, 整个数组就变得有序了。
public class Main {
//测试流程
//1. 往堆中不断添加元素
//2. 从堆中取出元素,放入数组
//3. 遍历数组中的值,看是否为从大到小排列的值
public static void main(String[] args) {
int n = 1000000;
MaxHeap<Integer> max = new MaxHeap<>();
Random random = new Random();
for(int i = 0; i < n; i++){
max.add(random.nextInt(Integer.MAX_VALUE));//测试上浮操作
}
int[] arr = new int[n];
for(int i = 0; i < n;i++){
arr[i] = max.extractMax();//测试下沉操作
}
//业务判断
for(int i = 1; i < n; i++){
if(arr[i] - arr[i-1] > 0){
throw new RuntimeException("Error");
}
}
System.out.println("yes");
}
}
堆的Replace操作
取出堆中的最大元素并且替换成新的元素
- 实现一∶先取出最大元素再添加元素,即shiftDown和shiftUp,就经历过了两次O(logn)的操作
- 实现二∶Replace操作:先直接把堆顶元素直接替换了,然后再不断shiftDown,只经过一层O(logn)即可实现
-
//取出堆中地最大元素,并且替换成元素e public E repalce(E e){ E max = findMax(); data.add(0, e); //替换元素,直接覆盖 shifDown(0); return max; }
堆的Heapify操作-将任意数组整理成堆
(1) 实现一∶扫描一遍数组,将扫描出来的数组元素添加到堆中O(nlogn):因为扫描一遍数组,需要O(n)的时间,add的操作需要O(logn)的时间,总共时间为O(nlogn)
(2) 实现二∶Heapity操作O(n):将数组看成一棵完全二叉树,然后从最后一个非叶子节点(在数组中表示就是最后一个叶子节点的父节点)开始倒着shiftDown;也就是把一个无序的完全二叉堆调整为二叉堆,本质上就是让所有非叶子节点下沉。 具体过程如下所示:
原始数组
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
15 | 17 | 19 | 13 | 22 | 16 | 28 | 30 | 41 | 62 |
调整过程
调整之后的结果
实现代码
public MaxHeap(ArrayList<E> arrayList){
//找到最后一个非叶子节点(最后一个节点的父节点)
int index = parent((arrayList.size() - 1));
for(int i = index; i >= 0; i--){
shifDown(i);
}
data = arrayList;//重现将arrayList指向给data
}
如何理解向上调整和向下调整过程?
往堆中插入元素后,利用向上调整。
将无序数据构建成堆时(heapify), 利用向下调整操作。
排序时,将根节点和最后一个节点交换后,也是利用向下调整操作。
用堆实现优先队列
(1) Queue.java
public interface Queue<E>{
void enqueue(E e);
E dequeue();
E getFront();
int getSize();
boolean isEmpty();
}
(2) MaxHeap.java
package heap;
import java.util.ArrayList;
import java.util.Collections;
public class MaxHeap<E extends Comparable<E>> {
private ArrayList<E> data;
public MaxHeap(){
data = new ArrayList<>();
}
public MaxHeap(int capacity){
data = new ArrayList<>(capacity);
}
//返回堆中数组元素的个数
public int size(){
return data.size();
}
//判断堆中是否为空
public boolean isEmpty(){
return data.isEmpty();
}
//得到父节点的索引值
public int parent(int index){
//表示的已经为堆的根节点了,再没有任何根节点
if(index <= 0){
throw new RuntimeException("index 值不合法");
}
return (index - 1) / 2;
}
//得到左孩子
public int leftChild(int index){
return index * 2 + 1;
}
//得到右孩子
public int rightChild(int index){
return index * 2 + 2;
}
//添加节点,刚开始添加在最后一个位置
public void add(E e){
data.add(e);
//然后浮动元素:传递一个位置即可
shiftUp(data.size() - 1);
}
private void shiftUp(int k){
//注意:两个&&条件的先后次序
while(k > 0 && data.get(parent(k)).compareTo(data.get(k)) < 0){
//利用集合类给我们提供好的方法
//编码技巧:对于静态方法,我们可以先导入这个类,然后直接使用swap()方法即可
Collections.swap(data, k, parent(k));
k = parent(k);//将返回的结果重新赋值为parent(k),一直下去
}
}
//取出堆中最大元素
public E extractMax(){
//找到最大元素(就是根节点的值)
//交换根和最后一个叶子节点
//删除交换后的根
//不断的shiftDown
E ret = findMax();
Collections.swap(data, 0, data.size() - 1);
data.remove(data.size() - 1);
shifDown(0);
return ret;
}
public E findMax(){
if(data.size() == 0){
throw new RuntimeException("this is a empty heap");
}
return data.get(0);
}
private void shifDown(int k){
//当leftChild越界了的话就直接停止下沉,因为rightChild的值会比leftChild的值大
while(leftChild(k) < data.size()){
int j = leftChild(k);
//如果右孩子还没有越界的话,表明还有右孩子并且右孩子的值比左孩子的值要大
//过滤出左右孩子谁是最大的
if(j + 1 < data.size() && data.get(j+1).compareTo(data.get(j)) >0){
j ++;
}
//下沉结束,什么都不用再操作了
if(data.get(k).compareTo(data.get(j)) >= 0){
break;
}
//交换值并且重新赋值k的值
Collections.swap(data, k ,j);
k = j;
}
}
}
(3) PriorityQueue.java
package heap;
import java.util.Collections;
public class PriorityQueue <E extends Comparable<E>> implements Queue<E>{
private MaxHeap<E> maxHeap;
public PriorityQueue(){
maxHeap = new MaxHeap<>();
}
public PriorityQueue(MaxHeap maxHeap){
this.maxHeap = maxHeap;
}
@Override
public void enqueue(E e) {
maxHeap.add(e);
}
@Override
public E dequeue() {
return maxHeap.extractMax();
}
@Override
public E getFront() {
return maxHeap.findMax();
}
@Override
public int getSize() {
return maxHeap.size();
}
@Override
public boolean isEmpty() {
return maxHeap.isEmpty();
}
}
优先队列的应用
(1)在n个元素中选出前m名
- 如果m=1,那么直接遍历一遍即可,时间复杂度就是O(n)
- n个元素排序,再选出前m个元素也可以完成,时间复杂度是O(nlogn)
- 如果使用优先队里,在O(nlogm)的时间复杂度中即可以完成该操作:利用优先队列维护队列中看到的前n个元素,如果扫到的元素比维护的队列中的m个元素的最小值还要大的话,就进行替换操作。(因此我们需要使用最小堆)
(2) 给定一个非空的整数数组,返回其中出现频率前k高的元素
- 输入:nums = [1,1,1,2,2,3], k = 2
- 输出: [1,2]
import java.util.*;
public class Solution {
//这里自己自定义一个对象:数字和出现的频率
private static class Freq implements Comparable<Freq>{
int e;
int freq;
public Freq(int e, int fre) {
this.e = e;
this.freq = fre;
}
//因为是最小堆
@Override
public int compareTo(Freq o) {
return o.freq - this.freq;
}
}
public static void main(String[] args) {
int[] nums = {1,1,1,2,2,3};
int k = 2;
topKEle(nums, k);
}
public static void topKEle(int[] nums, int k){
//1,统计数组中每个元素出现的频率
//2.将元素和出现的频率加工成一个对象放入到优先队列中
Map<Integer, Integer> map = new HashMap<>();
for(int num : nums){
if(map.containsKey(num)){
map.put(num, map.get(num) + 1);
}
else{
map.put(num, 1);
}
}
Queue<Freq> pq = new PriorityQueue<Freq>();
for(int key: map.keySet()){
if(pq.size() < k){
pq.offer(new Freq(key, map.get(key)));
}
else if(map.get(key) > pq.peek().freq){
pq.poll();
pq.offer(new Freq(key, map.get(key)));
}
}
//输出操作
List<Integer> list = new ArrayList<>();
while(!pq.isEmpty()){
list.add(pq.poll().e);
}
System.out.println(list);
}
}
Java默认提供的就是最小堆