归并排序与快速排序


排序算法是最基本的算法之一。
可划分为 内部排序外部排序
内部排序是数据记录在内存中进行排序
外部排序是因为排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。
常见的排序有冒泡排序,选择排序,插入排序,希尔排序,归并排序,快速排序,堆排序,计数排序,桶排序,基数排序等。
本篇博客主要详细讲解一下 归并排序快速排序
在这里插入图片描述

归并排序

归并排序采用分而治之的思想,其基本思想是:将有序的子序合并,从而得到完全有序的序列。

动图演示:
动图演示
如何进行归并排序呢,首先要得到有序子序,当序列分解到只有一个元素时就认为它是有序的,这时就可以进行合并了。
在这里插入图片描述

递归实现

void MergeSort_(int* a,int left,int right,int* tmp) 
{//传入参数 需要排序的数组a,数组左下标,右下标的值,以及新创建的数组tmp
    if(left >= right)   //归并结束条件,当只有一个数据或数据不存在时,结束分解
    {
        return ;
    }
    int mid = left + (right - left)/2;   //得到中间下标的
    MergeSort_(a,left,mid,tmp);    //对左边的序列进行归并
    MergeSort_(a,mid+1,right,tmp); //对右边的序列进行归并
    int begin1 = left,end1 = mid;
    int begin2 = mid+1,end2 = right;
    //标记两部分的起始数据的下标与终止数据的下标
    int i = left;
    while(begin1 <= end1&&begin2 <= end2)
    {
        //将小的数据优先放入tmp中
        if(a[begin1] < a[begin2]) 
        {
            tmp[i++] = a[begin1++];
        }
        else 
        {
            tmp[i++] = a[begin2++];
        }
    }
    //遍历完一个区间后将剩下的一个直接放到tmp后面
    while(begin1 <= end1) 
    {
        tmp[i++] = a[begin1++];
    }
    while(begin2 <= end2)
    {
        tmp[i++] = a[begin2++];
    }
    //归并完成后,拷贝回原数组
    int j = 0 ;
    for(j = left;j <= right;j++)
    {
        a[j] = tmp[j];
    }
}

void MergeSort(int* a,int n)
{
    int* tmp = (int*)malloc(sizeof(int)*n);   //申请一个与原数组相同的空间
    if(tmp == NULL)
    {
        printf("malloc fail\n");
        exit(-1);
    }
    MergeSort_(a,0,n-1,tmp);  //归并排序
    free(tmp);//释放空间
}

使用递归实现需要创建一个新的数组用于排序,排序后将数据拷贝回原数组。

非递归实现

非递归实现我们只需要控制每次参与合并的元素个数gap即可,在函数内利用循环进行排序。
在这里插入图片描述
三种特殊情况:
情况一:
当最后一个小组进行排序时,第二个区间内存在元素,但个数不足gap个,此时我们需要控制第二个区间的边界。如下图,我们需要将第二个区间的边界-1。
在这里插入图片描述
情况二:
当最后一个小组进行合并时,第二个区间内不存在元素,此时不需要对该小组进行合并。
在这里插入图片描述
情况三:
当最后一个小组进行合并时,第二个小区间不存在,并且第一个小区间元素个数不足gap个,此时也不需要对该小组进行合并。(可与情况2视为一类)
在这里插入图片描述
将这三种情况特殊讨论,得到代码如下

