快排、归并排序、计数排序

本文详细介绍了快速排序的hoare版本、挖坑法、左右指针法及其优化策略,包括三数取中和小区间优化。同时,讲解了归并排序的原理和特点,以及计数排序的适用场景和复杂度分析。
摘要由CSDN通过智能技术生成

目录

  • 快排整体思路
  • 快排实现
    • hoare版本
    • 挖坑法
    • 前后指针法
    • 快排优化(三数取中,小区间优化)
    • 快排非递归
  • 归并排序
  • 计数排序

一、快排整体思路

        在待排序元素中任取一元素作为基准值,然后将小于该元素的其他元素放到其左边,大于其元素的放到其右边,那么该元素就放在了他应该处在的位置,然后不断递归,对其左右子序列做重复的操作,使每个元素都处在应该放在的位置。

快排的特性:

1、快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快排;(极端情况:比如全是相同的数如2,排一大堆相同的数,或则重复率特别高的数效率会大大折扣,但这样的场景很少。)

 

2、时间复杂度:O(N * logN);

3、空间复杂度:O(logN);

4、稳定性:不稳定;

二、快排单趟

1、hoare版本

①选最左边的数做key,让右边先走,让右边找比key小的数,找到后停下;(选左边做key必须让右边先走)

②然后让左边再走,找到比key大的数停下来;

③交换左右找到的数;

④不断重复,直到左右相遇则停下;

⑤最后交换key与上述相遇的值

图解如下:

// 快速排序hoare版本
int PartSort1(int* a, int left, int right)
{
	int key = left;
	while (left < right)
	{
		//Ⅰ 这里必须加=,避免特殊情况,如全是5
		//Ⅱ 也必须带上left < right避免越界,当出现5,6,7,8,9
		//右边先走,找比key小的数,一直找不到,如果不限定就会跑出去
		while (left < right && a[right] >= a[key]) //先找右,找比key小的
			right--;
		while (left < right && a[left] <= a[key]) //再找左,找比key大的
			left++;

		Swap(&a[right], &a[left]);
	}

	Swap(&a[key], &a[left]);

	return left;
}

快排的实现

        单趟排完后,比key小的数就放到了左边,比key大的数就放在了右边,再不断递归,将其分成左右子区间,再在其子区间内再分为两段,直到为一个元素或则区间不存在就停下来。

void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;

	int keyi = PartSort1(a, left, right);

	//[left, keyi - 1] keyi [keyi + 1]
	QuickSort(a, left, keyi - 1); //左子区间
	QuickSort(a, keyi + 1, right); //右子区间
}

2、挖坑法

①先将左边第一个数据存放到临时变量key中,然后此位置形成一个坑位;

②然后让右边开始走,找到比key小的数停下来,将该值放到上述坑位当中,此位置形成新的坑位;

③然后左边再开始走,找到比key大的数停下来,将该值放到上述坑位当中,此位置形成新的坑位;

④不断重复,直到左右相遇

⑤最后将最开始保存的key值填入到坑位当中

 代码实现如下:

// 快速排序挖坑法
int PartSort2(int* a, int left, int right)
{
	int key = a[left];
	int hole = left;
	while (left < right)
	{
		//右边找小,放到左边的坑里面
		while (left < right && a[right] >= key)
			right--;
		a[hole] = a[right];
		hole = right;

		//左边找大,放到右边的坑里面
		while (left < right && a[left] <= key)
			left++;
		a[hole] = a[left];
		hole = left;
	}
	a[hole] = key;
	return hole;
}

3、左右指针法

①选左边做key,prev初始位置为最左边的元素,cur初始位置为prev的下一个元素;

②cur找到比key小的就停下来,++prev;

比较此时的cur与prev,如果相等就不做动作;

如果不相等,那么就交换此时prev与cur里面的值;

⑤当cur走出该序列时,交换此时key与prev里面的值。

总体思路:cur找小,把小的往左边翻,prev则把大的往右边翻

 代码实现如下:

// 快速排序前后指针法
int PartSort3(int* a, int left, int right)
{
	int key = left;
	int prev = left;
	int cur = prev + 1;
	while (cur <= right)
	{
		if (a[cur] < a[key] && ++prev != cur)
		{
			Swap(&a[cur], &a[prev]);
		}

		++cur;
	}
	Swap(&a[prev], &a[key]);

	return prev;
}

4、三数取中

        如果出现对有序或则接近有序进行排序(2,3,4,5,6),那么如果选最左边做key,那么其排序效率则会大大降低,时间复杂度为N^2,那么这时候我们可以选最左边、最右边、中间的数取其最中间的值,然后交换到最左边则会大大提升效率。

