排序-快排

1.定义

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

2.定义理解

由定义可知,快速排序基本思路是先选一个基准值(key),然后以基准值为标准,将待排序序列分为左右两部分,左右两个序列,不要求有序,我们只需满足左边的数(左序列)小于基准值,右边的数(右序列)大于基准值,此时基准值在待排序序列的位置就是排好序的位置,也就是说我们确定了一个元素的位置。然后我们再对左边和右边的数进行同样的操作,左边的数再选一个基准值,然后将大于该基准值的数挪到基准值的右边,小于该基准值的数挪到基准值的左边,此时我们右确定一个基准值的位置,以此类推,直至待排序序列中所有元素位置被却确定。

下面我们把每个基准值都选为这个序列的最左边的值进行说明

当我们选好key了,那我们要用什么方法挪动数据,才能把这个序列分为:小于key的部分,key,大于key的部分。

Hoare给出的思路是,我们创建两个变量,left和right,left这个变量指向待排序数组的最左边的元素,right指向待排序数组最右边的元素

然后右边的right往左遍历,找比key小的元素,找到后停下来,在右边找到比key小的元素之后,左边的left开始往右遍历,left找比key大的元素,找到之后停下来,然后交换left和right在数组中对应的数据,交换之后,right先走,继续找比key小的数据,找到后停下来,然后left找比key大的数据,找到后停下来,然后交换left和right所指向的数据,以此类推,直到right与left相遇,此时说明相遇位置左边都是小于key的,相遇位置右边都是大于key的,然后我们将相遇位置的与keyi对应的key交换,然后一次快排就完成了,此时待排序列被分为了:小于key的部分,key,大于key的部分。

此时一趟快排结束,

结束后,我们对keyi左边的序列按照快排的思路,进行同样的操作,右边的序列也进行同样的操作,直到序列中所有元素位置确定下来。

逻辑缕清后我们来写代码。

3.快排-递归代码

由上面的逻辑我们可以用类似二叉树的前序遍历来实现快排

1.Hoare

