常见排序:
- 排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
- 稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i] = r[j] ,且 r[i] 在 r[j] 之前,而在排序后的序列中,r[i] 仍在 r[j] 之前,则称这种排序算法是稳定的;否则称为不稳定的。
- 内部排序:数据元素全部放在内存中的排序。
- 外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
目录
交换排序类
交换排序类的基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
1、冒泡排序
冒泡排序的基本思想:比较相邻的元素。如若后者大(小),就交换二者。对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。即最后一个元素是排序区间的最大值。(依次往复直至完成)
//数据交换
void Swap(int* e1, int* e2)
{
int tmp = *e1;
*e1 = *e2;
*e2 = tmp;
}
//冒泡排序
void BubbleSort(int* a, int n)
{
assert(a);
//用于判断是否交换
int compare = 0;
//j循环: 序列到数第j个位置,是所需要用元素填补的位置,由于第一个元素最后为单,所以j < n
//i循环:将其余未排序的数据(范围[0 , n - j])进行比较排序
int j = 0;
for (j = 1; j < n; j++)
{
int i = 0;
for (i = 0; i < n - j; i++)
{
//>是升序,<是降序
if (a[i] > a[i + 1])
{
compare = 1;
Swap(&a[i], &a[i + 1]);
}
}
if (compare == 0) //排序是进行在未排序区的,如果一次交换都没有进行,那就证明未排序区是有序的
{
return;
}
}
}
1.1 冒泡排序的特性总结:
- 冒泡排序是一种非常容易理解的排序
- 时间复杂度:O(N^2)(最好为O(N) ~ 最差为O(N^2))
- 空间复杂度:O(1)
- 稳定性:稳定
1.1.1 时间复杂度:
最好O(N)的情况:
(有序即为最好)只有待排序值的前值皆与其有序,全程不需要交换,那么就可以直接退出,所以只需要比较一轮,即只需要判断,即O(N)。
最差O(N^2)的情况:
(反序即为最差)每轮比较都需要移动,那这样就是一个等差数列:N-1 + … + 3 + 2 + 1,则是:n (n-1+1)/2 则~O(n^2) 。即需要每个元素的一次判断,并移动的次数为最大,即O(N^2)。
1.1.2 空间复杂度:
即:空间复杂度为O(1)
2.1.2 稳定性:
2、快速排序:
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
所运用的主要操作(为快速排序递归实现的主框架):
而对于,将区间按照基准值划分为左右两半部分的常见方式有:
- hoare版本
-
挖坑版本
-
前后指针版本
- 三个的执行方式不同,但是核心思想是不变的,版本的更改只是为了更加便于理解。
1. hoare版本:
//数据交换
void Swap(int* e1, int* e2)
{
int tmp = *e1;
*e1 = *e2;
*e2 = tmp;
}
//hoare版本(分治法)
//将right边的数据调整至皆小于或大于所调整的数据,left边同理反之。即有序
void QuickSort1(int* a, int begin, int end)
{
assert(a);
if (begin >= end)
return;
//keyi:所需要调整的数据下标
int right, left, keyi;
left = begin, right = end;
keyi = begin;
//根据大小将左右元素交换
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]);
keyi = left;
//分治管理其左与右
//[begin, keyi - 1] keyi [keyi + 1, end]
QuickSort1(a, begin, keyi - 1);
QuickSort1(a, keyi + 1, end);
}
hoare版本易错点:
(同样的后面的挖坑版,也具同样的问题 )
1.对于比较时:
假如,没有等于:
2. 选择将key选择在左边后,选择右边先走的意义:
原因:因为要确保相遇位置的值,比key小或者就是key的位置。
会出现两种情况:
1. L先走,L停下来,R去遇到L。
2. L先走,直接与R相遇
2. 挖坑版本:
//数据交换
void Swap(int* e1, int* e2)
{
int tmp = *e1;
*e1 = *e2;
*e2 = tmp;
}
//挖坑版本
//将right边的数据调整至皆小于或大于所调整的数据,left边同理反之。即有序
void QuickSort2(int* a, int begin, int end)
{
assert(a);
if (begin >= end)
return;
//keyi:坑的下标
//key: 所需要调整的数据
int right, left, keyi, key;
keyi = left = begin;
right = end;
key = a[keyi];
//根据大小将坑进行填补
while (left < right)
{
while (left < right && a[right] >= key) //>=是升序,<=是降序
--right;
Swap(&a[keyi], &a[right]);
keyi = right;
while (left < right && a[left] <= key) //<=是升序,>=是降序
++left;
Swap(&a[keyi], &a[left]);
keyi = left;
}
a[left] = key;
keyi = left;//此时keyi为已经调整好的数据
//[begin, keyi - 1] keyi [keyi + 1, end]
QuickSort2(a, begin, keyi - 1);
QuickSort2(a, keyi + 1, end);
}
挖坑版本易错点
(与前面所提的:hoare版易错点,大同小异)
3. 前后指针版本:
//数据交换
void Swap(int* e1, int* e2)
{
int tmp = *e1;
*e1 = *e2;
*e2 = tmp;
}
//为下标为keyi的key排序
int Pointer_QSort(int* a, int begin, int end)
{
assert(a);
//keyi:所需要调整的数据下标
int keyi = begin;
int prev, cur;
prev = begin, cur = begin + 1;
while (cur <= end)
{
// cur位置的之小于keyi位置值
//<是升序,>是降序
if (a[cur] < a[keyi] && ++prev != cur)
Swap(&a[prev], &a[cur]);
++cur;
}
Swap(&a[prev], &a[keyi]);
keyi = prev;
return keyi;
}
//前后指针版本
void QuickSort3(int* a, int begin, int end)
{
assert(a);
if (begin >= end)
return;
int keyi = Pointer_QSort(a, begin, end);
//[begin, keyi - 1] keyi [keyi + 1, end]
QuickSort3(a, begin, keyi - 1);
QuickSort3(a, keyi + 1, end);
}
2.1. 快速排序的特性总结:
- 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
- 时间复杂度:O(N*logN)(最好为O(N*logN) ~ 最差为O(N^2))
- 空间复杂度:O(logN)(最好为O(logN) ~ 最差为O(N))
- 稳定性:不稳定
2.1.1 时间复杂度:
最好O(N*logN)的情况:
快速排序是分治法实现的,那么最完美的时间就是,次次完美二分,直到完成排序。
即:O(N*logN)
最差O(N^2)的情况:
快速排序是分治法实现的,那最差的情况就是:
即:O(N^2)
2.1.2 空间复杂度:
主要是递归造成的栈空间的使用。
最好O(logN)的情况:
需要进行logn递归调用,其空间复杂度为 O(logn)。
最差O(N)的情况:
需要进行n‐1递归调用,其空间复杂度为O(n)。
2.1.3 稳定性:
2.2 快速排序的优化:
优化:时间复杂度的最坏情况与空间复杂度的最坏情况。
2.2.1 时间复杂度的优化:
//三数取中
int GetMidIndex(int* a, int begin, int end)
{
int midi = (begin + end) / 2;
if (a[begin] < a[midi])
{
if (a[end] < a[begin])
return begin;
else if (a[end] < a[midi])
return end;
else
return midi;
}
else if (a[midi] < a[begin])
{
if (a[end] < a[midi])
return midi;
else if (a[end] < a[begin])
return end;
else
return begin;
}
}
正如此,当key是排序区间的最大值或最小值的时候时间复杂度为O(n^2),那么,就利用三数取中防止此情况的发生。
2.2.2 空间复杂度的优化:
函数的递归会每增加一层,为2倍递增。所以理论上只要我们减少最后一层,就可以直接减少一半的空间的运用。所以,选择在一定的时候就不使用递归,而使用直接插入排序就是最好的选择。