首先得了解下优先队列和普通的队列的区别:
普通队列:先进先出,后进后出
优先队列:出队顺序和入队顺序无关,与优先级相关
最典型的一个例子:
在操作系统中进行任务的调度,操作系统会为这些任务分配资源和cpu的时间片,具体分配资源的时候,操作系统就要看各个任务的优先级,动态的选择优先级最高的任务执行
对于优先队队列的实现依旧有多种底层,但是在这里采用二叉堆为底层
二叉堆的性质:
1.二叉堆是一个完全二叉树
2.堆中某个节点的值总是大于等于(或小于等于)其子节点, 对应的就是最大堆和最小堆
由于我们的二叉堆采用数组作为存储,所以在这里可以采用之前的动态数组作为底层来存储
首先定义我们的构造函数
private Array<E> data; public MaxHeap(int capaticy) { data = new Array<>(capaticy); } public MaxHeap() { data = new Array<>(); }
为了后续操作方便,编写几个辅助函数
/** * 通过数组索引获取到父节点的索引 * @param index * @return */ private int parent(int index) { if (index == 0) { throw new IllegalArgumentException("index - 0 doesn't have parent"); } return (index - 1) / 2; } /** * 通过索引获取到左孩子的索引 * @param index * @return */ private int leftChild(int index) { return index * 2 + 1; } private int rightChild(int index) { return index * 2 + 2; }
接下来就要实现向对中添加元素
可以看见52的插入是不满足最大堆的性质的,所以说接下来要做的就是依次将52与它的父节点比较,如果比它的父节点小,就进行交换(SiftUp操作)
public void add(E e) { //先将新的元素放置到数组的末尾 data.addLast(e); //将新加入的元素上浮到正确位置 siftUp(data.getSize() - 1); } /** * 将新加入的元素上浮到指定位置 * 每次都与此元素的父节点进行比较,如果比父节点比此元素小的话,进行交换 * 再对这个交换后的位置j继续比较,直至交换到正确位置 * * @param k */ private void siftUp(int k) { while (k > 0 && data.get(parent(k)).compareTo(data.get(k)) < 0) { data.swap(k, parent(k)); //指向新的位置 k = parent(k); } }
还有就是从堆中取出元素,只能从堆中取出最大元素
- 首先将堆中的第一个元素与最后一个元素进行交换
- 然后删除掉最后一个元素,这个元素就是堆中最大的元素
- 如果现在的根节点的左右孩子都不小于这个节点,将根节点与左右孩子中较大的那一个进行交换
- 重复执行上一步,直至最后左右节点的值都不大于此节点就停止,或者就是当这个节点的左孩子的索引已经越 界了
public E findMax() {
if (data.getSize() == 0) {
throw new IllegalArgumentException("Doesn't find max .");
}
return data.get(0);
}
/**
* 取出优先队列中优先级最大的元素
* 首先找到队列中最大的元素,将最后一个元素 与之交换,再删除掉最后一个元素
* 随后将第0个元素下沉至正确位置
*
* @return
*/
public E extractMax() {
E e = findMax();
data.swap(0, data.getSize() - 1);
data.removeLast();
siftDown(0);
return e;
}
/**
* 下沉操作,整个就是不停的将此元素与子节点中的最大值进行交换
*
* @param k
*/
private void siftDown(int k) {
while (leftChild(k) < data.getSize()) {
int j = leftChild(k);
//如果此元素的右结点存在,并且比左结点大,就将索引指向右子结点
if (j + 1 < data.getSize() && data.get(j + 1).compareTo(data.get(j)) > 0) {
j = rightChild(k);
}
//当父节点比子节点大的时候就不需要交换了
if (data.get(k).compareTo(data.get(j)) >= 0) {
break;
}
data.swap(k, j);
k = j;
}
}
替换元素(replace)
取出堆中的最大元素,再放入新的 元素
1.将堆顶的元素直接替换成新的元素
2.将堆顶的元素执行siftDown操作,下沉至正确位置即可
将任意数组整理成堆(heapify)
1.将这个数组当成一个完全二叉树
2.找到最后一个非叶子节点,从这个非叶子节点依次倒序进行siftDown操作
最后一个非叶子节点就是数组中最后一个元素的父节点
/**
* 传入数组,将数组转化为优先队列存储
* @param arr
*/
public MaxHeap(E[] arr) {
data = new Array<>(arr);
//从最后一个叶子节点的父节点
for (int i = parent(arr.length - 1); i >= 0; i--) {
siftDown(i);
}
}
有了Heapify这个操作之后我们可以将一个一个添加进堆与这个操作进行下时间上的比较
使用heapify操作的时间复杂度为O(n),一个一个添加的时间复杂度为O(nlogn)
@Test public void test1() { int n = 1000000; Random random = new Random(); Integer[] testData = new Integer[n]; for (int i = 0; i < testData.length; i++) { testData[i] = random.nextInt(); } System.out.println("without heapify :" + testHeap(testData, false) + "s"); System.out.println("with heapify :" + testHeap(testData, true) + "s"); } /** * 测试使用 heapify 与不使用 heapify 的时间差异 * * @param testData * @param isHeapify 为 true 表示使用heapify * @return */ private static double testHeap(Integer[] testData, boolean isHeapify) { long startTime = System.nanoTime(); MaxHeap<Integer> maxHeap; if (isHeapify) { maxHeap = new MaxHeap<Integer>(testData); } else { maxHeap = new MaxHeap<Integer>(); for (int num : testData) { maxHeap.add(num); } } //添加完成之后,又将堆中的元素按照从大到小的顺序取出来放入数组中 int[] arr = new int[testData.length]; for (int i = 0; i < testData.length; i++) { arr[i] = maxHeap.extractMax(); } //如果数组中的前一个元素比后一个小,说明操作有问题,抛一个异常 for (int i = 1; i < testData.length; i++) { if (arr[i - 1] < arr[i]) { throw new IllegalArgumentException("Error "); } } System.out.println("Test MaxHeap completed"); long endTime = System.nanoTime(); return (endTime - startTime) / 1000000000.0; }
优先队列可以以最大堆做为底层实现
public class PriorityQueue<E extends Comparable<E>> implements Queue<E> { private MaxHeap<E> maxHeap; public PriorityQueue() { maxHeap = new MaxHeap<E>(); } @Override public int getSize() { return maxHeap.size(); } @Override public void enqueue(E e) { maxHeap.add(e); } @Override public E dequeue() { return maxHeap.extractMax(); } @Override public boolean isEmpty() { return maxHeap.isEmpty(); } @Override public E getFront() { return maxHeap.findMax(); } }
还有整个最大堆的代码>
import DynamicArray.Array; /** * 完全二叉堆 满足父节点的值应该比子节点的值都大 * * @param <E> */ public class MaxHeap<E extends Comparable<E>> { private Array<E> data; public MaxHeap(int capaticy) { data = new Array<>(capaticy); } /** * 传入数组,将数组转化为优先队列存储 * @param arr */ public MaxHeap(E[] arr) { data = new Array<>(arr); //从最后一个叶子节点的父节点 for (int i = parent(arr.length - 1); i >= 0; i--) { siftDown(i); } } public MaxHeap() { data = new Array<>(); } public int size() { return data.getSize(); } public boolean isEmpty() { return data.isEmpty(); } public void add(E e) { //先将新的元素放置到数组的末尾 data.addLast(e); //将新加入的元素上浮到正确位置 siftUp(data.getSize() - 1); } /** * 将新加入的元素上浮到指定位置 * 每次都与此元素的父节点进行比较,如果比父节点比此元素小的话,进行交换 * 再对这个交换后的位置继续比较,直至交换到正确位置 * * @param k */ private void siftUp(int k) { while (k > 0 && data.get(parent(k)).compareTo(data.get(k)) < 0) { data.swap(k, parent(k)); //指向新的位置 k = parent(k); } } public E findMax() { if (data.getSize() == 0) { throw new IllegalArgumentException("Doesn't find max ."); } return data.get(0); } /** * 取出优先队列中优先级最大的元素 * 首先找到队列中最大的元素,将最后一个元素 与之交换,再删除掉最后一个元素 * 随后将第0个元素下沉至正确位置 * * @return */ public E extractMax() { E e = findMax(); data.swap(0, data.getSize() - 1); data.removeLast(); siftDown(0); return e; } /** * 下沉操作,整个就是不停的将此元素与子节点中的最大值进行交换 * * @param k */ private void siftDown(int k) { while (leftChild(k) < data.getSize()) { int j = leftChild(k); //如果此元素的右结点存在,并且比左结点大,就将索引指向右子结点 //此时的data[j] 就是左右孩子中的最大值 if (j + 1 < data.getSize() && data.get(j + 1).compareTo(data.get(j)) > 0) { j = rightChild(k); } //当父节点比子节点大的时候就不需要交换了 if (data.get(k).compareTo(data.get(j)) >= 0) { break; } data.swap(k, j); k = j; } } /** * 取出最大元素 ,并且替换为元素 e * * @param e * @return */ public E replace(E e) { E ret = findMax(); data.set(0, e); siftDown(0); return e; } /** * 通过数组索引获取到父节点的索引 * @param index * @return */ private int parent(int index) { if (index == 0) { throw new IllegalArgumentException("index - 0 doesn't have parent"); } return (index - 1) / 2; } /** * 通过索引获取到左孩子的索引 * @param index * @return */ private int leftChild(int index) { return index * 2 + 1; } private int rightChild(int index) { return index * 2 + 2; } }