數組第K小數問題 及其對於 快排和堆排 的相關優化比較


題目描述

給定一個整數數組a[0,...,n-1],求數組中第k小數


輸入描述

首先輸入數組長度n和k,其中1<=n<=5000, 1<=k<=n

然后輸出n個整形元素,每個數的范圍[1, 5000]


輸出描述

該數組中第k小數


樣例輸入

4 2
1 2 3 4


樣例輸出

2



其實可以用 堆 來做,保證根節點為最小值,然后逐步剔除。不過當然也可以直接排序。
權當熟悉一下STL:
 1 #include <vector>
 2 #include <algorithm>
 3 #include <iostream>
 4 using namespace std;
 5 
 6 int main()
 7 {
 8     int n, k;
 9     cin >> n >> k;
10 
11     vector<int> a(n, 0);
12     for (int i = 0; i < n; i++)
13     {
14         cin >> a[i];
15     }
16     sort(a.begin(), a.end());
17 
18     cout << a[k-1];
19 
20     return 0;
21 }

 



在和 Space_Double7 討論后(詳見討論區),於是有了想要比較 快排 和 堆排 對於這道題各自的效率的想法。
於是寫了一個程序來比較它們各自的運行時間,程序主要包括 隨機生成輸入數據、分別用快排和堆排求解並計時、比較 這幾個部分。
代碼如下:
  1 #include <vector>
  2 #include <algorithm>
  3 #include <iostream>
  4 #include <stdlib.h>
  5 #include <time.h>
  6 #include <windows.h>
  7 
  8 using namespace std;
  9 
 10 // for timing
 11 clock_t start, stop;
 12 double durationOfQsort, durationOfHeapsort;
 13 
 14 vector<int> num;
 15 int k, ans, n;
 16 bool found;
 17 
 18 // 快排: 選定軸點
 19 int parti(int lo, int hi)
 20 {
 21     swap(num[lo], num[lo + rand() % (hi - lo + 1)]);
 22     int pivot = num[lo];
 23     while (lo < hi)
 24     {
 25         while ((lo < hi) && (pivot <= num[hi])) hi--;
 26         num[lo] = num[hi];
 27         while ((lo < hi) && (num[lo] <= pivot)) lo++;
 28         num[hi] = num[lo];
 29     }
 30     num[lo] = pivot;
 31     if (lo == k)
 32     {
 33         found = true; // 表征已確定找到第 k 小數
 34         ans = num[k];
 35     }
 36     return lo;
 37 }
 38 
 39 // 快排主體
 40 void quicksort(int lo, int hi)
 41 {
 42     if ((hi - lo < 2) || (found))
 43     {
 44         if ((!found) && (lo == k))
 45         {
 46             found = true;
 47             ans = num[k];
 48         }
 49         return;
 50     }
 51     int mi = parti(lo, hi - 1);
 52     quicksort(lo, mi);
 53     quicksort(mi + 1, hi);
 54 }
 55 
 56 #define InHeap(n, i)        (( (-1) < (i) ) && ( (i) < (n) ))
 57 #define Parent(i)            ((i-1)>>1)
 58 #define LastInternal(n)        Parent(n-1)
 59 #define LChild(i)            (1+((i) << 1))
 60 #define RChild(i)            ((1+(i)) << 1)
 61 #define LChildValid(n, i)    InHeap(n, LChild(i))
 62 #define RChildValid(n, i)    InHeap(n, RChild(i))
 63 #define Bigger(PQ, i, j)    ((PQ[i])<(PQ[j])? j : i)
 64 #define ProperParent(PQ, n, i) \
 65     (RChildValid(n, i) ? Bigger(PQ, Bigger(PQ, i, LChild(i)), RChild(i)):\
 66     (LChildValid(n, i) ? Bigger(PQ, i, LChild(i)):i\
 67     )\
 68     )
 69 
 70 // 對向量前 n 個元素中的第 i 實施下濾操作
 71 int percolateDown(int n, int i)
 72 {
 73     int j;
 74     while (i != (j = ProperParent(num, n, i)))
 75     {
 76         swap(num[i], num[j]);
 77         i = j;
 78     }
 79     return i;
 80 }
 81 
 82 // Floyd 建堆算法
 83 void heapify()
 84 {
 85     for (int i = LastInternal(n); InHeap(n, i); i--)
 86         percolateDown(n, i);
 87 }
 88 
 89 // 刪除堆中最大的元素
 90 int delMax(int hi)
 91 {
 92     int maxElem = num[0]; 
 93     num[0] = num[hi];
 94     percolateDown(hi, 0);
 95     return maxElem;
 96 }
 97 
 98 // 堆排主體
 99 void heapsort()
