目录
1.交换排序—冒泡排序
1.1冒泡排序基本思想
1.2冒泡排序的实现
2.交换排序—快速排序
1.1快速排序基本思想
1.2基准值划分—分析
1. hoare版:
2. 挖坑法:
3. 前后指针版本
1.3 hoare快排的具体实现
1.4 挖坑法快排的具体实现
1.5 前后指针版本快排的具体实现
1.6 快速排序的优化
优化1:三数取中(提升效率,防止有序序列栈溢出)
优化2:小区间优化(极大减少递归次数)
1.7 快排非递归
1.8 快速排序的特性总结
1.交换排序—冒泡排序
1.1冒泡排序基本思想
基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排 序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
1.2冒泡排序的实现
void BubbleSort(int*a, int n)
{
int i = 0;
for (i = 0; i < n - 1; i++)//控制趟数
{
int flag = 1;//假设已经有序
int j = 0;
for (j = 0; j < n - 1 - i; j++)//控制比较次数
{
if (a[j] > a[j + 1])
{
flag = 0;
int temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
}
}
if (1 == flag)
{
break;
}
}
}
冒泡排序的特性总结:
1. 冒泡排序是一种非常容易理解的排序
2. 时间复杂度:O(N^2)
3. 空间复杂度:O(1)
4. 稳定性:稳定
2.交换排序—快速排序
1.1快速排序基本思想
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中 的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右 子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
// 假设按照升序对array数组中[left, right)区间中的元素进行排序 void QuickSort(int array[], int left, int right) { if (right - left <= 1) return; // 按照基准值对array数组的 [left, right)区间中的元素进行划分 int div = partion(array, left, right); // 划分成功后以div为边界形成了左右两部分 [left, div) 和 [div+1, right) // 递归排[left, div) QuickSort(array, left, div); // 递归排[div+1, right) QuickSort(array, div + 1, right); }
上述为快速排序递归实现的主框架,发现与二叉树前序遍历规则非常像,在写递归框架时可想想二叉 树前序遍历规则即可快速写出来,后序只需分析如何按照基准值来对区间中数据进行划分的方式即可。
1.2基准值划分—分析
将区间按照基准值划分为左右两半部分的常见方式有:
1. hoare版:
分析图:
hoare版—快排单趟排序实现:
//hoare int PartSort1(int* a, int left, int right) { int keyi = left; 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[keyi], &a[left]); // [begin , keyi - 1] keyi [keyi + 1,end] keyi = left; return keyi; }
2. 挖坑法:
分析图:
挖坑法—快排单趟排序实现:
//挖坑法 int PartSort2(int* a, int left, int right) { //坑值 int key = a[left]; //坑位 - left 坑位是动态变化 while (left < right) { while (left < right && a[right] >= key) { right--; } //此时将right,大于key的值,填坑到left坑位 a[left] = a[right]; while (left < right && a[left] <= key) { left++; } //此时left,小于key的值,填坑到right坑位 a[right] = a[left]; } a[left] = key; //返回最终填坑位置,再次分为[begin , keyi - 1] keyi [keyi + 1,end] return left; }
3. 前后指针版本
前后指针版本—快排单趟排序实现:
//前后指针版 int PartSort3(int* a, int left, int right) { int keyi = left; int prev = left, cur = left + 1; while (cur <= right) { if (a[cur] < a[keyi] && ++prev != cur) { Swap(&a[prev], &a[cur]); } cur++; } Swap(&a[keyi], &a[prev]); return prev; }
1.3 hoare快排的具体实现
当上述hoare第一趟排完,key值便到了其顺序的正确位置,此时分为 [begin , keyi - 1] keyi [keyi + 1,end] ,对 keyi 位置左边再次进行排序,其次在对 keyi 位置右边进行排序,再次分为 [begin , keyi - 1] keyi [keyi + 1,end] ,因此采用分治思想,利用递归,实现快排。
void QuickSort(int* a, int begin, int end) {
assert(a);
//当区间不存在,或者只有一个时,则不需要再处理
if (begin >= end) {
return;
}
int left = begin,right = end;
int key = a[left];
//坑位 - left 坑位是动态变化
while (left < right) {
while (left < right && a[right] >= key) {
right--;
}
//此时将right,大于key的值,填坑到left坑位
a[left] = a[right];
while (left < right && a[left] <= key) {
left++;
}
//此时left,小于key的值,填坑到right坑位
a[right] = a[left];
}
a[left] = key;
QuickSort(a, begin, left - 1);
QuickSort(a, left + 1, end);
}
1.4 挖坑法快排的具体实现
void QuickSort(int* a, int begin,int end){
assert(a);
//当区间不存在,或者只有一个时,则不需要再处理
if (begin >= end) {
return;
}
int key = a[left];
//坑位 - left 坑位是动态变化
while (left < right) {
while (left < right && a[right] >= key) {
right--;
}
//此时将right,大于key的值,填坑到left坑位
a[left] = a[right];
while (left < right && a[left] <= key) {
left++;
}
//此时left,小于key的值,填坑到right坑位
a[right] = a[left];
}
a[left] = key;
QuickSort(a, begin, key - 1);
QuickSort(a, key + 1, end);
}
1.5 前后指针版本快排的具体实现
void QuickSort(int* a, int begin, int end) {
assert(a);
//当区间不存在,或者只有一个时,则不需要再处理
if (begin >= end) {
return;
}
int left = begin, right = end;
int keyi = left;
int prev = left, cur = left + 1;
while (cur <= right) {
if (a[cur] < a[keyi] && ++prev != cur) {
Swap(&a[prev], &a[cur]);
}
cur++;
}
Swap(&a[keyi], &a[prev]);
keyi = prev;
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
1.6 快速排序的优化
优化1:三数取中(提升效率,防止有序序列栈溢出)
分析:
- 如果每次所选key值为中位数,此时效率较高,数据会快速趋向于有序。
- 如果一组数据,完全有序,但是快排仍需要递归从第一个key值进行检测,检测完在检测剩下 N-1 个数据,在检测剩下 N-2 个数据 ...最终检测剩下一个数据时,递归层层返回。效率极低,时间复杂度为O(N^2)。
- 如果数据量过大,层层递归检测,有可能会出现栈溢出情景。
解决方法:
- 随机选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 right;
}
else {
return left;
}
}
else {//a[left] > a[mid] a[mid] < a[right]
if (a[mid] > a[right]) {
return mid;
}
else if (a[left] < a[right]) {
return left;
}
else {
return right;
}
}
}
//前后指针版
int PartSort3(int* a, int left, int right) {
int mid = GetMidIndex(a, left, right);
Swap(&a[left], &a[mid]);
int keyi = left;
int prev = left, cur = left + 1;
while (cur <= right) {
if (a[cur] < a[keyi] && ++prev != cur) {
Swap(&a[prev], &a[cur]);
}
cur++;
}
Swap(&a[keyi], &a[prev]);
return prev;
}
void QuickSort(int* a, int begin,int end){
assert(a);
//当区间不存在,或者只有一个时,则不需要再处理
if (begin >= end) {
return;
}
int keyi = PartSort3(a, begin, end);
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
优化2:小区间优化(极大减少递归次数)
分析:
当一组数据,不断找key值,划分左右区间,假设当区间划分数据只有10个数时,递归如下图所示
仍然需要递归10多次,此时便可对其进行优化。
结论:
当递归划分小区间,区间比较小的时候,就不再递归划分去排序这个区间。可以考虑直接用其他排序对小区间进行处理。
解决方案:
仍采用三数取中优化,同时递归分区为小区间采用插入排序
void InsertSort(int* a, int n) {
for (int i = 0; i < n - 1; i++) {
int end = i;
int temp = a[end + 1];
while (end >= 0) {
if (temp < a[end]) {
a[end + 1] = a[end];
end--;
}
else {
break;
}
}
a[end + 1] = temp;
}
}
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 right;
}
else {
return left;
}
}
else {//a[left] > a[mid] a[mid] < a[right]
if (a[mid] > a[right]) {
return mid;
}
else if (a[left] < a[right]) {
return left;
}
else {
return right;
}
}
}
//前后指针版
int PartSort3(int* a, int left, int right) {
int mid = GetMidIndex(a, left, right);
Swap(&a[left], &a[mid]);
int keyi = left;
int prev = left, cur = left + 1;
while (cur <= right) {
if (a[cur] < a[keyi] && ++prev != cur) {
Swap(&a[prev], &a[cur]);
}
cur++;
}
Swap(&a[keyi], &a[prev]);
return prev;
}
void QuickSort(int* a, int begin, int end) {
assert(a);
//当区间不存在,或者只有一个时,则不需要再处理
if (begin >= end) {
return;
}
if (end - begin > 10) {
int keyi = PartSort3(a, begin, end);
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
else {
InsertSort(a + begin, end - begin + 1);
//对小区间前 n 个数进行排序
}
}
1.7 快排非递归
需要掌握递归改非递归,原因如下:
递归大问题,极端场景下,如果递归深度太深,就会出现栈溢出
改动方法:
1、直接改为循环——如斐波那契数列、归并排序
2、用数据结构栈模拟递归过程
3、用数据结构队列模拟递归过程
3、也可以采用其他数据结构模拟递归过程,实现非递归快排
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 right;
}
else {
return left;
}
}
else {//a[left] > a[mid] a[mid] < a[right]
if (a[mid] > a[right]) {
return mid;
}
else if (a[left] < a[right]) {
return left;
}
else {
return right;
}
}
}
//前后指针版
int PartSort3(int* a, int left, int right) {
int mid = GetMidIndex(a, left, right);
Swap(&a[left], &a[mid]);
int keyi = left;
int prev = left, cur = left + 1;
while (cur <= right) {
if (a[cur] < a[keyi] && ++prev != cur) {
Swap(&a[prev], &a[cur]);
}
cur++;
}
Swap(&a[keyi], &a[prev]);
return prev;
}
void QuickSortNonR(int* a, int begin, int end) {
assert(a);
ST st;
StackInit(&st);
StackPush(&st, end);
StackPush(&st, begin);
while (!StackEmpty(&st)) {
int left = StackPop(&st);
int right = StackPop(&st);
if (right - left < 10) {
InsertSort(a + left, right - left + 1);
continue;
}
int keyi = PartSort3(a, left, right);
if (keyi + 1 < right) {
StackPush(&st, right);
StackPush(&st, keyi + 1);
}
if (left < keyi - 1) {
StackPush(&st, keyi - 1);
StackPush(&st, left);
}
}
StackDestory(&st);
}
1.8 快速排序的特性总结
1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(logN)
4. 稳定性:不稳定