曾经遇到的一个另类的排序问题.

相信每一个程序员在写程序的时候,都或多或少地接触过排序问题.(还别说,我就真见过从来不写排序代码的家伙,号称是写数据库应用的,只要写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

(请直接看原理和最后的效果,忽略中间的代码,那个是低效的算法)


如有错漏之处,请高手不吝指正. 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值