数据结构排序比较

news2024/11/25 2:04:35

排序的概念及其运用

(1)排序的概念

排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次 序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排
序算法是稳定的;否则称为不稳定的。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。

(2)常见的排序算法

分为插入排序和选择排序,交换排序,以及归并排序
大家好好对比一下区别

在这里插入图片描述

测试每个排序的性能

当我们将每个排序的代码写完,我们需要测试一下,每种排序的性能怎么样,就是我们可以用一下我们的时间差,来看每个排序的时间

// 测试排序的性能对比
void TestOP()
{
	srand(time(0));
	const int N = 100000;
	int* a1 = (int*)malloc(sizeof(int) * N);
	int* a2 = (int*)malloc(sizeof(int) * N);
	int* a3 = (int*)malloc(sizeof(int) * N);
	int* a4 = (int*)malloc(sizeof(int) * N);
	int* a5 = (int*)malloc(sizeof(int) * N);
	int* a6 = (int*)malloc(sizeof(int) * N);
	for (int i = 0; i < N; ++i)
	{
		a1[i] = rand();
		a2[i] = a1[i];
		a3[i] = a1[i];
		a4[i] = a1[i];
		a5[i] = a1[i];
		a6[i] = a1[i];
	}
	int begin1 = clock();
	InsertSort(a1, N);
	int end1 = clock();

	int begin2 = clock();
	ShellSort(a2, N);
	int end2 = clock();

	int begin3 = clock();
	SelectSort(a3, N);
	int end3 = clock();

	int begin4 = clock();
	HeapSort(a4, N);
	int end4 = clock();

	int begin5 = clock();
	QuickSort(a5, 0, N - 1);
	int end5 = clock();

	int begin6 = clock();
	MergeSort(a6, N);
	int end6 = clock();
	printf("InsertSort:%d\n", end1 - begin1);
	printf("ShellSort:%d\n", end2 - begin2);
	printf("SelectSort:%d\n", end3 - begin3);
	printf("HeapSort:%d\n", end4 - begin4);
	printf("QuickSort:%d\n", end5 - begin5);
	printf("MergeSort:%d\n", end6 - begin6);
	free(a1);
	free(a2);
	free(a3);
	free(a4);
	free(a5);
	free(a6);
}

插入排序

直接插入排序其基本思想是

直接插入排序是一种简单的插入排序法,把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为 止,得到一个新的有序序列 。

直接插入排序

我们先写单趟排序,我们最后一个需要排的数,直接插入。
等我们写完单趟排序,然后再写个循环,将其控制起来

我们的单趟排序

void InsertSort(int* a, int n)
{
	//我们先写单趟排序,假设n个数,
	//前n-1个数有序,只有最后一个乱序,直接插入

	//end表示有序数组的最后一个位置的下标,现在我们不知道
	int end;
	//将最后一个乱序的数保留下来,防止有序数组向后走将其覆盖
	int tmp = a[end + 1];
	//然后遍历一遍,知道将乱序的数放到他的位置
	//end是下标,是从0开始,所以遍历完数组需要将end>=0
	while (end >= 0)
	{
		//假设升序,如果tmp<a[end]就将其向后挪动,给tmp留空间
		if (tmp < a[end])
		{
			a[end + 1] = a[end];
			end--;
		}
		else
		{
			//如果在升序>=a[end],我们就不用动直接跳出来就行了
			break;
		}
	}
	//跳出来
	a[end] = tmp;
}

我们在外面套一层循环,然后控制直接插入排序。

void InsertSort(int* a, int n)
{
	//循环到n-1,就行了,自己画图看一下防止越界。
	for (int i = 0; i < n ; i++)
	{
		//我们先写单趟排序,假设n个数,
		//前n-1个数有序,只有最后一个乱序,直接插入
		//end表示有序数组的最后一个位置的下标,现在我们不知道
		int end = i;
		//将最后一个乱序的数保留下来,防止有序数组向后走将其覆盖
		int tmp = a[end + 1];
		//然后遍历一遍,知道将乱序的数放到他的位置
		//end是下标,是从0开始,所以遍历完数组需要将end>=0
		while (end >= 0)
		{
			//假设升序,如果tmp<a[end]就将其向后挪动,给tmp留空间
			if (tmp < a[end])
			{
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				//如果在升序>=a[end],我们就不用动直接跳出来就行了
				break;
			}
		}
		//跳出来
		a[end + 1] = tmp;
	}
	
}

