数据结构·交换排序(冒泡排序、快速排序)

一、冒泡排序

1、核心思想

通过对待排序序列从前向后(从下标较小的元素开始),依次对相邻两个元素的值进行两两比较,若发现逆序则交换,使值较大的元素逐渐从前移向后部,就如果水底下的气泡一样逐渐向上冒。

2、算法分析

冒泡排序演示

设总的元素个数为n,那么由上边的排序过程可以看出:
(1)总计需要进行(n-1)轮排序,也就是(n-1)次大循环
(2)每轮排序比较的次数逐轮减少

3、代码实现

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

二、快速排序

1、快速排序核心思想

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法.

快速排序的核心思想是“分而治之”。它将一个未排序的数组划分为两个子数组,然后对这两个子数组分别进行排序,最后再将排好序的子数组合并在一起。 这个过程在递归的帮助下不断重复,直到整个数组有序为止。这种将问题切分成更小的子问题处理的方法,使得快速排序能够高效地处理大规模的数据

快速排序的核心操作是“划分”,通常是选择一个基准元素,将返回的基准位置分为左右两边,数组中比基准元素小的移到基准元素的左边,比基准元素大的移到基准元素的右边。 这个过程称为“分区”,它保证了在完成一轮分区后,基准元素的位置是确定了的。接下来,基准元素的左右子数组将分别作为新的子问题继续递归处理。直到所有元素都排列在相应位置上为止。

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

2、快速排序各版本

1、霍尔法
①.核心思想

选择序列的第一个元素作为基准值,并分别从序列的两端开始向中间遍历,交换不符合规则的元素,直到两个指针相遇。然后将基准值与指针相遇的位置的元素交换,此时基准值左侧的元素都小于等于它,右侧的元素都大于等于它。再对左右两个子序列分别递归进行同样的操作,直到排序完成。

②.算法分析

霍尔法

霍尔法单趟过程分析:
1.先记录下基准值key的位置,让 left 和 right 向两端靠近(直至相遇)。
2.right 小兵先走,直到遇到一个比 key 值要小的数字就停下
3**.right 小兵停下后,left 小兵再走,直到遇到一个比 key 值要大的数字就停下**。
4.交换 left 位置和 right 位置上的值
5.right 小兵继续走,重复 2,3,4动作,直到 left 小兵与 right 小兵相遇
6.相遇之后,将相遇位置的值与基准值 key 位置上的值交换,让相遇位置置成新的 key
注意:基准值 key 在左边, right 小兵先走;基准值 key 在右边,left 小兵先走。

③.代码实现
int PartSort1(int* a, int left, int right)
{
	//key left right
	int key = left;
	//右找小,左找大
	while (left < right)
	{
		while (left < right && a[right] >= a[key])
		{
			--right;
		}
		while (left < right && a[left] <= a[key])
		{
			++left;
		}
		Swap(&a[left], &a[right]);
	}
	Swap(&a[key], &a[left]);
	return left;
}
void QuickSort_v1(int* a, int left, int right)
{
	if (left >= right)
		return;
	int keyi = PartSort1(a, left, right);
	QuickSort_v1(a, left, keyi - 1);
	QuickSort_v1(a, keyi + 1, right);
}

常见误区

  • while (left < right && a[right] >= a[keyi])while (left < right && a[left] <= a[keyi])中的判断条件left < right要在前面,以防出现越界
  • 不能写成left = keyi + 1,如果keyi之后位置的值都比它大,那么rightleft 将会在 keyi+1 的位置相遇,可能导致错误的交换。

left与right相遇位置 值的分析( keyi在左边时)
若 left 小兵与 right 小兵相遇,又有两种情况:

  • left 小兵走的时候,遇到 right 小兵(L遇R);
  • right 小兵走的时候,遇到 left 小兵(R遇L)。
  1. left 小兵走的时候,遇到 right 小兵

因为是 key 位置在左边, right 小兵先走所以当 right 小兵停下时,其位置上的值一定是比 key 位置上的值小的。这时,left 小兵来了, 两个小兵相遇,相遇的位置就是 right 小兵停下的位置,即相遇的位置比 key 位置上的值要小。

  1. right 小兵走的时候,遇到 left 小兵

因为是 key 位置在左边, right 小兵先走。经过几轮交换之后,相遇的位置就是 left 小兵的位置,此时,因为经过了上一轮 left 位置上的值 与 right 位置上的值 交换。left 位置上的值就是上一轮交换中 right 停下位置上那个比 key 值小的值。即交换之后 left 位置上的值是比 key 位置上的值要小的,所以相遇的位置比 key 位置上的值要小

2、挖坑法(替换法、填充法)
①.核心思想

挖坑法的思路与霍尔法的思路大致相同。

②.算法分析

挖坑法

