排序(3)之交换排序

目录

前言

交换排序

1.冒泡排序

1.1冒泡排序的实现

1.2 特性总结

2.快速排序

2.1hoare版本

 2.2  挖坑法

 2.3 前后指针版本

3.快速排序的优化

3.1 三数取中法

3.2 小区间优化

4.快速排序的非递归实现


前言

今天小编给大家带来交换排序的内容,对于交换排序中的快速排序在理解上会相对困难一点,小编这里会配合图片给大家细细讲解。那么现在就开始我们今天的主题。


交换排序

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

1.冒泡排序

在介绍快速排序之前,这里先给大家介绍一个常见且理解上比较简单的排序——冒泡排序。

1.1冒泡排序的实现

我们这里以升序为例:

排序(3)之交换排序_第1张图片

 

冒泡排序的排序过程是通过元素之间的不断比较如果前一个元素比后一个元素大,那么就让前一个元素和后一个元素进行交换,那么通过元素之间的不断比较交换最后我们就会发现我们最后一个元素就是我们数组的最大值,那么此时我们就可以把最后一个元素固定,然后重复操作,我们每次都可以选出一个最大数,进行固定,知道我们固定了n-1个元素后,我们也完成了排序操作。

同样我们在实现冒泡排序之前我们需要交换函数:

void swap(int* p, int* q)
{
	int temp = *p;
	*p = *q;
	*q = temp;
}

然后这里我们看冒泡排序的代码:

void BubbleSort(int* a, int n)
{
	
	for (int j = 0; j < n; j++)
	{
		bool exchange = false;
		for (int i = 1; i < n - j; i++)
		{
			if (a[i] < a[i - 1])
			{
				exchange = true;
				swap(&a[i], &a[i - 1]);
			}
		}
		if (exchange == false)
		{
			break;
		}
	}
}

这里我们第一层循环用来控制循环次数,第二层循环控制每次参与循环的元素个个数,由于每循环一次我们下一次的循环元素就会少一个,所以第二层循环的结束条件就是也就是我们每次需要参数与循环的元素个数最大值就是等于n-j。

对于冒泡排序我们还可以对其做一个优化,那么具体是什么优化以及优化在哪里呢?

对于冒泡排序的时间复杂度,我们一般定义在O(N^2)这个量级,但是对于最好的情况下来讲,也就是我们已经是有序的情况,如果我们不加以优化,那么该时间复杂度仍然是在O(N^2)这个量级,但是如果我们在该进入第一次循环时,我们就加一个条件去判断该是否出现了后面值比前面小的情况,如果没出现,那么说明我们数组已经是有序的,那么我们此时直接退出那么这里的时间复杂度也就会大大缩减在O(N)这个量级。

所以小编用一个布尔类型的变量exchange,如果出现了后面值比前面值小的情况时exchange=true就说明,我们数组现在是一个无序的状态,但是没有出现此类情况,我们的exchang=false没有变化,说明数组有序我们直接退出循环。

1.2 特性总结

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

2.快速排序

对于快速排序,我们这里一共有三个版本的方法,但是该本质的思想都是一样的,只不过在代码的实现有着一定差异,这里我们都需要使用到递归的思想。

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

2.1hoare版本

hoare版本的快速排序就是快速排序的开始,对于该排序细节大家可以先看图一下

排序(3)之交换排序_第2张图片

 那么看完这个动态图我想大家已经对快排有了一定的理解,对于快排,首先我们就需要找到一个key值,一般我们选取的就是最左边和最右边,如果选择最左边,那么我们就需要让右边先走,反之就是最左边先走,这里我们让右边找小于key位置内容的值,找到后就停下,左边找比key位置内容大的值,找到后让左边和右边的值进行互换,那么最后左边和右边的相遇值就是key位置内容应该处于的位置,那么就需要让我们相遇位置的内容和key位置的内容进行互换。

对于这里大家可能有几个疑惑:

首先是对于key位置的选择,以及左右位置谁先走的问题,这里主要是为了让我们相遇的位置的值是小于key(左边做key),或者大于key(右边做key)(这是由于我们最后需要让左右相遇位置内容与key位置内容交换),这里小编以将key选最左边为例,给大家解释一下原因:

这里我们分类讨论一下, 对于右边一共就这两种情况相遇情况:

1.右边找到小,左边找大没有找到,左边遇到右边

2.右边找小,没有找到,直接遇到L,要么就是一个比key小的位置,或者直接到keyi

类似道理,右边做key,左边先走,相遇位置就比key要大

第二就是大家可能有疑问,为什么左右相遇的位置,就是我们key位置内容排序后应该处于的位置,这是由于排序的过程中已经让小于key位置的内容的值,放在了keyi的左边,大于的值放在了keyi值得右边,那么相遇的值就是keyi的值。

