C++ 归并排序算法的实现与改进(含笔试面试题)

       归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。

算法步骤:

1:申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列

2:设定两个指针,最初位置分别为两个已经排序序列的起始位置

3:比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置

4:重复步骤3直到某一指针达到序列尾

将另一序列剩下的所有元素直接复制到合并序列尾

实现代码:两种方法,重载了排序函数,并改进排序算法,使用了STL

/***************************************************************************
 *  @file       main.cpp
 *  @author     MISAYAONE
 *  @date       26  March 2017
 *  @remark     26  March 2017 
 *  @theme      Merge Sort 
 ***************************************************************************/

#include <iostream>
#include <vector>
#include <time.h>
#include <Windows.h>
using namespace std;
static int Max = 7;//自定义的使用插入排序的门限
int _count = 0;//逆序对的个数

/*第一种方法,传入参数为数组指针,左右限*/
//函数作用:合并[left,mid][mid+1,right]
void Merge(int a[],int left,int mid,int right)
{
	//两段区间的长度
	int length1 = mid-left+1;
	int length2 = right-mid;

	//分配两段新的内存空间存储原内容
	int *l1 = new int[length1];
	int *l2 = new int[length2];

	for (int i = 0; i < length1; ++i)
	{
		l1[i] = a[left+i];
	}
	for (int j = 0; j < length2; ++j)
	{
		l2[j] = a[j+mid+1];
	}

	//存入原内容之后,比较两个序列
	int i = 0,j = 0;
	int k = length1;
	//比较两个序列的重合部分,进行排序
	while (i<length1 && j<length2)
	{
		if (l1[i] < l2[j])
		{
			a[left++] = l1[i++];
		}
		else
		{
			a[left++] = l2[j++];
			//因为l2[j]大于l1[i],所以l2[j]肯定大于[0,length-1]之中[0,i]之间的所有数,产生逆序对
			if (l2[j] > l1[i])
			{
				_count +=  length1-i+1;
			}		
		}
	}
	//两序列的剩余部分分别放于结尾
	while (i<length1)
	{
		a[left++] = l1[i++];
	}

	while (j<length2)
	{
		a[left++] = l2[j++];
	}

	//分配的内存需要释放掉
	delete []l1;
	delete []l2;
}
void Merge_sort(int a[],int left,int right)
{
	if (left < right)
	{
		int mid = (left+right)/2;//首先进行分区,然后递归操作
		Merge_sort(a,left,mid);
		Merge_sort(a,mid+1,right);//第一次将其分为两个区间,进行合并操作
		Merge(a,left,mid,right);
	}
}

/*第二种方法,传入参数两个迭代器*/
//Merge函数的重载,接受三个迭代器
void Merge(vector<int>::iterator begin,vector<int>::iterator mid,vector<int>::iterator end)
{
	int length1 = (mid-begin)+1;
	int length2 = end-mid;

	int *p1 = new int[length1];
	int *p2 = new int[length2];

	for (int i=0; i < length1; ++i)
	{
		p1[i] = *(begin+i);
	}
	for (int j=0; j < length2; ++j)
	{
		p2[j] = *(mid+1+j);
	}

	int i = 0;
	int j = 0;
	int k = length1;
	while (i<length1 && j<length2)
	{
		if(p1[i] < p2[j])
		{
			*(begin++) = p1[i++];
		}
		else
		{
			*(begin++) = p2[j++];

		}
	}

	while(i < length1)
	{
		*begin++ = p1[i++];
	}
	while(j < length2)
	{
		*begin++ = p2[j++];
	}

	delete []p1;
	delete []p2;
}
//对函数进行重载,传入一对迭代器,并进行第一次改进
void Merge_sort(vector<int>::iterator begin, vector<int>::iterator end)
{
	//若只传递了一个参数,无需进行排序,直接输出
	if (begin >= end)
	{
		return;
	}
	//对长度较小的自序列使用插入排序
// 	if ((end-begin) <= Max)
// 	{
// 		Insertion_sort(begin,end);
// 	}

	if (begin != end)
	{
		auto mid = begin+(end - begin)/2;
		Merge_sort(begin,mid);
		Merge_sort(mid+1,end);//第一次将其分为两个区间,进行合并操作
		if (*(mid+1) >= *mid) 
		{
			return;
		}
		Merge(begin,mid,end);
	}
}
	
