数据结构-第5节-排序

目录

1.排序的概念及其运用

1.1.排序的概念

1.2.排序运用

1.3.常见的排序算法

2.常见排序算法的实现

2.1.直接插入排序

2.2.希尔排序( 缩小增量排序 )

2.3.冒泡排序

2.4.直接选择排序

2.5.堆排序

2.6.快速排序

2.6.1.三种单趟排序

2.6.2.三种单趟排序比较

2.6.3.快速排序特性总结

2.6.4.快速排序优化

2.7.归并排序

2.7.1.归并排序

2.7.2.归并排序优化

2.7.3.归并排序实现外排序

2.8.非比较排序

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


1.排序的概念及其运用

1.1.排序的概念

排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。

1.2.排序运用

1.3.常见的排序算法


2.常见排序算法的实现

2.1.直接插入排序

直接插入排序是一种简单的插入排序法,其基本思想是:

把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列。
实际中我们玩扑克牌时,就用了插入排序的思想
代码思想:
当插入第i(i>=1)个元素时,前面的array[0],array[1],…,array[i-1]已经排好序,此时用array[i]的排序码与array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移。

Test.c文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include "Sort.h"

void TestInsertSort()
{
	int a[] = { 9, 1, 2, 5, 7, 4, 8, 6, 3, 5 };
	InsertSort(a, sizeof(a) / sizeof(int));
	PrintArray(a, sizeof(a) / sizeof(int));
}


int main()
{
	TestInsertSort();

	return 0;
}

Sort.h文件:

#pragma once
#include <stdio.h>

void PrintArray(int* a, int n);
void InsertSort(int* a, int n);

Sort.c文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include "Sort.h"

void PrintArray(int* a, int n)
{
	for (int i = 0; i < n; ++i)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}


void InsertSort(int* a, int n)
{
	for (int i = 0; i < n - 1; ++i)
	{
		int end = i;
		// 单趟排序:[0, end]有序 end+1位置的值,插入进入,保持他依旧有序
		int tmp = a[end + 1];
		while (end >= 0)
		{
			if (tmp < a[end])
			{
				a[end + 1] = a[end];
				--end;
			}
			else
			{
				break;
			}
		}
		a[end + 1] = tmp;
	}
}

注:

1.如果单趟排序代码如下图第一行左所示,那么当下面右图所示情况的话(就是插入的数值要放在第一个位置),到最后所有的数都往后移动一个位置,并且end指向-1的位置,此时end小于0结束,数据并没有放进去,改进代码如下图第二行所示

   

2.插入排序的时间复杂度:O(N^{2})

最坏:逆序的情况,O(N^{2}

最好:顺序有序,O(N)

当要排序的序列大部分都已排好,只有个别数据有错时,插入排序的效率很高

2.2.希尔排序( 缩小增量排序 )

思路(以从小到大排序为例):

1.预排序:分组排序,使大的数更快的到后面,小的数更快的到前面,使序列接近有序

(1)将序列进行分组(gap为几就分成了几组)

         

(2)分别使用插入排序的思想对这gap组数据进行排序

       

        注:如果gap越小,越接近有序,大的数据到最后的速度较慢,小的数据到前面的速度                 较慢

               如果gap越大,大的数据可以更快的到后面,小的数据可以更快的到前面,但是他                 越不接近有序

2.直接插入排序

Test.c文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include "Sort.h"

void TestShellSort()
{
	int a[] = { 9, 1, 2, 5, 7, 4, 8, 6, 3, 5 };
	ShellSort(a, sizeof(a) / sizeof(int));
	PrintArray(a, sizeof(a) / sizeof(int));

}


int main()
{
	TestShellSort();

	return 0;
}

Sort.h文件:

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

void PrintArray(int* a, int n);

void ShellSort(int* a, int n);

Sort.c文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include "Sort.h"

void PrintArray(int* a, int n)
{
	for (int i = 0; i < n; ++i)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}

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

注:

1.预排序有两种写法:

第一种是使用三层循环,其中最里面的while循环是将一个tmp插入到前面排好的序列中,中间的for循环是控制一组数据进行排序,外面的for循环控制gap组数据进行排序

第二种是使用两层循环,其中里面的while循环是将一个tmp插入到前面排好的序列中,外面的for循环是从前往后多组同时进行排序

2.gap到底取多少(到底分多少组),取决于需要排序序列的数据个数,数据越多,gap的取值也应该越大。我们可以让gap从大到小多次预排列,到最后gap等于一时,就是直接插入排序,希尔排序结束。让gap每次减小进行预排序,我们使用gap=gap/3+1,这样我们就保证了最后一次一定是gap=1进行直接插入排序。

3.希尔排序的时间复杂度:

gap开始很大,到后面慢慢gap=gap/3+1变小

开始当gap很大时,数据跳的很快,里面的while循环可以忽略不计,时间复杂度约为O(N)

后来当gap很小时,此时序列已经很接近有序了,时间复杂度约为O(N)

因此,无论gap很大还是很小,总体可以理解为下面红框里的程序只要运行一次,时间复杂度就为O(N),因为gap初始为n,gap=gap/3+1,那么红框里的程序运行约log_{3}^{N}次,因此总的时间复杂度约为O(N\times log_{3}^{N})(log以3为底是因为我们这里是gap=gap/3+1变小,如果gap=gap/2那么就是log以2为底)

根据研究者们研究,综合各种情况,发现希尔排序的时间复杂度约为O(N^{1.3})

4.那么希尔排序 和 直接插入排序孰优孰劣,我们可以通过代码运行时间来判断,clock函数是获取计算机执行到该函数时的毫秒数,代码如下图所示,我们可以看出希尔排序比直接插入排序效率要高很多
Test.c文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include "Sort.h"

void TestOP()
{
	srand(time(0));
	const int N = 10000;
	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;
}

Sort.h文件:

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

void PrintArray(int* a, int n);

void InsertSort(int* a, int n);

void ShellSort(int* a, int n);

Sort.c文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include "Sort.h"

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


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

运行结果:

2.3.冒泡排序

Test.c文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include "Sort.h"

void TestBubbleSort()
{
	int a[] = { 9, 1, 2, 5, 7, 4, 8, 6, 3, 5 };
	BubbleSort(a, sizeof(a) / sizeof(int));

	PrintArray(a, sizeof(a) / sizeof(int));
}

int main()
{
	TestBubbleSort();

	return 0;
}

Sort.h文件:

#pragma once
#include <stdio.h>

void PrintArray(int* a, int n);

void BubbleSort(int* a, int n);

Sort.c文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include "Sort.h"

void PrintArray(int* a, int n)
{
	for (int i = 0; i < n; ++i)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}

void Swap(int* pa, int* pb)
{
	int tmp = *pa;
	*pa = *pb;
	*pb = tmp;
}

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

		if (exchange == 0)
		{
			break;
		}
	}
}

