【数据结构】排序(冒泡排序,快速排序)

一、冒泡排序

基本思想:

        冒泡排序属于交换排序,所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。

 冒泡排序实现:

排 n 个数,就需要循环n-1次,每次比较相邻两个元素如果满足条件,则交换。

这里的代码已经进行了优化,如果一次循环中没有发生交换,那么说明他们就是有序地,则可以直接跳出循环

void BubbleSort(int* a, int n)
{
	for (int j = 0; j < n; j++)
	{
		bool flag = true;
		for (int i = 1; i < n-j; i++)
		{
			if (a[i - 1] > a[i])
			{
				int tmp = a[i - 1];
				a[i - 1] = a[i];
				a[i] = tmp;

				flag = false;
			}
		}

		if (flag)
		{
			break;
		}
	}
}

冒泡排序特性及复杂度:

因为冒泡排序每次的比较次数,是随着趟数而减少的,找一下规律,其实可以发现,它的总执行次数是一个公差为 −1 的等差数列:(n - 1) + (n - 2) + … + 1 ,根据等差数列求和公式得:n^{2}/2 - n/2

虽然我们已经进行了优化,但其实效果也并不是很理想。

冒泡排序的特性总结:

1. 冒泡排序是一种非常容易理解的排序

2. 时间复杂度:O(N^2)

3. 空间复杂度:O(1)

4. 稳定性:稳定

二、快速排序 

基本思想:

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

快速排序递归实现:

快排在开始时,会选择一个 key 做基准值,然后进行单趟排序,单趟排序后,当排序停止,会把 key 的索引或 key 值本身与边界某一值交换,形成区间划分。

这个区间划分通常为 key 左边的值都小于 key ,key 右边的值都大于 key ,这样就使得区间被划分了。中间的 key 值不用管,当前 key 值已经到了正确的位置。那么现在排序就变为:对左区间和右区间的排序。

在实现快速排序时,我们需要运用到二叉树前序遍历的思想

快排在划分区间时,有三种方案:hoare 、挖坑法、双指针

void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}

	int keyi = PartSort3(a, begin, end);
	//[begin,key-1] key [key+1,end]

	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);

}
1.hoare 版本

快排单趟排序结束后要保证key元素之前的元素都比key元素小,单趟排序结束后要保证key元素之后的元素都比key元素大,然后返回key的下标。

1.先取数组首元素为key,指针left和right指向首元素和尾元素 。

2.right 先走,如果right 指向的元素大于 key 指向的元素,则right−− ,如果指向元素小于 key 指向的元素,则停止

3.停止之后,left 开始走,如果 left 指向的元素小于key 指向的元素,则left++ ,如果指向元素大于key 指向的元素,则停止

4.当 left 和right 都停下后,交换left 和 right 位置的值。

left≥right时,循环停止。

 结束后就能保证相遇值小于key,此时交换首元素与相遇值,key就是分割点,返回key。

这里有一些问题:

左右两边都有和 key 相等的值,导致左右两边卡死,改成大于等于,小于等于即可。

这里就会引出这个问题,如果key值都大于右边,数组会直接越界,这时我们就要加上另一个限制

int PartSort1(int* a, int left, int right)
{
	int mid = GetMidIndex(a, left, right);
	Swap(&a[mid], &a[left]);
	int keyi = left;
	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;
}
2.挖坑法 

挖坑法是对hoare版本的优化

单趟排序:
1.选择arr[left]作为key(key变量存储key元素的值)
2.以初始left指向的位置作为初始坑位(坑位下标用hole变量来记录)
3.right指针向前遍历寻找比key小的数,找到后将right指向的元素赋值到坑位上,将right下标值赋给hole(更新坑位)
4.left指针向后遍历寻找比key大的数,找到后将left指向的元素赋值到坑位上,将left下标值赋给hole(更新坑位)
5.重复上述迭代过程直到left指针和right指针相遇

1. 挖坑法需要我们创建一个hole,让key = a[left] 再把key放到一个临时变量里,这时我们假设这个位置被挖空

2.右边先走,循环条件为left<right 并且right 指向的元素大于等于key ,一旦 right 停下来,那么就把 a[hole]=a[right] ,把right 指向的值填到坑中,此刻right 作为新的坑。

