ExcelHome技术论坛

 找回密码
 免费注册

QQ登录

只需一步,快速开始

快捷登录

搜索
EH技术汇-专业的职场技能充电站 妙哉!函数段子手趣味讲函数 Excel服务器-会Excel,做管理系统 Excel Home精品图文教程库
HR薪酬管理数字化实战 Excel 2021函数公式学习大典 Excel数据透视表实战秘技 打造核心竞争力的职场宝典
300集Office 2010微视频教程 数据工作者的案头书 免费直播课集锦 ExcelHome出品 - VBA代码宝免费下载
用ChatGPT与VBA一键搞定Excel WPS表格从入门到精通 Excel VBA经典代码实践指南
楼主: aoe1981

[讨论] 排序算法学习

[复制链接]

TA的精华主题

TA的得分主题

 楼主| 发表于 2019-9-30 00:47 | 显示全部楼层
本帖最后由 aoe1981 于 2019-10-3 22:57 编辑

搞了大半晚上,终于把“二叉查找树排序”搞出来了,初步验证效率可与“快速排序”匹敌,而且是“稳定”排序,这个算法有两处很有意思:
1.居然不要用递归的结构,由于排序数据多时,会造成递归次数过大,带来“堆栈溢出”。神奇的是居然可以改成普通循环完成,反而让代码更简洁,只需一个过程即可完成。
2.当规定二叉查找树的“左孩子<=父节点、父节点<右孩子”时,该排序算法是不稳定的;
当规定二叉查找树的“左孩子<父节点、父节点<=右孩子”时,该排序算法是稳定的。
这个结论是我结合普通例子和极端例子初步分析得出的,或许有些不恰当,期待进一步验证。

上代码:

  1. Option Explicit
  2. Public Sub Sort13() '二叉查找树排序(稳定)
  3.     Dim t!, arr(), brr(), crr(), n&, i&, j&, rng_h&
  4.     Dim jl() As Boolean, lr() As Boolean, f&(), l&(), r&(), js& '记录判断,左右判断,所有节点的父节点、左孩子、右孩子序号,计数
  5.     Dim i1&, i2& '当前父节点序号,当前子节点序号
  6.     t = Timer()
  7.     Sht.Activate
  8.     Range("o3:o" & Rows.Count).ClearContents
  9.     rng_h = Range("a" & Rows.Count).End(xlUp).Row
  10.     If rng_h < 3 Then
  11.         End
  12.     ElseIf rng_h = 3 Then
  13.         ReDim arr(1 To 1, 1 To 1)
  14.         arr(1, 1) = Range("a3").Value
  15.     Else
  16.         arr = Range("a3:a" & rng_h).Value
  17.     End If
  18.     n = UBound(arr, 1)
  19.     ReDim brr(1 To n), crr(1 To n), jl(1 To n) As Boolean, lr(1 To n) As Boolean, f&(1 To n), l&(1 To n), r&(1 To n)
  20.     For i = 1 To n
  21.         brr(i) = arr(i, 1)
  22.     Next i
  23.    
  24.     For i = 2 To n '构建二叉查找树
  25.         j = 1
  26.         Do
  27.             If brr(i) < brr(j) Then '此处更改为brr(i)<=brr(j)则为不稳定排序
  28.                 If l(j) = 0 Then
  29.                     l(j) = i '记录左孩子
  30.                     f(i) = j '记录父节点
  31.                     Exit Do
  32.                 Else
  33.                     j = l(j) '比较左侧下一节点
  34.                 End If
  35.             Else
  36.                 If r(j) = 0 Then
  37.                     r(j) = i '记录右孩子
  38.                     lr(i) = True '记录右孩子位于父节点的右枝
  39.                     f(i) = j '记录父节点
  40.                     Exit Do
  41.                 Else
  42.                     j = r(j) '比较右侧下一节点
  43.                 End If
  44.             End If
  45.         Loop
  46.     Next i
  47.    
  48.     js = 0 '中序遍历记录排序序列
  49.     i1 = f(1)
  50.     i2 = 1
  51.     Do
  52.         If l(i2) = 0 Then '左孩子为空
  53.             If jl(i2) = False Then
  54.                 js = js + 1
  55.                 crr(js) = brr(i2) '记录节点
  56.                 jl(i2) = True
  57.             End If
  58.             If r(i2) = 0 Then '右孩子也为空
  59.                 If i2 > 1 Then '根节点无父节点
  60.                     If lr(i2) Then r(i1) = 0 Else l(i1) = 0 '当前节点为父节点的左(右)枝,父节点已记录的左(右)孩子归零
  61.                 End If
  62.             Else
  63.                 i1 = i2
  64.                 i2 = r(i2) '节点转至右孩子
  65.                 GoTo 100
  66.             End If
  67.         Else
  68.             i1 = i2
  69.             i2 = l(i2) '节点转至左孩子
  70.             GoTo 100
  71.         End If
  72.         If i1 = 0 Then i1 = f(1) Else i1 = f(i1)
  73.         If i1 = 0 Then i2 = 1 Else i2 = i1 '回溯到父节点
  74. 100:
  75.     Loop Until js = n
  76.    
  77.     For i = 1 To n
  78.         arr(i, 1) = crr(i)
  79.     Next i
  80.     Range("o3").Resize(n, 1).Value = arr
  81.     Range("o2").Value = Timer() - t
  82. End Sub
