排序2-冒泡排序与快速排序(递归加非递归讲解)

Posted 无聊的马岭头

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了排序2-冒泡排序与快速排序(递归加非递归讲解)相关的知识,希望对你有一定的参考价值。

前言

在这里插入图片描述

一、冒泡排序

冒泡排序:是一种交换排序,其基本思想是,两两比较相邻记录的关键字,如果反序则交换,直到没有反序的记录就为止。

当然,我们今天的主角是后面要介绍的快排,我相信朋友们对冒泡已经很熟悉了,我就简单带过一下。

在这里插入图片描述

代码:

//冒泡排序:两个两个比较
void BubblSort(int *arr,int n)
{
	for (int end = n; end > 0; end--)
	{
		int flag=0;//优化,防止已经在有序的情况下,还来做无意义的比较
		for (int i = 1; i < end; i++)
		{
			if (arr[i]>arr[i - 1])
			{
				Swap(&arr[i], &arr[i - 1]);
				flag=1;
			}
		}
		if(flag==0)
			break;
	}
}

总结:

  1. 时间复杂度:O(N2)
  2. 空间复杂度:O(1)
  3. 稳定性:稳定

二、快速排序

快排的基本思想:通过一趟排序将待排记录分割成独立的两个部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序的目的。

怎样来分割成这两个部分呢?

  • 在待排序中找一个关键字(key)来分割,我们一般选最左边或者最右边.

在这里插入图片描述

1.左右指针法

  1. 定义两个变量来记录下标,分别为left=0、right=n
  2. 右边的找比key小的值,左边的找比key大的值
  3. 其细节是,先移动right来找小,找到后,再来移动left来找大,然后交换,当left>=right的时候,停止,再交换left和key上的值。(第一趟分割完成)
  4. 然后递归

我们在写代码的时候,应该先分析成两个部分
1.先写第一趟分割,
2.然后写多趟。

代码:

void Swap(int *p, int *q)
{
	int temp = *p;
	*p = *q;
	*q = temp;
}
void PartSort1(int* arr, int begin, int end)
{	
	if (begin >= end)
		return;
	int left = begin,right=end;
	int keyi = left;//上面介绍中key的下标
	while (left < right)
	{
		//找小
		while (left<right&&arr[right]>=arr[keyi])
		{
			right--;
		}
		//找大
		while (left<right&&arr[left]<=arr[keyi])
		{
			left++;
		}
		//交换
		Swap(&arr[left], &arr[right]);
	}	
	Swap(&arr[keyi], &arr[left]);
	int meeti = left;//相遇点
//分割后两个部分的区间[begin,keyi-1][keyi+1,end]
//递归
	PartSort1(arr, begin, meeti - 1);
	PartSort1(arr, meeti+1,end);

}

2.挖坑法

挖坑法和上面的左右指针法有点类似。

  1. 找最左边关键字key并记录,arr[0]为一个坑,定义两个变量记录下标,left=0、right=n
  2. 右边先找小,找到后,把小的值填到arr[0]中,而找到的那个小的值其所在的位置就为一个新坑
  3. 然后左边再找大,找到后,把大的值放到新坑中,自己的位置就变成了一个新坑,依此往复。
  4. 当left>=right时,停止,然后把key放到新坑中(第一趟分割完了)
  5. 递归

代码:

//挖坑法,快排
void  PartSort4(int* arr, int begin, int end)
{
	if (begin >= end)
		return;
	int key = arr[begin];
	int left = begin;
	int right = end;
	while (left < right)
	{
		//找小
		while (left < right&&arr[right]>=key)
		{
			--right;
		}
		//放到左边的坑位中,右边形成新的坑位
		arr[left] = arr[right];
		//找大
		while (left< right&&arr[left]<=key)
		{
			left++;
		}
		//放到右边的坑位中,左边形成新的坑位
		arr[right] = arr[left];
	}
	arr[left] = key;//把key放入新坑
	int meeti = left;
	//分割后两个部分的区间[begin,meeti-1][meeti+1,end]
	//递归
	PartSort4(arr, begin, meeti - 1);
	PartSort4(arr, meeti+1, end);
}

3.前后指针法

  1. 由标题题意就知道,定义两个变量来记录下标,且下标是前后关系,left=0、right=left+1.
  2. 别忘了,我们是来分割的,所以我们还要定义一个关键字(key),取最左边的值为关键字,并保存下标为keyi.
  3. arr[right]<arr[left],left++,交换,然后right++(这里我在代码中做了一下优化)
  4. 直到right>n时,才停止,然后交换arr[left]和arr[keyi](第一趟分割完成)
  5. 递归(在递归中做了一些优化:当数据超大时,一直递归有可能会栈溢出,我们可以排序了一段时间后,直接用插入排序)