//归并排序(子函数)
void _MergeSortNonR(int* a, int* tmp, int begin1, int end1, int begin2, int end2)
{
	int j = begin1;
	//将两段子区间进行归并,归并结果放在tmp中
	int i = begin1;
	while (begin1 <= end1&&begin2 <= end2)
	{
		//将较小的数据优先放入tmp
		if (a[begin1] < a[begin2])
			tmp[i++] = a[begin1++];
		else
			tmp[i++] = a[begin2++];
	}
	//当遍历完其中一个区间,将另一个区间剩余的数据直接放到tmp的后面
	while (begin1 <= end1)
		tmp[i++] = a[begin1++];
	while (begin2 <= end2)
		tmp[i++] = a[begin2++];
	//归并完后,拷贝回原数组
	for (; j <= end2; j++)
		a[j] = tmp[j];
}
//归并排序(主体函数)
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int)*n);//申请一个与待排序列大小相同的空间,用于辅助合并序列
	if (tmp == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	int gap = 1;//需合并的子序列中元素的个数
	while (gap < n)
	{
		int i = 0;
		for (i = 0; i < n; i += 2 * gap)
		{
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;
			if (begin2 >= n)//最后一组的第二个小区间不存在或是第一个小区间不够gap个,此时不需要对该小组进行合并
				break;
			if (end2 >= n)//最后一组的第二个小区间不够gap个,则第二个小区间的后界变为数组的后界
				end2 = n - 1;
			_MergeSortNonR(a, tmp, begin1, end1, begin2, end2);//合并两个有序序列
		}
		gap = 2 * gap;//下一趟需合并的子序列中元素的个数翻倍
	}
	free(tmp);//释放空间
}

快速排序

快速排序是如今最常使用的排序,它的基本思想为:任取一个基准值,将所有小于基准值的元素放在基准值左边,将所有大于基准值的元素放在基准值右边。然后左右重复这一过程,直到所有元素排序完毕即可。
对于如何按照基准值将待排序列分为两个序列,常见有三种方式

  1. Hoare版本
  2. 挖坑法
  3. 前后指针法

Hoare版本

动图演示:
在这里插入图片描述
单趟排序的基本步骤如下:

  • 选出一个key,一般为最左边或最右边的
  • 定义一个L和一个R,L从左向右走,R从右向左走。(注意:若选择左边的数据作为key,则需要R先走;若选择右边的数据作为key,则需要L先走)
  • 在走的过程中,若R遇到小于key的数则停止运动,L开始走,直到L遇到一个大于key的数,则R与L所在地内容交换。R继续往前走,反复进行,当L与R相遇,此时相遇点与key交换即可。(选取左边的值作为key)

经过一次单趟排序后,使得key左边的元素都小于key,右边的元素都大于key。
然后将key左边和右边的元素再次进行单趟排序,如此反复操作,直到左右序列只有一个数据或左右序列不存在时,停止操作,因为这种序列可以认为是有序的。
代码:


```c
//快速排序(Hoare版本)
void QuickSort1(int* a, int begin, int end)
{
	if (begin >= end)//当只有一个数据或是序列不存在时,不需要进行操作
		return;
		
	int left = begin;//L
	int right = end;//R
	int keyi = left;//key的下标
	while (left < right)
	{
		//right先走,找小
		while (left < right&&a[right] >= a[keyi])
		{
			right--;
		}
		//left后走,找大
		while (left < right&&a[left] <= a[keyi])
		{
			left++;
		}
		if (left < right)//交换left和right的值
		{
			Swap(&a[left], &a[right]);
		}
	}
	int meeti = left;//L和R的相遇点
	Swap(&a[keyi], &a[meeti]);//交换key和相遇点的值

	QuickSort1(a, begin, meeti - 1);//key的左序列进行此操作
	QuickSort1(a, meeti + 1, end);//key的右序列进行此操作
}

挖坑法

在这里插入图片描述
挖坑法的单趟排序基本步骤:

  • 选出一个数据(一般是最左边的或最右边的)存放在key变量中,在该数据位置形成一个坑。
  • 还是定义一个L和一个R,L从左向右走,R从右向左走。(若在最左边挖坑,则需要R先走;若在最右边挖坑,则需要L先走)。
  • 在走的过程中,若R遇到小于key的数,则将该数抛入坑位,并在此处形成一个坑位,这时L在向右走,若遇到大于key的数,则将其抛入坑位,形成一个新的坑位。如此循环下去,直到R与L相遇,这时将key抛入坑位即可。(选取最左边的作为坑位)

经过一次单趟排序后,使得key左边的元素都小于key,右边的元素都大于key。
然后将key左边和右边的元素再次进行单趟排序,如此反复操作,直到左右序列只有一个数据或左右序列不存在时,停止操作,因为这种序列可以认为是有序的。
代码:

//快速排序(挖坑法)
void QuickSort2(int* a, int begin, int end)
{
	if (begin >= end)//当只有一个数据或是序列不存在时,不需要进行操作
		return;

	int left = begin;//L
	int right = end;//R
	int key = a[left];//在最左边形成一个坑位
	while (left < right)
	{
		//right向左,找小
		while (left < right&&a[right] >= key)
		{
			right--;
		}
		//填坑
		a[left] = a[right];
		//left向右,找大
		while (left < right&&a[left] <= key)
		{
			left++;
		}
		//填坑
		a[right] = a[left];
	}
	int meeti = left;//L和R的相遇点
	a[meeti] = key;//将key抛入坑位

	QuickSort2(a, begin, meeti - 1);//key的左序列进行此操作
	QuickSort2(a, meeti + 1, end);//key的右序列进行此操作
}

前后指针法

在这里插入图片描述
前后指针的单趟排序的基本步骤:

  • 选出一个key,一般是最左边的或最右边的
  • 起始时,prev指针指向序列开头,cur指针指向prev+1
  • 若cur指向的内容小于key,则prev先向后移动一位,然后交换prev和cur指针指向的内容,然后cur指针++;若cur指向的内容大于key,则cur指针直接++。如此进行下去,直到cur指针越界,此时将key和prev指针指向的内容交换即可。

经过一次单趟排序后,使得key左边的元素都小于key,右边的元素都大于key。
然后将key左边和右边的元素再次进行单趟排序,如此反复操作,直到左右序列只有一个数据或左右序列不存在时,停止操作,因为这种序列可以认为是有序的。
代码:

//快速排序(前后指针法)
void QuickSort3(int* a, int begin, int end)
{
	if (begin >= end)//当只有一个数据或是序列不存在时,不需要进行操作
		return;

    //三数取中
	// int midIndex = GetMidIndex(a, begin, end);
	// Swap(&a[begin], &a[midIndex]);

	int prev = begin;
	int cur = begin + 1;
	int keyi = begin;
	while (cur <= end)//当cur未越界时继续
	{
		if (a[cur] < a[keyi] && ++prev != cur)//cur指向的内容小于key
		{
			Swap(&a[prev], &a[cur]);
		}
		cur++;
	}
	int meeti = prev;//cur越界时,prev的位置
	Swap(&a[keyi], &a[meeti]);//交换key和prev指针指向的内容

	QuickSort3(a, begin, meeti - 1);//key的左序列进行此操作
	QuickSort3(a, meeti + 1, end);//key的右序列进行此操作
}

快排的两个优化

三数取中

快排在理想状态下,每次单趟排序结束后,key左右的数长度应该相同
在这里插入图片描述
每次选取key,如果key刚好为中间值时,则效率会很高,如果key为最大值或最小值时,那么快排的效率将会很低。
在这里插入图片描述
其实,对快排效率影响最大的是key的选取,key越接近中间位置,效率越高。
为了避免极端事件的发生,我们可以采取三数取中的方法
三数取中:三数指的是最左边的数,最右边的数以及中间位置的数。三数取中就是获取三个数中大小居中的那个数作为key,这就确保了我们选取的key不是最大值或最小值。

小区间优化

我们可以看到,就算是上面理想状态下的快速排序,也不能避免随着递归的深入,每一层的递归次数会以2倍的形式快速增长。
 为了减少递归树的最后几层递归,我们可以设置一个判断语句,当序列的长度小于某个数的时候就不再进行快速排序,转而使用其他种类的排序。小区间优化若是使用得当的话,会在一定程度上加快快速排序的效率,而且待排序列的长度越长,该效果越明显。

小结

  • 此篇博客主要讲解了归并排序与快速排序
  • 如有错误,欢迎指正!!
  • 详细讲解或其它排序可见

八大排序算法(C语言实现)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值