复制代码




附件说明:已更新至1楼。

(20190930上午8:32新增并完善代码。“新增”指续写出“二叉查找树排序”代码,“完善”主要针对1、2楼代码中关于数据源为空和只有一个源数据时的错误,已贴出的代码不再修改。)


原理容后细细道来,夜深了。细说原理,于我也是好的,日子久了,我也会看着糊涂的,写的过程确实很费劲。


TA的精华主题

TA的得分主题

 楼主| 发表于 2019-9-30 00:58 | 显示全部楼层
本帖最后由 aoe1981 于 2019-9-30 10:35 编辑

家中旧电脑100万个=RANDBETWEEN(1,1000000)的随机整数测试:
希尔排序:19.18798828秒;
归并排序:11.41894531秒;
快速排序:7.036132813秒;
堆排序:15.64599609秒;
计数排序:4.102050781秒;
基数排序:6.520019531秒;
二叉查找树排序:12.32397461秒。
可见:
希尔>堆排>二叉查找树>归并>快速>基数>计数


单位较新电脑100万个=RANDBETWEEN(1,1000000)的随机整数测试:
希尔排序:5.7265625秒;
归并排序:4.2890625秒;
快速排序:3.3515625秒;
堆排序:6.2265625秒;
计数排序:2.73046875秒;
基数排序:3.27734375秒;
二叉查找树排序:4.3515625秒。
可见:
堆排>希尔>二叉查找树>归并>快速>基数>计数


“二叉查找树排序”比不过“归并”和“快速”啊,也有可能是我的“二叉查找树排序”的代码还不够优化,期待大神指点。

也可以看出:二叉树中,“二叉查找树”比“堆”的效率高!!!

TA的精华主题

TA的得分主题

 楼主| 发表于 2019-9-30 18:28 | 显示全部楼层
本帖说说“二叉查找树排序”的道理。

以下列乱序序列为例:
i12345678910
brr(i)5261397648
一、对于排序“不稳定”的二叉查找树
左孩子键值<=父节点键值
父节点键值<右孩子键值
i的序号,依次可生成如下“二叉查找树”:
1.元素brr(i)的二叉查找树


图1.jpg

2.相对应的元素序号i的二叉图


图2.jpg


二、对于排序“稳定”的二叉查找树
左孩子键值<父节点键值
父节点键值<=右孩子键值
i的序号,依次可生成如下“二叉查找树”:
1.元素brr(i)的二叉查找树



图3.jpg


2.相对应的元素序号i的二叉图


图4.jpg


三、VBA中如何用数组构建二叉查找树
以文首乱序序列为例,以构建对排序“稳定”的二叉查找树为例。
i12345678910
brr(i)5261397648
1.记录左孩子序号
i12345678910
l(i)2400078000
2.记录右孩子序号
i12345678910
r(i)35609010000
3.记录父节点序号
i12345678910
f(i)0112236757
4.记录各节点位于父节点的左枝或右枝
i12345678910
lr(i)FalseFalseTrueFalseTrueTrueFalseFalseTrueTrue
其中:False表示左枝,True表示右枝。
5.标记各节点元素值(或键值)是否被“中序遍历”记录进有序区
i12345678910
jl(i)FalseFalseFalseFalseFalseFalseFalseFalseFalseFalse
全部为True时意味着程序(或“中序遍历”)的结束。
6.动态记录当前节点及其父节点的序号
i1:当前节点的父节点序号
i2:当前节点的序号
其实,程序中的所谓“二叉查找树”主要由源数组brr(i)和上面14项中的数组:l(i)r(i)f(i)lr(i)共同组成,是指它们的逻辑结构是“二叉查找树”,而非呈现形态。
四、二叉查找树的遍历取值
1.先序遍历:父节点→左孩子→右孩子;
2.中序遍历:左孩子→父节点→右孩子;
3.后序遍历:左孩子→右孩子→父节点。
可见,左孩子的访问始终在右孩子之前,以父节点的访问位置为划分依据,居左为“先序”,居中为“中序”,居右为“后序”。下面主要介绍“中序遍历”,因为该遍历方式下生成的是从小到大的已排序序列。
上例中,对于排序“稳定”的二叉查找树各节点的遍历次序如下图:



图5.jpg

其实,程序中实际的遍历寻找路线是:
5212343256108789810
虽然说“只需遍历一次”就可提取出完整的已排序序列,但是明显可以看出,这“一次”中既有“寻找”,又有“回溯”,来回折腾不在话下,故而效率低了些,比不上快速排序,但是比起“堆”这种二叉树的维护来说,效率又是高的。



TA的精华主题

TA的得分主题

 楼主| 发表于 2019-9-30 18:36 | 显示全部楼层
[广告] Excel易用宝 - 提升Excel的操作效率 · Excel / WPS表格插件       ★免费下载 ★       ★ 使用帮助
本帖最后由 aoe1981 于 2019-9-30 18:37 编辑
一把小刀闯天下 发表于 2019-9-28 23:04
'都是整数这样效率也不错,,,

Option Explicit

您这个也是“计数排序”,不过,我为何觉得内循环中的判断是不是没啥必要?
 If brr(i, 1) > 0 Then
      For j = 1 To brr(i, 1)
        m = m + 1
        arr(m, 1) = i
      Next Endif
为什么不直接写成:
      For j = 1 To brr(i, 1)
        m = m + 1
        arr(m, 1) = i
      Next

连lee1892大师也是这样做的,我想应该测试下速度。

评分

1

查看全部评分

TA的精华主题

TA的得分主题

发表于 2019-9-30 19:47 | 显示全部楼层
[广告] Excel易用宝 - 提升Excel的操作效率 · Excel / WPS表格插件       ★免费下载 ★       ★ 使用帮助
mark下,回头学习下

TA的精华主题

TA的得分主题

发表于 2019-9-30 20:27 | 显示全部楼层
  1. Sub pey_test()
  2. Dim arr, k0
  3. k0 = [a1].End(4).Row
  4. arr = [a1].Resize(k0, 1)

  5. Dim brr() As Long
  6. ReDim brr(0 To 1) '假设最小值 -1 最大值 65535
  7. brr(0) = -1: brr(1) = 65535

  8. Set dica = CreateObject("Scripting.Dictionary")
  9. Set dicb = CreateObject("Scripting.Dictionary")
  10. dica(-1) = 65535: dicb(-1) = 0

  11. '''''''循环插入
  12. For i = 1 To k0
  13.    dicb(arr(i, 1)) = dicb(arr(i, 1)) + 1
  14.    
  15.    If Not dica.exists(arr(i, 1)) Then
  16.       temp = Application.Lookup(arr(i, 1), brr)
  17.       dica(arr(i, 1)) = dica(temp): dica(temp) = arr(i, 1)
  18.       
  19.       ReDim Preserve brr(0 To UBound(brr) + 1)
  20.       For j = UBound(brr) To 0 Step -1
  21.           If brr(j - 1) = temp Then
  22.              brr(j) = arr(i, 1): Exit For
  23.           Else
  24.              brr(j) = brr(j - 1)
  25.           End If
  26.       Next j
  27.    End If
  28. Next i

  29. '''''''''结果输出
  30. ReDim brr(1 To k0, 1 To 1)
  31. k = 0: stra = -1

  32. Do
  33.    For i = 1 To dicb(stra)
  34.        k = k + 1: brr(k, 1) = stra
  35.    Next
  36.    stra = dica(stra)
  37. Loop Until k = k0

  38. [c1].Resize(k0, 1) = brr
  39. End Sub
复制代码
写了一个插入排序,A列为原始数字,数值范围暂假设(0-65535)可以是小数,C列为排序结果。

TA的精华主题

TA的得分主题

发表于 2019-9-30 23:44 | 显示全部楼层
[广告] Excel易用宝 - 提升Excel的操作效率 · Excel / WPS表格插件       ★免费下载 ★       ★ 使用帮助
  1. Sub pey_test()
  2. Dim arr, k0
  3. k0 = [a1].End(4).Row
  4. arr = [a1].Resize(k0, 1)

  5. Dim brr() As Double
  6. ReDim brr(0 To 1) '假设最小值 -1 最大值 65535
  7. brr(0) = -1: brr(1) = 65535

  8. Set dic = CreateObject("Scripting.Dictionary")
  9. '''''''循环插入
  10. For i = 1 To k0
  11.    If Not dic.exists(arr(i, 1)) Then
  12.       temp = Application.Lookup(arr(i, 1), brr)
  13.       
  14.       ReDim Preserve brr(0 To UBound(brr) + 1)
  15.       For j = UBound(brr) To 0 Step -1
  16.           If brr(j - 1) = temp Then
  17.              brr(j) = arr(i, 1): Exit For
  18.           Else
  19.              brr(j) = brr(j - 1)
  20.           End If
  21.       Next j
  22.    End If
  23.    dic(arr(i, 1)) = dic(arr(i, 1)) + 1
  24. Next i

  25. '''''''''结果输出
  26. Dim crr()
  27. ReDim crr(1 To k0, 1 To 1)

  28. For i = 1 To UBound(brr)
  29.    For j = 1 To dic(brr(i))
  30.        k = k + 1: crr(k, 1) = brr(i)
  31.    Next
  32. Next i

  33. [c1].Resize(k0, 1) = crr
  34. End Sub
