1. 排序的基本概念与分类
1.1 排序的稳定性
假设 ki=kj(1<=i<=n,1<=j<=n,i!=j) k i = k j ( 1 <= i <= n , 1 <= j <= n , i ! = j ) ,且在排序前的序列中 ri r i 领先于 rj r j (即i < j)。如果排序后 ri r i 仍领先于 rj r j ,则称所用的排序法是稳定的;反之,若可能使得排序后的序列中 rj r j 领先 ri r i ,则称苏勇的排序方法是不稳定的。
1.2 内排序与外排序
内排序是在排序整个过程中,待排序的所有记录全部被放置在内存中。外排序是由于排序记录个数太多,不能同时放置内存,整个排序过程需要再内外存之间多次交换数据才能进行。
对于内排序来说,排序算法的性能主要是受3个方面影响:
1. 时间性能
2. 辅助空间
3. 算法的复杂性
1.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;
}
2. 冒泡排序
2.1 最简单排序实现
冒泡排序(Bubble Sort)是一种交换排序,它的基本思想是:两两比较相邻记录的关键字,如果反序则交换,直到没有反序的记录为止。
/* 对顺序表L作交换排序(冒泡排序初级版) */
void BubbleSort0(SqList *L) {
int i, j;
for(i = 1; i < L->lenght; i++) {
for(j = i + 1, j <= L->length; j++) {
if(L->r[i] > L->r[j]) {
swap(L, i, j);
}
}
}
}
2.2 冒泡排序算法
/* 对顺序表L作冒泡排序 */
void BubbleSort(SqList *L) {
int i, j;
for(i = 1; i < L->length; i++) {
for(j = L->length - 1; j >= i; j--) {
if(L->r[j] > L->r[j + 1]) {
swap(L, j, j+1);
}
}
}
}
2.3 冒泡排序优化
/* 对顺序表L作改进冒泡算法 */
void BubbleSort2(SqList *L) {
int i, j;
Status flag = TRUE;
for(i = 1; i < L->length && flag; i++) {
flag = FALSE; /* 初始化为false */
for(j = L->length - 1; j >= i; j--) {
if(L->r[j] > L->r[j+1]) {
swap(L, j, j+1);
flag = TRUE;
}
}
}
}
2.4 冒泡排序复杂度分析
总的时间复杂度为 O(n2) O ( n 2 ) .
3. 简单选择排序
3.1 简单选择排序算法
简单选择排序法(Simple Selection Sort)就是通过n - i次关键字间的比较,从n - i + 1个记录中选出关键字最小的记录,并和第i个记录交换之。
/* 对顺序表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(i != min) /* 若min不等于i,说明找到最小值,交换 */
swap(L, i, min);
}
}
3.2 简单选择排序复杂度分析
时间复杂度与冒泡排序同为 O(n2) O ( n 2 ) ,但简单选择排序的性能上还是要略优于冒泡排序。
4. 直接插入排序
4.1 直接插入排序算法
直接插入排序(Straight Insertion Sort)的基本操作是将一个记录插入到已经排好序的有序表汇总,从而得到一个新的、记录数增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[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]; /* 插入到正确位置 */
}
}
}
4.2 直接插入排序复杂度分析
直接插入排序法的时间复杂度为 O(n2) O ( n 2 ) ,比冒泡和简单选择排序的性能要好一些。
5. 希尔排序
5.1 希尔排序原理
将相距某个“增量”的记录组成一个子序列,这样才能保证在子序列内分别进行直接插入排序后得到的结果是基本有序而不是局部有序。
5.2 希尔排序算法
/* 对顺序表L作希尔排序 */
void SheelSort(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];
for(j = i - increment; j > 0 && L->r[0] < L->r[j]; j -= increment)
L->r[j + increment] = L->r[j]; /* 记录后移,查找插入位置 */
L->r[j+increment] = L->r[0]; /* 插入 */
}
}
} while(increment > 1);
}
6. 堆排序
堆是具有下列性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
6.1 堆排序算法
堆排序(Heap Sort)就是利用堆进行排序的方法。它的基本思想是,将待排序的序列构造成一个大顶堆。此时,整个序列的最大值就是堆顶的根结点。将它移走(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值),然后剩余的n-1个序列重新构造成一个堆,这样就会得到n个元素中的次值。如此反复执行,便能到到一个有序序列了。
/* 对顺序表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; /* rc应插入在位置s上 */
L->r[s] = L->r[j];
s = j;
}
L->r[s] = temp; /* 插入 */
}
6.2 堆排序复杂度分析
时间复杂度为O(nlogn)。由于初始构建堆所需的比较次数较多,因此,它并不适合待排序序列个数较少的情况。
7. 归并排序
7.1 归并排序算法
归并排序(Merging Sort)就是利用归并的思想实现的排序算法。它的原理是假设初始序列有n个记录,得到n/2个长度为2或1的有序子序列;再两两归并,。。。,如此重复,知道得到一个长度为n的有序序列为止。
/* 对顺序表L作归并排序 */
void MergeSort(SqList *L) {
MSort(L->r, L->r, l, L->length);
}
/* 将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(SR, TR1, s, m t); /* 将TR2[s..m]和TR2[m+1..t]归并到TR1[s..t] */
}
}
/* 将有序的SR[i..m]和SR[m+1..n]归并为有序的TR[i..n] */
void Merge(int SR[], int TR[], int i, int m, int n) {
int j, k, l;
for(j = m + 1, k = i; i<= m && j <= n; k++) { /* 将SR中记录由小到大归并入TR */
if(SR[i] < SR[j])
TR[k] = SR[i++];
else
TR[k] = SR[j++];
}
if(i <= m) {
for(l = 0; l <= m - i; l++) {
TR[k + l] = SR[i + l]; /* 将剩余的SR[i..m]复制到TR */
}
}
if(j <= n) {
for(l = 0; l <= n - j; l++) {
TR[k + l] = SR[j + l]; /* 将剩余的SR[j..n]复制到TR */
}
}
}
7.2 归并排序复杂度分析
时间复杂度为O(nlogn),而且这是归并排序算法中最好、最坏、平均的时间性能。
7.3 非递归实现归并排序
/* 对顺序表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; /* 子序列长度加倍 */
}
}
/* 将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];
}
8. 快速排序
8.1 快速排序算法
快速排序(Quick Sort)的基本思想是:通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序的目的。
/* 对顺序表L作快速排序 */
void QuickSort(SqList *L) {
QSort(L, 1, L->length);
}
/* 对顺序表L中的子序列L->r[low..higth]作快速排序 */
void QSort(SqList *L, int low, int high) {
int pivot;
if(low < high) {
pivot = Partition(L, low, high); /* 将L->r[low..high]一分为二,算出枢轴值pivot */
QSort(L, low, pivot - 1); /* 对低子表递归排序 */
QSort(L, pivot + 1, high); /* 对高子表递归排序 */
}
}
Partition函数要做的,就是先选取当中的一个关键字,比如选择第一个关键字50,然后想尽办法将它放到一个位置,使得它左边的值都比它小,右边的值比它大,我们将这样的关键字称为枢轴(pivot)。
/* 交换顺序表L中子表的记录,是枢轴记录到位,并返回其所在的位置 */
/* 此时在它之前(后)的记录均不大(小)于它 */
int Partition(SqList *L, int low, int high) {
int pivotkey;
pivotkey = L->r[low]; /* 用子表的第一个记录作枢轴记录 */
while(low < high) { /* 从表的两端交替向中间扫描 */
while(low < high && L->r[high]>=pivotkey)
high--;
swap(L, low, high); /* 将比枢轴记录小的记录交换到低端 */
while(low < high && L->r[low] <= pivotkey)
low++;
swap(L, low, high); /* 将比枢轴记录打的记录交换到高端 */
}
return low;
}
8.2 快速排序复杂度分析
快速排序的时间复杂度为O(nlogn)。
8.3 快速排序优化
1 优化选取枢轴
int pivotkey;
int m = low + (high - low) / 2; /* 计算数组中间的元素的下标 */
if(L->r[low] > L->r[high])
swap(L, low, high); /* 交换左端与右端数据,保证左端较小 */
if(L->r[m] > L->r[high])
swap(L, high, m); /* 交换中间与右端数据,保证中间较小 */
if(L->r[m] > L->r[low])
swap(L, m, low); /* 交换中间与左端数据,保证左端较小 */
/* 此时L.r[low]已经为整个序列左中右三个关键字的中间值 */
pivotkey = L->r[low]; /* 用子表的第一个记录作枢轴记录 */
2 优化不必要的交换
/* 快速排序优化算法 */
int Partition1(SqList *L, int low, int high) {
int pivotkey;
// 这里省略三数取中代码
pivotkey = L->r[low]; /* 用子表的第一个记录作枢轴记录 */
L->r[0] = pivotkey; /* 将枢轴关键字备份到L->r[0] */
while(low < hight) { /* 从表的两端交替向中间扫描 */
while(low < high && L->r[high] >= pivotkey)
high--;
L->r[low] = L->r[high]; /* 采用替换而不是交换的方式进行操作 */
while(low < high && L->r[low] <= pivotkey)
low++;
L->r[high] = L->r[low]; /* 采用替换而不是交换的方式进行操作 */
}
L->r[low] = L->r[0]; /* 将枢轴数值替换回L.r[low] */
return low; /* 返回枢轴所在位置 */
}
3 优化小数组时的排序方案
#define MAX_LENGTH_INSERT_SORT 7 /* 数组长度阀值 */
/* 对顺序表L中的子序列L.r[low..high]作快速排序 */
void QSort(SqList &L, int low, int high) {
int pivot;
if((high - low) > MAX_LENGTH_INSERT_SORT) { /* 当high - low大于常数时用快速排序 */
pivot = Partition(L, low, high);
QSort(L, low, pivot - 1);
QSort(L, pivot + 1, high);
} else /* 当high - low小于等于常数时用直接插入排序 */
InsertSort(L);
}
4 优化递归操作
/* 对顺序表L中的子序列L.r[low..high]作快速排序 */
void QSrot1(SqList *L, int low, int high) {
int pivot;
if((high - low) > MAX_LENGTH_INSERT_SORT) {
while(low < high) {
pivot = Partition1(L, low, high);
QSort1(L, low, pivot - 1);
low = pivot + 1; /* 尾递归 */
}
} else
InsertSort(L);
}