数据结构—排序(常见的所有排序思路及代码实现)

1.前言

   我们在学习编程中,排序就离不开我们的,我们通常会将数据排成升序或者降序,我们可以更快的找到我们想要的数据,用好排序算法也可以提高我们的效率,从而提高程序的总体性能,可以让一个问题变得简单一些,在这里我会将常见的排序都去实现,冒泡排序,选择排序,插入排序,希尔排序,堆排序,快速排序,归并排序,将从简单的排序实现,每一个排序都会说明思路再去进行实现,循序渐进!

2.冒泡排序-O(N^2)

2.1冒泡排序思路

我们通过动图可以发现冒泡排序就是前一个和后一个进行比较,假设我们要排升序,那么也就是前一个和后一个比,如果前一个比后一个大就交换,交换到最后就是这个数据里最大的数,最大的数也就冒到了最后,这是一趟的排序,下一趟就不需要最后一个数了,因为我们在前一趟已经把他排好了,也就是下一趟到倒数第二个 ,以此类推,这样排下去数据就是有序的了!

2.2冒泡排序代码实现

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

接下来让我们来调试一下:

我们可以发现我们的arr数组已经排成有序了!冒泡排序就实现完了,接下来我们来看一下他的时间复杂度,我们可以看出它的时间复杂度是N^2!

3.选择排序 -O(N^2)

3.1选择排序思路

选择排序就是将数组遍历一遍,如果是升序,那么就遍历一遍数组选出最小的,将其放到最前面,以此类推,每次遍历去选最小的放到前面,这个排序让我感觉就是暴力选,一次次的遍历,我们可以对他进行一些优化,就是我们每次遍历数组的时候选一个最小,选一个最大,将最小的放在前面,最大的放在后面,这样遍历一次就可以选出两个数出来,提高的程序的效率!

3.2选择排序代码实现

在实现前我们要注意一些问题,不然会掉到坑里面,第一个就是在找小或找大的时候,我们要找的是下标,而不是值,如果是值会覆盖掉原本的,第二个就是当begin和max相等时,这个给大家画图来看一下更加容易理解!

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

 经过优化我们的选择排序就实现了,通过分析代码我们不难发现选择排序的时间复杂度是O(N^2)!

4.插入排序-O(N^2)

4.1插入排序思路

插入排序开始就是将一个看做有序的,把第二个插入进去,前两个有序,把第三个插入进去,以此类推,把前面看做有序,把后一个插入进去 。假设0-end有序,我们将end+1插入到0-end中,进行比较,如果0-end其中的值大于end+1的值,就要让其向后走,向后走会覆盖掉end+1的值,所以我们可以把end+1的值存到tmp中,这样就排就是一趟,以此类推就可以让数组有序!

4.2插入排序代码实现 

在实现前我们要注意一些问题,实现多次的时候循环条件是n-1,因为我们需要end+1,如果是n数组就越界了,还有一个就是当end--减到数组外时,我们要怎么去写,在哪里写,要想清楚

void InserSort(int* a, int n)
{
	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;
	}
}

这样插入排序就实现了,通过代码我们可以分析出时间复杂度为O(N^2)

5.希尔排序

5.1希尔排序思路

希尔排序是对插入排序的优化,如果排序的数组接近有序,那么插入排序的效率就越高,那么如何让数组更加接近有序呢?希尔这位大佬就发明了希尔排序,主要的思路分为两步,第一步就是预排序,选择一个整数做为增量,把一个大排序分成许多组,这些组之间的距离就是我们设置的整数,这里设为gap,将小组排成有序,然后将gap缩小继续排序,直到最后gap=1就是我们学过的插入排序,当gap=1就是我们的第二步直接插入排序。接下来给大家画图演示一下并且说明一下gap的值如何取!

首先我们来实现第一趟的排序,这里为了让大家更容易看懂把gap设置为3,一般情况gap=n/gap

在大家下面了解如何取gap的值以后,下一组组gap就变成了1也就是我们的插入排序

5.1.1如何选取希尔的增量值(gap)

希尔排序是一个复杂的问题,它的时间是一个关于增量序列的函数,非常复杂的一个数学问题,所以这个gap的值如何取最优,到现在也没有一个准确的数值,gap太大走的会很快,但是这样数组就不会越接近有序,gap越小,走的慢,但是数组越接近有序!最初希尔提出的是每次一分为2,也就是gap /= 2,当gap=1时就是插入排序,后来Knuth提出了一种gap = [gap/3] +1的方法,分为3分之1 。这里+1是因为当gap<=3时,会变为0,所以要+1!我们目前主流的是Knuth提出的一分为三的办法

