C实现排序算法核心理解(一)

个人理解记录————别从冒泡开始~
直接上快排

参考引用:

快速排序

关键在于理解整个流程,简单来说就是在每一次的循环中两个标志位的不断逼近和交换。C语言网这里抄的是这样描述:

首先在数组中选择一个基准点,然后分别从数组的两端扫描数组,设两个指示标志(low指向起始位置,high指向末尾),首先从后半部分开始,如果发现有元素比该基准点的值小,就交换low和high位置的值,然后从前半部分开始扫描,发现有元素大于基准点的值,就交换low和high位置的值,如此往复循环,直到low>=high,然后把基准点的值放到high这个位置。

字太多了,下面这个博主的博文足以描述核心的说法。
https://blog.csdn.net/morewindows/article/details/6684558
核心:挖坑填数+分治法
每一次的流程是一个挖坑填数,整体是分而治之。
挖坑填数:
一个数组,当我们取出基数的时候,相当于数据被挖了一个坑,我们一般取第一个数为基数。
有坑就要填,怎么填?
从后开始找数填坑同时形成新坑,按照如下规则找:

  • 从后往前时,找小的基数的
  • 从前往后时,找大于基数的
  • 一后一前依次寻找直到前后标志相遇

所以每一次基数相同的时候都是一个循环,在这个循环离会分别从后往前和从前往后搜索全数组,直到i==j

OK,看一百遍不如自己写一遍,试试

//先构建一个大循环,表示在这个循环的时候,基数是固定的x,i和j是起点和终点,一开始基数可以设置为第一个数
x =a[i]
while (i<j)
{
	//在循环内部,这里巧妙的点在于,我们不需要知道具体怎么交换,一共交换了多少次,只需要设置好边界条件即可
	//先后向前,这里先找到合适的j,这里又判断了一次i<j是为什么
	/* 外层那个表示初始基数的大小情况,这里是自减的边界
	找到小于基数的数的位置,可以用for带break
	for (;j>i;j--)
	{
		if (a[j]<=x)
		{
			a[i] = a[j];
			i++;
			break;
		}
	}
	 */
	
	while(i<j && a[j]>x)
		j--;
	//防止j--到最后也没找到相关值,相当于a[i]i之后就是最小值
	if (i<j)
	{
		a[i] = a[j];
		i++;
		/*不++也行,++就是避免重复运算,a[i]=a[j]<=x,i这边要找的是>x的,填a[j]的坑
		不手动++的话,a[i]=x,就死循环了*/
	}
	
	//重复上述操作,这里的i<j不能省略,因为上面的ij可能已经相等了
	while(i<j && a[i]<x)
		i++;
	//防止j--到最后也没找到相关值,相当于a[i]i之后就是最小值
	if (i<j)
	{
		a[j] = a[i];
		j--;
		/*不++也行,++就是避免重复运算,a[i]=a[j]<=x,i这边要找的是>x的,填a[j]的坑
		不手动++的话,a[i]=x,就死循环了*/
	}
}

//循环结束,必有i==j
a[i] = x;
return i;

挖坑填数的部分OK,那分而治之呢,用递归
得到中间数之后,两边再依次挖坑填数,直到只剩一个数

if (start<end)
{
	mid = digArray();//mid这个数可以不用管了实际上
	quiksort(a,start,mid-1);
	quiksort(a,mid+1,end);
}

上面这个是用递归的写法,能用递归就能用循环。怎么做?先放着


归并排序

跟快排差不多,我觉得可以用:归并+分而治之 当作核心思想
归并类似于一次往单管道里填充填数,啥意思?就好像有一根管子,我们总是取一个比较小的数字放进去,直到管道被填满。嗯,比快排好懂
在这里插入图片描述