3..左边则是相同的思路,同理左边停下后,让 a[hole]=a[left] ,让 left 作为新的坑。

4.最后把key填到hole中,此时hole就是分割点

int PartSort2(int* a, int left, int right)
{
	int mid = GetMidIndex(a, left, right);
	Swap(&a[mid], &a[left]);
	int key = a[left];
	int hole = left;	

	while (left < right)
	{
		while (left < right && a[right] >= key)
		{
			right--;
		}
		a[hole] = a[right];
		hole = right;

		while (left < right && a[left] <= key)
		{
			left++;
		}
		a[hole] = a[left];
		hole = left;
	}
	a[hole] = key;

	return hole;
}
 3.前后指针 版本

单趟排序:
1.选取arr[left]作为key(key变量作为下标指向key元素)
2.slow初值为left,fast指针从left+1位置开始遍历数组
3.若fast指针找到了比key小的元素,则令slow指针向后走一步,并交换slow和fast指针指向的元素
4.若fast指针找到了比key大的元素,slow指针不动,fast指针继续向后遍历数组
5.重复上述过程直到fast指针完成数组的遍历,最后再将key元素交换到slow最终指向的位置
6.最终从left位置到slow位置的所有元素就是整个数组中比key小的所有元素。

 

 定义三个变量:prev(记录小于key的下标),cur(遍历数组),key.

如果cur的值比key小,++prev,交换cur与key的值,cur++

如果cur 指向的元素大于等于key ,cur++ 

当cur遍历完数组后,交换prev和key的值,此时prev就是分割点

最后我们可以做一点小小的优化,我们可以发现当prev紧跟cur时,是不需要交换的,所以可以加一个判断,++prev!=cur

int PartSort3(int* a, int left, int right)
{
	int mid = GetMidIndex(a, left, right);
	//Swap(&a[mid], &a[left]);
	int key = left;
	int cur = left + 1;
	int prev = left;

	while (cur <= right)
	{
		if (a[cur] < a[key] && ++prev != cur)
		{
			//++prev;
			Swap(&a[prev], &a[cur]);
		}
		cur++;
	}
	Swap(&a[prev], &a[key]);

	return prev;
}
4.快速排序的缺点及改进方法

912. 排序数组

这里拿出一道题,我们去拿快排去做这道题时会非常难受,这道题的测试用例很针对快排。

1.有序或接近有序序列时间复杂度过高

当对于有序序列(排正序时遇到逆序),那么key每次只会在最边上,这样下来的时间复杂度就会变成n^2

这里我们引申出一个优化,三数取中,在begin , end,(begin+end)/2 中选取一个中间值,尽量让key 可以命中每段区间中点。

int GetMidIndex(int* a, int left, int right)
{
	int mid = (left + right) / 2;

	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
		{
			return mid;
		}
		else if (a[left] < a[right])
		{
			return right;
		}
		else
		{
			return left;
		}
	}
	else //a[left] > a[mid]
	{
		if (a[mid] > a[right])
		{
			return mid;
		}
		else if (a[left] > a[right])
		{
			return right;
		}
		else
		{
			return left;
		}
	}
}

2.递归层数过多造成空间浪费

我们把快排看做一棵满二叉树,满二叉树的最后一层的节点是总结点数的1/2,倒数第二层是1/4,倒数第三层是1/8……

那么省下三层就节省了87.5%的节点数,所以这里又引申出一个优化:小区间优化

	if (end - begin + 1 < 10)
	{
		InserSort(a + begin, end - begin + 1);
		return;
	}

3.对于过多的相同数据,三数取中就会失去效果

这里我们引申出了第三个优化:三路划分

思路:
1.设定一个 cur=begin+1,left = begin, right = end, key = a[left],将区间分割为左区间小于key ,中间区间等于 key ,右区间大于key 。
2.给定一个循环,循环中如果a[cur]<key ,此刻交换 cur 和left 指向的元素,使 left++ ,cur++ (如果一开始这个条件就满足,则会把 key 逐渐往中间推)
3.如果a[cur]>key ,此刻right 这个位置的值比 key 大,也有可能比 key 小。交换cur 和right 指向元素后,若cur++可能该位置元素就不满足最终区间划分条件,所以这里只能right−−.
4.如果a[cur]==key ,那么只需要 cur++。
5.当cur>right 时,right 后的元素都是大于key 的,区间也都调整好了,这时候循环也就停止了