int main(int argc, char **argv)
{
	clock_t Start_time = clock();
	int a[10] = {23,56,78,6,59,15,49,81,15,56};
	vector<int> vec(a,a+10);
	Merge_sort(a,0,9);
	for (size_t i = 0; i != 10; ++i)
	{
		cout<<a[i]<<" ";
	}

	cout<<endl;
	Merge_sort(vec.begin(),vec.end()-1);//使用迭代器的版本出现的错误很低级,困扰了我一晚上,结果发现是传入的end迭代器是不可访问的
	for (size_t i = 0; i != 10; ++i)
	{
		cout<<vec[i]<<" ";
	}
	cout<<endl;
	cout<<"逆序对个数:"<<_count<<endl;
	clock_t End_time = clock();
	cout<<"Running time is :"<<static_cast<double>(End_time-Start_time)/CLOCKS_PER_SEC*1000<<" ms"<<endl;
	cin.get();
	return 0;
}

小规模子数组使用插入排序,用不同的方法处理小规模问题能改进大多数递归算法的性能,因为递归会使小规模问题中方法的调用过于频繁,所以改进对它们的处理方法就能改进整个算法。对排序来说,我们已经知道插入排序(或者选择排序)非常简单,因此很可能在小数组上比归并排序更快。和之前一样,一幅可视轨迹图能够很好地说明归并排序的行为方式。图中的可视轨迹图显示的是改良后的归并排序的所有操作。使用插入排序处理小规模的子数组(比如长度小于15)一般可以将归并排序的运行时间缩短10%~15%。


我们可以添加一个判断条件,如果a[mid]小于等于a[mid+1],我们就认为数组已经是有序的并跳过merge()方法。这个改动不影响排序的递归调用,但是任意有序的子数组算法的运行时间就变为线性的了。


复杂度分析:

思想:运用分治法思想解决排序问题。
最坏情况运行时间:O(nlgn)
最佳运行时间:O(nlgn)

最优时间复杂度 O(n)
最差空间复杂度 O(n)


特点分析:稳定算法stable sort、Out-place sort

例题1:有10个文件,每个文件1G,每个文件的每一行存放的都是用户的query,每个文件的query都可能重复。要求你按照query的频度排序。
1、hash映射:顺序读取10个文件,按照hash(query)%10的结果将query写入到另外10个文件(记为)中。这样新生成的文件每个的大小大约也1G(假设hash函数是随机的)。
2、hash统计:找一台内存在2G左右的机器,依次对用hash_map(query, query_count)来统计每个query出现的次数。注:hash_map(query,query_count)是用来统计每个query的出现次数,不是存储他们的值,出现一次,则count+1。
3、堆/快速/归并排序:利用快速/堆/归并排序按照出现次数进行排序。将排序好的query和对应的query_cout输出到文件中。这样得到了10个排好序的文件(记为)。对这10个文件进行归并排序(内排序与外排序相结合)。


例题2:微软2010年笔试题,在一个排列中,如果一对数的前后位置与大小顺序相反,即前面的数大于后面的数,那么它们就称为一个逆序数对。一个排列中逆序的总数就称为这个排列的逆序数。如{2,4,3,1}中,2和1,4和3,4和1,3和1是逆序数对,因此整个数组的逆序数对个数为4,现在给定一数组,要求统计出该数组的逆序数对个数。


见上面程序。加入一个判断即可,且时间复杂度为O(nlogn)




    原文作者:递归与分治算法
    原文地址: https://blog.csdn.net/misayaaaaa/article/details/66478422
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