常见的排序算法详解(二)

在这里插入图片描述

  • 冒泡排序

冒泡排序基本上是我们学习过程中接触到的第一个排序方法,思想也比较简单,就是两两比较后,如果大的数在小的数前面就交换两者的位置,然后就往后走一步接着比较两个数。走到队尾就完成一次冒泡,把最大的数就移到了最后,再从头开始冒泡,这时的队长度是n-1,走一趟长度就减一,最终完成升序排列。

//冒一趟泡时
for (int i = 0; i < n-1; i++)    这是第一趟n个数要比较n-1{
		if (a[i] > a[i + 1])
		{
			swap(&a[i], &a[i + 1]);
		}
	}
//完整的冒泡排序
void bubblesort(int* a, int n)  
{
   int end = n - 1;              //n个数只要冒n-1趟就已经有序了         
while (end)
{
	int flat = 0;                  //      用一个标志来记录一趟冒泡是否有交换数据,如
	                                   // 果没有交换任何数据就表明已经有序了,可以提前跳出。
	for (int i = 0; i < end; i++)    
	{
		if (a[i] > a[i + 1])
		{
			swap(&a[i], &a[i + 1]);
			flat = 1;
		}
	}
	if (flat == 0)
		break;
		end--;
}
}

冒泡排序空间复杂度和时间复杂度分析

  • 空间复杂度为O(1),没有额外开辟过多变量空间

  • 时间复杂度:每次单趟冒泡要比较次数 n-1,n-2````````````1等差数列求和最高项n2 忽略系数所以时间复杂度为O(n2),最好情况是初始时就有序了比较一趟没有交换一次,时间复杂度为O(n)。

  • 快速排序

快速排序是一个相对比较复杂的排序,它的性能在各种排序中是非常优越的。对一组数据进行快速排序,首先分解成几个小部分进行讲解,首先用一种步骤,选取其中一个数为基准值,进行一趟比较后让这个数到一个位置上,让其左边的数都不大于它,其右边的数都不小于它,这个数的位置在排序最终完成后都不会在变化了。
方法一 hoare版本
在这里插入图片描述

在这里插入图片描述
这里的规则有:如果选取左边第一个值做key那么要让右边先走,选取右边第一个值作key要让左边先走。key在左边时,最后一次key与相遇位置值交换,一定是那个位置的值小于key,那就一定是左边L去碰见R,这时肯定R已经找到了比key小的值,要不然它不会停下来,它没找到的话,就直接去碰见了L,L此时的位置是上一次两人交换完的位置,而每次两人交换完后L的值小于key,R的值大于key,按这样的规则才能保证把key交换到相遇位置时还能保证我们最终要的效果。当选右边第一个做key时同理。

int parrtion1(int* a, int left, int 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[left], &a[keyi]);
	return left;         //返回确定了一个值的最终位置的坐标,后面在递归处理它左边的数和右边的数                            
}

像上面这样完成一步后,已经确定了一个数在数组中的位置,我们就按同样的思路划分区间来处理[0,key-1],[key+1,n-1],用递归的方法一直处理下去,递归结束的条件就是分到的区间只有一个数,此时它的位置也无法再调整,或者区间不存在了。
在这里插入图片描述

void quicksort(int* a, int left, int right)
{
	if (left >= right)
		return;
		int keyi = parrtion1(a, left, right);
		quicksort(a, left, keyi - 1);
		quicksort(a, keyi + 1, right);
}

我们用了hoare版本方法来确定一趟后选的key的最终位置,还有两种方法起到了和这种方法一样的作用。

  • 方法二 挖坑法
  • 在这里插入图片描述

也是先选取一个值为基准值,用变量key保存起来,这个基准值的位置坐标用pivot变量保存,让pivot右边的R先走找比key小的值,找到就停下,将这个位置的值放入到pivot记录位置中,pivot原本记录位置存放的基准值已经用key保存起来了,然后pivot记录R此时的位置,让左边走,找比key大的值,找到了就放入到pivot记录位置的地方去,让pivot记录L此时的位置,又让R走,如此往复直到RL相遇,将key的值放入这个位置,返回这个位置。
在这里插入图片描述
上面图示挖坑法走一趟后的数据是 5 1 2 4 3 6 9 7 10 8 使基准值6到了正确的位置。

int parrtion2(int* a, int left, int right)
{
	int key = a[left];
	int pivot = left;
	while (left < right)
	{
		while (left < right && a[right] >= key)
			right--;
		a[pivot] = a[right];
		pivot = right;
		while (left < right && a[left] <= key)
			left++;
		a[pivot] = a[left];
		pivot = left;
	}
	a[left] = key;
	return left;
}

