目录
- 快排整体思路
- 快排实现
- hoare版本
- 挖坑法
- 前后指针法
- 快排优化(三数取中,小区间优化)
- 快排非递归
- 归并排序
- 计数排序
一、快排整体思路
在待排序元素中任取一元素作为基准值,然后将小于该元素的其他元素放到其左边,大于其元素的放到其右边,那么该元素就放在了他应该处在的位置,然后不断递归,对其左右子序列做重复的操作,使每个元素都处在应该放在的位置。
快排的特性:
1、快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快排;(极端情况:比如全是相同的数如2,排一大堆相同的数,或则重复率特别高的数效率会大大折扣,但这样的场景很少。)
2、时间复杂度:O(N * logN);
3、空间复杂度:O(logN);
4、稳定性:不稳定;
二、快排单趟
1、hoare版本
①选最左边的数做key,让右边先走,让右边找比key小的数,找到后停下;(选左边做key必须让右边先走)
②然后让左边再走,找到比key大的数停下来;
③交换左右找到的数;
④不断重复,直到左右相遇则停下;
⑤最后交换key与上述相遇的值
图解如下:
// 快速排序hoare版本
int PartSort1(int* a, int left, int right)
{
int key = left;
while (left < right)
{
//Ⅰ 这里必须加=,避免特殊情况,如全是5
//Ⅱ 也必须带上left < right避免越界,当出现5,6,7,8,9
//右边先走,找比key小的数,一直找不到,如果不限定就会跑出去
while (left < right && a[right] >= a[key]) //先找右,找比key小的
right--;
while (left < right && a[left] <= a[key]) //再找左,找比key大的
left++;
Swap(&a[right], &a[left]);
}
Swap(&a[key], &a[left]);
return left;
}
快排的实现
单趟排完后,比key小的数就放到了左边,比key大的数就放在了右边,再不断递归,将其分成左右子区间,再在其子区间内再分为两段,直到为一个元素或则区间不存在就停下来。
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
int keyi = PartSort1(a, left, right);
//[left, keyi - 1] keyi [keyi + 1]
QuickSort(a, left, keyi - 1); //左子区间
QuickSort(a, keyi + 1, right); //右子区间
}
2、挖坑法
①先将左边第一个数据存放到临时变量key中,然后此位置形成一个坑位;
②然后让右边开始走,找到比key小的数停下来,将该值放到上述坑位当中,此位置形成新的坑位;
③然后左边再开始走,找到比key大的数停下来,将该值放到上述坑位当中,此位置形成新的坑位;
④不断重复,直到左右相遇
⑤最后将最开始保存的key值填入到坑位当中
代码实现如下:
// 快速排序挖坑法
int PartSort2(int* a, int left, int right)
{
int key = a[left];
int hole = left;
while (left < right)
{
//右边找小,放到左边的坑里面
while (left < right && a[right] >= key)
right--;
a[hole] = a[right];
hole = right;
//左边找大,放到右边的坑里面
while (left < right && a[left] <= key)
left++;
a[hole] = a[left];
hole = left;
}
a[hole] = key;
return hole;
}
3、左右指针法
①选左边做key,prev初始位置为最左边的元素,cur初始位置为prev的下一个元素;
②cur找到比key小的就停下来,++prev;
③比较此时的cur与prev,如果相等就不做动作;
④如果不相等,那么就交换此时prev与cur里面的值;
⑤当cur走出该序列时,交换此时key与prev里面的值。
总体思路:cur找小,把小的往左边翻,prev则把大的往右边翻
代码实现如下:
// 快速排序前后指针法
int PartSort3(int* a, int left, int right)
{
int key = left;
int prev = left;
int cur = prev + 1;
while (cur <= right)
{
if (a[cur] < a[key] && ++prev != cur)
{
Swap(&a[cur], &a[prev]);
}
++cur;
}
Swap(&a[prev], &a[key]);
return prev;
}
4、三数取中
如果出现对有序或则接近有序进行排序(2,3,4,5,6),那么如果选最左边做key,那么其排序效率则会大大降低,时间复杂度为N^2,那么这时候我们可以选最左边、最右边、中间的数取其最中间的值,然后交换到最左边则会大大提升效率。
int GetMidIndex(int* a, int left, int right)
{
//避免left和right是两个极大的数,如果使用(left+right)/2可能会撑破内存
int mid = left + (right - left) / 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[left] < a[right])
{
return left;
}
else if (a[mid] > a[right])
{
return mid;
}
else
{
return right;
}
}
}
int PartSort2(int* a, int left, int right)
{
int min = GetMidIndex(a, left, right);
Swap(&a[min], &a[left]);
......
}
5、小区间优化
当快排递归时,分割到小区间时,就可以不再用递归分割思路让这段子区间有序,当子区间小到一定程度时,再继续使用递归调用,那么会继续浪费空间,所以我们可以在递归到一定程度时使用插入排序让其有序。
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
//小区间优化,对于递归快排,减少其递归次数
if (right - left + 1 < 10)
{
InsertSort(a, left, right);
}
else
{
int keyi = PartSort3(a, left, right);
//[left, keyi - 1] keyi [keyi + 1]
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
}
6、快排非递归版本
实现非递归,一种方法就是用循环,另外一种就是用数据结构栈进行模拟,递归版本是不断调用栈帧去存储数组下标,那么我们可以用栈来存储每次调用的数组下标,栈也满足后进先出。
// 快速排序 非递归实现
void QuickSortNonR(int* a, int left, int right)
{
Stack st;
StackInit(&st);
StackPush(&st, left);
StackPush(&st, right);
while (!StackEmpty(&st))
{
int end = StackTop(&st);
StackPop(&st);
int begin = StackTop(&st);
StackPop(&st);
int key = PartSort2(a, begin, end);
//[begin, key - 1] key [key + 1, end]
if (key + 1 < end)
{
StackPush(&st, key + 1);
StackPush(&st, end);
}
if (begin < key - 1)
{
StackPush(&st, begin);
StackPush(&st, key - 1);
}
}
StackDestroy(&st);
}
三、归并排序
1、归并思路:是采用分而治之的思想,将已有序的子序列合并,得到完全有序的序列,即不断用二分法,先使每个子序列有序,再使子序列段间有序。再将两个有序表进行合并。
2、归并的特性:
Ⅰ、归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多在于解决磁盘外排序的问题;
Ⅱ、时间复杂度:O(N*logN);
Ⅲ、空间复杂度:O(N);
Ⅳ、稳定性:稳定;
图片分解与合并如下(图片来自网络):
//归并排序
void _MergeSort(int* a, int left, int right, int* tmp)
{
if (left >= right)
{
return ;
}
int mid = (left + right) / 2;
//让[left,mid] [mid+1, right]有序
_MergeSort(a, left, mid, tmp);
_MergeSort(a, mid + 1, right, tmp);
int begin1 = left, end1 = mid;
int begin2 = mid + 1, end2 = right;
int i = left;
//小的那个先入tmp数组
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
//剩下还有没有入完的
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
//再将tmp里面的数据拷贝回原数组,如果不拷贝回会被覆盖
for (int j = left; j <= right; j++)
{
a[j] = tmp[j];
}
}
// 归并排序递归实现
void MergeSort(int* a, int n)
{
//动态申请n个空间
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
printf("malloc fail\n");
exit(-1);
}
_MergeSort(a, 0, n - 1, tmp);
free(tmp);
tmp = NULL;
}
四、计数排序
计数排序适合范围比较集中的数组,如果range比较大,并且是浮点数就不适用了,空间复杂度为O(range);时间复杂度基本上为O(N),遍历一遍即可。
void CountSort(int* a, int n)
{
//求出最大值最小值求出range
//小细节,取数组中的数,不取数组外的数
int max = a[0], min = a[0];
for (int i = 1; i < n; i++)
{
if (a[i] > max)
{
max = a[i];
}
if (a[i] < min)
{
min = a[i];
}
}
int range = max - min + 1;
int* count = (int*)malloc(sizeof(int)*range);
if (count == NULL)
{
printf("malloc fail\n");
exit(-1);
}
//这里一定要全部初始化为0,不然都是一些随机值
memset(count, 0, sizeof(int)*range);
//统计次数
for (int i = 0; i < n; ++i)
{
count[a[i] - min]++;
}
//根据次数进行排序
int j = 0;
for (int i = 0; i < range; ++i)
{
while (count[i]--)
{
a[j++] = i + min;
}
}
free(count);
count = NULL;
}