JAVA数据结构(七)

优先队列和堆

一、优先队列
 普通队列:先进先出;后今后出
 优先队列:出队顺序和入队顺序无关;和优先级相关

  • 优先队列本质也是队列,因此接口是没有变化的。但是在实现接口方法的时候具体的方法是需要做改变的。
    在这里插入图片描述
  • 堆的基本结构
    二叉堆:满足一些特殊性质的二叉树
    1.二叉堆是一颗完全二叉树
    满二叉树:除了叶子节点外,其他节点的左右节点都不为空
    在这里插入图片描述
    完全二叉树:不一定是一个满二叉树,但是它不满的部分(缺失节点的部分)一定是在整棵树的右下侧。
    在这里插入图片描述
    二叉堆的性质:堆中某个节点的值总时小于等于其父节点的值(即根节点的值大于等于其孩子节点的值,最大堆。相应可以定义最小堆)。
  • 用数组来表示一个完全二叉树
    在这里插入图片描述
  • 当数据的索引从0开始计算时
    在这里插入图片描述
  • 根据以上分析,最大堆的初始化代码为
package cn.itcast.day7;

public class MaxHeap<E extends Comparable<E>> {
	private myArray<E> data;
	public MaxHeap(int capacity) {
		data = new myArray<>(capacity);
	}
	public MaxHeap() {
		data = new myArray<>();
	}
	//返回堆中的元素个数
	public int size() {
		return data.getSize();
	}
	//返回一个布尔值,表示堆中是否为空
	public boolean isEmpty() {
		return data.isEmpty();
	}
	
	//返回完全二叉树的数组表示中,一个索引所表示的元素的父亲节点的索引
	private int parent(int index) {
		if(index == 0) {
			throw new IllegalArgumentException("错误");
		}
		return (index-1)/2;
	}
	//返回完全二叉树的数组表示中,一个索引所表示的元素的左孩子节点的索引
	private int leftChild(int index) {
		return index*2+1;
	}
	//返回完全二叉树的数组表示中,一个索引所表示的元素的右孩子节点的索引
	private int rightChild(int index) {
		return index*2+2;
	}
}

  • 向堆中添加元素和Sift Up
    如:向下面数组添加元素时,首先直接在数组尾部添加数据52。得到[62,41,30,28,16,22,13,19,17,15,52],此时52的索引为10.
    在这里插入图片描述
    将52挂接在原始数组所构成的完全二叉树上,然后与相应的父亲节点进行比较,如果小于相应的父亲节点,则进行位置的交换。
    如下:将52挂接在16的右子树,大于16,交换位置,交换位置后,再与其新的父节点(41)比较,还是要打,再与41交换位置。依此类推。(完成了上浮过程)
    在这里插入图片描述
  • 代码实现(利用到之前实现的Array数组):
    1.首先在myArrayl类中添加元素交换的方法
public void swap(int i,int j) { //交换索引为i和j的元素的位置
		if(i<0 || i>= size || j <0 || j>=size) {
			throw new IllegalArgumentException("错误");
		}
		E t = data[i];
		data[i] = data[j];
		data[j] = t;
	}