上述的gap是我设置的,是想让大家先可以明白第一趟要怎么走,在我们实现当中当我们知道gap如何取值,那么我们就可以进行下列的多趟排序,让我们画图来看一下吧!

5.2希尔排序的实现

这里我分为一小步一小步来排,大家可以更加直观的感受!

5.2.1一个小组的排序

大家可以结合代码来看一下更加明了,后续会进行优化!我们可以发现,当gap=1时和插入排序一样。我们可以对比一下!

5.2.2一组的排序

我们只需要在一个小组的前面加一层循环即可!当j=0时就是第一组,当j=1时就是第二组,这样是一小组一小组的排序!

int gap = n;
	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 (gap >= 0)
			{
				if (a[end] > tmp)
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = tmp;
		}
	}

我们也可以改造一下上述的代码,三组循环变成两组循环! 就是小组同时进行排序!我们一般都使用2层循环

int gap = n;
	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 (gap >= 0)
			{
				if (a[end] > tmp)
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = tmp;
		}
	}

5.3总体实现

上面实现了一组的排序,那么我们现在实现多组直到gap=1进行插入排序整个数组就有序了!这里我们加的条件是gap>1,当gap<=1时就跳出循环!

void ShellSort(int* a, int n)
{
	int gap = n;
	while (gap > 1)
	{
		gap = gap / 3 + 1;
		for (int j = 0; j < gap; j++)
		{
			for (int i = j; i < n - gap; i++)
			{
				int end = i;
				int tmp = a[end + gap];
				while (gap >= 0)
				{
					if (a[end] > tmp)
					{
						a[end + gap] = a[end];
						end -= gap;
					}
					else
					{
						break;
					}
				}
				a[end + gap] = tmp;
			}
		}
	}
}

 这样我们的希尔排序就实现了,它还是非常快的,接下来让我们说一下他的时间复杂度,它的时间复杂度比较复杂,涉及到许多的数学问题,有兴趣的可以了解一下,这里就直接说明了,希尔排序的时间复杂度O(n^1.3)

6.堆排序O(N*logN)

6.1堆排序思路

堆排序需要我们首先建一个堆,这里我们采用向下调整建堆,因为它的时间复杂度是O(N),具体原因在堆的实现那一篇有详细讲解,假设我们要建大堆,那么如果是向下调整建堆需要我们的左右子树都是大堆,这样才可以向下调整建堆,所以我们可以从下向上调,从第一个非叶子节点开始调整,叶子节点没有调整的必要,建好堆以后,假设我们升序建大堆,那么数组下标为0的就是最大值,这个时候将下标为0和最后一个下标交换,最大的值就到了最后,这样一直交换下去,数组就有序了!我们可以画图来分析一下!

6.2堆排序代码实现 

因为在前一篇已经详细的实现了向下建堆和向上建堆,这里就直接实现了!

//假设建大堆
void AdjustDown(int* a, int n, int parent)
{
	//假设法,假设左孩子大
	int child = parent * 2 + 1;
	while (child < n)
	{
		if (child + 1 < n && a[child] < a[child + 1])
		{
			++child;
		}
		//当child>=n,已经没有孩子了
		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),堆排序的时间复杂度也就是O(N*logN)

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

这里快速排序会有三种方法,最经典的Hoare法,双指针法,非递归法三种办法!

7.1Hoare快排

7.1.1Hoare快速排序排序基本思想

快速排序是Hoare在1962年提出的一种二叉树结构的交换算法,主要的思想就是首先从数组当中选取一个数当做key,然后将数组分成2个子序列,让左子序列小于key,将小的值放在key的左边,右子序列大于key,大于key的放在key的右边,也就是左边找大,右边找小,用同样的办法左右子序列重复上面过程,数组就有序了!接下来我们画图分析一下

排完一趟以后这个数组分为三部分 [left,keyi-1]  keyi  [keyi+1,right] 

还有就是当left>=right的情况,当区间只有一个值或者区间不存在就不用再分割了

接下来给大家分析一下左边做keyi,右边先走的原因!

7.1.2Hoare排序代码实现

相信大家理解上面的思路来写代码就非常容易了!

void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;
	int begin = left, end = right;
	int keyi = left;
	while (begin < end)
	{
		while (end > begin && a[end] >= a[keyi])
		{
			--end;
		}
		while (end > begin && a[begin] <= a[keyi])
		{
			++begin;
		}
		Swap(&a[begin], &a[end]);
	}
	Swap(&a[keyi], &a[begin]);
	keyi = begin;
	QuickSort(a,left, keyi - 1);
	QuickSort(a,keyi + 1, right);
}

 7.2挖坑法