1.先将 key 的值存起来,注意:此处的 key 存的是值,而不是位置下标。将该数据位置看作是一个坑(记作是 hole )。
2.最初时,left 小兵在最左边(下标为0的位置),right 小兵在最右边。
3.如动图中,因为 hole坑在最左边,所以还是 right 小兵先走,找比 key值要小的值。找到之后,将 right 位置上的值放到原来的坑上,在将此时 right 位置 记作新坑。
4.right 位置上形成一个新坑后,left 小兵出发,找比 key 值要大的值。找到之后,将 left 位置上的值放到原来的坑上,在将此时 left 位置 记作新坑。
5.left 位置上形成新坑后,right 小兵再走,重复3,4动作,直到 left 小兵与 right 小兵相遇。
6.相遇之后,将坑上填入 key 值。
7.最后返回 hole 最后的位置,这个位置就是基准值。
通过这种方式,每一趟排序都会将一个基准元素放置到正确的位置上,并形成一个新的坑,然后再对左右两部分进行排序。这样不断重复,直到整个数组有序。挖坑法的关键在于通过交替填坑的方式实现元素的分割和排序。

两个小兵相遇的位置一定是一个坑。
有坑的小兵不走;填坑时,要先将原来的坑给补上,再建立新坑

③.代码实现
// 快速排序挖坑法
int PartSort2(int* a, int left, int right)
{
	int hole = left;
	int key = a[left];
	
	while (left < right)
	{
		while (left < right && a[right] >= key)
		{
			right--;
		}
		Swap(&a[hole], &a[right]);
		hole = right;
		while (left < right && a[left] <= key)
		{
			left++;
		}
		Swap(&a[hole], &a[left]);
		hole = left;
	}
	a[hole] = key;
	return hole;
}
void QuickSort_v2(int* a, int left, int right)
{
	if (left >= right)
		return;
	int key = PartSort2(a, left, right);
	QuickSort_v2(a, left, key - 1);
	QuickSort_v2(a, key + 1, right);
}
3、前后指针法
①.核心思想

相较于前文中的两种方法,前后指针法相对难理解,但代码实现更简单。

②.算法分析

快速排序前后指针法

1.选择数组的第一个元素下标作为基准值key。
2.初始化两个指针cur和prev,分别指向数组的起始位置和起始位置的下一个位置。
3.当cur遇到比keyi的大的值以后,只需要++cur,因为他们之间的值都是比key大的值,
4.如果cur指针指向的元素小于基准值,先将prev指针向右移动一位,然后在将快指针指向的元素与慢指针指向的元素交换。
5.重复步骤3到步骤4,直到cur指针超出数组范围。结束循环.
6.将基准值的元素与prev指针位置的元素交换。此时基准值的左边元素都是比基准值小或者等于基准值,右边都比他大或者等于基准值。
7.最后返回prev下标。

③.代码实现
int PartSort3(int* a, int left, int right)
{
	int prev = left;
	int cur = prev + 1;
	int key = a[left];
	
	while (cur <= right)
	{
		if (a[cur] < key && prev++ != cur)
		{
			Swap(&a[cur], &a[prev]);
		}
		cur++;
	}
	Swap(&a[prev], &a[left]);
	return prev;
}
void QuickSort_v3(int* a, int left, int right)
{
	if (left >= right)
		return;
	int key = PartSort3(a, left, right);
	QuickSort_v3(a, left, key - 1);
	QuickSort_v3(a, key + 1, right);
}

常见误区

  • 对于if (a[cur] < key && prev++ != cur)很难理解 prev++ != cur为什么要有这个判读条件。当prev + 1 != cur为真时表明此时prev与cur之间一定存在比key大的数,不为真时cur与prev都需要++,此时即执行了++有没有调用交换函数
4、三路划分法
①.核心思想

快速排序的三路划分是为了解决数组中存在大量重复元素时,快速排序算法性能下降的问题。
在传统的快速排序算法中,选择一个基准元素,将数组划分为两个子数组,其中一个子数组中的元素都小于基准元素,另一个子数组中的元素都大于基准元素,然后对两个子数组进行递归排序。然而,当数组中存在大量重复元素时,传统的快速排序算法会导致不必要的比较和交换操作,从而降低算法的效率。
三路划分的主要目的是将数组划分为三个部分,分别存放小于、等于和大于基准元素的元素,以减少不必要的比较和交换操作
通过三路划分,可以将相等的元素集中在一起,减少了对相等元素的重复比较和交换操作,提高了算法的效率。尤其在面对存在大量重复元素的情况下,三路划分可以有效地改善快速排序的性能。

②.算法分析

