排序算法—快速排序

1. 快速排序的算法思想

  • 快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中 的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

2. 快速排序的三种基本方法(递归实现)

  • hoare法(又称作左右指针法)

定义两个指针begin和end,其初值分别为待排序区间左端点、右端点,begin从左往右走,寻找比基准值大的元素,end从右往左走,寻找比基准值小的元素。当begin停下时,说明此时的元素大于基准值;当end停下时,表明此时的元素小于基准值。此时交换对应的两个元素,继续上述过程。最后当begin和end相等时,交换其中一个元素和基准值,并返回基准值此时的下标。

总结两点:
① 实现升序时,基准值 key 如果在右边,则左边指针先动(左边找比基准值大的值,右边找比基准值小的值);基准值 key 如果在左边,则右边指针先动(左边找比基准值大的值,右边找比基准值小的值)

② 实现降序时,基准值 key 如果在右边,则左边指针先动(左边找比基准值小的值,右边找比基准值大的值);基准值 key 如果在左边,则右边指针先动(左边找比基准值小的值,右边找比基准值大的值)

以基准值在右边,数组{ 2, 9, 3, 6, 7, 5 }为例,图示如下:
在这里插入图片描述
走到这里时候,基准值 5 将数组分成两个区间,然后再重复以上操作就好,直到整个数组有序。具体代码实现如下:

int part_lrpoint_quick(int* arr,int left,int right)//左右指针法
{
	int key_index = Getmid_index(arr, left, right);
	Swap(&arr[key_index], &arr[right]);
	int key = arr[right];//key如果定在右边,则左边指针先动       
	int right_index = right;
	while (left < right)
	{
		while (left < right&&arr[left] <= key)//left<right这个条件不能丢掉
	   //此步的left < right一定要写,因为如果区间本身已经有序,begin会一直往右走,直到走到end,还会继续往右走
		{
			left++;
		}
		//此时left所指向的元素比基准值大
		while (left < right&&arr[right] >= key)
		{
			right--;
		}
		//此时end所指向的元素比基准值小
		Swap(&arr[left], &arr[right]);
	}
	              
	Swap(&arr[left], &arr[right_index]);
	return right;
}
  • 挖坑法

先将基准值保存起来,此时相当于基准值的位置就空出来了。然后定义两个指针begin和end,其初始值分别为待排序区间的左端点和右端点,begin寻找比基准值大的元素,end寻找比基准值小的元素。当begin找到比基准值大的元素之后,此时将其赋给end所指位置,那么begin所指位置便又空出来了;当end找到比基准值小的元素之后,此时将其赋给begin所指位置,那么此时end所指位置又空出来了。重复上述过程,直到begin==end。此时将基准值赋给begin所指位置并返回下标begin。

以基准值在右边,数组{ 2, 9, 3, 6, 7, 5 }为例,图示如下:
在这里插入图片描述
走到这里时候,基准值 5 将数组分成两个区间,然后再重复以上操作就好,直到整个数组有序。具体代码实现如下:

int part_wakeng_quick(int* arr, int left, int right)//挖坑法
{
	int key = arr[right];
	int right_index = right;
	while (left < right)
	{
		while (left < right&&arr[left] <= key)//左边找大的,放右边的坑
		{
			left++;
		}
         arr[right] = arr[left];//填坑
		while (left < right&&arr[right] >= key)//右边找小的,放在左边的坑
		{
			right--;
		}
		arr[left] = arr[right];//填坑
	}
	arr[left] = key;
	return left;
}
  • 前后指针法

定义两个指针div和cur,初始值均为待排序区间的左端点,其中div所指位置之前表示比基准值小或等的元素,div和cur之间表示比基准值大的元素,cur之后表示待排序部分。在cur遍历整个待排序区域期间,如果cur所指元素小于div所指元素,则交换,此时div++。最后交换div所指元素和基准值,并返回基准值此时的下标。

以基准值在右边,数组{ 2, 9, 3, 6, 7, 5 }为例,图示如下:
在这里插入图片描述
走到这里时候,基准值 5 将数组分成两个区间,然后再重复以上操作就好,直到整个数组有序。具体代码实现如下:

