第8天排序
7.1 概 述
7.2 插入排序
7.3 快速排序
7.4 堆排序
7.5 归并排序
7.6 基数排序
7.7 各种排序方法的综合比较
7.1 概 述
一、排序的定义
二、内部排序和外部排序
三、内部排序方法的分类
内部排序和外部排序
若整个排序过程不需要访问外存便能完成,则称此类排序问题为内部排序;
反之,若参加排序的记录数量很大, 整个序列的排序过程不可能在内存中 完成,则称此类排序问题为外部排序。
三、内部排序的方法
内部排序的过程是一个逐步扩大 记录的有序序列长度的过程。
基于不同的“扩大” 有序序列长度的方法,内部排序方法大致可分下列几种类型:
插入类 交换类 选择类 归并类
//插入类: (1)直接插入排序 (2)折半插入排序 (3)希尔排序
//交换类; (1)冒泡排序 (2)一趟快速排序 (3)快速排序
//选择类: (1)简单选择排序 (2) 堆排序
//归并类: (1)归并排序
待排记录的数据类型定义如下:
#define MAXSIZE 1000 // 待排顺序表最大长度
typedef int KeyType; // 关键字类型为整数类型
typedef struct {
KeyType key; // 关键字项
InfoType otherinfo; // 其它数据项
} RcdType; // 记录类型
typedef struct {
RcdType r[MAXSIZE+1]; // r[0]闲置
int length; // 顺序表长度
} SqList; // 顺序表类型
1. 插入类
将无序子序列中的一个或几个记录“插入”到有序序列中,从而增加记录的有序子序列的长度。
2. 交换类
通过“交换”无序序列中的记录从而得到其中关键字最小或最大的记录,并将它加入到有序子序列中,以此方法增加记录的有序子序列的长度。
3. 选择类
从记录的无序子序列中“选择”关键字最小或最大的记录,并将它加入到有序子序列中,以此方法增加记录的有序子序列的长度。
4. 归并类
通过“归并”两个或两个以上的记录有序子序列,逐步增加记录有序序列的长度。
7. 2 插 入 排 序
实现“一趟插入排序”可分三步进行:
1.在R[1..i-1]中查找R[i]的插入位置, R[1..j].key <= R[i].key < R[j+1..i-1].key;
2.将R[j+1..i-1]中的所有记录均后移一个位置;
3.将R[i] 插入(复制)到R[j+1]的位置上。
不同的具体实现方法 导致不同的算法描述
直接插入排序(基于顺序查找)
折半插入排序(基于折半查找)
希尔排序(基于逐趟缩小增量)
一、直接插入排序
利用 “顺序查找”实现 “在R[1..i-1]中查找R[i]的插入位置”
算法的实现要点:
从R[i-1]起向前进行顺序查找,监视哨设置在R[0];
R[0] = R[i]; // 设置“哨兵
for (j=i-1; R[0].key<R[j].key; --j); // 从后往前找
循环结束表明R[i]的插入位置为 j +1
对于在查找过程中找到的那些关键字不小于R[i].key的记录,并在查找的同时实现记录向后移动;
for (j=i-1; R[0].key<R[j].key; --j);
R[j+1] = R[j]
上述循环结束后可以直接进行“插入
void InsertionSort ( SqList &L ) {
// 对顺序表 L 作直接插入排序。
for ( i=2; i<=L.length; ++i )
if (L.r[i].key < L.r[i-1].key)
{
L.r[0] = L.r[i]; // 复制为监视哨
for ( j=i-1; L.r[0].key < L.r[j].key; -- j )
L.r[j+1] = L.r[j]; // 记录后移
L.r[j+1] = L.r[0]; // 插入到正确位
}
} // InsertSort
内部排序的时间分析:
实现内部排序的基本操作有两个:
1)“比较”序列中两个关键字的 大小;
2)“移动”记录。
二、折半插入排序
因为 R[1..i-1] 是一个按关键字有序的有序序列,则可以利用折半查找实现
“在R[1..i-1]中查找R[i]的插入位置”,如此实现的插入排序为折半插入排序。
void BiInsertionSort ( SqList &L ) {
for ( i=2; i<=L.length; ++i ) {
L.r[0] = L.r[i]; // 将 L.r[i] 暂存到 L.r[0]
// L.r[1..i-1]中折半查找插入位置
for ( j=i-1; j>=high+1; --j )
L.r[j+1] = L.r[j]; // 记录后移
L.r[high+1] = L.r[0]; // 插入
} // for
} // BInsertSort
low = 1; high = i-1;
while (low<=high) {
m = (low+high)/2; // 折半
if (L.r[0].key < L.r[m].key)
high = m-1; // 插入点在低半区
else low = m+1; // 插入点在高半区
}
三、希尔排序 (又称缩小增量排序)
基本思想:对待排记录序列先作“宏观”调整,再作“微观”调整。
所谓“宏观”调整,指的是,“跳跃式”的插入排序。 具体做法为:
将记录序列分成若干子序列, 分别对每个子序列进行插入排序。
例如:将 n 个记录分成 d 个子序列:
{ R[1],R[1+d],R[1+2d],…,R[1+kd] }
{ R[2],R[2+d],R[2+2d],…,R[2+kd] }
…
{ R[d],R[2d],R[3d],…,R[kd],R[(k+1)d] }
其中,d 称为增量,它的值在排序过程中从大到小逐渐缩小,
直至最后一趟排序减为 1。
void ShellInsert ( SqList &L, int dk ) {
for ( i=dk+1; i<=n; ++i )
if ( L.r[i].key< L.r[i-dk].key) {
L.r[0] = L.r[i]; // 暂存在R[0]
for (j=i-dk; j>0&&(L.r[0].key<L.r[j].key);j-=dk)
L.r[j+dk] = L.r[j]; // 记录后移,查找插入位置
L.r[j+dk] = L.r[0]; // 插入
} // if
} // ShellInsert
7.3 快 速 排 序
一、起泡排序
二、一趟快速排序
三、快速排序
一、起泡排序
void BubbleSort(Elem R[ ], int n) {
i = n;
while (i >1)
{
lastExchangeIndex = 1;
for (j = 1; j < i; j++)
if (R[j+1].key < R[j].key)
{
Swap(R[j], R[j+1]);
lastExchangeIndex = j; //记下进行交换的记录位置
} //if
i = lastExchangeIndex; // 本趟进行过交换的
} // while // 最后一个记录的位置
} // BubbleSort
二、一趟快速排序(一次划分)
目标:找一个记录,以它的关键字作为“枢轴”,凡其关键字小于枢轴的记录均移动至该记录之前,
反之,凡关键字大于枢轴的记录均移动至该记录之后。
致使一趟排序之后,记录的无序序列R[s..t]将分割成两部分:
R[s..i-1]和R[i+1..t],且 R[j].key≤ R[i].key ≤ R[j].key (s≤j≤i-1) 枢轴 (i+1≤j≤t)。
int Partition (RedType R[], int low, int high) {
R[0] = R[low];
pivotkey = R[low].key; // 枢轴
while (low<high)
{
while(low<high&& R[high].key>=pivotkey)
-- high; // 从右向左搜索
R[low] = R[high];
while (low<high && R[low].key<=pivotkey)
++ low; // 从左向右搜索
R[high] = R[low];
}
R[low] = R[0];
return low;
}// Partition
三、快速排序
先对无序的记录序列进行“一次划分”,之后分别对分割所得两个子序列“递归”进行快速排序。
结论: 快速排序的时间复杂度为O(nlogn),若待排记录的初始状态为按关键字有序时,快速排序将蜕化为起泡排序,其时间复杂度为O(n2)。
void QSort (RedType & R[], int s, int t ) { // 对记录序列R[s..t]进行快速排序
if (s < t-1) // 长度大于1
{
pivotloc = Partition(R, s, t);// 对 R[s..t] 进行一次划分
QSort(R, s, pivotloc-1);// 对低子序列递归排序,pivotloc是枢轴位置
QSort(R, pivotloc+1, t); // 对高子序列递归排序
}
}
假设一次划分所得枢轴位置 i=k,则对n 个记录进行快排所需时间:
快速排序的时间复杂度为O(nlogn),若待排记录的初始状态为按关键字有序时,快速排序将蜕化为起泡排序,其时间复杂度为O(n2)。
10.4 堆 排 序
简 单 选 择 排 序
堆 排 序
一、简单选择排序
void SelectSort (Elem R[], int n ) { // 对记录序列R[1..n]作简单选择排序。
for (i=1; i<n; ++i)
{
// 选择第 i 小的记录,并交换到位
j = SelectMinKey(R, i);
// 在 R[i..n] 中选择关键字最小的记录
if (i!=j) R[i]←→R[j];
// 与第 i 个记录交换
}
} // SelectSort
二:堆排序
{12, 36, 27, 65, 40, 34, 98, 81, 73, 55, 49} 是小顶堆
{12, 36, 27, 65, 40, 14, 98, 81, 73, 55, 49} 不是小顶堆
堆排序的时间复杂度为O(nlogn)。
void HeapSort ( HeapType &H ) {
// 对顺序表 H 进行堆排序
for ( i=H.length/2; i>0; --i )
HeapAdjust ( H.r, i, H.length ); // 建大顶堆
for ( i=H.length; i>1; --i )
{
H.r[1]←→H.r[i];
// 将堆顶记录和当前未经排序子序列
// H.r[1..i]中最后一个记录相互交换
HeapAdjust(H.r, 1, i-1); // 对 H.r[1] 进行筛选
}
} // HeapSort
void HeapAdjust (RcdType &R[], int s, int m)
{ // 已知 R[s..m]中记录的关键字除 R[s] 之外均
// 满足堆的特征,本函数自上而下调整 R[s] 的
// 关键字,使 R[s..m] 也成为一个大顶堆
rc = R[s]; // 暂存 R[s]
for ( j=2*s; j<=m; j*=2 )
{ // j 初值指向左孩子自上而下的筛选过程;
if ( j<m && R[j].key<R[j+1].key ) ++j;
// 左/右“子树根”之间先进行相互比较
// 令 j 指示关键字较大记录的位置
if ( rc.key >= R[j].key ) break;
// 再作“根”和“子树根”之间的比较,
// 若“>=”成立,则说明已找到 rc 的插
// 入位置 s ,不需要继续往下调整
R[s] = R[j]; s = j;
// 否则记录上移,尚需继续往下调整
}
R[s] = rc; // 将调整前的堆顶记录插入到 s 位置
} // HeapAdjust
7.5 归 并 排 序
归并排序的过程基于下列基本思想进行: 将两个或两个以上的有序子序列 “归并” 为一个有序序列。
在内部排序中,通常采用的是2-路归并排序。即:将两个位置相邻的记录有序子序列 归并为一个记录的有序序列。
对 n 个记录进行归并排序的时间复杂度为Ο(nlogn)
void Merge (RcdType SR[], RcdType &TR[], int i, int m, int n) {
// 将有序的记录序列 SR[i..m] 和 SR[m+1..n]
// 归并为有序的记录序列 TR[i..n]
for (j=m+1, k=i; i<=m && j<=n; ++k) // 将SR中记录由小到大地并入TR
{
if (SR[i].key<=SR[j].key)
TR[k] = SR[i++];
else TR[k] = SR[j++];
}
if (i<=m)
TR[k..n] = SR[i..m];
// 将剩余的 SR[i..m] 复制到 TR
if (j<=n)
TR[k..n] = SR[j..n];
// 将剩余的 SR[j..n] 复制到 TR
} // Merge
7.7 各种排序方法的综合比较
1. 平均的时间性能
时间复杂度为 O(nlogn):快速排序、堆排序和归并排序
时间复杂度为 O(n2):直接插入排序、起泡排序和 简单选择排序
时间复杂度为 O(n): 基数排序
2. 当待排记录序列按关键字顺序有序时
直接插入排序和起泡排序能达到O(n)的时间复杂度,
快速排序的时间性能从O(nlogn),蜕化为O(n2) 。
3. 简单选择排序、堆排序和归并排序的时间性能不随记录序列中关键字的分布而改变。
4. 快速排序、堆排序和希尔排序是不稳定的排序方法。
//插入类: (1)直接插入排序 (2)折半插入排序 (3)希尔排序
//交换类; (1)冒泡排序 (2)一趟快速排序 (3)快速排序
//选择类: (1)简单选择排序 (2) 堆排序
//归并类: (1)归并排序
特别关注:快速排序
快速排序 :先对无序的记录序列进行“一次划分”,之后分别对分割所得两个子序列“递归”进行快速排序。
结论: 快速排序的时间复杂度为O(nlogn),若待排记录的初始状态为按关键字有序时,快速排序将蜕化为起泡排序,其时间复杂度为O(n2)。
掌握各种排序方法的时间复杂度的分析方法。能从“关键字间的比较次数”分析排序算法的平均情况和最坏情况的时间性能。
按平均时间复杂度划分,内部排序可分为三类:
O(n2)的简单排序方法,
O(nlogn)的高效排序方法
O(dn)的基数排序方法。
厚积薄发,行胜于言@飞鸟各投林