100 {
101     heapify();
102     int hi = n;
103     while (hi > 0)
104     {
105         --hi;
106         num[hi] = delMax(hi);
107         if (hi == k)
108         {
109             ans = num[k];
110             return;
111         }
112     }
113 }
114 
115 int main()
116 {
117     int scoreOfQsort = 0, scoreOfHeapsort = 0;
118 
119     for (int iter = 0; iter < 30; iter++)
120     {
121         // 確定 n 的大致最大范圍,注意隨機 n 會有向右 MaxN 的偏差 
122         const int MaxN = 3001;
123         
124         // 產生一個 0..n-1 的隨機序列輸入數組,n 最大為3000
125         cout << "**********************第" << iter + 1 << "次************************" << endl;
126         //srand(unsigned(clock()));
127         n = rand() % MaxN + MaxN;
128         vector<int> a(n, 0);
129         for (int i = 0; i < n; i++)
130             a[i] = i;
131         random_shuffle(a.begin(), a.end());
132 
133         cout << "產生一個 0.." << n - 1 << " 的隨機序列輸入數組:" << endl;
134         /*for (int i = 0; i < n; i++)
135             cout << a[i] << " ";*/
136         cout << endl;
137 
138         // 隨機生成 k
139         //srand(unsigned(clock()));
140         k = rand() % n;
141         cout << "k = " << k << endl << endl;
142 
143         // 第 k 小的數一定是 k,因為 random_shuffle,同時避免退化情形對快排一般性的影響
144         cout << "在該數組中第 " << k << " 小的數是: " << k << endl << endl << endl;
145 
146         // qsort
147         cout << "快排:" << endl;
148         num = a; 
149         start = clock();
150         found = false;
151         quicksort(0, n);
152         stop = clock();
153         durationOfQsort = ((double)(stop - start)*1000) / CLK_TCK;
154         cout << "Ok.. 我已經找到了第 " << k << " 小的數,它是: " << ans << endl;
155         cout << "快排用時: " << durationOfQsort << "ms" << endl;
156         if (ans != k)
157         {
158             cout << "注意!!!答案錯誤!!!" << endl;
159             system("pause");
160         }
161         /*else
162         {
163             cout << "此時的序列情況是: ";
164             for (int i = 0; i < n; i++)
165                 cout << num[i] << " ";
166         }*/
167         cout << endl << endl;
168 
169         // heapsort
170         cout << "堆排:" << endl;
171         num = a;
172         start = clock();
173         heapsort();
174         stop = clock();
175         durationOfHeapsort = ((double)(stop - start) * 1000) / CLK_TCK;
176         cout << "Ok.. 我已經找到了第 " << k << " 小的數,它是: " << num[k] << endl;
177         cout << "堆排用時: " << durationOfHeapsort << "ms" << endl;
178         if (num[k] != k)
179         {
180             cout << "注意!!!答案錯誤!!!" << endl;
181             system("pause");
182         }
183         /*else
184         {
185             cout << "此時的序列情況是: ";
186             for (int i = 0; i < n; i++)
187                 cout << num[i] << " ";
188         }*/
189         cout << endl << endl;
190 
191         if (durationOfHeapsort > durationOfQsort) scoreOfQsort++;
192         else scoreOfHeapsort++;
193     }
194     cout << "*******************END***********************";
195     cout << endl << endl << "總比分(快排:堆排): " << scoreOfQsort << " : " << scoreOfHeapsort;
196     cout << endl;
197 
198     return 0;
199 }

運行了幾下,大致可以看出 快排:堆排 的效率比 趨近於 2:1(對於每個case的耗時少則計分,計分多的效率高),這是其中 3 次的結果:

感興趣的可以自己在本地運行一下~

 


 

然而我發現依然存在一個問題,我們主要想討論的是 對於 “一找到 第k小 的元素便立即退出”這件事 對於整個完全排序本身優化程度的大小。

橫向來看,僅僅比較 優化后的排序 耗時並不能確定 到底是快速排序 本身對於隨機數據而言比 堆排序 性能好(從上面看,似乎是顯然的),還是 “一找到 第k小 的元素便立即退出”這個優化在這里 幫了快速排序的大忙,使其在這里表現出了更好的性能。

縱向來看,我們想看看該優化對於完全排序而言,能優化到什么程度。

所以我決定比較兩種排序 優化退出的時間占完全排序的時間 的比,來看看這個優化對於完全排序的影響程度。