那为什么说我们快速排序需要使用到递归的思想呢?

这是由于我们每次只能固定一个值的位置,那么为了不后面的排序不影响我们已经排好序的位置,我们这里就把这个数组分为两个区间:

区间位置如下:

排序(3)之交换排序_第3张图片

那么这里我们就继续把左,右区间安赵我们上面的操作进行排序,排序结束后继续分左右区间,那么这里也就类似于二叉树的前序遍历,因此我们这里使用递归思想进行区间分配排序。

那么我们这里看直接先看代码:

void PartSort1(int* a, int left, int right)
{
	if (right-left <= 0)
	{
		return;
	}
	int begin = left;
	int end = right;
	int 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]);
	keyi = left;
	PartSort1(a, begin, keyi - 1);
	PartSort1(a, keyi +1, end);
}

解释如下:

排序(3)之交换排序_第4张图片

 递归返回图:

 排序(3)之交换排序_第5张图片

 2.2  挖坑法

 挖坑法的本质和hoare的版本并没有很大的不同,只是在代码实现方面有点一点点不一样,那么该具体实现过程如下:

 这里由于和hoare大佬版本基本没有很大区别这里小编就不过多说明,这里的操作还是左边找大、右边找下,然后进行坑位互换即可,我们仍然是需要区间划分,而这仅仅只需要我们以排序结束后坑位为中心划分左右区间即可,还是递归思想实现,由于这个的展开图与上面并没有什么区别,这里小编就简单的给大家讲解一下代码的一些细节

// 快速排序挖坑法
void PartSort2(int* a, int left,int right)
{
	if (left >= right)
	{
		return;
    }
	int begin = left;
	int end = right;
	int hole = left;
	int key = a[left];
	while (left < right)
	{
		while (a[right] > key)
		{
			right--;
		}
		swap(&a[left], &a[right]);
		hole = right;
		while (a[left] < key)
		{
			left++;
		}
		swap(&a[left], &a[right]);
		hole = left;
	}
	a[hole] = key;
	PartSort2(a, begin, hole - 1);
	PartSort2(a, hole + 1, end);
}

排序(3)之交换排序_第6张图片

 2.3 前后指针版本

前后指针法的运行过程和之前两个版本有一点的不同,但是本质过程还是将大的值放在后面,小的值移到前面,最后来确定我们的key值所在位置。这里我们看图理解一下:

 

 这里大家可以发现该运行过程是:

1.cur找到比key小的值,++prev,cur和prev的位置的值进行交换,++cur

2.cur找到比key大的值,++cur

说明:

1.prev要么紧跟着cur(prev的下一个就是cur)

2.要么跟cur中间间隔着比key大的一段区间

过程中就是,把key大的值往右翻,比key小的值翻到左边,当cur遍历完最后一个元素,那么prev的位置就是我们key的位置,找到key的位置,我们就可以以key为标准对该的左右区间进行划分,然后继续对左右区间执行我们以上操作。对于区间的划分以及递归,这里的操作与前面并没有特别大的差别,因此这里我们主要给大家介绍一下代码的实现细节。

void PartSort3(int* a, int left,int right)
{
	if (left >= right)
	{
		return;
	}
	int begin = left;
	int end = right;
	int keyi = left;
	int prve = left;
	int cur = prve + 1;
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++prve != cur)
		{
			swap(&a[cur], &a[prve]);
		}
		cur++;
	}
	swap(&a[prve], &a[keyi]);
	keyi = prve;
	PartSort3(a, begin, keyi - 1);
	PartSort3(a, keyi + 1, end);
}

代码详解:

排序(3)之交换排序_第7张图片

3.快速排序的优化

3.1 三数取中法

快速排序在最糟糕的情况下,可能会出现O(N^2)的时间复杂度的情况,就比如一下情况:

排序(3)之交换排序_第8张图片

我们这里可以发现在已经排好序的情况,我们在经历完整的一次排序时,首先需要遍历N,次,然后就是N-1……,最后1次,而这个过程中递归分了N层,所以该所有次数相加就等于1+2+……+N=(1+N)*N/2那么此处的量级也就是O(N^2)。

那么实际上我们理想中的快速排序是:

排序(3)之交换排序_第9张图片

 这里我们可以看到当我们类似于一棵二叉树的形式进行递归,我们的分层也就是logN层,对于没一层的排序我们是近似于N次循环,那么就让我们得时间复杂度达到了O(N*logN),那么这里需要实现的条件,也就是让我们的key值最好接近或者等于我们排序结束后的中间位置值。

那么我们如何解决此类问题呢,我们采取的优化方式就是三数取中。那么什么是三数取中算法呢?

这里指的是我们分别取,最左端序列left,最右端序列right,以及mid=(left+right)/2,这三位置的数进行大小比较,取中间大小的数放到我们的最左端做key,这样就可以在一定程度上避免O(N^2)的情况出现。

