排序介绍(升序举例)

引言

    排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。

    稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次 序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排 序算法是稳定的;否则称为不稳定的。

    内部排序:数据元素全部放在内存中的排序。

    外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。

1.直接插入排序

    插入排序的思想就类似于我们在打扑克牌的时候模牌的过程,为了方便出牌我们每摸一张牌都会按照一定的顺序将牌放好,这里就按升序举例子。

    对于排序来说我们先看它的单趟,比如现在已经模了一堆牌了,我们也按照升序将它排好了。现在又摸了一张7出来,如何让7插入后有序?我们肯定是拿着7从前往后或者从后往前依次和每张牌比较,找到合适的位置插入进去后使牌堆继续保持有效。那最开始怎么走?最开始摸了第一张牌后第一张可以不排,放那里就行,剩下的牌摸上来就依次比对排序。

    那直接插入排序怎么实现?排序是给个数组,我们将数组排好就可以了。写排序时一般先研究单趟,再研究整体会比较好。对于单趟来说,比如数组中有一些值,前面已经排好序了,此时该排3了。

    此时需要定义一个end变量指向有序区间的结尾,end区间之前代表已经有序了,此时要将end后一个位置的值插入区间使区间继续保持有序。用tmp保存end+1位置的值,拿end位置的值和tmp的值比较,如果end位置的值大于tmp就让end的值挪到end+1位置。然后end不断向左走,直到end位置的值小于或等于tmp时就让tmp插入到end后面。

    end最坏走到-1的位置就停止了,此时直接插入到后面就可以了,所以单趟排序的代码可以这样实现:

void InsertSort(int* a, int n)
{
	int end;
	int tmp;
   //将tmp插入到[0,end]区间中,保持有序
	while (end >= 0)
	{
		if (a[end] > tmp)
		{
			a[end + 1] = a[end];
			end--;
		}
		else
		{
			break;
		}
	}
	a[end + 1] = tmp;
}

    单趟中end之前有序,让end的后一个值插入进来,当循环跳出来时,要么是end走到-1位置,要么是在中间的位置,反正最终都是将tmp的值放入到end+1的位置。单趟有了,那对于整体来说怎么排呢?像摸牌一样,数组中把0位置看作本来就是有序的,从1位置开始就像摸牌放牌的过程一样把每个数据插入进去即可。因此再套一层循环从下标为1的位置开始排,开始有序区间的结尾是end - i,tmp是end的下一个位置。

void InsertSort(int* a, int n)
{
	for (int i = 1; i < n; i++)
	{
		int end = i - 1;
		int tmp = a[i];
		//将tmp插入到【0,end】区间,保持有序
		while (end >= 0)
		{
			if (a[end] > tmp)
			{
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}
		}
		a[end + 1] = tmp;
	}
	
}

    什么情况下最坏呢?逆序时最坏,时间复杂度是O(N^2)(等差数列,排下标一时挪一下,排下标二时挪两下…)。最好的情况时间复杂度是O(N)(end遍历一遍)。

2.希尔排序

    希尔排序也叫缩小增量的排序,希尔排序也是一种插入排序,只不过在直接插入排序的基础上做了优化,它是怎么样的呢?比如现在有一组数据,对这组数据用直接插入排序感觉有一点慢,那我们对这组数组做些优化,让他接近有序,此时再用直接插入排序就会变快一些。

    那如果要排序的数组不是接近有序怎么办?一般我们分为两步:1:预排序,目的让数组接近有序。2:直接插入排序,让数组更快有序。

    那预排序怎么排呢?这里是分组排,间隔为gap的分为一组,对每组数据进行直接插入排序。现在给一些数据,假设gap是3:

    现在将数据分为了三组,这也意味着gap是几就将数据分为几组。现在对每组数据分别进行直接插入排序后会变成这样:

    分别对三组排完序后虽然没有完全有序,但比之前接近有序。比如9要走n-1次才能走到最后,现在可以更快。

    那如何实现代码呢?我们从小到大来实现,先看一组代码怎么排,拿红色的一组举例,还是和直接插入排序的思路一样有个end指向有序区间最后,有个tmp来保存下一个值通过比较来进行插入排序,只不过现在的tmp不是end后一个,而是后gap个。

//单趟排红色

void ShellSort(int* a, int n)
{
	int gap = 3;
	int end;
	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;
}
//整体红色

void ShellSort(int* a, int n)
{
	int gap = 3;
	for (int i = 0; i < n - gap; i += gap)
	{
		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;
	}
}

    现在红色的一组排完了,怎么样让剩余二组也像这样排序,其实就是控制好第一个有序的位置,也就是控制好end的位置。

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

    还有这样的写法:

void ShellSort(int* a, int n)
{
	int gap = 3;
	for (int i = 0; i < n - gap; i++)
	{
		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;
	}
}

    这两个效率差不多,只不过前一个是一组排完再排另外一组;后一个是多组并排。那现在已经达到了预排序的效果,是不是再单独调一次直接插入排序就可以了?确实可以这样做,但这个地方不用,因为可以发现当gap是1的时候其实就是直接插入排序。所以想办法控制gap就行,前面为了举例方便,gap给了3,那实际gap给多少合适呢?gap越大就跳的越快也越不接近有序,gap越小就跳的越慢也越接近有序。因此gap刚开始就给n,每次让它除2,当除到1时就是直接插入排序。

void ShellSort(int* a, int n)
{
	int gap = n;
	while (gap > 0)
	{
		gap /= 2;
		for (int i = 0; i < n - gap; i++)
		{
			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;
		}
	}
}

    希尔排序用了多组排序,效率到底有没有提升呢?我们可以对相同的数据排序,看看从开始排序到排序结束的时间来进行比较

void TestOP()
{
	srand(time(0));
	const int N = 100000;
	int* a1 = (int*)malloc(sizeof(int) * N);
	int* a2 = (int*)malloc(sizeof(int) * N);
	for (int i = 0; i < N; ++i)
	{
		a1[i] = rand();
		a2[i] = a1[i];
	}

	int begin1 = clock();
	InsertSort(a1, N);
	int end1 = clock();

	int begin2 = clock();
	ShellSort(a2, N);
	int end2 = clock();


	printf("InsertSort:%d\n", end1 - begin1);
	printf("ShellSort:%d\n", end2 - begin2);

	free(a1);
	free(a2);
}

int main()
{
	TestOP();
	return 0;
}

    这里发现,希尔排序的时间明显快。那希尔排序的时间复杂度是多少呢?先来看看外层循环跑了多少次?

    gap每次/2,当除到1的时候外层循环停止,除了多少个2外层循环就跑了多少次,假设除了x个2,因此n/2/2/2/2/…=1,x=logN,外层循环跑了logN次。

    再来看看里面for循坏整体跑了多少次?里面对每组依次插入排序。

    直接看不好看,我们从两个极端来看,最开始gap很大,数据被分为gap/2组后,每组的数据个数却非常小,每组可能也就2到3个数据,所以里面的while循环每次在多组插入移数据时可以看作常数次。又因为n-gap也是n这个数量级的,所以最开始gap很大时for整体跑了常数倍的n,也就是n这个量级。

    最后gap非常小的时候,for整体还是跑了n次,不是gap很小时应该和直接插入排序一样是n^2次吗?这里不能这样看,因为前面已经有了很多次预排序,当最后gap非常小排序时数组已经接近有序了,因此这里看作n次。

    基于以上两个原因,粗略的可以把希尔排序的时间复杂度看作O(N*logN)这个量级,但这样其实还是不准确,应该时间复杂度要比N*logN大一点,可以举个例子看看:

    从上面分析来看希尔排序的时间复杂度不好算,最后给出大家常说的结论:O(N^1.3)。

3.直接选择排序

    直接选择排序的思想类似于我们在玩牌时摸牌的时候刚好遇到了点事,让别人帮我们摸好牌放着,我们回来时一下子拿起一大堆牌整理,整理时扫一遍牌看谁最小放第一个位置,次小放第二个位置,依次类推。用代码实现就是从第一个数开始遍历一遍选择最小的数,选好后和第一个数交换位置,然后从第二个数开始遍历一遍,选好最小的数后和第二个数交换位置…。但实际写的时候一般喜欢优化一下,一次选最大和最小两个数,选好后分别放在左边和右边,然后缩小区间继续选。

void SelectSort(int* a, int n)
{
	int left = 0;
	int right = n - 1;
	while (left < right)
	{
		int min = left, max = left;
		for (int i = left + 1; i <= right; i++)
		{
			if (a[i] < a[min])
			{
				min = i;
			}
			if (a[i] > a[max])
            {
	            max = i;
            }
		}
		Swap(&a[left], &a[min]);
		Swap(&a[right], &a[max]);
		left++;
		right--;
	}
}

这样就写好了,我们给一组数运行一下:

发现结果并不对,这是为什么呢?通过调试我们发现:

    因此当left和min交换完成后,如果max和left重叠就会导致max指向的最大值被换到min指向的位置处,进而max和right交换时出现问题,因此交换完如果有重叠,则要更换max的下标。

void SelectSort(int* a, int n)
{
	int left = 0;
	int right = n - 1;
	while (left < right)
	{
		int min = left, max = left;
		for (int i = left + 1; i <= right; i++)
		{
			if (a[i] < a[min])
			{
				min = i;
			}
			if (a[i] > a[max])
			{
				max = i;
			}
		}
		Swap(&a[left], &a[min]);
		if (left == max)
		{
			max = min;
		}
		Swap(&a[right], &a[max]);
		left++;
		right--;
	}
}

    直接选择排序最坏时间复杂度是O(N^2)(第一次遍历N,第二次遍历N-2,第三次遍历N-4以此类推)。最好时间复杂度是O(N^2)(也要不断遍历,只不过没有交换而已)。

