一、归并排序
1. 相关问题
merge操作的同时,可以解决 “逆序对问题”;
2.优劣
相对稳定;即:相同的元素,排之后的次序和排之前的次序一致。
需要额外空间;即:在merge 的过程中,需要使用额外的空间。
3.复杂度分析
空间复杂度为O(n);
时间复杂度:
因为归并排序递归的将其二分,于是递归的层级就有logn层。具体的是以2为底n的对数。long2 n;
每层递归中,都将n个数据都扫描了一次。例如:第二层递归中,堆left-mid 和mid+1 -right 都扫描了一遍。
最终时间复杂度就为O(nlogn)
二、代码
1.normal
下面是没有经过优化的完全体:
// 将arr[l...mid]和arr[mid+1...r]两部分进行归并
template<typename T>
void __merge(T arr[], int l, int mid, int r){
T *aux = new T[r-l+1];
for( int i = l ; i <= r; i ++ )
aux[i-l] = arr[i];
// 初始化,i指向左半部分的起始索引位置l;j指向右半部分起始索引位置mid+1
int i = l, j = mid+1;
for( int k = l ; k <= r; k ++ ){
if( i > mid ){ // 如果左半部分元素已经全部处理完毕
arr[k] = aux[j-l]; j ++;
}
else if( j > r ){ // 如果右半部分元素已经全部处理完毕
arr[k] = aux[i-l]; i ++;
}
else if( aux[i-l] < aux[j-l] ) { // 左半部分所指元素 < 右半部分所指元素
arr[k] = aux[i-l]; i ++;
}
else{ // 左半部分所指元素 >= 右半部分所指元素
arr[k] = aux[j-l]; j ++;
}
}
delete[] aux;
}
// 递归使用归并排序,对arr[l...r]的范围进行排序
template<typename T>
void __mergeSort(T arr[], int l, int r){
if( l >= r )
return;
int mid = (l+r)/2;
__mergeSort(arr, l, mid);
__mergeSort(arr, mid+1, r);
__merge(arr, l, mid, r);
}
template<typename T>
void mergeSort(T arr[], int n){
__mergeSort( arr , 0 , n-1 );
}
2.merge优化+插入排序优化
区别在于
- merge之前如果已经发现两段是有序的,就不需要mege了
- 对于递归到小数量级的,使用插入排序,速度更佳。这是因为插入排序虽然是0(n2),但是其常数更小,在n并不大且近乎有序的情况下,比归并更快。
下面也将查部分的插入排序加入代码段
// 将arr[l...mid]和arr[mid+1...r]两部分进行归并
template<typename T>
void __merge(T arr[], int l, int mid, int r){
T *aux = new T[r-l+1];
for( int i = l ; i <= r; i ++ )
aux[i-l] = arr[i];
// 初始化,i指向左半部分的起始索引位置l;j指向右半部分起始索引位置mid+1
int i = l, j = mid+1;
for( int k = l ; k <= r; k ++ ){
if( i > mid ){ // 如果左半部分元素已经全部处理完毕
arr[k] = aux[j-l]; j ++;
}
else if( j > r ){ // 如果右半部分元素已经全部处理完毕
arr[k] = aux[i-l]; i ++;
}
else if( aux[i-l] < aux[j-l] ) { // 左半部分所指元素 < 右半部分所指元素
arr[k] = aux[i-l]; i ++;
}
else{ // 左半部分所指元素 >= 右半部分所指元素
arr[k] = aux[j-l]; j ++;
}
}
delete[] aux;
}
// 使用优化的归并排序算法, 对arr[l...r]的范围进行排序
template<typename T>
void __mergeSort(T arr[], int l, int r){
// 对于小规模数组, 使用插入排序
if( r - l <= 15 ){
insertionSort(arr, l, r);
return;
}
int mid = (l+r)/2;
__mergeSort(arr, l, mid);
__mergeSort(arr, mid+1, r);
// 对于arr[mid] <= arr[mid+1]的情况,不进行merge
// 对于近乎有序的数组非常有效,但是对于一般情况,有一定的性能损失
if( arr[mid] > arr[mid+1] )
__merge(arr, l, mid, r);
}
template<typename T>
void mergeSort(T arr[], int n){
__mergeSort( arr , 0 , n-1 );
}
// 对arr[l...r]范围的数组进行插入排序
template<typename T>
void insertionSort(T arr[], int l, int r){
for( int i = l+1 ; i <= r ; i ++ ) {
T e = arr[i];
int j;
for (j = i; j > l && arr[j-1] > e; j--)
arr[j] = arr[j-1];
arr[j] = e;
}
return;
}
3.额外空间的优化。
对于额外空间的优化其实很简单,只是将频繁的申请释放内存过程,直接在一开始阶段就申请好。
这样避免了频繁的申请、释放内存,浪费时间的同时,有可能使得空间申请失败。
于是我们就在一开始申请和原数组一样大的内存空间,每次merge 的时候只需要先cpoy一下。
这样空间复杂度虽然没有变化,但是时间性能还是有显著提高。
// 将arr[l...mid]和arr[mid+1...r]两部分进行归并
// 其中aux为完成merge过程所需要的辅助空间
template<typename T>
void __merge2(T arr[], T aux[], int l, int mid, int r){
// 由于aux的大小和arr一样, 所以我们也不需要处理aux索引的偏移量
// 进一步节省了计算量:)
for( int i = l ; i <= r; i ++ )
aux[i] = arr[i];
// 初始化,i指向左半部分的起始索引位置l;j指向右半部分起始索引位置mid+1
int i = l, j = mid+1;
for( int k = l ; k <= r; k ++ ){
if( i > mid ){ // 如果左半部分元素已经全部处理完毕
arr[k] = aux[j]; j ++;
}
else if( j > r ){ // 如果右半部分元素已经全部处理完毕
arr[k] = aux[i]; i ++;
}
else if( aux[i] < aux[j] ) { // 左半部分所指元素 < 右半部分所指元素
arr[k] = aux[i]; i ++;
}
else{ // 左半部分所指元素 >= 右半部分所指元素
arr[k] = aux[j]; j ++;
}
}
}
// 使用优化的归并排序算法, 对arr[l...r]的范围进行排序
// 其中aux为完成merge过程所需要的辅助空间
template<typename T>
void __mergeSort2(T arr[], T aux[], int l, int r){
// 对于小规模数组, 使用插入排序
if( r - l <= 15 ){
insertionSort(arr, l, r);
return;
}
int mid = (l+r)/2;
__mergeSort2(arr, aux, l, mid);
__mergeSort2(arr, aux, mid+1, r);
// 对于arr[mid] <= arr[mid+1]的情况,不进行merge
// 对于近乎有序的数组非常有效,但是对于一般情况,有一定的性能损失
if( arr[mid] > arr[mid+1] )
__merge2(arr, aux, l, mid, r);
}
template<typename T>
void mergeSort2(T arr[], int n){
// 在 mergeSort2中, 我们一次性申请aux空间,
// 并将这个辅助空间以参数形式传递给完成归并排序的各个子函数
T *aux = new T[n];
__mergeSort2( arr , aux, 0 , n-1 );
delete[] aux; // 使用C++, new出来的空间不要忘记释放掉:)
}
三、自下而上-迭代归并
自底向上的思路,稍显复杂。但是很有实际意义。
// 自底向上的归并排序中, merge函数并没有改变
// 将arr[l...mid]和arr[mid+1...r]两部分进行归并
template<typename T>
void __mergeBU(T arr[], int l, int mid, int r){
//* VS不支持动态长度数组, 即不能使用 T aux[r-l+1]的方式申请aux的空间
//* 使用VS的同学, 请使用new的方式申请aux空间
//* 使用new申请空间, 不要忘了在__merge函数的最后, delete掉申请的空间:)
T aux[r-l+1];
//T *aux = new T[r-l+1];
for( int i = l ; i <= r; i ++ )
aux[i-l] = arr[i];
// 初始化,i指向左半部分的起始索引位置l;j指向右半部分起始索引位置mid+1
int i = l, j = mid+1;
for( int k = l ; k <= r; k ++ ){
if( i > mid ){ // 如果左半部分元素已经全部处理完毕
arr[k] = aux[j-l]; j ++;
}
else if( j > r ){ // 如果右半部分元素已经全部处理完毕
arr[k] = aux[i-l]; i ++;
}
else if( aux[i-l] < aux[j-l] ) { // 左半部分所指元素 < 右半部分所指元素
arr[k] = aux[i-l]; i ++;
}
else{ // 左半部分所指元素 >= 右半部分所指元素
arr[k] = aux[j-l]; j ++;
}
}
//delete[] aux;
}
// 使用自底向上的归并排序算法
template <typename T>
void mergeSortBU(T arr[], int n){
// Merge Sort Bottom Up 无优化版本
for( int sz = 1; sz < n ; sz += sz )
for( int i = 0 ; i < n - sz ; i += sz+sz )
// 对 arr[i : i+sz-1] 和 arr[i+sz : min(i+sz+sz-1,n-1)] 进行归并
if( arr[i+sz-1] > arr[i+sz] )
__mergeBU(arr, i, i+sz-1, min(i+sz+sz-1,n-1) );
}
1.caution:
for( int sz = 1; sz < n ; sz += sz )
- int sz = 1 ; sz += sz;代表的是,第一次是将两个大小为1的合并起来。然后每次区间翻倍。
- 即:第一次将两个1合并为大小为2的、第二次将大小为2的合并成大小为4的。1、2、4、8、16这样下去。
for( int i = 0 ; i < n - sz ; i += sz+sz )
- i < n - sz ;为的是合并arr[i…i+sz-1] 和 arr[i+sz…i+2*sz-1] 时,第二个区间还存在.
- i += sz+sz 为的是,每次跳过合并好的两个区间。
__mergeBU(arr, i, i+sz-1, min(i+sz+sz-1,n-1) );
- merge 中min(i+sz+sz-1,n-1) ,为了取其最小值,防止越界