排序之交换排序

news2024/12/23 12:30:57

文章目录

  • 前言
  • 一、冒泡排序
    • 1、冒泡排序基本思想
    • 2、冒泡排序的效率
  • 二、快速排序 -- hoare版本
    • 1、快速排序基本思想
    • 2、快速排序代码实现
    • 3、为什么最左边值做key时,右边先走
  • 三、快速排序 -- 挖坑法
    • 1、快速排序 -- 挖坑法基本思想
    • 2、快速排序 -- 挖坑法代码实现
    • 3、为什么最左边值做key时,右边先走
  • 四、快速排序 -- 前后指针法
    • 1、快速排序 -- 前后指针法基本思想
    • 2、快速排序 -- 前后指针法代码实现
  • 五、快速排序的优化
  • 1、快速排序的效率分析
    • 2、三数取中法优化快速排序
    • 3、小区间使用插入排序优化快速排序
  • 六、非递归实现快速排序


前言

所谓交换,是指根据序列中两个元素关键字的比较结果来对换这两个记录在序列中的位置。
交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。即将数组中两个元素进行比较,如果前者大于后者,就让两个元素交换位置。

一、冒泡排序

1、冒泡排序基本思想

冒泡排序是一种非常容易理解的排序,其基本思想就是:从前往后(从后往前)两两比较相邻元素的值,若为逆序(即 A[i-1] > A[i] ),则交换它们,直到序列比较完。我们称它为第一趟冒泡,结果是将最大的元素交换到待排序列的最后一个位置(或将最小的元素交换到待排序列的第一个位置),关键字最大的元素如石头一般下沉置水底(或关键字最小的元素如气泡一般逐渐往上"漂浮"直至"水面"。下一趟冒泡时,前一趟确定的最小元素不再参与比较,每趟冒泡的结果是把序列中的最大元素(或最小元素)放到了序列的最终位置…这样最多做n-1趟冒泡就能把所以元素排好序。
例如有如下一个数组。
在这里插入图片描述
进行第一次冒泡排序时,会将最大的元素向后调。
在这里插入图片描述
第二遍冒泡排序就将次大的元素向后调,每次冒泡排序都能确定一个最大值(最小值)的最终位置,随着元素的位置排好序,未排序的元素一趟冒泡排序时比较的次数也越来越少。

void BubbleSort(int* arr, int n)
{
	int i = 0;
	int j = 0;
	//冒泡排序每一趟都可以确定一个最大值(最小值),所以要排序n个元素,只需n-1次即可,因为最后一位不用排序。
	for (i = 0; i < n - 1; i++)
	{
		//设置一个标识,如果数据已经有序,则冒泡排序第一遍不会发生元素交换
		int exchange = 0;
		//每一趟冒泡排序都会确定一个值的位置,所以比较的次数也越来越少。
		for (j = 0; j < n - i - 1; j++)
		{
			if (arr[j] > arr[j + 1])
			{
				Swap(&arr[j], &arr[j + 1]);
				exchange = 1;
			}
		}
		//如果标识的值没有改变,说明元素已经有序,直接退出函数即可。
		if (exchange == 0)
		{
			break;
		}
	}
}

2、冒泡排序的效率

冒泡排序最优的情况是数据已经有序,此时冒泡排序时间复杂度为O(N),而插入排序在数据已经有序时也为O(N),但是当数据接近有序或者局部有序时,冒泡排序就不是最优了,而此时插入排序还是最优的情况,所以综合情况下插入排序的效率比冒泡排序高一些。
1.冒泡排序是一种非常容易理解的排序
2.时间复杂度:O(N^2)
3.空间复杂度:O(1)
4.稳定性:稳定

二、快速排序 – hoare版本

1、快速排序基本思想

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
例如有如下一个数组。
在这里插入图片描述
我们选出一个数组元素为key,然后记录其下标为keyi,一般选择最左边或者最右边的值。然后再设置两个变量left和right,用来记录数组最左边和最右边的下标。此时另keyi=left,即将key的值为数组最左边的值。
在这里插入图片描述

然后从右边right开始向前遍历数组,找到第一个小于key的值的下标后,然后从左边开始向后遍历数组,找到第一个大于key的值得下标,然后将这两个元素得值交换。

在这里插入图片描述
在这里插入图片描述
然后right继续向前遍历数组,left继续向后遍历数组,重复上面的操作。
在这里插入图片描述
在这里插入图片描述
直到left和right相遇,此时left指的是比key小的值,所以此时将left和keyi位置的元素交换。然后将keyi=left,则此时keyi下标的左边都是小于key值的元素,keyi下标右边都是大于key值的元素。
在这里插入图片描述
在这里插入图片描述
此时就完成了一趟快速排序,如果我们记录数组开头和结尾下标为begin和end,此时我们就可以递归将数组中[begin,keyi-1]和[keyi+1,end]区域内的数据进行上述的操作,这样一直递归下去,就会使数组变为有序。
在这里插入图片描述

2、快速排序代码实现

我们可以先写一趟快速排序的代码。keyi用来记录key的下标。

void QuickSort(int* arr, int n)
{
	//left记录数组元素开始下标
	int left = 0;
	//right记录数组元素最后的下标
	int right = n - 1;
	//令key的值为数组最左边的值。用keyi来记录key的下标
	int keyi = left;
	while (left < right)
	{
		//先让右边先走,找第一个小于key的元素
		while (arr[right] > arr[keyi])
		{
			--right;
		}
		//再让左边走,找第一个大于key的元素
		while (arr[left] < arr[keyi])
		{
			++left;
		}
		//然后让这两个元素交换,
		Swap(&arr[left], &arr[right]);
	}
	//此时left下标的元素的值还小于key,此时将keyi和left位置的值交换,令keyi为left
	//则此时key的下标为keyi,而且此时keyi左边的元素都小于key,keyi右边的元素都大于key。
	Swap(&arr[keyi], &arr[left]);
	keyi = left;
}

当我们分析时,这个代码当遇到下列这种情况时,会一直陷入死循环,因为arr[right]此时等于key的值,arr[left]此时等于key的值,都会跳出循环,然后将left和right位置的值交换后,还是arr[right]等于key的值,arr[left]等于key的值,所以还会跳出循环,交换left和right位置的值。所以此种情况就会死循环交换left和right的值。
在这里插入图片描述
上述情况我们只需在循环条件中加上=即可,即变为while(arr[right]>=key)和while(arr[left]<=key),这样如果遇到arr[right]与key相等时,也会直接跳过而继续向左走,直到遇到arr[right]<key时才停下。

void QuickSort(int* arr, int n)
{
	//left记录数组元素开始下标
	int left = 0;
	//right记录数组元素最后的下标
	int right = n - 1;
	//另key的值为数组最左边的值。
	int keyi = left;
	while (left < right)
	{
		//先让右边先走,找第一个小于key的元素
		while (arr[right] >= arr[keyi])
		{
			--right;
		}
		//再让左边走,找第一个大于key的元素
		while (arr[left] <= arr[keyi])
		{
			++left;
		}
		//然后让这两个元素交换,
		Swap(&arr[left], &arr[right]);
	}
	//此时left下标的元素的值还小于key,此时将keyi和left位置的值交换,令keyi为left
	//则此时key的下标为keyi,而且此时keyi左边的元素都小于key,keyi右边的元素都大于key。
	Swap(&arr[keyi], &arr[left]);
	keyi = left;
}

如果在遇到边界情况时,上述的代码也会出现问题,例如如下情况,此时在第一次while(arr[right]>=arr[keyi])时,right会一直减减,直到减为-1,而此时arr[-1]已经越界访问,会出现错误。所以在while判断中还需要加上left<right,以防止第一次while循环就将right减减越界或者将left加加越界。
在这里插入图片描述

void QuickSort(int* arr, int n)
{
	//left记录数组元素开始下标
	int left = 0;
	//right记录数组元素最后的下标
	int right = n - 1;
	//另key的值为数组最左边的值。
	int keyi = left;
	while (left < right)
	{
		//先让右边先走,找第一个小于key的元素
		while (left<right && arr[right] >= arr[keyi])
		{
			--right;
		}
		//再让左边走,找第一个大于key的元素
		while (left<right && arr[left] <= arr[keyi])
		{
			++left;
		}
		//然后让这两个元素交换,
		Swap(&arr[left], &arr[right]);
	}
	//此时left下标的元素的值还小于key,此时将keyi和left位置的值交换,令keyi为left
	//则此时key的下标为keyi,而且此时keyi左边的元素都小于key,keyi右边的元素都大于key。
	Swap(&arr[keyi], &arr[left]);
	keyi = left;
}

这样上述的代码就完成了一次快速排序。当完成一次快速排序后,此时下标为keyi的位置的左边的元素都小于key,keyi右边位置的元素都大于key。我们在此时递归调用上述操作,将keyi左边的元素也变为有序,然后将keyi右边的元素一步一步变为有序。

void QuickSort(int* arr, int begin,int end)
{
	//区间不存在,或者只有一个值时则不需要处理
	if (begin >= end)
	{
		return;
	}
	//left记录数组元素开始下标
	int left = begin;
	//right记录数组元素最后的下标
	int right = end;
	//另key的值为数组最左边的值。
	int keyi = left;
	while (left < right)
	{
		//先让右边先走,找第一个小于key的元素
		while (left<right && arr[right] >= arr[keyi])
		{
			--right;
		}
		//再让左边走,找第一个大于key的元素
		while (left<right && arr[left] <= arr[keyi])
		{
			++left;
		}
		//然后让这两个元素交换,
		Swap(&arr[left], &arr[right]);
	}
	//此时left下标的元素的值还小于key,此时将keyi和left位置的值交换,令keyi为left
	//则此时key的下标为keyi,而且此时keyi左边的元素都小于key,keyi右边的元素都大于key。
	Swap(&arr[keyi], &arr[left]);
	keyi = left;

	//递归将keyi左边和右边的元素进行相同操作
	QuickSort(arr, begin, keyi - 1);
	QuickSort(arr, keyi + 1, end);
}

3、为什么最左边值做key时,右边先走

为什么最左边值做key时,要让右边先走呢?
因为要保证相遇位置的值,比key要小或者就是key的值,然后此时执行Swap(&arr[keyi], &arr[left]); keyi = left;此时key值得下标为keyi,这样才满足keyi位置左边元素都小于key,keyi位置右边元素都大于key。
第一种情况:right先走,当right停下来时,left去遇到right,相遇位置就是right停下来的位置,而right停下来的位置就是比key要小的位置。
在这里插入图片描述
此时将left位置的值和keyi位置的值交换,然后让keyi=left,此时key值的下标为keyi,这样就满足keyi位置左边元素都小于key,keyi位置右边元素都大于key。
在这里插入图片描述
第二种情况:right先走,right没有找到比key要小的值,right去遇到了left,而此时的相遇位置是left上一轮停下来的位置,或者就是key的位置。此时满足keyi位置的右边都是大于key的元素。而keyi左边没有元素。
在这里插入图片描述

三、快速排序 – 挖坑法

1、快速排序 – 挖坑法基本思想

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,在学习了Hoare提出的快速排序后,有人对快速排序的实现进行了一些小的修改。
例如有如下一个数组。
在这里插入图片描述

先将第一个数据存放在临时变量key中,形成一个坑位,创建一个piti用来记录坑位的下标。然后将创建两个变量left和right记录数组首尾元素下标,分别从数组首尾开始遍历数组。
在这里插入图片描述

先从right开始向前遍历数组,如果数组中下标为right的元素的值小于key的话,就将该元素放到现在的坑位中,即将arr[piti] = arr[right],因为数组第一个数据已经存在key之中了,所以将数组第一个元素的数据覆盖也没有事,因为key之中已经保存了。然后将piti = right,即将right位置作为新的坑位。
在这里插入图片描述
然后再从left开始向后遍历数组,如果数组中下标为left的元素的值大于key的话,就将该元素放到现在的坑位中,即将arr[piti] = arr[left],因为上一步骤已经将arr[right]的值放到上一个坑位了,所以现在将arr[left]覆盖现在的坑位数据也不会造成arr[right]元素的丢失。然后将piti = left,即将现在left的位置作为新的坑位。
在这里插入图片描述
然后再将right减减,继续向前遍历数组,当遇到下标为right的元素的值小于key时,再次重复上面的操作,即将 arr[piti] = arr[right],然后将piti = right,即将现在right的位置作为新的坑位。

在这里插入图片描述
然后将left加加,继续向后遍历数组,当遇到下标为left的元素的值大于key时,再次重复上面的操作,即将
arr[piti] = arr[right],然后将piti = left,即将现在的left位置作为新的坑位。
在这里插入图片描述
然后再将right减减,继续向后遍历数组,当遇到下标为right的元素的值小于key时,再次重复上面的操作,即将 arr[piti] = arr[right],然后将piti = right,即将现在right的位置作为新的坑位。
在这里插入图片描述
此时再将left加加,然后left和right相遇。然后将现在的坑位放入key的值,即arr[piti] = key,此时可以看到piti位置左边的元素都小于key,piti位置右边的元素都大于key。此时挖坑法的一趟快速排序就完成了,然后接下来就是递归将piti左边的元素和右边的元素进行上述操作。最后就会将数组变为一个升序数组。
在这里插入图片描述

2、快速排序 – 挖坑法代码实现

因为上面Hoare法代码实现时,我们已经分析了while()中判断条件为什么要写为((left<right) && (arr[right]>=key)),并且挖坑法实现时也会遇到与Hoare法实现时相同的情况,所以我们需要接着按照while ((left<right) && (arr[right]>=key))这样写。

//快速排序 -- 挖坑法
int PartSort2(int* arr, int begin, int end)
{
	int left = begin;
	int right = end;
	int key = arr[begin];
	int piti = begin;
	while (left < right)
	{
		//右边开始找小于key的值
		while ((left<right) && (arr[right]>=key))
		{
			--right;
		}
		//找到了就将小的值放入坑位中,然后将right变为新的坑位
		arr[piti] = arr[right];
		piti = right;
		//左边开始找大于key的值
		while ((left<right) && (arr[left]<=key))
		{
			++left;
		}
		//如果找到了就将大的值放入坑位中,然后将left变为新的坑位
		arr[piti] = arr[left];
		piti = left;
	}
	//如果left和right相遇,就将坑位中放入key,此时就满足了piti位置左边的元素都小于key的值,piti位置右边的元素都大于key的值
	arr[piti] = key;
	return piti;
}

void QuickSort(int* arr, int begin,int end)
{
	//区间不存在,或者只有一个值时则不需要处理
	if (begin >= end)
	{
		return;
	}
	
	//获得返回的keyi值
	int keyi = PartSort2(arr, begin, end);
	//递归将keyi左边和右边的元素进行相同操作
	QuickSort(arr, begin, keyi - 1);
	QuickSort(arr, keyi + 1, end);
}

3、为什么最左边值做key时,右边先走

我们在实现Hoare法时已经分析过了,当最左边值做key时,右边先走保证了left和right相遇位置的值,比key要小或者就是key的值,然后此时将arr[piti] = tmp,此时key值得下标为piti,这样才满足piti位置左边元素都小于key,keyi位置右边元素都大于key。详细分析同Hoare法实现时分析类似。

四、快速排序 – 前后指针法

1、快速排序 – 前后指针法基本思想

快速排序还可以使用前后指针法来实现。
例如有如下一个数组。
在这里插入图片描述
先将数组最左边的值作为key,并且使用keyi来记录key的下标,然后创建指针指向数组开头,并且创建curr指针指向prev指针的后一个位置。
在这里插入图片描述
然后判断curr指针指向的数据是否小于key,如果小于,则使prev指针向后移一位,并且使curr指向的内容与prev指向的内容交换,然后curr指针++。而当prev向后移一位时,如果prev和curr指向同一个元素,则curr指向的内容与prev指向的内容交换就没有意义了,所以我们需要加一个判断,即(++prev) != (curr)。这样就不会使curr与prev指向同一元素时还交换curr指向的内容与prev指向的内容。
在这里插入图片描述
此时curr++后指向的元素还是小于key,则prev也++指向下一个元素,但是此时prev和curr指向的元素还是一个元素,所以还是不会发生元素交换。继续将curr++。
在这里插入图片描述
此时curr指向的元素小于key,先将prev++,然后判断发现prev和curr不是指向同一元素,所以交换prev和curr指向元素的内容。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
然后将curr继续++,向后遍历数组,直到遇到curr指向的元素比key小。而此时curr指向的元素的值又比key小了,所以此时要重复上述操作,即先让prev++指向后一个元素,然后经过判断prev和curr没有指向同一元素,所以交换prev指向元素和curr指向元素的内容。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
然后将curr继续++,向后遍历数组,直到遇到curr指向的元素比key小。而此时curr指向的元素的值又比key小了,所以此时要重复上述操作,即先让prev++指向后一个元素,然后经过判断prev和curr没有指向同一元素,所以交换prev指向元素和curr指向元素的内容。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
然后继续将curr++,向后遍历数组。此时curr指向数组的最后一个元素。
在这里插入图片描述
当curr指向数组最后一个元素时,说明数组已经遍历一遍,此时将prev指向的内容和keyi指向的内容交换。
在这里插入图片描述
在这里插入图片描述
然后再将keyi=prev,则此时keyi位置左边的元素都小于key,keyi位置右边的元素都大于key。到此为止就完成了一趟前后指针法快速排序,接下来就可以将keyi左边的元素和右边的元素递归进行上述操作,直到将数组变为有序。
在这里插入图片描述

2、快速排序 – 前后指针法代码实现

//快速排序法 -- 前后指针法
int  PartSort3(int* arr, int begin, int end)
{
	//使数组最左边的元素作为key
	int keyi = begin;
	//用prev记录数组首位置,curr记录prev下一个元素位置
	int prev = begin;
	int curr = prev + 1;
	//如果curr没有遍历到数组末尾就继续向后遍历
	while (curr <= end)
	{
		/*
		//如果curr此时指向的元素小于key
		if (arr[curr] < arr[keyi])
		{
			//先让prev向后移一位
			prev++;
			//然后判断prev是否和curr指向同一个元素,如不指向同一个元素,就让prev指向的内容和curr指向的内容交换
			if (prev != curr)
			{
				Swap(&arr[prev], &arr[curr]);
			}
		}
		*/

		//上面的代码判断可以简化为下面的
		//如果curr指向的元素小于key,并且++prev后不等于curr,就让prev和curr指向的内容交换
		if ((arr[curr] < arr[keyi]) && (++prev != curr))
		{
			Swap(&arr[prev], &arr[curr]);
		}
		//curr一直继续向后遍历数组
		curr++;
	}
	//最后将keyi指向的key和prev指向的内容交换,此时prev指向的就是key
	Swap(&arr[keyi], &arr[prev]);
	//然后将keyi指向key,即此时keyi指向的元素就是key
	keyi = prev;
	//将此时key的位置返回
	return keyi;
}

void QuickSort(int* arr, int begin,int end)
{
	//区间不存在,或者只有一个值时则不需要处理
	if (begin >= end)
	{
		return;
	}
	//Hoare法
	//int keyi = PartSort1(arr, begin, end);   

	//挖坑法
	//int keyi = PartSort2(arr, begin, end);

	//前后指针法
	int keyi = PartSort3(arr, begin, end); 
	//递归将keyi左边和右边的元素进行相同操作
	QuickSort(arr, begin, keyi - 1);
	QuickSort(arr, keyi + 1, end);
}

五、快速排序的优化

1、快速排序的效率分析

经过上面的分析,我们可以知道快速排序执行的效率和key值的选择有关,当快速排序的key值选取的为数据中的中间值时,这时快速排序的效率最高。此时函数递归的情况如图所示。
在这里插入图片描述
而当key值选取的为数据中最小值或最大值时,这时快速排序的效率最低。此时函数递归的情况如图所示。即当数据为有序或接近有序时,快速排序的效率最低,复杂度可以达到O(N^2)。
在这里插入图片描述

2、三数取中法优化快速排序

三数取中法就是取begin的值,end的值,还有mid = (begin + end) / 2的值,然后比较三个值,选取中间值作为快速排序的key值,这样key值就有很大概率不是数据中的最大值和最小值。

//三数取中法
int GetMidIndex(int* arr, int begin, int end)
{
	int mid = (begin + end) / 2;
	if (arr[begin] < arr[mid])
	{
		if (arr[mid] < arr[end])
		{
			return mid;
		}
		else if (arr[begin] < arr[end])
		{
			return end;
		}
		else
		{
			return begin;
		}
	}
	//a[begin] > a[mid]
	else
	{
		if (arr[mid] > arr[end])
		{
			return mid;
		}
		else if (arr[begin] < arr[end])
		{
			return begin;
		}
		else
		{
			return end;
		}
	}
}


//快速排序法 -- 前后指针法
int  PartSort3(int* arr, int begin, int end)
{
	//使数组最左边的元素作为key
	int keyi = begin;
	//用prev记录数组首位置,curr记录prev下一个元素位置
	int prev = begin;
	int curr = prev + 1;

	//利用三数选中法得到的key值为最大值最小值的概率更小
	int midi = GetMidIndex(arr, begin, end);
	//然后将此时keyi中的值与三数选中法的到的值进行交换,则此时keyi中的值就是三数选中法选出来的值。
	Swap(&arr[keyi], &arr[midi]);
	//如果curr没有遍历到数组末尾就继续向后遍历
	while (curr <= end)
	{
		/*
		//如果curr此时指向的元素小于key
		if (arr[curr] < arr[keyi])
		{
			//先让prev向后移一位
			prev++;
			//然后判断prev是否和curr指向同一个元素,如不指向同一个元素,就让prev指向的内容和curr指向的内容交换
			if (prev != curr)
			{
				Swap(&arr[prev], &arr[curr]);
			}
		}
		*/

		//上面的代码判断可以简化为下面的
		//如果curr指向的元素小于key,并且++prev后不等于curr,就让prev和curr指向的内容交换
		if ((arr[curr] < arr[keyi]) && (++prev != curr))
		{
			Swap(&arr[prev], &arr[curr]);
		}
		//curr一直继续向后遍历数组
		curr++;
	}
	//最后将keyi指向的key和prev指向的内容交换,此时prev指向的就是key
	Swap(&arr[keyi], &arr[prev]);
	//然后将keyi指向key,即此时keyi指向的元素就是key
	keyi = prev;
	//将此时key的位置返回
	return keyi;
}

void QuickSort(int* arr, int begin,int end)
{
	//区间不存在,或者只有一个值时则不需要处理
	if (begin >= end)
	{
		return;
	}
	//Hoare法
	//int keyi = PartSort1(arr, begin, end);   

	//挖坑法
	//int keyi = PartSort2(arr, begin, end);

	//前后指针法
	int keyi = PartSort3(arr, begin, end); 
	//递归将keyi左边和右边的元素进行相同操作
	QuickSort(arr, begin, keyi - 1);
	QuickSort(arr, keyi + 1, end);
}

3、小区间使用插入排序优化快速排序

如图为快速排序递归调用函数图,当递归划分生成小区间,区间比较小的时候,就不再递归划分去排序这个小区间,因为每次递归就之排序一两个数,但是需要调用一次函数,排序这两个数的开销太大了,此时可以考虑使用其他的排序来对小区间的数据进行处理。下面我们假设当区间小于10时,就不再使用递归排序小区间,而使用插入排序来排序小区间。

void QuickSort(int* arr, int begin,int end)
{
	//区间不存在,或者只有一个值时则不需要处理
	if (begin >= end)
	{
		return;
	}

	if ((end-begin) > 10)
	{
		//Hoare法
		//int keyi = PartSort1(arr, begin, end);   

		//挖坑法
		//int keyi = PartSort2(arr, begin, end);

		//前后指针法
		int keyi = PartSort3(arr, begin, end);
		//递归将keyi左边和右边的元素进行相同操作
		QuickSort(arr, begin, keyi - 1);
		QuickSort(arr, keyi + 1, end);
	}
	else
	{
		//当区间的元素小于等于10时,就直接使用插入排序来进行这个区间的排序
		//插入排序接收的是数组中要排序的起始位置,然后还有要排序数据的长度
		//所以第一个参数要为arr + begin,而第二个参数为要排序的数据长度
		InsertSort(arr + begin, end - begin + 1);
	}
	
}

六、非递归实现快速排序

用递归解决问题有时候会很巧妙,但是递归最大的问题就是当递归调用的函数次数太多时,即递归深度太深时,就会出现栈溢出现象,所以当遇到问题时,我们不但要能用递归方法解决,还需要知道怎么将递归改为非递归。
递归改非递归可以考虑使用下面的两种办法:
(1).直接改为循环 – 比如斐波那契数列、归并排序。
(2).用数据结构栈模拟递归过程。
下面我们就用数据结构模拟递归过程来将上面写的递归实现快速排序改为非递归实现快速排序。
在这里插入图片描述
然后求出此时key值的位置,此时将先将right和keyi+1入栈,再让keyi-1和left入栈。这样出栈时顺序就为left、keyi-1、keyi+1、right,即先处理区间[left,keyi-1],再处理区间[keyi+1,right]。
在这里插入图片描述

然后会将left当作left,keyi-1当作right,求出新的keyi1,然后就会先将right1和keyi1+1入栈,再将keyi1-1和left1入栈,然后下一次循环又会将left1作为left,keyi1-1作为right,求出新的keyi2,就像递归调用函数一样,先将左边的元素排好序,然后再排序右边元素。
在这里插入图片描述

void QuickSortNonR(int* arr, int begin, int end)
{
	//创建一个栈
	ST st;
	StackInit(&st);
	//将end和begin进栈
	StackPush(&st,end);
	StackPush(&st, begin);
	while (!StackEmpty(&st))
	{
		//将栈顶元素出栈
		int left = StackTop(&st);
		StackPop(&st);
		//将栈顶元素出栈
		int right = StackTop(&st);
		StackPop(&st);
		int keyi = PartSort3(arr, left, right);
		if (right > (keyi + 1))
		{
			StackPush(&st, right);
			StackPush(&st, keyi + 1);

		}
		if (left < (keyi-1))
		{
			StackPush(&st, keyi - 1);
			StackPush(&st, left);
		}
	}

	StackDestory(&st);
}

当我们实现用栈模拟递归的实现后,我们会发现其实使用队列也可以实现模拟递归。使用栈模拟的话顺序类似于二叉树中的先序遍历,而当使用队列时,则顺序类似于层序遍历。
在这里插入图片描述

//使用队列模拟递归
void QuickSortNonRQueue(int* arr, int begin, int end)
{
	//创建一个队列
	Queue qt;
	QueueInit(&qt);
	QueuePush(&qt, begin);
	QueuePush(&qt, end);
	while (!QueueEmpty(&qt))
	{
		int left = QueueFront(&qt);
		QueuePop(&qt);
		int right = QueueFront(&qt);
		QueuePop(&qt);

		int keyi = PartSort3(arr, left, right);
		if (left < (keyi-1))
		{
			QueuePush(&qt, left);
			QueuePush(&qt, keyi - 1);
		}
		if (right > (keyi + 1))
		{
			QueuePush(&qt, keyi + 1);
			QueuePush(&qt, right);
		}
	}
	QueueDestroy(&qt);
}


void QuickSortNonR(int* arr, int begin, int end)
{
	//使用栈模拟递归
	//QuickSortNonRStack(arr, begin, end);

	//使用队列模拟递归
	QuickSortNonRQueue(arr, begin, end);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/964551.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

stable diffusion实践操作-随机种子seed

系列文章目录 stable diffusion实践操作 文章目录 系列文章目录前言一、seed是什么&#xff1f;二、使用步骤1.多批次随机生成多张图片2.提取图片seed3. 根据seed 再次培养4 seed使用4.1 复原别人图4.1 轻微修改 三、差异随机种子1. webUI位置2. 什么是差异随机种子3.使用差异…

找redis大key工具rdb_bigkeys

github官网 https://github.com/weiyanwei412/rdb_bigkeys 在centos下安装go [roothadoop102 rdb_bigkeys-master]# wget https://dl.google.com/go/go1.13.5.linux-amd64.tar.gz [roothadoop102 rdb_bigkeys-master]# tar -zxf go1.13.5.linux-amd64.tar.gz -C /usr/local将g…

安装bpftrace和bcc的踩坑记录

最后在Ubuntu22.04使用Ubuntu提供的安装命令完成了安装。这里是记录尝试在Ubuntu18.04和Ubuntu22.04使用源码安装未果的过程。 文章目录 22版本安装bcc准备工具安装命令使用报错&#xff1a;iovisor封装的安装方式ubuntu的安装方式 For Bionic (18.04 LTS)官方提供的源码安装准…

SpringCloudGateway集成SpringDoc

SpringCloudGateway集成SpringDoc 最近在搞Spring版本升级&#xff0c;按客户要求升级Spring版本&#xff0c;原来用着SpringBoot 2.2.X版本&#xff0c;只需要升级SpringBoot 2.X最新版本也就可以满足客户Spring版本安全要求&#xff0c;可是好像最新的SpringBoot 2.X貌似也不…

Laravel chunk和chunkById的坑

在编写定时任务脚本的时候&#xff0c;经常会用到chunk和chunkById的API。 一、前言 数据库引擎为innodb。 表结构简述&#xff0c;只列出了本文用到的字段。 字段类型注释idint(11)IDtypeint(11)类型mark_timeint(10)标注时间&#xff08;时间戳&#xff09; 索引&#x…

手撕 视觉slam14讲 ch13 代码(1)工程框架与代码结构

在学习slam一年之后开始&#xff0c;开始自己理思路&#xff0c;全手敲完成ch13的整个代码 我们按照自己写系统的思路进行&#xff0c;首先确定好SLAM整体系统的流程&#xff0c;见下图&#xff0c;输入为双目图像&#xff0c;之后进入前端位姿估计和后端优化&#xff0c;中间…

滑动窗口实例3(最大连续1的个数Ⅲ)

题目&#xff1a; 给定一个二进制数组 nums 和一个整数 k&#xff0c;如果可以翻转最多 k 个 0 &#xff0c;则返回 数组中连续 1 的最大个数 。 示例 1&#xff1a; 输入&#xff1a;nums [1,1,1,0,0,0,1,1,1,1,0], K 2 输出&#xff1a;6 解释&#xff1a;[1,1,1,0,0,1,1…

stable diffusion实践操作-宽高设置以及高清修复

系列文章目录 stable diffusion实践操作 文章目录 系列文章目录前言一、SD宽高怎么设置&#xff1f;1.1 宽高历史 二、高清修复总结 前言 主要介绍SD的宽高设置以及高清修复 一、SD宽高怎么设置&#xff1f; 1.1 宽高历史 SD生成256256图片效果最好。512512是SD一直使用的画…

【管理运筹学】第 7 章 | 图与网络分析(1,图论背景以及基本概念、术语)

文章目录 引言一、图与网络的基本知识1.1 图与网络的基本概念1.1.1 图的定义1.1.2 图中相关术语1.1.3 一些特殊图类1.1.4 图的运算 写在最后 引言 按照正常进度应该学习动态规划了&#xff0c;但我想换换口味&#xff0c;而且动态规划听说也有一定难度&#xff0c;还不一定会考…

设计模式—简单工厂

目录 一、前言 二、简单工厂模式 1、计算器例子 2、优化后版本 3、结合面向对象进行优化&#xff08;封装&#xff09; 3.1、Operation运算类 3.2、客户端 4、利用面向对象三大特性&#xff08;继承和多态&#xff09; 4.1、Operation类 4.2、加法类 4.3、减法类 4…

VTK——使用ICP算法进行模型配准

ICP算法 迭代最近点&#xff08;Iterative Closest Point&#xff0c;ICP&#xff09;算法是一种用于两个三维形状之间几何对齐&#xff08;也叫做配准&#xff09;的计算方法。通常&#xff0c;这两个形状至少有一个是点云数据。ICP算法用于最小化源点云与目标点云之间点到点…

【设计模式】Head First 设计模式——构建器模式 C++实现

设计模式最大的作用就是在变化和稳定中间寻找隔离点&#xff0c;然后分离它们&#xff0c;从而管理变化。将变化像小兔子一样关到笼子里&#xff0c;让它在笼子里随便跳&#xff0c;而不至于跳出来把你整个房间给污染掉。 设计思想 ​ 将一个复杂对象的构建与其表示相分离&…

【两个有序数组合并】

问题描述: 给定两个有序整数数组 A 和 B&#xff0c;将B合并到A中&#xff0c;使得 A 成为一个有序数组。 说明: 初始化 A 和 B 的元素数量分别为 m 和 n。A有足够的空间&#xff08;空间大小大于或等于 m n&#xff09;来保存 B 中的元素。默认升序。 输入输出描述&#xf…

大数据组件-Flume集群环境的启动与验证

&#x1f947;&#x1f947;【大数据学习记录篇】-持续更新中~&#x1f947;&#x1f947; 个人主页&#xff1a;beixi 本文章收录于专栏&#xff08;点击传送&#xff09;&#xff1a;【大数据学习】 &#x1f493;&#x1f493;持续更新中&#xff0c;感谢各位前辈朋友们支持…

《YOLOv5:从入门到实战》专栏介绍 专栏目录

&#x1f31f;YOLOv5&#xff1a;从入门到实战 | 目录 | 使用教程&#x1f31f; 本专栏涵盖了丰富的YOLOv5算法从入门到实战系列教程&#xff0c;专为学习YOLOv5的同学而设计&#xff0c;堪称全网最详细的教程&#xff01;该专栏从YOLOv5基础知识入门到项目应用实战都提供了详细…

JavaWeb_LeadNews_Day10-Xxljob, Redis实现定时热文章

JavaWeb_LeadNews_Day10-Xxljob, Redis实现定时热文章 xxl-job概述windows部署调度中心docker部署调度中心 xxl-job入门案例xxl-job分片广播热点文章定时计算思路分析具体实现热文章计算定时计算 查询文章接口改造来源Gitee xxl-job概述 windows部署调度中心 运行 xxl-job\do…

【数据结构】队列---C语言版(详解!!!)

文章目录 &#x1f438;一、队列的概念及结构&#x1f344;1、队列的概念定义&#x1f344;2、动图演示 &#x1f438;二、队列的实现&#x1f438;三、链表结构队列详解&#x1f34e;创建队列的结构⭕接口1&#xff1a;定义结构体&#xff08;QNode、Queue&#xff09;⭕接口2…

LeetCode 23 合并 K 个升序链表

LeetCode 23 合并 K 个升序链表 来源&#xff1a;力扣&#xff08;LeetCode&#xff09; 链接&#xff1a;https://leetcode.cn/problems/merge-k-sorted-lists/description/ 博主Github&#xff1a;https://github.com/GDUT-Rp/LeetCode 题目&#xff1a; 给你一个链表数组…

中心差分法-学习笔记《结构动力学-陈政清》

激励分段解析法仅仅对外载荷进行了离散&#xff0c;但对运动方程还是严格满足的&#xff0c;体系的运动在时间轴上依然是满足运动微分方程。然而&#xff0c;一般的时域逐步积分法进一步放松要求&#xff0c;不仅仅对外荷载进行离散化处理&#xff0c;也对体系的运动进行离散化…

前端Vue仿企查查天眼查高管信息列表组件

随着技术的不断发展&#xff0c;传统的开发方式使得系统的复杂度越来越高。在传统开发过程中&#xff0c;一个小小的改动或者一个小功能的增加可能会导致整体逻辑的修改&#xff0c;造成牵一发而动全身的情况。为了解决这个问题&#xff0c;我们采用了组件化的开发模式。通过组…