7.2.1挖坑法思想

如果对左边做keyi,右边先走可以来看一下挖坑法,这个不需要考虑那个问题,可更加同意理解,挖坑法就是挖一个坑,当右边走找到比keyi小的值,把值甩到坑位,然后坑位的下标走到end的下标,这个时候坑就在end的位置,当end和begin相遇时只需要把keyi的值放到坑里,挖坑法的好处2在于保持一个坑位,不断的填数字然后换坑! 

 7.2.2挖坑法代码实现

在实现挖坑法大致思想和Hoare的一样,一些地方有些变动!

void QuickSort1(int* a, int left, int right)
{
	if (left >= right)
		return;
	int begin = left, end = right;
	int keyi = a[left], pit = left;//坑位
	while (begin < end)
	{
		while (end > begin && a[end] >= keyi)
		{
			--end;
		}
		a[pit] = a[end];//将比keyi小的值甩到坑位
		pit = end;//坑位走到end
		while (end > begin && a[begin] <= keyi)
		{
			++begin;
		}
		a[pit] = a[begin];
		pit = begin;
	}
	//begin和end相遇了,将keyi的值给坑
	a[pit] = keyi;
	QuickSort1(a, left,pit-1);
	QuickSort1(a, pit + 1, right);
}

7.3快排优化版本

在上面我们实现的方法中都是选第一个数为keyi,这个keyi是不固定的,我们可以设想一些情况,如果这个数组就有序的,那么右边找小要找到第一个,每次这样就会非常的麻烦,还有一个问题就是到最后的几组,如果我们还是用快排,就有点杀鸡焉用牛刀的意思了,所以我们提出了两种办法,针对选keyi我们用一个三数取中的办法,针对最后几组数据我们直接插入排序,这样可以提高效率!

7.3.1三数取中

 三数取中就是在三个数当中取中间的那个数,不是最大也不是最小,这个实现也比较好实现,就是进行一些比较

int Getmid(int*a, int left, int right)
{
	int midi = (left + right) / 2;
	if (a[right] > a[midi])
	{
		if (a[midi] > a[left])
		{
			return midi;
		}
		else if (a[right] > a[left])//此时mid最小
		{
			return left;
		}
		else
		{
			return right;
		}
	}
	else //a[mid]>a[right]
	{
		if (a[left] > a[midi])
		{
			return midi;
		}
		else if (a[left] > a[right])//midi最大
		{
			return left;
		}
		else
		{
			return right;
		}
	}
}

7.3.2小区间优化 

当数据小于10个我们可以直接采用插入排序,少于几个采用大家可以自行选择!

if (right - left - 1 <= 10)
	{
		InserSort(a + left, right - left - 1);
	}
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;
	if (right - left - 1 <= 10)
	{
		InserSort(a + left, right - left - 1);
	}
	int midi = Getmid(a, left, right);
	Swap(&a[midi], &a[left]);
	int begin = left, end = right;
	int keyi = left;
	while (begin < end)
	{
		while (end > begin && a[end] >= a[keyi])
		{
			--end;
		}
		while (end > begin && a[begin] <= a[keyi])
		{
			++begin;
		}
		Swap(&a[begin], &a[end]);
	}
	Swap(&a[keyi], &a[begin]);
	keyi = begin;
	QuickSort(a,left, keyi - 1);
	QuickSort(a,keyi + 1, right);
}

 7.4双指针法

 7.4.1双指针法思路

双指针的代码非常少,但是不是特别好理解,需要两个指针,一个prev,一个cur,开始prev指向开头,cur指向prev的下一个,接下来进行比较,cur的值是否小于keyi,如果小于,prev和cur各走一步并且进行交换,如果cur的值大于keyi,只有cur走,这样就拉开距离了,继续比较进行递归就有序了,画图来看一下,大家把代码和图结合即可理解!

 7.4.2双指针法实现

void QuickSort2(int* a, int left, int right)
{
	if (left >= right)
		return;
	int prev = left, cur = prev + 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;
	QuickSort2(a, left, keyi - 1);
	QuickSort2(a, keyi + 1, right);
}

7.5非递归法 

我们上述的Hoare法,挖坑法,双指针法,都是用递归来实现,那么我们都知道如果递归的深度太深,程序就会崩溃,那么我们接下来就看一下非递归如何实现

7.5.1快排非递归思路