2.添加元素的实现

	//向堆中添加元素
	public void add(E e) {
		data.addLast(e);//向尾部添加元素
		shiftUp(data.getSize()-1);	//添加的元素的索引
	}
	private void shiftUp(int k) {
		//当k没有到达根节点并且索引k对应位置的元素的值小于当前父亲节点的值,则继续循环
		while(k > 0 && data.get(parent(k)).compareTo(data.get(k))<0) {
			data.swap(k, parent(k)); //交换位置
			k = parent(k);	//交换位置后,再让当前的k变为之前的父亲节点所在的索引位置
		}
	}
  • 取出堆中的最大元素和Sift Down
    1.堆顶的元素是最大的元素,如果直接将此元素取出,则要对剩下的左右子树部分进行融合成一个新的堆。这样的操作较为复杂,因此考虑另外一种方法:先将数组表示的堆底元素(数组的最后一个元素)放到堆顶,然后将堆低元素去掉。反映到数组中,则是将原来的数组[62,52,30,28,41,22,13,19,17,15,16]转变为[16,52,30,28,41,22,13,19,17,15]。元素个数减1且堆顶元素变为之前的堆底元素。
    在这里插入图片描述
    2.在进行上述操作后,不再满足最大堆的性质。此时,需要将新生成的堆的堆顶元素进行“下浮”操作:每次都将元素与其两个孩子节点进行比较,选择它的孩子节点中最大的那个元素比较,如果它的孩子节点中最大的元素比它自己还要大,那么它就和这个孩子节点中最大的元素交换位置。交换位置后,再将16与其新的孩子节点进行比较,判断是否还要继续下沉。如下所示:
    在这里插入图片描述
  • 代码实现
	//k看堆中的最大元素
	public E findMax() {
		if(data.getSize()==0) {
			throw new IllegalArgumentException("错误");
		}
		return data.get(0);
	}
	//取出堆中最大元素
	public E extractMax() {
		E ret = findMax();
		data.swap(0, data.getSize()-1); //将堆尾元素与堆顶元素交换位置
		data.removeLast();	//然后将堆尾元素去掉
		siftDown(0);	//下沉操作
		return ret;
	}
	
	private void siftDown(int k) {
		//如果k所处的位置已经没有孩子,是叶子节点时,循环终止;
		while(leftChild(k) < data.getSize()) {		//如果
			int j = leftChild(k);
			//j+1<data.getSize()说明有右孩子(rightChild(k)=leftChild(k)+1)
			if(j+1<data.getSize() && data.get(j+1).compareTo(data.get(j))>0) { //右孩子的值大
				j = rightChild(k);
				//此时data[j]是leftChild和rightChild中的最大值
			}
			if(data.get(k).compareTo(data.get(j))>=0) {//如果当前值比它的孩子节点的值都要大,就不用继续下沉了
				break;	
			}
			data.swap(k, j);
			k = j;
		}
	}

3.测试用例

package cn.itcast.day7;

import java.util.Random;

public class Main {
	public static void main(String[] args) {
		int n = 100000;
		MaxHeap<Integer> maxHeap = new MaxHeap<>();
		Random random = new Random();
		for(int i=0;i<n;i++) {
			maxHeap.add(random.nextInt(Integer.MAX_VALUE));
		}
		int[] arr = new int[n];
		for(int i=0;i<n;i++) {
			arr[i] = maxHeap.extractMax();//将所有取出的最大值存放到数组
		}
		
		for(int i=1;i<n;i++) {
			if(arr[i-1]<arr[i]) {	//不满足此条件说明堆出错
				throw new IllegalArgumentException("出错");
			}
		}
		System.out.println("运行结束");
	}
}
  • replace操作
    在这里插入图片描述
  • heapify:将任意数组整理成堆的形状
    思路:对于一个给定的数组,直接看作是一个完全二叉树,然后找到对于当前数组中的最后一个非叶子节点开始计算,如下图,22是最后一个非叶子节点。找到这个节点后,倒着从后向前不断的进行Sift Down操作。
    Question:如何定位最后一个非叶子节点的索引?(经典面试题)只需要找到最后一个节点对的索引,然后找到这个节点的父节点即可。
    在这里插入图片描述
    具体的操作如下图演示:
    在这里插入图片描述
  • 将n个元素逐个插入到一个空堆中,算法复杂度是O(nlogn)
  • heapify的过程,算法复杂度为O(n)

代码实现:
1.先在myArray类中添加构造函数

	public myArray(E[] arr) {
		data = (E[])new Object[arr.length];
		for(int i=0;i<arr.length;i++) {
			data[i] = arr[i];
		}
		size = arr.length;
	}

2.创建构造函数

	public MaxHeap(E[] arr) {
		data = new myArray<>(arr);
		for(int i = parent(arr.length-1);i>=0;i--) {
			siftDown(i);
		}
	}

二、基于堆的优先队列

  • 代码实现