注:

1.冒泡排序的时间复杂度:O(N^{2})

最坏:逆序的情况,O(N^{2}

最好:上面代码用exchange置零的方法进行优化,如果遇到刚好是顺序有序的序列,第一次大循环两两比较发现没有进行交换,直接跳出循环,时间复杂度O(N),如果不是顺序有序序列,时间复杂度 O(N^{2})。如果不进行优化,那么冒泡排序时间复杂度都为 O(N^{2}) 。

2.冒泡排序和插入排序想比较:

如果是顺序有序,那么插入和冒泡是一样的

如果是局部有序或者接近有序,那么插入适应性和比较次数更少

2.4.直接选择排序

基本思想:
每一次从待排序的数据元素中选出最小(或最大)的一个元素,与起始位置元素交换,然后剩下的元素重复前面操作,直到最后剩一个元素为止
优化版本:
每一次从待排序的数据元素中选出最小和最大的两个元素,分别与起始位置和末尾位置元素交换,然后剩下的元素重复前面操作,直到最后剩一个元素或没有剩下元素为止

Test.c文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include "Sort.h"

void TestSelectSort()
{
	int a[] = { 9, 1, 2, 5, 7, 4, 8, 6, 3, 5 };
	SelectSort(a, sizeof(a) / sizeof(int));
	PrintArray(a, sizeof(a) / sizeof(int));
}


int main()
{
	TestSelectSort();

	return 0;
}

Sort.h文件:

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

void PrintArray(int* a, int n);

void SelectSort(int* a, int n);

Sort.c文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include "Sort.h"

void PrintArray(int* a, int n)
{
	for (int i = 0; i < n; ++i)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}

void Swap(int* pa, int* pb)
{
	int tmp = *pa;
	*pa = *pb;
	*pb = tmp;
}

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

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

		Swap(&a[left], &a[mini]);
		Swap(&a[right], &a[maxi]);

		left++;
		right--;
	}
}

注:

1.下面左代码是有问题的,因为如果序列是9 1 2 5 7 4 8 6 3 5,那么第一次,left=0  right=9 mini=1 maxi=0,先执行Swap(&a[left], &a[mini]),此时序列为1 9 2 5 7 4 8 6 3 5,然后执行Swap(&a[right], &a[maxi]),此时序列为5 9 2 5 7 4 8 6 3 1,这并不是我们预想的结果,这种情况是因为left和maxi重叠,最小值换到left时,max被掉包换走了,换在了mini的位置。我们需要进行修正,如下图右代码

 

2.直接选择排序的时间复杂度:

最坏情况:O(N^{2})

最好情况:O(N^{2})

直接选择排序的时间复杂度: O(N^{2})

2.5.堆排序

Test.c文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include "Sort.h"

void TestHeapSort()
{
	int a[] = { 9, 1, 2, 5, 7, 4, 8, 6, 3, 5 };
	HeapSort(a, sizeof(a) / sizeof(int));
	PrintArray(a, sizeof(a) / sizeof(int));
}


int main()
{
	TestHeapSort();

	return 0;
}

Sort.h文件:

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

void PrintArray(int* a, int n);

void AdjustDwon(int* a, int n, int root);
void HeapSort(int* a, int n);

Sort.c文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include "Sort.h"

void PrintArray(int* a, int n)
{
	for (int i = 0; i < n; ++i)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}

void Swap(int* pa, int* pb)
{
	int tmp = *pa;
	*pa = *pb;
	*pb = tmp;
}

