目录
- 冒泡排序
1.1 基本思想
1.2 特性 - 快速排序
2.1 基本思想
2.1.1 hoare版
2.1.2 挖坑版
2.1.3 前后下标版
2.1.4 循环
2.2 特性
2.3 优化
2.3.1 key值优化
1. 冒泡排序
1.1 基本思想
数据中相邻的两数不断比较,找出最大的数进行交换,不断往后相比,找到整个数列中的最值,第二轮找到次值
void BubblingSort(int ary[], int len)
{
int flag = 1;
for (int i = 0; i < len - 1; i++)
{
for (int j = 0; j < len - i - 1; j++)
{
if (ary[j] > ary[j + 1])
{
flag = 0;
int temp = ary[j];
ary[j] = ary[j + 1];
ary[j + 1] = temp;
}
}
//未发生交换,原数据有序
if (flag == 1)
{
break;
}
}
}
1.2 特性
1.冒泡排序是一种很容易理解的排序
2.时间复杂度:O(N2)
3.空间复杂度: O(1)
4.稳定性: 稳定
2. 快速排序
2.1 基本思想
快速排序是hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想是:任取某待排序中某元素为基准值,按照排序码将待排序集合分割成左右子序列,左子序列所有元素均小于基准值,右子序列均大于基准值,然后在左右子序列中重复此过程
快速排序分为子序列分割的方法和调用,,以key值分割左右区间,下面先写出递归左右序列的框架
void Qsort(int ary[], int left, int right)
{
//区间错误,返回
if (left >= right)
{
return;
}
int keyi = QSplit(ary, left, right);
//递归左右区间,keyi处除外
Qsort(ary, left, keyi - 1);
Qsort(ary, keyi + 1, right);
}
2.1.1 hoare版
上面的过程,首先6是key值,R小人往左走,找到比6小的数字停下来。然后L小人往右走,找到比6大的,然后交换两个数。继续走,直到两个小人相遇,就交换key值和相遇位置
根据上述过程写出简单的单趟过程
void QSplit(int ary[], int left, int right)
{
int keyi = left;
while (left < right)
{
//右边找大
while (ary[right] > ary[keyi])
{
right--;
}
//左边找小
while (ary[left] < ary[keyi])
{
left++;
}
Swap(&ary[left], ary[right]);
}
Swap(&ary[left], &ary[keyi]);
}
上面的代码有三个问题:
死循环
在找大和找小的过程中如果相等也会退出,交换后还是两个相等值,就会不断进入循环
越界
如果寻找的key值是一个最值,寻找过程没有找到比它小的,就会一直找下去,会造成数组越界
int QSplit(int ary[], int left, int right)
{
int keyi = left;
while (left < right)
{
//右边找大
while (left < right && ary[right] >= ary[keyi])
{
right--;
}
//左边找小
while (left < right && ary[left] <= ary[keyi])
{
left++;
}
Swap(&ary[left], ary[right]);
}
Swap(&ary[left], &ary[keyi]);
return left;
}
针对上面两个问题修改,同时返回最后的keyi值
为什么left从0处开始找,如果从1处找,遇到下面的极端场景,会出现错误
这样,到最后交换的时候key值就会换到1去,顺序就会变反。从0下标开始找,0和自己交换不会产生错误
保证相遇位置正确
如果left和right相遇的位置比keyi处值大,交换后也会出错误
1.左边做key,右边先走,保障了相遇位置比key小
2.右边做k,左边先走,保证了相遇位置比key大
以1的方法举例,R先走,就是R遇到L,有两种情况,第一种,L有比key大的值,这时R遇到L肯定是比key大的值交换。第二种,L没移动,R一直往左走遇到了L,相遇位置就是key的位置。这时,就是自己和自己交换
2.1.2 挖坑版
key为初始设定的坑位的值,R先走,遇到比key小的数将该数填到坑的位置,设定当前位置为坑。然后L走,找到大的数填为新坑,这样下去直到两个相遇,将key值填过来
挖坑法和hoare的几轮的结果可能不一样
int QSplit2(int ary[], int left, int right)
{
//key值和坑位
int key = ary[left];
int hole = left;
while (left < right)
{
//右边找小
while (left < right && ary[right] >= key)
{
right--;
}
ary[hole] = ary[right];
hole = right;
//左边找大
while (left < right && ary[left] <= key)
{
left++;
}
ary[hole] = ary[left];
hole = left;
}
ary[hole] = key;
return hole;
}
2.1.3 前后下标版
cur不断向后走,当cur处的值小于key处值时,pre先++,pre和cur的值交换,如果是自身就不用交换。直到cur超出数组,将pre处的值和key值交换。这种方法当cur和pre拉开差距后,中间间隔的值都是比key值大的,可以快速将这些值滚到最后
//前后下标
int QSplit3(int ary[], int left, int right)
{
int pre = left;
int cur = left + 1;
int keyi = left;
while (cur <= right)
{
//cur值小于keyi时,且不等于自己
if (ary[cur] < ary[keyi] && ++pre != cur)
{
Swap(&ary[pre], &ary[cur]);
}
cur++;
}
Swap(&ary[pre], &ary[keyi]);
keyi = pre;
return keyi;
}
2.1.4 循环
替换递归过程,可以用栈,利用栈的先进后出,将左右区间先压进去。每次走一遍单趟可以吧新的区间按顺序压进去,处理不断处理栈顶的区间,区间不存在就返回。就可以模拟递归过程
下面是左边区间的栈过程
void CirculQsort(int ary[], int left, int right)
{
Stk st;
Init(&st);
//压入左右下标
Push(&st, left);
Push(&st, right);
while (!Empty(&st))
{
//取的时候相反
int right = Top(&st);
Pop(&st);
int left = Top(&st);
Pop(&st);
int keyi = QSplit(ary, left, right);
//区间存在压入
if (keyi + 1 < right)
{
Push(&st, keyi + 1);
Push(&st, right);
}
if (left < keyi - 1)
{
Push(&st, left);
Push(&st, keyi - 1);
}
}
Destory(&st);
}
2.2 特性
1.综合性能和使用场景都是比较好的,所以才叫快速排序
2.时间复杂度:最好,O(N*logN),取决于key的取值,如果key取值刚好是中位数,每次可以将数据正好分为两半。最差O(N2),有序的数列选出的key值
3.空间复杂度: O(logN)
4.稳定性: 不稳定
2.3 优化
2.3.1 key值优化
对于有序的数据快速排序的效率是很低的,因为key值每次取的是最值,划分出的区间作用很小,每次只能排一个数。所以对key值的取法优化可以解决有序数据的排序
三数取中
mid取left和right的中间位置,取三个下标的中间值
//三数取中
int GetIndex(int ary[], int left, int right)
{
int mid = (left + right) / 2;
if (ary[mid] > ary[left])
{
if (ary[mid] < ary[right])
{
return mid;
}
else if (ary[left] > ary[right])
{
return left;
}
else
{
return right;
}
}
//ary[mid] < ary[left]
else
{
if (ary[mid] > ary[right])
{
return mid;
}
else if (ary[left] > ary[right])
{
return right;
}
else
{
return left;
}
}
}
int QSplit(int ary[], int left, int right)
{
//三数取中
int mid = GetIndex(ary, left, right);
Swap(&ary[left], &ary[mid]);
int keyi = left;
while (left < right)
{
//右边找小
while (left < right && ary[right] >= ary[keyi])
{
right--;
}
//左边找大
while (left < right && ary[left] <= ary[keyi])
{
left++;
}
Swap(&ary[left], &ary[right]);
}
Swap(&ary[left], &ary[keyi]);
return left;
}
某些OJ题可能对mid值有针对,mid值也可以用随机数取,然后在三数取中
2.3.2 三路划分
针对多个相等数据,这种数据快速排序效率也很低,key值区间每次只能少1。所以三路划分,对于key的优化是key值和相等的在中间,小的在左边,大的在右边
基本思想
1.a[c] < key ,交换c和l位置的值,++l,++c
2.a[c] > key ,交换c和r位置的值,–r
3.a[c] == key ,++c
本质是将比key小的甩到左边,大的放到右边,相等的值推到中间,中间的这部分值没必要再递归去排序了
//快速排序单一数优化
void QsortEnd(int ary[], int left, int right)
{
//区间错误,返回
if (left >= right)
{
return;
}
//区间优化
int l = left;
int cur = left + 1;
int r = right;
//三数取中key优化
int mid = GetIndex(ary, left, right);
Swap(&ary[left], &ary[mid]);
int key = ary[left];
//小于等于循环
while (cur <= r)
{
//cur小于key
if (ary[cur] < key)
{
Swap(&ary[cur], &ary[l]);
l++;
cur++;
}
else if (ary[cur] > key)
{
Swap(&ary[cur], &ary[r]);
r--;
}
else
{
cur++;
}
}
//三段
// [left,l-1],[l,r],[r+1,right]
//递归左右区间,相等区间除外
QsortEnd(ary, left, l - 1);
QsortEnd(ary, r + 1, right);
}