【数据结构】排序

排序接口

//插入排序
void InsertSort(int* a, int n);
//希尔排序
void ShellSort(int* a, int n);
//冒泡排序
void BubbleSort(int* a, int n);
//选择排序
void SelectSort(int* a, int n);
//堆排序
void HeapSort(int* a, int n);
//快速排序
int GetMidIndex(int* a, int left, int right);//三数取中
int PartSort2(int* a, int left, int right);//对一段,无递归
void QuickSort(int* a, int begin, int end);//对整个数组,有递归,有小区间优化  //递归法

int PartSort2(int* a, int left, int right);//对一段,无递归
void QuickSortNonR(int* a, int begin, int end);//含栈         //非递归法
//归并排序
void _MergeSort(int* a, int begin, int end, int* tmp);
void MergeSort(int* a, int n);   //有小区间优化(插入排序)               //递归法

void MergeSortNonR(int* a, int n);                          //非递归法
//计数排序
void CountSort(int* a, int n);

排序方法

插入排序 O(N^2)

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

void InsertSort(int* a, int n)
{
	for (int i = 0; i < n - 1; ++i)   //以end为标准 从1开始插入
	{
		// [0, end] 有序,插入tmp依旧有序
		int end = i;
		int tmp = a[i + 1];   //tmp为end后一位

		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. 稳定性:稳定

希尔排序 O(N^1.3)

在这里插入图片描述
运用多组并排的方式,且一定会存在gap=1的情况,这时候就是插入排序
在这里插入图片描述
希尔排序思想:就是对插入排序的优化,若为升序,是让小的数更快的在前面,是让大的数更快的在后面,且最后一次gap=1,就是插入排序,总之希尔排序大大减少了插入排序一步步挪的次数。时间复杂度:O(N^1.3)

void ShellSort(int* a, int n)
{
	// 1、gap > 1 预排序
	// 2、gap == 1 直接插入排序

	int gap = n;
	while (gap > 1)
	{
		gap = gap / 3 + 1;  // +1可以保证最后一次一定是1    最后一定是两种情况:2/3   3/3
		// gap = gap / 2;     //最后一定是一种情况:2/2
		for (int i = 0; i < n - gap; ++i)  //保证tmp为数组中的数,没有越界情况
		{
			int end = i;
			int tmp = a[end + gap];
			while (end >= 0)
			{
				if (a[end] > tmp)
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}

			a[end + gap] = tmp;
		}
	}
}

选择排序 O(N^2)

思路:
1.先通过begin到end的遍历,找到该区间最大值和最小值
2.将最小值移到区间最左边,将最大值移到区间最右边
在这里插入图片描述

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

			if (a[i] < a[mini])
			{
				mini = i;
			}
		}

		Swap(&a[begin], &a[mini]);
		// 如果maxi和begin重叠,修正一下即可
		if (begin == maxi)
		{
			maxi = mini;
		}

		Swap(&a[end], &a[maxi]);

		++begin;
		--end;
	}
}

堆排序 O(N*logN)

排升序,建大堆,排降序,建小堆
向下调整和向上调整:时间复杂度O(logN)
利用向下调整建大/小堆:时间复杂度O(N)
堆排序:时间复杂度O(N*logN)

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

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

