曾經遇到的一個另類的排序問題.

相信每一個程序員在寫程序的時候,都或多或少地接觸過排序問題.(還別說,我就真見過從來不寫排序代碼的傢伙,號稱是寫數據庫應用的,只要寫SORT BY什麼的,從來不自己寫排序代碼的牛人)什麼冒泡排序,插入排序,快速排序等等,想必都聽出老繭來了.但是很多時候程序的要求並非直接要求你將一列數據從大到小,或者從小到大排一下就算完了.在此我想把我自己在實際應用中遇到的一種排序要求和我所使用的方法介紹給大家.
在我之前的系列文章中,曾經介紹了一種關於圖像濾波的算法.該算法的效果不錯,但是由於運算量比較大,所以處理速度相對其它功能就稍微慢一些.文章參考:http://blog.yesky.com/blog/wallescai/archive/2007/07/10/1692197.html
在該篇文章中,我主要介紹的是濾波算法的原理和算法.而這個算法中用到了一個比較另類的排序,具體要求如下:
在一個長度爲N的亂序整數數列中指定某一個數字,選出整個數列中和該數字的差值最小的M個數字.然後將這些數字求平均值.(其實這就是前面提到的那個濾波算法的關鍵核心)
N = (R * 2 + 1)^2  (R = 1,2,3,4...); => N = (9,25,49,81...)
M = N/2 (一般取值略小於N的一半) 
這並非嚴格意義上的排序,但是我想很多朋友如果在看完這些要求之後的第一反應就是:排序問題,然後就興致勃勃的開始寫代碼.
先別急,還有一個附加條件,由於這個算法是嵌在圖像處理算法中的,圖像中的每一個像素都需要應用到這個算法3次(紅,綠,藍三種顏色都要參與計算),因此哪怕是一個800*600大小的圖片最少也需要進行(800-2)*(600-2)*3 = 1431612次排序.所以要求這個算法異常精簡快速.
關於這個排序的算法,我已經在CSDN的論壇中和大家討論過,參考:http://topic.csdn.net/t/20060907/13/5005438.html
並且在帖子的後面,我總結出了一個比較快速的算法.並且將之用於我的程序之中,我所公佈的那個最新版本的ImageCast就是採用了這個排序算法.


但是,今天在仔細研究了這個算法之後,我發現自己錯了.這個算法還遠沒有達到最高效率,它依然有很大可挖掘的地方.
首先介紹思路:
假定原數列爲A(N),選定的參考數字爲S,最後要選出與S值最接近的M個數字 
首先建立一個和原數列相同長度的數組B(N).數組B用來存放A(N)中每一個元素和S的差的絕對值
然後將數組B的前M個值和後N-M個值去比較,如果前者大於後者,則兩者交換位置,同時將遠數組A的對應元素也交換位置.

測試代碼爲:
Option Explicit  
Private Declare Function timeGetTime Lib "winmm.dll"() As Long  
Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (pDest As Any, pSrc As Any, ByVal ByteLen As Long)

Const ALL As Long = 1000   '待選數組長度,上文中的N
Const NEAR As Long = 5   '最接近選定數字的數量,上文中的M
Dim A(ALL - 1) As Long   '這個數組用來存放原始數據
Dim B(All - 1) As Long   '用於生成最初的原始數據,每次測試時拷貝去A將A初始化

Private Sub Form_Load()  
Dim I As Long  
For I = 0 To ALL - 1  
    B(I) = Rnd * All  '產生一個隨機數列 
Next  
End Sub  
   
Private Sub FSort(ByVal Test As Long)   
Dim D(ALL - 1) As Long  
Dim I As Long  
Dim L As Long  
Dim M As Long  
Dim N As Long   
For I = 0 To ALL - 1   '先獲得數組每一個元素和指定數字的差值
    D(I) = Abs(A(I) - Test)  
Next  

For N = 0 To NEAR    '關鍵循環,總的循環次數爲 Near*(All-Near)
    For I = NEAR + 1 To 999                   
        If D(N) > D(I) Then   '將前面的值和後面的值比較

           M = D(N)   '如果後面的小,則交換差值
           D(N) = D(I)  
           D(I) = M      
                       
           M = A(N)   '同時交換原數組元素
           A(N) = A(I)  
           A(I) = M  
        End If  
    Next  
Next 
'上面的循環結束後,原數列中和指定值差距最小的M+1個數已經排列在數組A的最前面
For I = 0 To Near   '將選定的數Test本身從中剔除
    If A(I) = Test Then  
       M = A(I)
       A(I) = A(Near)
       A(Near) = M  
       Exit For  
    End If  
Next  
End Sub  
   
調用:  
Private Sub Command1_Click()  
Dim T As Long
Dim I As Long
T=TimeGetTime
For I =0 To 10000 '每次調用前先將原數組還原,否則前次排序將影響後次的結果
   CopyMemory A(0), B(0), ALL * 4 
   FSort 333
Next
Me.Cls
For I = 0 To 4
   Me.Print A(I)
Next
Me.Print "All=" & ALL & ",Near=" & NEAR & ",Loop=10000" & "Time=" & T & "ms"
End Sub

當N>>M的時候,算法複雜度爲O(N),當M=N/2的時候爲:O(N^2)
因爲當篩選完成後數組中最接近的數字已經被排列到數組的最前段,因此如果直接循環調用的話,後面幾次調用的運算速度將遠小於正常速度.

請大家仔細看程序中提到的關鍵循環:
For N = 0 To NEAR    '關鍵循環,總的循環次數爲 Near*(All-Near)
    For I = NEAR + 1 To 999                   
        If D(N) > D(I) Then   '將前面的值和後面的值比較

           M = D(N)   '如果後面的小,則交換差值
           D(N) = D(I)  
           D(I) = M      
                       
           M = A(N)   '同時交換原數組元素
           A(N) = A(I)  
           A(I) = M  
        End If  
    Next  
Next 
我忽然醒悟到其實我根本不必去交換數組D中的元素,因爲它的順序並不影響最終結果,而對原始數組A的排序纔是真正有用的東西.它起到的作用只是指出了數組A應該在何處交換位置而已.而在上面的程序中交換數組D中的元素內容,只是爲了不使後面的循環中重複選擇同樣的數據而已.
思考了一番之後,我將上面的"關鍵循環"修改如下:
For N = 0 To NEAR
   M = N
   For I = NEAR + 1 To ALL
      If D(M) > D(I) Then M = I '其實只要得到當前最小的元素的位置就可以了,根本不必急着交換
   Next
   If M <> N Then '上面的循環結束之後再根據得到的數組位置去交換原始數組即可
      D(M) = D(N) '數組D的完整性不必考慮,只要保證已經被選過的數字不會再出現即可
      I = A(N) '交換原始數組內容
      A(N) = A(M)
      A(M) = I
   End If
Next

雖然算法本身複雜度不變,但是在改進了代碼之後,速度的提高是相當顯著的.
和原來的代碼相比,當原數列的無序度越高,速度提高越明顯.

因爲只是討論算法,我沒有結合原來圖像處理程序中的N和M取值範圍,這樣更便於大家實際應用.

上文雖然是用VB來實現的,但是因爲沒有設計VB專有的函數,因此同樣可以應用與其它語言.

 

解決了算法上的問題,再回頭去改進我的圖像處理程序,果然性能提高不少,大家可以去看看實際的處理效果:

http://blog.csdn.net/WallesCai/archive/2007/07/13/1688633.aspx

(請直接看原理和最後的效果,忽略中間的代碼,那個是低效的算法)


如有錯漏之處,請高手不吝指正. 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章