int part_flpoint_quick(int* arr, int left, int right)//前后指针法 former / latter
{
	int cur = left;
	int prev = left - 1;
	int key = arr[right];
	while (cur < right) //cur找比key小的元素再进行交换
	{
		if (arr[cur] < key && ++prev != cur)//prev紧贴着cur
		{
			Swap(&arr[cur], &arr[prev]);
		}
		cur++;
	}
	++prev;//这个条件不能丢
	Swap(&arr[right], &arr[prev]);
	return prev;
}

快速排序在某种程度下还要进行优化才能体现出它的快:
①:三数取中法

思想:因为key的值最好每次都能把数组分成二分的两半,所以key的值最好是区域内比较居中的 值,所以每次把区域内的首元素、尾元素、中间的元素做比较,选出不大不小的那个, 然后把选出来的这个值,交换到数组的尾部,以便调整后它能回到数组中间的位置

代码实现:

int Getmid_index(int* arr, int left, int right)//三数取中法(快速排序的优化)
{
	int mid = left + (right - left) / 2;
	if (arr[left] < arr[right])
	{
		if (arr[right] < arr[mid])
		{
			return right;
		}
		else if (arr[left] < arr[mid])
		{
			return mid;
		}
		else
			return left;
	}
	//arr[left] > arr[right]
	else
	{
		if (arr[left]<arr[mid])
		{
			return left;
		}
		else if (arr[right] < arr[mid])
		{
			return mid;
		}
		return right;
	}
}

②:递归到小的子区间时,可以考虑使用插入排序,一般到元素个数小于 10 个时,考虑使用插入排序算法,代码实现:

void Insert_Sort(int* arr, int n)
{
	int i = 0;
	for (i = 0; i < n - 1; i++)
	{
		if (arr[i + 1] < arr[i])//找到比前一个数相比较小的当前数
		{
			int tmp = arr[i + 1];//先把当前数保存下来
			int j = i;//用J来记录当前数前一个数的下标
			for (j; tmp < arr[j] && j >= 0; j--)
			{
				if (tmp < arr[j])
				{
					arr[j + 1] = arr[j];
				}
			}
			arr[j + 1] = tmp;
		}
	}
}

  • 左右指针法通过优化后的代码实现
