归并排序(稳定的排序算法)
1.自顶向下的归并排序(两个优化)
图示
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
M E R G E S O R T E X A M P L E
E M
G R
E G M R
E S
O R
E O R S
E E G M O R R S
E T
A X
A E T X
M P
E L
E L M P
A E E L M P T X
A E E E E G L M M O P R R S T X
运用分治的思想,分而治之
要对子区间 [low … high] 排序
1.将其分成 [low … mid] 和 [mid + 1, high]
2.调用递归对分开后的左右区间单独排序
3.最后将有序的子区间归并
归并
简单的实现两数组归并是新开一个数组,将两个数组一个个放入新数组
显然,对于大量的数据,需要多次归并,每次都创建一个新数组不合适
优化:
将所有元素复制到辅助数组 temp[] ,然后再归并回原数组 a[] 中
void merge(int a[], int low, int mid, int high){
int i = low, j = mid + 1;
for (int k = low; k <= high; k ++)
temp[k] = a[k];
for (int k = low; k <= high; k ++){
if (i > mid) a[k] = temp[j ++]; //如果i到头了
else if (j > high) a[k] = temp[i ++]; //如果j到头了
else if (temp[i] < temp[j]) a[k] = temp[i ++]; //谁小放谁
else a[k] = temp[j ++];
}
}
需要注意,temp[] 数组不要在 sort函数 中创建,这会导致效率很低
排序
要对子数组 a [low … high] 排序,先将子数组分成
a [low … mid] 和 a [mid + 1 … high] ( mid = low + high >> 1 )
然后调用递归对分后的数组单独排序
最后将有序的子数组归并成最终的排序结果
优化1:
归并时,若前一半最大的数,小于后一半最小的数,可以跳过归并
优化2
:
切分到小数组时,用插入排序,插入排序排序小数组更快
//优化:切分到小数组时候用插入排序
void insertSort(int a[], int low, int high){
if (high - low < 2) return;
for (int i = low + 1; i <= high; i ++){
int idx = i;
while (idx != low && a[idx] < a[idx - 1]){
swap(a[idx], a[idx - 1]);
idx --;
}
}
}
void sort(int a[], int low, int high){
if (high <= low + cutoff - 1){
insertSort(a, low, high);
return;
}
int mid = low + (high - low) / 2;
sort(a, low, mid); //将左半边排序
sort(a, mid + 1, high); //将右半边排序
//优化,如果前一半最大的数,小于后一半最小的数,可以跳过归并
if (a[mid] < a[mid + 1]) return;
merge(a, low, mid, high); //归并结果
}
时间复杂度分析
假设有N个元素,令 2^n = N
易知,恰好分为 n 层
自顶向下的第 k(0~n-1) 层有 2^k 个子数组,每个数组长度为 2^(n-k),最多 2^(n-k) 次比较
每层比较次数:2^k * 2^(n-k) = 2^n
n层总共比较次数:n * 2^n = NlgN
Code
#include <iostream>
#include <cstring>
using namespace std;
const int N = 10000, cutoff = 7; //切分后的数组小于7时,调用插入排序
int temp[N]; //辅助函数
//优化:切分到小数组时候用插入排序
void insertSort(int a[], int low, int high){
if (high - low < 2) return;
for (int i = low + 1; i <= high; i ++){
int idx = i;
while (idx != low && a[idx] < a[idx - 1]){
swap(a[idx], a[idx - 1]);
idx --;
}
}
}
//归并
void merge(int a[], int low, int mid, int high){
int i = low, j = mid + 1;
for (int k = low; k <= high; k ++)
temp[k] = a[k];
for (int k = low; k <= high; k ++){
if (i > mid) a[k] = temp[j ++]; //如果i到头了
else if (j > high) a[k] = temp[i ++]; //如果j到头了
else if (temp[i] < temp[j]) a[k] = temp[i ++]; //谁小放谁
else a[k] = temp[j ++];
}
}
//排序
//辅助数组temp不要在sort中创建,这会导致效率很低
void sort(int a[], int low, int high){
if (high <= low + cutoff - 1){
insertSort(a, low, high);
return;
}
int mid = low + (high - low) / 2;
sort(a, low, mid); //将左半边排序
sort(a, mid + 1, high); //将右半边排序
//优化,如果前一半最大的数,小于后一半最小的数,可以跳过归并
if (a[mid] < a[mid + 1]) return;
merge(a, low, mid, high); //归并结果
}
int main(){
int a[10] = {2, 0, 1, 1, -22, -3, -3, -3, 99};
sort(a, 0, 9);
for (int i = 0; i < 10; i ++)
cout << a[i] << ' ';
return 0;
}
2.自底向上的归并排序(不需要递归)
先归并那些微型数组,然后再成对归并得到的子数组
直到将整个数组归并到一起
即开始先两两归并,然后再四四归并,然后…
在每一轮的归并中,最后一次归并的第二个数组可能比第一个数组小
图示
M E R G E S O R T E X A M P L E
E M
G R
E S
O R
E T
A X
M P
E L
E G M R
E O R S
A E T X
E L M P
E E G M O R R S
A E E L M P T X
A E E E E G L M M O P R R S T X
归并
实现与上文类似
排序
void sort(int a[], int len){
for (int i = 1; i < len; i *= 2){ //每次翻一倍
for (int low = 0; low < len - i; low += (i * 2)){ //找到当前长度下每个组的起点
merge(a, low, low + i - 1, min(len - 1, low + (2 * i) - 1)); //low mid high
}
}
}
Code
#include <iostream>
#include <cstring>
using namespace std;
const int N = 10000;
int temp[N];
void merge(int a[], int low, int mid, int high){
int i = low, j = mid + 1;
for (int k = low; k <= high; k ++)
temp[k] = a[k];
for (int k = low; k <= high; k ++){
if (i > mid) a[k] = temp[j ++]; //如果i到头了
else if (j > high) a[k] = temp[i ++]; //如果j到头了
else if (temp[i] < temp[j]) a[k] = temp[i ++]; //谁小放谁
else a[k] = temp[j ++];
}
}
void sort(int a[], int len){
for (int i = 1; i < len; i *= 2){ //每次翻一倍
for (int low = 0; low < len - i; low += (i * 2)){ //找到当前长度下每个组的起点
merge(a, low, low + i - 1, min(len - 1, low + (2 * i) - 1)); //low mid high
}
}
}
int main(){
int a[10] = {2, 0, 1, 1, -22, -3, -3, -3, 99};
sort(a, 10);
for (int i = 0; i < 10; i ++)
cout << a[i] << ' ';
return 0;
}
自顶向下和自底向上的比较
当数组长度是 2 的幂时,两种方法比较次数和数组访问次数相同,顺序不同
自底向上的归并排序更适用于
链表
组织的数据
将链表先按大小为 1 的子链表排序,然后是大小为 2 的子链表,然后…
这样只需要重新组织链表链接就能将链表原地排序,不需要创建任何新的链表结点