方法三 前后指针法
在数组左端或右端选一个基准值用key记录其位置,当选左端为基准值时用cur记录下一个数的位置,prev记录第一个数位置,让cur往右开始走,找比key小的值,找到了就让prev移动一位,再交换cur和prev位置上的值,继续让cur走下去,重复以上步骤,走到右端为止,再交换prev和key位置上的值。
cur的作用就是遍历除key位置以外的其他数。当选右端值为基准值时,cur记录第一个数的位置,prev在cur后一位,最后要先将prve+1,再于key位置值交换。
在这里插入图片描述

int parrtion3(int* a, int left, int right)
{
	int key = left;
	int prev = left;
	int cur = left + 1;
	while (cur <= right)
	{
		if (a[cur] < a[key]&&++prev!=cur)    //这里当prev=cur时自己跟自己交换就没必要了
		{
			swap(&a[cur], &a[prev]);
		}
		cur++;
	}
	swap(&a[key], &a[prev]);
	return prev;
}
  • 分析快速排序递归版的缺陷。

    当每次确定一个基准值最后在数组的中间位置时,再递归左右两边区间,层层下去逻辑上与完全二叉树类似,这时递归深度近似为logn,当要排序的数组本身就非常接近有序,或者都为一个数如2 2 2····· ,2 3 2 3········ 这些情况下, 我们选一端的值为基准值,一趟跑完后基准值最后的位置还是在这一端上,使得层层递归的情形如下图所示。
    在这里插入图片描述
    这样的深度就近似N了当个数N非常大时递归深度太深就容易栈溢出。 我们要改善这些情况,首先就该知道快速排序不适合用来排序初始就已经近似有序的一组数。然后还可以在选取key时改善方法,让每次基准值最后的位置相对靠中间,这要递归下去可以减小深度。
int getmid(int* a, int left, int right)
{
	int mid = (left + right) / 2;
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
			return mid;
		else if (a[left] > a[right])
			return right;
		else
			return left;
	}
	else
	{
		if (a[mid] > a[right])
			return mid;
		else if (a[left] < a[right])
			return left;
		else
			return right;
	}
}
int parrtion3(int* a, int left, int right)
{
	int tem = getmid(a, left, right);
	swap(&a[left], &a[tem]);
	int key = left;
	int prev = left;
	int cur = left + 1;
	while (cur <= right)
	{
		if (a[cur] < a[key]&&++prev!=cur)
		{
			swap(&a[cur], &a[prev]);
		}
		cur++;
	}
	swap(&a[key], &a[prev]);
	return prev;
}

像上面代码一样,当每次要对一个区间的数用以上三种方法之一将key放到正确位置时,就在左右两端的数和中间位置数中选出中间大小的数做基准值,使基准值最后在比较中间的位置,利于再划分子区间,上面方法就构造了一个取中间数的函数完成任务。其他的不变就把中间大小的数拿到第一个位置来,再按原来的方法跑一趟。

有一种小区间优化的方法也可以加在快速排序中来优化,就是每次递归时判断区间长度,如果区间已经很小了,在这段区间的排序就可以用其他排序方法来排序,这样可以大大减小递归的深度。

void quicksort(int* a, int left, int right)
{
 if (left >= right)
 {
 	return;
 }
 if (right - left + 1 < 10)          //到递归到层数较深时,区间小于10,可以用小区间其他排序方法
 {                                                    //来降低快排递归太深的问题
 	insertsort(a + left, right - left + 1);
 }
 else
 {
 	int keyi = parrtion3(a, left, right);
 	quicksort(a, left, keyi - 1);
 	quicksort(a, keyi + 1, right);
 }
}
  • 快速排序非递归版

快速排序的思想就是不断确定基准值的位置到最后所有数的位置都确定了排序就完成了,每次递归要接受的参数就是区间边界值,和数组首元素地址,由于递归可能带来递归过深从而栈溢出的问题,所以可以用非递归的方法来解决栈溢出的问题,非递归也是在模拟递归的执行过程,递归的参数是区间边界值,那我们可以用栈这种数据结构来保存每次要走一趟的区间边界值,这样模拟也可实现快速排序。

void quicksortnonR(int* a, int left, int right)
{
	Sta st;                        //建立一个栈
	stackinit(&st);             //栈初始化
	stackpush(&st, left);     //  把原始数组区间边界入栈
	stackpush(&st, right);
	while (!stackEmpty(&st))      //只要栈非空就说明还有区间要按上述方法走一趟
	{
		int end = stacktop(&st);            //拿出两个边界值,注意栈后进先出的原则
		stackpop(&st);
		int begin = stacktop(&st);
		stackpop(&st);
		int key = parrtion3(a,begin, end);          //这里就排序一趟
		if (key + 1 < end)                              //如果基准值右边还有数可以排,就将右子区间边界值入栈
		{
			stackpush(&st, key + 1);
			stackpush(&st, end);
		}
		if (begin < key-1)                        //这里同理判断左子区间
		{
			stackpush(&st, begin);
			stackpush(&st, key-1);
		}
	}
	stackdestory(&st);
}

