交换排序
1. 基本思想
所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
2. 冒泡排序
可以说冒泡排序是最经典的一个交换排序了,通过对待排序序列从前向后(从下标较小的元素开始),依次比较相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部,就象水底下的气泡一样逐渐向上冒。
2.1 实现
void BubbleSort(int* a, int n)
{
for (int i = 0; i < n - 1; i++)
{
int flag = 0;
for (int j = 0; j < n - 1 - i; j++)
{
flag = 1;
if (a[j] > a[j + 1])
Swap(&a[j], &a[j + 1]);
}
if (flag == 0)
break;
}
}
2.2 时间和空间复杂度分析
时间复杂度:O(N^2)
空间复杂度:O(1)
2.3 特性
- 冒泡排序是一种非常好理解的排序
- 我们控制相等的时候不交换,则该排序是稳定的
3.快速排序
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
3.1 递归实现
// 快速排序hoare版本(升序)
int PartSort1(int* a, int left, int right)
{
int keyi = left;
int key = a[keyi];
while (left < right)
{
/*如果key在左边,一定要先从右开始找,因为当要接近循环结束时,left和right相遇的位置可能会大于key,如果此时交换则会和预期不同*/
//从右找小
while (left < right && a[right] >= key)
right--;
//从左找大
while (left < right && a[left] <= key)
left++;
Swap(&a[left], &a[right]);
}
Swap(&a[left], &a[keyi]);
keyi = left;
return keyi;
}
// 快速排序挖坑法
int PartSort2(int* a, int left, int right)
{
int key = a[left];
int pivot = left;
while (left < right)
{
//右找小
while (left < right && a[right] >= key)
right--;
//填坑变成新坑
a[pivot] = a[right];
pivot = right;
//从左找大
while (left < right && a[left] <= key)
left++;
a[pivot] = a[left];
pivot = left;
}
a[left] = key;
return left;
}
// 快速排序前后指针法
int PartSort3(int* a, int left, int right)
{
int key = a[left];
int pre = left;
int cur = pre + 1;
//cur找小和pre下一个交换
while (cur <= right)
{
if (a[cur] < key && a[++pre] != a[cur])
{
Swap(&a[pre], &a[cur]);
}
cur++;
}
Swap(&a[pre], &a[left]);
return pre;
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
int keyi = PartSort1(a, left, right);
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
3.2 三数取中优化
快速排序虽然效率很高,但是遇到已经排好的序列的时间效率会很低为O(N^2),所以因此,我们想到了三数取中的思想。即序列的三个位置最左边left,最右边right,中间mid,三个数取出中间大小的数,用这个数做key.
代码展示
//三数取中(快排优化)
int GetMiddleIndex(int* a, int left, int right)
{
int middle = (left + right) >> 1;
if (a[left] < a[middle])
{
if (a[middle] < a[right])
{
return middle;
}
else
{
if (a[left] > a[right])
{
return right;
}
else
{
return left;
}
}
}
else
{
if (a[middle] > a[right])
{
return middle;
}
else
{
if (a[left] > a[right])
{
return right;
}
else
{
return left;
}
}
}
}
3.3 小区间优化
小区间优化的思想就是因为在快排递归到后面时大多数数据可能已经接近有序,但是我们仍然还需要对其进行区间划分然后排序,所以此时为了减少这些递归的次数,我们可以采用插入排序进行替换。
随着递归层数的增加,要建立的栈是呈指数级增长的,但是快速排序的最后几层,左右区间很短,但又占着大部分的建立的栈,所以我们判断一下,如果区间很小就没有必要快速排序了,用一些常规的排序例如插入、冒泡之类的就行,数据量小切接近有序的情况下我们大多数情况下选择插入排序。
代码展示
void QuickSort(int* a, int left, int right)
{
if (right <= left)
{
return;
}
int pivot = PartSort1(a, left, right);
//小区间优化,区间距离根据要排的数据多少自己把控,这里以10为例
if (pivot - 1 - left > 10)
{
//递归左
QuickSort(a, left, pivot - 1);
}
else
{
//插入排序
InsertSort(a, pivot - left);
}
if (right - pivot - 1 > 10)
{
//递归右
QuickSort(a, pivot + 1, right);
}
else
{
//插入排序
InsertSort(a, right - pivot);
}
}
3.4 非递归实现
在递归实现快速排序时,每次递归调用都会产生额外的函数调用开销,可能导致栈溢出的风险。
因此,掌握非递归实现快速排序可以避免递归调用的开销,提高算法的效率和性能。非递归实现快速排序通常使用栈或队列来模拟递归过程,将问题划分为多个子问题,并通过循环迭代来处理。
void QuickSortNonR(int* a, int begin, int end)
{
ST st;
STInit(&st);
STPush(&st, end);
STPush(&st, begin);
while (!STEmpty(&st))
{
int left = STTop(&st);
STPop(&st);
int right = STTop(&st);
STPop(&st);
int keyi = PartSort1(a, left, right);
// [lefy,keyi-1] keyi [keyi+1, right]
if (keyi+1 < right)
{
STPush(&st, right);
STPush(&st, keyi+1);
}
if (left < keyi-1)
{
STPush(&st, keyi-1);
STPush(&st, left);
}
}
STDestroy(&st);
}
这个原理就是通过栈这个数据结构模拟了这个递归的过程。
3.5 时间和空间复杂度分析
时间复杂度:
故时间复杂度为O(N * logN)
当没使用三数取中遇到最坏的情况时
故时间复杂度为O(N^2)、
空间复杂度:
当使用的是非递归方法时,空间复杂度为O(1)
当使用的是递归方法时,由于每一层都需要开辟栈空间,故空间复杂度为O(logN)
3.6 特性
- 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
- 快速排序不稳定
- 递归是快速排序的核心思想之一。