算法——冒泡排序与快速排序

目录

Ⅰ.冒泡排序

1.基本思想

2.代码实现

3.总结

Ⅱ.快速排序

1.基本思想

2.hoare法

基本思想:

代码实现:

3.挖坑法

基本思想:

代码实现: 

4.前后指针法

基本思想:

 代码实现:

5.非递归法

基本思想:

代码实现:

6.快速排序的优化与改进

7.总结


Ⅰ.冒泡排序

1.基本思想

反复交换相邻两个元素的位置,从而把待排序的元素序列逐个比较并按照大小重新排列。

2.代码实现

我们分析发现

在每一趟中都要保证本趟中最大的值处于本次的最后,保证排出升序

// 冒泡排序
void BubbleSort(int* a, int n)
{
	int j = 0;
	for(j = 1; j < n; j++)//进行n-1趟排序
	{
		int i = 0;
		for (i = 0; i < n - j; i++)//得到从开始到其后的n-j个数据的下标
		{
			//不断地将本趟中最大的值向后移动
			//保证每趟的最后一个数据一定是本趟中最大的数值
			if (a[i] > a[i + 1])
			{
				Swap(&a[i], &a[i + 1]);
			}
		}
	}

	ArrPrint(a, n);
}

我们用以下代码来验证

void test1()
{
	int a[] = { 8,6,4,9,7,1,5,3,2,0 };
	int sz = sizeof(a) / sizeof(a[0]);

	BubbleSort(a, sz);
}

int main()
{
	test1();

	return 0;
}

 有

3.总结

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

Ⅱ.快速排序

1.基本思想

任取待排序元素序列中 的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右 子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止

图示如下 

因此我们可以构建一个基础的通用代码模型

// 假设按照升序对a数组中[left, right]区间中的元素进行排序
void QuickSort(int* a, int left, int right)
{
	//如果最左的下标与最右重叠(当前仅有一个值)或大于最右(不存在)时就结束
	if (left >= right)
		return;

	// 按照基准值对a数组的 [left, right]区间中的元素进行划分
	int key = PartSortn(a, left, right);

	// 划分成功后以key为边界形成了三部分 [left, key-1]、key和 [key+1, right]
	// 递归排[left, key-1]
	QuickSort(a, left, key-1);

	// 递归排[key+1, right]
	QuickSort(a, key + 1, right);
}

2.hoare法

基本思想:

  1. 首先从数列中选择一个基准值。

  2. 将数列中小于等于基准值的元素放在基准值的左侧,将大于等于基准值的元素放在基准值的右侧。

  3. 对左右两个子序列分别重复以上步骤,直到每个子序列只剩下一个元素为止。

代码实现:

//取三数中的中间值
int GetMidNumi(int* a, int left, int right)
{
	int mid = (left + right) / 2;
	if (a[left] < a[mid])//left<mid
	{
		if (a[mid] < a[right])//left<mid<right
		{
			return mid;
		}
		else if (a[left] > a[right])//right<left<mid
		{
			return left;
		}
		else//left<right<mid
		{
			return right;
		}
	}
	else//left>mid
	{
		if (a[mid] > a[right])//left>mid>right
		{
			return mid;
		}
		else if (a[left] < a[right])//right>left>mid
		{
			return left;
		}
		else//left>right>mid
		{
			return right;
		}
	}
}
 

// 快速排序hoare版本
int PartSort1(int* a, int left, int right)
{
	//取左右中三个数中的最中间值
	int midi = GetMidNumi(a, left, right);
	if (midi != left)
	{
		Swap(&a[midi], &a[left]);
	}

	int keyi = left;
	while (left<right)//left与right不相遇就继续循环
	{
		//右边找到小(或等)或者左右相遇停止,
		//没找到就继续
		while (left < right && a[right] >= a[keyi])
			right--;
		//左边找到大(或等)或者左右相遇停止,
		//没找到就继续
		while (left < right && a[left] <= a[keyi])
			left++;
		//交换找到的值,将小值放在左边,大值放在右边
		Swap(&a[left], &a[right]);
	}
	//交换key与左值
	Swap(&a[keyi], &a[left]);
	//左值与key交换之后,处于left位置的数值已经处于正确的位置
	return left;
}

