概述
鸽了一天没更新了,今天简单地来给大家介绍一下归并算法。
相信大家刚开始学归并算法时,书上都会有这样一张图,这张图我们从上往下看,首先是分解的过程,将原本无序的序列分解成若干个包含单个元素的序列,然后把这些序列看作起点,对它们执行归并操作,其中归并操作的步骤如下:
(1)申请空间,使其大小为两个已经排序的序列之和,该空间用来存放合并后的序列。
(2)设定两个指针,最初位置分别为两个已经排序序列的起始位置。
(3)比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并将指向较小元素的指针移动到下一个位置。
(4)重复步骤三直到某一指针超出序列尾。
(5)将另一序列剩下的所有元素直接复制到合并序列尾。
基本思想
归并排序采用经典的分治策略(将问题分成一些小的问题然后递归求解,治的阶段将前一阶段得到的所有答案“修补”在一块儿,即分而治之),是建立在归并操作上的一种有效的排序算法。而分治策略往往就涉及到递归的思想,所以我们的算法实现也涉及递归的思想。
为什么说它是“史莱姆式”算法
这个是博主自己命名的(骄傲),因为史莱姆最传统的技能就是“分裂”再“聚合”,合成更强的史莱姆(大雾)。日漫里经常出现的词“翻修”也类似于此,将组织分解再重整,就可以达到强化肉体、治疗疾病的功能。
算法实现
递归版本:
void merge(vector<int>& v, vector<int>::iterator left, vector<int>::iterator mid, vector<int>::iterator right)
{
vector<int> help;
auto iter_1 = left, iter_2 = mid + 1;
while (iter_1 <= mid && iter_2 <= right)
help.push_back(*iter_1 > *iter_2 ? *iter_2++ : *iter_1++);
while (iter_1 <= mid)
help.push_back(*iter_1++);
while (iter_2 <= right)
help.push_back(*iter_2++);
for (size_t i = 0; i < help.size(); ++i)
*(left + i) = help[i];
}
void sort_process(vector<int>& v, vector<int>::iterator left, vector<int>::iterator right)
{
if (left < right)
{
auto mid = left + (right - left) / 2;
sort_process(v, left, mid);
sort_process(v, mid + 1, right);
merge(v, left, mid, right);
}
}
void merge_sort(vector<int>& v)
{
if (v.size() < 2)
return;
sort_process(v, v.begin(), v.end() - 1);
}
算法复杂度
时间复杂度:O(nlog2n),而且要注意归并排序是相对稳定的算法(不是指稳定性,而是指算法对不同序列的时间复杂度相对稳定),也就是说归并算法的时间复杂度与序列的有序程度没太大关系,最好最坏的情况下也是 O(nlog2n) ,其中 O(log2n) 是因为分解和归并的过程中每次都对序列进行折半操作,O(n) 是单次归并操作的时间复杂度。
空间复杂度:O(n+log2n),亦可写成O(n),是因为对比起 n ,log2n是相较可以忽略的小值。其中 n 是 merge 内使用到的辅助数组 vector help所占用的内存空间;而 log2n是递归所占用的栈空间。
算法稳定性
我们看下面这句代码:
help.push_back(*iter_1 > *iter_2 ? *iter_2++ : *iter_1++);
这里就可以看出merge_sort属于稳定的,因为iter_1永远指向范围内前半部分的元素,而iter_2指向后半部分的元素,而当iter_1 <= iter_2时,我们将iter_1所指向的元素放入help序列,说明在两者相同时我们选择先放入位置靠前的元素,就保持了稳定性。