排序之交换排序

文章目录

  • 前言
  • 一、冒泡排序
    • 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.mfbz.cn/a/100749.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

postgis中将数据库备份到其它数据库中还原

1、备份数据库 可以用命令操作 pg_dump -U postgres -h hostip -d joint-boot -Fc > "D:\\python\\Project\\PG\\data\\joint.jar"2、创建新的数据库 可以在其它postgis数据库中创建 3、还原数据库 可以用命令操作 pg_restore -U postgres -h hostip -d C -F…

无涯教程-Android - DatePicker函数

Android Date Picker允许您在自定义用户界面中选择由日,月和年组成的日期。为此功能,android提供了DatePicker和DatePickerDialog组件。 在本教程中,我们将通过DatePickerDialog演示日期选择器的用法, DatePickerDialog是一个包含DatePicker的简单对话框。 为了显示DatePicker…

【zookeeper】zookeeper介绍

分布式协调技术 在学习ZooKeeper之前需要先了解一种技术——分布式协调技术。那么什么是分布式协调技术&#xff1f;其实分布式协调技术主要用来解决分布式环境当中多个进程之间的同步控制&#xff0c;让他们有序的去访问某种临界资源&#xff0c;防止造成"脏数据"的…

vue 根据数值判断颜色

1.首先style样式给两种颜色 用:class 三元运算符判断出一种颜色 第一步&#xff1a;在style里边设置两种颜色 .green{color: green; } .orange{color: orangered; }在取数据的标签 里边 判断一种颜色 :class"item.quote.current >0 ?orange: green"<van-gri…

CSS 实现平面圆点绕椭圆动画

前言 &#x1f44f;CSS实现平面圆点绕椭圆动画,速速来Get吧~ &#x1f947;文末分享源代码。记得点赞关注收藏&#xff01; 1.实现效果 2.实现原理 transform-style&#xff1a;CSS 属性 transform-style 设置元素的子元素是位于 3D 空间中还是平面中。如果选择平面&#xf…

QTday3(对话框、发布软件、事件处理核心机制)

一、Xmind整理&#xff1a; 二、上课笔记整理&#xff1a; 1.消息对话框&#xff08;QMessageBox&#xff09; ①基于属性版本的API QMessageBox::QMessageBox( //有参构造函数名QMessageBox::Icon icon, //图标const Q…

短信验证码服务

使用的是 阿里云 阿里云官网 1.找到 左上角侧边栏 -云通信 -短信服务 2.在快速学习测试处 &#xff0c;按照步骤完成快速学习&#xff0c;绑定要测试的手机号&#xff0c;选专用 【测试模板】&#xff0c;自定义模板需要人工审核&#xff0c;要一个工作日 3.右上角 获取 Acces…

常见网络通信协议(http、https、ws)及安全协议(SSL、TLS、XTLS)

文章内容删除了一大半不合适的内容&#xff0c;发不出来&#xff0c;你懂得。&#x1f970; 一、常见网络通信协议1.1、HTTP 协议1.11 HTTP 协议简介1.12 HTTP 协议的工作流程1.13 HTTP 协议的常用方法1.14 HTTP 协议的常见状态码1.15 HTTP 的缺点 1.2 HTTPS 协议1.21 HTTPS 协…

C# 试图加载格式不正确的程序。 (异常来自 HRESULT:0x8007000B)

C# 在调用Cdll时&#xff0c;可能会出现 &#xff1a;试图加载格式不正确的程序。 (异常来自 HRESULT:0x8007000B)这个错误。 一般情况下是C#目标平台跟Cdll不兼容&#xff0c;64位跟32位兼容性问题&#xff0c; a.客户端调用Cdll报的错则&#xff0c; 1)允许的话把C#客户端…

怎样免费在公司访问家中的树莓派

最近拿起了大学时买的树莓派&#xff0c;刚好看到了一篇文章写到无公网IP&#xff0c;从公网SSH远程访问家中的树莓派 便来试试&#xff1a; 我的树莓派之前装过ssh&#xff0c;所以插上电就能用了。其实过程很简单&#xff0c;只需要在树莓派中下载一个cpolar即可。 curl -…

