新手上路,请多指教。如果有写的不对的地方,还请指出,非常感谢。
参考书目:数据结构(C语言版)(第2版),殷人昆编著, 清华大学出版社
目录
排序
¶ 前置代码
一、插入排序
1.1 直接插入排序
¶ 代码
1.2 折半插入排序
¶ 代码
1.3 希尔排序
¶ 代码
二、交换排序
2.1 冒泡排序
¶ 代码
2.2 快速排序
¶ 基本代码
2.2.1 Hoare划分
¶ Partition_Hoare
2.2.2 Rowe划分
¶ Partition_Rowe
2.2.3 改进一 cutoff
2.2.4 改进二 median3
¶ median3
三、选择排序
3.1 简单选择排序
¶ 代码
3.2 堆排序
¶ 代码
四、归并排序
4.1 二路归并排序
¶ 代码
五、基数排序
¶ 前置代码
5.1 MSD基数排序
5.2 LSD基数排序
六、其他排序
排序是计算机内经常进行的一种操作,
其目的是将一 组“无序”的记录序列调整为“有序”的记录序列。
假设含n个记录的序列为{ R1, R2, …, Rn } , 其相应的关键字序列为 { K1, K2, …,Kn } ,这些关键字相互之间可以进行比较,即在它们之间 存在着这样一个关系 : Kp1≤Kp2≤…≤Kpn , 按此固有关系将上式记录序列重新排列为 { Rp1, Rp2, …,Rpn } 的操作称作排序。
¶ 排序方法的分类
¶ 排序算法的稳定性
在待排记录序列中,任何两个关键字相同的记录,用某种排序方法排序后相对位置不变,则称这种排序方法是稳定的,否则称为不稳定的。
并非稳定算法优于不稳定算法,各有各的适合场合。在很一些情况下,数据原本的相对次序是没有意义或者不重要的,不稳定的算法完全可以满足要求。
要排序的内容是一个复杂对象的多个数字属性,其原本的初始顺序存在意义,且二次排序时保持原有排序的意义。此时必须使用稳定的算法。
一般来说,出现大跨度移动的算法是不稳定的。当然具体情况具体分析。比如将冒泡算法的比较条件改为 >= ,就不稳定了。
¶ 排序方法的比较
¶ 排序方法的选择
不同的排序方法适应不同的应用环境和要求
1. 若n较小,可采用直接插入或简单选择排序
¶ 当记录规模较小时,直接插入排序较好,它会 比选择更少的比较次数,且是稳定的;
¶ 当记录规模稍大时,因为简单选择移动的记录 数少于直接插入,宜用简单选择排序。
2. 若初始状态基本有序,则应选用直接插入、冒泡或随机的快速排序为宜;
3. 若n较大,则应采用时间复杂度为O(nlog2n)的排序
¶ 快速排序、堆排序、归并排序。
4. 特殊的基数排序
三种平均时间复杂度为O(nlog2n)的算法
¶ 内部排序和外部排序
排序算法可以分为内部排序和外部排序
本文中介绍的方法均为内部排序,使用数组实现,从小到大排列。并且添加了一些中间过程的输出。本人在自行测试的时候是没有问题。如果有朋友发现有误,还请提出,我尽快改正。请多多谅解。
#include
#include
typedef int DataType;
void printArray(DataType a[], int n)
{
for (int i = 0; i < n; i++)
printf("%d ", a[i]);
printf("\n");
}
void swap(DataType *a,DataType *b)
{
DataType t=*a;
*a=*b;
*b=t;
}
int main(int argc, char const *argv[])
{
int n;
scanf("%d",&n);
DataType *array=(DataType*)malloc(n*sizeof(DataType));
for (int i = 0; i < n; i++)
scanf("%d",array[i]);
printArray(array, n);
/*调用排序算法*/
printArray(array, n);
free(array);
return 0;
}
基本方法:
每步将一个待排序的元素按其排序码大小插入到前面有序区的适当位置,直到元素全部插入
- 在R[1..i-1]中查找R[i]的插入位置: R[1..j].key ≤ R[i].key < R[j+1..i-1].key;
- 将R[j+1..i-1]中的所有记录均后移一个位置;
- 将R[i] 插入(复制)到R[j+1]的位置上
¶ 基于顺序查找
从 i-1 往前 扫描查找待插入位置
直接插入排序 | ||||||
i | (0) | (1) | (2) | (3) | (4) | (5) |
初始序列 | 21 | 25 | 49 | 25* | 16 | 8 |
1 | 21 | 25 | 49 | 25* | 16 | 8 |
2 | 21 | 25 | 49 | 25* | 16 | 8 |
3 | 21 | 25 | 25* | 49 | 16 | 8 |
4 | 16 | 21 | 25 | 25* | 49 | 8 |
5 | 8 | 16 | 21 | 25 | 25* | 49 |
灰色为有序区,紫色为待插入数据
¶ 直接插入排序特点
void InsertSort(DataType a[], int n)
{
int i, j;
for (i = 1; i < n; ++i)
{//0~i-1是有序的
DataType tmp = a[i];
/*查找与移动同时进行*/
for (j = i - 1; j >= 0 && a[j] > tmp; --j)
a[j + 1] = a[j];//后移元素
a[j + 1] = tmp;
printf("The %2dth insertion: ", i);
printArray(a, n);
}
}
¶ 基于二分查找
通过二分查找待插入位置,与直接插入相比减少了比较次数
void BinaryInsertSort(DataType a[], int n)
{
for (int i = 1; i < n; i++)
{//0~i-1是有序的
DataType tmp = a[i];
int low = 0;
int high = i - 1;
while (low <= high)
{//二分查找插入位置
int mid = (low + high) / 2;
if (tmp < a[mid]) high = mid - 1;//向左缩区间
else low = mid + 1;//向右缩区间
}
for (int j = i - 1; j >= low; j--)
a[j + 1] = a[j];//后移元素
a[low] = tmp;
printf("The %2dth insertion: ",i);
printArray(a,n);
}
}
¶ 基于逐趟缩小增量
基本思想:对待排记录序列先作“宏观”调整,再作 “微观”调整。
- 对数据分组,在各组内进行直接插入排序;
- 作若干次使待排记录基本有序;
- 对全部记录进行一次顺序插入排序;
分组方式:将 n 个记录分成 d 个子序列:
d 称为增量,它的值在排序过程中从大到小逐渐缩小, 直至最后一趟排序减为 1
希尔排序 | |||||||||||
初始序列 | 16 | 25 | 12 | 30 | 47 | 11 | 23 | 36 | 9 | 18 | 31 |
第一趟希尔排序,设 d=5 | |||||||||||
分组 | 16 | 25 | 12 | 30 | 47 | 11 | 23 | 36 | 9 | 18 | 31 |
排序 | 11 | 23 | 12 | 9 | 18 | 16 | 25 | 36 | 30 | 47 | 31 |
第二趟希尔排序,设 d=3 | |||||||||||
分组 | 11 | 23 | 12 | 9 | 18 | 16 | 25 | 36 | 30 | 47 | 31 |
排序 | 9 | 18 | 12 | 11 | 23 | 16 | 25 | 31 | 30 | 47 | 36 |
第三趟希尔排序,设 d=1 | |||||||||||
分组 | 9 | 18 | 12 | 11 | 23 | 16 | 25 | 31 | 30 | 47 | 36 |
排序 | 9 | 11 | 12 | 16 | 18 | 23 | 25 | 30 | 31 | 36 | 47 |
¶ 希尔排序特点
void ShellSort(DataType a[], int n, int d[], int m)
{ // d[m]存放增量,d[0]=1
for (int k = m - 1; k >= 0; k--)
{
int gap = d[k];//缩小增量
for (int start = 0; start < gap; start++)
{ // 直接插入的变形,间隔从1改为gap
for (int j, i = start + gap; i < n; i += gap)
{
DataType tmp = a[i];
for (j = i - gap; j >= start && a[j] > tmp; j -= gap)
a[j + gap] = a[j];
a[j + gap] = tmp;
}
}
printf("The gap= %d: ", gap);
printArray(a, n);
}
}
基本思想:
将待排记录中两两记录关键字进行比较, 若逆序则交换位置。
¶ 设计思路:
- 比较相邻的元素。如果逆序,则交换之。
- 经历一趟交换后,从后往前将无序区最小元素交换到有序区尾、无序区头,扩大有序区(也可从前往后,无序区在前,有序区在后)。
- 设置flag, 在该趟过程中无逆序,则提前终止。
- 最多进行 n-1 趟交换后,排列整齐。
冒泡排序(从后向前) | ||||||
i | (0) | (1) | (2) | (3) | (4) | (5) |
初始序列 | 21 | 25 | 49 | 25* | 16 | 8 |
1 | 8 | 21 | 25 | 49 | 25* | 16 |
2 | 8 | 16 | 21 | 25 | 49 | 25* |
3 | 8 | 16 | 21 | 25 | 25* | 49 |
4 | 8 | 16 | 21 | 25 | 25* | 49 |
¶ 冒泡排序特点
void BubbleSort(DataType a[], int n)
{
for (int i = 0; i < n ; i++)
{
int flag = 0;
for (int j = n - 1; j > i ; j--)
{//将第i小的元素交换到第i位
if (a[j - 1] > a[j])
{
swap(&a[j - 1], &a[j]);
flag = 1;
}
}
printf("The %dth swap:", i + 1);
printArray(a, n);
if (!flag) return; // 没有逆序,排列整齐
}
}
基本思想:
分治与递归
- 通过一趟排序将待排序记录分割成两个部分,
- 选择一个关键字作为分割标准,称为pivot,
- 一部分记录的关键字比另一部分的小。
¶ 基本操作:
- 选定一记录R(pivot),将所有其他记录关键字k’ 与该记录关键字k比较,
- 若 k’
k 则将记录换至R之后, - 继续对R前后两部分记录进行快速排序,直至排序范围为1。
快速排序 | ||||||||
初始 | 49 | 38 | 65 | 97 | 76 | 13 | 27 | 49 |
1 | [27 | 38 | 13] | 49 | [76 | 97 | 65 | 49] |
2 | [13] | 27 | [38] | 49 | [49 | 65] | 76 | [97] |
3 | 13 | 27 | 38 | 49 | 49 | 65 | 76 | 97 |
¶ 快速排序特点
void QuickSort(DataType a[], int left, int right)
{
if (left >= right) return;
int len = right - left + 1;
printf(" ");
printArray(a + left, len);
//if(len < cutoff){Insert(a+left, len);return;}
//median3(a, left, right);
int pivotpos = pivotpos = Partition(a, left, right);
printf("The pivot =%3d : ", a[pivotpos]);
printArray(a + left, len);
QuickSort(a, left, pivotpos - 1);
QuickSort(a, pivotpos + 1, right,);
}
int Partition(DataType a[], int left, int right)
{
DataType pivot = a[left]; // 以最左元素为基准
int l = left, r = right;
while (l < r)
{
while (a[r] >= pivot && l < r)
r--; // 从右直到找到小于pivot
a[l] = a[r]; // 交换到左边
while (a[l] <= pivot && l < r)
l++; // 从左直到找到大于pivot
a[r] = a[l]; // 交换到右边
}
a[l] = pivot; // l=r,回放基准
return l;//返回基准元素位置
}
int Partition(DataType a[], int left, int right)
{
int l = left;
DataType pivot = a[l];//以最左元素为基准
for (int i = left + 1; i <= right; i++)
{//一趟扫描整个序列
if (a[i] < pivot)
{//left+1~l < pivot
l++; //小于基准,交换
if (l != i) swap(&a[l], &a[i]);
}
}
a[left] = a[l];
a[l] = pivot;
return l;//返回基准元素位置
}
当 n 很小时,快速排序往往慢于简单排序
当序列长度为5~25时,采用直接插入排序要比快速排序快至少 10%
设置cutoff,当长度小于设定值时采用直接插入排序
快速排序在处理比较有序的序列时效率很低,容易形成单枝树。
尽量将pivot取在中间位置。
一种方法是left,mid,right 三者取中作为 pivot;也可以取五个甚至更多。
void median3(DataType a[],int left,int right)
{
int k1, k2; // k1最小指针,k2次小指针
int mid = (left + right) / 2;
if (a[left] <= a[mid]) {k1 = left;k2 = mid;}
else {k1 = mid;k2 = left;}
if (a[right] < a[k1]) {k2 = k1;k1 = right;}
else if (a[right] < a[k2]) k2 = right;
if (k2 != left) swap(&a[k2], &a[left]);
printf("-----a[%d]=%d,a[%d]=%d,a[%d]=%d\n",
left,a[left],mid,a[mid],right,a[right]);
}
¶ 设计思路:
- 第 i 趟(i=0,1,…,n-2),在 i~n-1中选出最小元素,交换到a[i]位置,使之成为有序区的第 i 个元素。
- 共执行 n-1 趟。
¶ 基本步骤:
- 在一组元素 a[i]~a[n-1] 中选择最小元素
- 若它不是这组第一个元素(a[i]),将两者对调
- 在剩余序列 a[i+1]~a[n-1]中重复执行 1、2 ,直到剩下一个元素
简单选择排序 | |||||
21 | 25 | 49 | 25* | 16 | 8 |
8 | 25 | 49 | 25* | 16 | 21 |
8 | 16 | 49 | 25* | 25 | 21 |
8 | 16 | 21 | 25* | 25 | 49 |
8 | 16 | 21 | 25* | 25 | 49 |
8 | 16 | 21 | 25* | 25 | 49 |
不稳定
void SelectSort(DataType a[], int n)
{
int i, j, k;
DataType tmp;
for (i = 0; i < n - 1; i++)
{//第i趟找i位置的元素
k = i;
for (j = i + 1; j < n; j++)//寻找最小
if ((a[j] < a[k])) k = j;
if (k != i) swap(&a[k], &a[i]);
printf("The %2dth select: ", i+1);
printArray(a, n);
}
}
¶ 设计思想:
- 建立大根堆,堆顶元素即最大值
- 将其放在末尾,成为有序区的首位。有序区从后往前扩大
- 重新调成堆,得到当前序列最大值
¶ 基本步骤:
- 建立初始大根堆,自下而上;
- 将堆顶元素与最后一个元素对换;
- 调整堆 (H.R[s..m]中记录的关键字除 R[s] 之外均满足堆的特征;
- 重复上述过程,共进行n-1次。
创建大根堆
堆排序过程
¶ 堆排序特点
void siftDown(DataType a[], int start, int m)
{//从start到m自上而下建立大根堆
int i = start, j = 2 * i + 1; // j是i的左子节点
DataType tmp = a[i];
while (j <= m)
{
if (j < m && a[j] < a[j + 1]) // 左右节点都存在
j++; // j指向大的子节点
if (tmp >= a[j]) break; // 找到原根节点应存放位置
a[i] = a[j];
i = j; // i下降到大的子节点
j = 2 * j + 1;
}
a[i] = tmp;
}
void HeapSort(DataType a[], int n)
{
for (int i = (n - 2) / 2; i >= 0; i--)
siftDown(a, i, n - 1);//从倒数第二层向上扩大调成堆
printf("Initial heap:");
printArray(a,n);
for (int i = n - 1; i > 0; i--)
{
swap(&a[0], &a[i]); // 交换最大元素至无序序列末尾
printf("The %dth operation:", n - i);
printArray(a, i);
siftDown(a, 0, i - 1);
}
}
基本思想:分治
划分:将序列划为等长序列,为子序列排序
归并: 将子序列组合成一个新的有序表。
¶ 设计思想:
- 设初始序列含有n个记录,则可看成 n 个有序的子序列,每个子序列长度为1。
- 两两合并,得到 /2 个长度为 2 或1的有序子序列。
- 再两两合并,……
- 如此重复,直至得到一个长度为 n 的有序序列为止。
¶ 归并排序特点
void MergeSort(DataType a[],int left,int right)
{
if (left >= right) return;
/*划分*/
int mid = (left + right) / 2;
MergeSort(a, left, mid);
MergeSort(a, mid + 1, right);
/*归并*/
int i = left, j = mid + 1; // 左右子序列头指针
int len = right - left + 1, k = 0; // 辅助数组长度、指针
DataType *t = (DataType *)malloc(len * sizeof(DataType));
while (i <= mid && j <= right)
{
if (a[i] <= a[j]) t[k++] = a[i++];
else t[k++] = a[j++];
}
while (i <= mid) t[k++] = a[i++];
while (j <= right) t[k++] = a[j++];
for (int i = 0; i < len; i++)
a[left + i] = t[i];
printf("----");
printArray(t,len);
free(t);
}
基数排序:借助多关键字排序的方法对单关键字排序
将整数按位数切割成不同的数字,然后按每个位数分别比较
包含多位 k = k1,k2,…,kd 的单关键字
多关键字排序
¶ 基数排序的特点
#include
#include
#define MAX 20
#define BASE 10 // 基数
void printArray(int a[], int n)
{
for (int i = 0; i < n; i++)
printf("%d ", a[i]);
printf("\n");
}
int geMaxtDigit(int a[], int n)
{ // 待排序序列数字的最大位数
int digit = 1;
int base = BASE;
int max = a[0];
for (int i = 1; i < n; i++)
if (a[i] > max) max = a[i];
while (max >= base)
{
digit++;
base *= 10;
}
printf("%d\n",digit);
return digit;
}
int getFigure(int x, int k)
{ // 获取右起第k位数字
for (int i = 1; i < k; i++)
x /= 10;
return x % 10;
}
高位优先 先通过一次分配将数据分成多个组,然后对各组数据分别排序
void MSD(int a[],int left,int right,int k,int digit)
{
if (left >= right || k > digit) return;
int i, j;
int count[BASE] = {0}; // 统计某一位出现相同数字个数
for (i = left; i <= right; i++)
{
int index = getFigure(a[i], digit - k+1);
count[index]++; // 统计各桶元素个数
}
int start[BASE] = {0}; // 记录当前位上各个数字开始的位置
for (j = 1; j < BASE; j++)
start[j] = count[j - 1] + start[j - 1]; // 安排各桶元素位置
int *atemp = (int *)malloc((right - left + 1) * sizeof(int));
for (i = left; i <= right; i++)
{
int index = getFigure(a[i], digit - k+1);
atemp[start[index]++] = a[i];
}
for (i = left, j = 0; i <= right; i++, j++)
a[i] = atemp[j];
free(atemp);
int p1 = left,p2;//每个桶的头尾
for (j = 0; j < BASE; j++)
{//处理各桶里的数据
p2 = p1 + count[j] - 1;
MSD(a, p1, p2, k + 1,digit);
p1 = p2 + 1;
}
}
void radixSort_MSD(int a[], int n)
{
int digit = geMaxtDigit(a, n);
MSD(a, 0, n - 1, 1, digit);
}
接收器太小,递归层次增加,效率降低;太大会出现很多空接收器,同样影响效率
低位优先 通过多次对全体数据集的分配和收集实现排序
void radixSort_LSD(int a[], int n)
{
int digit = geMaxtDigit(a, n);
int base = 1;
int *atemp = (int *)malloc(sizeof(int) * n);
while (digit--)
{
int count[BASE] = {0}; // 统计某一位出现相同数字个数
for (int i = 0; i < n; i++)
{
int index = a[i] / base % BASE;
count[index]++;
}
int start[BASE] = {0}; // 记录当前位上各个数字开始的位置
for (int i = 1; i < BASE; i++)
start[i] = start[i - 1] + count[i - 1];
for (int i = 0; i < n; i++)
{
int index = a[i] / base % BASE;
atemp[start[index]++] = a[i];
}
printf("Collect the %d th digit: ", digit + 1);
printArray(atemp, n);
for (int i = 0; i < n; i++)
a[i] = atemp[i];
base *= BASE;
}
free(atemp);
}
除了上述排序还有一些也经常会用到比如计数排序、桶排序等等。
先留个坑,日后再写。