在前面的文章里系统的介绍了查找算法,从今天开始将进入排序章节。我们需要明白——排序,是为了更方便的查找,提高查找效率。本篇文章主要介绍排序的基本概念及最简单的直接插入排序。
一,基本概述
排序方法的分类
按数据存储介质:内部排序和外部排序
按比较器个数:串行排序和并行排序
按主要操作:比较排序和基数排序
按辅助空间:原地排序和非原地排序
按稳定性:稳定排序和非稳定排序
按自然性:自然排序和非自然排序
按比较器个数:
若待排序记录都在内存中,称为内部排序;
若待排序记录一部分在内存,一部分在外存,则称为外部排序。
注:外部排序时,要将数据分批调入内存来排序,中间结果还要及时放入外存,显然外部排序要复杂得多。
按主要操作:
比较排序:用比较的方法
插入排序、交换排序、选择排序、归并排序
基数排序:不比较元素的大小,仅仅根据元素本身的取值确定其有序位置。
按辅助空间:
原地排序:辅助空间用量为O(1)的排序方法。
(所占的辅助存储空间与参加排序的数据量大小无关)
非原地排序:辅助空间用量超过O(1)的排序方法。
按稳定性:
稳定排序:能够使任何数值相等的元素,排序以后相对次序不变。
非稳定性排序:不是稳定排序的方法。
排序的稳定性只对结构类型数据排序有意义。
例如:
n个学生信息(学号、姓名、语文、数学、英语、总分)
1、按数学成绩从高到低排序
2、按照总分从高到低排序。
3、总分相同的情况下,数学成绩高的排在前面
按自然性:
自然排序:输入数据越有序,排序的速度越快的排序方法。
非自然排序:不是自然排序的方法。
后面的文章将主要介绍
按数据存储介质:内部排序和外部排序
按比较器个数:串行排序和并行排序
按主要操作:比较排序和基数排序
记录序列以顺序表存储
# define MAXSIZE 20 //设记录不超过20个
typedef int KeyType ; //设关键字为整型量(int型)
Typedef struct { //定义每个记录(数据元素)的结构
KeyType key ; //关键字
InfoType otherinfo; //其它数据项
}RedType ;
Typedef struct { //定义顺序表的结构
RedType r [ MAXSIZE +1 ]; //存储顺序表的向量
//r[0]一般作哨兵或缓冲区
int length ; //顺序表的长度
}SqList ;
排序算法的好坏如何衡量?
时间效率:排序速度(比较次数与移动次数)
空间效率:占内存辅助空间的大小
二,直接插入排序
基本思想 :
每步将一个待排序的对象,按其关键码大小,插入到前面已经排好序的一组对象的适当位置上,直到对象全部插入为止。
即边插入边排序,保证子序列中随时都是排好序的。
下面由图片来理解:
插入排序是最简单的排序方法。
例: (13,6,3,31,9,27,5,11)
排序过程:整个排序过程为n-1趟插入,即先将序列中第1个记录看成是一个有序子序列,然后从第2个记录开始,逐个进行插入,直至整个序列有序。
插入排序的基本步骤:
在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]的位置上。
具体的代码如下:
void InsertSort(SqList &L)
{int i,j;
for(i=2;i<=L.length;++i)
if( L.r[i].key<L.r[i-1].key)//将L.r[i]插入有序子表
{ L.r[0]=L.r[i]; // 复制为哨兵
L.r[i]=L.r[i-1];
for(j=i-2; L.r[0].key<L.r[j].key;--j)
L.r[j+1]=L.r[j]; // 记录后移
L.r[j+1]=L.r[0]; //插入到正确位置
}
}
算法分析:
•设对象个数为n,则执行n-1趟
•比较次数和移动次数与初始排列有关
最好情况下:
• 每趟只需比较 1 次,不移动
• 总比较次数为 n-1
for(i=2;i<=L.length;++i)
if( L.r[i].key<L.r[i-1].key)
最坏情况下:
• 第 i 趟比较i次,移动i+1次
if( L.r[i].key<L.r[i-1].key) {
L.r[0]=L.r[i]; // 复制为哨兵
L.r[i]=L.r[i-1];
……
L.r[j+1]=L.r[0]; //插入到正确位置 }
下面是一段完整的直接插入排序c语言代码:
#include <stdio.h>
// 插入排序函数,参数为待排序数组arr和数组长度n
void insertion_sort(int arr[], int n) {
int i, key, j;
// 从第二个元素开始遍历数组
for (i = 1; i < n; i++) {
key = arr[i]; // 当前要插入的元素
j = i - 1; // 已排序部分的最后一个元素的索引
// 将大于key的元素向后移动一位
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j = j - 1;
}
// 将key插入到正确的位置
arr[j + 1] = key;
}
}
int main() {
int arr[] = {12, 11, 13, 5, 6}; // 待排序数组
int n = sizeof(arr) / sizeof(arr[0]); // 计算数组长度
insertion_sort(arr, n); // 调用插入排序函数对数组进行排序
printf("Sorted array: "); // 输出排序后的数组
for (int i = 0; i < n; i++)
printf("%d ", arr[i]);
printf(" ");
return 0;
}
三,折半插入排序
基本思想:
为了减少关键字的比较次数,我们在直接插入的基础上引入折半插入排序。
我们可以结合之前的折半查找来理解
代码如下:
void BInsertSort(SqList &L) {
// 遍历顺序表,从第二个元素开始
for (int i = 2; i <= L.length; ++i) {
// 将当前元素作为待插入元素
L.r[0] = L.r[i];
int low = 1, high = i - 1, m;
// 使用二分查找法确定待插入元素在已排序部分的正确位置
while (low <= high) {
m = (low + high) / 2;
if (L.r[0].key < L.r[m].key) {
high = m - 1;
} else {
low = m + 1;
}
}
// 将正确位置及其右侧的所有元素向后移动一位,为待插入元素腾出空间
for (int j = i - 1; j >= high + 1; --j) {
L.r[j + 1] = L.r[j];
}
// 将待插入元素插入到正确的位置
L.r[high + 1] = L.r[0];
}
} // BInsertSort
算法分析:
折半查找比顺序查找快,所以折半插入排序就平均性能来说比直接插入排序要快。
它所需要的关键码比较次数与待排序对象序列的初始排列无关,仅依赖于对象个数。在插入第 i 个对象时,需要经过 ë log2i û +1 次关键码比较,才能确定它应插入的位置 。
折半插入排序相较于直接插入排序:
四,希尔排序
基本思想:
先将整个待排记录序列分割成若干子序列,分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行一次直接插入排序。
希尔排序的技巧:
子序列不是简单“逐段分割”
将相隔增量dk的记录组成一个子序列
dk逐趟缩短(如依次取5,3,1),直到dk=1为止
希尔排序的优点:
小元素跳跃式前移
最后一趟增量为1时基本有序
平均性能优于直接插入排序
希尔排序算法主程序的代码:
dk值依次装在dlta[t]中
void ShellSort(SqList &L,int dlta[ ],int t){
//按增量序列dlta[0…t-1]对顺序表L作Shell排序
for(k=0;k<t;++k)
ShellInsert(L,dlta[k]);
//增量为dlta[k]的一趟插入排序
} // ShellSort
某一趟的排序操作:
void ShellInsert(SqList &L,int dk) {
//对顺序表L进行一趟增量为dk的Shell排序,dk为步长因子
for(i=dk+1;i<=L.length; ++ i)//开始将r[i] 插入有序增量子表
if(r[i].key < r[i-dk].key) {
r[0]=r[i];//暂存在r[0]
for(j=i-dk; j>0 &&(r[0].key<r[j].key); j=j-dk)
r[j+dk]=r[j];//关键字较大的记录在子表中后移
r[j+dk]=r[0];//在本趟结束时将r[i]插入到正确位置
}
}