快排的非递归我们需要用到栈,大家都知道栈是后进先出的,那么我们入栈的时候就可以入数据先将数组的最后一个下标和第一个下标入进去,这样取出来的时候就是第一个下标和最后一个下标,然后进行排序分割,分为左右两个区间,这样数组就有序了!画图结合理解一下!

7.5.2快排非递归实现

#include"Stack.h"
void QuickSort3(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 keyi = QuickSort2(a, begin, end);
		//先入右,再入左
		if(keyi + 1 < end)
		{
			STPush(&st, end);
			STPush(&st, keyi + 1);
		}
		if(begin < keyi - 1)
		{
			STPush(&st, keyi - 1);
			STPush(&st, begin);
		}
	}
	STDestroy(&st);
}

8.归并排序O(N*logN)

8.1递归归并排序

8.1.1归并排序思路

归并排序就是首先将一个数组一分为二,二分为四,一直分成小份,将小份排成有序后,再将有序列表将其归并,四合二,二合一这样一个数组就有序了,我们可以结合图例来看一下!

8.1.2归并排序实现

在实现时,我们需要malloc一个数组,这个malloc数组的作用就是存放已经排好序的数组,每一趟将排好的有序的数组存进去,最后整体有序在把malloc的数组放回原数组,这样原数组也就有序了,不要忘记释放申请的数组!

void _MergeSort(int* a, int* tmp, int left, int right)
{
	if (left >= right)
		return;
	//一分为2,2分为4
	int midi = (left + right) / 2;
	_MergeSort(a, tmp, left, midi);
	_MergeSort(a, tmp, midi+1, right);
	int begin1 = left, end1 = midi;
	int begin2 = midi + 1, end2 = right;
	//我们需要将排好的存到tmp当中,从left开始
	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++];
	}
	//比较完成 将tmp拷贝到a数组
	memcpy(a + left, tmp + left, sizeof(a[0]) * (right - left + 1));
}
void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc");
		return;
	}
	int left = 0, right = n - 1;
	_MergeSort(a, tmp, left, right);
	free(tmp);
	tmp = NULL;
}

8.2非递归归并排序

8.2.1非递归归并排序思路

我们可以看到递归归并是把一个数和一个数有序,再2个2个有序,那我们非递归是不是就可以把一个数看成有序,然后一个和一个归并,归并成2个数有序,有序后2个和2个归并,4个和4个归以此类推最终有序,画图看一下

8.2.2非递归归并排序实现

这里我们先排一组,再排多组

一组排好要记得拷贝到原数组当中,再排下一组

当gap>n就不需要排了,当gap=n数组已经有序了,所以我们写一个while循环gap<n,不要忘记每次让gap*=2,这样才能11归,22归,44归 

还有一些坑,如果数据个数2的倍数可以正确排序,但如果不是会出现一些问题,我们来看一下

我们可以看到一共只有10个数,划分的区间是有问题的,后面的存在数组越界的风险 

void MergeSort1(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc");
		return;
	}
	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;
			}
			//end2越界,更新
			if (end2 >= n)
			{
				end2 = n - 1;
			}
			//printf("[%d %d][%d %d] ",begin1, end1, begin2, end2);
			int j = i;
			//进行比较
			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");
		gap *= 2;
	}
	free(tmp);
	tmp = NULL;
}

 9.时间复杂度,空间复杂度,稳定性总体对比

相信大家都知道时间复杂度和空间复杂度知道怎么计算,来了解一下稳定性是什么

稳定性就是相同的值相对顺序不变


10.计数排序 

10.1计数排序思想

计数排序就是我们calloc一个原数组,calloc可以将数组都初始化为0,然后进行计数,是几就记到几的下标,相同的数count++,这是第一步,第二步就是排序,覆盖原数组,将calloc里的数有几个这样的数就放几个进去,我们来画图理解一下!

10.2计数排序实现 

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*)calloc(range, sizeof(a[0]));
	if (count == NULL)
	{
		perror("calloc");
		return;
	}
	//统计次数
	for (int i = 0; i < n; i++)
	{
		count[a[i] - min]++;
	}
	//排序
	int k = 0;
	for (int j = 0; j < range; j++)
	{
		while (count[j]--)
		{
			a[k++] = j + min;
		}
	}
	free(count);
	count = NULL;
}

11.结语

以上就是我们常见的排序,最常见的基本都列了出来,排序对我们的帮助还是很大的,合理的选择排序也很重要,我认为排序也是很重要的,处处都需要排序,希望这篇博客对大家有一些帮助!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值