#include <stdio.h>
void Swap(int* a, int* b)
{
	int t = *a;
	*a = *b;
	*b = t;
}
int GetMidNumi(int arr[], int left, int right)
{
	int midi = (left + right) / 2;
	if (arr[left] > arr[midi])
	{
		if (arr[midi] > arr[right])
		{
			return midi;
		}
		else
		{
			if (arr[right] > arr[left])
			{
				return left;
			}
			else
			{
				return right;
			}
		}
	}
	else //arr[left] < arr[midi]
	{
		if (arr[left] > arr[right])
		{
			return left;
		}
		else
		{
			if (arr[right] > arr[midi])
			{
				return midi;
			}
			else
			{
				return right;
			}
		}
	}
}
int PartSort(int arr[], int left, int right)
{
	//三数取中找key
	int midi = GetMidNumi(arr, left, right);
	Swap(&arr[left], &arr[midi]);
	int keyi = left;
	//挪数据
	while (right > left)
	{
		while (right > left && arr[right] >= arr[keyi])
		{
			right--;
		}
		while (left < right && arr[left] <= arr[keyi])
		{
			left++;
		}
		Swap(&arr[left], &arr[right]);
	}
	Swap(&arr[left], &arr[keyi]);
	keyi = left;
	return keyi;
}
void QuickSort(int arr[], int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int keyi = PartSort(arr, left, right);
	QuickSort(arr, left, keyi - 1);
	QuickSort(arr, keyi + 1, right);
}
int main()
{
	int arr[] = { 3, 1, 2, 5, 4, 6, 9, 7, 10, 8 };
	int n = sizeof(arr) / sizeof(arr[0]);
	for (int i = 0; i < n; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
	QuickSort(arr, 0, n-1);
	for (int i = 0; i < n; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
	return 0;
}

2.前后指针

#include <stdio.h>
void Swap(int* a, int* b)
{
	int t = *a;
	*a = *b;
	*b = t;
}
int GetMidNumi(int arr[], int left, int right)
{
	int midi = (left + right) / 2;
	if (arr[left] > arr[midi])
	{
		if (arr[midi] > arr[right])
		{
			return midi;
		}
		else
		{
			if (arr[right] > arr[left])
			{
				return left;
			}
			else
			{
				return right;
			}
		}
	}
	else //arr[left] < arr[midi]
	{
		if (arr[left] > arr[right])
		{
			return left;
		}
		else
		{
			if (arr[right] > arr[midi])
			{
				return midi;
			}
			else
			{
				return right;
			}
		}
	}
}
int PartSort(int arr[], int left, int right)
{
	//三数取中找key
	int midi = GetMidNumi(arr, left, right);
	Swap(&arr[left], &arr[midi]);
	int keyi = left;
	int prev = left;
	int cur = left + 1;
	while (cur <= right)
	{
		if (arr[cur] < arr[keyi])
		{
			prev++;
			Swap(&arr[prev], &arr[cur]);
			cur++;
		}
		else
		{
			cur++;
		}
	}
	Swap(&arr[keyi], &arr[prev]);
	keyi = prev;
	return keyi;
}
void QuickSort(int arr[], int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int keyi = PartSort(arr, left, right);
	QuickSort(arr, left, keyi - 1);
	QuickSort(arr, keyi + 1, right);
}
int main()
{
	int arr[] = { 3, 1, 2, 5, 4, 6, 9, 7, 10, 8};
	int n = sizeof(arr) / sizeof(arr[0]);
	for (int i = 0; i < n; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
	QuickSort(arr, 0, n-1);
	for (int i = 0; i < n; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
	return 0;
}

4.快排代码讲解

1.三数取中

我们发现上面的代码中,出现三数取中

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

三数取中是我们为了选择合适的key而想出的一种方法。

假设有个待排序序列为:“9,10,2,7,6,3,4,5,1,8”,如果我们直接将最左边的‘9’作为key进行快速排序

我们看这张图,如果我们选’9‘为key我们发现’10‘和’8‘交换,但是我们思考一下,如果我们想把这个序列排为有序,那么’8‘在这个序列属于靠后位置,但是现在’8‘被交换到了第二个位置,所以’10‘和’8‘的交换并不完美,并且后面的操作还会把’8‘挪回去,这样的挪数据会消耗不必要的时间和空间。

那么如果我们利用三数取中,即’9‘,’6‘,’10‘,我们选取’6‘作为key,并把’6‘与’9‘交换,让’6‘放在最左边作为key

我们可以看出,当’6‘作为key时,在进行一次快速排序后,每对元素交换都比较合理,他们交换后的位置都距离有序时的位置更近了,所以三数取中可以使我们快排的效率提升。

2.为什么right先走

我们发现我们每次都把序列的最左边元素作为key,然后left指向序列的最左边,right指向元素的最右边,我们每次都是让right先去找小,然后left再去找大,其实right先走是为了保证最后和left相遇时,相遇的数比key小,然后我们把相遇的元素与key交换。

这里面一共有三种情况:

  1. right找小,但是一直没有找到,直接与left相遇,也就是说left没有动,left,right,key都在同一位置,这时我们把相遇位置和key交换,虽然是自己自己交换,但是它还是保证了key的左边比key小,key的右边比key大。
  2. 当right找小找到了,然后我们的left找大,也找到了,这是我们交换,left和right所指向的元素,交换后right指向的元素大于key,left指向的元素小于key,然后right继续找小,如果找到了继续交换,同样交换后right指向的元素大于key,left指向的元素小于key,然后right继续找小,当right找不到比key小的元素时,right和left相遇,并且相遇位置的元素小于key,然后我们就直接交换相遇位置和key,这样也也是满足快排的要求的。
  3. 当right找小找到了,但是left找大没有找到,然后left和right相遇,此时我们也保证了相遇位置的元素小于key,然后我们直接将相遇位置的元素与key交换。

以上三种情况,我们都保证了当我们以序列最左边元素为key,right先走,left后走,left和right相遇位置的元素都是小于key的,然后我们可以直接将相遇位置与key交换。

同理,如果我们每次都以序列的最右端的元素为key的话,那我们要让left找大,先走,right找小,后走。

5.快排非递归理解

当我们对序列进行一次快排后我们分割出了:小于key的区间,key,大于key的区间,然后我们还要对小于key的区间,和大于key的区间继续分割,我们每分割一次都能将一个元素的最终位置确定。

如果我们想用非递归实现快排,我们可以先对待排序序列,进行一次快排,排好后待排序序列被分割为小于key的区间,key,大于key的区间,然后我们在对小于key的区间进行快排,同样分割出小于key的区间,key,大于key的区间,然后我们继续对小于key的区间快排,以此类推,直到小于key的区间只有一个元素,但是我们发现,我们一直都在对每次分割后小于key的区间进行排序,大于key的区间没有被排序,当我们把小于key的区间都排好时,我们就找不到每次分割对应的大于key的区间,那我们就需要想办法将大于key的区间存起来。

快排的非递归和递归的基本逻辑是相同的,但是我们用递归的方法时,虽然我们也在不停的对小于key的区间进行快排,但是当小于key的区间排好后,递推结束,回归时,系统帮我们把大于key的区间记录了下来,所以非递归我们要手动把每次分割的大于key的区间记录下来,并且我们要用栈,利用它的后入先出性质。

当快排后分割的区间只有一个元素时就不用入栈了,直接出栈顶原先入的区间,然后继续循环上面操作直到栈空了。

6.快排-非递归代码

#include "stack.h"
void Swap(int* a, int* b)
{
	int t = *a;
	*a = *b;
	*b = t;
}
int GetMidNumi(int arr[], int left, int right)
{
	int midi = (left + right) / 2;
	if (arr[left] > arr[midi])
	{
		if (arr[midi] > arr[right])
		{
			return midi;
		}
		else
		{
			if (arr[right] > arr[left])
			{
				return left;
			}
			else
			{
				return right;
			}
		}
	}
	else //arr[left] < arr[midi]
	{
		if (arr[left] > arr[right])
		{
			return left;
		}
		else
		{
			if (arr[right] > arr[midi])
			{
				return midi;
			}
			else
			{
				return right;
			}
		}
	}
}
void QuickSortNonR(int arr[], int left, int right)
{
	ST st;
	STInit(&st);
	STPush(&st, right);
	STPush(&st, left);
	while (!STEmpty(&st))
	{
		left = STTop(&st);
		STPop(&st);
		right = STTop(&st);
		STPop(&st);
		int begin = left, end = right;
		//三数取中找key
		int midi = GetMidNumi(arr, left, right);
		Swap(&arr[left], &arr[midi]);
		int keyi = left;
		//挪数据
		while (right > left)
		{
			while (right > left && arr[right] >= arr[keyi])
			{
				right--;
			}
			while (left < right && arr[left] <= arr[keyi])
			{
				left++;
			}
			Swap(&arr[left], &arr[right]);
		}
		Swap(&arr[left], &arr[keyi]);
		keyi = left;
		//区间入栈
		if (keyi + 1 < end)
		{
			STPush(&st, end);
			STPush(&st, keyi + 1);
		}
		if (begin < keyi - 1)
		{
			STPush(&st, keyi - 1);
			STPush(&st, begin);
		}
	}
	
}
int main()
{
	int arr[] = { 3, 1, 2, 5, 4, 6, 9, 7, 10, 8 };
	int n = sizeof(arr) / sizeof(arr[0]);
	for (int i = 0; i < n; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
	QuickSortNonR(arr, 0, n - 1);
	for (int i = 0; i < n; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
	return 0;
}

7.小区间优化

#include <stdio.h>
void Swap(int* a, int* b)
{
	int t = *a;
	*a = *b;
	*b = t;
}
int GetMidNumi(int arr[], int left, int right)
{
	int midi = (left + right) / 2;
	if (arr[left] > arr[midi])
	{
		if (arr[midi] > arr[right])
		{
			return midi;
		}
		else
		{
			if (arr[right] > arr[left])
			{
				return left;
			}
			else
			{
				return right;
			}
		}
	}
	else //arr[left] < arr[midi]
	{
		if (arr[left] > arr[right])
		{
			return left;
		}
		else
		{
			if (arr[right] > arr[midi])
			{
				return midi;
			}
			else
			{
				return right;
			}
		}
	}
}
void InsertSort(int arr[], int n)
{
	for (int i = 0; i < n - 1; i++)
	{
		int end = i;
		int tmp = arr[i+1];
		while (end >= 0)
		{
			if (arr[end] > tmp)
			{
				arr[end + 1] = arr[end];
				end--;
			}
			else
			{
				break;
			}
		}
		arr[end + 1] = tmp;
	}
	
}
int QuickSort(int arr[], int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	int left = begin, right = end;
	if ((end - begin + 1) <= 10)
	{
		InsertSort(arr+begin, end - begin + 1);
	}
	else
	{
		//三数取中找key
		int midi = GetMidNumi(arr, left, right);
		Swap(&arr[left], &arr[midi]);
		int keyi = left;
		//挪数据
		while (right > left)
		{
			while (right > left && arr[right] >= arr[keyi])
			{
				right--;
			}
			while (left < right && arr[left] <= arr[keyi])
			{
				left++;
			}
			Swap(&arr[left], &arr[right]);
		}
		Swap(&arr[left], &arr[keyi]);
		keyi = left;
		QuickSort(arr, begin, keyi - 1);
		QuickSort(arr, keyi + 1, end);
	}
}
int main()
{
	int arr[] = { 3, 1, 2, 5, 4, 6, 9, 7, 10, 8, 20, 0};
	int n = sizeof(arr) / sizeof(arr[0]);
	for (int i = 0; i < n; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
	QuickSort(arr, 0, n - 1);
	for (int i = 0; i < n; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
	return 0;
}

在这个代码我们做了小区间优化

小区间优化的原因:

  1. 在对序列不断进行快排后,此时这个序列就趋近有序,但是快排对接近有序的序列排序效率很低,最差是O(n^2),此时我们可以用插入排序,插入排序擅长趋于有序的序列排序。
  2. 无论是递归的快速排序还是非递归的快速排序,他们的思路都类似二叉树的前序遍历,如果每次key选的合适,那么快速排序的分割图就像满二叉树,如果我们对区间小于10的序列进行插入排序,那么我们就不用递归了,我们几乎不用对二叉树的最后2~3层递归,这将近减少了70%的递归量。

8.时间空间复杂度和稳定性

1.时间复杂度:O(nlogn)

我们以每一层为视角来看,每一层快排都相当于把初始的待排序序列遍历了一遍,遍历一遍时间复杂度为O(n),然后一共有logn层,所以整个的时间复杂度为O(nlogn)

2.空间复杂度:O(n)

因为整个过程中,我们都在初始序列上操作,而初始序列的大小就是空间复杂度的大小,为n

3.稳定性:不稳定

当一个序列有多个相同值时,它们的相对位置会被快排打乱。

9.三路划分

1.思路

三路划分是为了解决快排中存在大量重复数据,三路划分我们需要定义key,left,cur和right四个变量,key表示三数取中后,初始序列的首元素,left指向初始序列的第一个元素,cur指向初始序列的第二个元素, right指向初始序列的最后的元素。

left,cur,right的变化情况如下:

  1. 当arr[cur] < key时,交换arr[left]和arr[cur],left++, cur++
  2. 当arr[cur] > key时,交换arr[cur]和arr[right],right--。注意此时cur位置不变,我们要继续判断cur指向元素和arr[keyi]的大小情况
  3. 当arr[cur] = key时,cur++。

直到cur>right为止

2.代码

#include <stdio.h>
void Swap(int* a, int* b)
{
	int t = *a;
	*a = *b;
	*b = t;
}
int GetMidNumi(int arr[], int left, int right)
{
	int midi = (left + right) / 2;
	if (arr[left] > arr[midi])
	{
		if (arr[midi] > arr[right])
		{
			return midi;
		}
		else
		{
			if (arr[right] > arr[left])
			{
				return left;
			}
			else
			{
				return right;
			}
		}
	}
	else //arr[left] < arr[midi]
	{
		if (arr[left] > arr[right])
		{
			return left;
		}
		else
		{
			if (arr[right] > arr[midi])
			{
				return midi;
			}
			else
			{
				return right;
			}
		}
	}
}
void QuickSort(int arr[], int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int midi = GetMidNumi(arr, left, right);
	Swap(&arr[left], &arr[midi]);
	int begin = left;
	int end = right;
	int keyi = left, key = arr[left];
	int cur = left + 1;
	while (cur <= right)
	{
		if (arr[cur] < key)
		{
			Swap(&arr[cur], &arr[left]);
			cur++;
			left++;
		}
		else if (arr[cur] > key)
		{
			Swap(&arr[cur], &arr[right]);
			right--;
		}
		else
		{
			cur++;
		}
	}
	//[begin,left-1][left,right][cur,end]
	QuickSort(arr, begin, left - 1);
	QuickSort(arr, cur, end);
}
int main()
{
	int arr[] = { 3, 1, 2, 5, 4, 6, 9, 7, 10, 8, 11, 1, 1, 1, 1, 2, 2};
	int n = sizeof(arr) / sizeof(arr[0]);
	for (int i = 0; i < n; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
	QuickSort(arr, 0, n - 1);
	for (int i = 0; i < n; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值