// 排升序
void HeapSort(int* a, int n)
{
	// 建大堆
	for (int i = (n - 1 - 1) / 2; i >= 0; --i)
	{
		AdjustDown(a, n, i);
	}

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

冒泡排序 O(N)

在这里插入图片描述


void BubbleSort(int* a, int n)
{
	for (int j = 0; j < n; ++j)
	{
		bool exchange = false;
		for (int i = 1; i < n - j; i++)  //防越界:保后面,舍前面   所以a[i-1]为前面一位,a[i]为后面一位
		{
			if (a[i - 1] > a[i])        //每一趟都是从a[0]开始换,无论换没换,都会从a[0]移动到a[n-1-j]
			{                                                            //该代码中移动的是a[i-1]
				int tmp = a[i];
				a[i] = a[i - 1];
				a[i - 1] = tmp;

				exchange = true;
			}
		}

		if (exchange == false)    //若一趟中没发生任何交换,则数组有序
		{
			break;
		}
	}
}

快速排序(O(logN*N))

递归法

下面三种方法其实本质一样,形式不同
时间复杂度: O(logN*N)
空间复杂度:O(logN)
可以小区间优化,只有有递归,就可以小区间优化
在这里插入图片描述

1.1hoare

三数取中作用:防止a[keyi]为数组最小值
如果keyi每次取得都是中间值,则效率很好,但如果数组有序,a[keyi]每次取值都是最小值,则易出现下图情况,效率低。
在这里插入图片描述
所以三数取中就是来解决这个问题,注意:三数取中是间中间大小的数换掉最左边,而不是将keyi移到中间数位置,keyi(下标)永远在最左边

//三数取中
int GetMidIndex(int* a, int left, int right)  //left, right都是下标
{
	int mid = (left+right)/2;
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
		{
			return mid;
		}
		else if (a[left] < a[right])
		{
			return right;
		}
		else
		{
			return left;
		}
	}
	else // a[left] > a[mid]
	{
		if (a[mid] > a[right])
		{
			return mid;
		}
		else if (a[left] > a[right])
		{
			return right;
		}
		else
		{
			return left;
		}
	}
}

