归并排序


1.基本思想

  归并排序,是创建在归并操作上的一种有效的排序算法。算法是采用分治法(Divide and Conquer)的一个非常典型的应用,且各层分治递归可以同时进行。归并排序思路简单,速度仅次于快速排序,为稳定排序算法,一般用于对总体无序,但是各子项相对有序的数列。


2.图解

归并排序


3.实现逻辑

迭代法
● 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
● 设定两个指针,最初位置分别为两个已经排序序列的起始位置
● 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
● 重复步骤三直到某一指针到达序列尾
● 将另一序列剩下的所有元素直接复制到合并序列尾

递归法 ● 将序列每相邻两个数字进行归并操作,形成floor(n/2)个序列,排序后每个序列包含两个元素
● 设定两个指针,最初位置分别为两个已经排序序列的起始位置
● 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
● 重复步骤三直到某一指针到达序列尾
● 将另一序列剩下的所有元素直接复制到合并序列尾

5.代码实现及详解

c++版本
迭代版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
template<typename T> 
void merge_sort(T arr[], int len) { //使用指针来交换的方式
/*
* 这道题超级考验数组传参时指针的知识
* */
T *a = arr;
T *b = new T[len];
for (int seg = 1; seg < len; seg += seg) { //第一个循环,让最初始的一个容器开始进行融合,然后容器内容从1到2到4
for (int start = 0; start < len; start += seg + seg) { //第二个循环,让每次相邻的两个容器进行排序
int low = start, mid = min(start + seg, len), high = min(start + seg + seg, len);
// 比较的容器:第一个容器是从low到mid,第二个容器是从mid到high,然后循环的时候会一直增加两个容器的内容,也就是seg
//low ,mid,high都是每次比较容器的下标,所以要跟长度相比取最小,
// 有可能最后一次容器比较的时候,只有一个容器了,且容器的大小不足所以mid最后取到len,也可能第二容器不够seg了,只能取到len了

int k = low; //将本次比较的两个容器的起始头拿出来,在这里一个个赋值
int start1 = low, end1 = mid; //将两个容器的起始头拿出来
int start2 = mid, end2 = high;
//这个三个while循环会把两个容器里在的值都拿出来
while (start1 < end1 && start2 < end2) //这个while循环会把两个容器里在的值都拿出来,直到其中一个容器都没有了
b[k++] = a[start1] < a[start2] ? a[start1++] : a[start2++];
while (start1 < end1) //当第一个的循环已经做完之后,肯定有一个容器都没有了,但是判断是不是第一个容器还有东西,如果有东西就全部取出来
b[k++] = a[start1++];
while (start2 < end2) //当第一个的循环已经做完之后,肯定有一个容器都没有了,但是判断是不是第二个容器还有东西,如果有东西就全部取出来
b[k++] = a[start2++];
}
/*
* 下面三句是数组作为传参的精髓,数组作为传参进来的时候,是变成数组第一位的指针arr,指针传进来后可以对指针所指向的内容进行修改,但是怎么改都不会影响到外面arr的地址
* 所以一定每次处理的时候,都是要在arr地址上进行处理的,否则没有意义。【这是先验知识】
* 然后函数一开始就new了个b,这个b的地址是跟arr的地址不一样的
* 当b已经处理完后,要把b的东西给a,不能直接给,因为直接赋给a就是把b的地址给a,那么后续的处理时,arr的地址里内容还是没有变化呀
* 所以需要把a的地址暂时存放起来,此时一开始a的地址也是arr的地址,将其存放起来,再把b的地址给a,也就是把b里面的内容给了啊,因为,后续要拿a里的内容进行处理呀
* 此时做完第二句之后呢,a的地址和b的地址是一样的,再下一次for循环的时候,你不可以在同个地址上的内容上进行移动数值吧,那就出错了呀
* 类似【2,1】,处理后得到结果肯定变成【1,1】了
* 刚好又有个temp的地址可以使用,那就直接将temp的地址给b了,然后最后一次for循环处理后,b的地址还是在arr的地址之上
* */
T *temp = a;
a = b;
b = temp;
}

/*
* 虽然下面的if语句注释了,也可以正常得到结果,但是按照逻辑来说的话,上面的最后一次循环之后,a拿到的是b的地址,所以拿到的是b里面的内容,所以要进行
* 一次判断,看a是不是的地址是不是还在arr的地址上,如果不是的话,但是此时b的地址肯定是在arr的地址上的(因为是temp一直在保存着arr的地址),所以只要把
* a的内容给b,b的地址又是arr的地址,所以处理完成后,arr的地址上指的内容肯定就被修改好了

* */
if (a != arr) {
for (int i = 0; i < len; i++)
b[i] = a[i];
b = a;
}

delete[] b;
}

上述的代码更加考验数组和指针之间的知识多一点,下面还有一个版本,通过每次排序后进行赋值的方式来减少指针上的操作,更加简单易懂些

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
template<typename T> // 整數或浮點數皆可使用,若要使用物件(class)時必須設定"小於"(<)的運算子功能
void merge_sort2(T arr[], int len) { //不使用指针交换的方式
/*
* 这道题超级考验数组传参时指针的知识
* */
T *b = new T[len];
for (int seg = 1; seg < len; seg += seg) { //第一个循环,让最初始的一个容器开始进行融合,然后容器内容从1到2到4
for (int start = 0; start < len; start += seg + seg) { //第二个循环,让每次相邻的两个容器进行排序
int low = start, mid = min(start + seg, len), high = min(start + seg + seg, len);
// 比较的容器:第一个容器是从low到mid,第二个容器是从mid到high,然后循环的时候会一直增加两个容器的内容,也就是seg
//low ,mid,high都是每次比较容器的下标,所以要跟长度相比取最小,
// 有可能最后一次容器比较的时候,只有一个容器了,且容器的大小不足所以mid最后取到len,也可能第二容器不够seg了,只能取到len了

int k = low; //将本次比较的两个容器的起始头拿出来,在这里一个个赋值
int start1 = low, end1 = mid; //将两个容器的起始头拿出来
int start2 = mid, end2 = high;
//这个三个while循环会把两个容器里在的值都拿出来
while (start1 < end1 && start2 < end2) //这个while循环会把两个容器里在的值都拿出来,直到其中一个容器都没有了
b[k++] = arr[start1] < arr[start2] ? arr[start1++] : arr[start2++];
while (start1 < end1) //当第一个的循环已经做完之后,肯定有一个容器都没有了,但是判断是不是第一个容器还有东西,如果有东西就全部取出来
b[k++] = arr[start1++];
while (start2 < end2) //当第一个的循环已经做完之后,肯定有一个容器都没有了,但是判断是不是第二个容器还有东西,如果有东西就全部取出来
b[k++] = arr[start2++];
}
//在这里不能直接将b直接给a,否则就是将b的地址给arr了,后续就再也无法在arr的地址上进行运算【这里讨论的arr是指实参】
for(int i=0;i<len;i++)
{
arr[i] = b[i];
}
}
delete[] b;
}

5.分析

如果有n个数,最多需要走n-1次遍历就可以排好
平均时间复杂度:O(nlogn)
最佳时间复杂度:O(n)
最差时间复杂度:O(nlogn)
空间复杂度:O(n)


6.参考

https://zhuanlan.zhihu.com/p/75678113
https://zhuanlan.zhihu.com/p/124356219