快速排序3种实现方法及优化(详细图解)

目录

前言

🍁祝大家每天都能进步!!!

什么是快速排序

一.方法一左右指针法

1.1单趟排序图解

1.2单趟代码

1.3多趟递归图解1

1.4多趟递归图解2

1.5多趟递归代码

二.方法二挖坑法

2.1挖坑法单趟图解

2.2挖坑法单趟代码

2.3多趟递归代码

三.方法三前后指针法

3.1前后指针法单趟排序图解

 3.2单趟前后指针法代码

3.3多趟递归代码

四.快速排序优化

4.1三数取中

 4.4小区间优化

 五.非递归快速排序

5.1非递归图解

5.2先创建个功能较齐全的栈

5.3用栈实现非递归快排

六.全部代码

Stack.h

Test.C

运行结果


  • 🙏🙏你们的关注是我创作的最大动力🙏🙏

前言

🍁祝大家每天都能进步!!!

什么是快速排序

      这种排序算法综合了插入排序和分治的思想,通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可以分别对这两部分记录继续进行排序,以达到整个序列有序的目的。由于其时间复杂度为O(nlogn),即使在如今,快速排序仍然被誉为最好的算法之一。

一.方法一左右指针法

     快速排序左右指针法是一种高效的排序算法,其基本步骤如下:

     首先,选取数组的最后一个元素作为基准数key。然后进行分区过程:从数组的首元素开始向后找比key大的数,从数组尾部开始向前找比key小的数,找到后进行交换,直到首元素大于或等于尾部元素的位置,此时将头部元素与数组尾部元素进行交换,这样,key就位于了正确的位置,左边的所有数都小于key,右边的所有数都大于key。

     接着,对左区间和右区间重复第二步的操作,直到各区间只有一个数为止。

1.1单趟排序图解

1.2单趟代码

//左右指针法 [begin,end]
int PartShort1(int* a, int left, int right)
{
	//int midIndex = GetMidIndex(a, begin, end);
	//Swap(&a[midIndex], &a[end]);

	int keyindex = right;
	while (left < right)
	{
		//begin找大
		while (left < right && a[left] <= a[keyindex])
		{
			++left;
		}
		//end找小
		while (left < right && a[right] >= a[keyindex])
		{
			--right;

		}

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

	}

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

	return left;
}

1.3多趟递归图解1

[递归](递归不熟练的可以看这里,详细的递归分析图解)

1.4多趟递归图解2

1.5多趟递归代码

//左右指针法 [begin,end]
int PartShort1(int* a, int left, int right)
{
	//int midIndex = GetMidIndex(a, begin, end);
	//Swap(&a[midIndex], &a[end]);

	int keyindex = right;
	while (left < right)
	{
		//begin找大
		while (left < right && a[left] <= a[keyindex])
		{
			++left;
		}
		//end找小
		while (left < right && a[right] >= a[keyindex])
		{
			--right;

		}

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

	}

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

	return left;
}

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

	//[left,div-1] div [div+1,right]
	QuickSort(a, left, div - 1);
	QuickSort(a, div+1, right);
}

二.方法二挖坑法

     首先,选取数列中的一个元素作为基准值,通常选择最左侧或最右侧的元素。然后,重新排列数列,使得比枢轴值大的元素在其右边,比枢轴值小的元素在其左边,枢轴值位于其最终排序后的位置。这个过程被称作挖坑过程,这也是此方法得名的原因。

     接着,将枢轴值与挖好的坑位进行交换。需要注意的是,这里不是简单的元素交换,而是将枢轴值放到一个预留的位置,这个位置就是所谓的“坑”。

最后,对枢轴左右两侧的子序列重复上述步骤,直到序列长度为1或0,此时序列已经完全有序。

2.1挖坑法单趟图解

2.2挖坑法单趟代码

//挖坑法
int PartShort2(int* a, int begin, int end)
{
	int midIndex = GetMidIndex(a, begin, end);
	Swap(&a[midIndex], &a[end]);

	//坑
	int key = a[end];
	while (begin < end)
	{
		if (begin < end && a[begin] <= key)
		{
			++begin;
		}
		//左边找到比key大的填到右边的坑,begin位置形成新的坑
		a[end] = a[begin];


		while (begin < end && a[end] >= key)
		{
			--end;
		}
		//右边找到比key小的填到左边的坑,end位置形成新的坑
		a[begin] = a[end];
	}

	a[begin] = key;
	return begin;
}