4.堆排序

    直接选则排序没有任何技巧,就是遍历一遍查找。如果想要有有技巧的排序就衍生出了堆排序,这里直接给出代码(堆排序的详细讲解在二叉树中)。堆排序是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)
{
	// 建堆 -- 向下调整建堆 -- O(N)
	for (int i = (n - 1 - 1) / 2; i >= 0; --i)
	{
		AdjustDown(a, n, i);
	}

	// 自己先实现 -- O(N*logN)
	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[end], &a[0]);
		AdjustDown(a, end, 0);

		--end;
	}
}

5.冒泡排序

    冒泡排序思想就是两两元素相互比较交换。对于单趟排序来说,就是控制好变量遍历,前后不断比较交换,一趟下来最大的排到了最后。

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

    对于整体来说,第一趟i冒到了n-1的位置,第二趟i冒到n-2的位置,因此控制好冒到的位置就可以了。控制好的同时做进一步优化:如果冒一次遍历没有数据交换直接退出循环就可以了。

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

    冒泡排序最坏时间复杂度是O(N^2)(第一趟交换n-1次,第二趟交换n-2次,以此类推)。最好时间复杂度是O(N)(遍历一遍)。

    既然冒泡排序和直接选择排序最好和最坏时间复杂度都是一个量级,那细分哪一个稍微好些呢?在给的数据有序的情况下,它们是一样的;给的数据接近有序时,它们有一些差别;部分有序时,差距就非常大了。比如给的是1,2,3,4,6,5 。冒泡排序要先走N-1次,再走N-2次才能确定已经排好序。直接插入排序只需要遍历一遍就好。

6.快速排序

    1. hoare版本

    快速排序的单趟思想是选出一个关键值(一般是最左边或最右边)(这里以最左边举例),把它放到一个正确的位置(排好序后最终的位置)。这样做的目的是左边的值都比关键值小,右边的值都比关键值大(关键值也到了最终排好序后的位置)。

    先在右边找比key小的值,找到后停在那里,再从左边找比key大的值,然后相互交换(该过程实际上再把小的值往左边换,把大的值往右边换)。然后继续开始第二轮左找小,右找大,再交换,依次类推,直到左右相遇。 一般左边做key右边先走,右边做key左边先走,这样保证了相遇位置的值比key小,就可以与key交换,达到目的。快速排序需要定义区间,比较方便。

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

    更具上述描述,代码实现出来,那如果遇到下面问题呢?

    如果遇到等于key的值,以上图情景就会陷入死循环,所以相等时也继续走,因为最终比key小的在左,比k大的在右,和key一样的再左还是再右都是不影响的。因此做如下改进:

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

    现在的代码有什么问题吗?如果给这样一个例子:

    这样刚开始进来后right就会不断的--,最终跑出去了;或者也有可能遇到righ一直++跑出去的可能,所以每次都要限制一下:

void QuickSort(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[left], &a[keyi]);
    keyi = left;
}

    按照上图实现代码后有时候可能有这样的想法:反正key和left都指向的同一个数据,不入直接刚开始就让left++一下,这样就少走一次left++,这种想法是不可以的:

所以不要这样做。

    通过单趟排序,让关键值到了合适的位置,此时数组被分为了三段区间:左区间、key、右区间,也就是[begin, keyi - 1], keyi, [keyi + 1, end]。此时左右区间都没有序,可以用同样的方式在左区间找key,这里不断递归左区间;然后用同样的方式在右区间找key,这里不断递归右区间。

    当区间仅有一个值或区间不存在时递归结束。区间仅有一个值就是当left==right时,区间不存在时就是left>right时:

void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;
	int keyi = left;
	int begin = left, end = right;
	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[left], &a[keyi]);
	keyi = left;
	// [begin, keyi-1] keyi [keyi+1, end]
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}

    这样代码就完成了,那快速排序的时间复杂度是多少呢?

   

    快排的递归方式像二叉树,那对快排做一次单趟排序时间复杂度是多少?是O(N)(left和right在不断遍历)。那快排在递归时最多递归多少层呢?是logN层,因为每次选出一个key,就分为左右两层,像满二叉树那样。那每一层单趟时间复杂度是多少呢?第一层是N,第二层是N-1(除去上一次的keyi),第三次是N-3…这里发现每一层N都是变化的,非常不好处理。这里举个例子,比如N是100w,此时最多递归调用20层,又因为每一层N减的数都很小,只是减去了上一次筛选出的keyi的总个数,因此其实对N这个量级没有太大影响,相当于每一层还是属于N这个量级,所以每一层单趟的时间复杂度是O(N)。通过上述可以大概的将快排的时间复杂度化为O(N*logN)这个量级。

    快排什么情况下最坏呢?是在顺序或逆序的情况:

    排升序或降序时相当于每一次keyi都在第一个位置或者最后一个位置。这样每次固定好一个数,区间每一次缩小1,相当于等差数列,这样时间复杂度就变成了O(N^2),这样当数据太大时递归可能还会引发栈溢出的问题。时间复杂度不是看的最坏情况?为什么上面说时间复杂度是N*logN,这是因为这里有方法可以改进它,不像别的效率不高的算法无法改进。

    其实影响快排性能的就是keyi,因为keyi交换后越接近中间,就越接近二分,也就越接近满二叉树,这样递归深度均匀,效率也比较高。keyi刚开始本身就是最小或最大的交换后也就离中间位置远。因此开始给的数组乱一些效率高,那如果给的数组不乱有没有什么改进方式呢?比如可不可以用不太小也不太大的值做keyi呢?这里有人给出了随机选keyi的方法,但如果随便一个位置做keyi,这时单趟的逻辑就变了。因此生成一个随机数,然后模长度让随机数在长度范围内,最后加left,因为可能递归到右区间时随机值应该在右区间,但起始点不是0。这样虽然有可能选到极端值,但是概率大大降低了,因为在很多数中随机找最值概率还是很小的。随机位置选好后和左边位置的值交换,还是让左边做关键位置,这样保证单趟的逻辑。

void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;
	int begin = left, end = right;
	//随机选keyi
	int randi = left + (rand() % (right - left));
	Swap(&a[randi], &a[left]);

	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[left], &a[keyi]);
	keyi = left;
	// [begin, keyi-1] keyi [keyi+1, end]
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}

    还有人觉得上面的方法不好,又给出了三数取中法。什么是三数取中呢?比如给的数组有序时如果选的keyi是中间位置,这样单趟完了交换后keyi就可以在中间位置。不能直接将left位置的值和middle位置的值交换,因为实际中不能保证数组是不是有序。因此三数取中就是左、中、右三个位置的值中选大小适中的那个做keyi。

int GetMidNumi(int* a, int left, int 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 left;
		else
			return right;
	}
	else  //a[left] > a[mid]
	{
		if (a[right] > a[left])
			return left;
		else if (a[mid] > a[right])
			return mid;
		else
			return right;
	}

}

void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;
	int begin = left, end = right;
	随机选keyi
	//int randi = left + (rand() % (right - left));
	//Swap(&a[randi], &a[left]);

	//三数取中
	int midi = GetMidNumi(a, left, right);
	if (midi != left)
		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[left], &a[keyi]);
	keyi = left;
	// [begin, keyi-1] keyi [keyi+1, end]
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}

    解决完上面问题,还有一个问题就是为什么左边做keyi,右边先走,就能保证相遇后的值比keyi的小呢?因为右边先走然后逐渐相遇会有这样的情况:1.右边找到小,左边没有找到大直接就和右边相遇了。2.右边开始或过程中找小没有找到,此时就和左边相遇了,相遇后要么是keyi的位置,要么是已经交换后的位置。

    2.挖坑法

    因为有人感觉hoare方法不是很好理解,坑很多,因此玩了一种新的方法叫挖坑法。

    思路是先将第一个数据放在临时变量key中,因为第一个数据已经被保存的缘故,其他数据可以随意填充,因此说第一个位置形成了坑位。右边找小的放到坑中,此时小的位置’值没了‘形成了新的坑,然后左边找大的,找到后放在右边新坑中,此时左边位置右形成了新的坑,不断这样走…最后相遇位置一定在坑位(因为最初左边是坑,右边找到后右边变成坑,然后左边继续找),然后将key的值放在坑位就行。

void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;
	int begin = left, end = right;
	随机选keyi
	//int randi = left + (rand() % (right - left));
	//Swap(&a[randi], &a[left]);

	//三数取中
	int midi = GetMidNumi(a, left, right);
	if (midi != left)
		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;
	// [begin, hole-1] hole [hole+1, end]
	QuickSort(a, begin, hole - 1);
	QuickSort(a, hole + 1, end);
}


    两种方法单趟排出来的结果大概率不一样,但大思想一样,最终单趟左边比key小,右边比key大。

    3. 前后指针版本

    有人还提出了一种思路:前后指针法。这个思路说的是用cur和prev一前一后两个指针指向下标,cur找到比key小的就++prev,然后cur和prev位置的值交换,再++cur;cur找到比key大的值就++cur。

    在该过程中prev要么仅跟着cur(prev的下一个是cur);prev要么跟cur中间间隔着比key大的一段值区间。该过程把比key大的翻到右边,比key小的翻到左边。

    初次实现可能会写成这样:

void QuickSort3(int* a, int left, int right)
{
	if (left >= right)
		return;
	int begin = left, end = right;
	随机选keyi
	//int randi = left + (rand() % (right - left));
	//Swap(&a[randi], &a[left]);

	//三数取中
	int midi = GetMidNumi(a, left, right);
	if (midi != left)
		Swap(&a[left], &a[midi]);

	int key = a[left];
	int prev = left, cur = left + 1;  //不要写0,1
	while (cur <= right)
	{
		if (a[cur] < key && ++prev != cur)  
			Swap(&a[cur], &a[prev]);

		++cur;                          //比keyi小比keyi大都要++
	}
	Swap(&a[prev], &key);
	// [begin, prev-1] prev [prev+1, end]
	QuickSort1(a, begin, prev - 1);
	QuickSort1(a, prev + 1, end);
}


    这里的问题是最后数组中的值和局部变量里的值交换了,并没有和数组中的值交换。因此建议用keyi。

void QuickSort3(int* a, int left, int right)
{
	if (left >= right)
		return;
	int begin = left, end = right;
	随机选keyi
	//int randi = left + (rand() % (right - left));
	//Swap(&a[randi], &a[left]);

	//三数取中
	int midi = GetMidNumi(a, left, right);
	if (midi != left)
		Swap(&a[left], &a[midi]);

	int keyi = left;
	int prev = left, cur = left + 1;  //不要写0,1
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++prev != cur)
			Swap(&a[cur], &a[prev]);

		++cur;
	}
	Swap(&a[prev], &a[keyi]);
	keyi = prev;
	// [begin, keyi-1] keyi [keyi+1, end]
	QuickSort1(a, begin, keyi - 1);
	QuickSort1(a, keyi + 1, end);
}

    4.补充

     快速排序在很多地方还会考虑这样一个优化:我们可以看到快排在不断的进行递归,像二叉树一样不断挨着往下递归,这里假设按照最优情况理解,最优情况下可以看作满二叉树。比如数组中有1w个数,不断递归往下分,最后几次可能就剩5个或者10个数在递归。5个数选key继续分,直到分到区间为1或者区间不存在时就不能分了。这样将像5个数这样数据量较小的数再次通过递归来排比较费劲,像5个数排整体递归就对递归了6次,非常麻烦。

    因此需要做一定优化,假设现在有10个数,选出key后划分为左边5个数,右边4个数;再对5个数选key排的目的是最终让这5个数有序,因此不管用什么方法最终有序就可以了。有人做了这样一件事,当区间小于一定数量时可以考虑不再去递归,不再用快排的思路,直接让它有序。

    假设现在有5个数,用什么可以在比较好的情况下让它有序?这里选择用直接插入排序,有人想用希尔,但希尔目的是优化时让大的数快速的走到后面,但此时数据量比较小,没必要用。冒泡和直接选择和插入没可比性。堆排序每次要建队在选,数据量小也没有必要。直接插入排序只要给的数据有一小段有序都是有优势的。

    在递归的角度,把最后一层消灭了可以减少一半递归,最后几层数据分别占总数据1/2、1/4、1/8,因此每次分下来到区间有10个数选key时用直接插入排序就可以减少3~4递归,差不多消灭80%以上(这里是按照最理想二分说的),实际分开不一定是理想二分,但肯定也有一些优化。我们把上述思路也叫小区间优化,小区间优化具体区间中有几个数时再优化自己定,但提前注意这样做目的是为了优化递归麻烦的效率,平时选10左右效果好,可以基本优化后三层。

    前面讲的三种快排方法,单独排的方法不一样,但递归思路一致,因此做如下改进为了后期方便调用,实际也的时候不用考虑这些:

// Hoare
int PartSort1(int* a, int left, int right)
{
	// 三数取中
	int midi = GetMidNumi(a, left, right);
	if (midi != left)
		Swap(&a[midi], &a[left]);

	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]);
	keyi = left;

	return keyi;
}

