【排序算法】

发布于:2025-07-05 ⋅ 阅读:(17) ⋅ 点赞:(0)

1.冒泡排序

Alt 冒泡排序:两两之间互换比较交换,比较一趟能确定一个数的位置。假设有n个数,则需要比较n-1趟,就能确定每一个数的位置,第一趟下标范围为[0,n-2],第二趟下标范围为[0,n-3],…则可以写成代码。

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

alt

2.插入排序

Alt

 先写排好一个数字,假设[0,end]是一个有序数组,则下一个数用tmp保存,tmp =a[end+1],如果tmp<a[end],a[end]的数移到a[end+1],如果tmp>a[end],把tmp放到下标为end的位置。
 由此看来,end是有序数组最后一个数的下标,tmp保存的是下一个要放的数,一个无序数组,则end先指向第一个数,tmp保存第二个数,根据上述获得这两个数的正确排序,则end = n-2,tmp = a[end-1]时,进行最后一次排序,整个数组就排好序了。

//O(N) = O(N^2)
void InsertSort(int* a,int n)
{
	//
	//[0,end] end+1
	for (int i = 0; i < n-1; i++)
	{
		int end = i;
		int tmp = a[end + 1];
		while (end >= 0)
		{
			if (a[end] > tmp)
			{
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}
		}
		a[end + 1] = tmp;
	}
}

直接插入排序的特性总结:
1. 元素集合越接近有序,直接插入排序算法的时间效率越高
2. 时间复杂度:O(N^2)
3. 空间复杂度:O(1),它是一种稳定的排序算法
4. 稳定性:稳定

3.希尔排序

下面分的间隔gap=3,分成3组。
alt

 希尔排序在插入排序的基础上升级了一点,把一个无序数据按gap(间隔)分成gap个数组,对这gap个数组分别进行插入排序,先把gap的值取大一点,把一个组小的数排到前面,大的数排到后面,gap进行不断变化取值,当gap = 1时,就是插入排序,排序完成。

 先进行排第一个组排序如下。。

//[0,end]有序  tmp保存下一个数
	int gap = 3;  //把一个数组分成3个数组
	for(int i = 0;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;
	}

 对gap组数组进行排序,并且gap取值变化,当gap = 1时,完成排序。

void ShellSort(int*a ,int n)
{
	int gap = n;
	while(gap>0)
	{
		//[0,end]有序  tmp保存下一个数
		gap /= 2;  //把一个数组分成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;
			}
		}
	}
}

 时间复杂度:O(N) = N^(1.3)
 大致分析如下,不会算!
Alt
 按照gap = n,gap = gap/3来大致计算时间复杂度。
 第一次分组,gap = n/3,则把n个数据排成n/3个数组,每个数组的间隔为gap,每个数组的大小为3个数据,每个数组排序最坏情况移动3次,则移动次数为:3*(n/3) = n次。

… … … … …

最后一次,gap = 1,数组经过多轮预排之后,已经非常接近有序,大致移动n次就可以使数组有序。

4.选择排序

 先确定区间[begin,end],然后再这个区间找最大值,最小值,进行交换,begin++,end- -,缩小区间。

void swap(int*px,int*py)
{
	int tmp =*px;
	*px = *py;
	*py = tmp;
}
void SelectSort(int*a,int n)
{
	int begin = 0, end = n - 1;
	while (begin < end)
	{
		int mini = begin, maxi = begin;
		for (int i = begin + 1; i <= end; i++)
		{
			if (a[i] < a[mini])
			{
				mini = i;
			}
			if (a[i] > a[maxi])
			{
				maxi = i;
			}
		}
		swap(&a[begin], &a[mini]);
		if (maxi == begin)
		{
			maxi = mini;
		}
		swap(&a[end], &a[maxi]);
		begin++;
		end--;
	}

}

5.快排

 hoare版本。
 思路:快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

Alt

 取key永远为子序列最左边的一个数,定义left指向子序列最左边,right指向子序列最右边,先走最右边,找比key小的值,在走左边,找比keyi大的值,找到之后,两者交换,交换之后,比key小的在左边,比key大的在右边。left与right相遇则结束,然后key与相遇位置的数交换,key已经放到它应有的位置,分割区间[begin,keyi-1][keyi][keyi+1,end],进行递归遍历,放好每一个数。

 具体过程如下:
Alt

 通过写这个算法,明白还是提高代码组织能力,一个算法有了思路,怎么用代码实现的问题。

void QuickSort(int* a, int left,int right)
{
	if (left >= right)
		return;
	int begin = left;
	int end = right;
	int keyi = left;
	while (left < right)
	{
		//先走右边,右边找比a[key]小的
		while (left < right && a[keyi] >= a[right])
		{
			right--;
		}
		//在走左边,左边找比a[key]大的
		while (left < right && a[keyi] <= a[left])
		{
			left++;
		}
		swap(&a[left], &a[right]);
	}
	swap(&a[keyi], &a[left]);
	keyi = left;
	//[begin,keyi-1]keyi[keyi+1,end]
	QuickSort(a, begin,keyi-1);
	QuickSort(a, keyi+1,end);
}

 理解为啥相遇位置的值比key小?
alt
 时间复杂度分析:
Alt

 时间复杂度一般情况下O(N) = N* log ⁡ 2 ( N ) \log_{2}(N) log2(N)
 最坏情况下:为有序,O(N) = N^2.


 快排优化,快排时间复杂度高的原因是数据有序,第一个元素是最小的。
第一种方式随机数选keyi,使得第一个数不是最小的(升序)。具体如下。

 int randi = rand()%(right-left+1)+left;
 swap(&a[left],&a[randi]);
 其实上面这种优化,我觉得还是不靠谱,你怎么能保证你随机选的keyi就不是最小值呢?

void QuickSort(int*a ,int left,int right)
{
	if(left>=right)
		return;
	int begin = left,end = right;
	//随机数选keyi
	int randi = rand()%(right-left+1)+left;
	Swap(&a[left],&a[randi]);
	int keyi = left;
	while(left<right)
	{
		//先走右边,找比a[keyi]小的值
		while(left<right && a[keyi]<=a[right])
		{
			right--;
		}
		//在走左边,找比a[keyi]大的值
		while(left<right && a[keyi]>=a[left])
		{
			left++;
		}
		Swap(&a[left],&a[right]);
	}
	Swap(&a[keyi],&a[left]);
	keyi = left;
	//[begin,keyi-1] keyi [keyi+1,end]
	QuickSort(a,begin,keyi-1);
	QuickSort(a,keyi+1,end);
}

 第二种优化方式三数取中放keyi位置,这个方法比上面好。定义int GetMid(int *a,int left,int right)取每个区间的最左边值和最右边值,中间值,算出大小在中间的值做keyi。

//三数取中,left,mid,right
//大小居中的值,也就是不是最大也不是最小的
int  GetMidi(int* a, int left, int right)
{
	int midi = (left + right) / 2;
	if (a[left] < a[midi])
	{
		if (a[midi] < a[right])
		{
			return midi;
		}
		else if (a[left] > a[right]) //a[midi]>a[right]
		{
			return left;
		}
		else
		{
			return right;
		}
	}
	else  //a[left]>a[midi]
	{
		if (a[midi]>a[right])
		{
			return midi;
		}
		else if (a[left] < a[right])  //a[midi]<a[right]
		{
			return left;
		}
		else
		{
			return right;
		}
	}
}

void QuickSort(int*a ,int left,int right)
{
	if(left>=right)
		return;
	int begin = left,end = right;
	//随机数选keyi
//	int randi = rand()%(right-left+1)+left;
//	Swap(&a[left],&a[randi]);
	int midi = GetMidi(a,left,right);
	Swap(&a[left],&a[midi]);
	int keyi = left;
	while(left<right)
	{
		//先走右边,找比a[keyi]小的值
		while(left<right && a[keyi]<=a[right])
		{
			right--;
		}
		//在走左边,找比a[keyi]大的值
		while(left<right && a[keyi]>=a[left])
		{
			left++;
		}
		Swap(&a[left],&a[right]);
	}
	Swap(&a[keyi],&a[left]);
	keyi = left;
	//[begin,keyi-1] keyi [keyi+1,end]
	QuickSort(a,begin,keyi-1);
	QuickSort(a,keyi+1,end);
}

 第三种小区间优化,剩余10个数直接进行插入排序。这种写法递归下去之后在<10个数的区间,直接进行插入排序,不需要返回条件,程序执行完自动返回。

void QuickSort(int*a ,int left,int right)
{
	if(right-left+1<10)
	{
		InsertSort(a+left,right-left+1);
	}
	else
	{
		int begin = left,end = right;
		int keyi = left;
		while(left<right)
		{
			//先走右边,找比a[keyi]小的值
			while(left<right && a[keyi]<=a[right])
			{
				right--;
			}
			//在走左边,找比a[keyi]大的值
			while(left<right && a[keyi]>=a[left])
			{
				left++;
			}
			Swap(&a[left],&a[right]);
		}
		Swap(&a[keyi],&a[left]);
		keyi = left;
		//[begin,keyi-1] keyi [keyi+1,end]
		QuickSort(a,begin,keyi-1);
		QuickSort(a,keyi+1,end);
	}
}

 第二种写快排的方法:前后指针法。
在这里插入图片描述
alt
 定义两个指针,prev指向第一个数,cur指向第二个数,如果a[cur]<key,则prev++,然后prev与cur交换,在cur++,如果a[cur]>=key,则cur++,最后cur++出界,循环结束,prev的位置就是key值的位置,至于为什么这么操作,就把key值位置找到了,我也不知道。

void QuickSort(int*a,int left,int right)
{
	if(left>=right)
		return;
	int prev = left;
	int cur = left+1;
	int keyi =left;
	while(cur<=right)
	{
		if(a[cur]<a[keyi])
		{
			++prev;
			Swap(&a[cur],&a[prev]);
			++cur;
		}
		else{
			++cur;
		}
	}
	Swap(&a[keyi],&a[prev]);
	keyi = prev;
	//[left,keyi-1]keyi[keyi+1,righgt]
	QuickSort(a,left,keyi-1);
	QuickSort(a,keyi+1,right);
}

 模拟递归过程,使用栈来实现快排。
Alt

 模拟递归过程,写快排的关键是搞区间,区间确定了,搞一个单趟排就可以了,先把第一个区间[lefe,right]入栈,再走一个单趟排,则一个数字位置已确定,并且分割出两个区间[begin,keyi-1]keyi[keyi+1,end],两个区间在走个单趟,依次循环。

void QuickSortNonR(int*a,int left,int right)
{
	ST st;
	STInit(&st);
	STPush(&st,right);
	STPush(&st,left);
	while(!STEmpty(&st))
	{
		int begin = STTop(&st);
		STPop(&st);
		int end = STTop(&st);
		STPop(&st);
		//先把一趟排好
		int prev = begin;
		int cur = begin+1;
		int keyi =begin;
		while(cur<=right)
		{
			if(a[cur]<a[keyi])
			{
				++prev;
				Swap(&a[cur],&a[prev]);
				++cur;
			}
			else{
				++cur;
			}
		}
		Swap(&a[keyi],&a[prev]);
		keyi = prev;
		//[begin,keyi-1] keyi [keyi+1,end]
		if(keyi+1<end)
		{
			STPush(&st,end);
			STPush(&st,keyi+1);
		}
		if(begin<keyi-1)
		{
			STPush(&st,keyi-1);
			STPush(&st,begin);
		}
	}
	STDestory(&st);
}

6.归并排序

 基本思想:归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤:
alt
 思路:归并排序是走的二叉树的后序,而快排是走的二叉树的前序,归并排序是先把左子树、右子树、遍历到底部,只剩下一个数,返回时,进行比较,进而进行归并。
Alt

void _MergeSort(int* a, int begin, int end, int* tmp)
{
	if (begin == end)
		return;
	//后序来搞,分区间
	int mid = (begin + end) / 2;
	//[begin,mid][mid+1,end]
	printf("[%d,%d]-[%d,%d]\n", begin, mid, mid + 1, end);
	_MergeSort(a, begin, mid, tmp);
	_MergeSort(a, mid + 1, end, tmp);
	//大小处理,两个区间处理排序
	int begin1 = begin, end1 = mid;
	int begin2 = mid + 1, end2 = end;
	int i = begin1;
	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++];
	}
	memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
	PrintArray(a + begin, end - begin + 1);
}
void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		return;
	}
	_MergeSort(a, 0, n - 1, tmp);
}

 时间复杂度:O(N) = N* log ⁡ 2 ( N ) \log_{2}(N) log2(N)
归并排序循环实现
 一点点写这个算法,慢慢来感受怎么组织程序。
alt

 怎么弄呢?递归实现归并排序,用的是后序,那么通过循环实现归并,则要从正面出发来搞这个排序。
 首先把数组中的每一个数据看作是一个数组,也就是分割,gap = 1来分割,分割之后,两个数组之间进行排序,每两个一组排好序,gap = 2,再次排序。直到完成这个排序。
 先写第一次分割的两个数组的排序

int begin1 = 0, end1 = begin1 + gap - 1;
int begin2 = end1 + 1, end2 = begin2 + gap - 1;
int i = begin1;
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++];
}
memcpy(a, tmp , sizeof(int) * (end2 + 1));

 在写一次全部分割的排序

for (int j = 0; j < n; j += gap * 2)
{
	int begin1 = j, end1 = begin1 + gap - 1;
	int begin2 = end1 + 1, end2 = begin2 + gap - 1;
	int i = begin1;
	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++];
	}
	memcpy(a + j, tmp + j, sizeof(int) * (end2 - j + 1));
}

 整体来写

void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		return;
	}
	int gap = 1;
	while (gap < n)
	{
		for (int j = 0; j < n; j += gap * 2)
		{
			int begin1 = j, end1 = begin1 + gap - 1;
			int begin2 = end1 + 1, end2 = begin2 + gap - 1;
			int i = begin1;
			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++];
			}
			memcpy(a + j, tmp + j, sizeof(int) * (end2 - j + 1));
		}
		gap *= 2;
	}
	free(tmp);
	tmp = NULL;

}

 边界处理
//特殊情况矫正
//1.如果begin1越界,不用管,前面已经配对成对。
//2.如果end1越界,[begin1,end1),后面没有配对的另一个数组,不用归并
if(end1>=n)
break;
//3.如果begin2越界,[begin1,end1]已经排好序,不用归并,end1后面就没有归并的数组
if(begin1>=n)
break;
//4.如果end2越界,[begin2,end2)还有数据,一定要归并,只不过弄好数据范围
if(end2>=n)
end2 = n-1;

void MergeSortNonR(int*a,int n)
{
	int* tmp = (int*)malloc(sizeof(int)*n);
	if(tmp == NULL)
	{
		perror("malloc fail");
		return ;
	}
	int gap = 1;
	while(gap<n)
	{
		for(int j = 0;j<n;j += gap*2)
		{
			int begin1 = j,end1 =begin1+gap-1;
			int begin2 =end1+1,end2 = begin2+gap-1;
			int i = begin1;
			//特殊情况矫正
			//1.如果begin1越界,不用管,前面已经配对成对。
			//2.如果end1越界,[begin1,end1),后面没有配对的另一个数组,不用归并
			if(end1>=n)
				break;
			//3.如果begin2越界,[begin1,end1]已经排好序,不用归并,end1后面就没有归并的数组
			if(begin1>=n)
				break;
			//4.如果end2越界,[begin2,end2)还有数据,一定要归并,只不过弄好数据范围
			if(end2>=n)
				end2 = n-1;
			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++];
			}
			memcpy(a+j,tmp+j,sizeof(int)*(end2-j+1));
		}
		gap *= 2;
	}
	free(tmp);
	tmp  = NULL;
	
}

7、计数排序

算法思想:用相对映射来搞
alt

//时间复杂度:O(N+range)
//空间复杂度:O(range)
void CountSort(int* a, int n)
{
	int max = a[0], min = a[0];
	for (int i = 0; i < n; i++)
	{
		if (a[i] > max)
			max = a[i];
		if (a[i] < min)
			min = a[i];
	}
	int range = max - min + 1;
	int* count = (int*)malloc(sizeof(int) * (range));
	if (count == NULL)
	{
		perror("malloc fail");
		return;
	}
	memset(count, 0, sizeof(int) * (range));

	//统计次数,相对映射
	for (int i = 0; i < n; i++)
	{
		count[a[i] - min]++;
	}
	
	//排序
	int j = 0;
	for (int i = 0; i <(range); i++)
	{
		while(count[i] > 0)
		{
			a[j++] = min + i;
			count[i]--;
		}
	}
	free(count);
	count = NULL;
}

8、各个排序算法比较情况

Alt


 完结!