// hoare
int PartSort1(int* a, int left, int right)  //left, right都是下标
{
	int midi = GetMidIndex(a, left, right); //三数取中
	Swap(&a[left], &a[midi]);

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


void QuickSort(int* a, int begin, int end) //begin, end都是下标
{
	if (begin >= end)
		return;
    // 小区间优化
	/*if (end - begin + 1 < 10)
	{
		InsertSort(a+begin, end - begin + 1);

		return;
	}*/
	int keyi = PartSort1(a, begin, end);
	//int keyi = PartSort2(a, begin, end);
	//int keyi = PartSort3(a, begin, end);
	// [begin, keyi-1] keyi [keyi+1, end]

	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}

456
hoare思路:先右边找小,再左边找大,左右交换,直到相遇,再相遇点和最左边交换。
其实相遇点一定是比keyi小,理由如下:
222222

1.2 挖坑法

思路:
1. 先把最左边的数作为keyi存起来,且最左边为坑(hole)
2. 右边找小,找到小,小放入坑中,小的位置变成坑
 左边找大,找到大,大放入坑中,大的位置变成坑,重复直到相遇
3. 相遇后,相遇点一定是坑,因为left和right有一个一定是坑,将keyi放入坑中
在这里插入图片描述

// 挖坑法,也有三数取中
int PartSort2(int* a, int left, int right) //left, right是下标
{
	int midi = GetMidIndex(a, left, right);
	Swap(&a[left], &a[midi]);

	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;
}
void QuickSort(int* a, int begin, int end)//begin,end是下标
{
	if (begin >= end)
		return;
    // 小区间优化
	/*if (end - begin + 1 < 10)
	{
		InsertSort(a+begin, end - begin + 1);

		return;
	}*/
	//int keyi = PartSort1(a, begin, end);
	int keyi = PartSort2(a, begin, end);
	//int keyi = PartSort3(a, begin, end);
	// [begin, keyi-1] keyi [keyi+1, end]

	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}
1.3前后指针法

思路:1.起始位置:prev在最左边,cur在其后一位
   2.若a[cur]>=a[keyi],cur++,prev不加
    若a[cur]<a[keyi],cur++,prev++,a[cur]和a[prev]交换,直到cur>right
   (保证了a[prev]永远<keyi,除起始点)(两端区间的维护)
   3.cur>right后,a[prev]与keyi交换
在这里插入图片描述

// 前后指针法,也有三数取中
int PartSort3(int* a, int left, int right)//left,  right是下标
{
	int midi = GetMidIndex(a, left, right);
	Swap(&a[left], &a[midi]);

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

		++cur;
	}

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

void QuickSort(int* a, int begin, int end)// begin,  end是下标
{
	if (begin >= end)
		return;
    // 小区间优化
	/*if (end - begin + 1 < 10)
	{
		InsertSort(a+begin, end - begin + 1);

		return;
	}*/
	//int keyi = PartSort1(a, begin, end);
	//int keyi = PartSort2(a, begin, end);
	int keyi = PartSort3(a, begin, end);
	// [begin, keyi-1] keyi [keyi+1, end]

	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}
2.三路划分

目的:防止出现数组全为一个数或每个数很多的情况
在这里插入图片描述

//随机数三数取中,改mid
int GetMidIndex(int* a, int left, int right)  //left, right都是下标
{
	//int mid =(left+right)/2;
	int mid= left+ (rand() %(right-left));  //与固定三数取中唯一不同点,还要记得加srand(time(0));
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
		{
			return mid;
		}
		else if (a[left] < a[right])
		{
			return right;
		}
		else
		{
			return left;
		}
	}
	else // a[left] > a[mid]
	{
		if (a[mid] > a[right])
		{
			return mid;
		}
		else if (a[left] > a[right])
		{
			return right;
		}
		else
		{
			return left;
		}
	}
}
 void Swap(int* p1,int* p2)
 {
    int t=(*p1);
   (*p1)=(*p2);
    (*p2)=t;
 }
 void QuickSort(int* a,int begin,int end)
 {
    srand(time(0));
    if(begin>=end)
    {
      return;
    }
    int midi=GetMidIndex(a,begin,end); //三数取中:防止a[keyi]为最小值,但这个三数取中是随机数取中,是进阶版的三数取中,前面的三数取中也可以用进阶版
    Swap(&a[midi],&a[begin]);
    int key=a[begin];
    int cur=begin+1;
    int right=end;
    int left=begin;
    while(cur<=right)
    {
     if(a[cur]<key)
     {
       Swap(&a[cur],&a[left]);
       cur++; left++;
     }
     else if(a[cur]>key)
     {
       Swap(&a[cur],&a[right]);
       right--;
     }
     else
      {
        cur++;
      }
    }
    //[begin,left-1] [left,right] [right+1,end]
    QuickSort(a,begin,left-1);
    QuickSort(a,right+1,end);
 }

这道题排序用普通快排过不了,三路划分+随机取中可以过,归并排序也可以过。

非递归法(也含有1.1,1.2,1.3)

非递归法:我是用栈实现的,栈的相关函数在我的博客栈和队列的实现的源码里有,该方法也用到了hoare,挖坑法,前后指针法。
在这里插入图片描述

// 挖坑法
int PartSort2(int* a, int left, int right) //left, right是下标
{
	int midi = GetMidIndex(a, left, right);
	Swap(&a[left], &a[midi]);

	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;
}
void QuickSortNonR(int* a, int begin, int end)// begin, end是下标
{
	ST st;
	STInit(&st);
	STPush(&st, end);
	STPush(&st, begin);

	while (!STEmpty(&st))
	{
		int left = STTop(&st);
		STPop(&st);

		int right = STTop(&st);
		STPop(&st);

		int keyi = PartSort2(a, left, right);  //重点
		//int keyi = PartSort1(a, left, right);
		//int keyi = PartSort3(a, left, right);//1,2,3种方法都可以

		// [left, keyi-1] keyi [keyi+1, right]

		if (keyi + 1 < right)  //确保右区间的个数>=2
		{
			STPush(&st, right);
			STPush(&st, keyi + 1);
		}

		if (left < keyi - 1) 确保左区间的个数>=2
		{
			STPush(&st, keyi - 1);
			STPush(&st, left);
		}
	}

	STDestroy(&st);
}