這是修改以后的代碼,算的是百分比,百分比越小,優化越明顯

 
         
  1 #include <vector>
  2 #include <algorithm>
  3 #include <iostream>
  4 #include <stdlib.h>
  5 #include <time.h>
  6 #include <windows.h>
  7 
  8 using namespace std;
  9 
 10 // for timing
 11 clock_t start, stop, stop1;
 12 double durationOfQsort, durationOfHeapsort;
 13 
 14 vector<int> num;
 15 int k, ans, n;
 16 bool found;
 17 double time1, time2;
 18 double average1 = 0, average2 = 0;
 19 
 20 // 快排: 選定軸點
 21 int parti(int lo, int hi)
 22 {
 23     swap(num[lo], num[lo + rand() % (hi - lo + 1)]);
 24     int pivot = num[lo];
 25     while (lo < hi)
 26     {
 27         while ((lo < hi) && (pivot <= num[hi])) hi--;
 28         num[lo] = num[hi];
 29         while ((lo < hi) && (num[lo] <= pivot)) lo++;
 30         num[hi] = num[lo];
 31     }
 32     num[lo] = pivot;
 33     if ((!found)&&(lo == k))
 34     {
 35         stop1 = clock();
 36         found = true;
 37     }
 38     return lo;
 39 }
 40 
 41 // 快排主體
 42 void quicksort(int lo, int hi)
 43 {
 44     if (hi - lo < 2)
 45     {
 46         if ((!found) && (lo == k))
 47         {
 48             stop1 = clock();
 49             found = true;
 50         }
 51         return;
 52     }
 53     int mi = parti(lo, hi - 1);
 54     quicksort(lo, mi);
 55     quicksort(mi + 1, hi);
 56 }
 57 
 58 #define InHeap(n, i)        (( (-1) < (i) ) && ( (i) < (n) ))
 59 #define Parent(i)            ((i-1)>>1)
 60 #define LastInternal(n)        Parent(n-1)
 61 #define LChild(i)            (1+((i) << 1))
 62 #define RChild(i)            ((1+(i)) << 1)
 63 #define LChildValid(n, i)    InHeap(n, LChild(i))
 64 #define RChildValid(n, i)    InHeap(n, RChild(i))
 65 #define Bigger(PQ, i, j)    ((PQ[i])<(PQ[j])? j : i)
 66 #define ProperParent(PQ, n, i) \
 67     (RChildValid(n, i) ? Bigger(PQ, Bigger(PQ, i, LChild(i)), RChild(i)):\
 68     (LChildValid(n, i) ? Bigger(PQ, i, LChild(i)):i\
 69     )\
 70     )
 71 
 72 // 對向量前 n 個元素中的第 i 實施下濾操作
 73 int percolateDown(int n, int i)
 74 {
 75     int j;
 76     while (i != (j = ProperParent(num, n, i)))
 77     {
 78         swap(num[i], num[j]);
 79         i = j;
 80     }
 81     return i;
 82 }
 83 
 84 // Floyd 建堆算法
 85 void heapify()
 86 {
 87     for (int i = LastInternal(n); InHeap(n, i); i--)
 88         percolateDown(n, i);
 89 }
 90 
 91 // 刪除堆中最大的元素
 92 int delMax(int hi)
 93 {
 94     int maxElem = num[0]; 
 95     num[0] = num[hi];
 96     percolateDown(hi, 0);
 97     return maxElem;
 98 }
 99 