贴一下概念:归并排序(Merge sort)是建立在归并操作上的一种有效、稳定的排序算法,该算法是采用分治法(Divide and
Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

还是写一下就懂

#include <iostream>
using namespace std;

//单次的归并操作,感觉比较好理解,唯一需要注意的就是边界条件要缕清,这里是<=,用<的话start2=mid?
void mergeSortArray(int a[], int first, int mid, int last, int temp[])
{
	//这里比较需要注意的是边界条件
	int start1 = first;
	int end1 = mid;
	int start2 = mid+1;
	int end2 = last;
	int k = 0;

	while (start1 <= end1 && satrt2 <= end2)
	{
		if (a[start1] <= a[start2])
			temp[k++] = a[start1++];
		else
			temp[k++] = a[start2++];
	}
	while (start1 <= end1)
	{
		temp[k++] = a[start1++];
	}
	while (start2 <= end2)
	{
		temp[k++] = a[start2++];
	}
	//注意下面这个函数的写法
	for(int i=0;i<k;i++)
   {
      a[i+first] = temp[i];
   }
}

void mergeSort(int a[], int first, int last, int temp[])
{
	//这里依然唯一需要注意的是边界条件,传进来的last=n-1,和上面<=有关
	if (first < last)
	{
		int mid = (first+last)/2;
		mergeSort(a,first,mid,temp);
		mergeSort(a,mid+1,last,temp);
		mergeSortArray(a, first, mid, last, temp);
	}
}

void main()
{
	int a[9] = {3,8,6,5,5,4,1,7,2};
    int n = 9;
    int* temp = new int[9];

    mergeSort(a,0,n-1,temp);

    for (int i=0;i<n;i++)
        cout<<a[i]<<' ';

    delete [] temp;
}

同样的,能用递归就能用循环。
这里直接看这个博主写的,很好很清晰。
https://blog.csdn.net/dong132697/article/details/132646066
归并是怎么变成循环的?我们用递归排序的时候,是不断对半分,直到分成只有一个数的时候。那当我们拿到一个数组,其中的每个数都可以看作是递归到最下层的情况。于是,我们可以把1 2 3 4 5 6 7 8 位次分别递归,而后就是12 34 56 78,最后演变为1234 5678。
依据我们做事的核心思想:从简到难,就先把最简单的情况写出来,就是刚好有2^n是可以完美划分的。那么

mergeSort2(int a[], int first, int last, int temp)
{
	int gap = 1; //gap就是每一次划分组的时候的每组里的数量
	int i = 0;
	int k = 0; //归并必备辅助数组的指针

	while(gap<n) //完美情况下,gap=n,不需要再归并了
	{
		//每一次归并就是一个循环,我们每次都从0开始,其实就是一个依次划分的游戏,间2gap一个新的划分
		//除了上面这些,每次实际间隔划分就是一次归并,那就和上面的归并没有区别,把3个while拿过来
		//那唯一剩下的问题,start1/2,end1/2是什么
		for (i = 0;i<n;i += 2*gap)
		{
			start1 = i;//每一次循环都是从i开始的
			end1 = i+gap-1;//2gap对半分,为什么减一,因为下面<=会达到end1
			start1 = i+gap;
			end2 = i+2*gap;

			/* 核心关键,k需要每次更新,这是不同于递归的地方,递归每次进到函数,k都是0
			递归完成,实际使用的就是数组前面的一小段,所以数组a和temp不能整体交换,只交换前面k位
			 */
			k = i; 
			while (start1 <= end1 && satrt2 <= end2)
			{
				if (a[start1] <= a[start2])
					temp[k++] = a[start1++];
				else
					temp[k++] = a[start2++];
			}
			while (start1 <= end1)
				temp[k++] = a[start1++];
			while (start2 <= end2)
				temp[k++] = a[start2++];
		}

		swap(a,temp);//每次循环完成,temp数组就是最终数组,具有连贯性

		gap = 2*gap;
	}
}

OK,有了完美情况,再考虑特殊边界条件,这里还是看上面参考的博主的文章,这里不再赘述,那在我们的代码里怎么写这个特殊边界?

说白了,还是改start1/2,end1/2

#define min(a, b) (a<b?a:b)

start1 = i;//不需要改变
end1 = min(n-1,i+gap-1);  //特殊情况下,end1就是数组的末尾n-1
start2 = i+gap; //两种特殊情况,先考虑简单的,存在,则为i+gap,另一种情况,实际上不存在,那么end2也应该不存在,但是不能都设置为n-1,不然会导致start2=end2,也会做一次归并操作
end2 = min(n-1,i+gap*2); //这里要加min,让end有所限制,避免不存在时start2=end2,存在时=min(n-1,i+gap*2)

good,归并的两种形式都拿下,对其原理也比之前理解的更加深刻了。

回到最上面,快速排序的循环形式怎么写?

快速排序(循环写法)

有一丢复杂,先鸽了

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值