通过对算法的分析和问题的分析来引导如何去编程,一步步论证,检测,再论证去用程序来实现心中的想法。
2.1.3 mergeSort 先分后治
分治法是采用分治思想来求解问题的一种思想,该思想在实际解决问题的过程中分为先治后分和先分后治两种情况。快速排序是先治后分的典型,而合并排序则是先分后治的典型。
与快速排序相同的是合并排序也基于分治思想而想出来的一种排序算法;而与快速排序不同的是合并排序采用的是先分后治的方法,且合并排序不需要进行左右数据的交换,而是采用数据的插入方法。
- 首先为什么不进行交换就能排序是因为基于一个最基本的原理是,给你两个数,对其进行排序,就是对其比较大小,谁小谁就先保存给向量vector的第一个位置,然后在把大的再紧随其后赋值在第二个位置;这样在把向量输出来,数据就是从小到大已排序了。
- 对于两组两边各两个共4个数,其实也是一样,依次将A组的第一个数与B组的第一个数进行比较,谁小谁就保存到第三个向量C,然后对剩余的数(已经入向量C的数在不考虑),即两组头部数进行(1)操作。
- 根据以上(1)(2)可以实现两组已排序的数进行合并排序成一个整体,即便原有的已排序的两组A和B数据不连续,但是在经过比较插入到C后,就是有序的了。这样我们采用递归思想,由小及大==由里及外==由深到浅进行递归到最终的一整列数,那么递归回来的数就是已排好序了。
- 注:通过下标来指向一组数的两部分的范围下标进行控制,特注两个小部分的数插入合并后的结果可以直接返回(但这样比较耗内存),最好的方法是再次赋值给入参&num,这样相当于将num中原本左右部分有序但总体无序的数进行了插入排序,最后得到的是已排好序的&num的入参部分。然后都递归回去即已排序。
首先,对一整列数据进行分成两部分,如何去编程实现分,一种最直接的想法是定义两个向量来存储左右的数,然后对返回的子向量进行合并比较插入到第三个向量中并返回,最后对左右两个已返回的向量进行合并,然后层层递归到最顶层。另一种最直接的编程实现方法是采用下标指针指示法,即通过low,mid,high指针指向并操作同一个向量。
因此,基本的程序框架结构实现如下:
void DivideMerge::mergeSort(vector<int>&num,int low,int high)
{
if (low < high)
{
int mid = (low + high) / 2;
mergeSort(num, low, mid); //不断的递归下去,分到原子左已排序好
mergeSort(num, mid + 1, high); //右已排序好
conquer(num, low, mid, high); //治就是插入合并到一个数组中排序
}
}
注意:分和递归的结束条件有两种:一种是递归的参数为减小到1;本质上是递归的参数是否满足递归的条件,只有满足才会指向里面的函数和程序实现递归,若不满足参数的约束条件,不论有没有return,都会结束掉此次函数的调用,本质上只要不让它继续调用下一步的递归函数,就不会产生深一步的递归,自然就会回溯。
下面是治的方法conquer实现如下:本质上是通过3个while循环就可以遍历两个向量的所有数
void DivideMerge::conquer(vector<int>&num, int low, int mid, int high) //治就是插入合并到一个数组中排序
{
int i = low;
int j = mid+1;
vector<int>mergeNum;
while (i <= mid&&j <= high) //治的过程
{
if (num[i] <= num[j])
{
mergeNum.push_back(num[i]);
i++;
}
else
{
mergeNum.push_back(num[j]);
j++;
}
}
while (i <= mid) //当一个向量中的数比较完,而另一个没有时,则直接把另一个向量后面的数赋值在C向量中
{
mergeNum.push_back(num[i]);
i++;
}
while (j<=high)
{
mergeNum.push_back(num[j]);
j++;
}
int k = 0;
while (low <= high) //这个是返回赋值
{
try
{
if (k >= mergeNum.size())
{
throw "index out";
}
num[low] = mergeNum[k++];
}
catch (char *str)
{
cout << str<<":k++ 的地址越界" << endl;
system("pause");
}
low++;
}
}
总结:基于分治法思想的合并排序,合并排序从分的思想出发也分为左右两边,合并排序直接进行比较插入,与QuickSort不同的是,合并排序不需要进行左右分,寻找标志位,所以左右分的界限(即标志位)可以通过任意计算,一般直接从中间分开。
与快速排序不同的是合并是先分后治,因为快排需要寻找标志位,所以必须先治;而合并可直接根据low和high计算,且由递归由深至浅排序好。
时间复杂度分析:合并排序的为什么时间复杂度不是O(n2)而是O(nlogn),其最根本最主要的原因是待插入比较的数已排序,所以左边的第一个数与右边的第一个数比较后,就无需再和右边的其他数进行比较,所以减少了比较的次数,左边的数减少了重复比较的次数。