三路划分演示——1

  1. 选择一个基准元素。(通常是数组的第一个元素)
  2. 初始化三个指针:begin指针指向基准值的索引位置,cur指针指向begin + 1的位置,end指针指向数组末尾的位置
  3. 从数组的起始位置开始遍历到末尾位置。
  4. a[c] < key如果当前元素小于基准元素,则将当前cur指针指向的元素交换到begin指针的位置,并将begin指针右移,cur指针右移。
  5. a[c] > key如果当前cur指针元素大于基准元素,则将当前cur指针指向元素交换到end指针的位置,并将end指针左移。由于交换后的元素是未经比较的新元素,所以cur指针不移动。
  6. a[c] == key如果当前元素等于基准元素,则将cur指针右移。
  7. 重复步骤4-6,直到cur指针遇见end指针则遍历完成。循环结束。
  8. 最后, 数组被划分为了小于基准元素、等于基准元素和大于基准元素的三个部分。接下来,需要对小于和大于基准元素的两个部分分别进行递归排序。
  • 对小于基准元素的部分进行递归排序:将小于基准元素的部分作为新的子数组,重复进行上述三路划分和递归排序的过程。
  • 对大于基准元素的部分进行递归排序:将大于基准元素的部分作为新的子数组,重复进行上述三路划分和递归排序的过程。
③.代码实现
//快速排序三路划分法
void QuickSort_v4(int* a, int left, int right)
{
	if (left >= right)
		return;

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

	int key = a[left];

	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 cur++;
	}
	QuickSort_v4(a, begin, left - 1);
	QuickSort_v4(a, right + 1, end);
}

5、非递归版本

①.核心思想

快速排序是一种常用的排序算法,基于递归的实现方式是最常见的。然而,当输入规模较大时,递归层数过深会导致栈溢出的问题。为了解决这个问题,可以使用栈来实现快速排序的非递归版本.
在非递归版本的快速排序中,被用来模拟递归调用的过程。具体而言,该算法使用一个栈来存储待排序子数组的起始和结束索引。通过迭代的方式将原本递归调用的过程转化为循环,避免了递归函数调用的开销

②.算法分析
③.代码实现
// 快速排序 非递归实现
void QuickSortNonR(int* a, int left, int right)
{
	Stack St;
	StackInit(&St);
	StackPush(&St, right);
	StackPush(&St, left);
	while (!StackEmpty(&St))
	{
		int begin = StackTop(&St);
		StackPop(&St);
		int end = StackTop(&St);
		StackPop(&St);
		int mid = PartSort1(a, begin, end);
		if (mid < end - 1)
		{
			StackPush(&St, end);
			StackPush(&St, mid + 1);
		}
		if (mid > begin + 1)
		{
			StackPush(&St, mid - 1);
			StackPush(&St, begin);
		}
	}
	StackDestroy(&St);
}

注意事项

  • 由于栈后进先出的特性,在对数据进行入栈时先入右,再入左,则出栈时先出左,后出右,反之亦然。

定义一个栈,用于记录每个待排序子数组的起始和终止索引。
将初始的起始索引和终止索引入栈,表示要对整个数组进行排序。
进入循环,直到栈为空
出栈得到当前子数组的起始和结束索引。
以子数组的第一个元素作为基准,对子数组进行划分,将小于基准的元素放在基准的左侧,大于基准的元素放在基准的右侧。

  • 如果基准元素的左侧仍有未排序的元素,将其起始索引和终止索引入栈;
  • 如果基准元素的右侧仍有未排序的元素,将其起始索引和终止索引入栈。
    如果划分后得到的左右子数组的长度大于1,则将它们的起始和结束索引依次入栈。
    当栈为空时,排序完成。

6、优化

1.三数取中选key法 + 随机数生成选key法

当数组接近有序,快速排序会变的变成非常糟糕,时间复杂度是O(N^2)。
每次选择的基准元素可能会导致分割得到的左右子序列的大小差异很大,从而使得快速排序的效率下降。

具体来说,当数组接近有序时,快速排序的分割操作可能会将一个较小的元素放在一个较大的元素的右边,或者将一个较大的元素放在一个较小的元素的左边。这样一来,在每一次划分操作后,都会有一个较小的子序列和一个较大的子序列。如果这种情况持续发生,那么快速排序就会退化成类似于冒泡排序的过程,每次只能将一个元素放到它最终的位置上,排序的效率会大大降低。

// 快速排序递归实现
int GetMidIndex(int* a, int left, int right)
{
	if (right - left == 0)
		return left;
	int mid = left + (rand() % (right - left));
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
			return mid;
		else if (a[right] > a[left])
			return right;
		else return left;
	}
	//a[left] > a[mid]
	else
	{
		if (a[left] < a[right])
			return left;
		else if (a[mid] > a[right])
			return mid;
		else return right;
	}
}
2.小区间优化