解密Spring事务生效的内部机制

声明式事务和编程式事务对比&#xff1a; 声明式事务&#xff1a; 使用注解或XML配置的方式&#xff0c;在代码中声明事务的属性和行为。通过AOP和代理模式实现&#xff0c;将事务管理与业务逻辑代码解耦。适用于大多数情况&#xff0c;简化了代码&#xff0c;提高了可维护性和…

Python分享之redis(2)

Hash 操作 redis中的Hash 在内存中类似于一个name对应一个dic来存储 hset(name, key, value) #name对应的hash中设置一个键值对&#xff08;不存在&#xff0c;则创建&#xff0c;否则&#xff0c;修改&#xff09; r.hset("dic_name","a1","aa&quo…

学用 CountDownLatch 与 CyclicBarrier

开篇即说结论&#xff0c;如果搞不清楚两者区别&#xff0c;那就无脑用 CountDownLatch&#xff0c;问题也不大&#xff08;因为我也不是太懂&#xff09;。 CountDownLatch 模拟了100米赛跑&#xff0c;10名选手已经准备就绪&#xff0c;只等裁判一声令下。当所有人都到达终…

8月28日上课内容 第四章 MySQL备份与恢复

本章结构 前言&#xff1a;日志⭐⭐ MySQL 的日志默认保存位置为 /usr/local/mysql/data ##配置文件 vim /etc/my.cnf [mysqld] ##错误日志&#xff0c;用来记录当MySQL启动、停止或运行时发生的错误信息&#xff0c;默认已开启 log-error/usr/local/mysql/data/mysql_error.l…

Docker 容器学习笔记

Docker 容器学习笔记 容器的由来 早先&#xff0c;虚拟机通过操作系统实现相互隔离&#xff0c;保证应用程序在运行时相互独立&#xff0c;避免相互干扰。但是操作系统又笨又重&#xff0c;耗费资源严重&#xff1a; 容器技术只隔离应用程序的运行时环境但容器之间共享同一个…

统一使用某一个包管理工具,比如yarn pnpm

原因&#xff1a;前端每个人的习性不一样&#xff0c;有人用npm 有人用yarn等包管理工具&#xff0c;混合下载插件容易出bug&#xff0c;就用个小工具锁住就行了&#xff0c;只能使用yarn或者pnpm反向下载依赖和下载插件。不然就报错 1.在项目主目录下创建preinstall.js // 如…

你搞清楚了吗?| GET请求方式的长度限制到底是多少?

目录 &#x1f4cd; 浏览器限制 &#x1f4cd; 服务器限制 在大多数人的一贯认识中&#xff0c;一直认为get请求方式有2048B的长度限制&#xff0c;其实这种说法是有失偏颇的&#xff0c;甚至可以说是错误的。 这个问题一直以来似乎是被N多人误解&#xff0c;其实Http Get方…

分布式锁实现二. memcached分布式锁

文章目录 memcached分布式锁实现原理&#xff1a;优缺点 开发准备安装memcached服务端安装jar到maven本地仓库 代码开发初始化Memcached客户端锁相关操作核心代码本地运行效果docker运行效果 memcached分布式锁 实现原理&#xff1a; memcached带有add函数&#xff0c;利用ad…

VScode 国内下载源 以及 nvm版本控制器下载与使用

VScode 国内下载源 进入官网 https://code.visualstudio.com/ 点击下载 复制下载链接到新的浏览器标签 将地址中的/stable前的az764295.vo.msecnd.net换成vscode.cdn.azure.cn&#xff0c;再回车就会直接在下载列表啦。 参考大神博客 2.使用nvm 对 node 和npm进行版本控制…

重装Windows10系统

以前清理电脑我一般是重置电脑的&#xff0c;但是重置电脑会清理C盘&#xff0c;新系统又遗留有以前的系统文件&#xff0c;导致后面配置环境遇到了棘手的问题&#xff0c;所以我打算重装系统。 第一次重装windows10系统&#xff0c;踩了很多坑&#xff0c;搞了两天才配回原来的…
最新文章