这里我们直接看代码:

int GetMid(int *a,int left, int right)
{
	int mini = (left + right) / 2;
	if (a[left] > a[mini])
	{
		if (a[mini] > a[right])
		{
			return mini;
		}
		else if (a[left] < a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
	else
	{
		if (a[mini] < a[right])
		{
			return mini;
		}
		else if (a[left] > a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
}

这里我们的代码逻辑如下:

排序(3)之交换排序_第10张图片

 那么我们以前后指针法,看一下优化的后代码:

// 快速排序前后指针法
void PartSort3(int* a, int left,int right)
{
	if (left >= right)
	{
		return;
	}
	int mini = GetMid(a, left, right);
	if (mini != left)
		swap(&a[left], &a[mini]);
	int begin = left;
	int end = right;
	int keyi = left;
	int prve = left;
	int cur = prve + 1;
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++prve != cur)
		{
			swap(&a[cur], &a[prve]);
		}
		cur++;
	}
	swap(&a[prve], &a[keyi]);
	keyi = prve;
	PartSort3(a, begin, keyi - 1);
	PartSort3(a, keyi + 1, end);
}

3.2 小区间优化

 这里我们为什么要做这样的优化呢,这里的原因自然是由于过深的递归有着一定的局限性,那么这里我们就来一起分析一下

递归太深存在以下几个问题:

1.效率。(影响不是很大)

2.深度太深,会导致栈溢出

那么为了避免递归太深,这里我们就可以使用小区间优化的方式:

小区间优化——小区间直接使用插入排序(当我们的递归到一个区间只有十几个元素时,我们还用递归来求解,那么该效率时远远小于插入排序的,而且递归越深,递归次数越多,所以我们就可以让快速排序的区间在10多一点时,使用插入排序)

这里我们直接看代码:

void PartSort3(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	if ((right - left + 1) > 10)
	{
		int mini = GetMid(a, left, right);
		if (mini != left)
			swap(&a[left], &a[mini]);
		int begin = left;
		int end = right;
		int keyi = left;
		int prve = left;
		int cur = prve + 1;
		while (cur <= right)
		{
			if (a[cur] < a[keyi] && ++prve != cur)
			{
				swap(&a[cur], &a[prve]);
			}
			cur++;
		}
		swap(&a[prve], &a[keyi]);
		keyi = prve;
		PartSort3(a, begin, keyi - 1);
		PartSort3(a, keyi + 1, end);
	}
     //直接插入排序
	else
	{
		InsertSort(a+left,(right-left+1));
	}
	
}

这样我们几乎就消灭了最底层的三到四层递归,而对于我们这里的二路递归每增加一层就是上一层两倍,所以我们消灭底层的三四层递归,也就消灭了大部分递归,也对此起到了优化作用。

4.快速排序的非递归实现

由于递归有以上的不足之处,所以我们也要学会将递归改为非递归,我们这里一起分析一下。我们在每次递归的实现过程中,我们改变的就是一个区间的范围,所以我们这里也就是要学会去控制我们区间的范围,然后按递归的顺序去用循环实现即可。

那么为了按递归的顺序利用循环去实现,我们这里就需要去用栈这个特殊的数据结构去实现,那么对于栈不熟悉的大家可以去看看小编有关栈的介绍。

那么这里的具体过程如下:

排序(3)之交换排序_第11张图片

我们发现,这样就可以合理控制区间,进行非递归实现,代码如下:

//单趟排序(前后指针法)
int PartSort(int *a,int left,int right)
{
	//三数取中
	int mini = GetMid(a, left, right);
	if (left != mini)
		Swap(&a[left], &a[mini]);
	//排序
	int cur = left+1;
	int prve = left;
	int keyi = left;
	while (cur<=right)
	{
		if (a[cur] < a[keyi] && ++prve != cur)
		{
			Swap(&a[prve], &a[cur]);
		}
		cur++;
	}
	Swap(&a[prve], &a[keyi]);
	return prve;
}
//非递归实现快速排序
void QuickSort(int *a,int left,int right)
{
	ST  s;
	STInity(&s);
	STpush(&s, right);
	STpush(&s, left);
	while (!STEmpty(&s))
	{
		int begin = STTop(&s);
		STPop(&s);
		int end = STTop(&s);
		STPop(&s);
		int keyi=PartSort(a, begin, end);
		if (keyi+1 < end)
		{
			STpush(&s,end);
			STpush(&s, keyi + 1);
		}
		if (begin < keyi - 1)
		{
			STpush(&s, keyi - 1);
			STpush(&s, begin);
		} 
	}

	STDestory(&s);
}

你可能感兴趣的:(排序算法,数据结构,算法)