快速排序遇到的小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语言实现》 马克.艾伦.维斯

posted @ 2022-01-23 23:15  bruce628  阅读(104)  评论(0编辑  收藏  举报