【DS】- 排序

排序是数据结构与算法中,重要的一环,本节来学习及实现各大排序算法。

常见排序算法

		 
插入排序  -插入排序
		 -希尔排序
		 
选择排序  -	选择排序
		 -堆排序
		 
交换排序  -冒泡排序
		 -快速排序

归并排序  -归并排序

选择排序

插入排序

思想:假设前面部分有序,后面插入一个新的数到前面有序部分。

  1. ​ 插入一个数:往前挪到适当位置保证前面部分有序。

  2. ​ 把第一个数当作有序部分,有序插入后面的n-1个元素,最终完成排序。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UBwSpPW8-1658585696791)(X:\编程\code\CSDN 博客\插入排序.gif)]

实现

​ 1.有序地插入一个数:

​ 初始化:用end保存有序部分最后一个元素下标,用tmp存储插入的数。

​ arr[end]与tmp比较,若前者大于tmp,后挪arr[end+1]=arr[end]

​ 最后arr[end]<tmp时,说明找到有序的位置,令arr[end+1]=tmp 。(这里可以每次交换,但效率不高)

​ 2.最后一次单躺排序时,end在倒数第一个位置:对应索引为n-2 ,故代码如下

//升序
void InsertSort(int* a, int n)
{
	for (int end = 0; end < n - 1; end++) {
		//[0,end]有序
		int tmp = a[end + 1];
        //end最少为0的位置
		while (end >= 0)
		{
			if (a[end] > tmp)
			{
				a[end + 1] = a[end];
				end--;
			}
			//找到正确的位置
			else
				break;
		}
		a[end + 1] = tmp;
	}
}

希尔排序

  1. 预排序 :分组(步长gap) 每组分别单躺,完成大致有序

  2. 再一次整体单趟插排

实现

i遍历每组的每个元素。 i 初始值为一组的首元素索引,i的边界应为lenth-gap(刚好为上图的6对应的索引),步增gap

end的应该等于每一组的开头索引,end=i。此时,

  • 可以再套一层循环,遍历每一组
  • i 的遍历顺序改为i++,因此变成了轮流遍历各分组的元素。最终效果与第一种方法一样,这种方法更加巧妙。

代码

void ShellSort(int* a, int n) {
	//1.预排序 分组步长gap 每组分别单躺插入

	int gap = 2;
	for (int i = 0; i < n - gap; i++) {
		int end = i;
		int tmp = a[end + gap];
		while (end >= 0)
		{
			if (tmp < a[end])
			{
				a[end + gap] = a[end];
				end -= gap;
			}
			else
				break;
		}
		a[end + gap] = tmp;
	}
	//2.再单趟插入
	InsertSort(a, n);
}
关于gap的取值

升序: gap取值越大,大数越快到后面,小数越快到前面,越不接近有序

​ gap取值越小,越接近有序。当gap==1时,为插入排序。

假设数据有10000个,此时gap取3就没意义了,因为与gap==1相差不大。

这里有种比较官方的算法:gap初始化为n,每次自除3。为了让gap最后能等于1,又需要+1:gap=gap/3+1

这样能让当gap==2,3时,必得到1 。

优化后代码

void ShellSort(int* a, int n) {
	//gap>1时 预排序
	//gap==1时,插入排序 
	int gap = n;
	while (gap > 1)
	{
		gap /= 3 + 1;
		for (int i = 0; i < n - gap; i++) {
			int end = i;
			int tmp = a[end + gap];
			while (end >= 0)
			{
				if (tmp < a[end])
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
					break;
			}
			a[end + gap] = tmp;
		}
	}
}

时间复杂度:希尔排序的时间复杂度非常难算,上面的算法可以近似视为:n/3/3/3/……=1 →3^x=n (x为除以3的次数)

所以x约等于log3(N) 以3为底N的对数

根据前人的经验,我们一般记作O(n^1.3)

选择排序

选择排序,故名思意,遍历一遍选择数组里面最大(小)的,与首(尾)交换。选择排序

然后因为遍历一次是可以记录最小值,跟最大值的,所以这里是可以对其再进行优化。

实现

