快速排序介绍:

  快速排序是一种非常常用的排序方法,它在1962由C. A. R. Hoare(霍尔)提的一种二叉树结构的交换排序方法,故因此它又被称为霍尔划分,它基于分治的思想,所以整体思路是递归进行的。

基本思想:

任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止.

基本思路:

  1. 先在待排的数中找到一个key值,一般key值在数组第一个数(后续优化过程中,会将待排数的第一个数,中间的数,以及最后一个数进行比较,取中间大的数做key值).
  2. 确定key后,经过操作将key放在其合适位置,即其左边的数都比key小,右边数都比key大,但左边或右边的数并不一定有序,随后在由递归思想将其,key左边以及右边部分分别进行递归处理,最后直到待排区间不存在或只有一个元素时返回.

递归版本

一.hoare版本

为什么先走right,而不是先走left?

这是因为当先走left时,最后在两者相遇前交换了一次数据,此时right指向的值大于key,left往后走与right相遇,key与right交换,此时一个大于key的值便交换到左边,因此不能先走left,当走right时,两者相遇的数一定是比key小或等于key.

步骤展示:

下图是对其进行的详细分析讲解

代码:

void Quicksort(int* a, int left,int right)
{
	if (left >= right)
	{
		return;
	}//n=1时即为left=right,当n<0,left>right
	int Pleft = left;
	int Pright = right;
	int keyi = left;
	while (left < right)
	{
		while (left < right && a[right]>=a[keyi])
		{
			right--;
		}
		while (left < right && a[left]<=a[keyi])
		{
			left++;
		}
		swap(&a[left], &a[right]);
	}
	swap(&a[keyi], &a[left]);
	Quicksort(a, Pleft, left - 1);
	Quicksort(a, left + 1, Pright);
}

二.挖坑法

步骤展示:

代码:

void QuickSort(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int Pleft = left;
	int Pright = right;
	int key = a[left];
	while (right > left)
	{
		while (right > left && a[right] >= key)
		{
			right--;
		}
		a[left] = a[right];
		while (right > left && a[left] <= key)
		{
			left++;
		}
		a[right] = a[left];
	}
	a[left] = key;
	QuickSort(a,Pleft,left-1);
	QuickSort(a,left+1,Pright);
}

三.前后指针法

虽然名字较指针法,但在实际上并没有运用到指针

步骤展示:

int a[] = { 6,1,2,7,9,3,4,5,10,8 };//依旧以该数组为例

pre默认为left,cur默认为pre+1

代码:

int PartQsort3(int* a, int left, int right)
{
	int keyi = left;
	int pre = left;
	int cur = pre + 1;
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++pre != cur)
		{
			swap(&a[cur], &a[pre]);
		}
		cur++;
	}
	swap(&a[pre], &a[keyi]);
	return pre;
}
void QuickSort3(int* a, int left, int right)
{

	int keyi = PartQsort3(a,left,right);
	QuickSort2(a, left, keyi - 1);
	QuickSort2(a, keyi+1, right);
}

非递归版本 

前置知识

栈的相关知识https://blog.csdn.net/2401_88240248/article/details/151156635?fromshare=blogdetail&sharetype=blogdetail&sharerId=151156635&sharerefer=PC&sharesource=2401_88240248&sharefrom=from_link

相关思路

在非递归版本中具体的核心交换可以采用前三种的任意一种,其与前方的本质区别是用栈取代了递归

下图中的区间实际是一个数一个数插入,这里只是为了方便观看,将一个区间写入一个内存块中

代码:

void swap(int* a, int* b)
{
	int temp = *a;
	*a = *b;
	*b = temp;
}

int Partsort(int *a,int left,int right)
{
	if (left >= right)
	{
		return -1;
	}
	int keyi = left;
	while (left < right)
	{
		while (left < right && a[right] >= a[keyi])
		{
			right--;
		}
		while (left < right && a[left] <= a[keyi])
		{
			left++;
		}
		swap(&a[left], &a[right]);
	}
	swap(&a[keyi], &a[left]);
	return left;
}//这里采用的hoare版本的快排

void Qucicksort(int *a,int n)
{
	ST st1;
	STInit(&st1);
	STPush(&st1, n - 1);//先压right
	STPush(&st1,0);//在压left
	while (!STempty(&st1))
	{
		int begin = STTop(&st1);
		STPop(&st1);
		int end= STTop(&st1);
		STPop(&st1);
		int keyi = Partsort(a,begin,end);
		if (keyi != -1)
		{    //压右区间
			STPush(&st1, end);
			STPush(&st1, keyi + 1);
            //压左区间
			STPush(&st1, keyi - 1);
			STPush(&st1, begin);
		}
	}
}