归并排序 O(logN * N)

时间复杂度:O(logN * N)
空间复杂度:O(N)
可以用小区间优化,在递归法里

递归法

在这里插入图片描述

归并排序不是左右同时进行的,是有左右顺序的,因为递归是有左右顺序的
在这里插入图片描述
含小区间优化

void _MergeSort(int* a, int begin, int end, int* tmp)  //先分割成两部分,两部分分别归并,成为有序的两部分,这两部分再一起归并,成为一个有序的整体
{
	if (begin == end)
		return;
    // 小区间优化
	/*if (end - begin + 1 < 10)
	{
		InsertSort(a+begin, end - begin + 1);

		return;
	}*/
	int mid = (begin + end) / 2;
	// [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 = begin;
	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));
}
void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);

	_MergeSort(a, 0, n - 1, tmp);

	free(tmp);
}

非递归法

在这里插入图片描述

我们可以发现,其实每一个数最后一趟都会归并到,不会落数。归并两部分,不需要两部分数量相同,只需要两部分有序
接下来,讲讲在某一趟中,边界的情况分析:
总的来说,就是只有第二部分有,就归并,无就break
在这里插入图片描述

void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);

	// 1  2  4 ....
	int gap = 1;   //有gap个数
	while (gap < n)
	{
		int j = 0;
		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;

			//printf("[%d,%d][%d,%d]\n", begin1, end1, begin2, end2);

			if (end1 >= n || begin2 >= n)
			{
				break;
			}

			// 修正
			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));
		}
		printf("\n");

		//memcpy(a, tmp, sizeof(int) * n);
		//在这里,为什么不可以归并一趟,拷贝一趟,是因为数组末尾有些直接break,这些数都没有赋值到tmp数组上,那tmp缺数了,不能拷贝n个数
		gap *= 2;
	}

	free(tmp);
}

若当区间2不存在时,将区间2设为不存在区间,那么那一组就可以进行归并,那就可以实现归并一趟,拷贝一趟,代码如下:(不过,更推荐上面的归并一组,拷贝一组的方法)

void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);

	// 1  2  4 ....
	int gap = 1;
	while (gap < n)
	{    
		int j = 0;
		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;

			//printf("修正前:[%d,%d][%d,%d]\n", begin1, end1, begin2, end2);

			if (end1 >= n)
			{
				end1 = n - 1;

				// 不存在区间
				begin2 = n;
				end2 = n - 1;
			}
			else if (begin2 >= n)
			{
				// 不存在区间
				begin2 = n;
				end2 = n - 1;
			}
			else if(end2 >= n)
			{
				end2 = n - 1;
			}

			//printf("修正后:[%d,%d][%d,%d]\n", begin1, end1, begin2, end2);


			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++];
			}
		}
		printf("\n");

		memcpy(a, tmp, sizeof(int) * n);
		gap *= 2;
	}

	free(tmp);
}

计数排序 O(N+Range)

时间复杂度:O(N+Range)
空间复杂度:O(Range)
缺陷1:依赖数据范围,适用于范围集中的数组
缺陷2:只能用于整形
在这里插入图片描述

void CountSort(int* a, int n)
{
	int min = a[0], max = a[0];
	for (int i = 0; i < n; i++)
	{
		if (a[i] < min)
		{
			min = a[i];
		}

		if (a[i] > max)
		{
			max = a[i];
		}
	}

	int range = max - min + 1;
	int* countA = (int*)malloc(sizeof(int) * range);
	memset(countA, 0, sizeof(int) * range);

	// 统计次数
	for (int i = 0; i < n; i++)
	{
		countA[a[i] - min]++;
	}

	// 排序
	int k = 0;
	for (int j = 0; j < range; j++)
	{
		while (countA[j]--)
		{
			a[k++] = j + min;
		}
	}
}

排序的稳定性

在这里插入图片描述

  • 25
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值