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

熊孩子開學嘍發表於2007-08-17

相信每一個程式設計師在寫程式的時候,都或多或少地接觸過排序問題.(還別說,我就真見過從來不寫排序程式碼的傢伙,號稱是寫資料庫應用的,只要寫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

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


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

相關文章