// 挖坑法
int PartSort2(int* a, int left, int right)
{
	// 三数取中
	int midi = GetMidNumi(a, left, right);
	if (midi != left)
		Swap(&a[midi], &a[left]);

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

// 前后指针法
int PartSort3(int* a, int left, int right)
{
	// 三数取中
	int midi = GetMidNumi(a, left, right);
	if (midi != left)
		Swap(&a[midi], &a[left]);

	int keyi = left;

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

		++cur;
	}

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

	return keyi;
}

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

    下面来完成小区间优化:

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

	//小区间优化
	if ((right - left + 1) > 10)
	{
		int keyi = PartSort3(a, left, right);
		QuickSort(a, left, keyi - 1);
		QuickSort(a, keyi + 1, right);
	}
	else
	{
		InsertSort(a + left, right - left + 1);
	}
}

    直接插入排序那里为什么要写a+left,因为二分后可能不是右边的区间,而是左边的区间。

    5.非递归

     快排有时候可能会面临将递归改为非递归的情况,为什么呢?因为只要涉及递归就会有这样的问题:1.效率问题(该问题影响并不是很大)。2.递归深度太深的时候栈区撑不住,会面临栈溢出。

    那如何将递归改为非递归呢?一种是直接改循环(如斐波那契);另一种是用栈辅助改循环。对于快速排序来说不好直接改循环,因此用栈辅助改循环。那怎么改呢?先来按照递归的思路走一走

    可以发现,每次递归变化的是递归区间,数组指并没有变,一直指向那个数组。因此递归栈帧中存的是区间,所以在栈中存区间就可以了。那怎么存呢?假设有10个数要排序,每次选的key都可以均匀的二分。

    现在先要排一个单趟,那就把区间0~9存进栈中,然后取出这个区间进行单趟排序,排完后假设keyi是5,然后可以继续分为左区间0~4,右区间6~9。继续先压区间6~9,再压区间0~4(栈是后进先出,也符合递归思路),每次单趟拍完后就把子区间入栈。然后出0~4,假设keyi是2,0~4在被分割为0~1和3~4。再先入3~4,再入0~1。取0~1再单趟排,假设此时keyi是1,区间被分0~0(一个区间不用入)和不存在(区间不存在不入)。再取3~4用上述同样方法,此时0~4就有序了,然后再用同样方法对待6~9,最后栈为空时循环结束。

    思路有了,代码怎么实现呢?比如怎么在栈中存区间:要么写个结构体,结构体体面放左右区间;要么就一次入两个,先入右在入左(可以确保先出左再出右)。比如0~9区间取出来了怎么办?现在就用快排单趟方式选一个keyi出来,然后会划分为两段区间,以前就分治递归了,现在就继续入栈就行,注意不用分的区间就不进了,区间不存在也不进。

void QuickSortNonR(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 = PartSort3(a, begin, end);
		//[begin,keyi-1]keyi[keyi+1,end]
		if (keyi + 1 < end)
		{
			STPush(&st, end);
			STPush(&st, keyi + 1);
		}

		if (keyi - 1 > begin)
		{
			STPush(&st, keyi - 1);
			STPush(&st, begin);
		}
	}
	STDestroy(&st);
}

7.归并排序

    说到归并可以想到,如果有两个数组是有序数组,那么可以将这两个数组归并为一个有序数组。现在只有一个数组,如果可以想办法将这一个数组分为左右两个区间,并使左右两个区间都有序,那么就可以对两个区间进行归并了:依次比较,将小的尾插到新空间。

    要想归并前提条件是左右区间都有序,有人说那就见到一个数组先分为左右两个区间,对左区间排序,再对右区间排序,然后再用归并的思想就可以了,这样最后可以排好序,但是假如有100W个数,分为50W和50W个,左边先排,右边再排,然后归并,这样消耗比较大,和直接对整体排没什么区别。再仔细想想,这个过程和以前建堆很像,对根向下调,但前提是左右子树都是大堆,就用了分治的思想。这里也是一样,也用类似的思想,比如现在想让8个数有序,那就把这8个数分为4个数和4个数归并,分别有序就直接归并,没有序就分别分为2个数和2个数,分别有序就直接归并,没有序就分别分为1个数和1个数;1个数分别是有序的,就归并为2个数有序,2个数和2个数再归并为4个数有序,4个数和4个数再归并为8个数有序。这个过程也像后续遍历一样,该过程感觉像是对称的走的:

    但实际是先走左,再走右,左右都回来做一次归并:

    这就是归并排序的大致思路,那归并排序的时间复杂度是多少呢?通过刚才分析可以看到,归并排序的深度是logN,向下分割并没有消耗,归并的时候有消耗,每一层归并的时间复杂度是O(N),因此整体的时间复杂度是O(N*logN)。

    那现在8个数分成4个和4个,4个数再分2个和2个…,是不是要开很多小数组呢?不会的,因为消耗太大,其实只用新开一个临时数组就可以了,归并的时候归并到新数组,归并完再拷贝回去,左边递归完再递归右边。向下走就是不断的分,回来的时候归并,最后完成后释放临时开的数据就可以了。理论上归并排序传参时也要传区间,但是因为有临时开数组这个操作,说明空间复杂度是O(N),如果直接传区间会在递归时涉及函数自己调用自己,这样会导致每次都要新开临时数组,不太合适,因此里面写一个子函数,在子函数中传区间。