最后比key大的值在右面,比key小的在左面,等于key的在中间,这时只需要排序左右区间即可 

void QuickSortUltimate(int* a, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}

	int left = begin;
	int right = end;
	int cur = left + 1;

	int mid = GetMidIndex(a, left, right);
	Swap(&a[mid], &a[left]);
	int key = a[left];

	//三路划分
	while (cur <= right)
	{
		if (a[cur] < key)
		{
			Swap(&a[cur], &a[left]);
			left++;
			cur++;
		}
		else if (a[cur] == key)
		{
			cur++;
		}
		else
		{
			Swap(a[cur], a[right]);
			right--;
		}
	}
	//[begin, left - 1] [left, right][right + 1, end]
	QuickSort(a, begin, left - 1);
	QuickSort(a, right + 1, end);
}

 但是,就算我们已经进行了多次优化,在力扣中却还是超时,所以我们再添加一个优化,随机选K

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

int GetMidIndex(int* a, int left, int right)
{
	//int mid = (left + right) / 2;
    int mid = left + (rand() % (left - right));
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
		{
			return mid;
		}
		else if (a[left] < a[right])
		{
			return right;
		}
		else
		{
			return left;
		}
	}
	else //a[left] > a[mid]
	{
		if (a[mid] > a[right])
		{
			return mid;
		}
		else if (a[left] > a[right])
		{
			return right;
		}
		else
		{
			return left;
		}
	}
}


void QuickSortUltimate(int* a, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}

	int left = begin;
	int right = end;
	int cur = left + 1;

	int mid = GetMidIndex(a, left, right);
	Swap(&a[mid], &a[left]);
	int key = a[left];

	while (cur <= right)
	{
		if (a[cur] < key)
		{
			Swap(&a[cur], &a[left]);
			left++;
			cur++;
		}
		else if (a[cur] == key)
		{
			cur++;
		}
		else
		{
			Swap(&a[cur], &a[right]);
			right--;
		}
	}
	//[begin, left - 1] [left, right][right + 1, end]
	QuickSortUltimate(a, begin, left - 1);
	QuickSortUltimate(a, right + 1, end);
}

int* sortArray(int* nums, int numsSize, int* returnSize) 
{
    srand(time(0));
    QuickSortUltimate(nums, 0, numsSize - 1);
    *returnSize = numsSize;
    return nums;
}

 快速排序的非递归实现

非递归的优点就是不需要多次递归开辟多层函数栈帧,在空间消耗上略有优势。实现非递归无需要依靠顺序栈来完成。

思路:
1.开始,我们将 begin 和 end 分别入栈。给定一个循环,如果栈不为空就继续循环。
2.由于栈是后进先出,所以先用right 接收end 右区间,再用 left 接收左区间,在接收完之后,将这两个元素分别出栈。
3.得到了区间后,对区间进行单趟排序(可以调用上面的 hoare 等),用 key 接收分隔点。
4先处理左区间[left,key−1] ,再处理 [key+1,right] 。由于栈先进后出,所以要先入右区间,在入左区间。
5.每次循环只会取出两个值,那么就是一小段区间,在取出左区间后,会先处理左区间,然后不断分割小区间,每次取出两个值一直对栈顶上的两个元素的区间进行处理,这样就模拟了快排的过程。

void QuickSortNonR1(int* a, int begin, int end)
{
	ST st;
	STInit(&st);
	STPush(&st, end);
	STPush(&st, begin);

	while (!STEmpty(&st))
	{
		int left = STTop(&st);
		STPop(&st);

		int right = STTop(&st);
		STPop(&st);

		int keyi = PartSort3(a, left, right);
		//[begin,key-1] key [key+1,end]

		if (keyi + 1 < right)
		{
			STPush(&st, right);
			STPush(&st, keyi + 1);
		}

		if (keyi - 1 > left)
		{
			STPush(&st, keyi - 1);
			STPush(&st, left);
		}
	}

	STDestroy(&st);
}

快速排序特性及复杂度

1. 快速排序整体的综合性能和使用场

景都是比较好的,所以才敢叫快速排序

2. 时间复杂度:O(N*logN)

3. 空间复杂度:O(logN)

4. 稳定性:不稳定

  • 25
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值