我们用如下代码测试

void test2()
{
	int a[] = { 8,6,4,9,7,1,5,3,2,0 };
	int sz = sizeof(a) / sizeof(a[0]);

	QuickSort(a, 0, sz - 1);
	ArrPrint(a, sz);
}

int main()
{
	test2();

	return 0;
}

 有

3.挖坑法

基本思想:

  1. 选取数组的一个元素作为基准值 key。

  2. 从数组的最右端开始向左遍历,寻找第一个小于 key 的元素,将其挖出,并将该位置留空作为坑。

  3. 从数组的最左端开始向右遍历,寻找第一个大于 key 的元素,将其挖出,并填入刚刚留下的坑中。

  4. 重复执行步骤 2 和步骤 3,直到左右两个遍历指针相遇。

  5. 将基准值 key 填入最后一个坑中,此时 key 左侧所有元素都小于 key,右侧所有元素都大于等于 key。

  6. 对左右两个子序列分别重复以上步骤,直到每个子序列只剩下一个元素为止。

图示如下

代码实现: 

// 快速排序挖坑法
int PartSort2(int* a, int left, int right)
{
	//取左右中三个数中的最中间值
	int midi = GetMidNumi(a, left, right);
	if (midi != left)
	{
		Swap(&a[midi], &a[left]);
	}

	//将基准值保存,并设置坑位
	int key = a[left];
	int hole = left;
	while (left < right)//左右未相遇就继续寻找
	{
		//右边找小
		while (left < right && key < a[right])
			--right;
		//找到小或者相遇时停止,将找到的数放入坑中并将坑置于right处
		a[hole] = a[right];
		hole = right;

		//左边找大
		while (left<right && key>a[left])
			++left;
		//找到大或者相遇时停止,将找到的数放入坑中并将坑置于left处
		a[hole] = a[left];
		hole = left;
	}
	//将key值填入hole中
	a[hole] = key;
	//此时hole处所在位置的值已经处于正确位置
	return hole;
}

还是采用hoare法的检测代码,有

4.前后指针法

基本思想:

  1. 选取数组的一个元素作为基准值 pivot。

  2. 设置两个指针,一个指向数组的第一个元素(即前指针),一个指向 pivot(即后指针)。

  3. 从前指针开始向后遍历,寻找第一个大于等于 pivot 的元素。

  4. 将该元素与后指针指向的元素交换位置,后指针向后移动一个位置。

  5. 重复执行步骤 3 和步骤 4,直到前指针遍历到数组的最后一个元素。

  6. 将 pivot 与后指针指向的元素交换位置,此时 pivot 左侧所有元素都小于 pivot,右侧所有元素都大于等于 pivot。

  7. 对左右两个子序列分别重复以上步骤,直到每个子序列只剩下一个元素为止。

图示如下

 代码实现:

// 快速排序前后指针法
int PartSort3(int* a, int left, int right)
{
	//取左右中三个数中的最中间值
	int midi = GetMidNumi(a, left, right);
	if (midi != left)
	{
		Swap(&a[midi], &a[left]);
	}

	int keyi = left;
	int cur = left + 1;//第二个数作cur
	int prev = left;//第一个数作prev

	while (cur <= right)//cur不越界就继续
	{
		//找小,如果当前值小于key值且prev增加后不与cur相同
		//(如果相同说明prev与cur紧邻,此时只需对cur++即可)
		//交换key与cur的值
		if (a[keyi] > a[cur] && prev++ != cur)
			Swap(&a[cur], &a[prev]);

		cur++;
	}
	//cur出界后,交换keyi与prev所在位置的值
	Swap(&a[keyi], &a[prev]);
	//此时,prev所处位置的值已处于正确位置
	return prev;
}