void AdjustDown(int* a, size_t size, size_t root)
{
	size_t parent = root;
	size_t child = parent * 2 + 1;
	while (child < size)
	{
		// 1、选出左右孩子中小的那个
		if (child + 1 < size && a[child + 1] > a[child])
		{
			++child;
		}

		// 2、如果孩子小于父亲,则交换,并继续往下调整
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

// O(logN*N)
void HeapSort(int* a, int n)
{
	// 向上调整--建堆 O(N*logN)
	//for (int i = 1; i < n; ++i)
	//{
	//	AdjustUp(a, i);
	//}

	// 向下调整--建堆 O(N)
	for (int i = (n - 1 - 1) / 2; i >= 0; --i)
	{
		AdjustDown(a, n, i);
	}

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

2.6.快速排序

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法
其基本思想为:
任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

2.6.1.三种单趟排序

将区间按照基准值划分为左右两半部分(单趟排序)进行分类,常见方式有:
一.hoare版本

hoare版本单趟排序:

1.思路(升序排序为例):

假设keyi取第一个位置,R从右往左找比Keyi处小的值,L从左往右找比Keyi处大的值,找到之后交换,再继续找,相遇以后(这里一定会相遇,因为最后一定是一个不动另一个在往下找然后相遇)把相遇位置的值跟keyi位置的值交换。

因为keyi是第一个位置,所以最后要保证交换前相遇点位置的值比keyi处值要小,这样才能确保交换后相遇点左边的值都比keyi处值小右边的值都比keyi处值大。如何保证交换前相遇点位置的值比keyi处值要小呢?答案是让右边先走(左右交换完了也是右边先走),解释如下(两种情况):

(1)如果相遇是因为右边停下左边来相遇,那么相遇点就是右边停下的这个位置,该位置             的值一定比keyi处值小

(2)如果相遇是因为左边停下右边来相遇,那么相遇点就是左边停下的这个位置,这个位             置的数因为是之前和右交换过的数,因此也一定是比keyi处值小的

注:

1.如果keyi取第一个位置,那么应该让右边先走

   如果keyi取最后一个位置,那么应该让左边先走

2.hoare法一次单趟排序时间复杂度为O(N)

2.过程:

1.2.

3. 4.

5. 6.

7. 8.

9. 10.

11. 12.

13. 14.

15. 16.

3.代码:

int PartSort(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;
}

注:

1.下面代码是有问题的,因为如果序列是5 5 2 3 5,那么right永远指向最右边的5,left永远指向最左边的5,程序死循环,因此应该改成当a[right]>=a[keyi]时--right,当a[left]<=a[keyi]时++left。

但是如果两个修改成大于等于的话,如下图所示,对于序列1 2 3 4 5,right从最右边开始遍历整个序列都大于等于keyi,然后再--,越界访问。因此条件应改成当left < right && a[right] >= a[keyi]时--right,当left < right && a[left] <= a[keyi]时++left。

正确代码如下图所示

hoare版本快速排序:

1.思路:

一次单趟排序完后的keyi对应元素在该位置就不会再动了,已经放在了正确的位置,将该位置赋值给keyi,因此可以分为keyi左边的序列和keyi右边的序列,左边右边两个序列都有序那么整个序列都有序了。左边和右边如何有序呢?

答案是分支解决子问题,分别对左边序列和右边序列再进行单趟排序,以此类推,当子序列只有一个值或者不存在(begin>=end)就结束

注:这里面不是以子序列只有一个值(begin=end)作为结束标志,因为子序列是有可能不存在的(begin>end),如下图所示,左下角2 1序列进行单趟排序为1 2序列,此时keyi为1,再往下递归左序列的begin=0 、end=keyi-1=0,右序列begin=keyi+1=2、end=1,做序列有一个数据,右序列不存在,都应结束

 2.代码:

Test.c文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include "Sort.h"

void TestQuickSort()
{
	int a[] = { 9, 1, 2, 5, 7, 4, 8, 6, 3, 5 };
	QuickSort(a, 0, sizeof(a) / sizeof(int) - 1);

	PrintArray(a, sizeof(a) / sizeof(int));
}


int main()
{
	TestQuickSort();

	return 0;
}

Sort.h文件:

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

void PrintArray(int* a, int n);

void QuickSort(int* a, int begin, int end);

Sort.c文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include "Sort.h"

void PrintArray(int* a, int n)
{
	for (int i = 0; i < n; ++i)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}

void Swap(int* pa, int* pb)
{
	int tmp = *pa;
	*pa = *pb;
	*pb = tmp;
}

int PartSort(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;
}

void QuickSort(int* a, int begin, int end)
{
	// 子区间相等只有一个值或者不存在那么就是递归结束的子问题
	if (begin >= end)
		return;

	int keyi = PartSort(a, begin, end);
	// [begin, keyi-1]keyi[keyi+1, end]
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}

注:上面代码可以画递归展开图加深理解,并结合代码调试进行验证

二.挖坑法版本

挖坑法版本单趟排序:

1.思路(升序排序为例):

假设key取第一个位置的值,将第一个位置的值保存在变量key中,这样第一个位置就是坑位可以被覆盖,R从右往左找比key小的值,将找到的值放到坑位处,此时R位置处就是新的坑位,L从左往右找比key大的值,将找到的值放到坑位处,此时L位置处就是新的坑位,R再从右往左找以此类推,直到L和R相遇,此时相遇的位置也就是坑位的位置(左边先走右边一定是坑,右边先走左边一定是坑,最后相遇一定是在坑上),将key中保存的值填进坑位即可

注:

1.如果key取第一个位置的值,那么应该让右边先走

   如果key取最后一个位置的值,那么应该让左边先走

2.挖坑法相比于hoare法更好理解:

(1)不需要理解为什么最终相遇位置比key小

(2)不需要理解为什么左边做key,右边先走

3.挖坑法相比于hoare法效率上能好一点点,但对于计算机来说二者差别不大

4.挖坑法一次单趟排序时间复杂度为O(N)

2.过程

1. 2.

3. 4.

5. 6.

  

7. 8.

  

9. 10.

11. 12.

13. 14.

  

3.代码:

int PartSort(int* a, int left, int right)
{
	int key = a[left];
	// 坑位
	int pit = left;
	while (left < right)
	{
		// 右边先走,找小
		while (left < right && a[right] >= key)
		{
			--right;
		}

		a[pit] = a[right];
		pit = right;

		// 左边走,找大
		while (left < right && a[left] <= key)
		{
			++left;
		}

		a[pit] = a[left];
		pit = left;
	}

	a[pit] = key;
	return pit;
}

挖坑法版本快速排序:

1.思路:

挖坑法版本和hoare版本只是单趟排序有所差异,后面的快速排序进行递归是完全相同的

2.代码:

Test.c文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include "Sort.h"

void TestQuickSort()
{
	int a[] = { 6,1,2,7,9,3,4,5,10,8 };
	QuickSort(a, 0, sizeof(a) / sizeof(int)-1);

	PrintArray(a, sizeof(a) / sizeof(int));
}


int main()
{
	TestQuickSort();

	return 0;
}

Sort.h文件:

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

void PrintArray(int* a, int n);

void QuickSort(int* a, int begin, int end);

Sort.c文件:

#define _CRT_SECURE_NO_WARNINGS 1

#include "Sort.h"

void PrintArray(int* a, int n)
{
	for (int i = 0; i < n; ++i)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}


int PartSort(int* a, int left, int right)
{
	int key = a[left];
	// 坑位
	int pit = left;
	while (left < right)
	{
		// 右边先走,找小
		while (left < right && a[right] >= key)
		{
			--right;
		}

		a[pit] = a[right];
		pit = right;

		// 左边走,找大
		while (left < right && a[left] <= key)
		{
			++left;
		}

		a[pit] = a[left];
		pit = left;
	}

	a[pit] = key;
	return pit;
}

void QuickSort(int* a, int begin, int end)
{
	// 子区间相等只有一个值或者不存在那么就是递归结束的子问题
	if (begin >= end)
		return;

	int keyi = PartSort(a, begin, end);
	// [begin, keyi-1]keyi[keyi+1, end]
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}

三.前后指针版本

前后指针版本单趟排序:

1.思路(升序排序为例):

假设keyi取第一个位置,开始prev取第一个位置cur取第二个位置,cur往后走找比keyi处值小的值,找到之后prev++,然后交换prev和cur位置的值,cur再往后找比keyi处值小的值以此类推,最后当cur走到最后一个位置后面的位置,此时prev处的值是比keyi处值小的值,prev后面的值都是比keyi处值大的值,将prev处的值和keyi处的值交换即可,单趟排序结束

prev和cur的关系:

(1)cur还没遇到比keyi大的值时,prev紧跟着cur,一前一后

(2)cur遇到比keyi大的值以后,prev和cur之间间隔着一段比keyi大的值的区间

注:

1.如果keyi取第一个位置,开始prev取第一个位置cur取第二个位置,最后当cur走到最后一个位置后面的位置,将prev处的值和keyi处的值交换,单趟排序结束。

   如果keyi取最后一个位置,开始prev取第一个位置前面的位置cur取第一个位置,最后当cur走到最后一个位置,将prev++处的值(此时prev处的值是小于keyi处值的,而prev后面的值都是大于keyi处值的,因此应该用prev++进行交换)和keyi处的值交换,单趟排序结束,具体代码如下图所示

2.过程:

1.2.

3.4.

5.6.

7.8.

9.10.

11.12.

13.14.

15.16.

3.代码:

int PartSort(int* a, int left, int right)
{

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

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

	return prev;
}

注:

1.下面左边代码是有问题的,因为变量key是一个局部变量,存储的是第一个位置的数值,取key的地址进行交换,序列第一个位置的数据是不会改变的。我们可以用keyi保存第一个位置的下标,然后取a[keyi]的地址进行交换,这样第一个位置的数据就会被交换,如下面右边代码所示

前后指针版本快速排序:

1.思路:

前后指针版本和hoare版本只是单趟排序有所差异,后面的快速排序进行递归是完全相同的

2.代码:

Test.c文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include "Sort.h"

void TestQuickSort()
{
	int a[] = { 6,1,2,7,9,3,4,5,10,8 };
	QuickSort(a, 0, sizeof(a) / sizeof(int)-1);

	PrintArray(a, sizeof(a) / sizeof(int));
}


int main()
{
	TestQuickSort();

	return 0;
}

Sort.h文件:

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

void PrintArray(int* a, int n);

void QuickSort(int* a, int begin, int end);

Sort.c文件:

#define _CRT_SECURE_NO_WARNINGS 1

#include "Sort.h"

void PrintArray(int* a, int n)
{
	for (int i = 0; i < n; ++i)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}

void Swap(int* pa, int* pb)
{
	int tmp = *pa;
	*pa = *pb;
	*pb = tmp;
}

int PartSort(int* a, int left, int right)
{

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

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

	return prev;
}


void QuickSort(int* a, int begin, int end)
{
	// 子区间相等只有一个值或者不存在那么就是递归结束的子问题
	if (begin >= end)
		return;

	int keyi = PartSort(a, begin, end);
	// [begin, keyi-1]keyi[keyi+1, end]
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}

2.6.2.三种单趟排序比较

三种单趟排序方法,排序后的序列不一定相同

例如:对于序列6 1 2 7 9 3 4 5 10 8

2.6.3.快速排序特性总结

1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫 快速排序
2. 时间复杂度:O( N \times log_{2}^{N})
最好的情况:每次选key都是中位数,时间复杂度O( N \times log_{2}^{N}),如下图所示,第一行一个                       序列单趟排序为N,第二行两个序列单趟排序为2N可以看成是N........以此类                           推,每一行都为N,有高度为 log_{2}^{N}行,因此时间复杂度O( N \times log_{2}^{N})

最坏的情况(有序数列):每次选key都是最小或最大的,时间复杂度O(N^{2}),如下图所                                                   示,每一行都是一个序列单趟排序为N,有高度为N行,因此时                                               间复杂度O(N^{2})

快速排序的时间复杂度:虽然有序或接近有序的数列快速排序效率很低,但是我们有办法进行优化,优化方法下面会说,经过优化后,快速排序总的来说时间复杂度约为O(N \times log_{2}^{N})

2.6.4.快速排序优化

一. 三数取中优化

前面提到如果是有序或接近有序的序列,前面的快速排序方法效率会很低,因此我们需要对其进行优化,优化方法有以下两种思路:

1.随机选key

2.三数取中(选不是最大,也不是最小的那个)

下面我们用思路2实现,得到中间的那个数之后与最左边(最右边)的数交换,后面keyi会拿到最左边(最右边)的数下标,此时keyi对应的数就是得到的中间数

Test.c文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include "Sort.h"

void TestQuickSort()
{
	int a[] = { 6,1,2,7,9,3,4,5,10,8 };
	QuickSort(a, 0, sizeof(a) / sizeof(int)-1);

	PrintArray(a, sizeof(a) / sizeof(int));
}


int main()
{
	TestQuickSort();

	return 0;
}

Sort.h文件:

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

void PrintArray(int* a, int n);

void QuickSort(int* a, int begin, int end);

Sort.c文件:

#define _CRT_SECURE_NO_WARNINGS 1

#include "Sort.h"

void PrintArray(int* a, int n)
{
	for (int i = 0; i < n; ++i)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}

void Swap(int* pa, int* pb)
{
	int tmp = *pa;
	*pa = *pb;
	*pb = tmp;
}

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

int PartSort(int* a, int left, int right)
{
	int midi = GetMidIndex(a, left, right);
	Swap(&a[midi], &a[left]);

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

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

	return prev;
}


void QuickSort(int* a, int begin, int end)
{
	// 子区间相等只有一个值或者不存在那么就是递归结束的子问题
	if (begin >= end)
		return;

	int keyi = PartSort(a, begin, end);
	// [begin, keyi-1]keyi[keyi+1, end]
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}

注:

1.上面代码是对前后指针版本快速排序进行优化,hoare版快速排序和挖坑法快速排序同理

2.上面这种优化后的快速排序就没有了时间复杂度最坏的情况,综合各种场景来说是效率非常高的一种排序方法

二. 小区间优化

前面的快速排序方法,递归到后面对于只有几个数的某段序列进行排序,还需要进行多次递归才能排好,效率很低。如下图所示,对于3 1 2 5 4这个序列,还需进行7次递归调用才能完成排序。对于这种的小区间相比于快速排序我们可以选用其他排序法效率可能会更高,一般选用的是直接插入排序法

Test.c文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include "Sort.h"

void TestQuickSort()
{
	int a[] = { 6,1,2,7,9,3,4,5,10,8 };
	QuickSort(a, 0, sizeof(a) / sizeof(int)-1);

	PrintArray(a, sizeof(a) / sizeof(int));
}


int main()
{
	TestQuickSort();

	return 0;
}

Sort.h文件:

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

void PrintArray(int* a, int n);

void InsertSort(int* a, int n);

void QuickSort(int* a, int begin, int end);

Sort.c文件:

#define _CRT_SECURE_NO_WARNINGS 1

#include "Sort.h"

void PrintArray(int* a, int n)
{
	for (int i = 0; i < n; ++i)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}

void Swap(int* pa, int* pb)
{
	int tmp = *pa;
	*pa = *pb;
	*pb = tmp;
}

void InsertSort(int* a, int n)
{
	for (int i = 0; i < n - 1; ++i)
	{
		int end = i;
		// 单趟排序:[0, end]有序 end+1位置的值,插入进入,保持他依旧有序
		int tmp = a[end + 1];
		while (end >= 0)
		{
			if (tmp < a[end])
			{
				a[end + 1] = a[end];
				--end;
			}
			else
			{
				break;
			}
		}
		a[end + 1] = tmp;
	}
}

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

int PartSort(int* a, int left, int right)
{
	int midi = GetMidIndex(a, left, right);
	Swap(&a[midi], &a[left]);

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

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

	return prev;
}


void QuickSort(int* a, int begin, int end)
{
	// 子区间相等只有一个值或者不存在那么就是递归结束的子问题
	if (begin >= end)
		return;

	// 小区间直接插入排序控制有序
	if (end - begin + 1 <= 30)
	{
		InsertSort(a + begin, end - begin + 1);
	}
	else
	{
		int keyi = PartSort(a, begin, end);
		// [begin, keyi-1]keyi[keyi+1, end]
		QuickSort(a, begin, keyi - 1);
		QuickSort(a, keyi + 1, end);
	}

}

三. 非递归优化

前面的快速排序是利用对单趟排序递归的方法实现的,但是当序列过大时可能会有栈溢出的风险,我们可以将递归形式优化成非递归的形式。

将递归改成非递归一般有两种方法:

(1)利用循环进行修改

(2)利用栈进行修改

一般简单一点的递归可以直接用循环修改,复杂一点的递归循环搞不定就用栈来修改,栈里面是动态开辟空间的,开辟的空间存放在堆中,在32位linux下,堆的内存空间位2G左右,栈的内存空间在8M左右,因此我们可以不用去考虑堆溢出的问题,下面我们就用栈来进行优化。

Test.c文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include "Sort.h"

void TestQuickSort()
{
	int a[] = { 6,1,2,7,9,3,4,5,10,8 };
	QuickSort(a, 0, sizeof(a) / sizeof(int)-1);

	PrintArray(a, sizeof(a) / sizeof(int));
}


int main()
{
	TestQuickSort();

	return 0;
}

Sort.h文件:

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

void PrintArray(int* a, int n);

void InsertSort(int* a, int n);

void QuickSort(int* a, int begin, int end);

Sort.c文件:

#define _CRT_SECURE_NO_WARNINGS 1

#include "Sort.h"
#include "Stack.h"

void PrintArray(int* a, int n)
{
	for (int i = 0; i < n; ++i)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}

void Swap(int* pa, int* pb)
{
	int tmp = *pa;
	*pa = *pb;
	*pb = tmp;
}

void InsertSort(int* a, int n)
{
	for (int i = 0; i < n - 1; ++i)
	{
		int end = i;
		// 单趟排序:[0, end]有序 end+1位置的值,插入进入,保持他依旧有序
		int tmp = a[end + 1];
		while (end >= 0)
		{
			if (tmp < a[end])
			{
				a[end + 1] = a[end];
				--end;
			}
			else
			{
				break;
			}
		}
		a[end + 1] = tmp;
	}
}

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

int PartSort(int* a, int left, int right)
{
	int midi = GetMidIndex(a, left, right);
	Swap(&a[midi], &a[left]);

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

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

	return prev;
}


void QuickSort(int* a, int begin, int end)
{
	ST st;
	StackInit(&st);
	StackPush(&st, begin);
	StackPush(&st, end);

	while (!StackEmpty(&st))
	{
		int right = StackTop(&st);
		StackPop(&st);
		int left = StackTop(&st);
		StackPop(&st);

		int keyi = PartSort(a, left, right);
		// [left,keyi-1][keyi+1,right]
		if (left < keyi - 1)
		{
			StackPush(&st, left);
			StackPush(&st, keyi - 1);
		}

		if (keyi + 1 < right)
		{
			StackPush(&st, keyi + 1);
			StackPush(&st, right);
		}
	}

	StackDestory(&st);

}

Stack.h文件:

#pragma once

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>


typedef int STDataType;

typedef struct Stack
{
	STDataType* a;
	int top;		// 栈顶的位置
	int capacity;	// 容量
}ST;

void StackInit(ST* ps);
void StackDestory(ST* ps);
void StackPush(ST* ps, STDataType x);
void StackPop(ST* ps);
bool StackEmpty(ST* ps);
int StackSize(ST* ps);
STDataType StackTop(ST* ps);

Stack.c文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include "Stack.h"

void StackInit(ST* ps)
{
	assert(ps);
	ps->a = NULL;
	ps->top = 0;
	ps->capacity = 0;
}

void StackDestory(ST* ps)
{
	assert(ps);
	free(ps->a);
	ps->a = NULL;
	ps->capacity = ps->top = 0;
}

void StackPush(ST* ps, STDataType x)
{
	assert(ps);
	// 
	if (ps->top == ps->capacity)
	{
		int newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
		ps->a = (STDataType*)realloc(ps->a, newCapacity * sizeof(STDataType));
		if (ps->a == NULL)
		{
			printf("realloc fail\n");
			exit(-1);
		}

		ps->capacity = newCapacity;
	}

	ps->a[ps->top] = x;
	ps->top++;
}

void StackPop(ST* ps)
{
	assert(ps);
	assert(ps->top > 0);
	--ps->top;
}

bool StackEmpty(ST* ps)
{
	assert(ps);

	/*if (ps->top > 0)
	{
		return false;
	}
	else
	{
		return true;
	}*/
	return ps->top == 0;
}

STDataType StackTop(ST* ps)
{
	assert(ps);
	assert(ps->top > 0);

	return ps->a[ps->top - 1];
}


int StackSize(ST* ps)
{
	assert(ps);
	return ps->top;
}

2.7.归并排序

2.7.1.归并排序

基本思想:
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤:

Test.c文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include "Sort.h"

void TestMergeSort()
{
	int a[] = { 10,6,7,1,3,9,4,2 };

	MergeSort(a, sizeof(a) / sizeof(int));

	PrintArray(a, sizeof(a) / sizeof(int));
}

int main()
{
	TestMergeSort();

	return 0;
}

Sort.h文件:

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>

void PrintArray(int* a, int n);

void MergeSort(int* a, int n);

Sort.c文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include "Sort.h"

void PrintArray(int* a, int n)
{
	for (int i = 0; i < n; ++i)
	{
		printf("%d ", a[i]);
	}
	printf("\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 index = begin;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
			tmp[index++] = a[begin1++];
		else
			tmp[index++] = a[begin2++];
	}

	while (begin1 <= end1)
		tmp[index++] = a[begin1++];

	while (begin2 <= end2)
		tmp[index++] = a[begin2++];

	memcpy(a + begin, tmp + begin, (end - begin + 1) * sizeof(int));
}

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

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

	free(tmp);
}

注:

1.上面代码可以画递归展开图加深理解,并结合代码调试进行验证

2.分解时我们不能分解成[begin,mid-1] [mid,end]因为如果最后分解成了例如1 2序列,那么序列1 2再分解还是1 2,会死循环,因此应该分解成[begin,mid] [mid+1,end]

3.合并的时候在原数组直接排序会噪声数据覆盖而丢失数据,所以我们创建tmp数组,将原数组排序合并到tmp数组中,再将tmp数组中刚刚合并的序列memcpy拷贝回原数组中

4.归并排序时间复杂度:O(N\times log_{2}^{N}) (总共有log_{2}^{N}层,每一层合计N)

   归并排序空间复杂度:O(N) (tmp数组N个空间,每一个递归占用1,程序运行总共占用log_{2}^{N}个递归空间,因此占用log_{2}^{N}个空间,总共N+log_{2}^{N}

2.7.2.归并排序优化

将递归的归并排序优化成非递归的归并排序,我们直接将原序列当成分解后的一个个数,用gap变量控制一次合并中一组元素的数量。例如gap=1,那么就是第1个元素和第2个元素合并第2个元素和第3个元素合并...,gap=2,那么就是第1、2元素和3、4元素合并...,如下图所示,当gap大于等于原序列元素个数就不再递归了

Test.c文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include "Sort.h"

void TestMergeSortNonR()
{
	int a[] = { 10,6,7,1,3,9,4,2,5,2 };

	MergeSortNonR(a, sizeof(a) / sizeof(int));

	PrintArray(a, sizeof(a) / sizeof(int));
}

int main()
{
	TestMergeSortNonR();

	return 0;
}

Sort.h文件:

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>

void PrintArray(int* a, int n);

void MergeSortNonR(int* a, int n);

Sort.c文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include "Sort.h"

void PrintArray(int* a, int n)
{
	for (int i = 0; i < n; ++i)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}


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

	while (gap < n)
	{
		// 间距为gap是一组,两两归并
		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;
			// end1 越界,修正
			if (end1 >= n)
				end1 = n - 1;

			// begin2 越界,第二个区间不存在
			if (begin2 >= n)
			{
				begin2 = n;
				end2 = n - 1;
			}

			// begin2 ok, end2越界,修正end2即可
			if (begin2 < n && end2 >= n)
				end2 = n - 1;

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

			while (begin1 <= end1)
				tmp[index++] = a[begin1++];

			while (begin2 <= end2)
				tmp[index++] = a[begin2++];
		}
		memcpy(a, tmp, n * sizeof(int));

		gap *= 2;
	}

	free(tmp);
}

注:

1.下面第一个代码所示还有一些问题。该代码如果序列的数据个数不是2的次方倍,那么排序结果就会有问题,我们将每一次归并的begin1 end1 begin2 end2打印出来,见下面第二三四个代码所示,我们可以从结果中判断end1 begin2 end2的赋值是有问题的,begin1=i而i是小于n的,所以begin1是不会越界的。

我们进行分析,第一种情况当end1越界了,对end1进行修正(修正就是end1强制等于最后一个元素的下标)即可;第二种情况当begin2越界了,说明第二个区间是一个不存在的区间,此时不能对begin2和end2进行修正,如果修正了,begin2和end2都指向最后一个元素,而因为begin2越界,end1假设不越界会拿走最后一个元素,end1假设越界了进行修正后还会将最后一个元素拿走,第一个区间拿走了最后一个元素,此时begin2和end2都指向最后一个元素又将序列最后一个元素再拿一遍,序列会多加一个元素,都后面存到tmp数组时会造成越界,所以如果begin2越界,不再归并,要想不再归并,将begin2改的比end2大即可;第三种情况当begin没越界,end2越界了,对end2进行修正即可。

2.7.3.归并排序实现外排序思路

内部排序:待排序记录存放在计算机内存进行的排序过程。

外部排序:待排序记录的数量很大,以致于内存不能一次容纳全部记录,所以在排序过程中需要对磁盘进行访问的排序过程。

注:

1.前面学到的所有排序都是内排序,归并排序可以内排序也可以用来做外排序

2.内存中数据是存在数组里的,所以可以进行下标随机访问;磁盘中数据是存在文件中,只能串行访问

   内存中数据访问较快,磁盘中数据访问较慢

原本归并思路不可行:

如果直接使用原本归并的思路进行外排序,一个大文件拆分成两个小文件,两个小文件拆分成两个更小的文件......拆分完之后再两个小文件合并成一个大文件......,这种方法是不可取的,因为如果数据量很大,比如十亿个整数排序,那么需要递归拆分成30层,后面的每一层都有非常多的小文件,因此需要改进。

改进思路:

核心思想:数据量大,加载不到内存。想办法控制两个有序文件,两个有序文件归并成一个更大的有序文件

例如:十亿个整数文件,只给你1G的运行内存,请对文件中的十亿个数进行排序

思路:十亿个数大约占用4G内存,将十亿个数的文件分成四份,每一份2.5亿个数,占1G内存,分别将每一份文件数据拿入内存然后使用前面学过的内排序进行排序,最后将四份文件数据都排成有序的之后,将四份文件归并成一个文件即可

2.8.非比较排序

非比较排序分为:

1.计数排序

2.基数排序(桶排序)

注:基数排序(桶排序)只适用于对整数进行排序,该排序复杂不实用,所以不讲

计数排序:

思路1(绝对映射):

第一步:重新开一个数组a,新数组a下标从0开始到原序列最大数值n,新数组全部数据初始化为0,遍历原序列,每遇到一个数val就对a[val]处的值加一

例如:对序列10 6 7 1 6 1排序

00000000000
012345678910

02000021001
012345678910
 

第二步:从前往后遍历数组a,数组a每一个下标i对应一个数值,下标i对应处的值a[i]是i在原数组中出现的次数

02000021001
012345678910

1166710

注:该思路如果对10000 9999 5000 9999 5000 8888这样的序列排序,那么新数组a中下标5000之前都是多余开销,因此需要改进

思路2(相对映射):

第一步:重新开一个数组a,新数组a下标从0开始到原序列最大数值x - 最小数值y=n,新数组全部数据初始化为0,遍历原序列,每遇到一个数val就对a[val-y]处的值加一

例如:对序列10000 9999 5000 9999 5000 8888排序

2...1...2...1
0(5000)...3888(8888)...4999(9999)...5000(10000)

2...1...2...1
0(5000)...3888(8888)...4999(9999)...5000(10000)

第二步:从前往后遍历数组a,数组a每一个下标i对应数值i+5000,下标i对应处的值a[i]是数值i+5000在原数组中出现的次数

2...1...2...1
0(5000)...3888(8888)...4999(9999)...5000(10000)

5000500088889999999910000

Test.c文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include "Sort.h"

void TestCountSort1()
{
	int a[] = { 10, 6, 7, 1, 6, 1};

	PrintArray(a, sizeof(a) / sizeof(int));

	CountSort(a, sizeof(a) / sizeof(int));

	PrintArray(a, sizeof(a) / sizeof(int));
}

void TestCountSort2()
{
	int a[] = { 100, -60, 70, 100, 65, -60 };

	PrintArray(a, sizeof(a) / sizeof(int));

	CountSort(a, sizeof(a) / sizeof(int));

	PrintArray(a, sizeof(a) / sizeof(int));
}
int main()
{
	TestCountSort1();
	TestCountSort2();

	return 0;
}

Sort.h文件:

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>

void PrintArray(int* a, int n);

void CountSort(int* a, int n);

Sort.c文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include "Sort.h"

void PrintArray(int* a, int n)
{
	for (int i = 0; i < n; ++i)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}


void CountSort(int* a, int n)
{
	int min = a[0], max = 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);
	assert(countA);
	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;
		}
	}
}

注:

1.计数排序时间复杂度:O(range+N)

   计数排序空间复杂度:O(range)

这就说明计数排序效率较高,适用于范围确定且集中的序列

时间复杂度解释:下面代码中外面的for循环走range次,里面while循环走N次,虽然这里嵌套,但两循环相互独立,而全部代码中其他循环都是走N次,因此时间复杂度O(range+N)

2.计数排序使用相对映射的思路写出来的代码对有负数的序列依然可以进行排序,使用绝对映射思路的代码无法实现有负数的序列排序。但是计数排序(无论相对映射思路还是绝对映射思路)对其他类型的数据是无法排序的


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

随风张幔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值