数据结构之八大排序算法

发布于:2025-02-11 ⋅ 阅读:(9) ⋅ 点赞:(0)

前言:各位铁子们好啊,博客已经好久没有更新了。今天就来看看新的文章吧。

在日常生活中,我们能够发现在许多地方会存在排序的问题。比如学校排名,成绩排名,手机销量排名等等。而常见的排序有八种,我们一起来看看都有哪八种排序算法。

在这里插入图片描述

1. 直接插入排序

直接插入排序的基本思想是将待排序的关键码值按照大小插入到一个已经有序的序列中,直到将所有的关键码值插入完为止,得到一个新的有序序列

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

在这里插入图片描述

若end>=0,说明应该继续比较当前end所处位置的关键码值与待排序
关键码值的大小关系。若end位置的关键码值大,就将end位置的关键
码值往后移一步,继续--end,重复上述步骤,直到end位置的关键码
值小于等于待排序的关键码值,跳出循环。此时end+1的位置就是待
排序关键码值应该插入的位置。

2. 希尔排序

希尔排序是对于直接插入排序的一种优化,又叫做缩小增量法希尔排序的基本思想是先选定一个整数(通常是gap=gap/3+1),把待排序文件所有记录分成各组,所有距离相等的记录分在同一组内,并对每一组内的记录进行排序,然后gap=gap/3+1得到下一个整数,再将数组分成各组,进行插入排序。当gap=1时,就相当于直接插入排序

void ShellSort(int* arr, int n)
{
	int gap = n;
	//gap > 1都是预排序,目的是为了让数组更接近有序
	while(gap > 1)
	{
	//组数越多,循环次数多
	//组数越少,每组内比较的次数增多
	//3是一个折中的取法
	//+1(只有一组,相当于直接插入排序)是为了保证最后一组数据是有序的
		gap = gap / 3 + 1;
		for (int j = 0; j < n; ++j)
		{
			int tmp = arr[j];
			int end = j - gap;
			while (end >= 0)
			{
				if (arr[end] > tmp)
				{
					arr[end + gap] = arr[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			arr[end + gap] = tmp;
		}
	}
}

在这里插入图片描述

对待排序文件记录进行分组,对每一组的记录先进行排序,若
arr[end]>tmp,将当前end位置的记录往后移,更新end,继续判断
end位置的关键码值与tmp的大小关系,若arr[end]<tmp,就跳出循
环,此时end+gap就是待排序关键码值要插入的位置,再重新分
组,重复上述步骤。

希尔排序特性总结

1.希尔排序是对直接插入排序的优化

2.gap>1都是预排序,目的是为了让数组更加接近有序,gap=1,就相当于直接插入排序

3. 直接选择排序

1.在元素集合arr[i]...arr[n-1]选择最小(或最大)的元素
2.若它不是这组元素中的第一个(或最后一个)元素时,就将它与这
组元素中的第一个(或最后一个)元素进行交换。
3.在剩余的元素集合中arr[i+1]...arr[n-2],重复上述步骤,直到集合剩
余一个元素
//时间复杂度O(N^2)
//直接选择排序
void SelectSort(int* arr, int n)
{
	int begin = 0, end = n - 1;
	while (begin < end)
	{
		//最小值和最大值都要从begin位置开始找起,因为begin位置的元素有可能就是最大值或最小值
		int mini = begin, maxi = begin;
		//找最大,小值
		for (int i = begin + 1; i <= end; ++i)
		{
			if (arr[i] > arr[maxi])
			{
				maxi = i;
			}
			if (arr[i] < arr[mini])
			{
				mini = i;
			}
		}
		if (maxi == begin)
		{
			maxi = mini;
		}
		swap(&arr[begin], &arr[mini]);
		swap(&arr[maxi], &arr[end]);
		++begin;
		--end;
	}
}

在这里插入图片描述


在这里插入图片描述

从begin~end区间内mini找最小值,maxi找最大值,找到后mini位置
的元素与begin位置的元素进行交换,maxi与end元素进行交换。

4. 堆排序

堆排序是一种利用堆这种数据结构的排序算法升序建大堆,降序建小堆

void AdjustDown(int* arr, int parent, int n)
{
	//左孩子
	int child = 2 * parent + 1;
	while (child < n)
	{
		//右孩子大,++child
		if (child + 1 < n && arr[child + 1] > arr[child])
		{
			++child;
		}
		//孩子节点大于父节点
		if (arr[child] > arr[parent])
		{
			swap(&arr[child], &arr[parent]);
			parent = child;
			child = 2 * parent + 1;
		}
		else
		{
			break;
		}
	}
}
//堆排序
void HeapSort(int* arr, int n)
{
	for (int i = (n - 1 - 1) / 2; i >= 0; --i)
	{
		AdjustDown(arr, i, n);
	}
	int end = n - 1;
	while (end > 0)
	{
		swap(&arr[0], &arr[end]);
		AdjustDown(arr, 0, end);
		--end;
	}

在这里插入图片描述


在这里插入图片描述

从最后一棵子树开始进行向下调整算法,直到根节点调整完毕,此时
为一个有效的堆。再将根节点与最后一个节点进行交换,调整堆,删
除最后一个节点。重复上述步骤,直至元素有序。

5. 归并排序

归并排序采用的是分治法,是将已有序的子序列进行合并,得到一个完全有序的序列

void _MergeSort(int* arr, int left, int right, int* tmp)
{
	if (left >= right)
	{
		return;
	}
	int mid = ((right - left) >> 1) + left;
	//划分左右区间
	//左区间  [left,mid]
	//右区间  [mid + 1,right]
	_MergeSort(arr, left, mid, tmp);
	_MergeSort(arr, mid + 1, right, tmp);
	//进行合并
	int begin1 = left, end1 = mid;
	int begin2 = mid + 1, end2 = right;
	int index = begin1;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (arr[begin1] > arr[begin2])
		{
			tmp[index++] = arr[begin2++];
		}
		else
		{
			tmp[index++] = arr[begin1++];
		}
	}
	//若某子数组有剩余元素,则将该数组剩余元素依次填充
	while (begin1 <= end1)
	{
		tmp[index++] = arr[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[index++] = arr[begin2++];
	}
	for (int i = left; i <= right; ++i)
	{
		arr[i] = tmp[i];
	}
}
//归并排序
void MergeSort(int* arr, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	_MergeSort(arr, 0, n - 1, tmp);
	free(tmp);
}

在这里插入图片描述

对一个序列不断地进行划分左右区间,直到不能划分为止,再对子区
间依次进行排序,合并即可。

6. 计数排序(非比较排序)

计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用,操作步骤:

1.统计相同元素出现次数

2.根据统计的结果将序列回收到原来的序列中

//计数排序
void CountSort(int* arr, int n)
{
	//求数组内最大,最小值
	int max = arr[0], min = arr[0];
	for (int i = 0; i < n; ++i)
	{
		if (arr[i] > max)
		{
			max = arr[i];
		}
		if (arr[i] < min)
		{
			min = arr[i];
		}
	}
	//开空间
	int gap = max - min + 1;
	int* count = (int*)calloc(sizeof(int), gap);
	//统计每一个元素出现的次数
	for (int i = 0; i < n; ++i)
	{
		count[arr[i] - min]++;
	}
	//排序
	int index = 0;
	for (int i = 0; i < gap; ++i)
	{
		while (count[i]--)
		{
			arr[index++] = i + min;
		}
	}
	free(count);
}

在这里插入图片描述

之所以开空间没有按照元素的个数去开空间,是因为如果元素个数非
常庞大的情况下,可能会申请失败,浪费空间。因此对原数组进行遍
历,求数组内的最大值和最小值,再开辟空间。统计原数组内每一个
元素出现的次数,根据统计的结果对序列进行排序

7. 快速排序

快速排序的基本思想任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后对左右子序列重复该过程

7.1 hoare版本的快速排序

思路

1.创建左右指针,确定基准值

2.从右往左找比基准值小的,从左往右找比基准值大的,左右指针数据交换,进行下次循环

//hoare版本
int _QuickSort(int* arr, int left, int right)
{
	int key = left;
	//如果不++left,那么当right指向的数据都大于key(基准
	//值)时,会存在越界问题
	++left;
	while (left <= right)
	{
		//从右往左找比基准值小的
		while (left <= right && arr[right] > arr[key])//arr[right] == arr[key]要不要交换
		{
			--right;
		}
		//从左往右找比基准值大的
		while (left <= right && arr[left] < arr[key])
		{
			++left;
		}
		if (left <= right)
		{
			swap(&arr[left++], &arr[right--]);
		}
	}
	swap(&arr[right], &arr[key]);
	return right;
}

在这里插入图片描述

right指针从右往左找比基准值小的数据,left指针从左往右找比基准值
大的数据,左右指针数据进行交换,继续重复上述步骤。跳出循环之
后,right指向的位置就是基准值的位置。

7.2 挖坑法

思路

创建左右指针。首先从右往左找比基准值小的数据,找到后放入左边坑中,当前位置变为新的“坑”;然后从左往右找比基准值大的数据,找到后放入右边坑中,当前位置变为新的“坑”,结束循环后,将基准值放入当前“坑”中,返回当前“坑”下标

//挖坑法
int _QuickSort1(int* arr, int left, int right)
{
	int key = arr[left];
	int hole = left;
	while (left < right)
	{
		//从右往左找比基准值小的
		while (left < right && arr[right]  >= key)
		{
			--right;
		}
		arr[hole] = arr[right];
		hole = right;
		//从左往右找比基准值大的
		while (left < right && arr[left] <= key)
		{
			++left;
		}
		arr[hole] = arr[left];
		hole = left;
	}
	arr[hole] = key;
	return hole;
}

在这里插入图片描述


在这里插入图片描述

7.3 lomuto前后指针法

思路
创建前后指针,从左往右找比基准值小的数据进行交换,使小的数据都排在基准值左边

int _QuickSort2(int* arr, int left, int right)
{
	int key = left;
	int prev = left, cur = prev + 1;
	while (cur <= right)
	{
		//从左往右找比基准值小的数据
		if (arr[cur] < arr[key] && ++prev != cur)
		{
			swap(&arr[prev], &arr[cur]);
		}
		++cur;
	}
	swap(&arr[prev], &arr[key]);
	return prev;
}
//快速排序
void QuickSort(int* arr, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int key = _QuickSort(arr, left, right);
	
	//划分左右序列
	QuickSort(arr, left, key - 1);
	QuickSort(arr, key + 1, right);
}

在这里插入图片描述

基准值确定之后,在对原数组进行左右区间划分,重复上述步骤

7.4 非递归版本的快速排序

//非递归版本(栈实现)
void QuickSortNonR(int* arr, int left, int right)
{
	Stack s;
	//栈初始化
	StackInit(&s);
	//栈顶插入数据
	StackPush(&s, right);
	StackPush(&s, left);
	//判断栈是否为空
	while (!StackEmpty(&s))
	{
		//取栈顶数据
		int begin1 = StackTop(&s);
		StackPop(&s);
		int end1 = StackTop(&s);
		StackPop(&s);

		int key = begin1;
		int prev = begin1;
		int cur = prev + 1;
		while (cur <= end1)
		{
			if (arr[cur] < arr[key] && ++prev != cur)
			{
				swap(&arr[cur], &arr[prev]);
			}
			++cur;
		}
		swap(&arr[prev], &arr[key]);
		if (prev + 1 < end1)
		{
			StackPush(&s,end1);
			StackPush(&s, prev + 1);
		}
		if (begin1 < prev - 1)
		{
			StackPush(&s, prev - 1);
			StackPush(&s, begin1);
		}
	}
	StackDestroy(&s);
}

先入右区间的左右端点,再入左区间的左右端点,因为栈遵从先入后出的原则