数据结构中排序大汇总

排序在我们学习算法中,是十分重要的。现在我们就对初阶数据结构的算法进行一个总结。

一.插入排序

插入排序是在时间复杂度为o(n^2)中,最快的一个。其思想就是,在数组中从左往右开始,确定每一个数,在其前面所在的位置,遍历完数组后,即可实现排序。

由于思路比较简单,这边就直接上代码了:

void InsertSort(int* a, int n)
{
	for (int i = 0; i < n; ++i)
	{
		int j = i;
		while (j>0)
		{
			if (a[j] < a[j - 1])
			{
				swap(&a[j], &a[j - 1]);
				j--;
			}

			else
				break;
		}
	}
}

其实插入排序只是一个前菜,真正的插入排序plus是希尔排序,希尔排序是在插入排序的基础上,分组进行插入排序,通过预处理,来实现时间复杂度较低的排序。

我们来分析下面一段代码:

// 希尔排序
void ShellSort(int* a, int n)
{
	int gap = n;
	while(gap>1)
	{
		gap = gap / 3+1;
		for (int i = 0; i < n; i++)
		{
			int j = i;
			while (j > gap-1)
			{
				if (a[j] < a[j - gap])
				{
					swap(&a[j], &a[j - gap]);
					j-=gap;
				}

				else
					break;
			}
		}
	}
}

gap是希尔排序中,每段插入排序的间隔,明白了这个点,那就好分析了。

gap会随着排序不断减小,直到1,但是经过多轮的预排序后,最后一轮的插入排序,时间复杂度会很低。这就是希尔排序,时间复杂度大概是o(n^1.3)。

二.选择排序

第二种是选择排序,顾名思义,就是在整个数组中,选出最大或者最小的,放在数组最前面,或者最后面,在确定好相对最小最大值后,再依次往后遍历。

话不多说,直接上代码:

//选择排序
void SelectSort(int* a, int n)
{
	for (int i = 0; i < n; ++i)
	{
		int mini = i;
		for (int j = i; j < n; ++j)
		{
			if (a[mini] > a[j])
				mini = j;
		}
		swap(&a[i], &a[mini]);
	}
}

当然,如果只是它分为一类,那也太low了,其实,我们之前提到的堆排序也是选择排序的一种,它通过堆顶元素和最后一个元素交换,再整理堆,重复这个过程,就可以实现升序或者降序。

向上向下调整法在这里就不多说了,具体的在上一篇博客可以看到。

void HeapSort(int* a, int n)
{
	for (int i = 0; i < n; ++i)
	{
		AdjustUp(a, i);
	}

	for (int i = n-1; i >=0; --i)
	{
		swap(&a[i],&a[0]);
		AdjustDwon(a, i, 0);
	}
}

通过这个方法也可以实现排序,并且时间复杂度只有o(nlogn)。

三.交换排序

我们知道一个很经典的交换排序,就是冒泡排序,但是呢,有一个更加出名的排序,叫做快速排序,即快排。创始人霍尔的思想是:选定一个基准值,规定left指向最左边的值,right指向最右边的值。从right向前找第一个小于基准值的数,找到后,再从left向后寻找第一个大于基准值的数,此时交换两个数后,继续从此刻right的位置向前找第一个小于基准值的数,再从left位置向后找第一个大于基准值的数,找到后,两数进行交换,重复如此,直到left和right相遇,再交换left与right相遇的值和基准值,此时,一趟快速排序便结束了,基准值也就找到了自己合适的位置,基准值的左右两边又是新的无序序列,这时只需要递归的进行快速排序即可。

实现快速排序的单趟排序其实不止一种,除了霍尔大佬的想法,还有其他的方法,这里我一共提供三种,一种是原始版:

// 快速排序hoare版本
int PartSort1(int* a, int left, int right)
{
	int keyi = left;

	while (left<right)
	{
		while (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;
}

第二个是挖坑法,思想是,还是确定一个基准值(数组第0个元素),坑位在一开始的基准值,从右往左找到一个比基准值大的,将其跟基准值交换,并把坑位交给它。再从左往右找一个比基准值小的,再将其跟基准值交换,并把坑位交给它。重复此过程,当left>=right的时候,即可找到基准值的最终位置。

// 快速排序挖坑法
int PartSort2(int* a, int left, int right)
{
	int key = a[left];
	int hole = left;

	while (left < right)
	{
		while (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;
}

第三种是前后指针法,这个稍有些抽象,这个了解就好。

// 快速排序前后指针法
int PartSort3(int* a, int left, int right)
{
	int keyi = left;
	int first = left;
	int second = left+1;
	while(second<=right)
	{
		if (a[second] < a[keyi])
		{
			++first;
			swap(&a[second], &a[first]);
		}

		++second;
	}

	swap(&a[first], &a[keyi]);
	return first;
}

这个就是快速排序的单趟排序的函数。

快速排序的实现一般都是用递归实现,通过前序遍历,即可得到排序。

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

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

其实快速排序还有非递归版本,其需要用栈来实现,因为递归的本质是函数压栈,所以我们可以通过栈模拟递归,依旧是前序的思想,每次压栈头尾,实现了单趟排序之后,头尾出栈,再实现左右压栈。

上代码:

void QuickSortNonR(int* a, int left, int right)
{
	Stack sk;
	StackInit(&sk);
	StackPush(&sk, left);
	StackPush(&sk, right);


	while (!StackEmpty(&sk))
	{
		int end = StackTop(&sk);
		StackPop(&sk);


		int begin = StackTop(&sk);
		StackPop(&sk);


		int keyi = PartSort3(a, begin, end);


		if (begin < keyi - 1)
		{
			StackPush(&sk, begin);
			StackPush(&sk, keyi - 1);
		}


		if (end > keyi+1)
		{
			StackPush(&sk, keyi + 1);
			StackPush(&sk, end);
		}


	}
	StackDestroy(&sk);
}

这就是所有的快速排序。

四.归并排序

接下来是归并排序,归并排序运用的也是二叉树的思想,将一段顺序表不断分成两半,知道只有一个元素,通过后序遍历,两两进行排序归并,这样就可以实现归并排序。

// 归并排序递归实现
void MergeSort(int* a, int left,int right,int *tmp)
{
	if (left >= right)
		return;
	
	int mid = (right + left) / 2;
	MergeSort(a,left, mid,tmp);
	MergeSort(a, mid+1, right,tmp);

	int begin1 = left;     int end1 = mid;
	int begin2 = mid + 1;  int end2 = right;
	int i = left;

	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] <= a[begin2])
		{
			tmp[i] = a[begin1];
			begin1++;
		}

		else
		{
			tmp[i] = a[begin2];
			begin2++;
		}
		i++;
	}

	while (begin1 <= end1)
	{
		tmp[i] = a[begin1];
		begin1++;
		i++;
	}

	while (begin2 <= end2)
	{
		tmp[i] = a[begin2];
		begin2++;
		i++;
	}

	memcpy(a + left, tmp + left , sizeof(int) * (right - left + 1));
}

可以看到,我们开辟了一个新数组tmp来进行元顺序表的改变。

当然归并排序也可以进行非递归的方法

// 归并排序非递归实现
void MergeSortNonR(int* a, int n)
{
	int i = 0;
	int* tmp = (int*)malloc(sizeof(int)*100);
	int gap = 1;

	while(gap<n)
	{

		for(i = 0;i < n;i += gap * 2)
		{
			int begin1 = i;          int end1 = begin1 + gap - 1;
			int begin2 = gap + i;    int end2 = begin1 + 2 * gap - 1;
			int j = i;


			//一共考虑三张情况的越界。
			if (end1 >= n || begin1 >= n)
				break;

			//修正end2
			if (end2 >= n)
				end2 = n - 1;

			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] <= a[begin2])
				{
					tmp[j] = a[begin1];
					begin1++;
				}

				else
				{
					tmp[j] = a[begin2];
					begin2++;
				}
				j++;
			}

			while (begin1 <= end1)
			{
				tmp[j] = a[begin1];
				begin1++;
				j++;
			}

			while (begin2 <= end2)
			{
				tmp[j] = a[begin2];
				begin2++;
				j++;
			}

			memcpy(a+i, tmp+i, sizeof(int)*(end2-i+1));
		}
		gap *= 2;
	}

	free(tmp);
}

这里就简单了解一下,要注意,右边界越界的问题(有兴趣的朋友们可以想一想什么情况会越界,是哪种情况的越界)。

五.计数排序

计数排序在排序大家庭中可以说是一个远门亲戚了,他的思想非常之独特:开辟一个新数组,其下标是原顺序表中的元素的值,通过对新数组(我们叫做tmp)的对应下标的数进行加减,来确定原数组中,对应元素数值的个数,再将tmp对应着返回给原数组。

听起来有些抽象,我们看看代码:

// 计数排序
void CountSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(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 gap = max - min + 1;
	
	for (int i = 0; i < gap; ++i)
	{
		tmp[i] = 0;
	}

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

	int i = 0;
	int j = 0;
	while (i < n &&j < max)
	{
		if (tmp[j] != 0)
		{
			a[i] = j+min;
			tmp[j]--;
			++i;
		}

		else
			j++;
	}

	free(tmp);
}

这个就是计数排序。

六.总结

排序对于我们后续学习算法中有着至关重要的作用,上面经典的排序也要初步掌握,如果我有哪里不正确的地方,也请大家指点一下,谢谢浏览!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值