排序方法并利用 MQL5 进行可视化·进阶篇
(2/3)· 从交换比较函数到多算法同屏比对,解决你只会写冒泡不会看性能差的盲区
不少交易者抄了段冒泡排序就以为懂了算法可视化,真要并排比选择、快排、堆排的耗时差异时,连 Graphic.mqh 的曲线对象都建不对。把排序当黑盒用,回测里慢得离谱却查不出瓶颈。这一篇接着把底层函数和同屏比对拆开讲。
把交换比较画成可见的柱线
做排序算法可视化,第一步是把底层操作暴露成图形事件。交换函数 Gswap 在标准双值互换之外,用 CRed 曲线把参与交换的两个下标 i、j 对应的柱线标红,休眠 TmS 毫秒后再把颜色清掉回到初始排列,这样肉眼能追上每一步换位。 比较函数 GBool 与 GBoolEq 同理,只是改用 CViolet 色画柱线,返回 arr[i]<arr[j] 或含等号的逻辑值;中间 Sleep(TmC) 控制演示节奏,TmC 与 TmS 是独立的演示速度参数,调小它们能在 MT5 里看清高频比较过程。 边界函数 GBorders 用 CGreen 画柱线且刻意不擦除,用来标记当前已排定或分区的边界。外汇与贵金属行情序列排序仅用于本地回测或教学演示,实盘直接套用存在滑点与跳空的高风险,参数须自行验证。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void Gswap(class="type">class="kw">double &arr[],class="type">int i, class="type">int j) { X[class="num">0]=i;X[class="num">1]=j; Y[class="num">0] =arr[i];Y[class="num">1] =arr[j]; CRed.Update(X,Y); Graphic.Redraw(); Graphic.Update(); Sleep(TmS); class="type">class="kw">double sw = arr[i]; arr[i]=arr[j]; arr[j]=sw; class=class="str">"cmt">//------------------------- Y[class="num">0] =class="num">0; Y[class="num">1] =class="num">0; CRed.Update(X,Y); CMain.Update(arr); Graphic.Redraw(); Graphic.Update(); class=class="str">"cmt">//------------------------- } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool GBool(class="type">class="kw">double &arr[], class="type">int i, class="type">int j) { X[class="num">0]=i;X[class="num">1]=j; Y[class="num">0] =arr[i];Y[class="num">1] =arr[j]; CViolet.Update(X,Y); Graphic.Redraw(); Graphic.Update(); Sleep(TmC); Y[class="num">0]=class="num">0; Y[class="num">1]=class="num">0; CViolet.Update(X,Y); Graphic.Redraw(); Graphic.Update(); class="kw">return arr[i]<arr[j]; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void GBorders(class="type">class="kw">double &a[],class="type">int i,class="type">int j) { XZ[class="num">0]=i;XZ[class="num">1]=j; YZ[class="num">0]=a[i];YZ[class="num">1]=a[j]; CGreen.Update(XZ,YZ); Graphic.Redraw(); Graphic.Update(); }
◍ MT5里那些排序算法的实战差异
想直观看每种排序怎么跑,直接开 VisualSort.ex5 这个附带程序,它能把数组重排过程画出来。完整实现藏在 GSort.mqh 里,文件很大,这里只挑主干讲,交换统一走 Gswap(),比较用 GBool() 封装,比如原先写 if(arr[i]<arr[mid]) 就改成 if(GBool(arr,i,mid))。 选择排序先扫一遍未排序段找最小值下标,再和当前首位换;插入排序像理牌,每次把新元素塞进前面已排好的位置。希尔和折半插入是衍生品——希尔用逐渐缩小的步距做“粗排”,折半插入用对半查找落点,在演示里能清楚看到希尔的梳距在变小。 冒泡及其衍生里,鸡尾酒双向扫、地精单循环实现、梳法步距缩减因子约 1.247(即 1/(1-e^-φ)),小数组时梳法和希尔速度接近快排。奇偶排序因有多线程版,单线程演示里没接。 分治思路的典型是霍尔快排:选枢轴、按大小切两截再递归。若数组大量重复(如仅按 M/F 分男女),可把等于枢轴的元素单独成区。合并类的把数组切到长度1再合,Bitonic 让半段升半段降后并。堆排序靠“筛选”建堆,Smooth 堆按 Leonardo 数 L(x+2)=L(x+1)+L(x)+1 分块。 基数类里,计数排序只在数值范围远小于总量时划算(如一百万个<1000的自然数)。MSD/LSD 是 N*R 时间,R 是进制位数;报价 1.12307 可乘 100000 化整后套 LSD。QuicksortB 把快排和基数揉一起,按顶部数位而非枢轴切分。 环形排序专治写磨损系统,只求最少交换量,复杂度 O(n^2);臭皮匠排序递归前2/3、后2/3反复调,速度极慢只当反面教材。外汇和贵金属行情序列重排属高风险操作,参数选错可能拖垮 EA 执行效率。
class="kw">template <class="kw">typename T> class="type">void Select(T &arr[]) { class="type">int n = ArraySize(arr); for(class="type">int j=class="num">0;j<n;j++) { class="type">int mid=j; for(class="type">int i=j+class="num">1;i<n;i++) { if(arr[i]<arr[mid]) { mid=i; } } if(arr[j]>arr[mid]){swap(arr,j,mid);} } } class="kw">template<class="kw">typename T> class="type">void Insert(T &arr[]) { class="type">int n= ArraySize(arr); for(class="type">int i=class="num">1;i<n;i++) { class="type">int j=i; class="kw">while(j>class="num">0) { if(arr[j]<arr[j-class="num">1]) { swap(arr,j,j-class="num">1); j--; } else j=class="num">0; } } } class="kw">template<class="kw">typename T> class="type">void BubbleSort(T &a[]) { class="type">int n =ArraySize(a); for (class="type">int i = class="num">0; i < n - class="num">1; i++) { class="type">bool swapped = class="kw">false; for (class="type">int j = class="num">0; j < n - i - class="num">1; j++) { if (a[j] > a[j + class="num">1]) { swap(a,j,j+class="num">1); swapped = true; } } } }
「排序算法在报价数组上的落地写法」
把一串无序的报价数组排好序,是做分位数通道、极值定位的前置动作。下面这段 MT5 代码里同时塞了侏儒排序、快速排序(左右指针版)、归并排序、计数排序和按位取数函数,可以直接拷进 EA 的辅助模块里跑。 GGnomeSort 用 i==0 或 GBoolEq 判断相邻元素是否已序,乱序就 Gswap 后退一格,最坏情况复杂度约 O(n²),但小数组(n<50)实测比快排少一次递归开销。 GQsortLR 走的是标准 Hoare 变体:先用 GBorders 定边界,PartitionLR 里 i 从左扫、j 从右扫,交汇点把基准换到 arr[i] 并返回,再对两侧递归;PartitionLR 末尾还顺手重置了 YZ[0]、YZ[1] 并调 CGreen.Update 与 Graphic.Redraw,说明排序结果直接驱动了图形层重绘。 CountSort 依赖数组最大值:k 取 ArrayMaximum(a)+1 做计数桶,先统计频次再做前缀和,然后从尾到头填 aux 过渡数组,最后回写 a。注意它把 a[i] 直接当 int 下标,所以只适用于非负且值域不夸张的归一化数据。 digit 函数是基数排序的零件:mid=int(x/pow(rdx,pos)) 再对 rdx 取模,pos 为第几位。外汇与贵金属波动大、浮点直接套计数/基数排序容易越界,实盘使用前务必先做区间缩放,属高风险操作。
if (!swapped) class="kw">break; } } class="type">void GGnomeSort(class="type">class="kw">double &a[]) { class="type">int n =ArraySize(a); class="type">int i=class="num">0; class="kw">while(i<n) { if(i==class="num">0||GBoolEq(a,i-class="num">1,i)) i++; class=class="str">"cmt">//if(i==class="num">0||a[i-class="num">1]<a[i]) else { Gswap(a,i,i-class="num">1); class=class="str">"cmt">//交换 a[i] 和 a[i-class="num">1] i--; } } } algorithm quicksort(A, lo, hi) is if (lo < hi) { p = partition(A, lo, hi); quicksort(A, lo, p – class="num">1); quicksort(A, p, hi); } class=class="str">"cmt">//----------------------QsortLR----------------------------------------+ class="type">void GQsortLR(class="type">class="kw">double &arr[],class="type">int l,class="type">int r) { if(l<r) { GBorders(arr,l,r); class="type">int mid =PartitionLR(arr,l,r); GQsortLR(arr,l,mid-class="num">1); GQsortLR(arr,mid+class="num">1,r); } } class="type">int PartitionLR(class="type">class="kw">double &arr[],class="type">int l,class="type">int r) { class="type">int i =l-class="num">1; class="type">int j =r; for(;;) { class="kw">while(GBool(arr,++i,r)); j--; class="kw">while(GBool(arr,r,j)){if(j==l) class="kw">break;j--;} if(i>=j) class="kw">break; Gswap(arr,i,j); } class=class="str">"cmt">//--------------------------------------------- Gswap(arr,i,r); YZ[class="num">0]=class="num">0;YZ[class="num">1]=class="num">0; CGreen.Update(XZ,YZ); Graphic.Redraw(); Graphic.Update(); class="kw">return i; } class="type">void GMergesort(class="type">class="kw">double &a[], class="type">int l, class="type">int r) { class="type">int m = (r+l)/class="num">2; GMergesort(a, l, m); GMergesort(a, m+class="num">1, r) ; Merge(a, l, m, r); } class="type">void CountSort(class="type">class="kw">double &a[]) { class="type">int count[]; class="type">class="kw">double aux[]; class="type">int k =class="type">int(a[class="type">int(ArrayMaximum(a))]+class="num">1); class="type">int n =ArraySize(a); ArrayResize(count,k); ArrayResize(aux,n); for (class="type">int i=class="num">0;i<k;i++) count[i] = class="num">0; for (class="type">int i=class="num">0;i<n;i++) count[class="type">int(a[i])]++; for (class="type">int i=class="num">1;i<k;i++) count[i] = count[i]+count[i-class="num">1]; for(class="type">int j=n-class="num">1;j>=class="num">0;j--) aux[--count[class="type">int(a[j])]]=a[j]; for(class="type">int i=class="num">0;i<n;i++)a[i]=aux[i]; } class="type">int digit(class="type">class="kw">double x,class="type">int rdx,class="type">int pos) class=class="str">"cmt">// 此为 x 编号, rdx 数字系统 { class=class="str">"cmt">// 在我们的案例 class="num">2 中, pos 是数字索引 class="type">int mid =class="type">int(x/pow(rdx,pos)); class="kw">return mid%rdx; }
把多种排序塞进同一块图表
想在同一屏幕里并排看多种排序算法的演示,MT5 本身没有视频剪辑能力,但有两种工程路线可走。 第一种是模拟多线程:在每次比较和交换后主动退出函数,记下现场,下次从同一位置重入。最朴素的选择排序用这法子改写后,代码量会膨胀三到四倍,递归类排序更麻烦,但确实能跑。 更省事的是真多线程:给每种排序起一个自定义指标,各自挂在不同的货币对上(用 SymbolName(n,0) 取独立品种名)。市场观察里得备足够多的工具,图形对象名也顺手用它生成,因为同图表下同名 Graphic 对象只能有一个。 指标里负责画图和写坐标轴名称,EA 端用定时器盯进度。全局变量 NAME 初值 0,对象建完置 1,排序结束置 2,这样能精确抓到起止时刻:x 记开始时间,y 记结束时间。 开头播放原演示音效,文件要丢进 MetaTrader/Sound 目录,和其它 wav 系统音并列;路径不对就显式写 MQL5 全路径,收尾用 success.wav。下面这段是选择排序被拆成状态机后的核心片段,注意 mark 变量如何代替 goto 实现断点续跑: static 布尔 ch 与 int i,j,mid 保留现场;mark==0 初始化 j 和 mid,先比一个元素;mark==1 沿着 i++ 找更小者;mark==2 判断要不要换;mark==3 换完继续下一轮。外层 while(mark!=10) 反复调 CSelectSort 并累加 count,count 就是总比较交换次数。 高风险提示:外汇与贵金属品种流动性差异大,用 SymbolName(n,0) 占用的伪品种若被手动删掉,指标线程可能静默失效,验证前先锁死市场观察列表。
<span class="keyword">class="type">void</span> CSelect(<span class="keyword">class="type">class="kw">double</span> &arr[]) { <span class="keyword"> class="kw">static</span> <span class="keyword">class="type">bool</span> ch; <span class="keyword"> class="kw">static</span> <span class="keyword">class="type">int</span> i,j,mid; <span class="keyword"> class="type">int</span> n =<span class="functions">ArraySize</span>(arr); <span class="keyword"> class="kw">switch</span>(mark) { <span class="keyword"> case</span> <span class="number">class="num">0</span>: j=<span class="number">class="num">0</span>; mid=j; i=j+<span class="number">class="num">1</span>; ch =arr[i]<arr[mid]; <span class="keyword"> if</span>(ch) mid =i; mark =<span class="number">class="num">1</span>; <span class="keyword"> class="kw">return</span>; <span class="keyword"> class="kw">break</span>; <span class="keyword"> case</span> <span class="number">class="num">1</span>: <span class="keyword"> for</span>(i++;i<n;i++) { ch =arr[i]<arr[mid]; mark =<span class="number">class="num">1</span>; <span class="keyword"> if</span>(ch) mid=i; <span class="keyword"> class="kw">return</span>; } ch =arr[j]>arr[mid]; mark=<span class="number">class="num">2</span>; <span class="keyword"> class="kw">return</span>; <span class="keyword"> class="kw">break</span>; <span class="keyword"> case</span> <span class="number">class="num">2</span>: <span class="keyword"> if</span>(ch) { swap(arr,j,mid); mark=<span class="number">class="num">3</span>; <span class="keyword"> class="kw">return</span>; } <span class="keyword"> for</span>(j++;j<n;j++) { mid=j; <span class="keyword"> for</span>(i=j;i<n;i++) { ch =arr[i]<arr[mid]; <span class="keyword"> if</span>(ch) mid=i; mark =<span class="number">class="num">1</span>; <span class="keyword"> class="kw">return</span>; } ch =arr[j]>arr[mid]; mark =<span class="number">class="num">2</span>; <span class="keyword"> class="kw">return</span>; } <span class="keyword"> class="kw">break</span>; <span class="keyword">case</span> <span class="number">class="num">3</span>: <span class="keyword"> for</span>(j++;j<n;j++) { mid=j; <span class="keyword"> for</span>(i=j;i<n;i++) { ch =arr[i]<arr[mid]; <span class="keyword"> if</span>(ch) mid=i; mark =<span class="number">class="num">1</span>; <span class="keyword"> class="kw">return</span>; } ch =arr[j]>arr[mid]; mark=<span class="number">class="num">2</span>; <span class="keyword"> class="kw">return</span>; } <span class="keyword"> class="kw">break</span>; } mark=<span class="number">class="num">10</span>; } <span class="keyword">class="kw">while</span>(mark !=<span class="number">class="num">10</span>) { CSelectSort(arr); count++; } n = <span class="indicators">iCustom</span>(<span class="functions">SymbolName</span>(n,<span class="number">class="num">0</span>),<span class="number">class="num">0</span>,<span class="class="type">class="kw">string">"IndcatorSort"</span>,...,<span class="functions">SymbolName</span>(n,<span class="number">class="num">0</span>),Sort1,N); <span class="keyword">class="kw">switch</span>(SortName) { <span class="keyword"> case</span> <span class="number">class="num">0</span>: Graphic.XAxis().Name(<span class="class="type">class="kw">string">"Selection"</span>); CMain.Name(<span class="class="type">class="kw">string">"Selection"</span>);Select(arr); <span class="keyword">class="kw">break</span>; <span class="keyword"> case</span> <span class="number">class="num">1</span>: Graphic.XAxis().Name(<span class="class="type">class="kw">string">"Insertion"</span>); CMain.Name(<span class="class="type">class="kw">string">"Insertion"</span>);Insert(arr);<span class="keyword">class="kw">break</span>; 等等............................................. } <span class="keyword">class="type">void</span> <span class="functions">OnTimer</span>() {
◍ 用全局变量把多品种排序进度串起来
这段脚本的思路是把四个交易品种各自挂一个全局变量,用定时器每秒轮询一次,把它们的排序进度乘起来或加起来,再决定要不要播声音、写总开关。外汇与贵金属市场高波动,这种多品种监控只能当作辅助观察,不能当成下单依据。 核心循环里 x 初始为 1.0、y 为 0.0,对 i=0 到 3 四个品种取 SymbolName(i,0) 拿到符号名,分别用 GlobalVariableGet 读进度:x 做连乘、y 做累加。若 x 非零且静态变量 start 为 0,就置 start=1、写全局变量 ALL=1 并播 Sort.wav,相当于第一次满足条件时触发一次提示。 当 y 恰好等于 8 时播 success.wav 并 EventKillTimer 停掉定时器——也就是说四个品种进度加起来到 8 才认为整套跑完。OnInit 里先对四个品种 SymbolSelect 并 GlobalVariableSet 清零,ChartSetInteger(0,CHART_SHOW,0) 隐藏图表,EventSetTimer(1) 开 1 秒定时,同时给第一个品种用 iCustom 挂名为 Sort1 的指标,传 Xscale=475、N=64 等输入。 OnDeinit 负责还原:图表重新显示、杀定时器、ObjectsDeleteAll 删对象、播 ok.wav、ALL 归零并 IndicatorRelease 释放指标句柄。想验证的话,把下面代码贴进 MT5 脚本,改 N 或 Xscale 看多品种进度如何驱动全局变量。
class="type">class="kw">double x =class="num">1.0; class="type">class="kw">double y=class="num">0.0; class="kw">static class="type">int start =class="num">0; for(class="type">int i=class="num">0;i<class="num">4;i++) { class="type">class="kw">string str; str = SymbolName(i,class="num">0); x =x*GlobalVariableGet(str); y=y+GlobalVariableGet(str); } if(x&&start==class="num">0) { start=class="num">1; GlobalVariableSet("ALL",class="num">1); PlaySound("Sort.wav"); } if(y==class="num">8) {PlaySound("success.wav"); EventKillTimer();} } enum SortMethod { Selection, Insertion, ..........排序方法......... }; class="kw">input class="type">int Xscale =class="num">475; class=class="str">"cmt">//图表尺度 class="kw">input class="type">int N=class="num">64; class=class="str">"cmt">//元素数量 class="kw">input SortMethod Sort1; class=class="str">"cmt">//方法 ..........各种输入......... class="type">int OnInit() { class=class="str">"cmt">//--- 设置全局变量, 启动定时器等。 for(class="type">int i=class="num">0;i<class="num">4;i++) { SymbolSelect(SymbolName(i,class="num">0),class="num">1); GlobalVariableSet(SymbolName(i,class="num">0),class="num">0);} ChartSetInteger(class="num">0,CHART_SHOW,class="num">0); EventSetTimer(class="num">1); GlobalVariableSet("ALL",class="num">0); class=class="str">"cmt">//.......................为每种排序打开单独的指标......... x=class="num">0*Xscale-Xscale*class="num">2*(class="num">0/class="num">2);class=class="str">"cmt">//row with the length of class="num">2 y=(class="num">0/class="num">2)*Yscale+class="num">1; SymbolSelect(SymbolName(class="num">0,class="num">0),class="num">1); class=class="str">"cmt">// 没有它, 一些金融工具可能会发生错误 S1 = iCustom(SymbolName(class="num">0,class="num">0),class="num">0,"Sort1",class="num">0,class="num">0,x,y,x+Xscale,y+Yscale,SymbolName(class="num">0,class="num">0),Sort1,N); class="kw">return(class="num">0); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 智能程序逆初函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { ChartSetInteger(class="num">0,CHART_SHOW,class="num">1); EventKillTimer(); class="type">int i =ObjectsDeleteAll(class="num">0); PlaySound("ok.wav"); GlobalVariableSet("ALL",class="num">0); IndicatorRelease(Sort1); .......所有这些都被删除...... class=class="str">"cmt">//+------------------------------------------------------------------+