100 // 堆排主體
101 void heapsort()
102 {
103     heapify();
104     int hi = n;
105     while (hi > 0)
106     {
107         --hi;
108         num[hi] = delMax(hi);
109         if (hi == k) stop1 = clock();
110     }
111 }
112 
113 int main()
114 {
115     // 確定 n 的大致最大范圍,注意隨機 n 會有向右 MaxN 的偏差 
116     const int MaxN = 3001;
117     
118     // 計算次數
119     const int times = 30;
120 
121     int scoreOfQsort = 0, scoreOfHeapsort = 0;
122 
123     for (int iter = 0; iter < times; iter++)
124     {
125         
126         
127         // 產生一個 0..n-1 的隨機序列輸入數組,n 最大為3000
128         cout << "**********************第" << iter + 1 << "次************************" << endl;
129         //srand(unsigned(clock()));
130         n = rand() % MaxN + MaxN;
131         vector<int> a(n, 0);
132         for (int i = 0; i < n; i++)
133             a[i] = i;
134         random_shuffle(a.begin(), a.end());
135 
136         cout << "產生一個 0.." << n - 1 << " 的隨機序列輸入數組:" << endl;
137         /*for (int i = 0; i < n; i++)
138             cout << a[i] << " ";*/
139         cout << endl;
140 
141         // 隨機生成 k
142         //srand(unsigned(clock()));
143         k = rand() % n;
144         cout << "k = " << k << endl << endl;
145 
146         // 第 k 小的數一定是 k,因為 random_shuffle,同時避免退化情形對快排一般性的影響
147         cout << "在該數組中第 " << k << " 小的數是: " << k << endl << endl << endl;
148 
149         // qsort
150         cout << "快排:" << endl;
151         num = a; 
152         start = clock();
153         found = false;
154         quicksort(0, n);
155         stop = clock(); 
156         time1 = (double)(stop1 - start) * 1000 / CLK_TCK;
157         time2 = (double)(stop - start) * 1000 / CLK_TCK;
158         cout << "找到 k 的時間: " << time1 << " ms" << endl;
159         cout << "完全排序 的時間: " << time2 << " ms" << endl;
160         durationOfQsort = time1 / time2 * 100;
161         average1 += durationOfQsort;
162         /*cout << "Ok.. 我已經找到了第 " << k << " 小的數,它是: " << ans << endl;*/
163         cout << "快排占比: " << durationOfQsort << " %" << endl;
164         /*if (ans != k)
165         {
166             cout << "注意!!!答案錯誤!!!" << endl;
167             system("pause");
168         }*/
169         /*else
170         {
171             cout << "此時的序列情況是: ";
172             for (int i = 0; i < n; i++)
173                 cout << num[i] << " ";
174         }*/
175         cout << endl << endl;
176 
177         // heapsort
178         cout << "堆排:" << endl;
179         num = a;
180         start = clock();
181         heapsort();
182         stop = clock();
183         time1 = (double)(stop1 - start) * 1000 / CLK_TCK;
184         time2 = (double)(stop - start) * 1000 / CLK_TCK;
185         cout << "找到 k 的時間: " << time1 << " ms" << endl;
186         cout << "完全排序 的時間: " << time2 << " ms" << endl;
187         durationOfHeapsort = time1 / time2 * 100;
188         average2 += durationOfHeapsort;
189         //cout << "Ok.. 我已經找到了第 " << k << " 小的數,它是: " << num[k] << endl;
190         cout << "堆排占比: " << durationOfHeapsort << " %" << endl;
191         /*if (num[k] != k)
192         {
193             cout << "注意!!!答案錯誤!!!" << endl;
194             system("pause");
195         }*/
196         /*else
197         {
198             cout << "此時的序列情況是: ";
199             for (int i = 0; i < n; i++)
200                 cout << num[i] << " ";
201         }*/
202         cout << endl << endl;
203 
204         if (durationOfHeapsort > durationOfQsort) scoreOfQsort++;
205         else scoreOfHeapsort++;
206     }
207     cout << "*******************END***********************";
208     cout << endl << endl << "總比分(快排:堆排): " << scoreOfQsort << " : " << scoreOfHeapsort;
209     cout << endl;
210     cout << endl << "快排平均占比: " << average1 / times << " %" << endl;
211     cout << endl << "堆排平均占比: " << average2 / times << " %" << endl;
212 
213     return 0;
214 }
 
         

 

 
        

 運行 3 次的結果:

 

可見,

1、這個優化對於兩種排序的影響程度是差不多的(百分比越小,優化越明顯);

2、對於完全排序而言,它大概相當於在前面加了一個 0.6 的系數,也就是 只 干了完全排序 0.6 倍的工作量,正如分析來看,依然是常系數級的優化。

由於數據是完全隨機的(並且沒有重復元素),快排也適應得很好,在實際用途中(對於近似隨機數據),它的效率是可觀的。

 


 

不!還沒完!

來來來,現在我們回到原問題本身,回到 查找 數組 第 k 小數 這樣經典而基礎的問題本身上來……

 盡管 原問題 數據規模小,水水就能過,但是既然已經鼓搗過了,干脆鼓搗完。

所以我還是決定寫一個對於大規模數據具有普適意義的盡可能優化的算法來解決問題(優化到線性復雜度)。

 

 再考慮這個問題,在寫了一遍快排之后,會發現這是一個與 快排中選取軸點 很類似的問題。

軸點是左右集合的分界點,左集合所有元素比軸點小,右集合所有元素比軸點大,你可以發現,找第 K 小數就是找在位置 K 上的軸點(也正如上述優化所想)!

 

然而我們依然要向上面所寫的程序一樣,找到 k 就退出嗎?

 

