排序----快速排序(快排)(递归版)

首先讲一下单趟的思路:

        在这一块数据中,记录第一个元素为key,然后设置L和R两个指针,L找比key处的元素大的,R找比key处元素小的,找到了就交换这两个位置的元素。当两个指针相遇时,若相遇点的元素比key处的值小,就把相遇点的元素与key处的元素进行交换;若相遇点的数据比key处的数据大,就把相遇点之前的元素与key处的元素相比较。

        那么这样走完一趟的过程中,key处的元素就位于正确的位置,同时,key左侧的元素都比他小,右侧的元素的都比他大。这一趟也实现了自然分块,那么再递归进行每一块的排序,同上一段所述。直到最后每一块只有一个元素,就代表排好了。

注意:

1.keyi记录的需要是下标,然后通过下标进行最后一次交换。万万不可把下标对应的值赋值给keyi,这样的话就不是交换了。

2.注意指针前进/后退的循环的进行条件,一定要保证left<right,这样的话进入循环++就会left=right,否则left和right可能正好错过了,变成了left>right。

3.注意递归的终止条件。

        举例:当left=0、right=1、keyi=1时:

4.为什么最后与keyi交换的位置的元素一定比keyi处的元素小?

//以下为简单版快排(即还存在着一些坑)

        时间复杂度:大约是O(N*logN)。递归的过程可以看成是满二叉树,递归的次数就是树的高度就是logN,然后每一层begin和end分别++和--,就可以看成是N次,即可得到这个时间复杂度。

***继续找坑:

        当我们要排序的序列是一个有序序列时,我们选择第一个元素为keyi,但是end找不到比keyi的元素更小的元素,那就一直往前走,走到头和自己交换一下;begin同理。然后不断建立栈帧,每次建立都是第一个元素为keyi,然后begin和end找不到符合条件的元素,这样的话会导致递归层次太深(需要进行N次,时间复杂度为O(N^2)) ,就会造成栈溢出。

        因此,不能固定选择第一个元素为keyi。

        我们采用三数取中的方法:

        即left、midi、right中选择对应元素不是最大也不是最小的位置来作为keyi。

//三数取中函数

//保证取得keyi处的值不是数组的最小值

//排序主函数

        下一个效率问题来了,当区间内只有五个数据时,加入我们还继续选择快排来不断递归,那么我们需要进行好多次递归和判断。(快排相当于满二叉树,满二叉树最后一层占整棵树的50%,倒数第二层占25%,倒数第三层占12.5%...,假如我们把这几层去掉,那么时间效率大大提高)

        那我们这个小区间采用什么排序方法呢?希尔排序和堆排序有点大材小用,就是说一共最多十个数据,没有必要分组或者建堆;冒泡排序和选择排序太慢。所以我们选择插入排序。

        注意插入排序两个参数的写法。

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

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

	// 小区间优化,不再递归分割排序,减少递归的次数
	if ((right - left + 1) < 10)
	{
		InsertSort(a+left, right - left + 1);
	}
	else
	{
		// 三数取中
		int midi = GetMidi(a, left, right);
		Swap(&a[left], &a[midi]);

		int keyi = left;
		int begin = left, end = right;
		while (begin < end)
		{
			// 右边找小
			while (begin < end && a[end] >= a[keyi])
			{
				--end;
			}

			// 左边找大
			while (begin < end && a[begin] <= a[keyi])
			{
				++begin;
			}

			Swap(&a[begin], &a[end]);
		}

		Swap(&a[keyi], &a[begin]);
		keyi = begin;
		// [left, keyi-1] keyi [keyi+1, right]
		QuickSort(a, left, keyi - 1);
		QuickSort(a, keyi + 1, right);
	}
}

//在进行数据测试的时候发现,当重复数据过多的时候,快排比希尔排序和堆排序性能高很多。

ps:面试时不要写三数取中和小区间优化,这一块写起来很浪费时间,可以稍微和面试官说一下。

        更好理解的但是没有任何效率提升的版本:挖坑法。

(此处仅说明思路)

        我们选择第一个位置为key,把第一个位置的值给key,然后begin++和end--(此处若选择左边为key,那就end先走;若选择右边为key,那就begin先走),end遇到比key处的元素更小的元素,就把这个元素给begin指针指向的位置(该指针指向的位置为选择的key,形成坑位),同时,这个end的位置形成一个坑位。然后begin++,begin遇到比key大的元素就把这个元素给end形成的坑位,同时,begin由于把元素给出去了,就形成了一个坑位......最后begin和end相遇,此时相遇的位置一定是一个坑位,就把最开始取出来的key值放进去。就完成了第一轮排序。

另一种思路(霍尔排序):

整体思想:把比选定的key的值大的元素不断后移。

        我们首先选定第一个元素为key,第一个元素处为前指针,第二个元素处为后指针。后指针处的元素小于选定的key值就进行前后指针交换,然后后指针往后走;若后指针指向的元素大于选定的key的值,那就让后指针继续往后走。最后后指针走到头,前指针指向最后一个小于key的元素,将这个元素与key交换,那么key左边全是小于(等于)他的,右边全是大于他的,同时返回这个位置,也就把数组进行自然分组了。然后递归,进行同样的做法。

int GetMidi(int* a, int left, int right)
{
	int midi = (left + right) / 2;
	// left midi right
	if (a[left] < a[midi])
	{
		if (a[midi] < a[right])
		{
			return midi;
		}
		else if (a[left] < a[right])
		{
			return right;
		}
		else
		{
			return left;
		}
	}
	else // a[left] > a[midi]
	{
		if (a[midi] > a[right])
		{
			return midi;
		}
		else if (a[left] < a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
}
int PartSort2(int* a, int left, int right)
{
	// 三数取中
	int midi = GetMidi(a, left, right);
	Swap(&a[left], &a[midi]);
	int keyi = left;

	int prev = left;
	int cur = prev+1;
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++prev != cur)
			Swap(&a[prev], &a[cur]);
		
		cur++;
	}

	Swap(&a[prev], &a[keyi]);
	return prev;
}
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值