2.3多趟递归代码

//挖坑法
int PartShort2(int* a, int begin, int end)
{
	int midIndex = GetMidIndex(a, begin, end);
	Swap(&a[midIndex], &a[end]);

	//坑
	int key = a[end];
	while (begin < end)
	{
		if (begin < end && a[begin] <= key)
		{
			++begin;
		}
		//左边找到比key大的填到右边的坑,begin位置形成新的坑
		a[end] = a[begin];


		while (begin < end && a[end] >= key)
		{
			--end;
		}
		//右边找到比key小的填到左边的坑,end位置形成新的坑
		a[begin] = a[end];
	}

	a[begin] = key;
	return begin;
}

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

	int div = PartShort2(a, left, right);
	QuickSort(a, left, div - 1);
	QuickSort(a, div+1, right);
}

三.方法三前后指针法

3.1前后指针法单趟排序图解

 3.2单趟前后指针法代码

//前后指针法
int PartShort3(int* a, int begin, int end)
{
	int prev = begin - 1;
	int cur = begin;
	int keyindex = end;

	while (cur < end)
	{
		if (a[cur] < a[keyindex] && ++prev != cur)
			Swap(&a[prev], &a[cur]);

		++cur;

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

	return prev;
}

3.3多趟递归代码

递归图在方法一图解那[递归基础]

//前后指针法
int PartShort3(int* a, int begin, int end)
{
	int prev = begin - 1;
	int cur = begin;
	int keyindex = end;

	while (cur < end)
	{
		if (a[cur] < a[keyindex] && ++prev != cur)
			Swap(&a[prev], &a[cur]);

		++cur;

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

	return prev;
}

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

	int div = PartShort3(a, left, right);
	QuickSort(a, left, div - 1);
	QuickSort(a, div+1, right);
}

四.快速排序优化

4.1三数取中

   快速排序的时间复杂度在最好、最坏和平均情况下分别是O(nlogn)、O(n^2)和O(nlogn)。最好情况是指每次划分都能使枢轴元素位于序列的中间位置,使得每次递归的区间长度都减半;最坏情况是指每次划分都使枢轴元素位于已排序序列的一端,导致每次递归的区间长度为1;平均情况通常以数组的长度为基准。

    三数取中是,取三个部分,取左端、中间、右端三个值进行比较,选大小为中间的。

用快速排序,排有序数组,时间复杂度将会变成最坏,也就是O(n^2)。但使用三数取中把快速排序优化就可以避免最坏情况出现,就不存在最坏时间复杂度,所以平均时间复杂度就直接为O(nlogn)。

//三数取中
int GetMidIndex(int* a, int begin, int end)
{
	int mid = (begin + end) / 2;
	if (a[begin] < a[mid])
	{
		if (a[mid] < a[end])
		{
			return mid;
		}
		else if (a[begin] > a[end])
		{
			return begin;
		}
		else
		{
			return end;
		}
	}
	else//a[begin] > a[mid]
	{
		if (a[mid] > a[end])
		{
			return mid;
		}
		else if (a[begin] < a[end])
		{
			return begin;
		}
		else
		{
			return end;
		}
	}
}
//挖坑法
int PartShort2(int* a, int begin, int end)
{
	int midIndex = GetMidIndex(a, begin, end);
	Swap(&a[midIndex], &a[end]);

	//坑
	int key = a[end];
	while (begin < end)
	{
		if (begin < end && a[begin] <= key)
		{
			++begin;
		}
		//左边找到比key大的填到右边的坑,begin位置形成新的坑
		a[end] = a[begin];


		while (begin < end && a[end] >= key)
		{
			--end;
		}
		//右边找到比key小的填到左边的坑,end位置形成新的坑
		a[begin] = a[end];
	}

	a[begin] = key;
	return begin;
}

 4.4小区间优化

递归调用时需要创建栈帧来保存当前的环境,当数据个数小于一定数值时,创建栈帧相对就会较慢。如c语言库函数中,个数小于10时直接把当前快速排序算法切换成插入排序。如下:



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


	if ((right - left + 1) > 10)
	{
		int div = PartShort3(a, left, right);

		QuickSort(a, left, div - 1);
		QuickSort(a, div + 1, right);
	}
	else
	{
		//小于10个以内的区间,不再递归排序
		 InsertSort(a+left, right-left+1);
	}
}

 五.非递归快速排序