void SelectSort(int* a, int n)
{
	//记录最大值,最小值的索引
	int maxi = 0, mini = 0;

	//用begin,end记录需要交换的索引
	int begin = 0, end = n - 1;

	while (begin < end) {
		//单趟,一个数需要做的
		for (int i = begin + 1; i <= end; i++) {
			if (a[i] > a[maxi])
				maxi = i;
			if (a[i] < a[mini])
				mini = i;
		}
		swap(&a[begin], &a[mini]);
		//有可能交换后,maxi指向不是最大值,原最大值被交换到mini,所以要重定向
		if (maxi == begin)
			maxi = mini;
		swap(&a[end], &a[maxi]);
		end--;
		begin++;
	}
}

堆排序

堆排序见此文章

冒泡排序

一个非常容易理解的排序。

这里只讲讲冒泡的优化,当数组已经有序的时候,遍历一边是不发生交换的,但是程序仍然会往下遍历,可以在这里加入个flag用来判断是否交换,以省去后续的遍历。

void BubbleSort(int* a, int n)
{
	int flag = 0;
	for (int i = 0; i < n; i++)
	{
		for (int j = i; j < n - 1; j++)
		{
			if (a[j] > a[j + 1]) {
				swap(&a[j], &a[j + 1]);
				flag = 1;
			}
		}
		if (!flag)
			break;
	}
}

快速排序

hoare版本

发明快排的计算机科学家的最初始的版本。

选出一个keyi,记录最左/最右的值(记住索引而不是值,是为了后面方便交换)

单趟完后,左部分得比key小;右部分得比key大。

image-20220721145146285

一趟快排的简单实现

	int left = begin, right = end;
	int keyi = left;
	//单趟
	while (left < right)
	{
		//当右边全都大于左边,这个时候end会越界到左边而停不下来,故需要再加一层判断left<right【防越界】
		while (left < right && a[right] >= a[keyi])//当当前右边是大于key,就不动他;让end--
		{
			right--;
		}
		while (left < right && a[left] <= a[keyi])
		{
			left++;
		}
		swap(&a[left], &a[right]);
	}
	swap(&a[keyi], &a[right]);

为什么选左边的时候,要先移动右指针?

这个算法必须保证相遇位置的值,要比key小。【左找大,右找小】

如果L先走,就可能会找比Key大的值;只有R先走才能保证找到比key小或等于的值。

然后用分治的思想,递归的解决两边排序,退出的条件是当begin>=end时(区间不存在或者只有一个值的时候)

实现
void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
		return;
	int left = begin, right = end;
	int keyi = left;
	//单趟
	while (left < right)
	{
		//当右边全都大于左边,这个时候end会越界到左边而停不下来,故需要再加一层判断left<right【防越界】
		while (left < right && a[right] >= a[keyi])//当当前右边是大于key,就不动他;让end--
		{
			right--;
		}
		while (left < right && a[left] <= a[keyi])
		{
			left++;
		}
		swap(&a[left], &a[right]);
	}
	swap(&a[keyi], &a[right]);
	//更新keyi
	keyi = left;

	//[begin,keyi-1] keyi [keyi+1,end]
	//排序左右
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}

挖坑法

相对上一个版本,思想上没有特别大的区别,只是用一个key,及piti保存位置,替代了多次交换

实现
//单趟 挖坑版
int partsort2(int* a, int begin, int end) {

	int key = a[begin];
	int piti = begin;
	while (begin < end)
	{
		while (begin < end && a[end] >= key)
			end--;

		//填坑,并更新piti
		a[piti] = a[end];
		piti = end;

		while (begin < end && a[begin] <= key)
			begin++;
		//填坑,并更新piti
		a[piti] = a[begin];
		piti = begin;

	}
	//key填入最后的相遇的坑
	a[piti] = key;
	return piti;
}

前后指针版

prev维护前面小于key的左部分,cur往后找大于key的右部分

对于cur指向值:

  • 小于key,让prev维护的部分扩大,即prev自己往后一步,然后把cur指向小于key的值跟prev指向的值交换;

    prev++; 
    swap(&a[cur],&a[prev]);
    
  • 大于key,让cur往后走,不动prev;保持cur维护大于key值部分。

    cur++;
    