int Getmid_index(int* arr, int left, int right)//三数取中法(快速排序的优化)
{
	int mid = left + (right - left) / 2;
	if (arr[left] < arr[right])
	{
		if (arr[right] < arr[mid])
		{
			return right;
		}
		else if (arr[left] < arr[mid])
		{
			return mid;
		}
		else
			return left;
	}
	//arr[left] > arr[right]
	else
	{
		if (arr[left]<arr[mid])
		{
			return left;
		}
		else if (arr[right] < arr[mid])
		{
			return mid;
		}
		return right;
	}
}
int part_lrpoint_quick(int* arr,int left,int right)//左右指针法
{
	int key_index = Getmid_index(arr, left, right);
	Swap(&arr[key_index], &arr[right]);
	int key = arr[right];//key如果定在右边,则左边指针先动       
	int right_index = right;
	while (left < right)
	{
		while (left < right&&arr[left] <= key)//left<right这个条件不能丢掉
	   //此步的left < right一定要写,因为如果区间本身已经有序,begin会一直往右走,直到走到end,还会继续往右走
		{
			left++;
		}
		//此时left所指向的元素比基准值大
		while (left < right&&arr[right] >= key)
		{
			right--;
		}
		//此时end所指向的元素比基准值小
		Swap(&arr[left], &arr[right]);
	}
	              
	Swap(&arr[left], &arr[right_index]);
	return left;
}
void quick_Sort1(int* arr, int left, int right)
{
	if (left >= right)
		return;
	if (right - left + 1 > 10)//快速排序的优化
	{
    	int key_index = part_lrpoint_quick(arr, left, right);//左右指针法
		quick_Sort1(arr, left, key_index - 1);
		quick_Sort1(arr, key_index + 1, right);
	  }
   else 
	{
	   Insert_Sort(arr + left, right - left + 1);
	}
}
void quickSort_test1()
{
	int arr[] = { 4, 3, 2, 8, 0, 9, 7, 8, 5, 4, 8, 9, 5, 6, 4 };
	int n = sizeof(arr) / sizeof(int);
	int left = 0;
	int right = n - 1;
	quick_Sort1(arr, left, right);
	printf("左右指针法—快速排序结果:");
	for (int i = 0; i < n; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}
int main()
{
   quickSort_test1();
   system("pause");
   return 0;
}
  • 挖坑法通过优化后的代码实现
int Getmid_index(int* arr, int left, int right)//三数取中法(快速排序的优化)
{
	int mid = left + (right - left) / 2;
	if (arr[left] < arr[right])
	{
		if (arr[right] < arr[mid])
		{
			return right;
		}
		else if (arr[left] < arr[mid])
		{
			return mid;
		}
		else
			return left;
	}
	//arr[left] > arr[right]
	else
	{
		if (arr[left]<arr[mid])
		{
			return left;
		}
		else if (arr[right] < arr[mid])
		{
			return mid;
		}
		return right;
	}
}
int part_wakeng_quick(int* arr, int left, int right)//挖坑法
{
	int key_index = Getmid_index(arr, left, right);
	Swap(&arr[key_index], &arr[right]);
	int key = arr[right];
	int right_index = right;
	while (left < right)
	{
		while (left < right&&arr[left] <= key)//左边找大的,放右边的坑
		{
			left++;
		}
         arr[right] = arr[left];//填坑
		while (left < right&&arr[right] >= key)//右边找小的,放在左边的坑
		{
			right--;
		}
		arr[left] = arr[right];//填坑
	}
	arr[left] = key;
	return left;
}
void quickSort_test1()
{
	int arr[] = { 4, 3, 2, 8, 0, 9, 7, 8, 5, 4, 8, 9, 5, 6, 4 };
	int n = sizeof(arr) / sizeof(int);
	int left = 0;
	int right = n - 1;
	quick_Sort1(arr, left, right);
	printf("左右指针法—快速排序结果:");
	for (int i = 0; i < n; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}
int main()
{
   quickSort_test1();
   system("pause");
   return 0;
}
  • 前后指针法通过优化后的代码实现
int Getmid_index(int* arr, int left, int right)//三数取中法(快速排序的优化)
{
	int mid = left + (right - left) / 2;
	if (arr[left] < arr[right])
	{
		if (arr[right] < arr[mid])
		{
			return right;
		}
		else if (arr[left] < arr[mid])
		{
			return mid;
		}
		else
			return left;
	}
	//arr[left] > arr[right]
	else
	{
		if (arr[left]<arr[mid])
		{
			return left;
		}
		else if (arr[right] < arr[mid])
		{
			return mid;
		}
		return right;
	}
}
int part_flpoint_quick(int* arr, int left, int right)//前后指针法 former / latter
{
	int key_index = Getmid_index(arr, left, right);
	Swap(&arr[key_index], &arr[right]);
	int cur = left;
	int prev = left - 1;
	int key = arr[right];
	while (cur < right) //cur找比key小的元素再进行交换
	{
		if (arr[cur] < key && ++prev != cur)//prev紧贴着cur
		{
			Swap(&arr[cur], &arr[prev]);
		}
		cur++;
	}
	++prev;//这个条件不能丢
	Swap(&arr[right], &arr[prev]);
	return prev;
}
void quickSort_test1()
{
	int arr[] = { 4, 3, 2, 8, 0, 9, 7, 8, 5, 4, 8, 9, 5, 6, 4 };
	int n = sizeof(arr) / sizeof(int);
	int left = 0;
	int right = n - 1;
	quick_Sort1(arr, left, right);
	printf("挖坑法—快速排序结果:");
	for (int i = 0; i < n; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}
int main()
{
   quickSort_test1();
   system("pause");
   return 0;
}

3. 快速排序的另外一种方法(非递归实现,通过自己实现的栈实现该算法)

  • 利用栈的后进先出特征,代码如下:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include <malloc.h>
#include <stdbool.h>
typedef int STDataType;
typedef struct Stack
{
	STDataType* _array;
	int _top;       // 栈顶
	int _capacity;  // 容量
}Stack;
void StackInit(Stack* ps)
{
	assert(ps);
	ps->_top = 0;
	ps->_capacity = 6;
	ps->_array = (STDataType*)malloc(sizeof(STDataType)*ps->_capacity);
}
void StackDestory(Stack* ps)
{
	assert(ps);
	if (ps->_array == NULL)
	{
		free(ps->_array);
		ps->_array = NULL;
		ps->_capacity = ps->_top = 0;
	}
}

void StackPush(Stack* ps, STDataType x)
{
	assert(ps);
	if (ps->_top == ps->_capacity)
	{
		size_t newcapacity = ps->_capacity == 0 ? 2 : ps->_capacity * 2;
		ps->_array = (STDataType*)realloc(ps->_array, sizeof(STDataType)*newcapacity);
		ps->_capacity = newcapacity;
	}
	ps->_array[ps->_top] = x;
	++ps->_top;
}
void StackPop(Stack* ps)
{
	assert(ps);
	if (ps->_top == 0)
	{
		return;
	}
	--ps->_top;
}
STDataType Stacktop(Stack* ps)
{
	assert(ps);
	if (ps->_top == 0)
	{
		return;
	}
	return ps->_array[ps->_top - 1];//此处的ps->_top 需要减 1
}

bool StackEmpty(Stack* ps)
{
	assert(ps);
	return ps->_top == 0 ? 0 : 1;
}
int StackSize(Stack* ps)
{
	assert(ps);
	return ps->_top;
};

void quick_Sort2(int* arr, int left, int right)
{
	Stack s;
	StackInit(&s);
	StackPush(&s, left);
	StackPush(&s, right);
	while (StackEmpty(&s))
	{
		int end = Stacktop(&s);
		StackPop(&s);
		int begin = Stacktop(&s);
		StackPop(&s);

		int key_index = part_flpoint_quick(arr, begin, end);//左右指针法
		//[begin,key_index - 1] key_index [ key_index + 1,end]
		if (begin < key_index - 1)
		{
			StackPush(&s, begin);
			StackPush(&s, key_index - 1);
		}
		if (key_index + 1 < end)
		{
			StackPush(&s, key_index + 1);
			StackPush(&s, end);
		}
	}
}
int main()
{
	quickSort_test2();
	system("pause");
	return 0;
}

4.快速排序的特性总结

  • 时间复杂度分析
    ①最优情况下时间复杂度:O( nlogn ),具体证明比较复杂就不详细说了,可以查阅其他资料。

    ②最差情况下时间复杂度:O( n^2 ),此时待排序的序列为正序或者逆序,每次划分只得到一个比上次划分少一个记录的子序列,注意另一个为空,如果递归树画出来,它就是一棵斜树,此时需要执行 n-1 次递归调用,且第 i 次划分需要经过 n-1 次关键字的比较才能找到第 i 个记录,也就是枢轴的位置,因此比较次数为,如图:
    在这里插入图片描述

  • 空间复杂度分析
    首先就快速排序使用的空间是O(1)的,也就是个常数级;而真正消耗空间的就是递归调用了,因为每次递归就要保持一些数据;快速排序每次递归都会返回一个中间值的位置,必须使用栈。所以空间复杂度就是栈用的空间。所以快速排序的空间复杂度为:
    ①: 最优的情况下空间复杂度为:O(logn) ;每一次都平分数组的情况。
    ②: 最差的情况下空间复杂度为:O( n ) ;待排序的序列为正序或者逆序的情况。

  • 稳定性分析
    快排是不稳定的,这个不稳定表现在如果相同的比较元素,可能顺序不一样,假设我们有这样一个序列:2 , 2 ,2 ,但是这三个 2 是有区别的,我们标记为2a,2b,2c,快排后的结果不一定就是 2a, 2b, 2c 这样的排列,可能这三个 2 的顺序发生了变化,所以在某些特定场合我们要用结构体来使其稳定。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值