复制代码
简化为 一个字典

评分

1

查看全部评分

TA的精华主题

TA的得分主题

发表于 2019-10-1 08:52 | 显示全部楼层
[广告] VBA代码宝 - VBA编程加强工具 · VBA代码随查随用  · 内置多项VBA编程加强工具       ★ 免费下载 ★      ★使用手册
aoe1981 发表于 2019-9-30 18:36
您这个也是“计数排序”,不过,我为何觉得内循环中的判断是不是没啥必要?
 If brr(i, 1) > 0 Then
  ...

感谢老师指点,试了一下确实是这样的。

各测试10次,分别累加计时值,去除if的效率能提升10%。只是觉得挺奇怪的,if本来就是用于逻辑判断,当条件不满足时不进入下层循环效率理应会更高些,但事实并不是这样的。

评分

1

查看全部评分

TA的精华主题

TA的得分主题

 楼主| 发表于 2019-10-1 09:29 | 显示全部楼层
一把小刀闯天下 发表于 2019-10-1 08:52
感谢老师指点,试了一下确实是这样的。

各测试10次,分别累加计时值,去除if的效率能提升10%。只是觉 ...

您就别这样客气了,在这个坛子里,我也是一个学习者,岂敢以“老师”自居。
用if判断,您和lee1892都是这样处理的,故而我想探讨,下一句for是从1开始循环,终值为0按理应该不会执行循环体,难道for后面的条件计算判断的效率比if的低?故而大神也会采取这种办法?
这才是我的疑惑,不能简单就说,我的想法是专业的或对的。

感谢感谢。

TA的精华主题

TA的得分主题

 楼主| 发表于 2019-10-1 09:58 | 显示全部楼层
[广告] Excel易用宝 - 提升Excel的操作效率 · Excel / WPS表格插件       ★免费下载 ★       ★ 使用帮助
zopey 发表于 2019-9-30 23:44
简化为 一个字典

您才是高手,写出的代码思路新奇,让人大开眼界!
(1)temp = Application.Lookup(arr(i, 1), brr)旨在查找不大于arr(i,1)的在brr中最接近的值;
(2)If Not dic.exists(arr(i, 1)) Then排除了重复值;
(3)循环插入旨在为接下来的字典计数做好准备,使得访问字典的key值是有序的。
佩服佩服,学习学习!

您需要登录后才可以回帖 登录 | 免费注册

本版积分规则

手机版|关于我们|联系我们|ExcelHome

GMT+8, 2024-4-27 00:41 , Processed in 0.041015 second(s), 10 queries , Gzip On, MemCache On.

Powered by Discuz! X3.4

© 1999-2023 Wooffice Inc.

沪公网安备 31011702000001号 沪ICP备11019229号-2

本论坛言论纯属发表者个人意见,任何违反国家相关法律的言论,本站将协助国家相关部门追究发言者责任!     本站特聘法律顾问:李志群律师

快速回复 返回顶部 返回列表