一、冒泡排序
两重循环,内层循环每次比较相邻两个数的大小关系并做交换处理,每次内循环结束一定可以将每轮最大(小)的元素归位;外层循环控制总共需要归位元素的次数。
改良算法,加入flag标志位,如果元素提前排序好即可直接结束排序算法
//冒泡排序算法
void Bubble_Sort(ElementType A[],int N) {
int flag;//用于表示一趟排序是否已经完成
for (int i = 0; i < N-1; i++) {
flag = 0;
for (int j = N - 1; j > i; j--) {
if (A[j] < A[j - 1]) {
Swap(A[j], A[j - 1]);
flag = 1;
}
}
if (flag == 0) break;
}
}
时间复杂度:最糟情况:O( n 2 n^2 n2) 最好情况:O( n n n)
二、(普通)插入排序
将初始序列分为有序区和无序区,每次从无序区队列中选择一个,然后在当前有序区内比较,并将其插入在有序区中的合适位置,每一轮比较插入操作都需要对已排好序列进行调整。
//插入排序算法
void Insertion_Sort(ElementType A[],int N) {
ElementType Temp;//保存待插入的值
int i, j;//i外层变量遍历A中每一个值,j内层变量控制插入的位置
for (i = 1; i < N; i++) {
Temp = A[i];
for (j = i; j > 0 && A[j - 1] > Temp; j--) {
A[j] = A[j-1];
}
A[j] = Temp;
}
}
时间复杂度:最糟情况:O( n 2 n^2 n2) 最好情况:O( n n n)
三、折半插入排序
对比简单插入排序算法可知,该方法在每次插入时需要提前进行查询,这是影响算法执行的效率的关键。所以在插入算法中结合了折半查找算法,可以有效改进算法的执行效率。
//折半插入排序算法
void Bin_Insertion_Sort(ElementType A[], int N) {
int low, high, mid;
ElementType Temp;
int i, j;
for (i = 1; i < N; i++) {
if (A[i] < A[i - 1]) {
Temp = A[i];
low = 0;
high = i - 1;
while (low <= high) {
mid = (low + high) / 2;
if (Temp < A[mid]) {
high = mid - 1;
}
else
{
low = mid + 1;
}
}
//找到应该在high+1的位置插入
for (j = i - 1; j >= high + 1; j--) {
A[j + 1] = A[j];
}
A[high + 1] = Temp;
}
}
}
时间复杂度
O
(
n
2
)
O(n^2)
O(n2)
事实上,折半查找并没有改善时间复杂度,但他减少了在插入时元素之间的比较次数,因而提高了整个算法的效率。
四、希尔排序
将待排序的序列分为若干个子序列区间,然后在每个子序列中进行简单插入排序。具体思路为:
- ①取D=N/2
- ②将整个序列分为D组,对每个组内对应位置的元素进行插入排序
- ③递归D=D/2,重复步骤②,直到D=1执行完后停止
//希尔排序算法
void Shell_Sort(ElementType A[], int N) {
int D, tmp;
int i, p;
for (D = N / 2; D > 0; D = D / 2) {
//采用插人排序算法
for (p = D; p < N; p++) {
tmp = A[p];
for (i = p; i >= D && A[i - D] > tmp; i -= D) {
A[i] = A[i - D];
}
A[i] = tmp;
}
}
}
时间复杂度 O ( n 1.3 ) O(n^{1.3}) O(n1.3) 通过复杂的证明得出,这里只记住结论。此外, D k D_k Dk间隔有序的结果,在经过 D k − 1 D_{k-1} Dk−1次排序后仍然是 D k D_k Dk间隔有序的。
五、(简单)选择排序
将全局序列分为有序区和无序区,每次从无序区中选择一个最小的元素放入有序区中。
//寻找从下标为i到下标为n-1的元素中最小的一个
ElementType FindMin(ElementType A[], int N, int i) {
int j, k = i;
for (j = i + 1; j < N; j++) {
if (A[k] > A[j]) {
k = j;
}
}
return k;
}
//选择排序算法
void Select_Sort(ElementType A[], int N) {
int i, min, tmp;
for (i = 0; i < N-1; i++) {
min = FindMin(A, N, i);
if (min != i) {
tmp = A[i];
A[i] = A[min];
A[min] = tmp;
}
}
}
时间复杂度O( n 2 n^2 n2) 无论是最好还是最坏的情况,意味着与初始序列顺序无关
其实,选择排序和插入排序的思想十分类似。插入排序是先随便找一个然后再找到合适的位置;而选择排序是先找到下一个最小(大)的元素,再直接放到应对位置上。
六、堆排序
在前面的简单选择排序中我们发现了每次都需要找到无序区中的最小(大)值,而为了实现这一点,我们可以直接利用最大、最小堆的特性来实现。
整个算法大致分为两部分,辅助算法是将一个n元素的序列中从low到high的结点调整成为一个最大堆。主算法先将整个序列调整为一个最大堆,然后每次直接从根结点取最大值放在序列末尾,再将除末尾外的其他元素再次调整为一个最大堆。
//调整堆的结构
void ShiftHeap(ElementType A[], int low, int high) {
int i = low;
int j = 2 * i + 1;//j表示i的左孩子
ElementType tmp = A[i];
while (j <= high) {
if (j < high && A[j] < A[j + 1])j++;//使j指向左右孩子中的大的
if (tmp < A[j]) {
A[i] = A[j];
i = j;
j = i * 2 + 1;
}
else break;
}
A[i] = tmp;
}
//堆排序算法
void Heap_Sort(ElementType A[], int N) {
int i;
for (i = N / 2; i >= 0; i--) {
ShiftHeap(A, i, N);
}
for (i = N - 1; i > 0; i--) {
Swap(A[i], A[0]);
ShiftHeap(A, 0, i - 1);
}
}
平均时间复杂度O( n l o g n nlogn nlogn)
七、归并排序
归并排序算法顾名思义就是不断合并的意思。这里我们具体使用到的是二路归并,即将两个有序的子序列通过算法处理后得到一个有序的序列,这样的过程称为一次二路归并。
这里我们使用递归的排序算法,每次都先将序列的左半部分排序好,再将右半部分排序好,最后调用一次二路归并算法进行合并。
//归并排序--统一函数接口
void Merge_Sort(ElementType A[], int N) {
ElementType *TmpA;
TmpA = new ElementType[N];
if (TmpA != NULL) {
MSort(A, TmpA, 0, N - 1);
delete TmpA;
TmpA = NULL;
}
else {
printf("空间不足");
}
}
//递归调用二路归并算法
void MSort(ElementType A[], ElementType TmpA[], int L, int RE) {
int Center;
if (L < RE) {
Center = (L + RE) / 2;
MSort(A, TmpA, L, Center);
MSort(A, TmpA, Center + 1,RE);
Merge(A, TmpA, L, Center + 1, RE);
}
}
/*
将两个有序数组合并为一个有序数组,假设这两个有序数组挨着放在A中
L:第一个数组的起点
R:第二个数组的起点
RE:第二个数组的终点
A:待排的两个连在一起的数组
tmpA:归并到的新辅助空间
*/
void Merge(ElementType A[], ElementType tmpA[], int L, int R, int RE) {
int LE = R - 1; //得到左边数组的终点位置,假设左右两列挨着
int Tmp = L;//存放结果数组的初始位置
int NumElements = RE - L + 1;
while (L <= LE && R <= RE) {
if (A[L] < A[R]) {
tmpA[Tmp++] = A[L++];
}
else {
tmpA[Tmp++] = A[R++];
}
}
while (L <= LE)
tmpA[Tmp++] = A[L++];
while (R <= RE)
tmpA[Tmp++] = A[R++];
for (int i = 0; i < NumElements; i++, RE--)
A[RE] = tmpA[RE];
}
时间复杂度
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)
空间复杂度
O
(
n
)
O(n)
O(n)
八、快速排序
快速排序号称是所有算法中在生活使用中最快的排序算法,但事实,任何一个排序算法都没有绝对的最好最快,任何算法的优劣都需要依据待排序序列本身的特征而言。
快速排序的思路是选取一个主元,然后将剩下的序列根据这个主元进行划分,将所有小于主元的值放在主元的左侧,将大于主元的值放在主元的右侧,然后再在左右子区间中分别再做相同的操作。
//武汉大学版-直接选取数组中第一个作为参考元,直接用快速排序做完全部内容
void QuickSort(ElementType A[], int L, int R) {
int i = L, j = R;
ElementType tmp;
if (L < R) {
tmp = A[L];
while (i != j) {
while (j > i && A[j] >= tmp) j--;
A[i] = A[j];
while (i < j && A[i] <= tmp) i++;
A[j] = A[i];
}
A[i] = tmp;
QuickSort(A, L, i - 1);
QuickSort(A, i + 1, R);
}
}
//统一接口函数
void Quick_Sort(ElementType A[], int N) {
QuickSort(A, 0, N - 1);
}
//选择主元
ElementType GetPivot(ElementType A[], int Left, int Right) {
int Center = (Left + Right) / 2;
if (A[Left] > A[Center])
Swap(A[Left], A[Center]);
if (A[Left] > A[Right])
Swap(A[Left], A[Right]);
if (A[Center] > A[Right])
Swap(A[Center], A[Right]);
//将Center位置处的元素放置于Right的前一位,方便后续程序处理
Swap(A[Center], A[Right - 1]);
return A[Right - 1];
}
//浙江大学版-选取中位数作为主元;当排序片段较小时,采用插入排序法替代
void QuickSort(ElementType A[], int L, int R) {
if (R - L > 2) {
int i = L, j = R - 1;
int Pivot = GetPivot(A, L, R);
while (1) {
while (A[++i] < Pivot) {}
while (A[--j] > Pivot) {}
if (i < j)
Swap(A[i], A[j]);
else
break;
}
Swap(A[i], A[R - 1]);
QuickSort(A, L, i - 1);
QuickSort(A, i + 1, R);
}
else {
Insertion_Sort(A + L, R - L + 1);
}
}
//统一接口函数
void Quick_Sort(ElementType A[], int N) {
QuickSort(A, 0, N - 1);
}
快速排序算法的效率一部分取决于主元质量的选择,因而不同的快速排序在操作上存在差异(例如武汉大学版为了简洁代码直接采用了序列的第一个元素,而浙江大学版的则特别构造了函数选取序列头、尾、中三个数的中位数),但整体的思路都是一样的。
时间复杂度 最好情况:O(
n
l
o
g
n
nlogn
nlogn) 最坏情况:O(
n
2
n^2
n2) 平均情况O(
n
l
o
g
n
nlogn
nlogn)
空间复杂度 最好情况:O(
l
o
g
n
logn
logn) 最坏情况:O(
n
n
n) 平均情况O(
l
o
g
n
logn
logn)
正是因为快速排序算法的平均时间空间复杂度都接近于最好情况,所以快速排序才被认作生活中最好用的排序算法。
九、基数排序
所谓基数其实就是进制,二进制数的基数就是2,十进制数的基数就是10。基数排序就是通过对序列中数进行逐位的处理,最后得到需要的有序序列。
基数排序主要分为LSD(最低位优先)和MSD(最高位优先)两种思路。LSD就是说在排位时优先从低位开始比较,最后再比较高位数,MSD则刚好与之相反。选择哪种排序方式主要考虑到我们希望得到的结果是递增序列还是递减序列,如果想要得到递增序列,则采用LSD,即将越重要的位放在最后处理,反之亦然。
基数排序思路(以10进制数采用LSD作为例子):
先构造10个队列,分别标号0-9。再将序列中每个数按初始顺序构成链表,然后从低位开始比较,将对应数字按顺序放在0-9不同的队列中,(例如21放在1号队列,18放在8号队列),当所有数字做完一轮处理之后就按照队列顺序(从0-9)再将其重新串联成一个新链表。再将新链表重新进行下一次排序,这次从十位比较,做和个位比较相同的操作,如此反复,直到做完最高位比较,最后得到的链表就是一个递增排好序的链表。
小结
排序算法没有所谓绝对的最好或者最坏,任何算法的使用都需要针对实际的情况使用。
这里对上述所提及的所有排序算法进行简单总结
排序方法 | 时间复杂度 | 空间复杂度 | 稳定性 | ||
---|---|---|---|---|---|
平均情况 | 最坏情况 | 最好情况 | |||
冒泡排序 | 稳定 | ||||
直接插入排序 | 稳定 | ||||
折半插入排序 | 稳定 | ||||
希尔排序 | 不稳定 | ||||
简单选择排序 | 不稳定 | ||||
堆排序 | 不稳定 | ||||
归并排序 | 稳定 | ||||
快速排序 | 不稳定 | ||||
基数排序 | 稳定 |
PS:有没有朋友知道如何在CSDN的HTML标签中插入数学公式啊,上面这些数学公式都是我按图片格式插入的,太麻烦了!
补充:
关于算法的稳定与否的理解:一般来说,当在算法中需要将元素进行较大间隔的移动或者交换时候,该算法是不稳定的,因为这样很可能会把原来排在前面的元素移动到具有相同关键字的另一个元素后面,例如:
希尔排序:不同组之间需要以较大间隔移动
选择排序:选出的最小元素可能需要以较大间隔与无序区第一个元素交换
堆排序:每次需要重新构建大根堆时需要对元素进行较大间隔的交换
快速排序:i和j之间需要进行较大间隔的交换