5.1非递归图解

  其实就是把递归思想利用数据结构栈表现出来,与下图的递归思想是一样的

5.2先创建个功能较齐全的栈

栈(Stack)是一种特殊的线性表,它只允许在表的一端进行插入和删除运算。这一端被称为栈顶(top),相对地,把另一端称为栈底(bottom)。向一个栈插入新元素又称作进栈、入栈或压栈(push),它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈(pop),它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。因此,栈具有“后进先出”(LIFO)的特点。

栈的基本操作包括初始化空栈、判断一个栈是否为空、进栈、出栈、读栈顶元素以及销毁栈等。如果用数据结构来描述,栈可以采用顺序存储或链式存储。顺序栈是利用一组地址连续的存储单元存放自栈底到栈顶的数据元素,同时附设一个指针(top)指示当前栈顶的位置。而链式存储的栈,其节点中需含有指针域,指向直接前驱和直接后继的节点。无论是采用哪种方式,其核心操作—压栈和弹栈的时间复杂度都为O(1)。

除此之外,当发生栈满(称为上溢)而继续压栈或者栈空(称为下溢)而继续弹栈时,都会导致错误。这种情况需要特殊处理。

[这里有栈的基础与细节]

#include"Stack.h"
void StackInit(ST* ps)
{
	assert(ps);
	ps->a = NULL;
	ps->top = ps->capacity = 0;
}
void StackDestroy(ST* ps)
{
	assert(ps);
	free(ps->a);
	ps->a = NULL;
	ps->top = ps->capacity = 0;
}
void StackPush(ST* ps, STDataType x)
{
	assert(ps);
	if (ps->top == ps->capacity)
	{
		int newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
		STDataType* tmp = realloc(ps->a, sizeof(STDataType) * newcapacity);
		if (tmp == NULL)
		{
			printf("realloc fail\n");
			exit(-1);
		}
		ps->a = tmp;
		ps->capacity = newcapacity;
	}
	ps->a[ps->top] = x;
	ps->top++;
}
void StackPop(ST* ps)
{
	assert(ps);
	assert(!StackEmpty(ps));
	ps->top--;
}
STDataType StackTop(ST* ps)
{
	assert(ps);
	assert(!StackEmpty(ps));

	return ps->a[ps->top - 1];
}
int StackSize(ST* ps)
{
	assert(ps);
	return ps->top;
}



bool StackEmpty(ST* ps)
{
	return ps->top == 0;
}


5.3用栈实现非递归快排

//前后指针法
int PartShort3(int* a, int begin, int end)
{
	int prev = begin - 1;
	int cur = begin;
	int keyindex = end;

	while (cur < end)
	{
		if (a[cur] < a[keyindex] && ++prev != cur)
			Swap(&a[prev], &a[cur]);

		++cur;

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

	return prev;
}


void QuickSortNonR(int* a, int left, int right)
{
	ST st;
	StackInit(&st);

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

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

		//[begin,left]
		int div = PartShort3(a, begin, end);
		//[begin,div-1] div [div+1,end]

		if (div + 1 <= end)
		{
			StackPush(&st, end);
			StackPush(&st, div + 1);
		}

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

	}
	StackDestroy(&st);

}

六.全部代码

Stack.h

#include"Stack.h"
void StackInit(ST* ps)
{
	assert(ps);
	ps->a = NULL;
	ps->top = ps->capacity = 0;
}
void StackDestroy(ST* ps)
{
	assert(ps);
	free(ps->a);
	ps->a = NULL;
	ps->top = ps->capacity = 0;
}
void StackPush(ST* ps, STDataType x)
{
	assert(ps);
	if (ps->top == ps->capacity)
	{
		int newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
		STDataType* tmp = realloc(ps->a, sizeof(STDataType) * newcapacity);
		if (tmp == NULL)
		{
			printf("realloc fail\n");
			exit(-1);
		}
		ps->a = tmp;
		ps->capacity = newcapacity;
	}
	ps->a[ps->top] = x;
	ps->top++;
}
void StackPop(ST* ps)
{
	assert(ps);
	assert(!StackEmpty(ps));
	ps->top--;
}
STDataType StackTop(ST* ps)
{
	assert(ps);
	assert(!StackEmpty(ps));

	return ps->a[ps->top - 1];
}
int StackSize(ST* ps)
{
	assert(ps);
	return ps->top;
}