在这里插入图片描述
代码:

void  PartSort3(int* arr, int begin, int end)
{
	if (begin >= end)
		return;
	if (end - begin > 17)//优化
	{
		int left = begin;
		int right = left + 1;
		int keyi = begin;
		while (right <= end)
		{
			
			if (arr[right] < arr[keyi] && ++left != right)
			{
				Swap(&arr[right], &arr[left]);
			}
			++right;
		
		}
		Swap(&arr[left], &arr[keyi]);
		keyi = left;//这时候left的位置就是分割的地方
		//分割后两个部分的区间[begin,keyi-1][keyi+1,end]
		//递归
		PartSort2(arr, begin, keyi - 1);
		PartSort2(arr, keyi + 1, end);
	}
	else
	{
		InsertSort(arr + begin, end - begin + 1);		
	}
}

4.快排的优化(三数取中)

看完上面三种方法后,我们应该已经发现了,当一个数组中是有序的情况下,我们每次分割只得到比上一次小一个记录的子序列,注意另一个为空,这时候我们的时间复杂度是O(N2)

在这里插入图片描述
所以,我们可以三数取中来优化

  • 找下标为0的值,和下标为n的值,和下标为(n+0)/2的值,用它们的中位数来当key

代码:

//三数取中,找中间值
int GetMidIndex(int *arr,int left,int right)
{
	int mid = (left + right) >> 1;
	if (arr[left] > arr[mid])
	{
		if (arr[mid] > arr[right])
			return mid;
		else if (arr[left] < arr[right])
			return left;
		else
			return right;
	}
	else//arr[left]<=arr[mid]
	{
		if (arr[left]>arr[right])
			return left;
		else if (arr[mid] < arr[right])
			return mid;
		else
			return right;		
	}
}

5.迭代实现(非递归)

这可是我们的大餐啊,都说快排用递归实现,那我们如何用非递归实现呢?

这就得我们用栈或者队列来实现了

  1. 栈是来存储我们要进行分割排序的这段区间的下标的
  2. 把我们要进行区间的下标放入栈中,然后再放入,再取出。(每一次放入的都不同)
  3. 每取出一段区间,我们都对这段区间来进行单趟排序
  4. 然后把分割成两部分的区间下标,再放入栈中,等下次来取
  5. 判断结束的标志是栈中是否还有未处理的区间
  6. 而判断区间下标是否需要放入栈中的条件是,其区间中元素的个数是否大于1
  7. 排完后,销毁栈

代码:

//非递归快排,迭代
void QuickSortNonR(int* a, int left, int right)
{
	//创建栈
	Stack st;
	//初始化栈
	StackInit(&st, 10);
	//先入大区间
	if (left < right)
	{
		StackPush(&st, right);
		StackPush(&st, left);
	}
	//栈不为空,说明还有没处理的区间
	while (StackEmpty(&st) != 0)//终止条件
	{
		left = StackTop(&st);
		StackPop(&st);
		right = StackTop(&st);
		StackPop(&st);
		//快排单趟排序
		int div = PartSort5(a, left, right);//这个函数是进行单趟排序的
		// [left div-1]
		// 把大于1个数的区间继续入栈
		if (left < div - 1)//判断是否还需要接着排序的条件
		{
			StackPush(&st, div - 1);
			StackPush(&st, left);
		}

		// [div+1, right]
		if (div + 1 < right)
		{
			StackPush(&st, right);
			StackPush(&st, div + 1);
		}
	}
	StackDestroy(&st);//最后销毁栈
}

总结:

  1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
  2. 时间复杂度:O(N*logN)
  3. 空间复杂度:O(logN)
  4. 稳定性:不稳定

总结

有问题的地方欢迎指出,共同学习,我们下期见。

以上是关于排序2-冒泡排序与快速排序(递归加非递归讲解)的主要内容,如果未能解决你的问题,请参考以下文章

nodejs实现冒泡排序和快速排序

java快速排序引起的StackOverflowError异常

8种面试经典!排序详解--选择,插入,希尔,冒泡,堆排,3种快排,快排非递归,归并,归并非递归,计数(图+C语言代码+时间复杂度)

8种面试经典排序详解--选择,插入,希尔,冒泡,堆排,3种快排及非递归,归并及非递归,计数(图+C语言代码+时间复杂度)

8种面试经典排序详解--选择,插入,希尔,冒泡,堆排,3种快排及非递归,归并及非递归,计数(图+C语言代码+时间复杂度)

六千字快速复习七种常用排序