前言:
现时小伙伴们对“优先队列java大根堆”可能比较讲究,大家都需要了解一些“优先队列java大根堆”的相关文章。那么小编也在网上搜集了一些关于“优先队列java大根堆””的相关资讯,希望姐妹们能喜欢,大家快快来学习一下吧!队也叫做优先级队列。队列是一种先进先出的数据结构,有的时候,要操作的数据有优先级,需要优先级高的先出队列,就是这样的数据结构。堆的数据结构提供了两个最基本的操作:返回最高级优先级对象和添加新的对象。堆实际上是一种在完全二叉树的基础上进行元素的调整。
1、堆基本概念
如果有一个集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储 在一 个一维数组中,并满足:Ki = K2i+1 且 Ki >= K2i+2) i = 0,1,2…,则称为 小堆(或大 堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆的性质:1,堆总是一颗完全二叉树。2,堆中的某个节点的值总是不大于或不小于其父亲节点的值。这里就是指大根堆或小根堆。
堆是一颗使用数组来存储的完全二叉树,整个的遍历过程使用了层序遍历。和二叉树相比,堆是线性顺序存储的,但是在画图理解的时候,我们画成完全二叉树的形式。
2、堆的创建
关于堆的创建,需要用到堆的向下调整。因为堆是把元素存在数组中的,如果我们假设i为节点在数组中的下标,那么根据完全二叉树的性质,i是被称为父亲节点的话:
(1)如果i为0,则i表示的节点为根节点,否则i节点的双亲节点为 (i - 1)/2;
(2)如果2 * i + 1 小于节点个数,则节点i的左孩子下标为2 * i + 1,否则没有孩子;
(3)如果2 * i + 2 小于节点个数,则节点i的右孩子下标为2 * i + 2,否则没有孩子。
这个在后面堆的创建中需要用到,下面我们以建立大堆为例。堆(优先级队列)的存储数据的方式是使用数组来存储的,先定义好数组和有效数组的大小。
class TestHeap { public int[] elem; public int usedSize; public TestHeap(int[] elem) { this.elem = new int[10]; }}
然后把要建立堆的数组进行拷贝。也可以不拷贝,直接使用。拷贝好之后,从什么地方开始调整?这就需要画图来看一看。把使用数组存储的数据画成完全二叉树的形式后,发现:
直接从0下标开始的话,调整起来不方便。所以从尾节点对应的父节点开始,一步一步调整,一直到0下标。以9下标为例,它的父节点是4下标,是整个数组有效长度的一半。所以调整要从整个数组有效长度的一半开始调整。使用createHeap方法。
class TestHeap { public int[] elem; public int usedSize; public TestHeap(int[] elem) { this.elem = new int[10]; } //建堆 public void createHeap(int[] array) { //拷贝过程 for (int i = 0; i < array.length; i++) { this.elem[i] = array[i]; ++this.usedSize; } //开始调整过程 for (int p = (usedSize - 1 - 1) >>> 1; p >= 0; p--) { shiftDown(p, usedSize); } }}
调整的过程使用一个shiftDown方法,传进去这个下标和数组的有效长度。
shiftDown方法
这个方法就是具体的调整方法。为了方便,我们定义parent和child变量。其中,parent就是父亲节点,child就是父亲节点对应的孩子节点,有左右孩子之分。他们的关系需要用到前面的性质。其实从图上就能看出来,以3号下标为例,它的左右孩子下标就是7和8。对于左孩子来说,和父节点的下标关系就是child = parent * 2 + 1;右孩子下标就是左孩子下标再加1。要注意的是整个的组织是按照完全二叉树的方式来组织的,调整需要知道父亲和孩子节点的下标关系。而孩子节点的下标在整个数组有效长度里面就说明存在的,比如父节点为5下标的节点。
private void shiftDown(int root, int len) { int parent = root; int child = 1 + root << 1;}
然后就开始循环调整。整个堆成为大根堆,不能只调整一部分,要调整到从根节点到叶子节点的全部。大根堆是父亲节点的值大于孩子节点的。所以需要先找到孩子节点的最大值,再和父亲节点对比交换。
进入循环的条件是只要有孩子就可以调整了。父亲节点和孩子节点调整的前提是至少要有一个孩子。进入循环后,寻找左右孩子的最大值和父亲节点交换。寻找的前提是要存在左右孩子。
找到孩子的最大值后,就和父节点对比,交换。如果没有交换,说明已经是大根堆了,就直接结束循环。否则,就移动父亲节点到孩子节点的位置继续调整。
建立堆的时间复杂度是O(N)。
private void shiftDown(int root, int len) { int parent = root; int child = 1 + root << 1; //进入这个循环,至少有一个孩子 while (child < len) { //如果有右孩子,找到左右孩子的最大值 if (child + 1 < len && this.elem[child] < this.elem[child + 1]) { ++child; } //child一定是最大值的下标 //进行交换 if (this.elem[child] > this.elem[parent]) { swap(elem, parent, child); parent = child; child = 1 + parent << 1; } else { break; } } } private void swap(int[] elem, int parent, int child) { int tmp = this.elem[child]; this.elem[child] = this.elem[parent]; this.elem[parent] = tmp; }3、堆的插入
堆的插入是把元素插入到堆的尾部,也就是数组的尾部,然后进行调整。插入的时候要判断数组元素的空间够不够,不够的话就进行扩容。因为要调整的是一条路径,需要向上调整。使用push方法。调整的方法使用shiftUp方法。
public void push(int val) { //判断是不是满了 if (isFull()) { elem = Arrays.copyOf(elem,2 * elem.length); elem[usedSize] = val; //开始调整 shiftUp(usedSize); ++usedSize; } }
private boolean isFull() { return usedSize == elem.length; }shiftUp方法
因为整个数据按照完全二叉树的形式组织的,所以,新插入的元素要找到它的父亲节点。对于孩子节点,设下标为i,它的父亲节点下标为 ( i - 1 )/ 2。就然后进行比较交换。这里的交换可以直接使用前面的swap方法。一次比较交换完成后,还要继续比较,一直到整个路径上的元素调整好。如果没有,就结束。
private void shiftUp(int child) { int parent = (child - 1) >>> 1; while (child > 0) { if (elem[child] > elem[parent]) { int tmp = elem[child]; elem[child] = elem[parent]; elem[parent] = tmp; child = parent; parent = (child - 1) >>> 1; } else { break; } } }4、堆的删除
堆删除需要删除堆顶的元素,但是不能直接在堆顶上删,而是要把堆顶元素和堆尾元素交换后来删除。减少有效元素个数,同时,对堆顶元素进行向下调整。整个的过程需要保持大根堆。删除之前先要判断这个堆是不是空的堆。
public void poll() { if (isEmpty()) { return; } int tmp = elem[0]; elem[0] = elem[usedSize - 1]; elem[usedSize - 1] = tmp; --usedSize; //调整 shiftDown(0, usedSize); } //判断堆是不是空 private boolean isEmpty() { return usedSize == 0; }
从堆的插入和删除来看,堆的操作中的核心就是对堆的向上调整和向下调整方法。
5、获取堆顶元素
直接返回第一个元素下标就可以。如果是空的堆,就返回-1。
public int peek() { if (isEmpty()) { return -1; } return elem[0]; }6、堆排序
要从小到大排序,需要建立大根堆,而从大到小排序,需要建立小根堆。从最后一个元素开始调整。每一次调整将该元素与堆顶元素交换,在向下调整。
public void HeapSort() { int end = this.usedSize - 1; while (end > 0) { swap(this.elem, 0, end); shiftDown(0, end); --end; } }
堆中最重要的几个操作都调用了向上调整方法或向下调整方法。搞明白这两个方法是关键。
7、Java官方的实现
Java集合框架中提供了PriorityQueue和PriorityBlockingQueue两种类型的优先级队列,PriorityQueue是线程不安全的,PriorityBlockingQueue是线程安全的,这里介绍PriorityQueue。
使用这个方法的时候,要注意:
(1)PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出 ClassCastException异常;
(2)不能插入null对象,否则会抛出NullPointerException;
(3)PriorityQueue默认情况下是小堆---即每次获取到的元素都是最小的元素。
PriorityQueue (Java Platform SE 7 ) (oracle.com)
PriorityQueue<Integer> queue = new PriorityQueue<Integer>(); queue.add(3); queue.add(2); queue.add(1); System.out.println(queue); System.out.println("============="); queue.poll(); System.out.println(queue); System.out.println("============="); System.out.println(queue.peek());
要改为大堆创建的形式,需要实现接口。
class IntCmp implements Comparator<Integer> { @Override public int compare(Integer o1, Integer o2) { return o2 - o1; }}public class Main { public static void main(String[] args) { PriorityQueue<Integer> queue = new PriorityQueue<Integer>(new IntCmp()); queue.offer(3); queue.offer(2); queue.offer(1); System.out.println(queue); System.out.println("============="); queue.poll(); System.out.println(queue); System.out.println("============="); System.out.println(queue.peek()); }}
标签: #优先队列java大根堆