[数据结构与算法]堆与优先级队列

时间:2022-12-28 16:16:10

优先级队列是一种数据结构,其中能被访问和删除的是仅具有最高优先级的元素。所谓优先级是通过一些方法对元素进行比较得出的。基本的思路是队列中存在等待服务的元素。对元素的选取不再严格基于先来先服务原则。


定义如下:优先级队列是这样的一种数据结构,对它的访问或者删除操作只能对集合中通过指定优先级方法得出的最高优先级元素进行。


优先级队列是公平的,对于任何两个具有相同优先级的元素,首先被删除的是那个在队列中存在时间最长的元素。如果元素是Integer类型且按照其排列的顺序进行比较,那么具有最高优先级的元素就是优先级队列中相应的int值最小的那个元素。如果元素是Integer类型,但是比较方法与原排列顺序相反,那么具有最高优先级的元素就是优先级队列中相应的int值最大的元素。到底是最小的还是最大的元素优先,具体的优先比较算法由实现PriorityQueue接口的程序员来决定。


那么堆与优先级队列又有什么关系呢,实质上堆可用来实现优先级队列,或者说堆就是一种优先级队列。由于堆的添加元素与删除元素时都会破坏堆结构,所以添加与删除进都要进行结构调整。一般普通的队列添加是在最后加入,但优先级队列却不一定添加到最后,他会按照优先级算法把它插入到队列当中去,出队时还是从第一个(也即最小元素,优先级最高)开始,即取根元素,这样保证了队列中优先级高的先服务,而不是先来先服务了。

Heap类是对PriorityQueue接口的一种高效实现。堆是一种完全二叉树。由于使用基于数组的完全二叉树的表示,可以根据子节点的索引快速计算出你父节点的索引,反之亦然,所以,使用数组来表示堆,它是利用了数组可以根据给定索引随机访问元素的特性。

 

 1 package priorityqueue;
 2 
 3 /**
 4  * 优先级队列接口
 5  * 
 6  * @author jzj
 7  * @data 2010-1-5
 8  * @param <E>
 9  */
