文章目录
- 冒泡排序
- 快速排序
- 快排的优化
- 单次快排的其他方案
- 快排的非递归实现
冒泡排序
冒泡排序,Bubble sort,通过重复遍历要排序的数列,比较每一对相邻元素,并在顺序错误时交换它们。这个过程一直重复,直到没有需要交换的相邻元素为止。
也就是每一次选出一个最大值,然后由此排序.
代码实现:
void BubbleSort(int* a, int n){for (int i = 0; i < n - 1; i++){int p = 1;for (int j = 0; j < n - i -1; j++){if (a[j] > a[j + 1]){Swap(&a[j], &a[j + 1]);p = 0;}}if (p){break;}}}
性能分析:冒泡排序的时间复杂度是标准的 O ( N2)O(N^2)O(N2),最好情况下是顺序有序,时间复杂度为 O ( N )O(N)O(N)。
由于冒泡排序性能的局限性,在实际场景的应用性几乎为零(有没有应用我是不清楚的), 其教学意义大于其实际意义,属于是最容易理解的排序算法。
快速排序
快速排序,Quick sort,它的基本思想是通过选取一个基准元素,将数组分为两部分,一部分小于基准元素,一部分大于基准元素,然后对这两部分分别进行递归排序,最终得到一个有序的数组。
通过上面的分析我们知道,快速排序选择一个基准值key,然后让key的左边的数小于等于key,右边的数大于等于key。那么我们通过一次排序后就决定好了一个元素的位置也就是key的位置。然后再对两部分进行递归排序。
这个思想明显有着二叉树递归的影子,key最好的情况下在确定在数组中间,再将key左边的数组(左子树)和右边的数组(右子树)递归。那么递归的深度就是二叉树的高度也就是 l o g NlogNlogN,每一层要遍历元素个数就是N,N-1,N-2…
代码实现:
void QuickSort(int* a, int begin, int end){if (begin >= end){return;}int left = begin, right = end;int keyi = begin;while (left < right){while (left < right && a[right] >= a[keyi])right--;while (left < right && a[left] <= a[keyi])left++;Swap(&a[left], &a[right]);}Swap(&a[left], &a[keyi]);QuickSort(a, begin, left - 1);QuickSort(a, left + 1, end);}
性能分析:时间复杂度为 O ( N l o g N )O(NlogN)O(NlogN)。
观察上述代码,值得注意的有以下几点:
- keyi即为key值对应的下标,一般选最左边的值也就是begin。
- 首先right这个下标找的是比key小的值,那么必须是right先走,这样才确保确保最后left和right相遇的时候下标对应的数组元素比key小或者相等。
分析:left和right相遇有以下两种情况:
1 right遇上left,那么left一开始就在等于key的地方。或者执行了Swap,那么left对应的数组值就是小于key的。
2.left遇上right,既然left可以移动了那就是说right已经找到了比key小的值,所以相遇的下标对应的数组元素也是比key小。
综上所述,当right先移动时,最终left==right时对应的下标的数组元素小于或等于key。 - left初始化不可以取begin+1有些初学者认为,begin不就是key值本身对应的下标么,那么我们和不从begin+1开始比较?有这种思想是正常的,但也是错误的。只要是任何一种顺序有序(或者其他情况),那么这种快排都是错的。
例如:数组int arr[]={1,2,3},left=1,right=2,keyi=0.那么left和right相遇在1,最后得到的结果为{2,1,3}。显然错误。 - left和right移动前要判断left < right ,防止越界。
- a[right] >= a[keyi]和a[left] a[keyi]和a[left] < a[keyi],否则left和right大概率不会相遇了。T.T
快排的优化
- 细心的读者已经发现了,当数组a为顺序有序的时候,快排的时间复杂度竟然是O( N 2)O(N^2) O(N2),而且还要不断递归申请空间,这时候甚至比不上BubbleSort了。这是因为我们keyi默认取最开始的值,又因为顺序有序所以key排在最左边,这样左边部分数组为空,右边部分数组大小为N-1。然后就N-2,N-3…递归。
所以优化方法也很简单我们换一种取keyi的方式,这里有个参考方案如下:
int GetMidi(int* a, int left, int right){int midi = (left + right) / 2;if (a[left] > a[midi]){if (a[midi] > a[right])return midi;else if (a[right] > a[left])return left;elsereturn right;}else{if (a[left] > a[right])return left;else if (a[right] > a[midi])return midi;elsereturn right;}}void quicksort(int* a, int begin, int end){if (begin >= end){return;}int left = begin, right = end;int keyi = begin;swap(&a[begin], &a[getmidi(a, left, right)]);while (left < right){while (left < right && a[right] >= a[keyi])right--;while (left < right && a[left] <= a[keyi])left++;swap(&a[left], &a[right]);}swap(&a[left], &a[keyi]);quicksort(a, begin, left - 1);quicksort(a, left + 1, end);}
这样我们就得到了一个较优的keyi取值。
- 此外,通过二叉树的结构我们知道,最下面几层的结点个数占所有结点个数的80%以上。也就是说当end-begin+1(数组长度)较小的时候,需要递归的次数就会较多。那么当长度较小的时候我们可以采用其他排序方式来大大减少递归次数。
void QuickSort(int* a, int begin, int end){if (begin >= end){return;}if (end - begin + 1 < 10){InsertSort(a + begin, end - begin + 1);}else{int left = begin, right = end;int keyi = begin;swap(&a[begin], &a[getmidi(a, left, right)]);while (left < right){while (left < right && a[right] >= a[keyi])right--;while (left < right && a[left] <= a[keyi])left++;swap(&a[left], &a[right]);}swap(&a[left], &a[keyi]);quicksort(a, begin, left - 1);quicksort(a, left + 1, end);}}
上述代码中,当数组长度小于10的时候我们选择用直接插入排序。
事实上这个优化可有可无,当今编译器优化做的已经非常好了多递归这几次和少递归这几次区别也不大。
单次快排的其他方案
上述快排是由霍尔提出的,但显然这个初代的版本要注意的细节太多。以下还有两种其他方式实现单词快排。
1.挖坑法
int PartSort2(int* a, int begin, int end){Swap(&a[begin], &a[GetMidi(a, begin, end)]);int key = a[begin];int hole = begin;while (begin < end){while (begin < end && a[end] >= key){end--;}a[hole] = a[end];hole = end;while (begin < end && a[begin] <= key){begin++;}a[hole] = a[begin];hole = begin;}a[hole] = key;return hole;}
其实这种实现方式和Hoare的实现大同小异,就不过多赘述请自行观摩。
2.前后指针法:
int PartSort3(int* a, int begin, int end){Swap(&a[begin], &a[GetMidi(a, begin, end)]);int prev = begin, cur = begin + 1;while (cur <= end){if (a[cur] < a[begin] && ++prev != cur){Swap(&a[prev], &a[cur]);}cur++;}Swap(&a[begin], &a[prev]);return prev;}
这种方法是通过cur找出比key小的值,再和prev交换然后prev++。
我个人认为这种方式是较上面两种方式简洁的而且也不会有那么多陷阱。
快排的非递归实现
要将一个递归的代码转换成非递归一般有以下两种方式:
1.转换成迭代(那说明这个代码本身就可以用循环写)
2.借助栈(先进先出的性质)来实现非递归。
代码如下:
void QuickSortNonR(int* a, int left, int right){Stack st;StackInit(&st);StackPush(&st, right);StackPush(&st, left);while (StackSize(&st) != 0){int begin = StackTop(&st);StackPop(&st);int end = StackTop(&st);StackPop(&st);int keyi = PartSort3(a, begin, end);if (begin < keyi - 1){StackPush(&st, keyi - 1);StackPush(&st, begin);}if (keyi + 1 < end){StackPush(&st, end);StackPush(&st, keyi + 1);}}StackDestroy(&st);}
假设这么一个数组有100个元素,并且每次key值都排到了中间。
那么上述代码就是说我先压栈了99,0.那么就可以取出left和right。拍好之后找个keyi=55.
随后压栈54,0,99,55.就可以取出右部分数组的begin和right。得到keyi=77.
压栈76,55,99,78.又得到了右部分数组。
…
就这样我们实现了用栈模拟出递归的效果。