下方是栈中的代码

#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
//用顺序表实现栈
typedef int STtype;
typedef struct Stack {
	STtype* a;
	int top;
	int capacity;
}ST;

//初始化与销毁
void STInit(ST* p);
void STDestory(ST*p);
//压栈  出栈
void STPush(ST* p,STtype x);
void STPop(ST* p);
//获取栈顶元素
STtype STTop(ST* p);
//获取栈中元素个数
int STSize(ST* p);
//判断栈是否为空
bool STempty(ST* p);

void STInit(ST* p)
{
	assert(p);
	p->a = NULL;
	p->capacity = p->top = 0;
}

//压栈
void STPush(ST* p, STtype x)
{
	assert(p);
	if (p->top == p->capacity)
	{
		int newcapcity = p->capacity == 0 ? 4 : p->capacity * 2;
		STtype* temp = (STtype*)realloc(p->a, newcapcity*sizeof(STtype));
		if(temp==NULL)
		{
			perror("realloc fail");
			exit(1);
		}
		p->a = temp;
		p->capacity = newcapcity;
	}
	p->a[p->top] = x;
	p->top++;
}
//出栈
void STPop(ST* p)
{
	assert(p);
	assert(p->top > 0);
	p->top--;
}
//获取栈顶元素
STtype STTop(ST* p)
{
	assert(p);
	assert(p->top);
	return p->a[p->top - 1];
}
//获取栈中元素个数
int STSize(ST* p)
{
	assert(p);
	return p->top;
}
//判断栈是否为空
bool STempty(ST* p)
{
	assert(p);
	return p->top == 0;
}
void STDestory(ST* p)
{
	assert(p);
	free(p->a);
	p->a = NULL;
	p->capacity = p->top = 0;
}

快排的相关优化:

1.三数取中

在确定key值时,我们总会面临如果key的值最小,那么right向一直向前遍历如下数,或最大,left一直后遍历,都会使速率变慢,最后甚至退化到O(N^2),

因此出现了三数取中,使成为key的数即不是最大也不是最小,这样就可以避免了这种局面

三数的取值分别为数组最左边的数,最右边的数,以及中间的数,比较这三个数找到第二大的数,将其位置与left交换,因此其也不会改变left做key值

