目录
10.1排序的概述
排序的定义
将一批任意序列的数据元素(或记录),按关键字有序重新排列成一个新的序列叫排序,从小到大。
2. 排序的目的是什么? ——便于查找!
3. 什么叫内部排序?什么叫外部排序?
•若待排序记录都在内存中,称为内部排序;
•若待排序记录一部分在内存,一部分在外存,则称为外部排序。
注:外部排序时,要将数据分批调入内存来排序,中间结果还要及时放入外存,显然外部排序要复杂得多。
4.排序算法的好坏如何衡量?
•时间效率——排序速度(比较次数与移动次数)
•空间效率——占内存辅助空间的大小
•稳定性——A和B的关键字相等,排序后A、B的先后次序保持不变,则称这种排序算法是稳定的。
排序的分类
Ø插入排序:直接插入排序、折半插入排序、希尔排序
Ø交换排序:冒泡排序、快速排序
Ø选择排序:简单选择排序、堆排序
Ø归并排序:2-路归并排序
Ø基数排序
排序的操作
Ø比较:两个关键字大小
Ø移动:将记录从一个位置移动到另一个位置
10.2插入排序
基本思想: 每步将一个待排序的对象,按其关键码大小,插入到前面已经排好序的一组对象的适当位置上,直到对象全部插入为止。
即边插入边排序,保证子序列中随时都是排好序的
直接插入排序(基于顺序查找)
基本过程
第i次插入的数是a[i],这时数组a中的a[0]~a[i-1]已经有序了,将a[i]插入到数组a中的合适位置。插入a[i]的过程,分为3个步骤:
Ø从a[i-1]开始,在a[0]~a[i-1]范围内找第一个小于a[i]的数,其位置记为pos
Ø从a[i-1]开始,将a[i-1]~a[pos]依次后移一个位置
Ø将a[i]放置在a[pos]位置上,替换a[pos]
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
#include <iostream>
#include <map>
#include <vector>
#include <queue>
#include <set>
#define eps 1e-8
const double PI = acos(-1.0);
const int maxn = 1e5;
const int INF = 1e9;
using namespace std;
void InsertionSort(int A[],int n)
{
int preidx,curr;
for(int i = 1; i<n; i++)
{
preidx = i-1;
curr = A[i];
while(preidx>=0 && A[preidx]>curr)
{
A[preidx+1] = A[preidx];
preidx--;
}
A[preidx+1] = curr;
}
}
int main()
{
int n;
scanf("%d",&n);
int A[n+5];
for(int i = 0; i<n; i++)
scanf("%d",&A[i]);
InsertionSort(A,n);
for(int i = 0; i<n; i++)
printf("%d ",A[i]);
printf("\n");
return 0;
}
算法分析
•设对象个数为n,则执行n-1趟
•比较次数和移动次数与初始排列有关
•最好情况下:
Ø每趟只需比较 1 次,不移动
Ø总比较次数为 n-1
最坏情况下:第 i 趟比较 i 次,移动 i+1 次
•若出现各种可能排列的概率相同,则可取最好情况和最坏情况的平均情况
•平均情况比较次数和移动次数为(n^2)/4
Ø时间复杂度为 O(n2)
Ø空间复杂度为 O(1)
Ø是一种稳定的排序方法
小结
Ø排序趟数:n个数——n-1趟
Ø比较次数、移动次数——与对象排序码的初始排列有关
Ø比较次数最少n-1次,移动次数最少没有移动
Ø比较次数最多(n+2)(n-1)/2次,移动次数最多(n+4)(n-1)/2
Ø平均比较次数和移动次数——n2/4,时间复杂度为 O(n2)
Ø插入排序的稳定性——稳定
折半插入排序
•在插入 r[i] 时,利用折半查找法寻找 r[i] 的插入位置
void BInsertSort(int A[])
{
int curr;
for(int i = 1; i<10; i++)
{
int low = 0;
int high = i-1;
curr = A[i];
while(low<=high)
{
int mid = (low+high)/2;
if(curr<A[mid])
high = mid-1;
else
low = mid+1;
}
for(int j = i-1; j>=high+1; j--)
A[j+1] = A[j];
A[high+1] = curr;
}
}
算法分析
•折半查找比顺序查找快,所以折半插入排序就平均性能来说比直接插入排序要快
•它所需要的关键码比较次数与待排序对象序列的初始排列无关,仅依赖于对象个数。在插入第 i 个对象时,需要经过 log2i +1 次关键码比较,才能确定它应插入的位置
•当 n 较大时,总关键码比较次数比直接插入排序的最坏情况要好得多,但比其最好情况要差
•在对象的初始排列已经按关键码排好序或接近有序时,直接插入排序比折半插入排序执行的关键码比较次数要少
•折半插入排序的对象移动次数与直接插入排序相同,依赖于对象的初始排列
•减少了比较次数,但没有减少移动次数
•平均性能优于直接插入排序
Ø时间复杂度为 O(n2)
Ø空间复杂度为 O(1)
Ø是一种稳定的排序方法
希尔排序
基本思想
先将整个待排记录序列分割成若干子序列,分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行一次直接插入排序。
子序列的构成不是简单地“逐段分割”,将相隔某个增量dk的记录组成一个子序列,让增量dk逐趟缩短(例如依次取5,3,1)。直到dk=1为止。
优点:小元素跳跃式前移,最后一趟增量为1时,序列已基本有序,平均性能优于直接插入排序
- dk 值较大,子序列中对象较少,速度较快;
- dk 值逐渐变小,子序列中对象变多,但大多数对象已基本有序,所以排序速度仍然很快。
void shellSort(int A[],int len)
{
int i,j;
int increment = len;//增量
int key;
while(increment > 1)//最后在增量为1并且是执行了情况下停止。
{
increment = increment/3 + 1;//根据公式
printf("increment:%d\n",increment);
for (i = increment; i<len; i++)//从[0]开始,对相距增量步长的元素集合进行修改。
{
key = A[i];
//以下和直接插入排序类似。
j = i-increment;
while(j>=0)
{
if (key<A[j] )
{
int temp = A[j];
A[j] = key;
A[j+increment] = temp;
}
j = j-increment;
}
}
}
}
算法分析
•时间复杂度是n和d的函数:O(n^1.25)~O(1.6n^1.25)—经验公式
•空间复杂度为 O(1)
•是一种不稳定的排序方法
如何选择最佳d序列,目前尚未解决
Ø最后一个增量值必须为1,无除1以外的公因子
Ø不宜在链式存储结构上实现
10.3交换排序
基本思想
两两比较,如果发生逆序则交换,直到所有记录都排好序为止。
冒泡排序
两两比较待排序数据元素的大小,发现两个数据元素的次序相反时即进行交换,直到没有反序的数据元素为止。
冒泡排序步骤
比较第0个数与第1个数,若为逆序,即a[0]>a[1],则交换;然后比较第1个数与第2个数;以此类推,直至第n-2个数和第n-1个数比较完为止,即第0趟冒泡排序结束。第0趟排序的结果是最大的数被安置在最后一个元素位置上。
对前n-1个数进行第1趟冒泡排序,结果使次大的数被安置在第n-1个元素位置。
如此重复上述过程,共经过n-1趟冒泡排序后,排序结束。
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
#include <iostream>
#include <map>
#include <vector>
#include <queue>
#include <set>
#define eps 1e-8
const double PI = acos(-1.0);
const int maxn = 1e5;
const int INF = 1e9;
using namespace std;
void BubbleSort(int A[],int n)
{
int t;
int flag = 1;
for(int i = 0; i<n-1&&flag; i++)
{
flag = 0;
for(int j = 0; j<n-1-i; j++)
{
if(A[j]>A[j+1])
{
t = A[j];
A[j] = A[j+1];
A[j+1] = t;
flag = 1;
}
}
}
}
int main()
{
int n;
scanf("%d",&n);
int A[n+5];
for(int i = 0; i<n; i++)
scanf("%d",&A[i]);
BubbleSort(A,n);
for(int i = 0; i<n; i++)
printf("%d ",A[i]);
printf("\n");
return 0;
}
算法分析
•设对象个数为n
•比较次数和移动次数与初始排列有关
最好情况下:
•只需 1趟排序,比较次数为 n-1,不移动
最坏情况下:
•需 n-1趟排序,第i趟比较n-i次,移动3(n-i)次
•时间复杂度为 O(n2)
•空间复杂度为 O(1)
•是一种稳定的排序方法
快速排序
基本思想
•任取一个元素 (如第一个) 为中心
•所有比它小的元素一律前放,比它大的元素一律后放,形成左右两个子表;
•对各子表重新选择中心元素并依此规则调整,直到每个子表的元素只剩一个
①每一趟的子表的形成是采用从两头向中间交替式逼近法;
②由于每趟中对各子表的操作都相似,可采用递归算法。
void quickSort(int a[],int st,int ed)
{
if(ed - st <= 0)
return;
int key =a[st];
int low = st;
int high = ed;
while(low != high)
{
while(low<high && a[high]>=key)
high--;
while(low<high && a[low]<=key)
low++;
if(low<high)
{
int t = a[low];
a[low] = a[high];
a[high] = t;
}
}
a[st] = a[low];
a[low] = key;
quickSort(a,st,low-1);
quickSort(a,low+1,ed);
}
算法分析
•可以证明,平均计算时间是O(nlog2n)。
•实验结果表明:就平均计算时间而言,快速排序是我们所讨论的所有内排序方法中最好的一个。
•快速排序是递归的,需要有一个栈存放每层递归调用时参数(新的low和high)。
•最大递归调用层次数与递归树的深度一致,因此,要求存储开销为 O(log2n) 。
•最好:划分后,左侧右侧子序列的长度相同
•最坏:从小到大排好序,递归树成为单支树,每次划分只得到一个比上一次少一个对象的子序列,必须经过 n-1 趟才能把所有对象定位,而且第 i 趟需要经过 n-i 次关键码比较才能找到第 i 个对象的安放位置
时间效率:O(nlog2n) —每趟确定的元素呈指数增加
空间效率:O(log2n)—递归要用到栈空间
稳 定 性: 不稳定 —可选任一元素为支点。
10.4选择排序
基本思想
每一趟在后面 n-i +1个中选出关键码最小的对象, 作为有序序列的第 i 个记录
简单选择排序
void SelectionSort(int A[],int n)
{
int minidx,t;
for(int i = 0; i<n-1; i++)
{
minidx = i;
for(int j = i+1; j<n; j++)
{
if(A[minidx]>A[j])
{
minidx = j;
}
}
t = A[i];
A[i] = A[minidx];
A[minidx] = t;
}
}
算法分析
移动次数
最好情况:0;最坏情况:3(n-1)
比较次数:
时间复杂度:O(n²) 空间复杂度:O(1) 不稳定
树形选择排序
• 改进:简单选择排序没有利用上次选择的结果,是造成速度满的重要原因。如果,能够加以改进,将会提高排序的速度。
堆排序
n个元素的序列{k1,k2,…,kn},当且仅当满足下列关系时,成为堆
–如果将序列看成一个完全二叉树,非终端结点的值均小于或大于左右子结点的值。
–利用树的结构特征来描述堆,所以树只是作为堆的描述工具,堆实际是存放在线形空间中的。
基本思想
- 将无序序列建成一个堆
- 输出堆顶的最小(大)值
- 使剩余的n-1个元素又调整成一个堆,则可得到n个元素的次小值
- 重复执行,得到一个有序序列
堆的重新调整
- 输出堆顶元素后,以堆中最后一个元素替代之
- 将根结点与左、右子树根结点比较,并与小者交换
- 重复直至叶子结点,得到新的堆
算法分析
时间效率:O(nlog2n) 空间效率:O(1) 稳 定 性:不稳定 适用于n 较大的情况
归并排序
归并:将两个或两个以上的有序表组合成一个新有序表
2-路归并排序
排序过程
- 初始序列看成n个有序子序列,每个子序列长度为1
- 两两合并,得到n/2个长度为2或1的有序子序列
- 再两两合并,重复直至得到一个长度为n的有序序列为止
算法分析
时间效率:O(nlog2n) 空间效率:O(n) 稳 定 性:稳定
10.5基数排序
前面的排序方法主要通过关键字值之间的比较和移动;而基数排序不需要关键字之间的比较
多关键字排序:
最高位优先MSD ( Most Significant Digit first )
最低位优先LSD ( Least Significant Digit first)
链式基数排序
链式基数排序:用链表作存储结构的基数排序
最高位优先法
Ø先对最高位关键字k1(如花色)排序,将序列分成若干子序列,每个子序列有相同的k1值;
Ø然后让每个子序列对次关键字k2(如面值)排序,又分成若干更小的子序列;
Ø依次重复,直至就每个子序列对最低位关键字kd排序,就可以得到一个有序的序列。
十进制数比较可以看作是一个多关键字排序
最低位优先法
首先依据最低位排序码Kd对所有对象进行一趟排序
再依据次低位排序码Kd-1对上一趟排序结果排序
依次重复,直到依据排序码K1最后一趟排序完成,就可以得到一个有序的序列。
这种方法不需要再分组,而是整个对象组都参加排序
链式基数排序
–先决条件:
•知道各级关键字的主次关系
•知道各级关键字的取值范围
利用“分配”和“收集”对关键字进行排序
–过程
•首先对低位关键字排序,各个记录按照此位关键字的值‘分配’到相应的序列里。
•按照序列对应的值的大小,从各个序列中将记录‘收集’,收集后的序列按照此位关键字有序。
•在此基础上,对前一位关键字进行排序。
链式基数排序步骤
–设置10个队列,f[i]和e[i]分别头指针和尾指针
–第一趟分配对最低位关键字(个位)进行,改变记录的指针值,将链表中记录分配至10个链队列中,每个队列记录的关键字的个位相同
–第一趟收集是改变所有非空队列的队尾记录的指针域,令其指向下一个非空队列的队头记录,重新将10个队列链成一个链表
–重复上述两步,进行第二趟、第三趟分配和收集,分别对十位、百位进行,最后得到一个有序序列
算法分析
n个记录 每个记录有 d 位关键字 关键字取值范围rd(如十进制为10)
- 重复执行d趟“分配”与“收集”
- 每趟对 n 个记录进行“分配”,对rd个队列进行“收集”
- 需要增加n+2rd个附加链接指针。
时间效率:O(d( n+rd)) 空间效率:O(n+rd) 稳 定 性:稳定
小结
(数据不是顺次后移时将导致方法不稳定)
排序算法比较
•按平均时间排序方法分为四类
O(n2)、O(nlogn)、O(n1+e)、O(n)
•快速排序是基于比较的内部排序中平均性能最好的
•基数排序时间复杂度最低,但对关键字结构有要求(知道各级关键字的主次关系,知道各级关键字的取值范围)
为避免顺序存储时大量移动记录的时间开销,可考虑用链表作为存储结构
直接插入排序、归并排序、基数排序
不宜采用链表作为存储结构的
折半插入排序、希尔排序、快速排序、堆排序
排序算法选择规则
n较大时:
(1)分布随机,稳定性不做要求,则采用快速排序
(2)内存允许,要求排序稳定时,则采用归并排序
(3)可能会出现正序或逆序,稳定性不做要求,则采用堆排序或归并排序
n较小时:
(1)基本有序,则采用直接插入排序
(2)分布随机,则采用简单选择排序,若排序码不接近逆序,也可以采用直接插入排序