快速排序空间复杂度和时间复杂度分析

  • 空间辅助度:每次确定一个基准值要递归一层下去,每个单趟是O(1)递归平均深度logn,所以空间辅助度是O(logn)。

  • 时间辅助度:递归每一层要比较的总数是n,层数是logn,时间辅助度最好就是O(nlogn),最坏的情况就是待排序的一组数是同一个数,这时递归的深度n,每层比较的数据个数是n n-1 n-2```````1,所以时间复杂度是O(n2)。

  • 归并排序

归并的思想是将两组已经有序的数组归并成一组有序的数组。

就如动图所示,刚开始一组数中就一个数就认为已经有序了,这样两个数就可以归并为一组有序数组,我们拿到待排序的数组时,先动态开辟n个数据大小空间,用来存归并了的数,然后再拷回原数组,拿到一组数,初始是无序的,将区间对半分,递归左右区间,一直递归到区间只有一个数或者没有数就返回上一层将递归回来已经有序的左右区间两组数就可以归并成一组了,不断返回上一层又再归并,最终就有序了。

void _mergesort(int* a, int left, int right, int* tem)
{
	if (left >= right)                             //如果区间只有一个数或没有就认为有序了,返回上一层进行归并。
		return;
	int mid = (left + right) / 2;               //这里就是不断分区间递归下去。
	_mergesort(a, left, mid, tem);           //递归左区间
	_mergesort(a, mid + 1, right, tem);   //递归右区间
	int begin1 = left;                               //返回到这一层,左右两组数已经有序了,归并到tem数组,再拷回原数组
	int end1 = mid;
	int begin2 = mid + 1;
	int end2 = right;
	int j = left;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			tem[j++] = a[begin1++];
		}
		else
		{
			tem[j++] = a[begin2++];
		}
	}
	while(begin1<=end1)
		tem[j++] = a[begin1++];
	while(begin2<=end2)
		tem[j++] = a[begin2++];
	for (int i = left; i <= right; i++)
		a[i] = tem[i];
}
void mergesort(int* a, int n)
{
	int* tem = (int*)malloc(sizeof(int) * n);    //动态开辟tem临时数组
	if (tem == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	_mergesort(a, 0, n - 1,tem);    //传原始区间过去层层递归
	free(tem);
	tem = NULL;
}
  • 归并排序非递归版

归并排序开始归并时是归并的两组都分别只有1个数时,对一组原始数据可以先两个归为一组,这样遍历走一趟,如果最后剩单个没有匹配归并,那这个数可以原封不动的送回原位置,然后下一趟循环是两个为一组归成一组四个数的有序序列,没有凑到四个时归完后依然也是有序的,如此重复下去,最后归成最终有序序列。

void mergesortnonR(int* a, int n)
{
   int* tem = (int*)malloc(sizeof(int) * n);
   if (tem == NULL)
   {
   	printf("malloc fail\n");
   	exit(-1);
   }
   int gap = 1;
   while (gap < n)
   {
   	for (int i = 0; i < n; i += 2 * gap)   //一个步长走过两组,即归并完两组接着归并下一个两组
   	{
   		int begin1 = i, end1 = i + gap - 1;  //  左边这一组的边界
   		int begin2 = i + gap, end2 = i + 2 * gap - 1;   //右边这一组的边界
   		if (end1 >= n || begin2 >= n)    //归到最右端可能右边那组不存在了,这就不用归了
   			break;
   		if (end2 >= n)    //当最右端右边这一组没有和左边这一组同样多的数时,就修正一下右边组右端边界,防止越界访问
   		{
   			end2 = n - 1;
   		}
   		int j = i;
   		while (begin1 <= end1 && begin2 <= end2)   //下边就是正常归并,然后拷回原数组
   		{
   			if (a[begin1] < a[begin2])
   			{
   				tem[j++] = a[begin1++];
   			}
   			else
   			{
   				tem[j++] = a[begin2++];
   			}
   		}
   		while (begin1 <= end1)
   			tem[j++] = a[begin1++];
   		while (begin2 <= end2)
   			tem[j++] = a[begin2++];
   		for (j = i; j <= end2; j++)
   			a[j] = tem[j];
   	}
   	gap *= 2;    //两两归成一组后,再将刚归并出的有序组两两再归成一组,
   	                   // 直到这一组就是完整的原始数组的有序序列
   }
   free(tem);
   tem = NULL;
}

归并排序空间复杂度和时间复杂度分析

  • 空间辅助度:就开辟了临时数组tem,空间复杂度为O(n)。
  • 时间辅助度:递归深度logn,每层归并O(n),所以时间复杂度时O(nlogn)。
  • 7
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值