还是采用上述的代码检验,有

5.非递归法

基本思想:

  1. 将待排序序列的起始位置和结束位置压入栈中,作为当前子序列的起始状态。

  2. 循环执行以下操作,直到栈为空:

    • 弹出栈顶元素,得到当前子序列的起始位置 left 和结束位置 right。
    • 对子序列进行一次快速排序,得到基准值的位置 keyi。
    • 如果基准值左侧子序列元素个数大于 1,将左侧子序列的起始位置 left 和结束位置 keyi-1 压入栈中,等待下一轮排序。
    • 如果基准值右侧子序列元素个数大于 1,将右侧子序列的起始位置 keyi+1 和结束位置 right 压入栈中,等待下一轮排序。
  3. 排序完成后,原序列就变成了有序的序列。

图示如下

在对0(begin)到9(end)进行一趟快排后,得到一个基准值key,如果左右两侧元素均不只有一个元素,将key左侧起始位置(begin)到key-1(end)位置压栈,再将key右侧keyi+1(begin)位置到右侧结束位置(end)压栈

代码实现:

// 快速排序 非递归实现(借助栈实现)
void QuickSortNonR(int* a, int left, int right)
{
	//创建并初始化栈后,分别将left与right分别压栈
	ST st;
	STInit(&st);
	STPush(&st, left);
	STPush(&st, right);
	
	while (!STEmpty(&st))//当栈不为空时,重复进行快排
	{
		//分别取得本区域内的起始位置与结束位置
		int end = STTop(&st);
		STPop(&st);
		int begin = STTop(&st);
		STPop(&st);

		//对本区域的数据进行一次快排
		int keyi = PartSort3(a, begin, end);
		//得到[begin,keyi-1],keyi,[keyi+1,end]三个区域
		//将keyi左边的起始位置begin与结束位置keyi-1分别压栈
		if (begin < keyi - 1)
		{
			STPush(&st, begin);
			STPush(&st, keyi - 1);
		}
		//将keyi右边的起始位置keyi+1与结束位置end分别压栈
		if (keyi + 1 < end)
		{
			STPush(&st, keyi + 1);
			STPush(&st, end);
		}
	}
}

我们使用如下的检测代码

void test3()
{
	int a[] = { 8,6,4,9,7,1,5,3,2,0 };
	int sz = sizeof(a) / sizeof(a[0]);

	QuickSortNonR(a, 0, sz - 1);
	ArrPrint(a, sz);
}

int main()
{
	test3();

	return 0;
}

 有

6.快速排序的优化与改进

其实对于快速排序,我们可以发现它的整体是类似于完全二叉树的形式,而在数据量较少时,我们可以采取效率更高的插入排序,即

// 假设按照升序对a数组中[left, right]区间中的元素进行排序
void QuickSort(int* a, int left, int right)
{
	//如果最左的下标与最右重叠(当前仅有一个值)或大于最右(不存在)时就结束
	if (left >= right)
		return;

	//当小区间数据量小于一定量时,使用插入排序
	if (right - left < 15)
	{
		InsertSort(a+left,right-left);
	}

	// 按照基准值对a数组的 [left, right]区间中的元素进行划分
	int key = PartSort3(a, left, right);

	// 划分成功后以key为边界形成了三部分 [left, key-1]、key和 [key+1, right]
	// 递归排[left, key-1]
	QuickSort(a, left, key-1);

	// 递归排[key+1, right]
	QuickSort(a, key + 1, right);
}

在使用了插入排序后,我们可以发现对于一整个快速排序而言,最后一排的数据量近乎占据了整个数据量的一半,因此对栈帧的开辟作出了一定的优化。

7.总结

1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫 快速 排序
2. 时间复杂度: O(N*logN)
3. 空间复杂度: O(logN)
4. 稳定性:不稳定
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值