排序:将一组杂乱无章的数据排列成一个按关键字有序的序列。
关键字(key):通常数据对象有多个属性域,即多个数据成员组成,其中有一个属性域可用来区分对象,该域即为关键字,作为排序依据。
排序的主要目的:便于查找。
排序算法的稳定性:数据集合中有Ri和Rj,它们的关键字相同Ki=Kj,排序前,Ri排在Rj前面,排序后,Ri仍在对象Rj前面,则该排序方法是稳定的,否则不稳定。
内部排序和外部排序:
内部排序:待排序记录存放在内存。
外部排序:待排序记录一部分在内存,一部分在外存,排序过程中需对外存进行访问的排序。
内部排序分类:
按排序依据原则 | 按排序所需工作量 |
插入类排序:直接插入排序、折半插入排序、希尔排序 | 简单的排序方法:T(n)=O(n²) |
交换类排序:冒泡排序、快速排序 | 先进的排序方法:T(n)=O(nlogn) |
选择类排序:简单选择排序、堆排序 | 基数排序:T(n)=O(d*n) |
归并类排序:2-路归并排序 | |
分配类排序:基数排序 |
在排序过程中,一般进行两种基本操作:
(1)比较两个关键字的大小;
(2)将记录从一个位置移动到另一个位置。
排序的时间开销:排序的时间开销是衡量算法好坏的最重要的标志。排序的时间开销可用算法执行中的数据比较次数与数据移动次数来衡量。
#define MAXSIZE 20
//设记录不超过20个
typedef int KeyType;//设关键字为整型量( int型)
typedef struct //定义每个记录(数据元素)的结构
{
KeyType key; //关键字
InfoType otherinfo;//其它数据项
}ElemType;
typedef struct//定义顺序表的结构
{
ElemType r[MAXSIZE+1];//存储顺序表的向量
//r[0]一般作哨兵或空闲
int length; //顺序表的长度
}SqList ;
插入排序
基本思想:每步将一个待排序的对象,按其关键码大小,插入到前面已经排好序的一组对象的适当位置上,直到对象全部插入为止。即边插入边排序,保证子序列中随时都是排好序的。
不同的具体实现方法导致不同的算法描述:
直接插入排序(基于顺序查找,最简单的排序法)
折半插入排序(基于折半查找)
希尔排序(基于逐趟缩小增量)
直接插入排序
基本思想:
当插入第i(i ≥1)个对象r[i]时,前面的r[1],...,r[i-1]已经排好序。用r[i]的关键字与r[i-1],r[i-2],...,r[1]的关键字顺序比较。将所有关键字大于K[i]的记录依次向后移动一个位置,直到遇见一个关键字小于或者等于K[i]的记录r[j],此时r[j]后面必为空位置,将第i个记录插入空位置即可。
typedef struct
{
ElemType r[MAXSIZE+1];
int length;
}SqList;
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[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[i];
//从后向前比较直到发现不比L.r[0]大的元素为止
L.r[j+1]=L.r[0];
}
}
}
算法分析:
设对象个数为n,则执行n-1趟。比较次数和移动次数与初始排列有关。
最好情况下:每趟只需比较1次,不移动总比较次数为n-1。
最坏情况下:第i趟比较i次,移动i+1次。
比较次数:
移动次数:
若出现各种可能排列的概率相同,则可取最好情况和最坏情况的平均情况。平均情况比较次数和移动次数约为n²/4。
时间复杂度为O(n²)
空间复杂度为O(1)
直接插入排序特点:是一种稳定的排序方法,实现容易,也适用于链式存储结构,只适宜于记录个数较少,或记录关键字基本有序的情况。
其他插入排序(折半插入、希尔排序等)
[折半插入排序]
在插入r[i]时,利用折半查找法寻找r[i]的插入位置。
[希尔排序(缩小增量排序)]
基本思想:先将整个待排记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行一次直接插入排序。
排序过程:先取一个正整数d1<n,把所有相隔d1的记录放一组,组内进行直接插入排序;然后取d2<d1,重复上述分组和排序操作;直至di=1,即所有记录放进一个组中排序为止。
例:对下列序列采用希尔排序:{49,38,65,97,76,13,27,49,55,04}
希尔排序特点:子序列的构成不是简单的“逐段分割”,而是将相隔某个增量的记录组成一个子序。希尔排序可提高排序速度,因为分组后n值减小,所以T(n)从总体上看是减小了关键字较小的记录跳跃式前移,在进行最后一趟增量为1的插入排序时,序列已基本有序
增量序列取法:递减,最后一个增量值必须为1,无除1以外的公因子。
效率分析:
时间性能:希尔排序的分析非常困难,原因是何种步长序列最优难以断定。
时间复杂度是n和d的函数:O()。
空间性能:只用一个额外空间,空间复杂度为O(1)。
稳定性:希尔排序是不稳定的排序算法。
例:已知序列{70,83,100,65,10,32,7,9},采用希尔排序法(增量依次为5,3,1),写出每一趟的结果。
交换排序
基本思想:两两比较待排序记录的关键码,如果发生逆序(即排列顺序与排序后的次序正好相反),则交换之,直到所有记录都排好序为止。
交换排序的主要算法有:冒泡排序、快速排序
冒泡排序(Bubble Sort)
基本思想:大数沉底,小数冒出(或小数沉底,大数冒出)
冒泡排序的基本方法是:设待排序对象序列中的对象个数为n,最多作n-1趟排序。在第i趟中相邻两个元素进行比较,如果逆序就交换。
void BubbleSort(SqList &L)
{
for (int i=1;i<L.length;i++)
{
Exchange = 0;
for (int j=1;j<=L.length-i;j++)
{
if (L.r[j+1]<L.r[j])
{
int temp=L.r[j+1]
L.r[j+1]=L.r[i];
L.r[i]=temp;
Exchange=1;
}
}
if (Exchange==0) return;
}
}
算法分析:比较次数和移动次数与初始排列有关
最好的情况(关键字在记录序列中顺序有序):只需进行一趟起泡
“比较”的次数:n-1
“移动”的次数:0
最坏的情况(关键字在记录序列中逆序有序):需进行n-1趟起泡
“比较”的次数:
“移动”的次数:
时间复杂度:O(n²)
空间复杂度:O(1)
稳定性:稳定
快速排序
基本思想:任取待排序对象序列中的某个对象(常取第一个对象)作为枢轴,按照该对象的关键字大小,将整个对象序列划分为左右两个子序列:左侧子序列中所有对象的关键字都小于枢轴对象的关键字;右侧子序列中所有对象的关键字都大于枢轴对象的关键字。枢轴对象则排在这两个子序列中间(这也是该对象最终应安放的位置)。然后分别对这两个子序列重复施行上述方法,直到所有的对象都排在相应位置上为止。
例:对下列序列采用快速排序:{49,38,65,97,76,13,27,49}
首先对49之前的数据采用快速排序
然后对49之后的数据采用快速排序
void QSort (Sqlist &L,int low,int high)
{
if (low<high)
{
int pivotloc=Partition(L,low,high);
QSort(L,low,pivotloc-1);
QSort(L, pivotloc+1, high);
}
}
int partition(sqlist &L,int low,int high) 对从下标low到high的顺序表做一趟快速排序
{
pivotkey=L.r[low];
while(low<high)//将低下标设置为枢轴
{
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]=pivotkey;
return low;//返回值是枢轴的排序位置
}
算法分析:
时间复杂度:
平均情况:O()
最坏情况(每次总是选到最小或最大元素作枢轴):O(n²)
快速排序在所有同数量级(O(nlogn))的排序方法中,其平均性能最好。
空间复杂度:递归算法,需要一个辅助栈空间,最好与平均为O(),最坏O(n)
稳定性:快速排序为不稳定的排序算法。
选择排序
直接(简单)选择排序
基本思想:每一趟在n-i+1(i=1,2,...,n-1)个记录中选取关键字最小的记录作为有序序列中第i个记录。
其中最简单、且最熟悉的是简单选择排序。
排序过程:首先通过n-1次关键字比较,从n个记录中找出关键字最小的记录,将它与第一个记录交换,再通过n-2次比较,从剩余的n-1个记录中找出关键字次小的记录,将它与第二个记录交换。重复上述操作,共进行n-1趟排序后,排序结束。
void SelectSort (SqList &L)//对顺序表L作简单选择排序
{
for(int i=1;i<L.length;++i)//选择第i小的记录,并交换到位
{
//在L.r[i..L. 1ength]中选择key最小的记录
min=i ;
for(int j=i+1;j<=L.1ength;j++) if(L.r[j]<L.r[min]) min=j;
if(i!=min)//与第i个记录交换
{
temp=L.r[i];
L[i]=L.r[min];
L.r[min]=temp;
}
}
}
算法分析:
移动次数:最少0次,最多为3(n-1)次
比较次数:
时间复杂度:O(n²)
空间复杂度:O(1)。
稳定性:简单选择排序是不稳定的排序算法。
堆排序
堆的定义:n个元素的序列{k1,k2,...,kn}当且仅当满足下关系时,称之为堆。
例:{96,83,27,38,11,9}
例:{12,36,24,85,47,30,53}
基本思想:将无序序列建成一个堆,得到关键字最小(或最大)的记录;输出堆顶的最小(大)值后,使剩余的n-1个元素重又建成一个堆,则可得到n个元素的次小值;重复执行,得到一个有序序列,这个过程叫堆排序。
由此,实现堆排序需要解决两个问题:
(1)如何由一个无序序列建成一个堆?
(2)如何在输出堆顶元素之后,调整剩余元素成为一个新的堆?
第一个问题(初始建堆)的解决方法
从无序序列的第⌊n/2⌋个元素(即此无序序列对应的完全二叉树的最后一个非终端结点)起,至第一个元素止,进行反复筛选。
例:含8个元素的无序序列{49,38,65,97,76,13,27,50}
第二个问题解决方法——筛选
筛选方法:输出堆顶元素之后,以堆中最后一个元素替代之;然后将根结点值与左、右子树的根结点值进行比较,并与其中小者进行交换;重复上述操作,直至叶子结点,将得到新的堆,称这个从堆顶至叶子的调整过程为“筛选”。
算法评价:
时间复杂度:平均和最坏情况下为O(nlogn),相对于快速排序来说,这是堆排序的最大优点
空间复杂度:S(n)=O(1)
稳定性:不稳定
初始建堆所需比较次数较多,适用于n较大的情况。
归并排序
归并是将两个或两个以上的有序表合成一个新的有序表。用这种思想实现的排序称作归并排序。
假设初始序列有n个记录,可以看成是有n个有序的子序列,然后两两归并,得到n/2个有序的子序列,再两两归并,直到得到一个长度为n的有序序列为止,这种排序方法称为2-路归并排序。2-路归并排序是最简单和常用的归并排序。其核心操作是将一维数组中前后相邻的2个有序序列归并成为一个有序序列。
例:
将每一个元素看成一个有序表,两两归并。
算法分析:
在归并排序算法中,递归深度为O(),对象关键字的比较次数为O(
)。算法总的时间复杂度为O(
)。
归并排序占用附加存储较多,需要另外一个与原待排序对象数组同样大小的辅助数组,空间复杂度为O(n)。这是这个算法的缺点。
归并排序是一个稳定的排序方法。
各种排序方法的比较
排序方法 | 平均时间复杂度 | 最坏时间复杂度 | 辅助储存 | 稳定性 |
---|---|---|---|---|
简单排序 | O(n²) | O(n²) | O(1) | # |
快速排序 | O( | O(n²) | O( | 不稳定 |
堆排序 | O( | O( | O(1) | 不稳定 |
归并排序 | O( | O( | O(n) | 稳定 |
①简单排序包括直接插入排序(稳定),冒泡排序(稳定),简单选择排序(不稳定);
②快速排序在最坏情况下退变成冒泡排序;
③堆排序平均时间和最坏情况变化不大,辅助空间也少。