一、前言
前面我们讲了直接插入排序和希尔排序这两种插入排序,以及直接选择排序和堆排序这两种选择排序。如果还有什么问题就请点击下面的链接
那么今天我们就来讲一讲交换排序。
二、交换排序
基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
三、冒泡排序:最简单的交换排序
第一次排序:
1、从第一个下标开始,也就是0,比较第一个和第二个元素。
2、如果第一个元素比第二个元素大,那就交换两者。
3、然后比较第二个元素和第三个元素,如果两者也不是升序,那交换两者。
4、一直比较和交换,直到最后。
接下来就是从下标为1的开始,重复上述步骤,向后比较交换,直至倒数第二个数比较交换完。
如下图:
* 代码实现
void swap(int* a, int* b) { int tmp = *a; *a = *b; *b = tmp; } void BubbleSort(int* a, int n) { for (int i = 0; i < n; i++) { for (int j = 0; j < n - 1 - i; j++) { if (a[j] > a[j + 1]) { Swap(&a[j], &a[j + 1]); } } } }
冒泡排序的特性总结:
1. 冒泡排序是一种非常容易理解的排序
2. 时间复杂度:O(N^2)
3. 空间复杂度:O(1)
4. 稳定性:稳定
四、快速排序
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
快速排序算法通过多次比较和交换来实现排序,其排序流程如下:
1、首先设定一个分界值,通过该分界值将数组分成左右两部分。
2、将大于或等于分界值的数据集中到数组右边,小于分界值的数据集中到数组的左边。此时,左边部分中各元素都小于分界值,而右边部分中各元素都大于或等于分界值。这个分界值就在它正确的位置上。
3、然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理。
4、重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左、右两个部分各数据排序完成后,整个数组的排序也就完成了。
当然,我们可以利用递归来实现快排。这里我们就先搭建一个快速排序的框架,代码如下:
void QuickSort(int* a, int left, int right) { if (left >= right) return; int pivot = PartSort(a, left, right); QuickSort(a, left, pivot - 1); QuickSort(a, pivot + 1, right); }
从上图我们可以看出我们实现快速排序的关键就是找到pivot,即实现快速排序的单趟排序。
那么接下来我们就会讲解实现单趟排序的三个方法。
* 挖坑法:单趟排序思想如下图
代码实现:
int PartSort1(int* a, int left, int right) { int begin = left; int end = right; int pivot = begin; int key = a[pivot]; while(begin < end) { //从右找小 while(begin < end && a[end] > key) { end--; } a[pivot] = a[end]; pivot = end; //从左找大 while(begin < end && a[begin] < key) { begin++; } a[pivot] = a[begin]; pivot = begin; } pivot = begin; a[pivot] = key; return pivot; }
* 左右指针法:单趟排序思想如下图
代码实现
void swap(int* a, int* b) { int tmp = *a; *a = *b; *b = tmp; } int PartSort2(int* a, int left, int right) { int begin = left; int end = right; int keyi = begin;//基准值下标 while(left < end) { while(a[begin] < a[keyi]) { begin++; } while(a[end] > a[keyi]) { end--; } swap(&a[begin], &a[end]); } swap(&a[keyi], &a[begin]); keyi = begin; return keyi; }
* 前后指针法:单趟排序思想如下图
代码实现
int PartSort3(int* a, int left, int right) { int cur = left; int prev = left + 1; int keyi = left; while (prev <= right) { //prev找小 if (a[prev] < a[keyi] && ++cur != prev) Swap(&a[cur], &a[prev]); prev++; } Swap(&a[keyi], &a[cur]); keyi = cur; return keyi; }
五、快速排序的优化
1、 三数取中法选key
//三数取中 int GetMidIndex(int* a, int left, int right) { int mid = (left + right) / 2; if (a[left] < a[mid]) { if (a[mid] < a[right]) { return mid; } else if (a[left] > a[right]) { return left; } else { return right; } } else { if (a[mid] > a[right]) { return mid; } else if (a[right] > a[left]) { return left; } else { return right; } } }
然后我们将三数取中里得到的位置上的数与最左边的数交换,然后再实现单趟排序即可。
2、递归到小的子区间时,可以考虑使用插入排序(直接插入排序请参考前面所讲的)
void QuickSort(int* a, int left, int right) { if (left >= right) return; int pivot = PartSort(a, left, right); QuickSort(a, left, pivot - 1); QuickSort(a, pivot + 1, right); //小区间优化:区间足够小后直接使用直接插入排序 if (pivot - 1 - left > 10) { QuickSort(a, left, pivot - 1); } else { InsertSort(a + left, pivot - 1 - left + 1); } if (right - pivot - 1 > 10) { QuickSort(a, pivot + 1, right); } else { InsertSort(a + pivot + 1, right - (pivot + 1) + 1); } }
六、快速排序的非递归
为什么要实现快速排序的非递归呢?
因为快速排序是用递归的思想,递归的实现则需要不断地建立栈帧,如果待排序数据很多很多,那么可能会发生栈溢出,程序出错。所以我们可以考虑使用快速排序的非递归。
思路:这里我们可以利用栈来帮助我们实现这个递归过程。(这里我们就先不在这实现栈了,我们直接使用)。
//快速排序的非递归:利用栈实现递归过程 void QuickSortNonR(int* a, int begin, int end) { struct stack st; StackInit(&st); StackPush(&st, end); StackPush(&st, begin); while(!StackEmpty(&st)) { int left = StackTop(&st); StackPop(&st); int right = StackTop(&st); StackPop(&st); int keyi = PartSort(a, left, right); //[left,keyi-1] keyi [keyi+1,right] if(left < keyi-1) { StackPush(&st, keyi-1); StackPush(&st, left); } if(keyi+1 < right) { StackPush(&st, right); StackPush(&st, keyi+1); } } StackDestory(&st); }
快速排序的特性总结:
1、 时间复杂度:O(N*logN)
2、空间复杂度:O(logN)
3、 稳定性:不稳定