void _MergeSort(int* a, int begin, int end, int* tmp)
{
	if (begin >= end)
	{
		return;
	}
	int mid = (begin + end) / 2;
	//分为[begin,mid][mid+1, end]这两端区间
	_MergeSort(a, begin, mid, tmp);
	_MergeSort(a, mid + 1, end, tmp);
	//此时[begin,mid][mid+1,end]这两段区间已经有序,进行归并
	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);
	if (tmp == NULL)
	{
		perror("malloc is fail");
		return;
	}
	_MergeSort(a, 0, n - 1, tmp);
	free(tmp);
}

    拷贝回去的时候注意区间位置,有时候可能在右边进行了分开。不管奇数偶数都适用,因为归并只要左右两边有序就可以归并。下面是递归展开图,可以帮助理解:

    1.归并排序的非递归

      前面快排中递归改非递归时我们借助了栈,那归并这里递归改非递归也要借助栈吗?这里不用,因为归并这里是一种后序的思想,左区间有序了,右区间有序了,再一归并整体就有序了。而借助栈实现有前序的思想,每次分为左右两个区间,先入右后入左,然后取左区间,再入左区间划分的左右区间…每次右区间回来时要继续取左区间来进行递归,因此用栈不太好弄。既然借助栈不太好弄,那就选择直接改循环就行。观察归并的递归可以发现,比如8个数,递归时把8个数分为4个和4个归,4个数分为2个和2个归,2个数分为1个和1个归。因此我们直接可以让这组数1个和1个递归,递归完后变2个和2个递归,递归完后再变4个和4个递归,每次递归完拷贝回去,最后一次递归完拷贝回去再释放临时数组就可以了。

    这种分析听起来感觉没什么问题,但其实里面有很多问题,这里先更具上述思路写一个大框架,后面慢慢分析问题。这里控制一个叫gap的变量,gap是归并过程中每组数据的个数,gap是1就代表1个和1个归,gap是2就代表2个和2个归……

void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc is fail");
		return;
	}
	int gap = 1;
	while (gap < n)
	{
		for (int i = 0; i < n; i += 2 * gap)   //先写好这里 i是每组数据起始位置
		{
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;
			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++];
			}
		}
		gap *= 2;
	}
	free(tmp);
}

     现在大框架就好了,好了后有这样几个问题:这里我们是按照偶数举例的,那如果不是偶数,比如说有7个数?当gap是4的时候,区间会被分为[0,3]和[4,7],但是数组下标实际上是[0,6],此时就有越界的问题,也就是[begin2,end2]出现了越界问题;还有拷贝时我们选择归并一小组拷贝回去一小组好(也就是[0,0][1,1]归完拷回去,[2,2][3,3]归完拷回去)还是说归并完一大组再拷回去好呢([0,0],[1,1]归完,[2,2][3,3]归完…都好后整体拷回去)。

     上面有很多问题,数据拷贝问题看字面说法感觉归完一大组后在拷回去比较省事,这里先按照这种方法来,然后给偶数个数(测试8个)看看能不能完成排序:

void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc is fail");
		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;
			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, tmp, sizeof(int) * n);
		gap *= 2;
	}
	free(tmp);
}

    8个数据感觉没有什么问题,也没有出现错误,现在多给一个数据,比如多给个11再次运行看看情况如何:

    此时就发生了错误,程序崩了,为了更方便的找到问题,方便观察越界情况,我们每次把归并区间打印出来:

void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc is fail");
		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;

			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++];
			}
		}
		printf("\n");
		memcpy(a, tmp, sizeof(int) * n);
		gap *= 2;
	}
	free(tmp);
}

    打印出来归并区间后来进行分析:

    可以发现这里end2会越界,begin2会越界,end1也会越界,只有begin1不会越界。因为begin1每次都是i,i < n才会进循环,这里这么多越界,非常复杂,那怎么处理呢?一般遇到复杂的问题,我们试着把复杂的问题拆为简单的问题,也就是分类处理。根据上面分析,begin1不可能越界,那就有这样几类问题:1.end1越界。如果end1越界了,后面肯定也是越界的,因此就不用在归并了,那需要将剩下的数据拷贝到临时数组中吗?这里需要,因为前面我们的拷贝是一大组归并完整体拷贝回去,如果不把剩下的数据拷贝到临时数组中整体拷贝回去的时候会有随机值覆盖原数组剩余的值。所以这也暴露出一把拷回去的缺点,不得不把剩余边边角角的数据弄下来。2.end1没有越界,begin2越界了。这里和刚才第一点的处理方法一模一样,不需要归并,拷贝就行。3.end1和begin2没有越界,end2越界了。这里还要继续归并,只是要修改end2到n-1就行了。下面就更具这些问题进行修正。

    当end1越界时,有时候容易改成这个样子:

if (end1 >= n)
{
	end1 = n - 1;
	begin2 = n - 1;
	end2 = n - 1;
}

    这样是不对的,比如[8,9][10,11],都修改为8对应的下标后,会多归并一次。因此直接改为区间不存在就可以了,这样可以不进循环然后借助后面的循环拷贝,再整体拷贝回来。

