【再撕排序】A:快速排序 冒泡排序(超详解+附动图+源码)

系列文章目录
第一章 A : 快速排序 冒泡排序 (含直接插入排序)

第二章 B : 更新中

前言

较为常见的排序可大致四类,按个人的喜好,在本篇(A篇)中先介绍快速排序,冒泡排序



一、排序铺垫

在排序时,交换数一直很常见,单拎出来方便后续操作 

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

在排序完后,次次都要调试才看出来,过于麻烦。打包一个函数,方便看出来排序排成了个啥,最起码 知道个对错😂。

void PrintfA(int* a, int n)
{
	for (int i = 0; i < n; ++i)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}


例如

void  TestTQSort()
{

	int a[] = { 6,1,2,7,9,3,4,5,1,8,10,8 };
	//int a[] = { 2,2,2,2,2,2,2,2,2,2,2,2,2 };
	//int a[] = { 6,1,2,7,9,3,4,5,1,8,10,8 };
	TQSort(a,0, sizeof(a) / sizeof(int)-1);
	PrintfA(a, sizeof(a) / sizeof(int));
}


二、快速排序(内含插入排序)

插入排序


快速排序(hoare版)图示作引 

快速排序的基本套路:

1) 快排的关键便是找到一个key,以他为分界[begin,key-1],key,[key+1,end]独立的两部分。

2)在key左右两边同时经行排序 交换一部分记录的元素值均比基准元素值小,另一部分记录的 元素值比基准值大。

3)然后分别对这两部分记录用同样的方法继续进行排序,直到整个序列有序。(一般递归即可)

快速排序中有两个较恶劣情况:

1.要排序的数非常非常多,针对key的寻找较为困难,可采用随机值版的三数取中

2.所有数都相等,例如 对2000个2进行排序,这种情况可采用三路快排的方法

现直接出示我认为最舒服的版本,并会在后面放出一般的非递归版本



1.插入排序(在这作为快排的一部分)

直接插入排序虽性能较差,但胜在思路与操作简单,非常适合在小区间使用

void InsertSort(int* a, int n)
{
	for(int i=0;i<n-1;++i)
	{
		int end = i;
		int tmp = a[end + 1];
		while (end >= 0)
		{
			if (tmp < a[end])//升序
			{
				a[end + 1] = a[end];
				--end;
			}
			else
			{
				break;
			}
		}
		a[end + 1] = tmp;
	}
}

时间复杂度效率:时间复杂度:O(n^2).

其他的插入排序有二分插入排序,2-路插入排序。


2.三数取中(随机值版)

三数取中即是为了找个基准值key,这对其性能影响较大。

即在在起始位置,中间位置,末尾位置中选出中间值,作为基准值。

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

3.三路快排(排序的主体)

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

	if ((end - begin + 1) < 15)
	{
		// 小区间用直接插入替代,减少递归调用次数
		InsertSort(a+begin, end - begin + 1);
	}
	else
	{
		int mid = GetMidIndex(a, begin, end);
		Swap(&a[begin], &a[mid]);

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

		// [begin, left-1][left, right][right+1,end]
		TQSort(a, begin, left - 1);
		TQSort(a, right + 1, end);
	}
}


三.冒泡排序

                                                          冒泡排序

就是一个带一个,两两一对送到后排

void BubbleSort(int* a, int n)
{
	for (int j = 0; j < n; ++j)
	{
		int exchange = 0;
		for (int i = 0; i < n - j; ++i)
		{
			if (a[i - 1] > a[i])
			{
				Swap(&a[i - 1], &a[i]);
				exchange = 1;
			}
		}
		//如果一趟冒泡
		if (exchange == 0)
		{
			break;
		}
	}
}

经常出现(n-1)是因为只剩两个元素时只需要一趟就可以完成)

冒泡排序可谓是c语言中的扫盲级存在。。。



四.快速排序的非递归

快速排序非递归实现,需要借助栈,栈中存放的是需要排序的左右区间

而且非递归可以彻底解决栈溢出的问题。

其实他的思想与递归是类似的:

  1. 将数组左右下标入栈,
  2. 若栈不为空,两次取出栈顶元素,分别为闭区间的左右界限
  3. 将区间中的元素按照前后指针法排序(其余两种也可)得到基准值的位置
  4. 再以基准值为界限,若基准值左右区间中有元素,则将区间入栈
  5. 重复上述步骤直到栈为空

 代码如下:

void QuickSortNonR(int* a, int left, int right)
{
    //创建栈
	Stack st;
	StackInit(&st);
 
    //原始数组区间入栈
	StackPush(&st, right);
	StackPush(&st, left);
 
    //将栈中区间排序
	while (!StackEmpty(&st))
	{
        //注意:如果right先入栈,栈顶为left
		left = StackTop(&st);
		StackPop(&st);
		right = StackTop(&st);
		StackPop(&st);
        
        //得到基准值
		int mid = PartSort3(a, left, right);
 
        // 以基准值为分割点,形成左右两部分
		if (right > mid+1)
		{
			StackPush(&st, right);
			StackPush(&st, mid + 1);
		}
		if (left < mid - 1)
		{
			StackPush(&st, mid - 1);
			StackPush(&st, left);
		}
	}
	StackDestroy(&st);
}


五.总结

快速排序的特性总结:

  1.  快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序,一个人外号叫飞毛腿,那肯定是有道理。
  2.  时间复杂度:O(N*logN)
  3.  空间复杂度:O(logN)
  4.  稳定性:不稳定

直接插入排序的特性总结:

  1. 元素集合越接近有序,直接插入排序算法的时间效率越高
  2. 时间复杂度:O(N^2)
  3. 空间复杂度:O(1),它是一种稳定的排序算法
  4. 稳定性:稳定

冒泡排序的特性总结:

  1. 冒泡排序是一种非常容易理解的排序
  2. 时间复杂度:O(N^2)
  3. 空间复杂度:O(1)
  4. 稳定性:稳定

如鲠在喉,不吐不快:快排有一种分而治之的美。

                                                                ——————  我希望正在读这句话的人永远开心



六.避坑版本(请自行观察,注意避错)

如有需要将全部打上"//",以免对大家产生误导

 1.经典整个交换函数还传值,入门级错误

void Swap(int x, int y)
{
	int tmp = 0;
	tmp = x;
	x = y;
	y = tmp;
}


2.浪费版冒泡

{
	for (int j = 0; j < n; ++j)
	{
		for (int i = 0; i < n - j; ++i)
		{
			if (a[i - 1] > a[i])
			{
				Swap(&a[i - 1], &a[i]);
			}
		}
	}
}

 3.真有这么简单吗😄(不止一处,且需注意链表队列的常见错 )

void QuickSort(int* a, int n)
{
	int left = 0,  right = n - 1;
	int key = a[left];
	while (left < right)
	{
		//右边先走找小
	    while (a[right] > key)
		{
			--right;
		}
		//左边再走,找大
		while (a[left] < key)
		{
			++left;
		}

		Swap(&a[left], &a[right]);
	}
	Swap(&a[left], key);
}

本篇彻底完结,感谢你的支持。送人玫瑰,手有余香,你的三连将会是我最大动力,谢谢。

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值