10 public interface PriorityQueue<E extends Comparable<E>> {
11     int size();
12 
13     boolean isEmpty();
14 
15     //向队列加入元素,添加时会按照优先级别排序
16     void add(E elem);
17 
18     //取优先高的元素
19     E getMin();
20 
21     //从优先级队列中删除优先级最高的元素
22     E removeMin();
23 }

 

 1 package priorityqueue;
 2 
 3 import java.util.Comparator;
 4 import java.util.LinkedList;
 5 import java.util.ListIterator;
 6 
 7 /**
 8  * 基于 LinkedList 优先级队列的实现
 9  * 
10  * @author jzj
11  * @data 2010-1-5
12  * @param <E>
13  */
14 public class LinkedListPriorityQueue<E extends Comparable<E>> implements PriorityQueue<E> {
15     //LinkedList只用来存储,并不排序,排序操作由外界实现
16     private LinkedList<E> list;
17     private Comparator<E> comp;
18 
19     public LinkedListPriorityQueue() {
20         list = new LinkedList<E>();
21     }
22 
23     public LinkedListPriorityQueue(Comparator<E> c) {
24         this();
25         comp = c;
26     }
27 
28     private int compare(E elem1, E elem2) {
29         return comp == null ? elem1.compareTo(elem2) : comp.compare(elem1, elem2);
30     }
31 
32     /**
33      * 插队优先级实现
34      * @param elem
35      */
36     public void add(E elem) {
37         /*
38          * 如果list本身为空,或者元素的值大于等于list中最后一个元素的值,add方法就把元素对象
39          * 追加到list的末尾。否则,从头部逐个访问直到找到某个元素,该元素的值大于新增元素的值,
40          * 然后将给定的元素插入到该元素的前面。
41          */
42         if (list.isEmpty() || compare(elem, list.get(list.size() - 1)) >= 0) {
43             list.addLast(elem);
44             //或 list.add(elem),add方法其实就是在循环链最后即header头节点前添加元素
45         } else {
46             ListIterator<E> itr = list.listIterator();
47             //找到第一个比它大的元素
48             while (itr.hasNext() && compare(elem, itr.next()) >= 0)
49                 ;
50             itr.previous();//因为迭代器的next方法取之后会下移,所以要后移一位
51             itr.add(elem);//在比新元素大的节点前插入元素
52         }
53 
54     }
55 
56     public E getMin() {
57         return list.getFirst();
58     }
59 
60     public boolean isEmpty() {
61 
62         return list.isEmpty();
63     }
64 
65     public E removeMin() {
66         return list.removeFirst();
67     }
68 
69     public int size() {
70         return list.size();
71     }
72 }

 

  1 package priorityqueue.heap;
  2 
  3 import java.util.Arrays;
  4 import java.util.Comparator;
  5 import java.util.LinkedList;
  6 import java.util.NoSuchElementException;
  7 import java.util.Random;
  8 
  9 import priorityqueue.PriorityQueue;
 10 
 11 /**
 12  * 基于堆的优先级队列实现
 13  * 
 14  * 堆(heap)是一个完使用二叉树,它是一棵空树或
 15  * 1、根元素大于左右子节点(这时叫大顶堆)
 16  * 2、左右子节点又是堆
 17  * 
 18  * 堆是一种完全二叉树,所以这里先要熟悉要用的二叉树几个性质:
 19  * N(N>1)个节点的的完全二叉树从层次从左自右编号,最后一个分枝节点(非叶子节点)的编号为 N/2 取
 20  * 整。且对于编号 i(1<=i<=N 编号从1开始)有:父节点为 i/2 向下取整;若2i>N,则节点i没有左孩子
 21  * ,否则其左孩子为2i;若2i+1>N,则没有右孩子,否则其右孩子为2i+1。
 22  * 注,这里使用完全二叉树只是为了好描述算法,它只是一种逻辑结构,真真在实现时我们还是使用数组来存
 23  * 储这棵二叉树的,因为完全二叉树与数组可以连续的一一对应赶快来
 24  * 
 25  * 数组具有的随机访问特性对于堆的处理很方便:给定元素的索引值,很快就能得到该元素的子节点元素。例
 26  * 如,节点编号为i的元素的子节点分别为 2i 或 2i+1,那么对应到数组的索引号就分别为 2i-1 或 2i,
 27  * 节点编号为j的元素的父节点为 j/2 向下取整,对应的数组元素索引在父节点编号基础上减一即可得到。
 28  * 所以,堆可以快速交换父节点和小于它的子节点的值,这使得堆成为实现PriorityQueue接口的一种高效
 29  * 数据结构。
 30  * 
 31  * @author jzj
 32  * @data 2010-1-5
 33  * @param <E>
 34  */
 35 public class Heap<E extends Comparable<E>> implements PriorityQueue<E> {
 36     private E[] heap;//使用数组来实现堆存储
 37     private Comparator<E> comp;
 38     private int size;
 39 
 40     public Heap() {
 41         heap = (E[]) new Comparable[5];
 42     }
 43 
 44     public Heap(Comparator<E> c) {
 45         this();
 46         comp = c;
 47     }
 48 
 49     //类似于TreeMap中的私有方法 compare(Object k1, Object k2) 
 50     private int compare(E elem1, E elem2) {
 51         return comp == null ? elem1.compareTo(elem2) : comp.compare(elem1, elem2);
 52     }
 53 
 54     //添加元素,
 55     public void add(E elem) {
 56         if (++size == heap.length) {//预判断放入后是否满,如果满则先扩容后再加
 57             E[] newHeap = (E[]) new Comparable[2 * heap.length];
 58             System.arraycopy(heap, 0, newHeap, 0, size);
 59             heap = newHeap;
 60         }
 61         heap[size - 1] = elem;
 62         adjustUp();//添加后堆规则可能打破,所以需重新调整堆结构
 63     }
 64 
 65     //添加元素后向上调整堆结构,构造小顶堆,即添加的小的元素向上(根)浮
 66     private void adjustUp() {
 67         /* 添加28               28小于50,交换
 68          *   →        26            →                 26
 69          *           /  \                            /  \
 70          *         32    30                        32    30
 71          *        /  \   / \                      /  \   / \
 72          *       48  50 85 36                    48  28 85 36
 73          *      / \  / \                        / \  / \
 74          *    90 80 55 28                     90 80 55 50
 75          *    
 76          * 现28小于父,还需交换           现28大于父26,所以调整完成
 77          *   →        26           
 78          *           /  \                         
 79          *         28    30                      
 80          *        /  \   / \                      
 81          *       48  32 85 36                   
 82          *      / \  / \                       
 83          *    90 80 55 50      
 84          *                   
 85          */
 86         int child = size;//新加入的叶节点编号,即最后一个节点
 87         int parent;//父节点编号
 88         while (child > 1) {//如果调整到了根节点则直接退出
 89             parent = child / 2;//新增叶子节点的父节点编号
 90             //如果父节点小于等于子节点(新增节点),则退出
 91             if (compare(heap[parent - 1], heap[child - 1]) <= 0) {
 92                 break;
 93             }
 94             //如果新增节点小于它的父节点时,则交换
 95             E tmp = heap[parent - 1];
 96             heap[parent - 1] = heap[child - 1];
 97             heap[child - 1] = tmp;
 98             child = parent;//新增节点移到父节点位置,以便下次循环
 99         }
100     }
101 
102     public E getMin() {
103         if (size == 0) {
104             throw new NoSuchElementException();
105         }
106         return heap[0];
107     }
108 
109     public boolean isEmpty() {
110         return size == 0;
111     }
112 
113     //删除堆顶元素,使用最后一个元素替换根元素,然后再进结构调整
114     public E removeMin() {
115         if (size == 0) {
116             throw new NoSuchElementException();
117         }
118         /* 
119          * 删除根元素,根元素是最小元素,删除后使用树的最后叶节点替换他,此时堆结构会破坏,
120          * 需从根节点向下重新调整堆结构
121          * 
122          * 删除26             交换根26与最后叶节点55          55比最小子节点30大,需交换     
123          *   →      26              →            55                →
124          *         /  \                         /  \
125          *       32    30                     32    30
126          *      / \    / \                   / \    / \
127          *     48  50 85 36                 48 50  85 36 
128          *    / \  /                        /\ 
129          *  90 80 55                       90 80
130          * 
131          *                   55比最小子节点36大,需交换             
132          *          30               →                  30     堆结构恢复,调整结束 
133          *         /  \                                /  \
134          *       32    55                            32    36
135          *      / \    / \                          / \    / \
136          *     48  50 85 36                       48  50  85 55
137          *    / \  /                             / \   /
138          *  90 80 55                            90 80 55
139          */
140         //交换最后与根元素位置
141         E minElem = heap[0];//堆顶元素
142         heap[0] = heap[size - 1];
143         //不能把heap[--size]置为null,因为后面的堆排序方法heapSort要用
144         heap[--size] = minElem;
145         adjustDown(1);//删除后从根开始向下调整
146         return minElem;
147     }
148 
149     //堆结构调整,从指定的节点向下开始调整
150     private void adjustDown(int nodeNum) {
151 
152         int parent = nodeNum;//从指定节点开始往下调整
153         int child = 2 * parent;//指定节点左子节点编号
154         //如果左孩子存在
155         while (child <= size) {
156             int minNum = parent;//假设父就是最小的
157             //与左孩子比,如果比左孩子大,则最小设置为左孩子
158             if (compare(heap[parent - 1], heap[child - 1]) > 0) {
159                 minNum = child;
160             }
161 
162             //如果右孩子存在,则更小时
163             if ((child + 1) <= size && compare(heap[minNum - 1], heap[child]) > 0) {
164                 minNum = child + 1;
165             }
166 
167             //如果发现最小元素为子节点时需交换
168             if (minNum != parent) {
169                 E tmp = heap[minNum - 1];
170                 heap[minNum - 1] = heap[parent - 1];
171                 heap[parent - 1] = tmp;
172                 parent = minNum;
173                 child = 2 * minNum;
174             } else {//否则退出
175                 break;
176             }
177         }
178     }
179 
180     /**
181      * 堆排序
182      * 使用堆结构对某个数组进行排序
183      * @param elems
184      */
185     public E[] heapSort(E[] elems) {
186 
187         int length = elems.length;
188 
189         heap = elems;
190         size = length;
191         /*
192         * 创建初始堆,从最后一个非叶子节点开始调整所有的非叶子节点,直到根节点,
193         * 所有的节点调整都采用向下调整的方法
194         */
195         for (int i = length / 2; i >= 1; i--) {
196             adjustDown(i);
197         }
198         //再对初始堆进行排序
199         while (size > 0) {
200             //删除的过程实质上就是排序过程
201             removeMin();
202         }
203         return elems;
204     }
205 
206     //树的层次遍历
207     private void levelOrder() {
208         if (size == 0) {
209             return;
210         }
211         LinkedList queue = new LinkedList();
212         queue.add(1);
213         System.out.print("层次遍历 - ");
214         while (!queue.isEmpty()) {
215             int num = (Integer) queue.removeFirst();
216             System.out.print(heap[num - 1] + " ");
217 
218             if (num * 2 <= size) {
219                 queue.add(num * 2);
220                 if (num * 2 + 1 <= size) {
221                     queue.add(num * 2 + 1);
222                 }
223             }
224         }
225         System.out.println();
226     }
227 
228     public int size() {
229 
230         return size;
231     }
232 
233     /**
234      * @param args
235      */
236     public static void main(String[] args) {
237         Heap<Integer> h = new Heap<Integer>();
238 
239         Random rr = new Random(System.currentTimeMillis());
240         Integer[] itg = new Integer[rr.nextInt(20)];
241         for (int i = 0; i < itg.length; i++) {
242             Integer tmp = new Integer(rr.nextInt(100));
243             h.add(tmp);
244             itg[i] = tmp;
245         }
246         h.levelOrder();
247         System.out.print("优先队列 - ");
248         while (h.isEmpty() == false) {
249             System.out.print(h.removeMin() + " ");
250         }
251 
252         System.out.println();
253         itg = h.heapSort(itg);
254         System.out.print("堆排序 - ");
255         System.out.println(Arrays.toString(itg));
256     }
257 }