void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc is fail");
		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;

			//printf("[%d,%d][%d,%d] ", 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] ", 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++];
			}
		}
		printf("\n");
        // 间距为gap的多组数据,归并完以后,一把拷贝
		memcpy(a, tmp, sizeof(int) * n);
		gap *= 2;
	}
	free(tmp);
}

     这样就成功了,end1越界和begin1越界可以放在一起处理吗?可以,但分开逻辑显得更清晰。

    前面说了整体拷贝回去的缺点,那如果不整体拷贝而是一小组一组拷贝呢?这样遇到end1越界或者end1不越界begin2越界时,不用归并,也不用考虑往临时数组拷贝数据了,直接break就行。

void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc is fail");
		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;

			//printf("[%d,%d][%d,%d] ", begin1, end1, begin2, end2);
			//修正
			if (end1 >= n || begin2 >= n)
			{
				break;
			}
			else 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);
}

  2.补充

    归并排序除了运用在内排序(内存中排)外还会运用于外排序(磁盘中排),什么场景会去磁盘中排序呢?就是当数据量非常大的时候。比如500G的数据在磁盘中怎么用归并排?这里不用其他排序的原因是磁盘中不支持随机访问。这里可以用递归思想把500G分为250G,为了使250G有序继续分,但比较麻烦。可以分为500个1G的文件,1G的数据读内存中用效率高的排序直接排好,排好后每个1G文件都有序,然后1G归并为2G,2G归并为4G……就可以了。

8.计数排序

    前面提到的排序都是通过比较大小来进行排序的,而计数排序不通过比较大小进行排序。它的第一步是去统计数据出现的次数,怎么统计呢?就是待排序的数据中最大值是几就开几+1的空间,然后给每个位置一些专门的坑位,这些坑位是[0,max],这些专属坑位就是用来统计次数的。

    刚开始新开的空间中每个坑位都默认是0,遍历一遍原数组,出现几就在所对应的坑位上计数(如出现6在6的位置++)。

    第二部就是排序,遍历计数数组,对原数组进行覆盖,坑位上有几个数就对原数组覆盖几个坑位(如坑位1上有2,那就覆盖2次1)。

    这样就完成了计数排序,但如果遇到这样的情况:

    这样需要开110个空间,而且计数的坑位集中在后面,待排序的值都映射到了后面的区域,这样不太好,相当于前面的空间白开了一样。因此需要优化,这里采用相对映射,用最大值-最小值作为最大值坑位,遍历原数组时让每个值都减最小值存再到相对应的坑位计数。

    排序的时候让计数坑位+最小值覆盖原数组就可以了。

void CountSort(int* a, int n)
{
	int max = a[0];
	int min = a[0];
	for (int i = 1; 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);
	if (countA == NULL)
	{
		perror("malloc is fail");
		return;
	}

	memset(countA, 0, sizeof(int) * range);

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

	free(countA);
}

    这就是计数排序,有了相对映射,对负数也同样可以排序。计数排序第一步要遍历一遍原数组,时间复杂度是O(N);第二步遍历计数数组,计数数组大小取决于原数组最大值和最小值,时间复杂度是O(range)。总体来说计数排序时间复杂度是O(N)+O(range),因此计数排序适合对范围集中,且范围不大的整形数组排序。不适合对范围分散或者非整形数据排序。

9.排序算法复杂度及稳定性分析

    什么是排序算法的稳定性?通俗的讲就是排完序后相同数据的相对顺序不变。

    比如说现在对一组数据排序,排序前红5在蓝5的前面,排序后红5依旧在蓝5的前面,这样排序就是稳定的,否则就是不稳定的。

    那稳定性在实际中有用吗,可以看这样的场景:比如有某个比赛,要求分高的排名在前,相同分数按照提交时间来进行排序,每个人提交完后就会将成绩依次保留下来,这样直接用稳定的排序排就可以了。再比如考试科目多,我们把所有科目单个分数和总分存结构体中,排名时总分一样按照语文成绩高低来排先后,这里就可以先按照语文成绩排,排好后用稳定的排序对总分排。

    下面这个表对排序做了总结,接下来依次分析一下稳定性:

    冒泡排序:两两元素交换,我们可以控制元素相等时不交换,因此稳定。

    简单选择排序:比如{8 1 1 8},找出最大最小数后交换完相对顺序就变了,因此不稳定。

    直接插入排序:比如{2 2 2 2}相等时不移动数据就行,因此稳定。

    希尔排序:相同的数据可能会被分到不同组预排,因此不稳定。

    堆排序:向下调整时相对顺序会变,因此不稳定。

    归并排序:一样时让左边区间数据先下了就行,因此是稳定的。

    快速排序:比如{5 1 2 5 6 7 5 2},左边做key排好后不能动了,因此不稳定。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值