一趟走完,prev在左部分的最后一个,跟key交换,使其在中间:[小于key] key [大于key]

swap(&a[keyi], &a[prev]);

再对prev交换进行优化:因为prev与cur重叠时是不需要交换的,故可以跳过: 加上条件prev!=cur

实现
//单趟 前后指针版
int partsort3(int* a, int begin, int end) {
	int prev = begin, cur = begin + 1;
	int keyi = begin;
	while (cur <= end)
	{
		判断cur值:小于key,让prev往后一步(扩大左部分)
		//if (a[cur] < a[keyi]) {
		//	prev++;
		//	swap(&a[prev], &a[cur]);
		//}

		if (a[cur] < a[keyi] && ++prev != cur)
		{
			swap(&a[prev], &a[cur]);
		}
		cur++;
	}
	swap(&a[keyi], &a[prev]);
	keyi = prev;
	return keyi;
}

优化

快排的最坏情况,每次key的值拿到了最小/最大,时间单趟时间复杂度变成O(N),整体变成O(N²);更糟的是,快排很有可能导致栈溢出。

所以有了三数取中的算法,每次取首尾中三个数中的中间值,确保不会是整体的最大/最小值。

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

从上可以得知当,栈溢出是快排最大的问题,我们可以在当区间缩小到一定范围的时候,选择其他排序,减少递归的次数。

下图h为高度,右边标的为每层递归调用次数

快排优化

非递归快排

  1. 改循环

  2. 利用数据结构栈(堆中开辟),模拟内存开辟的栈区。

    非递归快排

归并排序

类似二叉树的后序遍历

思路

对一堆数据排序,先成左右两组,两组排序好后,

再把2组有序地归并到一个tmp数组,再复制回原数组。

归并排序

结束条件就是,区间只有一个,或者不存在(奇数)时,直接返回。

实现

void _MergeSort(int* a, int begin, int end, int* tmp) {
	//如果,只有一个值/不存在区间,就不需要合并  即begin>=end
	if (begin >= end)
		return;

	int mid = (begin + end) / 2;
	//分治,后序遍历
	_MergeSort(a, begin, mid, tmp);
	_MergeSort(a, mid + 1, end, tmp);

	//归并  
	//有序左区间[begin,mid]  有序右区间 [mid+1,end]
	//再合成有序的大区间tmp  :双指针


	//归并到tmp:利用索引
	int begin1 = begin, end1 = mid;//表示左区间
	int begin2 = mid + 1, end2 = end;//右区间
	int i = begin1;//记录tmp存放索引
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			tmp[i++] = a[begin1++];
		}
		else
		{
			tmp[i++] = a[begin2++];
		}
	}
	//其中一个没有走完
	while (begin1 <= end1)
		tmp[i++] = a[begin1++];
	while (begin2 <= end2)
		tmp[i++] = a[begin2++];

	//再把tmp的值,按顺序给a数组
	memcpy(a + begin, tmp + begin, (end - begin + 1) * sizeof(int));


}

void MergeSort(int* a, int n) {
	//开辟动态临时数组
	int* tmp = malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		printf("");
		exit(-1);
	}
	_MergeSort(a, 0, n - 1, tmp);

	//释放
	free(tmp);
}

非递归版

直接对数组进行迭代归并

非递归归并排序

归并的算法与上面大致一样。但是因为如果数组不是2的次方倍,而每次gap*=2 ,会导致越界。

而又不能用i<=n-gap 这会使i跳过某些区间 ,所以还需要对边界进行修正