直接插入排序的时间复杂度

有的同学看到两层循环,就直接说是 O ( N 2 ) O(N^2) O(N2),但是并不是我们要自己分析一下

分为两种情况:
有最好和最坏的情况:
最好的情况是:顺序有序我们的时间复杂度O(N)
最坏的情况是:逆序,我们每个都要进行时间复杂度就是 O ( N 2 ) O(N^2) O(N2).

希尔排序

什们是希尔排序
希尔排序法又称 缩小增量法
希尔排序法的 基本思想是:先选定一个整数,把待排序文件中所有记录分成个 组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工 作。当到达=1时,所有记录在统一组内排好序。

简而言之就是分为两步
1.预排序(分组排序)
2.插入排序

还是根直接插入排序一样,
将我们先写第一趟的希尔排序的代码。就是先把间距为gap的排好。
我们发现其实和直接插入排序的单趟排序一样,就是将1变为了gap而已,而且只是单趟中的一次交换

//希尔排序
void ShellSort(int* a, int n)
{
	int gap = 3;
	int end;
	//将最后一个乱序的数保留下来,防止有序数组向后走将其覆盖
	int tmp = a[end + gap];
	//然后遍历一遍,知道将乱序的数放到他的位置
	//end是下标,是从0开始,所以遍历完数组需要将end>=0
	while (end >= 0)
	{
		//假设升序,如果tmp<a[end]就将其向后挪动,给tmp留空间
		if (tmp < a[end])
		{
			a[end + gap] = a[end];
			end -= gap;
		}
		else
		{
			//如果在升序>=a[end],我们就不用动直接跳出来就行了
			break;
		}
	}
	//跳出来
	a[end + gap] = tmp;
}

那们将其变为单趟就再加一层循环,
自己分析一下代码,这是一整趟的希尔排序


//希尔排序
void ShellSort(int* a, int n)
{
	int gap = 3;
	for (int j = 0; j < gap; j++)
	{
		for (int i = j; i < n - gap; i += gap)
		{
			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;
		}
	}
}

下面将这个简化一下,效率没有任何提升,只是代码更简洁了。

//希尔排序
void ShellSort(int* a, int n)
{
	int gap = 3;
	//gap组并排,就是直接按照顺序排,只不过
	//不像刚才一组一组的。
	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;
	}
}

控制gap,实现多趟

//希尔排序
void ShellSort(int* a, int n)
{
	int gap = n;
	while (gap > 1)
	{
		//当gap=1的时候就是直接插入排序
		// 下面两种都是为了让最后一层gap=1.
		// 只不过第二中效率更高
		//gap = gap / 2;
		gap = gap / 3 + 1;


		//gap组并排,就是直接按照顺序排,只不过
		//不像刚才一组一组的。
		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;
		}
	}
	
}

希尔排序的理解

我们要理解我们的gap的作用
gap>1,就是我们的预排序
gap=1,就是我们的直接插入排序。

我感觉大家跟我第一次学的时候,一样感觉这个有点挫,最后都是是进行直接插入排序吗?为什们不直接用呢?

其实并不是,如果一组数是逆序排列,要排升序,最大的数在最前面,最小的数在最后面,我们要和每个数进行比较,然后才能到他的位置。

而我们希尔排序,利用gap,极大的减轻了他的负担,将大或小的数以gap步向它的位置靠近,让我们最后一次的直接插入排序十分轻松。

对于预排序来说 gap有什们要求没有?
1. gap越大,大的数可以更快的到后面,小的可以更快的到前面,却不接近有序。
2. gap越小,数据跳动的越慢,也就越接近有序。

希尔排序的时间复杂度

他其实比较难算,类似于一种期望公式,可以想象一下,就是乱序,越来越变得有序,这种必须那种具有数学很高的素养的人才有可能算出来。我们就估算就可以

我们大概估算约等于 O ( N 1.3 ) O(N^{1.3}) O(N1.3)

选择排序

我们的选择排序有选择排序和堆排序,堆排序我们介绍过了,就不多介绍了,我们主要讲选择排序。

直接选择排序

就是遍历一遍数组,选出最小的,放到第一个,然后再除了第一个选最小的放到开始。

那我们升级一下,再选最小的时候,同时选出最大的,放到开始和结尾,这样就会更快。直接看代码吧,太简单你了。

//直接选择排序
void SelectSort(int* a, int n)
{
	int begin = 0;
	int end = n - 1;
	int min = begin;
	int max = begin;
	while (begin < end)
	{
		for (int i = begin + 1; i <= end; i++)
		{
			if (a[i] < a[min])
			{
				min = i;
			}
			if (a[i] > a[max])
			{
				max = i;
			}
		}
		Swap(&a[begin], &a[min]);
		if (max == begin)
		{
			max = min;
		}
		Swap(&a[end], &a[max]);
		begin++;
		end--;
	}
	
}

直接选择排序的时间复杂度

O ( N 2 ) O(N^2) O(N2),很简单我们自己分析一下。

堆排序

我们在我们将堆的时候已经写过一会了,我们就不重复了,大家自己看《堆的概念和结构以及堆排序》就可以了。
我给一下这个堆排序的c代码

//交换元素
void Swap(int* p1, int * p2)
{
	assert(p1 && p2);
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
//向下调整
void AdjustDown(int* a, int n, int parent)
{
	int child = parent * 2 + 1;

	while (child < n)
	{
		if (child + 1 < n && a[child + 1] > a[child])
		{
			child += 1;
		}

		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}


}
void HeapSort(int*a, int n)
{
	assert(a);
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, n, i);
	}
	for (int i = 0; i < n; i++)
	{
		Swap(&a[0], &a[n - 1 - i]);
		AdjustDown(a, n - 1 - i, 0);
	}
}

交换排序

我们的交换排序,有两种一个是冒泡排序和快速排序

冒泡排序

我们的冒泡排序比直接选择排序还简单,就是注意控制一下循环就好了

//冒牌排序
void BubbleSort(int* a, int n)
{
	for (int i = 0; i < n -1; i++)
	{
		for (int j = 1; j <= n - 1 - i; j++)
		{
			if (a[j] < a[j-1])
			{
				Swap(&a[j], &a[j - 1]);
			}
		}
	}
}

快速排序(递归)

快排的单趟排序(升序)
左边的值比key要小,右边的值比key要大。key一般是做左边的值。找两个变量一个控制左,一个控制右。一个叫L,一个叫R。
单趟排序的意义
1.分割出左右区间,左区间比key小,右区间比key大
2.key已经落到他的正确的位置(排序的最终位置)。
剩下的问题
左区间有序,右区间有序,那么整体就ok了。
就是将大问题转换为小问题,用递归解决。
单趟的规定
左边做key,右边先走;能够保证相遇位置比key小。
右边做key,左边先走,能够保证相遇位置比key大。
为什们要这么规定?
会遇到两种情况
1.相遇,一种是R停住,L遇到R。相遇的位置就是R停住的位置
2.相遇,一种是L停住,R遇到L。相遇的位置就是L停住的位置。
因为只有R先走,因为R停住位置小于key,这样才能保证这两个位置都比key小。交换才可以key左边小右边大

快排的单趟排序。

(1) hoare版本

//快速排序
void QuickSort(int* a, int n)
{
	int left = 0;
	int right = n - 1;
	//防止出现下面的问题,将key变成下标
	//int key = a[left];
	int key = left;
	while (left < right)
	{
		//右边先走,找小
		//加等于的意思就是我们防止等于key造成死循环
		//left<right防止我们key右边的值都大于key造成越界
		while (left<right&&a[right] >= a[key])
		{
			right--;
		}
		//左边再走,找大
		while (left < right && a[left] <= a[key])
		{
			left++;
		}
		Swap(&a[left], &a[right]);
	}
	//不能跟key换,key是一个局部变量,所以将key成下标,这样就能修改数组。
	//Swap(&a[left], &key);
	Swap(&a[left], &a[key]);


}

快排递归代码

//快速排序
void QuickSort(int* a, int begin,int end)
{
	//递归结束条件
	if (begin >= end)
	{
		return;
	}

	int left = begin;
	int right = end ;
	//防止出现下面的问题,将key变成下标
	//int key = a[left];
	int key = left;
	while (left < right)
	{
		//右边先走,找小
		//加等于的意思就是我们防止等于key造成死循环
		//left<right防止我们key右边的值都大于key造成越界
		while (left<right&&a[right] >= a[key])
		{
			right--;
		}
		//左边再走,找大
		while (left < right && a[left] <= a[key])
		{
			left++;
		}
		Swap(&a[left], &a[right]);
	}
	//不能跟key换,key是一个局部变量,所以将key成下标,这样就能修改数组。
	//Swap(&a[left], &key);
	Swap(&a[left], &a[key]);
	key = left;

	//然后就分为了三个区间
	//[begin,key-1] key [key+1,end]
	QuickSort(a, begin, key - 1);
	QuickSort(a, key + 1, end);

}

(2)挖坑法

我们挖坑法的思路就是,挖坑填坑的思路
我们先将key的位置挖坑,在挖坑之前将我们的key的下标保存下来,然后我们还是先从右边找比他小的位置,(至于为什们找比它小的位置原理跟上面一样)然后将这个小的位置填到我们前面的坑里,
然后再从左边找比他大的位置,找到后填到上一个坑位中,这样依次类推,我们最后左大于等于右后,就停下来,把我们的key放到坑中,就是这样。

// 挖坑版本的单趟排序
int PartSort2(int* a, int begin, int end)
{
	int mid = GetMidIndex(a, begin, end);
	Swap(&a[begin], &a[mid]);

	int left = begin;
	int right = end;
	//防止出现下面的问题,将key变成下标
	//int key = a[left];
	int key = a[left];
	int hole = left;
	while (left < right)
	{
		//右边找小,填到坑中
		while (left < right && a[right] >= key)
		{
			right--;
		}
		a[hole] = a[right];
		hole = right;
		//左边找大,填到右边的坑中
		while (left < right && a[left] <= key)
		{
			left++;
		}
		a[hole] = a[left];
		hole = left;
	}
	
	a[hole] = key;
	return hole;
}

(3)双指针

思路:
1.cur找比key小,找到后停下来
2.++pre,交换prev位置和cur位置的值
3.等cur超出数组,我们将pre和key最后交换。在这里插入图片描述

// 快速排序前后指针法
int PartSort3(int* a, int left, int right)
{
	int mid = GetMidIndex(a, left, right);
	Swap(&a[left], &a[mid]);

	int key = left;
	int pre = left;
	int cur = left + 1;
	while (cur <= right)
	{
		if (a[cur] < a[key] && ++pre != cur)
		{
			Swap(&a[pre], &a[cur]);
		}
		cur++;

	}
	Swap(&a[pre], &a[key]);
	key = pre;
	return key;
	
}

快排的时间复杂度

先算快排的单趟,就是遍历一边数组,就是经典的O(N)。

递归的时间复杂度好像看着代码来分析
我们可以想最好情况和最坏情况分析

最好情况就是:在每次递归的时候都是在key都是在中间。
假设N个值,
这个满二叉树的高度就是 l o g 2 ( N ) log_2(N) log2(N),每一层看作N
他的时间复杂度就是 O ( N ∗ l o g ( N ) ) O(N*log(N)) O(Nlog(N))
在这里插入图片描述

最坏情况:每次选key都是最大或者最小
实际情况:有序
时间复杂度就是 O ( N 2 ) O(N^2) O(N2),就是想当于冒泡排序。
在这里插入图片描述

快排的空间复杂度

就是算我们创建了多少层栈帧,我们看上面的图知道,创建了 O ( l o g N ) O(logN) O(logN)个栈帧.我们知道栈帧是可以重复利用的。
所以我们的空间复杂度就是 O ( l o g N ) O(logN) O(logN)

快排的缺点

当我们的数据是有序,或者接近有序的,我们的快排就会一直占用函数栈帧,导致我们的栈溢出,造成极大的风险。

三数取中法

如果是有序的,我们的key就不需要动,就一直要递归,最终造成栈溢出,并且他这时的效率就会很低。
我们选中间的的数,然后跟key交换,就可以很好的解决这个问题

//三数取中
//beigin  mid end
int GetMidIndex(int* a, int begin, int end)
{
	int mid = (begin + end) / 2;
	if (a[begin] < a[mid])
	{
		if (a[mid] < a[end])
		{
			return mid;
		}
		else if (a[begin]>a[end])
		{
			return begin;
		}
		else
		{
			return end;
		}
	}
	else//begin>mid
	{
		if (a[mid] > a[end])
		{
			return mid;
		}
		else if (a[begin] < a[end])
		{
			return begin;
		}
		else
		{
			return end;
		}
	}
}
//快速排序
void QuickSort(int* a, int begin,int end)
{
	//递归结束条件
	if (begin >= end)
	{
		return;
	}
	int mid = GetMidIndex(a, begin, end);
	Swap(&a[begin], &a[mid]);

	int left = begin;
	int right = end ;
	//防止出现下面的问题,将key变成下标
	//int key = a[left];
	int key = left;
	while (left < right)
	{
		//右边先走,找小
		//加等于的意思就是我们防止等于key造成死循环
		//left<right防止我们key右边的值都大于key造成越界
		while (left<right&&a[right] >= a[key])
		{
			right--;
		}
		//左边再走,找大
		while (left < right && a[left] <= a[key])
		{
			left++;
		}
		Swap(&a[left], &a[right]);
	}
	//不能跟key换,key是一个局部变量,所以将key成下标,这样就能修改数组。
	//Swap(&a[left], &key);
	Swap(&a[left], &a[key]);
	key = left;

	//然后就分为了三个区间
	//[begin,key-1] key [key+1,end]
	QuickSort(a, begin, key - 1);
	QuickSort(a, key + 1, end);

}

加入三数取中后的时间复杂度

使用三数取中以后,快排瞬间从最坏变成最好
快排几乎不会出现最坏的情况
快排的时间复杂度就是 O ( N ∗ l o g N ) O(N*logN) O(NlogN)

小区间优化(减少递归次数)

我们当递归到最后,分割分割,假设剩10个数,光递归高度最少也得4次,而且高度越低,结点越多,假设高度是h,那最后一层的递归调用 2 h − 1 2^h-1 2h1
就是我们杀鸡用牛刀,即浪费了空间,又浪费了时间。

在这里插入图片描述

所以当数据量小的时候,我们既不用的了递归了,我们用直接插入排序。

我们的减少区间的代码就是在快速排序的递归条件中加一个条件,如果我们递归的数小于15个数,我们就用直接插入排序。

	//小区间优化,减少递归调用
	//因为是闭区间,所以要加1
	if ((end - begin + 1) < 15)
	{
		InsertSort(a + begin, end - begin + 1);
	}
	else
	{
		int key = PartSort3(a, begin, end);
		
		QuickSort(a, begin, key - 1);
		QuickSort(a, key + 1, end);
	}

快速排序(非递归)

我们的非递归就是,还是用他的单趟排序,和递归的思路还是一样,而我们的递归就是传我们的区间。

我们非递归的重点就是将区间保存下来,我们的思路就是用栈将区间保存下来,至于怎么保存,大家看看代码。重要的就是利用了栈的功能。

/快排(非递归)
void QuickSortNonR(int* a, int begin, int end)
{
	ST st;
	StackInit(&st);
	StackPush(&st, begin);
	StackPush(&st, end);
	while (!StackEmpty(&st))
	{
		int right = StackTop(&st);
		StackPop(&st);
		int left = StackTop(&st);
		StackPop(&st);
		//进行单趟排序
		int key = PartSort1(a, left, right);
		//[left,key-1][key][key+1,right]
		if (key + 1 < right)
		{
			StackPush(&st, key + 1);
			StackPush(&st, right);
		}
		if (left < key - 1)
		{
			StackPush(&st, left);
			StackPush(&st, key - 1);
		}
	}


	StackDestroy(&st);
}

归并排序

归并排序(递归)

归并的单趟思想

归并的思想
归并排序是建立在 归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。
将已有序的子序列合并,得到完全有序的序列;
即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
在这里插入图片描述

所以我们写单趟排序,就是将两个有序的数组,合并成一个有序的数组。
这就是我们的大思路。

注意:我们再归并两个数组的时候,我们不能修改原数组,如果将原本的内容覆盖,我们就不能进行比较了,所以我们创建一个临时数组,将比较的内容放到临时数组上然后在拷贝到原数组。

归并递归代码

//归并排序的子函数
void _MergeSort(int* a, int left, int right, int* tmp)
{
	//结束条件
	if (left >= right)
	{
		return;
	}

	//分割左右两部分
	int mid = (left + right) / 2;
	// [0~mid] [mid+1~right]
	
	//递归将其变有序
	_MergeSort(a, left, mid, tmp);
	_MergeSort(a, mid + 1, right, tmp);

	//这是经过递归就会产生两个有序的数组
	//而归并的单趟就是将两个有序的数组合并成一个有序的数组

	//我们就不要用上面的变量了,重新定义一个
	int begin1 = left, end1 = mid;
	int begin2 = mid + 1, end2 = right;
	int n = left;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			tmp[n++] = a[begin1++];
		}
		else
		{
			tmp[n++] = a[begin2++];
		}
	}
	while (begin1 <= end1)
	{
		tmp[n++] = a[begin1++];
	}
	while(begin2 <= end2)
	{
		tmp[n++] = a[begin2++];
	}
	memcpy(a + left, tmp + left, sizeof(int) * (right - left + 1));
}

// 归并排序递归实现
void MergeSort(int* a, int n)
{
	//创建一个数组,就是防止我们在递归的过程中产生覆盖
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	_MergeSort(a, 0, n - 1, tmp);
}

归并排序的时间复杂度和空间复杂度

时间复杂度: O ( N ∗ l o g N ) O(N*logN) O(NlogN)
空间复杂度:O(N)

空间复杂度:也要算栈帧,但是 l o g N logN logN可以忽略不记

归并排序(非递归)

我们快排,是用栈,是因为他是前序
而我们的递归是后序,就不能用这个。

我们就控制每个区间就可以归并了,因为归并是二分。
我们的区间可以很好的控制,不想快排不是二分控制不了。

但是思路简单,但是代码是什们复杂的
rangeN表示每组归并的数据个数

在这里插入图片描述
在这里插入图片描述

归并非递归代码

// 归并排序非递归实现
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	//归并每组的数据个数,从1开始,因为认为1个数是有序的,就可以直接归并
	int rangeN = 1;
	while (rangeN < n)
	{
		for (int i = 0; i < n; i += 2 * rangeN)
		{
			//[begin1,end1] [begin2,end2]
			//将上面归并的内容拿下来

			int begin1 = i, end1 = i + rangeN - 1;
			int begin2 = i + rangeN, end2 = i + 2 * rangeN - 1;
			int n = i;
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					tmp[n++] = a[begin1++];
				}
				else
				{
					tmp[n++] = a[begin2++];
				}
			}
			while (begin1 <= end1)
			{
				tmp[n++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				tmp[n++] = a[begin2++];
			}
			//归并一部分,拷贝一部分
			memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
		}
	}

	free(tmp);
	tmp = NULL;
}

非递归代码问题

我们合并两个有序数组的时候,我们需要控制区间,但是我们的两个区间并没有进行控制,就会发生溢出,导致程序的崩溃。
在这里插入图片描述

经过分析,共有三种越界
分别是:
1.end1越界,begin2越界,end2越界
2.begin2越界,end2越界
3.end2越界

非递归正确代码

// 归并排序非递归实现
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	//归并每组的数据个数,从1开始,因为认为1个数是有序的,就可以直接归并
	int rangeN = 1;

	while (rangeN < n)
	{

		for (int i = 0; i < n; i += 2 * rangeN)
		{
			//[begin1,end1] [begin2,end2]
			//将上面归并的内容拿下来
			int begin1 = i, end1 = i + rangeN - 1;
			int begin2 = i + rangeN, end2 = i + 2 * rangeN - 1;
			printf("[%d,%d][%d,%d]\n", begin1, end1, begin2, end2);
			int j = i;
			
			// end1 begin2 end2 越界
			if (end1 >= n)
			{
				break;
			}
			else if (begin2 >= n)
			{
				break;
			}
			else if (end2 >= n)
			{
				end2 = n - 1;
			}
			
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] <= a[begin2])
				{
					tmp[j++] = a[begin1++];
				}
				else
				{
					tmp[j++] = a[begin2++];
				}
			}
			
			while (begin1 <= end1)
			{
				tmp[j++] = a[begin1++];
			}
			
			while (begin2 <= end2)
			{
				tmp[j++] = a[begin2++];
						}
			//归并一部分,拷贝一部分
			memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
		}
		rangeN *= 2;
	}

	free(tmp);
	tmp = NULL;
}

排序比较

选择排序的稳定性:不稳定
举例:在这里插入图片描述

在这里插入图片描述

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

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

相关文章

Spring-AOP工作流程

Spring-AOP工作流程 3&#xff0c;AOP工作流程 3.1 AOP工作流程 由于AOP是基于Spring容器管理的bean做的增强&#xff0c;所以整个工作过程需要从Spring加载bean说起: 流程1:Spring容器启动 容器启动就需要去加载bean,哪些类需要被加载呢?需要被增强的类&#xff0c;如:B…

C++ Qt自建网页浏览器

C Qt自建网页浏览器如需安装运行环境或远程调试&#xff0c;见文章底部个人QQ名片&#xff0c;由专业技术人员远程协助&#xff01;前言这篇博客针对<<C Qt自建网页浏览器>>编写代码&#xff0c;代码整洁&#xff0c;规则&#xff0c;易读。 学习与应用推荐首选。文…

手把手教你实现书上的队列,进来试试?

一.队列的基本概念队列的定义队列&#xff08;queue&#xff09;是只允许在一端进行插入操作&#xff0c;而在另一端进行删除操作的线性表。队列是一种先进先出&#xff08;First In First Out&#xff09;的线性表&#xff0c;简称FIFO。允许插入的一端称为队尾&#xff0c;允…

如何取消电脑开机密码?1分钟可学会,快速取消

​如果小伙伴觉得每次开机都需要输入密码很烦&#xff0c;那么小伙伴应该学会如何取消电脑开机密码。本篇文章&#xff0c;小编将以图文教学的方式&#xff0c;向小伙伴介绍如何在短短的1分钟内&#xff0c;就能够取消电脑开机密码&#xff0c;让小伙伴摆脱烦人的开机密码&…

Pytorch优化器Optimizer

优化器Optimizer 什么是优化器 pytorch的优化器&#xff1a;管理并更新模型中可学习参数的值&#xff0c;使得模型输出更接近真实标签 导数&#xff1a;函数在指定坐标轴上的变化率 方向导数&#xff1a;指定方向上的变化率&#xff08;二元及以上函数&#xff0c;偏导数&am…

windows安装docker-小白用【避坑】【伸手党福利】

目录实操开启 Hyper-V 和容器特性下载docker安装dockercmd中&#xff0c;使用命令测试是否成功报错解决办法&#xff1a;下载linux模拟器wsl&#xff1a;双击打开docker重新打开cmd&#xff0c;输入命令&#xff0c;成功显示sever和clinet实操 开启 Hyper-V 和容器特性 控制面…

项目进度管理:项目经理应该怎么做?

项目经理的职责是非常清晰的、界面分明的。项目经理经常忙碌的原因是&#xff0c;缺乏规划&#xff0c;觉得很多业务都跟自己相关&#xff0c;但不知道到底要做哪些工作&#xff0c;没有把多个角色分清楚。 1、目标。 项目目标是实施项目所要达到的期望结果&#xff0c;一个明…

基于嵌入式linux的OpenSSL源码移植(基于arm64)

SSL是Secure Sockets Layer&#xff08;安全套接层协议&#xff09;的缩写&#xff0c;可以在Internet上提供秘密性传输。Netscape公司在推出第一个Web浏览器的同时&#xff0c;提出了SSL协议标准。其目标是保证两个应用间通信的保密性和可靠性,可在服务器端和用户端同时实现支…

类和对象(一)

类和对象&#xff08;一&#xff09; C并不是纯面向对象语言 C是面向过程和面向对象语言的&#xff01; 面向过程和面向对象初步认识&#xff1a; C语言是面向过程的&#xff0c;关注的是过程&#xff0c;分析出求解问题的步骤&#xff0c;通过函数调用逐步解决问题。 C是基…

驱动程序开发:FTP服务器和OpenSSH的移植与搭建、以及一些笔记

目录一、FTP服务器移植与搭建1、在ubuntu下安装vsftpd2、在window下安装FileZilla3、移植vsftpd到开发板上4、Filezilla 连接测试5、注意点二、开发板 OpenSSH 移植与使用1、移植 zlib 库2、移植 openssl 库3、移植 openssh 库4、openssh 使用测试三、关于u-boot上的操作及根文…

数据表(二) - 数据表的制作方式

本篇来介绍下数据表的几种制作数据的方式。Excel是大部分数值策划选择用的填数工具&#xff0c;因为Excel是天生为数据处理而生&#xff0c;而Excel转为什么格式就需要选择了。最简单的就是直接将Excel里的数据复制黏贴到文本文件作为游戏数据。这种简单快捷的方式任何人都能做…

DatenLord前沿技术分享 No.20

达坦科技专注于打造新一代开源跨云存储平台DatenLord&#xff0c;致力于解决多云架构、多数据中心场景下异构存储、数据统一管理需求等问题&#xff0c;以满足不同行业客户对海量数据跨云、跨数据中心高性能访问的需求。喷泉码具有极高的纠错能力&#xff0c;且具有低延迟、地复…

CnOpenData·A股上市企业数字化转型指数数据

一、数据简介 企业数字化转型是近年来中国社会各界重点关注的领域&#xff0c;但基础数据的不完善在很大程度上制约了相关科学研究的开展。构建合理、科学的数字化转型指标体系有利于学者定量地研究企业数字化的相关问题&#xff0c;也有利于衡量企业的数字化水平。广东金融学院…

Linux驱动开发

一、驱动分类Linux中包含三大类驱动&#xff1a;字符设备驱动、块设备驱动和网络设备驱动。其中字符设备驱动是最大的一类驱动&#xff0c;因为字符设备最多&#xff0c;从led到I2C、SPI、音频等都属于字符设备驱动。块设备驱动和网络设备驱动都要比字符设备驱动复杂。因为其比…

标度不变性(scale invariance)与无标度(scale-free)概念辨析

文章目录标度标度种类名义标度序级标度等距标度比率标度常用标度方法不足标度不变性标度不变&#xff08;Scale-invariant&#xff09;曲线和自相似性&#xff08;self-similarity&#xff09;射影几何分形随机过程中的标度不变性标度不变的 Tweedie distribution普适性&#x…

蓝牙及其安全技术概述

作者 | 陆杰 上海控安可信软件创新研究院研发工程师 来源 | 鉴源实验室 01 背 景 汽车已成为现代社会生活不可或缺的一部分。车辆蓝牙[1]安全非常重要&#xff0c;因为未经保护的蓝牙连接可能会被黑客利用来获取车辆的敏感信息、控制车辆等&#xff0c;从而对车辆的安全和车主…

12N65-ASEMI高压MOS管12N65

编辑-Z 12N65在TO-220封装里的静态漏极源导通电阻&#xff08;RDS(ON)&#xff09;为0.68Ω&#xff0c;是一款N沟道高压MOS管。12N65的最大脉冲正向电流ISM为48A&#xff0c;零栅极电压漏极电流(IDSS)为10uA&#xff0c;其工作时耐温度范围为-55~150摄氏度。12N65功耗&#x…

【项目精选】基于Java的愤怒的小鸟游戏的设计与实现(视频+论文+源码)

点击下载源码 基本功能包括&#xff1a;新游戏、载入游戏、控制帮助、退出游戏等。本系统结构如下&#xff1a; &#xff08;1&#xff09;新游戏&#xff1a; 需要输入你的昵称&#xff1b; 选择难度&#xff1a;容易、中等、困难、噩梦(每个级别都有5个关卡) &#xff08;2&…

【Fabric 超级账本学习【3】Fabric2.4 使用Tape进行吞吐量量性能测试】

如果想测试一下超级账本fabric对某个合约函数的执行时间是多少&#xff0c;简单地可以通过打印合约函数开始执行时间和结束执行时间来计算时间差就可以了。 Tape 是一款轻量级 Hyperledger Fabric 性能测试工具。 tape的github地址&#xff1a;https://github.com/Hyperledge…

Spark的基本概念与架构

一、Spark简介 Spark 是一种与 Hadoop 相似的开源集群计算环境&#xff0c;但是两者之间还存在一些不同之处&#xff0c;这些有用的不同之处使 Spark 在某些工作负载方面表现得更加优越&#xff0c;换句话说&#xff0c;Spark 启用了内存分布数据集&#xff0c;除了能够提供交…