1、快速選取算法

考慮這樣的情形,

當前選取的軸點正好是 第k個,自然就退出;

當選取的軸點比 k 小時,我們實際上可以不用再對左集合排序了!因為我們只需要知道它們都比軸點要小,而且知道它們的個數,而此時軸點比 k 還要小,所以我們可以繼續只對 右集合 分治下去!

同樣的,當選取的軸點比 k 大時,我們實際上可以不用再對右集合排序了!因為我們只需要知道它們都比軸點要大,而且知道它們的個數,而此時軸點比 k 還要大,所以我們可以繼續只對 左集合 分治下去!

這樣,我們可以把每一次的向下兩次遞歸變成一次。

用 非遞歸 版本代碼大致表示如下:

 1 void quickSelect()
 2 {
 3     for (int lo = 0, hi = n-1; lo < hi; )
 4     {
 5         int i = lo, j = hi, pivot = num[lo]; 
 6         while (i < j)
 7         {
 8             while ( (i<j) && (pivot <= num[j]) ) j--;
 9             num[i] = num[j];
10             while ( (i<j) && (num[i] <= pivot) ) i++;
11             num[j] = num[i];
12         }
13         num[i] = pivot;
14         if ( k<=i ) hi = i - 1;
15         if ( i<=k ) lo = i + 1;
16     }
17 } // 結束后 num[k] 即是解

 

對於一般情況(數據接近隨機),跟快排一樣,此算法效率很高,不過快排的缺點它也一樣具有。也就是當軸點把左右集合划分得極不均勻甚至某一個集合為空時,此時效率跟快排一樣退化到 O(n^2)。

 

接下來考慮, 堆排是不是也有優化空間呢?

 

2、堆選取算法

題目只需要我們選取 第 K 小數,跟快速選取一樣,無關目的的部分我們完全沒必要做無用功。

基於這樣的考慮,我們完全可以只維護一個規模為 K 的大根堆嘛!~

算法思考大致是:

首先將序列前 K 的元素用 Floyd 建堆(O(k)效率)維護成一個大根堆。

然后將剩下的元素依次插入堆,每插入一個,隨機刪除堆頂,使堆的規模保持在 k。

這樣當所有的元素插入完畢,那么堆的根就是問題的解。

 

一般情況下,它的效率是比完全排序效率要高的,不過當 k 接近於 n/2 的時候,它的復雜度又會退化到 O( nlogn )。

 

難道真的不能從實質上將這個問題優化到線性效率上來嗎?

 

3、k-選取算法

 算法里面對於排序有一個喪病的思路:選定一個 k,當序列長度小於 k 時,sort 函數直接不作處理返回原序列。整個序列經過這樣一次 sort 之后當然不是有序的,此時對這個序列做一次插入排序(因為插入排序在處理 “幾乎” 有序的序列時,運行非常快)。根據算導的結論,這樣的復雜度是 O(nk + n log(n/k)) 的。(其實就是相當於做n/k次k長的插入)

 

這種思想在這里我們也可以借鑒,大致的算法思想如下:

0) 選定一個數Q,Q為一個不大的常數;

select(A, k):

1) 如果序列A規模不大於 Q 時直接蠻力算法;      // 遞歸基

2) 將A均勻划分為 n/Q 個子序列,各含 Q 個元素;

3) 各子序列分別排序(可采用任何排序算法),計算中位數,並將所有中位數組成一個序列;

4) 遞歸調用select(),計算出中位數序列的中位數,記作M;

5) 根據元素相對於 M 的大小,將 A 中元素分為三個子集:L(小於),E(相等)和G(大於);

6) if ( |L| >= k ) return select(L, k);

  else if ( |L| + |E| >= k ) return M;

    else return select(G, k - |L| - |E|);

 

 復雜度分析:(計最壞情況下運行時間為 T(n))

2): O(n);

3): 由於Q為常數,累計也為 O(n);

4): 遞歸調用,T(n/Q)

5): O(n);

6): 中位數序列的中位數一定是全局中位數 M,而 L 和 G 的規模一定不會超過 3n/4。

 

所以可得如下遞推關系:

T(n) = cn + T(n/Q) + T(3n/4),c 為常數

如果取 Q = 5, 則有:

T(n) = cn + T(n/5) + T(3n/4) = O(20cn) = O(n)

 

可見,復雜度是線性。雖如此,其常系數過大,且算法過程較復雜,在一般規模的應用中難以真正體現出效率的優勢。

 

Reference : 《數據結構習題解析》,鄧俊輝

 
       


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM