数据结构 - 排序


前言

本文会介绍一些常见的排序算法。


提示:以下是本篇文章正文内容,下面案例可供参考

一、排序的概念

排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:在排序过程中,碰到一样的元素,在排序完成后,相同元素的前后位置没有改变就是稳定的,反之不稳定。
内部排序:数据元素全部放到内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。

二、插入排序

1.直接插入排序

思想:把前面的数当成排好的序列,然后后面的数字跟前面的序列比较,小于它往前面插,大于就指向下一个数字。

void InsertSort(int* a, int n)
{
	for (int i = 0; i < n - 1; i++)
	{
		int end = i;
		int tmp = a[end + 1];
		while (end >= 0)
		{
			//找到比tmp小的值,跳出循环
			if (tmp < a[end])
			{
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}
		}
		//然后再tmp插入到,end的下一个位置
		a[end + 1] = tmp;
	}
}

时间复杂度:O(N^2)
空间复杂度:O(1)
稳定性:稳定

2.希尔排序

对数据先进行预排序,让它接近有序,然后再直接插入排序

void ShellSort(int* a, int n)
{
	int gap = n;		//步长
	while (gap > 1)
	{
		gap = gap / 3 + 1;
		for (int i = 0; i < n - gap; i++)
		{
			int end = i;
			int tmp = a[end + gap];
			while (end >= 0)
			{
				//找到比tmp小的值,跳出循环
				if (tmp < a[end])
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			//然后再tmp插入到,end的下一个位置
			a[end + gap] = tmp;
		}
	}
}

时间复杂度:O(N^1.3)
空间复杂度:O(1)
稳定性:不稳定

三、选择排序

1.直接选择排序

依次选出最大的和最小的,放到对应的位置上

void Swap(int* a, int* b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}
void SelectSort(int* a, int n)
{
	int left = 0;
	int right = n - 1;
	while (left < right)
	{
		int min = left, max = right;
		for (int i = left; i <= right; i++)
		{
			if (a[i] < a[min])
				min = i;
			if (a[i] > a[max])
				max = i;
		}
		//找出最大的和最小的,然后交换
		Swap(&a[left], &a[min]);
		//如果最大的元素,在left位置上,要换一下
		if (left == max)
		{
			max = min;
		}
		Swap(&a[max], &a[right]);
		right--;
		left++;
	}
}

时间复杂度:O(N^2)
空间复杂度:O(1)
稳定性:不稳定

2.堆排序

void AdjustDown(int* a, int parent, int n)
{
	int child = 2 * parent + 1;
	while (child < n)
	{
		if (child + 1 < n && a[child + 1] > a[child])
			child++;
		if (a[parent] < a[child])
			Swap(&a[parent], &a[child]);
		parent = child;
		child = parent * 2 + 1;
	}
}

void HeapSort(int* a, int n)
{
	//从最小的孩子开始建堆,建大堆
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, i, n);
	}

	int end = n-1;
	while (end >= 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, 0, end);
		end--;
	}
}

时间复杂度:O(N*logN)
空间复杂度:O(1)
稳定性:不稳定

四、交换排序

1.冒泡排序

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

时间复杂度:O(N^2)
空间复杂度:O(1)
稳定性:稳定

2.快速排序

基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止

//左右指针法
int PartSort1(int* a, int left, int right)
{
	int keyi = left;
	while (left < right)
	{
		while (left < right && a[right] >= a[keyi])
			right--;
		
		while (left < right && a[left] <= a[keyi])
			left++;
		Swap(&a[left], &a[right]);
	}
	Swap(&a[keyi], &a[left]);
	return left;
}
//挖坑法
int PartSort2(int* a, int left, int right)
{
	int keyi = a[left];
	while (left < right)
	{
		while (left < right && a[right] >= a[keyi])
			right--;
		a[left] = a[right];
		while (left < right && a[left] <= a[keyi])
			left++;
		a[right] = a[left];
	}
	a[left] = keyi;
	return left;
}
//前后指针法
int PartSort3(int* a, int left, int right)
{
	int prev = left, cur = left + 1;
	int keyi = left;
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++prev != cur)
			Swap(&a[cur], &a[prev]);

		cur++;
	}
	Swap(&a[keyi], &a[prev]);
	return prev;
}

void QuickSort(int* a, int left,int right)
{
	if (left >= right)
		return;

	int keyi = PartSort3(a, left, right);
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}

优化

优化:如果keyi取不好,时间复杂度可能会达到O(N^2)
1.三数取中法:找到一个中间值作为keyi
2.递归到小的区间时,可以用插入排序

int GetMidIndex(int* a, int left, int right)
{
	int keyi = (left + right) >> 1;
	if (a[left] > a[keyi])
	{
		if (a[keyi] > a[right])
			return keyi;
		else if (a[right] > a[left])
			return left;
		else
			return right;
	}	//a[keyi]>a[left]
	else
	{
		if (a[left] > a[right])
			return left;
		else if (a[right] > a[keyi])
			return keyi;
		else
			return right;
	}
}
void QuickSort(int* a, int left,int right)
{
	if (left >= right)
		return;

	int keyi = PartSort1(a, left, right);
	if (right - left > 20)
	{
		QuickSort(a, left, keyi - 1);
		QuickSort(a, keyi + 1, right);
	}
	else
		InsertSort(a + left, right - left + 1);
}

快排非递归

用栈来实现,依次把头尾传入栈中,然后对这段区间进行一次快排,返回一个中间值的序号,以这个序号把区间分成两段,再分别进行上述操作,直到最后一个元素,就是排好了

void QuickSortNonR(int* a, int left, int right)
{
	Stack st;
	StackInit(&st);
	StackPush(&st, left);
	StackPush(&st, right);
	while (!StackEmpty(&st))
	{
		int right1 = StackTop(&st);
		StackPop(&st);

		int left1 = StackTop(&st);
		StackPop(&st);

		int keyi = PartSort1(a, left1, right1);
		if (left1 < keyi - 1)
		{
			StackPush(&st, left1);
			StackPush(&st, keyi - 1);
		}
		if (keyi + 1 < right1)
		{
			StackPush(&st, keyi + 1);
			StackPush(&st, right1);
		}
	}
	StackDestroy(&st);
}

时间复杂度:O(N*log(N))
空间复杂度:O(logN)
稳定性:不稳定

五、归并排序

归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
也就是说要把序列划分成好多个小区间,再合并


void _MergeSort(int* a, int left, int right, int* tmp)
{
	if (left >= right)
		return;

	int mid = (left + right) >> 1;
	_MergeSort(a, left, mid, tmp);
	_MergeSort(a, mid + 1, right, tmp);

	int begin1 = left, end1 = mid;
	int begin2 = mid + 1, end2 = right;
	int i = left;
	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++];

	for (int i = left; i <= right; i++)
	{
		a[i] = tmp[i];
	}
}

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);

	free(tmp);
}

归并非递归

设置一个gap,刚开始gap=1,就是每个小区间为1个元素,两个区间去比,小的排到前面,然后gap*=2,按照上面的步骤接着比
有两点要注意:1.比到最后的时候,有可能最后一个区间不够gap个
2.有可能不存在最后一个区间

void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	int gap = 1;
	while (gap < n)
	{
		for (int i = 0; i < n; i += 2 * gap)
		{
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;

			//第二个小区间不存在
			if (begin2 >= n)
				break;
			
			//第二个小区间存在,但是不够gap个,结束位置越界了
			if (end2 >= n)
				end2 = n - 1;

			_Merge(a, begin1, end1, begin2, end2, tmp);
		}
		gap *= 2;
	}
}

时间复杂度:O(N*log(N))
空间复杂度:O(N)
稳定性:稳定

六、非比较排序 - 计数排序

思路:统计相同元素出现的次数
根据统计的结果再放回原来的序列中

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* tmp = (int*)malloc(sizeof(int) * range);
	if (tmp == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	memset(tmp, 0, sizeof(int) * range);

	for (int i = 0; i < n; i++)
	{
		tmp[a[i] - min]++;			 //对应位上的个数有几个
	}

	int j = 0;
	for (int i = 0; i < range; i++)
	{
		while (tmp[i]--)
		{
			a[j++] = i + min;
		}
	}

	free(tmp);
}

只适合整数,数据范围要集中
时间复杂度:O(N+range)
空间复杂度:O(range)
稳定性:稳定

总结

以上就是今天要讲的内容,如果读者对于代码没读懂,建议多去画图

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值