int GetMidIndex(int* a, int left, int right)
{
	//避免left和right是两个极大的数,如果使用(left+right)/2可能会撑破内存
	int mid = left + (right - left) / 2;
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
		{
			return mid;
		}
		else if (a[left] > a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
	else
	{
		if (a[left] < a[right])
		{
			return left;
		}
		else if (a[mid] > a[right])
		{
			return mid;
		}
		else
		{
			return right;
		}
	}
}
int PartSort2(int* a, int left, int right)
{
	int min = GetMidIndex(a, left, right);
	Swap(&a[min], &a[left]);

    ......
}

5、小区间优化

        当快排递归时,分割到小区间时,就可以不再用递归分割思路让这段子区间有序,当子区间小到一定程度时,再继续使用递归调用,那么会继续浪费空间,所以我们可以在递归到一定程度时使用插入排序让其有序。

void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;

    //小区间优化,对于递归快排,减少其递归次数
	if (right - left + 1 < 10)
	{
		InsertSort(a, left, right);
	}
	else
	{
		int keyi = PartSort3(a, left, right);
		//[left, keyi - 1] keyi [keyi + 1]
		QuickSort(a, left, keyi - 1);
		QuickSort(a, keyi + 1, right);
	}
}

6、快排非递归版本

        实现非递归,一种方法就是用循环,另外一种就是用数据结构栈进行模拟,递归版本是不断调用栈帧去存储数组下标,那么我们可以用栈来存储每次调用的数组下标,栈也满足后进先出。

// 快速排序 非递归实现
void QuickSortNonR(int* a, int left, int right)
{
	Stack st;
	StackInit(&st);

	StackPush(&st, left);
	StackPush(&st, right);

	while (!StackEmpty(&st))
	{
		int end = StackTop(&st);
		StackPop(&st);

		int begin = StackTop(&st);
		StackPop(&st);

		int key = PartSort2(a, begin, end);
		//[begin, key - 1] key [key + 1, end]
		if (key + 1 < end)
		{
			StackPush(&st, key + 1);
			StackPush(&st, end);
		}

		if (begin < key - 1)
		{
			StackPush(&st, begin);
			StackPush(&st, key - 1);
		}
	}

	StackDestroy(&st);
}

 三、归并排序

1、归并思路:是采用分而治之的思想,将已有序的子序列合并,得到完全有序的序列,即不断用二分法,先使每个子序列有序,再使子序列段间有序。再将两个有序表进行合并。

2、归并的特性:

Ⅰ、归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多在于解决磁盘外排序的问题;

Ⅱ、时间复杂度:O(N*logN);

Ⅲ、空间复杂度:O(N);

Ⅳ、稳定性:稳定;

图片分解与合并如下(图片来自网络):

归并排序的递归和非递归实现,思想分析,时间复杂度分析

 在这里插入图片描述

 

//归并排序
void _MergeSort(int* a, int left, int right, int* tmp)
{
	if (left >= right)
	{
		return ;
	}

	int mid = (left + right) / 2;
	//让[left,mid] [mid+1, right]有序
	_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;
	//小的那个先入tmp数组
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			tmp[i++] = a[begin1++];
		}
		else
		{
			tmp[i++] = a[begin2++];
		}
	}
	//剩下还有没有入完的
	while (begin1 <= end1)
	{
		tmp[i++] = a[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[i++] = a[begin2++];
	}

	//再将tmp里面的数据拷贝回原数组,如果不拷贝回会被覆盖
	for (int j = left; j <= right; j++)
	{
		a[j] = tmp[j];
	}
}
// 归并排序递归实现
void MergeSort(int* a, int n)
{
	//动态申请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);
	tmp = NULL;
}

四、计数排序

计数排序适合范围比较集中的数组,如果range比较大,并且是浮点数就不适用了,空间复杂度为O(range);时间复杂度基本上为O(N),遍历一遍即可。

算法修炼之路 三 排序

 

void CountSort(int* a, int n)
{
	//求出最大值最小值求出range
	//小细节,取数组中的数,不取数组外的数
	int max = a[0], min = a[0];
	for (int i = 1; i < n; i++)
	{
		if (a[i] > max)
		{
			max = a[i];
		}
		if (a[i] < min)
		{
			min = a[i];
		}
	}
	int range = max - min + 1;
	int* count = (int*)malloc(sizeof(int)*range);
	if (count == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	//这里一定要全部初始化为0,不然都是一些随机值
	memset(count, 0, sizeof(int)*range);
	
	//统计次数
	for (int i = 0; i < n; ++i)
	{
		count[a[i] - min]++;
	}
	//根据次数进行排序
	int j = 0;
	for (int i = 0; i < range; ++i)
	{
		while (count[i]--)
		{
			a[j++] = i + min;
		}
	}

	free(count);
	count = NULL;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值