目录
1 排序的基本概念和分类
1.1 排序定义
1.2 排序的稳定性
1.3 内排序与外排序
1.4 排序用到的结构与函数
2 直接插入排序(Straight Insertion Sort)
2.1 算法思想
2.2 算法实现
2.3 算法复杂度分析
3 希尔排序(Shell Sort)
3.1 算法思想
3.2 算法实现
3.3 算法复杂度分析
--------------------------------------我是分界线-------------------------------------------
4 冒泡排序(Bubble Sort)
4.1 算法思想
4.2 算法实现
4.3 算法复杂度分析
5 快速排序
--------------------------------------我是分界线-------------------------------------------
6 简单选择排序(Simple Selecion Sort)
6.1 算法思想
6.2 算法实现
6.3 算法复杂度分析
7 堆排序(Heap Sort)
7.1 堆
7.2 算法思想
7.3 算法实现
7.4 算法复杂度分析
--------------------------------------我是分界线-------------------------------------------
8 归并排序(Merging Sort)
8.1 算法思想
8.2 算法实现
8.3 算法复杂度分析
假设含有n个记录的序列为,其相应的关键字分别为
,需确定1,2,……,n的一种排列
,使其相应的关键字满足
非递减(或非递增)关系,即使得序列成为一个按关键字有序的序列
,这样的操作称为排序。
假设,且在排序前的序列中
领先于
,即
。如果排序后
仍然领先于
,则称所用的排序方法是稳定的;反之,若可能使得排序后的序列中
领先于
,则称所用的排序方法是不稳定的。
·外排序
由于排序的记录个数太多,不能同时放置在内存,整个排序过程需要在内外存之间多次交换数据才能进行。
·内排序
在排序整个过程中,待排序的所有记录全部被放在内存中。
内排序算法性能主要受3个方面影响:
(1)时间性能
在内排序中,主要进行两种操作:关键字比较和记录位置的移动。高效率的内排序算法应该是具有尽可能少的关键字比较次数和尽可能少的记录移动次数。
(2)辅助空间
辅助空间是除了存放待排序所占用的存储空间之外,执行算法所需要的其他存储空间。
(3)算法复杂性
这里指算法本身的复杂度,而不是指算法的时间复杂度。
根据排序过程中借助的主要操作,七种内排序算法分类:
按照算法的复杂程度分为两大类
·简单算法:冒泡排序、简单选择排序和直接插入排序
·改进算法:希尔排序、堆排序、归并排序、快速排序
为了之后讲清楚排序算法代码,先提供一个用于排序的顺序表结构。
#define MAXSIZE 10 //用于要排序数组个数最大值,可根据需要修改
typedef struct
{
int r[MAXSIZE + 1]; //用于存储要排序数组,r[0]用作哨兵或临时变量
int length; //用于记录顺序表的长度
}SqList;
另外,由于排序常用到操作是来数据两元素的交换,因此将它写成函数,之后会经常用到。
/*交换L中数组r的下标为i和j的值*/
void swap(SqList *L, int i, int j)
{
int temp = L->r[i];
L->r[i] = L->r[j];
L->r[j] = temp;
}
直接插入排序的基本操作是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增1的有序表。
/*对顺序表L作直接插入排序*/
void InsertSort(SqList *L)
{
int i, j;
for (i = 2; i <= L->length; i++)
{
if (L->r[i] < L->r[i - 1]) //需将L->r[i]插入有序子表
{
L->r[0] = L->r[i]; //设置哨兵
for (j = i - 1; L->r[j] > L->R[0]; j--)
{
L->r[j + 1] = L->r[j]; //记录后移
}
L->r[j + 1] = L->r[0]; //插入到正确位置
}
}
}
PS:排序中哨兵的作用
从空间上来看,只需要一个记录的辅助空间,因此,关键看它的时间复杂度。
最好情况:待排序表本身有序。比较次数n-1,无移动记录,时间复杂度O(n)。
最坏情况:待排序表逆序。比较次数2+3+······+n=(n+2)(n-1)/2,记录的移动次数1+2+······+(n-1)=n(n-1)/2。
如果排序记录随机,根据概率相同的原则,平均比较和移动次数约为n^2/4次,因此,直接插入排序的时间复杂度为O(n^2)。
?同样的O(n^2)复杂度,直接插入排序法比冒泡和简单选择排序的性能更好一些。
|
|
|
---------------------------------------------------------------我是直接插入排序的改进-------------------------------------------------------------------------
希尔排序是D.L.Shell于1959年提出来的一种排序算法,在这之前排序算法的时间复杂度基本都是O(n^2)的,希尔排序算法是突破这个时间复杂度的第一批算法之一。
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
简单插入排序很循规蹈矩,不管数组分布是怎么样的,依然一步一步的对元素进行比较,移动,插入,比如[5,4,3,2,1,0]这种倒序序列,数组末端的0要回到首位置很是费劲,比较和移动元素均需n-1次。而希尔排序在数组中采用跳跃式分组的策略,通过某个增量将数组元素划分为若干组,然后分组进行插入排序,随后逐步缩小增量,继续按组进行插入排序操作,直至增量为1。希尔排序通过这种策略使得整个数组在初始阶段达到从宏观上看基本有序,小的基本在前,大的基本在后。然后缩小增量,到增量为1时,其实多数情况下只需微调即可,不会涉及过多的数据移动。
/*对顺序表L作希尔排序*/
void ShellSort(Sqlist *L)
{
int i, j;
int increment = L->length;
do
{
increment = increment / 3 + 1; //增量序列
for (i = increment + 1; i <= L->length; i++)
{
if (L->r[i] < L->r[i - increment])
{
//需将L->r[i]插入有序增量子表
L->r[0] = L->r[i]; //暂存在L->r[0]
for (j = i - increment; j > 0 && L->r[0] < L->r[i]; j -= increment)
{
L->r[j + increment] = L->r[j]; //记录后移,查找插入位置
}
L->r[j + increment] = L->r[0]; //插入
}
}
}
while (increment > 1);
}
希尔排序的关键并不是随便分组后各自排序,而是将相隔某个"增量"的记录组成一个子序列,实现跳跃式的移动,使得排序效率提高。
增量的选取非常关键,迄今为止还没有人找到一种最好的增量序列。不过大量研究表明,当增量序列为dlta[k]=2^(t-k+1)(0<=k<=t<=向下取整[log2(n+1)])时,可以获得不错的效率,其时间复杂度为On^(3/2)。
需注意,增量序列的最后一个增量必须等于1才行。另外由于记录是跳跃式的移动,希尔排序并不是一种稳定的排序算法。
冒泡排序是一种交换程序,基本思想:两来你个比较相邻记录的关键字,如果反序则交换,直到没有反序的记录为止。
·冒泡排序初级版
/*对顺序表L作交换排序(冒泡排序初级版)*/
void BubbleSort0(SqList *L)
{
int i, j;
for (i = 1; i < L->length; i++)
{
for (j = i + 1; j <= L->length, j++)
{
if (L->r[i] > L->r[j])
swap(L, i, j); //交换L->r[i] 和 L->r[j]的值
}
}
}
该算法版本效率低。
·冒泡排序正宗版
/*对顺序表L作冒泡排序(冒泡排序正宗版)*/
void BubbleSort(SqList *L)
{
int i, j;
for (i = 1; i < L->length; i++)
{
for (j = L->length - 1; j >= i; j--) //注意j是从后往前循环
{
if (L->r[j] > L->r[j + 1]) //若前者大于后者(注意这里与上一算法差异)
swap(L, j, j + 1); //交换L->r[j]和 L->r[j + 1]的值
}
}
}
·冒泡排序优化版
/*对顺序表L作冒泡排序(冒泡排序优化版)*/
void BubbleSort(SqList *L)
{
int i, j;
Status flag = TRUE; //flag用来作标记
for (i = 1; i < L->length && flag; i++) //若flag为FALSE,则退出循环
{
for (j = L->length - 1; j >= i; j--) //注意j是从后往前循环
{
if (L->r[j] > L->r[j + 1]) //若前者大于后者(注意这里与上一算法差异)
{
swap(L, j, j + 1); //交换L->r[j]和 L->r[j + 1]的值
flag = TRUE; //若有数据交换,则flag为TRUE
}
}
}
}
代码改动的关键就是在i变量的循环中,增加了对flag是否为TRUE的判断,可以避免因已经有序的情况下的无意义循环判断。
最好情况下:要排序的表本身就是有序的,经过n-1次比较,无数据交换,时间复杂度O(n);
最坏情况下:待排序表为逆序,需比较次,并作等数量级的记录移动,因此,总的时间复杂度为O[n^2]
|
|
|
---------------------------------------------------------------我是直接插入排序的改进-------------------------------------------------------------------------
详见《王道》第13章 树--PART1
选择排序的基本思想是每一趟在n-i+1(i=1,2,…,n-1)个记录中选取关键字最小的记录作为有序序列的第i个记录。这里先介绍简单选择排序法。
简单选择排序法就是通过n-i次比较,从n-i+1个记录中选择出关键字最小的记录,并和第i(1<=i<=n)个记录交换之。
*对顺序表L作简单选择排序*/
void SelectSort(SqList *L)
{
int i, j,min;
for (i = 1; i < L->length; i++)
{
min = i; //将当前下标定义为最小值下标
for (j = i + 1; j <= L->length; j++) //循环之后的数据
{
if (L->r[min] > L->r[j]) //如果有小于当前最小值的关键字
min = j; //将此关键字的下标赋值给min
}
if (min != i;) //若min不等于i,说明找到最小值,交换
swap(L, i, min); //交换L->r[min]和L->r[min]的值
}
}
简单排序最大的特点:交换移动数据次数相当少,最好情况下交换0次,最差请求交换n-1次;适用于数组个数不多,但每个数组元素较大的情况。
时间复杂度:无论是最好最差情况,比较次数一样多,n(n-1)/2,总的时间复杂度O(n^2)。
简单选择排序性能上略优于冒泡排序。
|
|
|
---------------------------------------------------------------我是直接插入排序的改进-------------------------------------------------------------------------
堆排序就是利用堆(假设利用大顶堆)进行排序的方法。它的基本思想是,将待排序的序列构成一个大顶堆。此时,整个序列的最大值就是堆顶的根结点。将它移走(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值),然后将剩余的n-1个序列重新构造成一个堆,这样就会得到n个元素中的次大值。如此反复执行,便能得到一个有序序列了。
实现需要解决两个问题:
(1)如何由一个无序序列构建成一个堆?
(2)如果在输出堆顶元素后,调整剩余元素成为一个新的堆?
/*对顺序表L作堆排序*/
void HeapSort(SqList *L)
{
int i;
for (i = L->length / 2; i > 0; i--) //把L中的r构建成一个大顶堆
HeapAdjust(L, i, L->length);
for (i = L->length; i > 1; i--)
{
swap(L, 1, i); //将堆顶记录和当前未经排序子序列的最后一个记录交换
HeapAdjust(L, 1, i - 1); //将L->r[1..i-1]重新调整为大顶堆
}
}
/*已知L->r[s..m]中记录的关键字除L->r[s]之外均满足堆的定义*/
/*本函数调整L->r[s]的关键字,使L->r[s..m]称为一个大顶堆*/
void HeapAdjust(SqList *L, int s, int m)
{
int temp, j;
temp = L->r[s];
for (j = 2 * s; j <= m; j *= 2) //沿关键字较大的孩子结点向下筛选
{
if (j < m && L->r[j] < L->r[j + 1])
++j; //j为关键字中较大的记录的下标
if (temp >= L->r[j])
break;
L->r[s] = L->r[j];
s = j;
}
L->r[s] = temp; //插入
}
堆排序的时间复杂度为O(nlogn)。由于堆排序对原始记录的排序状态并不敏感,因此它无论是最好、最坏和平均时间复杂度均为O(nlogn)。这在性能上显然要远远好过于冒泡、简单选择、直接插入的O(n^2)的时间复杂度了。
空间复杂度上,它只有一个用来交换的暂存单元,也非常的不错。不过由于记录的比较与交换是跳跃式进行,因此,堆排序也是一种不稳定的排序方法。
另外,由于初始构建堆所需的比较次数比较多,因此,它并不适合待排序序列个数较少的情况。
归并排序就是利用归并的思想实现的排序方法。原理是假设初始序列含有n个记录,则可以看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到向上取整[n/2]个长度为2或1的有序子序列;再两两归并,……,如此重复,直至得到一个长度为n的有序序列为止,这种排序方法称为2路归并排序。
1.递归实现
/*将有序的SR[i...m]和SR[m+1...n]归并为有序的TR[i...n]*/
void Merge(int SR[], int TR[], int i, int m, int n) //将SR中记录由小到大归并入TR
{
int j, k, l;
for (j = m + 1, k = i; i <= m && j <= n;k++)
{
if (SR[i] < SR[j])
TR[k] = SR[i++];
else
TR[k] = SR[j++];
}
if (i <= m) //将剩余的SR[i...m]复制到TR
{
for (l = 0; l <= m - i; l++)
TR[k + l] = SR[i + l];
}
if (j <= n) //将剩余的SR[j...n]复制到TR
{
for (l = 0; l <= n - j; l++)
TR[k + 1] = SR[j + l];
}
}
/*将SR[s...t]归并排序为TR1[s...t]*/
void MSort(int SR[], int TR1[], int s, int t)
{
int m;
int TR2[MAXSIZE + 1];
if (s == t)
TR1[s] = SR[s];
else
{
m = (s + t) / 2; //将SR[s...t]平分为SR[s...m]和SR[m+1...t]
MSort(SR, TR2, s, m); //递归将SR[s...m]归并为有序的TR2[s...m]
MSort(SR, TR2, m+1, t); //递归将SR[m+1...t]归并为有序的TR2[m+1...t]
Merge(TR2, TR1, s, m, t); //将TR2[s...m]和TR2[m+1...t]归并到TR1[s...t]
}
}
/*对顺序表L作归并排序*/
void MergeSort(SqList *L)
{
MSort(L->r, L->r, L->length);
}
2. 非递归实现
/*将有序的SR[i...m]和SR[m+1...n]归并为有序的TR[i...n]*/
void Merge(int SR[], int TR[], int i, int m, int n) //将SR中记录由小到大归并入TR
{
int j, k, l;
for (j = m + 1, k = i; i <= m && j <= n;k++)
{
if (SR[i] < SR[j])
TR[k] = SR[i++];
else
TR[k] = SR[j++];
}
if (i <= m) //将剩余的SR[i...m]复制到TR
{
for (l = 0; l <= m - i; l++)
TR[k + l] = SR[i + l];
}
if (j <= n) //将剩余的SR[j...n]复制到TR
{
for (l = 0; l <= n - j; l++)
TR[k + 1] = SR[j + l];
}
}
/*将SR[]中相邻长度为s的子序列两两归并到TR[]*/
void MergePass(int SR[], int TR[], int s, int n)
{
int i = 1;
int j;
while (i <= n - 2 * s + 1)
{
Merge(SR, TR, i, i + s - 1, i + 2 * s - 1); //两两归并
i = i + 2 * s;
}
if (i < n - s + 1) //归并最后两个序列
Merge(SR, TR, i, i + s - 1, n);
else //若最后只剩下单个子序列
{
for (j = i; j <= n; j++)
TR[j] = SR[j];
}
}
/*对顺序表L作归并非递归排序*/
void MergeSort2(SqList *L)
{
int* TR = (int*)malloc(L->length*sizeof(int)); //申请额外空间
int k = 1;
while (k < L->length)
{
MergePass(L->r, TR, k, L->length);
k = 2 * k; //子序列长度加倍
MergePass(TR, L->r, k, L->length);
k = 2 * k; //子序列长度加倍
}
}
1. 非递归算法
时间复杂度:O(nlogn)
空间复杂度:O(n+logn)
稳定性:稳定
2. 非递归算法
时间复杂度:O(nlogn)
空间复杂度:O(n)
稳定性:稳定