目录
数据结构-排序(第十章)的整理笔记,若有错误,欢迎指正。
归并排序和基数排序
归并排序(merge sort)
- 归并排序是多次将两个或两个以上的有序表合并成一个新的有序表。最简单的归并是直接将两个有序的子表合并成一个有序的表,即二路归并。
- 二路归并排序的基本思路是将R[0…m-1]看成是n个长度为1的有序序列,然后进行两两归并,得到 ⌈ n 2 ⌉ ⌈\frac n2⌉ ⌈2n⌉个长度为2(最后一个有序序列的长度可能为2)的有序序列,再进行两两归并,得到 ⌈ n 4 ⌉ ⌈\frac n4⌉ ⌈4n⌉个长度为4(最后一个有序序列的长度可能小于4)的有序序列,…,直到得到一个长度为n的有序序列。
- 说明:归并排序每趟产生的有序区只是局部有序的,也就是说在最后一趟排序结束前所有元素并不一定归位了。
代码实现
void Merge(ElemType a[], ElemType b[], ElemType c[], int num1, int num2) //归并排序
{
int i=0, j=0, k=0;
while (i < num1 && j < num2)
{
if (a[i] <= b[j]) c[k++] = a[i++];
else c[k++] = b[j++];
}
if (i >= num1)
while(j < num2) c[k++] = b[j++];
if (j >= num2)
while (i < num1) c[k++] = a[i++];
}
运行结果
二路归并排序(2-way merge sort)
代码实现
int* b = (ElemType*)malloc(n * sizeof(ElemType)); //辅助数组b
void Merge(ElemType a[], int low, int mid, int high)
//表a的两段a[low..mid]和a[mid+1,high]各自有序,将它们合并成一个有序表
{
int i, j, k;
for (k = low; k <= high; k++) b[k] = a[k]; //将a中所有元素复制到b中
k = low, i = low, j = mid + 1;
while (i <= mid && j <= high)
{
if (b[i] <= b[j]) a[k++] = b[i++]; //比较b的左右两段中的元素
else a[k++] = b[j++]; //将较小值复制到a中
}
while (i <= mid) a[k++] = b[i++]; //若第一个表未检测完,复制
while (j <= high) a[k++] = b[j++]; //若第二个表未检测完,复制
}
void MergeSort(ElemType a[], int low, int high)
{
if (low < high)
{
int mid = (low + high) / 2; //从中间划分两个子序列
MergeSort(a, low, mid); //对左侧子序列进行递归排序
MergeSort(a, mid + 1, high); //对右侧子序列进行递归排序
Merge(a, low, mid, high); //归并
}
}
运行结果
程序分析
- 时间复杂度:对于长度为n的排序表,二路归并需要进行 ⌈ l o g 2 n ⌉ ⌈log_2^n⌉ ⌈log2n⌉趟,每趟归并时间为 O ( n ) O(n) O(n),故其时间复杂度无论是在最好还是在最坏情况下均是 O ( n l o g 2 n ) O(nlog_2^n) O(nlog2n),显然平均时间复杂度也是 O ( n l o g 2 n ) O(nlog_2^n) O(nlog2n)。
- 空间复杂度:在二路归并排序过程中,每次二路归并都需要使用一个辅助数组来暂存两个有序子表归并的结果,而每次二路归并后都会释放其空间,但最后一趟需要所有元素参与归并,所以总的辅助空间复杂度为 O ( n ) O(n) O(n)。
- 稳定性:在一次二路归并中,如果第1段元素R[i]和第2段元素R[j]的关键字相同,总是将R[i]放在前面、R[j]放在后面,相对次序不会发生改变,所以二路并归并排序是一种稳定的排序算法。
- 归并排序可以是多路的,如三路归并排序等。以三路归并排序为例,归并的趟数是 ⌈ l o g 3 n ⌉ ⌈log_3^n⌉ ⌈log3n⌉,每一趟的时间为 O ( n ) O(n) O(n),对应的执行时间为 O ( n l o g 3 n ) O(nlog_3^n) O(nlog3n),但 l o g 3 n = l o g 2 n / l o g 2 3 log_3^n=log_2^n/log_2^3 log3n=log2n/log23,所以时间复杂度仍为 O ( n l o g 2 n ) O(nlog_2^n) O(nlog2n),不过三路归并排序算法的实现远比二路归并排序算法复杂。
基数排序(radix sort)
- 基数排序是一种很特别的排序方法,它不基于比较和移动进行排序,而基于关键字各位的大小进行排序。基数排序是一种借助多关键字排序的思想对单逻辑关键字进行排序的方法。
- 假设长度为n的线性表中每个结点 a j a_j aj的关键字由d元组( k j d − 1 , k j d − 2 , . . . , k j 1 , k j 0 k_j^{d-1},k_j^{d-2},...,k_j^{1},k_j^{0} kjd−1,kjd−2,...,kj1,kj0)组成,满足 0 ≤ k j i ≤ r − 1 ( 0 ≤ j < n , 0 ≤ i ≤ d − 1 ) 0≤k_j^i≤r-1(0≤j<n,0≤i≤d-1) 0≤kji≤r−1(0≤j<n,0≤i≤d−1)。其中 k j d − 1 k_j^{d-1} kjd−1为最主位关键字, k j 0 k_j^0 kj0为最次位关键字。
- 为实现多关键字排序,通常有两种方法:第一种是最高位优先(MSD)法,按关键字位权重递减依次逐层划分成若干更小的子序列,最后将所有子序列依次连接成一个有序序列。第二种是最低位优先(LSD)法,按关键字权重递增依次进行排序,最后形成一个有序序列。
- 下面描述以r为基数的最低位优先基数排序的过程,在排序过程中,使用r个队列
Q
0
,
Q
1
,
.
.
.
,
Q
r
−
1
Q_0,Q_1,...,Q_{r-1}
Q0,Q1,...,Qr−1。基数排序的过程如下:
对 i = 0 , 1 , . . . , d − 1 i=0,1,...,d-1 i=0,1,...,d−1,依次做一次“分配”和“收集”(其实是一次稳定的排序过程)。
分配:开始时,把 Q 0 , Q 1 , . . . , Q r − 1 Q_0,Q_1,...,Q_{r-1} Q0,Q1,...,Qr−1各个队列置成空队列,然后依次考察线性表中的每个结点 a j ( j = 0 , 1 , . . . , n − 1 ) a_j(j=0,1,...,n-1) aj(j=0,1,...,n−1),若 a j a_j aj的关键字 k j i = k k_j^i=k kji=k,就把 a j a_j aj放进 Q k Q_k Qk队列中。
收集:把 Q 0 , Q 1 , . . . , Q r − 1 Q_0,Q_1,...,Q_{r-1} Q0,Q1,...,Qr−1各个队列中的结点依次首尾相接,得到新的结点序列,从而组成新的线性表。
代码实现
void RadixSort(LNode*& p, int r, int d)
{
LNode* head[MAXR], * tail[MAXR], * t; //定义各链队的首尾指针
int i, j, k;
for (i = 0; i <= d - 1; i++) //从低位到高位循环
{
for (j = 0; j < r; j++) head[j] = tail[j] = NULL; //初始化各链队的首、尾指针
while (p != NULL) //分配:对于原链表中的每个结点循环
{
k = p->data[i] - '0'; //找第k个链队
if (head[k] == NULL) //第k个链队空时,队头、队尾均指向结点p
{
head[k] = p;
tail[k] = p;
}
else //第k个链队非空时结点p进队
{
tail[k]->next = p;
tail[k] = p;
}
p = p->next; //取下一个待排序的元素
}
p = NULL; //重新用p来收集所有结点
for (j = 0; j < r; j++) //收集:对于每一个链队循环
if (head[j] != NULL) //若第j个链队是第一个非空链队
{
if (p == NULL)
{
p = head[j];
t = tail[j];
}
else //若第j个链队是其他非空链队
{
t->next = head[j];
t = tail[j];
}
}
t->next = NULL; //最后一个结点的next域置NULL
}
}
程序分析
基数排序算法的性能分析如下:
- 空间效率:一趟排序需要的辅助存储空间为r(r个队列:r个队头指针和r个队尾指针),但以后的排序中会重复使用这些队列,所以基数排序的空间复杂度为 O ( r ) O(r) O(r)。
- 时间效率:基数排序需要进行d趟分配和收集,一趟分配需要 O ( n ) O(n) O(n),一趟收集需要 O ( r ) O(r) O(r),所以基数排序的时间复杂度为 O ( d ( n + r ) ) O(d(n+r)) O(d(n+r)),它与序列的初始状态无关
- 稳定性:对于基数排序算法而言,很重要一点就是按位排序时必须是稳定的。因此,这也保证了基数排序的稳定性。
- 基数排序擅长解决的问题:
①数据元素的关键字可以方便地拆分为d组,且d较小
②每组关键字的取值范围不大,即r较小
③数据元素个数n较大
各种内部排序算法的比较
算法种类 | 排序方法 | 时间复杂度 | 空间复杂度 | 稳定性 | 复杂性 | 实现思想 | 适合的存储方式 | ||
---|---|---|---|---|---|---|---|---|---|
最好情况 | 平均情况 | 最坏情况 | |||||||
插入排序 | 直接插入排序 | O(n) | O(n²) | O(n²) | O(1) | 稳定 | 简单 | 从第二个元素开始,和前面所有的元素进行比较,依次将它们插入到合适的位置,直到最后一个元素插入结束 | 顺序表/链表 |
折半插入排序 | 初始时从中间划分,大于中间元素则在右半部分继续划分;反之在左半部分继续划分,直到不能划分时插入元素 | 顺序表 | |||||||
希尔排序 | O(n^1.3) | 不稳定 | 较复杂 | 选取一个小于序列长度的整数作为第一个增量d1,将距离d1的倍数的元素划分到一个组,在各组内进行直接插入排序,接下来不断选取更小的增量并重复以上过程,直到增量为1时,排序完所有元素即结束 | |||||
交换排序 | 冒泡排序 | O(n)(正序) | O(n²) | O(n²) | O(1) | 稳定 | 简单 | 对每两个相邻元素进行比较,不满足排序要求则交换,重复该过程 | 顺序表/链表 |
快速排序 | O(nlog₂n) | O(log₂n)(递归调用栈) | 不稳定 | 较复杂 | 任取一个元素作为基准,把它放入适当位置后,对该元素左、右部分的元素重复该操作 | 顺序表 | |||
选择排序 | 简单选择排序 | O(n²) | O(1) | 不稳定 | 简单 | 每次从待排元素中选取最小(最大)元素放到表头(表尾),重复该操作,直到无待排元素为止 | 顺序表/链表 | ||
堆排序 | O(nlog₂n)(建立初始堆+反复重建堆) | 较复杂 | 把元素序列写成完全二叉树的形式(为了方便,通常从数组下标1的位置开始存数据),建立初始堆→反复重建堆 | 顺序表 | |||||
二路归并排序 | O(nlog₂n) | O(n) | 稳定 | 较复杂 | 将原表元素复制一份到新表,从新表的中间进行划分,形成两个子表,不断比较两个子表中的值,选取较小(较大)元素放入原表。若其中一个表的元素都已经比较过,直接将另一个表中剩下的元素复制到原表尾部。 | 顺序表 | |||
基数排序 | O(d(n+r)) | O(r) | 稳定 | 较复杂 | 按关键字权重进行排序,最后形成一个有序序列。 | 链表 |
-
元素的移动次数与关键字的初始排列次序无关的是:基数排序
-
元素的比较次数与初始序列无关是:选择排序、折半插入排序
-
算法的时间复杂度与初始序列无关的是:选择排序、堆排序、归并排序、基数排序
-
算法的排序趟数与初始序列无关的是:插入排序、选择排序、基数排序
-
不同的排序方法适应不同的应用环境和要求,所以选择合适的排序方法应综合考虑下列因素:
- 待排序的元素数目n(问题规模)。
- 元素的大小(每个元素的规模)。
- 关键字的结构及其初始状态。
- 对稳定性的要求。
- 语言工具的条件。
- 数据的存储结构。
- 时间和空间复杂度等。
- 没有哪一种排序方法是绝对好的,每一种排序方法都有其优缺点,适合于不同的环境,因此在实际应用中应根据具体情况做选择。首先考虑排序对稳定性的要求,若要求稳定,则只能在稳定方法中选取,否则可以在所有方法中选取;其次要考虑待排序元素个数n的大小,若n较大,则可在改进方法中选取,否则在简单方法中选取;然后再考虑其他因素。下面给出综合考虑了以上几个方面所得出的大致结论:
- 若n较小(如n≤50),可采用直接插入或简单选择排序。一般地,这两种排序方法中,直接插入排序较好,但简单选择排序移动的元素数少于直接插入排序。
- 若文件初始状态基本有序(指正序),则选用直接插入或冒泡排序为宜。
- 若n较大,应采用时间复杂度为 O ( n l o g 2 n ) O(nlog_2^n) O(nlog2n)的排序方法,例如快速排序、堆排序或二路归并排序。快速排序是目前基于比较的内排序中被认为是较好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最少;但堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况。这两种排序都是不稳定的,若要求排序稳定,则可选用二路归并排序。
- 若需要将两个有序表合并成一个新的有序表,最好用二路归并排序方法。
- 基数排序可能在O(n)时间内完成对n个元素的排序。但遗憾的是,基数排序只适用于像字符串和整数这类有明显结构特征的关键字,而当关键字的取值范围属于某个无穷集合(例如实数型关键字)时无法使用基数排序,这时只有借助于"比较"的方法来排序。由此可知,若n很大,元素的关键字位数较少且可以分解时采用基数排序较好。