int FindMidi(int*a,int left, int right)
{
	int midi = (left + right) / 2;
	if (a[midi] < a[left])
	{
		if (a[midi] > a[right])
		{
			return midi;
		}
		else if (a[right] > a[left])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
	//a[midi]>a[left]
	else {
		if (a[left] > a[right])
		{
			return left;
		}
		else if (a[midi]>a[right])
		{
			return right;
		}
		else {
			return midi;
		}
	}
}

2.小区间优化

小区间优化主要运用到了插入排序

将上图每一个圈看成递归所开辟的空间,越到下面区间越小,且开辟的空间越多,因此当我们如果当区间数到达一定数后,不在进行快排而是进行其他类较快处理几个数的排序,那么不仅节省空间,速度也没有较大的影响.

因此小区间优化所选择的为插入排序

下方代码是在hoare基础上加入小区间优化

void swap(int* str1, int* str2)
{
	int  temp = *str1;
	*str1 = *str2;
	*str2 = temp;
}
void InsertSort(int* arr, int n)
{
	for (int i = 0;i < n-1;i++)
	{
		int end=i;
		int temp = arr[end + 1];
		while (end>=0&&arr[end] > temp)
		{
			arr[end + 1] = arr[end];
			end--;
		}
		arr[end+1] = temp;
	}
}
void Quicksort(int* a, int left,int right)
{
	if (left >= right)
	{
		return;
	}//n=1时即为left=right,当n<0,left>right

///////////////添加部分//////////////////////////////
	if (right - left + 1 < 10)
	{
		InsertSort(a + left,right-left+1);
	}
///////////////////////////////////////////////////
	int Pleft = left;
	int Pright = right;
	int keyi = left;
	while (left < right)
	{
		while (left < right && a[right]>=a[keyi])
		{
			right--;
		}
		while (left < right && a[left]<=a[keyi])
		{
			left++;
		}
		swap(&a[left], &a[right]);
	}
	swap(&a[keyi], &a[left]);
	Quicksort(a, Pleft, left - 1);
	Quicksort(a, left + 1, Pright);
}

3.自省排序

在一些特别极端的情况下,例如,左区间一个数,右区间几百万个数,这样最后会由于递归的层次过于深,导致递归会遇见一个常见的问题即为栈溢出

那么如何处理该问题呢,那就是这里所说的自省排序,当深度到达一定程度后结束继续递归的排序,进行堆排序,来避免出现堆溢出的情况

一般将最大深度设为标准深度(即每次都是在区间中间分步)的二倍,如下图

对于深度如何求,即可以参照二叉数的深度计算,当左右均匀分步时,其类似于二叉数,因此深度为logN


堆的相关知识https://blog.csdn.net/2401_88240248/article/details/152210382?fromshare=blogdetail&sharetype=blogdetail&sharerId=152210382&sharerefer=PC&sharesource=2401_88240248&sharefrom=from_link


代码:

void AdjustDown(int* a, int n, int paraent)
{
	int child = paraent * 2 + 1;
	while (child < n)
	{
		if (child+1<n&&a[child] < a[child + 1])
		{
			child = child + 1;
		}

		if (a[child] > a[paraent])
		{
			swap(&a[child], &a[paraent]);
			paraent = child;
			child = paraent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}
void HeapSort(int* a, int n)
{
	for (int i = (n - 1 - 1) / 2;i >= 0;i--)
	{
		AdjustDown(a, n, i);
	}
	int end = n - 1;
	while (end > 0)
	{
		swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		end--;
	}
}

void InsertSort(int* arr, int n)
{
	for (int i = 0;i < n-1;i++)
	{
		int end=i;
		int temp = arr[end + 1];
		while (end>=0&&arr[end] > temp)
		{
			arr[end + 1] = arr[end];
			end--;
		}
		arr[end+1] = temp;
	}
}
int PartQsort(int* a, int left, int right)
{
	int ret = FindMidi(a, left, right);
	swap(&a[ret], &a[left]);
	int keyi = left;
	int pre = left;
	int cur = pre + 1;
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++pre != cur)
		{
			swap(&a[cur], &a[pre]);
		}
		cur++;
	}
	swap(&a[pre], &a[keyi]);
	return pre;
}


void _QuickSort(int* a, int left, int  right, int depth, int curdep)
{
	int N = right - left + 1;//总数
	if (N < 16)
	{
		InsertSort(a + left, N);
		return;
	} 

/////////////////核心////////////////////////////////////
	if (curdep > depth)
	{
		HeapSort(a+left, N);
	}
//////////////////////////////////////////////////////////
	
else
	{
		int keyi = PartQsort(a, left, right);
		_QuickSort(a, left, keyi - 1, depth, curdep+1);
		_QuickSort(a, keyi+1,right, depth, curdep+1);
	}
}

void QuickSort(int* a, int left, int right)//自省排序
{
	int N = right - left + 1;//总数
	int curdep=0;
	int depth = 0;
	for (int i = 1;i <= N;i*=2)
	{
		depth++;
	}
	_QuickSort(a,left,right,depth*2,curdep+1);
}

4.三路划分(不常用)

当一个快排有了自省机制后,基本上不会用到三路划分,三路划分与前方几种优化机制不同,其主要是对单次遍历进行改变

主要在遇到重复数比较多时,利用三路划分可以避免出现栈溢出的现象,但是有了自省则可避免栈溢出,此外三路划分频繁的交换也增添了消耗.

代码

void swap(int* str1, int* str2)
{
	int  temp = *str1;
	*str1 = *str2;
	*str2 = temp;
}
void ThreeQuicksort(int *a,int left ,int right)
{
	if (left >= right)
	{
		return;
	}
	int Pleft = left;
	int Pright = right;
	int key = a[left];
	int cur = left + 1;
	while (cur <= right)
	{
		if (a[cur] < key)
		{
			swap(&a[left], &a[cur]);
			++left;
			++cur;
		}
		else if (a[cur] == key)
		{
			++cur;
		}
		else
		{
			swap(&a[right], &a[cur]);
			right--;
		}
	}
	ThreeQuicksort(a,Pleft,left-1);
	ThreeQuicksort(a,right+1,Pright);
}

总结:

  • 时间复杂度:O(N*logN)
  • 空间复杂度:O(logN)(因为需要不断的开栈,二分划分区间不断向下开空间)
  • 稳定性:不稳定.
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