在快速排序算法中,当子区间的大小足够小时,可以考虑使用插入排序来代替递归调用。这是因为插入排序在处理小规模数据时具有较好的性能

当子区间的大小较小时,递归调用的开销可能会比排序本身的开销更大,因为递归调用需要额外的函数调用和栈空间的使用。而插入排序是一种简单且高效的排序算法,对于小规模的数据集,它的性能优于快速排序。

在实践中,可以通过设置一个阈值来决定是否使用插入排序。当子区间的大小小于阈值时,使用插入排序;否则,继续使用快速排序进行递归划分。

使用插入排序的优点是它对于部分有序的数据集具有较好的性能,因为插入排序每次将一个元素插入到已排序的序列中,对于有序度较高的数据集,插入排序的比较和移动操作会较少。

总而言之,使用插入排序来替代递归调用的快速排序可以在处理小规模数据时提高性能,并减少递归调用的开销。这是一种常见的优化策略,可以根据实际情况进行调整和实现。

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

    int keyi = Part_Sort3(a, left, right);
    if (keyi - left > 10)
    {
        Quick_Sort(a, left, keyi - 1);
    }
    else
    {
        InsertSort(a + left, keyi - 1 - left + 1);
    }

    if (right - keyi > 10)
    {
        Quick_Sort(a, keyi + 1, right);
    }
    else
    {
        InsertSort(a + keyi + 1, right - keyi + 1 - 1);
    }
}

7、快速排序各版本+优化完整代码

// 快速排序递归实现
int GetMidIndex(int* a, int left, int right)
{
	if (right - left == 0)
		return left;
	int mid = left + (rand() % (right - left));
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
			return mid;
		else if (a[right] > a[left])
			return right;
		else return left;
	}
	//a[left] > a[mid]
	else
	{
		if (a[left] < a[right])
			return left;
		else if (a[mid] > a[right])
			return mid;
		else return right;
	}
}
// 快速排序hoare版本
int PartSort1(int* a, int left, int right)
{
	int keyi = GetMidIndex(a,left,right);
	Swap(&a[left], &a[keyi]);
	keyi = 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;
}
// 快速排序挖坑法
int PartSort2(int* a, int left, int right)
{

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

	hole = left;
	while (left < right)
	{
		while (left < right && a[right] >= key)
		{
			right--;
		}
		Swap(&a[hole], &a[right]);
		hole = right;
		while (left < right && a[left] <= key)
		{
			left++;
		}
		Swap(&a[hole], &a[left]);
		hole = left;
	}
	a[hole] = key;
	return hole;
}
// 快速排序前后指针法
int PartSort3(int* a, int left, int right)
{
	int prev = GetMidIndex(a, left, right);
	Swap(&a[left], &a[prev]);
	prev = left;
	int cur = prev + 1;
	int key = a[left];
	while (cur <= right)
	{
		if (a[cur] < key && prev++ != cur)
		{
			Swap(&a[cur], &a[prev]);
		}
		cur++;
	}
	Swap(&a[prev], &a[left]);
	return prev;
}
//快速排序三路划分法
//int PartSort4(int* a, int left, int right);

void QuickSort_v1(int* a, int left, int right)
{
	if (left >= right)
		return;
	int key = PartSort1(a, left, right);
	QuickSort_v1(a, left, key - 1);
	QuickSort_v1(a, key + 1, right);
}
void QuickSort_v2(int* a, int left, int right)
{
	if (left >= right)
		return;
	int key = PartSort2(a, left, right);
	QuickSort_v2(a, left, key - 1);
	QuickSort_v2(a, key + 1, right);
}
void QuickSort_v3(int* a, int left, int right)
{
	if (left >= right)
		return;
	int key = PartSort3(a, left, right);
	QuickSort_v3(a, left, key - 1);
	QuickSort_v3(a, key + 1, right);
}
//快速排序三路划分法
void QuickSort_v4(int* a, int left, int right)
{
	if (left >= right)
		return;

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

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

	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 cur++;
	}
	QuickSort_v4(a, begin, left - 1);
	QuickSort_v4(a, right + 1, end);
}
// 快速排序 非递归实现
void QuickSortNonR(int* a, int left, int right)
{
	Stack St;
	StackInit(&St);
	StackPush(&St, right);
	StackPush(&St, left);
	while (!StackEmpty(&St))
	{
		int begin = StackTop(&St);
		StackPop(&St);
		int end = StackTop(&St);
		StackPop(&St);
		int mid = PartSort1(a, begin, end);
		if (mid < end - 1)
		{
			StackPush(&St, end);
			StackPush(&St, mid + 1);
		}
		if (mid > begin + 1)
		{
			StackPush(&St, mid - 1);
			StackPush(&St, begin);
		}
	}
	StackDestroy(&St);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值