void MergeSortNonR(int* a, int n) {

	int* tmp = malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		printf("");
		exit(-1);
	}

	int gap = 1;//区间个数
	while (gap < n) {

		//单趟:
		for (int i = 0; i < n; i += 2 * gap)
		{
			//    gap		   gap		  2*gap
			//[begin1,end1][begin2,end2]
			//[i,i+gap-1][i+gap,i+gap+gap-1]
			int begin1 = i, end1 = i + gap - 1;//表示左区间
			int begin2 = i + gap, end2 = i + gap + gap - 1;//右区间

			//区间个数非2的次方数,会出现越界
			//在此对end修正
			if (end1 >= n) {//end1越界  ,把后面右区间修为不存在的区间  因为end1都越界了,所以后面的绝对越界
				end1 = n - 1;
				begin2 = n;
				end2 = n - 1;
			}
			else if (begin2 >= n) {//begin2越界,区间不存在
				begin2 = n;
				end2 = n - 1;
			}
			else if (end2 >= n)//end2越界
			{
				end2 = n - 1;
			}




			int tmpi = begin1;//记录tmp存放索引
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					tmp[tmpi++] = a[begin1++];
				}
				else
				{
					tmp[tmpi++] = a[begin2++];
				}
			}
			//其中一个没有走完
			while (begin1 <= end1)
				tmp[tmpi++] = a[begin1++];
			while (begin2 <= end2)
				tmp[tmpi++] = a[begin2++];

		}
		memcpy(a, tmp, n * sizeof(int));
		//每次让区间变成2倍
		gap *= 2;
	}
}

外排序:海量数据读取磁盘文件排序。

以上7种排序只有归并排序适合外排序

时间复杂度分析

稳定性:是指排序数组中,一组相同数据在排完序后,仍能保持一样的相对顺序,即称为稳定性好。

稳定性并非指,比如快排有时能到最好情况O(nlogN),有时最坏情况O(N²),这种波动并非指这里的稳定性。

			最坏(时间)		最 好		  空间		 稳定性		
-插入排序		O(N²)		 O(N)		O(1)		 稳定
-希尔排序		  O(N^1.3)(平均)   	   O(1)		   不稳定	(预排时,相同的数据可能分到不同的组)
		 
-选择排序		O(N²)		 O(N²)		O(1)		不稳定	 (每次选最小/最大,可能把相对顺序改掉,如4a 4b 1变1 4b 4a)
-堆排序		O(NlogN)    O(NlogN)	 O(1)		 不稳定  9, 7- 6 ,7- 7  最后一个7于9换,已经改变相对顺序
		 
-冒泡排序		O(N²)		O(N)		O(1)		 稳定
-快速排序		O(N²)	   O(NlogN)		O(logN)		不稳定  5 6 5  -2-  6 5 7,排完第一个5跑到中间,顺序改变了

-归并排序		O(NlogN)   O(NlogN)     O(N)		 稳定   1 2 - 1 3 让左1先排即可稳定
好的,我可以为你解答关于希尔排序的问题。希尔排序是插入排序的一种改进算法,也被称为“缩小增量排序”。它的基本思想是将待排序的序列按照一定的间隔分成若干个子序列,对每个子序列进行插入排序,然后逐步缩小间隔,直到间隔为1,最后对整个序列进行一次插入排序。 希尔排序的时间复杂度与间隔序列的选择有关,最坏时间复杂度为$O(n^2)$,平均时间复杂度为$O(n \log n)$。相比于插入排序,希尔排序的时间复杂度有了明显的提升,且其在实际应用中也表现得比插入排序好。 下面是希尔排序的示例代码: ```python def shell_sort(arr): n = len(arr) gap = n // 2 while gap > 0: for i in range(gap, n): temp = arr[i] j = i while j >= gap and arr[j - gap] > temp: arr[j] = arr[j - gap] j -= gap arr[j] = temp gap //= 2 return arr ``` 在这段代码中,我们首先定义了一个希尔排序的函数shell_sort,它接收一个列表arr作为参数,并返回排好序的列表。在函数中,我们首先获取列表的长度n和初始的间隔gap,然后开始循环,直到间隔为1为止。在每次循环中,我们将列表按照间隔分成若干个子序列,对每个子序列进行插入排序,将子序列按照升序排列。最后,我们将间隔除以2,继续循环直到间隔为1。 使用希尔排序对列表进行排序非常简单,只需要调用shell_sort函数即可。例如,我们可以这样调用函数: ```python arr = [5, 2, 8, 3, 1, 6] sorted_arr = shell_sort(arr) print(sorted_arr) ``` 这段代码会输出排好序的列表[1, 2, 3, 5, 6, 8]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值