排序演算法之——優先佇列經典實現(基於二叉堆)
許多應用都需要處理有序的元素,但有時,我們不要求所有元素都有序,或是一定要一次就將它們排序,許多情況下,我們會收集這些元素裡的最大值或最小值。
這種情況下一個合適的資料結構應該支援兩種操作:插入元素、刪除最大元素。
優先佇列與棧和佇列類似,但它有自己的奇妙之處。
在本文中,會講解基於二叉堆的一種優先佇列的經典實現方法(程式碼沒有任何難度,主要是理解思想)。
一、關於堆
1、堆的定義:
資料結構二叉堆能很好地實現優先佇列的操作。在二叉堆中,每個元素都要保證大於等於另外兩個位置的元素,相應的,這些位置的元素又至少要大於等於陣列中的另外兩個元素。
將所有元素畫成一顆二叉樹,就能很容易看出這種結構。
(圖示1)
2、堆的演算法:
在堆有序的過程中我們會遇到兩種情況:
某個節點的優先順序上升,我們需要由下至上恢復堆的順序。
當某個節點的優先順序下降,我們需要由上至下恢復堆的順序。
在排序演算法中,我們只通過私有輔助函式來訪問元素:
1 private void exch(int i, int j) {
2 Key temp = pq[i];
3 pq[i] = pq[j];
4 pq[j] = temp;
5 }
6
7 private boolean less(int i, int j) {
8 return pq[i].compareTo(pq[j]) < 0;
9 }
①、由下至上的堆的有序化(上浮)
1 private void swim(int k) {// 二叉堆
2 while (k > 1 && less(k / 2, k)) {
3 exch(k / 2, k);
4 k = k / 2;
5 }
6 }
k/2即為k節點的父節點,當k大於k/2時交換兩者,並繼續與其父節點比較,直到找到合適的位置。
②、由上至下的堆的有序化(下沉)
1 private void sink(int k) {// 二叉堆
2 while (2 * k <= N) {
3 int j = 2 * k;
4 if (j < N && less(j, j + 1)) {
5 j++;
6 }
7 if (!less(k, j)) {
8 break;
9 }
10 exch(k, j);
11 k = j;
12 }
13 }
對於二叉樹,2*k即為k的左子節點,將左右子節點進行比較,再將父節點與較大的子節點比較,如果子節點大於父節點,就將他們交換,並繼續向下比較,直到找到合適的位置。
③、調整陣列大小
如果不知道元素的個數,任意在初始化時造成空間的浪費。我們需要創造一個函式,用來調整陣列的大小。
在插入方法中,如果空間已滿,就將陣列大小擴充套件為原來的兩倍。在刪除方法中,如果元素的個數小於陣列長度的1/4,就將陣列的長度減小一半。
1 private void resize(int n) {
2 Key[] temp = (Key[]) new Comparable[n + 1];
3 for (int i = 1; i <= N; i++) {
4 temp[i] = pq[i];
5 }
6 pq = temp;
7 L = n;
8 }
有了上面的方法,我們只需在插入和刪除方法中加入判斷語句即可。
④、多叉堆(瞭解即可)
在掌握了二叉堆的原理之後,將其改進為多叉堆只需要做幾個改動。下面直接放程式碼,有興趣的朋友可以自己動手。
1 private void swim(int k, int d) {// d叉堆:(k+d-2)/d為d叉堆第k個節點的父節點
2 while (k > 1 && less((k + d - 2) / d, k)) {
3 exch((k + d - 2) / d, k);
4 k = (k + d - 2) / d;
5 }
6 }
7
8 private void sinkM(int k, int d) {// d叉堆
9 while (d * k - (d - 2) <= N) {// d叉堆k節點的第一個子節點
10 int j = d * k - (d - 2);
11 int flag = k;
12 while (j <= N && j <= d * k + 1) {
13 if (less(flag, j)) {
14 flag = j;
15 }
16 j++;
17 }
18 if (flag == k) {// flag沒有改變
19 break;
20 }
21 exch(k, flag);
22 k = flag;
23 }
24 }
二、堆排序(非降序):
(示意圖2)
堆排序的sink()方法經過修改sink(a,b)中a是被排序的元素,b為排序的最大範圍(修改之前排序的最大範圍為元素總個數)。
1 public void sort(Comparable[] a) {//堆排序
2 int n=N;
3 for(int k=n/2;k>=1;k--) {
4 sink(k,n);
5 }
6 while(n>1) {
7 exch(1,n--);
8 sink(1,n);
9 }
10 }
1、heap construction(堆的構造)
可以看到在for迴圈中,我們只掃描了陣列一半元素,因為我們跳過了大小為1的子堆,每次對一個節點排序時,以該節點為根節點的子堆就是有序的,所以我們最後會得到一個堆有序的二叉堆。
2、sortdown(下沉排序)
下沉排序每次選出最大的元素放入陣列空出的位置,這有點像選擇排序,但所需的比較要小得多,因為堆提供了一種從未排序部分找到最大元素的有效方法。
三、java程式碼展示(所有程式碼)
1 public class MaxPQ<Key extends Comparable<Key>> {
2 private Key[] pq;
3 private static int N = 0;// 元素個數
4 private static int L;// 陣列長度(不包括0)
5
6 public MaxPQ(int maxN) {
7 pq = (Key[]) new Comparable[maxN + 1];
8 L = maxN;
9 }
10
11 public boolean isEmpty() {
12 return N == 0;
13 }
14
15 public int size() {
16 return N;
17 }
18
19 public void insert(Key v) {// 二叉堆
20 if (N == L) {
21 resize(2 * N + 1);
22 System.out.println("resize Double");
23 }
24 pq[++N] = v;
25 swim(N);
26 }
27
28 public void insert(Key v, int d) {// d叉堆
29 if (N == L) {
30 resize(2 * N + 1);
31 System.out.println("resize Double");
32 }
33 pq[++N] = v;
34 swim(N, d);
35 }
36
37 public Key delMax() {
38 Key max = pq[1];
39 exch(1, N--);
40 pq[N + 1] = null;
41 sink(1);
42 if (N > 0 && N == L / 4) {
43 resize(L / 2);
44 System.out.println("resize 1/2");
45 }
46 return max;
47 }
48
49 public Key delMax(int d) {
50 if (N == 0) {
51 System.out.println("none!");
52 }
53 Key max = pq[1];
54 exch(1, N--);
55 pq[N + 1] = null;
56 sinkM(1, d);
57 if (N > 0 && N == L / 4) {
58 resize(L / 2);
59 System.out.println("resize 1/2");
60 }
61 return max;
62 }
63
64 private void swim(int k) {// 二叉堆
65 while (k > 1 && less(k / 2, k)) {
66 exch(k / 2, k);
67 k = k / 2;
68 }
69 }
70
71 private void swim(int k, int d) {// d叉堆:(k+d-2)/d為d叉堆第k個節點的父節點
72 while (k > 1 && less((k + d - 2) / d, k)) {
73 exch((k + d - 2) / d, k);
74 k = (k + d - 2) / d;
75 }
76 }
77
78 private void sink(int k) {// 二叉堆
79 while (2 * k <= N) {
80 int j = 2 * k;
81 if (j < N && less(j, j + 1)) {
82 j++;
83 }
84 if (!less(k, j)) {
85 break;
86 }
87 exch(k, j);
88 k = j;
89 }
90 }
91
92 private void sinkM(int k, int d) {// d叉堆
93 while (d * k - (d - 2) <= N) {// d叉堆k節點的第一個子節點
94 int j = d * k - (d - 2);
95 int flag = k;
96 while (j <= N && j <= d * k + 1) {
97 if (less(flag, j)) {
98 flag = j;
99 }
100 j++;
101 }
102 if (flag == k) {// flag沒有改變
103 break;
104 }
105 exch(k, flag);
106 k = flag;
107 }
108 }
109
110 private void resize(int n) {
111 Key[] temp = (Key[]) new Comparable[n + 1];
112 for (int i = 1; i <= N; i++) {
113 temp[i] = pq[i];
114 }
115 pq = temp;
116 L = n;
117 }
118
119 private void exch(int i, int j) {
120 Key temp = pq[i];
121 pq[i] = pq[j];
122 pq[j] = temp;
123 }
124
125 private boolean less(int i, int j) {
126 return pq[i].compareTo(pq[j]) < 0;
127 }
128
129 public void sort(Comparable[] a) {//堆排序
130 int n=N;
131 for(int k=n/2;k>=1;k--) {
132 sink(k,n);
133 }
134 while(n>1) {
135 exch(1,n--);
136 sink(1,n);
137 }
138 }
139
140 private void sink(int k, int n) {//二叉樹 從k到n排序
141 while (2 * k <= n) {
142 int j = 2 * k;
143 if (j < n && less(j, j + 1)) {
144 j++;
145 }
146 if (!less(k, j)) {
147 break;
148 }
149 exch(k, j);
150 k = j;
151 }
152 }
153
154 public static void main(String[] args) {
155 MaxPQ mpq = new MaxPQ<>(3);
156 mpq.insert(1);
157 mpq.insert(2);
158 mpq.insert(3);
159 mpq.insert(4);
160 mpq.insert(5);
161 mpq.insert(6);
162 mpq.insert(7);
163 mpq.insert(8);
164 mpq.insert(9);
165 mpq.insert(10);
166 mpq.insert(11);
167 mpq.sort(mpq.pq);
168 for(int i=1;i<=N;i++) {
169 System.out.println(mpq.pq[i]);
170 }
171 /*for (int i = 1; i <= 13; i++) {
172 System.out.println(mpq.delMax());
173 }*/
174 }
175
176 }