package cn.itcast.day7;

public class PriorityQueue<E extends Comparable<E>> implements Queue<E>{
	private MaxHeap<E> maxHeap;
	
	public PriorityQueue() {
		maxHeap = new MaxHeap<>();
	}
	@Override
	public int getSize() {
		return maxHeap.size();
	}
	@Override
	public boolean isEmpty() {
		return maxHeap.isEmpty();
	}
	@Override
	public E getFront() {
		return maxHeap.findMax(); //findMax()中已经进行了堆为空的处理
	}
	@Override
	public void enqueue(E e) {
		maxHeap.add(e);
	}
	@Override
	public E dequeue() {
		return maxHeap.extractMax();
	}
}

三、优先队列的经典问题
1.在1000000个元素中选出前100名?(在N个元素中选出前M个元素)

  • 使用排序算法:算法复杂度为NlogN
  • 使用优先队列:算法复杂度为NlogM
    优先队列解决:使用优先队列,维护当前看到的前M个元素----> 先将M个元素放进这个优先队列中,之后每次看到一个新的元素,如果这个新的元素比当前的优先队列中的最小的元素还要大的话,那么就把这个优先队列中的最小的元素给扔出去,再换上这个新元素。(需要使用最小堆:要非常快速的取出当前看到的前M个元素中的最小的元素。因为我们是不断的将前M个大的元素中的最小的元素进行替换
    leetCode347题:前K个高频元素
class Solution {
	private class Freq implements Comparable<Freq>{
		int e,freq;
		public Freq(int e,int freq) {
			this.e = e;
			this.freq = freq;
		}
		public int compareTo(Freq o) {
			if(this.freq<o.freq) {		//注意:在这里使用的“优先”规则,定义的是数字较小的数优先出列(优先队列的优先规则是可以自定义的)
				return 1;
			}else if(this.freq>o.freq) {
				return -1;
			}else {
				return 0;
			}
		}
		
	}
    public List<Integer> topKFrequent(int[] nums, int k) {
    	TreeMap<Integer,Integer> map = new TreeMap<>();
        for(int num:nums) {
        	if(map.containsKey(num)) {
        		map.put(num, map.get(num)+1);
        	}else {
        		map.put(num, 1);
        	}
        }
        
        PriorityQueue<Freq> pq = new PriorityQueue<>();
        for(int key:map.keySet()) { //map.keySet() 得到所有的键
        	if(pq.getSize()<k) {
        		pq.enqueue(new Freq(key,map.get(key)));
        	}else if(map.get(key)>pq.getFront().freq) {
        		pq.dequeue();
        		pq.enqueue(new Freq(key,map.get(key)));
        	}
        }
        LinkedList<Integer> res = new LinkedList<>();
        while(!pq.isEmpty()) {
        	res.add(pq.dequeue().e);
        }
        return res;
    }
}
  • 利用java标准库的优先队列解决
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.PriorityQueue;
import java.util.TreeMap;

class Solution {
    public List<Integer> topKFrequent(int[] nums, int k) {
    	TreeMap<Integer,Integer> map = new TreeMap<>();
        for(int num:nums) {
        	if(map.containsKey(num)) {
        		map.put(num, map.get(num)+1);
        	}else {
        		map.put(num, 1);
        	}
        }
        
        PriorityQueue<Integer> pq = new PriorityQueue<>(new Comparator<Integer>() {
        	public int compare(Integer a, Integer b) {
    			return map.get(a) - map.get(b);
    		}	
        });	//java内部维护的是一个最小堆.创建优先队列时可以接收一个比较器,使用的匿名内部类
        for(int key:map.keySet()) { //map.keySet() 得到所有的键
        	if(pq.size()<k) {
        		pq.add(key);
        	}else if(map.get(key)>map.get(pq.peek())) {
        		pq.remove();
        		pq.add(key);
        	}
        }
        LinkedList<Integer> res = new LinkedList<>();
        while(!pq.isEmpty()) {
        	res.add(pq.remove());
        }
        return res;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值