数据结构——排序
一、排序的基本概念
排序: 将一个数据元素的任意序列重新排列成一个按关键字有序的序列。 假设n个元素的序列{
R
1
\displaystyle R_1
R1?,
R
2
\displaystyle R_2
R2?,…,
R
n
\displaystyle R_n
Rn?},相应的关键字序列为{
k
1
\displaystyle k_1
k1?,
k
2
\displaystyle k_2
k2?,…,
k
n
\displaystyle k_n
kn?},确定1,2,…,n的一种排列
p
1
\displaystyle p_1
p1?,
p
2
\displaystyle p_2
p2?,…,
p
n
\displaystyle p_n
pn? ,使相应的关键字满足非递减(或非递增)关系
k
p
1
,
k
p
2
,
.
.
.
,
k
p
n
\displaystyle k_{p1},k_{p2},...,k_{pn}
kp1?,kp2?,...,kpn?,从而得到一个按关键字有序的序列。这样的一个操作过程就是排序。
稳定的排序: 若
k
i
=
k
j
\displaystyle k_i=k_j
ki?=kj?,且在排序前的序列中
R
i
\displaystyle R_i
Ri?领先于
R
j
\displaystyle R_j
Rj?,排序后
R
i
\displaystyle R_i
Ri?仍领先于
R
j
\displaystyle R_j
Rj?,则称所用的排序方法是稳定的;反之,若可能使排序后的序列中
R
j
\displaystyle R_j
Rj?仍领先于
R
i
\displaystyle R_i
Ri?,则称所用的排序方法是不稳定的。
内部排序: 待排序的记录存放在计算机的内存中所进行的排序操作称为内部排序。
外部排序: 待排序的记录数量很大,以致内存一次不能容纳全部记录,在排序过程中需要访问外存的排序过程称为外部排序。
正序与逆序: 若有序表是按排序码升序排列的,则称为升序表或正序表,否则称为降序表或逆序表。不失普遍性,我们一般只讨论正序表。
排序方法度量: 排序过程主要是比较记录的关键字和移动记录。 因此排序的时间复杂性可以算法执行中的数据比较次数及数据移动次数来衡量。 当一种排序方法使排序过程在最坏或平均情况下所进行的比较和移动次数越少,则认为该方法的时间复杂性就越好。 针对一种排序方法,不仅要分析它的时间复杂性,而且要分析它的空间复杂性、稳定性和简单性等。
二、内部排序
1. 插入排序
(1) 直接插入排序 ● 基本思想: n个待排序的元素由一个有序表和一个无序表组成,开始时有序表中只包含一个元素。排序过程中,每次从无序表中取出第一个元素,将其插入到有序表中的适当位置,使有序表的长度不断加长,完成排序过程。
void InsertSort(SqList &L){
int i;
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[j];
L.r[j+1] = L.r[0];
}
}
}
●复杂度: 最好情况(正序):元素比较次数为n-1,元素移动次数为0 最坏情况(逆序):元素比较次数为
(
n
+
2
)
(
n
?
1
)
2
\displaystyle \frac{(n+2)(n-1)}{2}
2(n+2)(n?1)?,元素移动次数为
(
n
+
4
)
(
n
?
1
)
2
\displaystyle \frac{(n+4)(n-1)}{2}
2(n+4)(n?1)? 时间复杂度:
O
(
n
2
)
\displaystyle O(n^2)
O(n2) 空间复杂度:
O
(
1
)
\displaystyle O(1)
O(1) 稳定的排序方法 适用情况:元素数目少,或者元素的初始序列基本有序。 ①元素基本有序时,直接插入排序的时间复杂度接近于O(n) ②元素数目较少时,直接插入排序效率较高
(2) 折半插入排序 ● 基本思想: 在寻找插入位置时采用二分查找,则称为折半插入排序。
void BInsertSort(SqList &L)
{
for(i=2; i <= L.length; i++)
{
L.r[0] = L.r[i];
low = 1;
high = i-1;
while(low <= high)
{
mid = (low+high)/2;
if (L.r[0].key > L.r[mid].key)
low = mid+1;
else high = mid-1;
}
for(j = i-1; j >= high+1; j--)
{
L.r[j+1] = L.r[j];
}
L.r[high+1] = L.r[0];
}
}
(3) 希尔排序(缩小增量排序) ●算法思想: 先将整个待排序元素序列分割成若干个子序列(由相隔某个“增量”的元素组成的),分别进行直接插入排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。
void ShellSort(SqList &L,int dlta[],int t){
for(k = 0; k < t; k++)
ShellInsert(L,dlta[k]);
}
void ShellInsert(SqList &L, int dk){
for(i = dk+1; i <= L.length; i++)
{
if(L.r[i].key < L.r[i-dk].key)
{
L.r[0] = L.r[i];
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];
}
}
}
●复杂度: 希尔排序的分析是一个复杂问题,增量序列的设置是关键,尚没有正式的依据说明如何设置最佳的增量序列,大量的实验表明希尔排序所需的比较和移动次数可达到
n
1.3
\displaystyle n^{1.3}
n1.3 时间复杂度:
O
(
n
1.3
)
\displaystyle O(n^{1.3})
O(n1.3) 空间复杂度:
O
(
1
)
\displaystyle O(1)
O(1) 不稳定的排序方法
2. 交换排序
(1) 冒泡排序 (以顺序表作为存储结构) ●基本思想:将相邻位置的关键字进行比较,若为逆序则交换之。 若在一趟排序过程中没有进行过交换记录的操作,则整个排序过程终止。
void BubbleSort(SqList &L){
for(i = 1, change = TRUE; i < L.length && change; i++)
{
change = FALSE;
for(j = 1; j < L.length-i+1; ++j)
{
if(L.r[j].key > L.r[j+1].key){
L.r[0] = L.r[j];
L.r[j] = L.r[j+1];
L.r[j+1] = L.r[0];
change = TRUE;
}
}
}
}
●复杂度: 最好情况:元素比较次数为n-1,元素移动次数为0 最坏情况:元素比较次数为
n
(
n
?
1
)
2
\displaystyle \frac{n(n-1)}{2}
2n(n?1)?,元素移动次数为
n
(
n
?
1
)
2
\displaystyle \frac{n(n-1)}{2}
2n(n?1)? 时间复杂度:
O
(
n
2
)
\displaystyle O(n^2)
O(n2) 空间复杂度:
O
(
1
)
\displaystyle O(1)
O(1) 稳定的排序方法 适用情况:元素数目少,或者元素的初始序列基本有序。
(2) 快速排序 (是迄今为止所有内,排序算法中速度最快的一种) ●基本思想: 通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序的目的。 取待排序序列中的某个元素作为基准(一般取第一个元素),通过一趟排序,将待排元素分为左右两个子序列,左子序列元素的关键字均小于或等于基准元素的关键字,右子序列的关键字则大于基准元素的关键字,然后分别对两个子序列继续进行排序,直至整个序列有序。
int Partition(SqList &L,int low,int high) {
pivotkey = L.r[low].key; i = low; j = high;
while (i<j) {
while (i<j && L.r[j].key >= pivotkey)
j--;
L.r[i] ←→ L.r[j];
while (i<j && L.r[i].key <= pivotkey)
i++;
L.r[j] ←→ L.r[i];
}
return i;
}
int Partition(SqList &L, int low, int high){
L.r[0] = L.r[low];
pivotkey = L.r[low].key;
while(low < high){
while(low < high && L.r[high].key >= pivotkey)
--high;
L.r[low] = L.r[high];
while(low < high && L.r[high].key <= pivotkey)
++low;
L.r[high] = L.r[low];
}
L.r[low] = L.r[0];
return low;
}
void QSort(SqList &L, int low, int high){
if(low < high){
pivotloc = Partition(L, low, high);
QSort(L, low, pivotloc - 1);
QSort(L, pivotloc + 1, high);
}
}
void QuickSort(SqList &L){
QSort(L, 1, L.length);
}
●复杂度: 快速排序是对冒泡排序的一种改进方法,算法中元素的比较和交换是从两端向中间进行的,排序码较大的元素一次就能够交换到后面的单元,排序码较小的记录一次就能够交换到前面的单元,记录每次移动的距离较远,因而总的比较和移动次数较少。 时间复杂度:
O
(
n
l
o
g
2
n
)
\displaystyle O(nlog_2n)
O(nlog2?n) ①最好情况:
O
(
n
l
o
g
2
n
)
\displaystyle O(nlog_2n)
O(nlog2?n) ②最坏情况(逆序或正序):
O
(
n
2
)
\displaystyle O(n^2)
O(n2) 空间复杂度:
O
(
l
o
g
2
n
)
\displaystyle O(log_2n)
O(log2?n) 不稳定的排序方法 不适合对小规模的序列进行排序。
●快速排序方法的改进: 枢轴元素的选取:①三者取中;②随机选择 划分的过程中进行 “起泡” 操作 当划分出的子序列长度小于某个值时,不再递归,而进行直接插入排序 快速排序算法被认为是内部排序方法中最好的一种
3. 选择排序
(1) 简单选择排序 ●基本思想: 第一趟在n个记录中选取最小记录作为有序序列的第一个记录,第二趟在n-1个记录中选取最小记录作为有序序列的第二个记录,第 i 趟在n-i+1个记录中选取最小的记录作为有序序列多中的第i个记录。
void SelectSort(SqList &L){
for(i = 1; i < L.length; i++){
for(j = i + 1, k = i; j <= L.length; j++){
if(L.r[j].key < L.r[k].key)
k = j;
}
}
if(j != i){
L.r[i] ←→ L.r[j];
}
}
●复杂度: 逆序情况:元素比较次数为
n
(
n
?
1
)
2
\displaystyle \frac{n(n-1)}{2}
2n(n?1)?,元素移动次数为
3
(
n
?
1
)
2
\displaystyle \frac{3(n-1)}{2}
23(n?1)? 顺序情况:元素比较次数为
n
(
n
?
1
)
2
\displaystyle \frac{n(n-1)}{2}
2n(n?1)?,元素移动次数为0 时间复杂度:
O
(
n
2
)
\displaystyle O(n^2)
O(n2) 在n个关键字中选出最小者,需要n-1次比较,继续在剩余的n-1个元素中选出次小者需要n-2次比较,依此类推。 空间复杂度:
O
(
1
)
\displaystyle O(1)
O(1) 不稳定的排序方法 适用情况:元素数目少、无需完全排序的情况
●对简单选择排序过程进行改进: 利用前面已经进行过的比较结果
(2) 树形选择排序(锦标赛排序) ●基本思想: 首先对n个记录的关键字两两进行比较,然后在
n
2
\displaystyle \frac{n}{2}
2n?个较小者之间再进行两两比较,如此重复,直至选出最小关键字的记录。 整个过程可用一个含有n个叶结点的二叉树表示。 选出最小记录后,将树中的该最小记录修改为∞,然后从该叶子结点所在子树开始,修改到达树根的路径上的结点。 以后每选出一个小元素,都只需进行
l
o
g
n
\displaystyle logn
logn次比较 ●缺陷: ① 需要较多的辅助空间 ② 存在与 " ∞ " 进行比较的冗余比较
(3) 堆排序 ●定义: 对于n个元素的序列{
k
1
,
k
2
,
.
.
.
,
k
n
\displaystyle k_1,k_2,...,k_n
k1?,k2?,...,kn?},当且仅当满足以下关系时,称之为堆(完全二叉树)。 ①
k
i
≥
k
2
i
\displaystyle k_i≥k_{2i}
ki?≥k2i? 且
k
i
≥
k
2
i
+
1
\displaystyle k_i≥k_{2i+1}
ki?≥k2i+1?(大顶堆) ②
k
i
≤
k
2
i
\displaystyle k_i≤k_{2i}
ki?≤k2i? 且
k
i
≤
k
2
i
+
1
\displaystyle k_i≤k_{2i+1}
ki?≤k2i+1?(小顶堆)
i
=
1
,
2
,
.
.
.
,
[
n
2
]
\displaystyle i=1, 2, ...,[\frac {n}{2}]
i=1,2,...,[2n?] ●基本思想: 对一组待排序记录的关键字,首先把它们按堆的定义建成小(大)顶堆,然后输出堆顶的最小(大)关键字所代表的记录,再对剩余的关键字建堆,以便得到次小(大)的关键字,如此反复进行,直到全部关键字排成有序序列为止。 ●需解决的问题: ① 如何建堆? ② 输出堆顶元素后,如何将剩余元素重新调整为一个新堆? ●建初始堆 a. 从最后一个具有孩子的结点(编号[n/2])开始建子堆,依次考查结点[n/2]-1,[n/2]-2,…,1等是否为堆,若否则调整为堆。 当以下标1为根的完全二叉树为堆时,初始堆已建立。 也可以从空堆开始建初始堆。 b.筛选法建立初始堆 将待排序的关键字分放到一棵完全二叉树的各个结点中(此时完全二叉树并不一定具备堆的特性),显然,所有 i≥[n/2] 的结点
K
i
K_i
Ki? 都没有子结点,以这样的
K
i
K_i
Ki? 为根的子树已经是堆,因此初始建堆可从完全二叉树的第 i 个结点
K
i
K_i
Ki? 开始(i=[n/2])。 通过调整,逐步使以
K
[
n
2
]
,
K
[
n
2
?
1
]
,
K
[
n
2
?
2
]
,
.
.
.
\displaystyle K_{[\frac {n}{2}]},K_{[\frac {n}{2}-1]},K_{[\frac {n}{2}-2]},...
K[2n?]?,K[2n??1]?,K[2n??2]?,...为根的子树满足堆的定义,直至进行到以
K
1
K_1
K1? 为根的树排成堆为止。 在对
K
i
K_i
Ki? 为根的子树建堆时,其子树已经为堆,要使得以
K
i
K_i
Ki? 为根的完全二叉树为堆,则可能要调整父、子结点的位置,如此下一层已建成堆的子树可能不再满足堆的定义,则需继续进行调整,如此一次次递推下去,最多可能一直延伸到树叶。 这种方法就像过筛子一样,把最小(大)的关键字一层一层地筛选出来,最后输出堆顶的最小(大)关键字。 ●调整堆元素(筛选) 对于给出的关键字序列,经初始建堆后便得到小(大)顶堆,从而得到最小(大)关键字。 在输出堆顶元素之后,用堆中最后一个元素替代之。此时由于以K2和K3为根的子树仍具有堆的特性,因此只需自上而下进行调整即可。 调整过程为:首先令K1的两个子树根进行比较,令其中较小(大)者与K1相比较,将最小(大)元素交换至K1,使得K1、K2和K3成为堆。由于交换后破坏了子树的堆性质,则需再次进行与上述过程类似的调整,直至进行到最下层的叶结点为止。调整后即得到一个有n-1个元素的新堆,从而得到次小关键字。
void HeapSort(HeapType &H){
for (i = H.length/2; i > 0; --i)
HeapAdjust ( H, i, H.length);
for (i = H.length; i > 1; --i){
H.r[1]←→H.r[i];
HeapAdjust (H, 1, i - 1);
}
}
void HeapAdjust(HeapType &H, int s, int m){
rc = H.r[s];
for ( j=2*s; j<=m; j*=2) {
if ( j<m && H.r[j].key > H.r[j+l].key) ++ j;
if (rc. Key < H.r[ j].key ) break;
H.r[s] = H.r[j];
s = j;
}
H.r[s] = rc;
}
●复杂度: 时间复杂度:
O
(
n
l
o
g
2
n
)
\displaystyle O(nlog_2n)
O(nlog2?n) 堆排序的整个算法时间是由建立初始堆和不断调整堆这两部分时间代价构成的,建立初始堆时关键字的比较次数不超过4n,不断调整堆的时间不超过
O
(
n
l
o
g
2
n
)
\displaystyle O(nlog_2n)
O(nlog2?n)。 空间复杂度:
O
(
1
)
\displaystyle O(1)
O(1) 不稳定的排序方法 适用情况:对于记录数较少的序列来说,堆排序的优越性并不明显,但对数量大的序列来说堆排序是很有效的。
4.归并排序
归并: 所谓“归并”,是将两个或两个以上的有序序列合并成为一个新的有序序列。 归并排序
(1) 两路归并排序(以顺序表作为存储结构) 可将由n个记录的一个无序序列看成是由n个长度为1的有序子序列组成的序列,然后进行两两归并,得到
[
n
2
]
[\frac{n}{2}]
[2n?]个长度为2或1的有序子序列,再两两归并,……,如此重复,直到最后形成包含n个记录的一个有序序列为止。 这种总是反复将两个有序序列归并成一个有序序列的排序方法称为两路归并排序。
void MergeSort(SqList &L, int s, int t){
if (s < t) {
m = (s+t)/2;
MergeSort(L, s, m);
MergeSort(L, m+1, t);
Merge(L, s, m, t);
}
}
void Merge(SqList &L, int i, int m, int n) {
b = i;
for(j = m+1,k =1; i <=m && j<=n; ++k) {
if (L.r[i].key < L.r[j].key) temp.r[k] = L.r[i++];
else temp.r[k] = L.r[j++];
}
for (; i <= m; ) temp.r[k++] = L.r[i++];
for (; j <= n; ) temp.r[k++] = L.r[j++];
for(i = b, k = 1; i <= n; ) L.r[i++] = temp.r[k++];
}
●复杂度: 时间复杂度:
O
(
n
l
o
g
2
n
)
\displaystyle O(nlog_2n)
O(nlog2?n) 对于具有n个元素的一个序列,元素的移动次数可以使用下面的递归公式计算: M(1) = 0 M(n) = 2M(n/2) + 2n 空间复杂度:
O
(
n
)
\displaystyle O(n)
O(n) (缺点) 稳定的排序方法 用迭代取代递归进行归并排序效率更高
5. 分配排序
基数排序 基数排序是一种借助多关键字排序的思想对单逻辑关键字进行排序的方法,不需要进行记录关键字间的比较。 多关键字排序: n个元素的序列{
R
1
,
R
2
,
.
.
.
,
R
n
R_1,R_2,...,R_n
R1?,R2?,...,Rn?},每个元素
R
i
R_i
Ri?有d个关键字(
K
i
0
,
K
i
1
,
.
.
.
,
K
i
d
?
1
K_i^0,K_i^1,...,K_i^{d-1}
Ki0?,Ki1?,...,Kid?1?),则序列对关键字(
K
i
0
,
K
i
1
,
.
.
.
,
K
i
d
?
1
K_i^0,K_i^1,...,K_i^{d-1}
Ki0?,Ki1?,...,Kid?1?)有序是指: 对于序列中任意两个记录
R
i
R_i
Ri?和
R
j
R_j
Rj?记都满足下列有序关系:(
K
i
0
,
K
i
1
,
.
.
.
,
K
i
d
?
1
K_i^0,K_i^1,...,K_i^{d-1}
Ki0?,Ki1?,...,Kid?1?) < (
K
j
0
,
K
j
1
,
.
.
.
,
K
j
d
?
1
K_j^0,K_j^1,...,K_j^{d-1}
Kj0?,Kj1?,...,Kjd?1?) 其中
K
0
K^0
K0称为最主位关键字,
K
d
?
1
K^{d-1}
Kd?1称为最次位关键字。 最高位优先(MSD) 先对最主位关键字
K
0
K^0
K0进行排序,将序列分成若干个子序列,每个子序列中的元素具有相同的
K
0
K^0
K0值,然后分别就每个子序列对关键字
K
1
K^1
K1进行排序,按
K
1
K^1
K1值的不同再分成更小的子序列,依次重复,直至对
K
d
?
2
K^{d-2}
Kd?2进行排序之后得到的每个子序列中的元素都具有相同的(
K
0
K^0
K0,
K
1
K^1
K1,…,
K
d
?
2
K^{d-2}
Kd?2),而后分别对每个子序列对
K
d
?
1
K^{d-1}
Kd?1 进行排序,最后将所有子序列依次联接在一起成为一个有序序列。 最低为优先(LSD) 先对最次位关键字
K
d
?
1
K^{d-1}
Kd?1进行排序,然后对
K
d
?
2
K^{d-2}
Kd?2进行排序,依次重复,直至对
K
0
K^0
K0进行排序后便形成一个有序序列。 链式基数排序 借助“分配”和“收集”两种操作对单逻辑关键字进行排序的方法。 有的单逻辑关键字可以看成是由若干个关键字复合而成。
具体来讲,第一次对最低位关键字(个位数)进行分配,将初始序列中的元素分配到RADIX个队列中去,每个队列中的元素的关键字的个位数相同。用f[i]和e[i]分别表示第i个队列的头指针和尾指针。
●复杂度: 采用链式存储,避免元素移动 时间复杂度:
O
(
d
(
n
+
r
d
)
)
\displaystyle O(d(n+rd))
O(d(n+rd)) 每一趟分配: O(n) 每一趟收集: O(rd) 共d趟: d(n+rd) 空间复杂度:
O
(
r
d
)
\displaystyle O(rd)
O(rd) 2rd个队列指针 稳定的排序方法 适用情况:适用于元素数目n很大且关键字很小的情况
按平均时间复杂度划分,内部排序可分为三类:
O
(
n
2
)
O(n^2)
O(n2)的简单排序方法,
O
(
n
l
o
g
2
n
)
O(nlog_2n)
O(nlog2?n)的高效排序方法和
O
(
d
n
)
O(dn)
O(dn)的基数排序方法。
三、外部排序
待排序的记录数量很大,不能一次装入内存,则无法利用前面讨论的内部排序方法(否则将引起频繁访问外存); 对外存中数据的读/写是以“数据块”为单位进行的; 读/写外存中一个“数据块”的数据所需要的时间为:
T
I
/
O
=
t
s
e
e
k
+
t
l
a
+
n
?
t
w
m
\displaystyle T_{I/O}=t_{seek}+t_{la}+n*t_{wm}
TI/O?=tseek?+tla?+n?twm? 其中,
t
s
e
e
k
\displaystyle t_{seek}
tseek? 为寻查时间(查找该数据块所在磁道)
t
l
a
\displaystyle t_{la}
tla? 为等待(延迟)时间
n
?
t
w
m
\displaystyle n*t_{wm}
n?twm?为传输数据块中n个记录的时间
外部排序由相对独立的两个步骤组成: 1.按可用内存大小,利用内部排序方法,构造若干( 记录的) 有序子序列,通常称外存中这些记录有序子序列为 “归并段”; 2.通过“归并”,逐步扩大 (记录的)有序子序列的长度,直至外存中整个记录序列按关键字有序为止。
1. 多路平衡归并排序
2. 置换-选择排序
3. 最佳归并树
|