bool StackEmpty(ST* ps)
{
	return ps->top == 0;
}


Test.C

#include<stdio.h>
#include<assert.h>
#include"Stack.h"
void PrintArry(int* a, int n)
{
	for (int i = 0; i < n; ++i)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}

void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}


//三数取中
int GetMidIndex(int* a, int begin, int end)
{
	int mid = (begin + end) / 2;
	if (a[begin] < a[mid])
	{
		if (a[mid] < a[end])
		{
			return mid;
		}
		else if (a[begin] > a[end])
		{
			return begin;
		}
		else
		{
			return end;
		}
	}
	else//a[begin] > a[mid]
	{
		if (a[mid] > a[end])
		{
			return mid;
		}
		else if (a[begin] < a[end])
		{
			return begin;
		}
		else
		{
			return end;
		}
	}
}

//左右指针法 [begin,end]
int PartShort1(int* a, int left, int right)
{
	//int midIndex = GetMidIndex(a, begin, end);
	//Swap(&a[midIndex], &a[end]);

	int keyindex = right;
	while (left < right)
	{
		//begin找大
		while (left < right && a[left] <= a[keyindex])
		{
			++left;
		}
		//end找小
		while (left < right && a[right] >= a[keyindex])
		{
			--right;

		}

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

	}

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

	return left;
}


//挖坑法
int PartShort2(int* a, int begin, int end)
{
	int midIndex = GetMidIndex(a, begin, end);
	Swap(&a[midIndex], &a[end]);

	//坑
	int key = a[end];
	while (begin < end)
	{
		if (begin < end && a[begin] <= key)
		{
			++begin;
		}
		//左边找到比key大的填到右边的坑,begin位置形成新的坑
		a[end] = a[begin];


		while (begin < end && a[end] >= key)
		{
			--end;
		}
		//右边找到比key小的填到左边的坑,end位置形成新的坑
		a[begin] = a[end];
	}

	a[begin] = key;
	return begin;
}


//前后指针法
int PartShort3(int* a, int begin, int end)
{
	int prev = begin - 1;
	int cur = begin;
	int keyindex = end;

	while (cur < end)
	{
		if (a[cur] < a[keyindex] && ++prev != cur)
			Swap(&a[prev], &a[cur]);

		++cur;

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

	return prev;
}





//快速排序
//时间复杂度:O(N*logN)
//空间复杂度:O(logN)
//[left,right]
void QuickSort(int* a, int left, int right)
{
	assert(a);
	if (left >= right)return;

	int div = PartShort3(a, left, right);
	QuickSort(a, left, div - 1);
	QuickSort(a, div+1, right);
}


void QuickSortNonR(int* a, int left, int right)
{
	ST st;
	StackInit(&st);

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

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

		//[begin,left]
		int div = PartShort3(a, begin, end);
		//[begin,div-1] div [div+1,end]

		if (div + 1 <= end)
		{
			StackPush(&st, end);
			StackPush(&st, div + 1);
		}

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

	}
	StackDestroy(&st);

}

void TestQuickSort1()
{
	printf("QuickSort:递归快排\n");
	int a[] = { 26,25,24,23,22,21,20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1 };
	PrintArry(a, sizeof(a) / sizeof(a[0]));
	QuickSort(a, 0, sizeof(a) / sizeof(a[0]) - 1);
	PrintArry(a, sizeof(a) / sizeof(a[0]));
	printf("\n\n");
}

void TestQuickSortNonR()
{
	printf("QuickSortNonR:非递归快排\n");
	int a[] = { 26,25,24,23,22,21,20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1 };
	PrintArry(a, sizeof(a) / sizeof(a[0]));
	QuickSortNonR(a, 0, sizeof(a) / sizeof(a[0]) - 1);
	PrintArry(a, sizeof(a) / sizeof(a[0]));
}



int main()
{
	TestQuickSort1();
	TestQuickSortNonR();
}

运行结果

以上就是本期补齐的内容,欢迎参考指正,如有不懂,欢迎评论或私信出下期!!!  

  • 19
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

锻炼²

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值