快速排序遇到的小bug
测试环境
Ubuntu 18.04, gcc 8.4
复习一下快排算法,不料却得到了非预期的结果。示例代码如下
1 #include <stdio.h> 2 3 void mySwap(int *p, int *q) 4 { 5 *p ^= *q; 6 *q ^= *p; 7 *p ^= *q; 8 } 9 10 void getPivot(int *srcArr, int left, int right) 11 { 12 int mid = left + (right-left)/2; 13 14 if(srcArr[left] > srcArr[mid]) 15 { 16 mySwap(&srcArr[left], &srcArr[mid]); 17 } 18 19 if(srcArr[mid] > srcArr[right]) 20 { 21 mySwap(&srcArr[mid], &srcArr[right]); 22 } 23 24 if(srcArr[left] > srcArr[right]) 25 { 26 mySwap(&srcArr[mid], &srcArr[right]); 27 } 28 } 29 30 void quickSort(int *srcArr, int left, int right) 31 { 32 if (left >= right) 33 { 34 return; 35 } 36 37 int low = left; 38 int high = right; 39 40 int pivot; 41 //getPivot(srcArr, low, high); 42 43 int mid = low + (high-low)/2; 44 45 mySwap(&srcArr[low], &srcArr[mid]); 46 47 pivot = srcArr[low]; 48 49 while (low < high) 50 { 51 while (srcArr[high] >= pivot && low < high) 52 { 53 --high; 54 } 55 56 srcArr[low] = srcArr[high]; 57 58 while (srcArr[low] <= pivot && low < high) 59 { 60 ++low; 61 } 62 63 srcArr[high] = srcArr[low]; 64 65 } 66 67 srcArr[low] = pivot; 68 quickSort(srcArr, left, low-1); 69 quickSort(srcArr, low+1, right); 70 } 71 72 int main() 73 { 74 int srcArr[10] = {1, 3, 5, 7, 9, 2, 4, 6, 8, 10}; 75 76 quickSort(srcArr, 0, 9); 77 78 for(int i = 0; i < 10; ++i) 79 { 80 printf("%d ", srcArr[i]); 81 } 82 83 putchar(10); 84 85 return 0; 86 }
运行结果如下
第一个元素为0,本应该是1的。在这个过程中我并没有修改数组中的元素呀,为什么出现了0呢?为什么只有第一个元素有问题呢?其它的元素为什么没有问题呢?
使用gdb调试,在排序的序列尽量靠近左侧时打印下标和元素的值,发现了一个非预期的现象。当排序第0个和第1个元素这两个元素所在的序列时,执行到截图中第44行(交换两个元素的值)代码后,下标都没有问题,low = 0, mid = 0, high = 1。
问题是第0个元素值此时变成了0了。只是一个交换,第0个元素咋就变成0了呢?再看看交换两个元素的值是通过异或来进行的,一下子反应过来了,估计是与自身异或了。任何整数与自身异或结果为0。
(截图中的行号与前面示例代码中的行号有点出入,请知悉)
调整,在交换之前判断下是否为同一个元素,为不同的元素才进行交换。
1 if (low != mid) 2 { 3 mySwap(&srcArr[low], &srcArr[mid]); 4 }
再次运行
结果符合预期。
再回到这个问题,为什么只有第一个元素有问题呢?其它的元素为什么没有问题呢?
按照上面提供的数组中的数据,复盘了下排序的过程,排序过程中,中轴两边的序列(左边的元素小于中轴,右边的元素大于中轴),前面的几轮中,右边都只有一个元素。最后几轮中,刚好有一轮是4个元素,即第0个到第3个元素。排序后,中轴左边剩下两个元素,即经0个和第1个元素,中轴右侧只剩下第3个元素。
这两个元素的序列在排序前,先进行了交换,想着尽可能取中间的元素作为中轴(假设中间的元素为均值),于是将最左侧的元素与序列中间的元素进行了交换。注意,此时,中间的元素与第0个元素的下标一致,即中间的元素即为第0个元素。相同的元素异或,结果为0,结果使第0个元素值为0了。
这也导致了问题的出现。
从复盘过程中也明显感受到了快速排序的弊端,中轴的值取得不好的情况,像示例代码中,总是导致中轴一侧仅有一个元素,导致了最坏的情况和最坏的时间复杂度。取合适的中轴的值是值得思考的,取得一个好的中间的值,可以使得中轴两的序列分布比较均匀,可以凸显快排的优势。一是可以采用随机算法,得到一个随机的下标,但这也不能保证它一定就是最优的,另外随机算法也需要一定的开销。二是,取首元素、中间元素、尾元素三者中的中间者作为中间元素(即比较三者大小取中间者),这也要求序列中至少有三个元素,当然这个也是在拼人品。并不能保证中轴值最为合理。
下面示例代码采用了第二种方式实现了一下,为保证序列中至少有三个元素,增加了判断条件。
1 int low = left; 2 int high = right; 3 int center = (left+right)/2; 4 int pivot; 5 6 if(right - left >= 2) 7 { 8 if(arr[left] > arr[right]) 9 { 10 mySwap(&arr[left], &arr[right]); 11 } 12 13 if(arr[center] > arr[right]) 14 { 15 mySwap(&arr[center], &arr[right]); 16 } 17 18 if(arr[left] > arr[center]) 19 { 20 mySwap(&arr[left], &arr[center]); 21 } 22 23 mySwap(&arr[left], &arr[center]); 24 } 25 26 pivot = arr[left];
加个题外话,如果元素数量较少(如少于20)时,可以选择其它排序算法如插入排序等。上面的示例代码仅是作复习快速排序算法用的。
注:因为bug是后面重新复现的,截图的调试中代码行号与示例代码中的行号有3行内的误差,请知悉。
参考材料:
《数据结构与算法分析 C语言实现》 马克.艾伦.维斯