【数据结构--排序算法】超详细

目录

  • 1.排序的概念
    • 1.1 排序的概念
    • 1.2 常见的排序算法
  • 2.常见排序算法的实现
    • 2.1插入排序(InsertSort)
      • 2.1.1直接插入排序
      • 2.1.2希尔排序(ShellSort)
    • 2.2 选择排序(SelectSort)
      • 2.2.1 直接选择排序(SelectSort)
      • 2.2.2 堆排序(HeapSort)
    • 2.3 交换排序
      • 2.3.1冒泡排序
      • 2.3.2 快速排序
    • 2.4 归并排序
    • 2.5 计数排序
  • 3.排序算法复杂度及稳定性分析
  • 4.非递归实现
    • 4.1 非递归实现快速排序
    • 4.2 非递归实现归并排序
  • 5.所有代码
  • 6.总结

1.排序的概念

1.1 排序的概念

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

1.2 常见的排序算法

【数据结构--排序算法】超详细_第1张图片
✨✨✨这一次我们会研究插入排序、希尔排序、选择排序、堆排序、冒泡排序、快速排序、归并排序、计数排序并对他们进行排序性能的测试

在开始之前我先介绍一下一个测定算法效率的代码。

void TestOP()
{
	srand(time(0));
	const int N = 100000;
	int* a1 = (int*)malloc(sizeof(int) * N);

	for (int i = 0; i < N; ++i)
	{
		a1[i] = i;
	}
	int begin1 = clock();
	InsertSort(a1, N);//调用你要测试的算法
	int end1 = clock();
	free(a1);
}

2.常见排序算法的实现

2.1插入排序(InsertSort)

基本思想
把待排序的按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列。

实际中我们玩扑克牌时,就用了插入排序的思想

【数据结构--排序算法】超详细_第2张图片

2.1.1直接插入排序

动图演示
【数据结构--排序算法】超详细_第3张图片
【数据结构--排序算法】超详细_第4张图片

代码实现

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;//不管是end = 0或者是tmp>a[end]都需要将tmp的值放到a[end+1]中
			}
		}
		a[end + 1] = tmp;
	}
}
void PrintArray(int* a,int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}
void TestInsertSort()
{
	int a[] = { 3,5,2,7,8,6,1,9,4,0 };
	InsertSort(a, sizeof(a) / sizeof(int));
	PrintArray(a, sizeof(a) / sizeof(int));
}
int main()
{
	TestInsertSort();
	return 0;
}

特性总结
1.元素越接近有序,直接插入排序算法的时间效率越高
2.时间复杂度:O(N^2)
3.空间复杂度:O(1),它是一种稳定的排序算法
4.稳定性:稳定
具体分析
1.每一次插入都要与前面的数字比较大小,在最坏的情况下要与前面的所有数字交换位置,即是排n个数最坏需要交换(1+2+3+4+……+n-1)也就是(n+)*n/2次,时间复杂度也就是O(N^2)
2.没有额外的开辟空间,所以为O(1)

2.1.2希尔排序(ShellSort)

先选定一个整数(gap),把待排序文件中的所有记录按照距离gap标记,所有距离为gap的记录分在同一组内,并对每一组内的记录进行排序,然后缩小gap的值,重复上面的工作。当gap=1时,就排好序了

动图演示


实现思路
【数据结构--排序算法】超详细_第5张图片
注意事项
1、由于希尔排序当gap>1的时候,我们将这个阶段称为预排序,这个过程可以快速的将大数移到后面的位置,效率比较高,当gap==1的时候数组已经接近有序了,这个时候效率也比较高。所以gap的选取就很重要,由于数组大小不确定,所以gap最好可以根据数组大小而变化。我们一般取数组长度一半作为gap的初始值(或者是1/3)
代码实现

void ShellSort(int* a, int n)
{
	int gap = n;
	while (gap > 0)
	{
		gap /= 2;
		//gap = gap/3+1;//保证了gap最后的值一定为1
		//gap>1时都是预排序
		//gap ==1时就是直接插入排序 

		//把间隔为gap的多组数据同时排
		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;
			}
		}
	}
}
void PrintArray(int* a,int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}
void TestShellSort()
{
	int a[] = { 3,5,2,7,8,6,1,9,4,0 };
	ShellSort(a, sizeof(a) / sizeof(int));
	PrintArray(a, sizeof(a) / sizeof(int));
}
int main
{
	TestShellSort();
	return 0;
}

细心的宝子可能已经发现了,这不就是gap=1的时候不就是插入排序吗?对的,希尔排序是简单排序的优化,思路都是一样的,都是插入排序下面的子类。
特性总结

1、希尔排序是对直接插入排序的优化。
2、当gap>1都是预排序,目的是让数组更接近有序,。当gap=1时数组已经接近有序的了,这样就恢复很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
3、希尔排序的时间复杂度不好计算,需要进行推导,推导出来平均时间复杂度:O(N^1.3 - N*N)
4、稳定性:不稳定

具体分析
1.由于希尔排序存在预排序分组的过程,如果相同的值,分到了不同的组里面,那么他们的顺序就会发生变化。
【数据结构--排序算法】超详细_第6张图片

2.2 选择排序(SelectSort)

基本思想
每一次从待排序的数据元素中选出最小(或最大的元素),存放在序列的起始位置,直到全部待排序的数据元素排完。

2.2.1 直接选择排序(SelectSort)

动图演示
【数据结构--排序算法】超详细_第7张图片
实现思路
通过遍历记录下最值的索引值,然后与正确位置上的数交换实现排序
代码实现
就像动图所示的那样,一次遍历只排一个数据效率太低,我们这里对代码进行优化。同时选择最大和最小分别放到数组的两端。

//直接选择排序
void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
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]);
		//如果begin跟maxi重叠,需要修正一下maxi的位置
		if (begin == maxi)
		{
			maxi = mini;
		}
		Swap(&a[maxi], &a[end]);
		++begin;
		--end;
	}
}
void PrintArray(int* a,int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}
void TestSelectSort()
{
	int a[] = { 3,5,2,7,8,6,1,9,4,0 };
	SelectSort(a, sizeof(a) / sizeof(int));
	PrintArray(a, sizeof(a) / sizeof(int));
}
int main()
{
	TestSelectSort();
	return 0;
}

特性总结
1.时间复杂度:O(N^2)
2.空间复杂度:O(1)
3.稳定性:不稳定

【数据结构--排序算法】超详细_第8张图片

2.2.2 堆排序(HeapSort)

堆的逻辑结构是一颗完全二叉树 堆的存储结构(物理结构)是一个数组 同时堆的父子节点下标满足以下关系:
1、leftchild =parent * 2+1
2、rightchild = parent * 2+2
3、parent = (child-1)/2

【数据结构--排序算法】超详细_第9张图片

堆排序(HeapSort)是指利用堆这种数据姐结构所涉及的一种排序算法,它是选择排序的一种,它是通过堆来进行选择数据,需要注意的是排升序要建大堆,排降序要建小堆

✨现在我们来研究一下为什么排升序要建大堆,排降序要建小堆

我们知道
大堆:每个结点的值都大于或等于其左右孩子结点的值
小顶堆:每个结点的值都小于或等于其左右孩子结点的值
所以大堆最后一个叶子节点的值一定比根节点小,在交换后重新建大堆只需要将堆顶的值向下调整就行,此时堆顶左右子树都是小堆,时间复杂度为O(logN ),即调整层数(logN)次就可以了;如果建立小堆,堆顶的元素就是最小的元素,然后寻找次小值的过程就是判断堆定左右孩子谁较小,再把他们俩的较小值作为堆顶,此时交换可能左右子树就不满足小堆这个关系,也就是堆的结构全乱了,相当于我们要从剩下的(N-1)个元素里面重新建立堆,比较麻烦!!!

正如建房子一样,我们都是先打好地基然后再往上垒高,建堆的过程也可以理解为这样的过程,既然排升序需要建大堆,大堆的左右子树都是大堆,我们不妨从树的尾部开始建自下而上的调整,这样就能保证每次调整的那颗树的左右子树都是大堆了。由于叶子节点的左右子树都是NULL,相当于已经是大堆了,所以我们从第一个最后一个度为2的节点开始。如下图:值为20的那个节点开始

【数据结构--排序算法】超详细_第10张图片

动图演示

过程图演示
【数据结构--排序算法】超详细_第11张图片

从这张图我们不难发现,排序前后相同数值4的位置发生了变化,红4在蓝4前面但排序后却反过来了,这也反映了堆排序的不稳定性。

✨现在我们来研究一下堆排序的时间复杂度
【数据结构--排序算法】超详细_第12张图片

代码实现

void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
//建大堆
void AdjustDown(int* a, int n, int root)
{
	int parent = root;
	int child = parent * 2 + 1;//默认是左孩子
	while (child < n)
	{
		//1、选出左右孩子中大的那一个
		if (child + 1 < n && a[child + 1] > a[child])
		{
			child += 1;
		}
		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);
	}
	//排升序,建大堆
	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;
	}
}
void PrintArray(int* a,int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}
void TestHeapSort()
{
	int a[] = { 3,5,2,7,8,6,1,9,4,0 };
	HeapSort(a, sizeof(a) / sizeof(int));
	PrintArray(a, sizeof(a) / sizeof(int));
}
int main()
{
	TestHeapSort();
	return 0;
}

特性总结

1、使用堆来选数排序,效率高了很多
2、时间复杂度:O(N*logN)
3、空间复杂度:O(1)
4、稳定性不稳定

2.3 交换排序

基本思想

所谓交换,就是根据徐柳中两个记录键值的比较结果来兑换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动

2.3.1冒泡排序

动图演示
【数据结构--排序算法】超详细_第13张图片
特性总结

  1. 冒泡排序是一种非常容易理解的排序
  2. 时间复杂度:O(N^2)
  3. 空间复杂度:O(1)
  4. 稳定性:稳定

由于这个算法比较简单,我就不过多赘述了直接上代码
代码实现

void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
void BubbleSort(int* a, int n)
{
	for (int i = 0; i < n; i++)
	{
		int flag = 0;
		for (int j = 0; j < n - 1 - i; j++)
		{
			if (a[j] > a[j + 1])
			{
				Swap(&a[j], &a[j + 1]);
				flag = 1;
			}
		}
		if (flag == 0)
		{
			break;
		}
	}
}
void PrintArray(int* a,int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}
void TestBubbleSort()
{
	int a[] = { 3,5,2,7,8,6,1,9,4,0 };
	BubbleSort(a, sizeof(a) / sizeof(int));
	PrintArray(a, sizeof(a) / sizeof(int));
}
int main()
{
	TestBubbleSort();
	return 0;
}

2.3.2 快速排序

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

动图演示
【数据结构--排序算法】超详细_第14张图片

将区间按照基准值划分为左右两半部分的常见方式有:

  1. 挖坑法
  2. 左右指针法
  3. 前后指针版本

上面三种方法我们都会讲解实现一下:
✨1、挖坑法

20221003_200438


通过动图,我们可以感受到,每一次排序都只能将key的值放到它对应的位置上,所以key的选取很关键,如果key太大或者太小都不太好(在递归的时候会出现左右边不均衡的问题),所以我先介绍一种三数字取中的方法,可以避免这种情况的发生。

int GetMidIndex(int* a, int begin, int end)
{
	//int mid = (begin + end) / 2;
	int mid = begin + rand() % (end - begin);//随机选key
	if (a[begin] < a[mid])
	{
		if (a[mid] < a[end])
		{
			return mid;
		}
		else if (a[begin] > a[end])
		{
			return begin;
		}
		else
		{
			return end;
		}
	}
	else//a[begin] > a[mid]
	{

		if (a[mid] > a[end])
		{
			return mid;
		}
		else if (a[begin] < a[end])
		{
			return begin;
		}
		else
		{
			return end;
		}
	}
}

单趟排序


//挖坑法
int PartSort1(int* a, int left, int right)
{
	int mid = GetMidIndex(a, left, right);
	Swap(&a[left], &a[mid]);
	int begin = left, end = right;
	int hole = left;
	int key = a[left];
	while (begin < end)
	{
		//右边找小,填到左边坑里面
		while (begin < end && a[end] >= key)
		{
			--end;
		}
		a[hole] = a[end];
		hole = end;
		//左边找大,填到右边的坑里
		while (begin < end && a[begin] >= key)
		{
			++begin;
		}
		a[hole] = a[begin];
		hole = begin;
	}
	a[hole] = key;
	return hole;
}

✨2、左右指针

左右指针法和挖坑法本质是一样的,

int PartSort2(int* a, int left, int right)
{
	int mid = GetMidIndex(a, left, right);
	Swap(&a[left], &a[mid]);
	int begin = left, end = right;
	int keyi = begin;
	while (begin < end)
	{
		//右边找小
		while (begin < end && a[right ]>= a[keyi])
		{
			end--;
		}
		//左边找大
		while (begin < end && a[left] <= a[keyi])
		{
			begin++;
		}
		Swap(&a[begin], &a[end]);
	}
	Swap(&a[begin], &a[keyi]);
	return begin;
}

✨3、前后指针法

前后指针法


//前后指针法
//cur找小,每次遇到比keyi小的值就停下来,++prev交换prev和cur的值
//前后指针法
int PartSort3(int* a, int left, int right)
{
	int mid = GetMidIndex(a, left, right);
	Swap(&a[left], &a[mid]);
	int prev = left;
	int 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;
	return prev;
}

现在我们已经实现了排单趟的功能,我们来试着编写QuickSort函数。那么怎样把剩下的数字也变得有序呢?
这个时候不得不提出一种思想:分治递归了。就将一个完成不了的大问题一步一步细分化小然后实现。

但是说到递归我们就不难想到递归对对内存的消耗,当递归深度很深的时候,难免会造成堆栈的溢出,这不是我们想看到的,所以有没有什么办法进行优化呢。经过分析我们发现每完成一次递归之后,内存的栈区上存的函数栈帧会呈现指数形的增长,所以为了减少递归调用的内存消耗,我们只用在最后几次递归的时候不采用递归的方式就可以完成优化。那么新的问题又来了,选用哪一种排序方式来进行最后的排序呢?答案是直接插入排序!!!,看下面的时间复杂度好像是希尔和堆排序更好,但是在数据比较少的时候,希尔排序预排序都分不到几组,时间复杂度就接近于直接插入排序了。
冒泡排序O(n^2)
简单选择排序O(n^2)
直接插入排序O(n^2)
希尔排序O(N*logN)
堆排序需要建堆

进行优化后的代码

void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	if (end - begin + 1 < 10)
	{
		InsertSort(a + begin, end - begin + 1);
	}
	else
	{
		int keyi = PartSort3(a, begin, end);
		QuickSort(a, begin, keyi - 1);
		QuickSort(a, keyi + 1, end);
	}
}

代码实现

void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
int GetMidIndex(int* a, int begin, int end)
{
	//int mid = (begin + end) / 2;
	int mid = begin + rand() % (end - begin);//随机选key
	if (a[begin] < a[mid])
	{
		if (a[mid] < a[end])
		{
			return mid;
		}
		else if (a[begin] > a[end])
		{
			return begin;
		}
		else
		{
			return end;
		}
	}
	else//a[begin] > a[mid]
	{

		if (a[mid] > a[end])
		{
			return mid;
		}
		else if (a[begin] < a[end])
		{
			return begin;
		}
		else
		{
			return end;
		}
	}
}
//挖坑法
int PartSort1(int* a, int left, int right)
{
	int mid = GetMidIndex(a, left, right);
	Swap(&a[left], &a[mid]);
	int begin = left, end = right;
	int hole = left;
	int key = a[left];
	while (begin < end)
	{
		//右边找小,填到左边坑里面
		while (begin < end && a[end] >= key)
		{
			--end;
		}
		a[hole] = a[end];
		hole = end;
		//左边找大,填到右边的坑里
		while (begin < end && a[begin] >= key)
		{
			++begin;
		}
		a[hole] = a[begin];
		hole = begin;
	}
	a[hole] = key;
	return hole;
}
//左右指针法
int PartSort2(int* a, int left, int right)
{
	int mid = GetMidIndex(a, left, right);
	Swap(&a[left], &a[mid]);
	int begin = left, end = right;
	int keyi = begin;
	while (begin < end)
	{
		//右边找小
		while (begin < end && a[right ]>= a[keyi])
		{
			end--;
		}
		//左边找大
		while (begin < end && a[left] <= a[keyi])
		{
			begin++;
		}
		Swap(&a[begin], &a[end]);
	}
	Swap(&a[begin], &a[keyi]);
	return begin;
}

//前后指针法
int PartSort3(int* a, int left, int right)
{
	int mid = GetMidIndex(a, left, right);
	Swap(&a[left], &a[mid]);
	int prev = left;
	int 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;
	return prev;
}

void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	if (end - begin + 1 < 10)
	{
		InsertSort(a + begin, end - begin + 1);
	}
	else
	{
		int keyi = PartSort3(a, begin, end);
		QuickSort(a, begin, keyi - 1);
		QuickSort(a, keyi + 1, end);
	}
}
void PrintArray(int* a,int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}
void TestQuickSort()
{
	int a[] = { 3,5,2,7,8,6,1,9,4,0 };
	QuickSort(a, 0,sizeof(a) / sizeof(int)-1);
	PrintArray(a, sizeof(a) / sizeof(int));
}
int main()
{
	TestQuickSort();
	return 0;
}

快速排序终极版
通过测试我们发现,上面的代码仍然无法应对当一个数组中全是一样的数字,或者说当数组中出现大量重复值的时候,表现不好。所以再次对上面的方法进行了优化。“三路”,把大的往后甩,小的往前面甩,中间的都是等于key的
代码实现

 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 Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
int GetMidIndex(int* a, int begin, int end)
{
	//int mid = (begin + end) / 2;
	int mid = begin + rand() % (end - begin);//随机选key
	if (a[begin] < a[mid])
	{
		if (a[mid] < a[end])
		{
			return mid;
		}
		else if (a[begin] > a[end])
		{
			return begin;
		}
		else
		{
			return end;
		}
	}
	else//a[begin] > a[mid]
	{

		if (a[mid] > a[end])
		{
			return mid;
		}
		else if (a[begin] < a[end])
		{
			return begin;
		}
		else
		{
			return end;
		}
	}
}
void QuickSortPLUS(int* a, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	if (end - begin + 1 < 10)
	{
		InsertSort(a + begin, end - begin + 1);
	}
	else
	{
		int mid = GetMidIndex(a, begin, end);
		Swap(&a[begin], &a[mid]);
		int left = begin, right = end;
		int cur = begin + 1;
		int key = a[begin];
		while (cur <= right)
		{
			if (a[cur] < key)
			{
				Swap(&a[left], &a[cur]);
				cur++;
				left++;
			}
			else if (a[cur] > key)
			{
				Swap(&a[cur], &a[right]);
				--right;
			}
			else
			{
				++cur;
			}
		}
		QuickSortPLUS(a, begin, left - 1);
		QuickSortPLUS(a, right + 1, end);
	}
}

特性总结

  1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
  2. 时间复杂度:O(N*logN)
  3. 空间复杂度:O(logN)
  4. 稳定性:不稳定

2.4 归并排序

基本思想

归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and
Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有
序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤:
【数据结构--排序算法】超详细_第15张图片

简而言之:就是依次对比取小的放到临时数组

动图演示
【数据结构--排序算法】超详细_第16张图片
代码实现

void _MergeSort(int* a, int begin, int end, int* tmp)
{
	if (end <= begin)
	{
		return;
	}
	int mid = ( begin+end)>>1;
	//左边有序,右边有序
	_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,end-begin+1);
	for (int i = begin; i <= end; i++)
	{
		a[i] = tmp[i];
	}
}
void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc failed");
		exit(-1);
	}
	_MergeSort(a,0, n-1,tmp);
	free(tmp);
	tmp = NULL;
}
void PrintArray(int* a,int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}
void TestMergeSort()
{
	int a[] = { 3,5,2,7,8,6,1,9,4,0 };
	MergeSort(a, sizeof(a) / sizeof(int));
	PrintArray(a, sizeof(a) / sizeof(int));
}
int main()
{
	TestMergeSort();
	return 0;
}

特性总结:

  1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
  2. 时间复杂度:O(N*logN)
  3. 空间复杂度:O(N)
  4. 稳定性:稳定
    5.归并排序,也叫外排序,还可以对文件中的数据排序。假设内存只有1G,现在有10G的文件,每次读1G到内存中放到一个数组,用快排对其排序在写到一个文件,再继续读下一个1G的数据。

2.5 计数排序

计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

动图演示
【数据结构--排序算法】超详细_第17张图片


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*)malloc(sizeof(int) * range);
		memset(count, 0, sizeof(int) * range);
		//统计次数
		for (int i = 0; i < n; i++)
		{
			count[a[i] - min]++;
		}
		int j = 0;
		for (int i = 0; i < range; i++)
		{
			while (count[i]--)
			{
				a[j++] = i+min;
			}
		}
		free(count);
	}
}
void PrintArray(int* a,int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}
void TestCountSort()
{
	int a[] = { 3,5,2,7,8,6,1,9,4,0 };
	CountSort(a, sizeof(a) / sizeof(int));
	PrintArray(a, sizeof(a) / sizeof(int));
}
int main()
{
	TestCountSort();
	return 0;
}

特性总结:

  1. 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
  2. 时间复杂度:O(MAX(N,范围))
  3. 空间复杂度:O(范围)
  4. 稳定性:稳定

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

【数据结构--排序算法】超详细_第18张图片
【数据结构--排序算法】超详细_第19张图片

4.非递归实现

递归改非递归无非下面两种方式
1、直接改循环;
2、借助数据结构的栈模拟递归过程

4.1 非递归实现快速排序

我们借助数据结构的栈来模拟,由于c语言缺少相关的库所以,我们只有自己编写栈的相关函数,很是麻烦

#pragma once
#include
#include
#include
#include

typedef int STDataType;

typedef struct Stack
{
	STDataType* a;
	int top;
	int capacity;
}ST;

void StackInit(ST* ps)
{
	assert(ps);
	ps->a = (STDataType*)malloc(sizeof(STDataType)*4);
	if (ps->a == NULL)

	{
		printf("malloc fail\n");
		exit(-1);
	}
	ps->capacity = 4;
	ps->top = 0;

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

}
//栈顶插入删除数据
//入栈
void StackPush(ST* ps, STDataType x)
{
	assert(ps);

	if (ps->top == ps->capacity)
	{
		STDataType* tmp = realloc(ps->a, ps->capacity * 2 * sizeof(STDataType));
		{
			if (tmp == NULL)

			{
				printf("realloc fail\n");
				exit(-1);
			}
			else
			{
				ps->a = tmp;
				ps->capacity *= 2;
			}
		}
	}
	ps->a[ps->top] = x;
	ps->top++;
}
//出栈
void StackPop(ST* ps)
{
	assert(ps);
	//栈空了,调用Pop直接终止程序报错
	assert(ps->top>0);


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


	//如果栈空了,调用Pop直接终止程序报错
	assert(ps->top>0);

	return ps->a[ps->top - 1];
}
int StackSize(ST* ps)
{
	assert(ps);
	return ps->top;
}
int StackEmpty(ST* ps)
{
	assert(ps);
	return ps->top == 0;
}
void QuickSortNonR(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 = PartSort3(a, left, right);
		//[left,keyi-1] keyi [keyi+1,right]
		//区间内至少有两个数
		if (keyi+1<right)
		{
			StackPush(&st, keyi + 1);
			StackPush(&st, right);
		}
		if (left < keyi - 1)
		{
			StackPush(&st, left);
			StackPush(&st, keyi - 1);
		}
	}
	StackDestroy(&st);
}
void PrintArray(int* a,int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}
void TestQuickSort()
{
	int a[] = { 3,5,2,7,8,6,1,9,4,0 };
	QuickSortNonR(a,sizeof(a) / sizeof(int) );
	PrintArray(a, sizeof(a) / sizeof(int));
}
int main()
{
	TestQuickSort();
	return 0;
}

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

4.2 非递归实现归并排序

void MergeSortNonR(int* a, int n)
{
	int gap = 1;
	int* tmp = (int*)malloc(sizeof(int) * n);
	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 index = i;
			if (end2 >= n)
			{
				end2 = n - 1;
			}
			if (begin2 >= n)
			{
				break;
			}
			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 + i, tmp + i, sizeof(int) * (end2 - i + 1));
		}
		gap *= 2;
	}
	free(tmp);
	tmp = NULL;
}

5.所有代码

#define _CRT_SECURE_NO_WARNINGS
#include
#include
#include
//插入排序
//时间复杂度O(n^2)
void Swap(int* p1, int* p2);
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;//不管是end = 0或者是tmp>a[end]都需要将tmp的值放到a[end+1]中
			}
		}
		a[end + 1] = tmp;
	}
}

void PrintArray(int* a,int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}
//希尔排序——直接插入排序的基础上的优化
//1、先进行预排序,让数组接近有序
//2、直接插入排序
//时间复杂度:O(LogN*N)或者O(Log3N*N)
void ShellSort(int* a, int n)
{
	int gap = n;
	while (gap > 0)
	{
		gap /= 2;
		//gap>1时都是预排序
		//gap ==1时就是直接插入排序 


		//gap很大时,下面预排序时间复杂度O(N)
		//gap很小时,数组已经接近有序了,这时差不多也是O(N);


		//把间隔为gap的多组数据同时排
		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;
			}
		}
	}
}
//堆排序
//整体时间复杂度O(N*logN)
void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
void AdjustDown(int* a, int n, int root)
{
	int parent = root;
	int child = parent * 2 + 1;//默认是左孩子
	while (child < n)
	{
		//1、选出左右孩子中大的那一个
		if (child + 1 < n && a[child + 1] > a[child])
		{
			child += 1;
		}
		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);
	}
	//排升序,建大堆
	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;
	}
}
//直接选择排序
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]);
		//如果begin跟maxi重叠,需要修正一下maxi的位置
		if (begin == maxi)
		{
			maxi = mini;
		}
		Swap(&a[maxi], &a[end]);
		++begin;
		--end;
	}
}
void BubbleSort(int* a, int n)
{
	for (int i = 0; i < n; i++)
	{
		int flag = 0;
		for (int j = 0; j < n - 1 - i; j++)
		{
			if (a[j] > a[j + 1])
			{
				Swap(&a[j], &a[j + 1]);
				flag = 1;
			}
		}
		if (flag == 0)
		{
			break;
		}
	}
}
//快速排序
//时间复杂度O(N*logN)
//最坏的情况为有序,时间复杂度O(N*N)
//挖坑法
//三数取中
int GetMidIndex(int* a, int left, int right)
{
	int mid = (left + right) >> 1;
	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 PartSort1(int* a, int left, int right)
{
	int index = GetMidIndex(a, left, right);
	Swap(&a[left], &a[index]);//始终用左边位置的值做key,同时保证了key不为最值

	int begin = left, end = right;
	int pivot = begin;
	int key = a[begin];

	while (begin < end)
	{
		//右边找小,放在左边
		while (begin < end && a[end] >= key)
		{
			end--;
		}
		a[pivot] = a[end];
		pivot = end;


		//左边找大,放在右边
		while (begin < end && a[begin] <= key)
		{
			begin++;
		}
		a[pivot] = a[begin];
		pivot = begin;
	}
	pivot = begin;
	a[pivot] = key;

	return pivot;
}
//左右指针
int PartSort2(int* a, int left, int right)
{
	int index = GetMidIndex(a, left, right);
	Swap(&a[left], &a[index]);
	int begin = left, end = right;
	int keyi = begin;

	while (begin < end)
	{
		//找小
		while (begin < end && a[end] >= a[keyi])
		{
			--end;
		}
		//找大
		while (begin < end && a[begin] <= a[keyi])
		{
			++begin;
		}
		Swap(&a[begin], &a[end]);
	}
	Swap(&a[begin], &a[keyi]);
	return begin;
}
//前后指针法
//cur找小,每次遇到比keyi小的值就停下来,++prev交换prev和cur的值
int PartSort3(int* a, int left, int right)
{
	int index = GetMidIndex(a, left, right);
	Swap(&a[left], &a[index]);

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

void QuickSort(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int keyIndex = PartSort3(a, left, right);
	//QuickSort(a,left,keyIndex-1);
	//QuickSort(a, keyIndex + 1, right);
	//小区间优化(减少递归次数)
	if (keyIndex - 1 - left > 10)
	{
		QuickSort(a, left, keyIndex - 1);
	}
	else
	{
		InsertSort(a + left, keyIndex - 1-left + 1);
	}
	QuickSort(a, keyIndex + 1, right);
	if (right - (keyIndex + 1) > 10)
	{
		InsertSort(a+ keyIndex +1, right-(keyIndex -1 )+ 1);
	}
	else
	{
		InsertSort(a, keyIndex - 1 - left + 1);
	}
}
//归并排序
//依次对比取小的放到临时数组
//归并排序,也叫外排序,还可以对文件中的数据排序。
//假设内存只有1G,现在有10G的文件,每次读1G到内存中放到一个数组,用快排对其排序在写到一个文件,
//再继续读下一个1G的数据
void _MergeSort(int* a, int left, int right,int* tmp)
{
	if (left >= right)
		return ;

	int mid = (left + right) >> 1;
	//假设[left,mid][mid+1,right]有序,那么就可以归并了
	_MergeSort(a, left, mid, tmp);
	_MergeSort(a, mid+1, right, tmp);

	int begin1 = left, end1 = mid;
	int begin2 = mid + 1, end2 = right;
	int index = left;
	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++];
	}
	//拷贝回去
	for (int i = left; i <= right; ++i)
	{
		a[i] = tmp[i];
	}
}
void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	_MergeSort(a, 0, n - 1, tmp);
	free(tmp);
}

//递归的缺陷:栈空间不够用,可能会溢出


void TestInsertSort()
{
	int a[] = { 3,5,2,7,8,6,1,9,4,0 };
	InsertSort(a, sizeof(a) / sizeof(int));
	PrintArray(a, sizeof(a) / sizeof(int));
}
//基数排序(桶排序)
// 
// 
//计数排序
//统计每个数出现的次数,使用了哈希的一个映射思想,利用次数就可以排序了
//时间复杂度:O(N+range),说明它适用于范围集中一组整形数据排序
//空间复杂度:O(range)
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*)malloc(sizeof(int) * range);
		memset(count, 0, sizeof(int) * range);
		//统计次数
		for (int i = 0; i < n; i++)
		{
			count[a[i] - min]++;
		}
		int j = 0;
		for (int i = 0; i < range; i++)
		{
			while (count[i]--)
			{
				a[j++] = i+min;
			}
		}
		free(count);
	}
}
void TestShellSort()
{
	int a[] = { 3,5,2,7,8,6,1,9,4,0 };
	ShellSort(a, sizeof(a) / sizeof(int));
	PrintArray(a, sizeof(a) / sizeof(int));
}

void TestHeapSort()
{
	int a[] = { 3,5,2,7,8,6,1,9,4,0 };
	HeapSort(a, sizeof(a) / sizeof(int));
	PrintArray(a, sizeof(a) / sizeof(int));
}
void TestSelectSort()
{
	int a[] = { 3,5,2,7,8,6,1,9,4,0 };
	SelectSort(a, sizeof(a) / sizeof(int));
	PrintArray(a, sizeof(a) / sizeof(int));
}
void TestBubbleSort()
{
	int a[] = { 3,5,2,7,8,6,1,9,4,0 };
	BubbleSort(a, sizeof(a) / sizeof(int));
	PrintArray(a, sizeof(a) / sizeof(int));
}
void TestQuickSort()
{
	int a[] = { 3,5,2,7,8,6,1,9,4,0 };
	QuickSort(a, 0,sizeof(a) / sizeof(int)-1);
	PrintArray(a, sizeof(a) / sizeof(int));
}
void TestMergeSort()
{
	int a[] = { 3,5,2,7,8,6,1,9,4,0 };
	MergeSort(a, sizeof(a) / sizeof(int));
	PrintArray(a, sizeof(a) / sizeof(int));
}
void TestCountSort()
{
	int a[] = { 3,5,2,7,8,6,1,9,4,0 };
	CountSort(a, sizeof(a) / sizeof(int));
	PrintArray(a, sizeof(a) / sizeof(int));
}
void TestOP()
{
	srand(time(0));
	const int N = 100000;
	int* a1 = (int*)malloc(sizeof(int) * N);
	int* a2 = (int*)malloc(sizeof(int) * N);
	int* a3 = (int*)malloc(sizeof(int) * N);
	int* a4 = (int*)malloc(sizeof(int) * N);
	int* a5 = (int*)malloc(sizeof(int) * N);
	int* a6 = (int*)malloc(sizeof(int) * N);
	int* a7 = (int*)malloc(sizeof(int) * N);
	int* a8 = (int*)malloc(sizeof(int) * N);

	for (int i = 0; i < N; ++i)
	{
		a1[i] = i;
		a2[i] = i;
		a3[i] = i;
		a4[i] = i;
		a5[i] = i;
		a6[i] = i;
		a7[i] = i;
		a8[i] = i;
	}
	int begin1 = clock();
	InsertSort(a1, N);
	int end1 = clock();

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

	int begin3 = clock();
	SelectSort(a3, N);
	int end3 = clock();

	int begin4 = clock();
	HeapSort(a4, N);
	int end4 = clock();

	int begin5 = clock();
	//QuickSort(a5, 0, N - 1);
	QuickSortNonR(a5, N);
	int end5 = clock();


	int begin6 = clock();
	BubbleSort(a6, N);
	int end6 = clock();


	int begin7 = clock();
	MergeSort(a7, N);
	int end7 = clock();

	int begin8 = clock();
	CountSort(a8, N);
	int end8 = clock();




	printf("InsertSort:%d\n", end1 - begin1);
	printf("ShellSort:%d\n", end2 - begin2);
	printf("SelectSort:%d\n", end3 - begin3);
	printf("HeapSort:%d\n", end4 - begin4);
	printf("QuickSort:%d\n", end5 - begin5);
	printf("BubbleSort:%d\n", end6 - begin6);
	printf("MergeSort:%d\n", end7 - begin7);
	printf("CountSort:%d\n", end8 - begin8);

	free(a1);
	free(a2);
	free(a3);
	free(a4);
	free(a5);
	free(a6);
	free(a7);
	free(a8);


	
}
int main()
{
	TestInsertSort();
	TestShellSort();
	TestHeapSort();
	TestSelectSort();
	TestBubbleSort();
	TestQuickSort();
	TestMergeSort();
	TestCountSort();
	TestOP();
	return 0;
}

运行结果
【数据结构--排序算法】超详细_第20张图片

6.总结

这一次了解了八种排序算法,和两种递归转非递归的方式。这一次的博客内容众多,有很多动图图片,写的很累,战线拉得很长。很开心能在今天把这篇博客发出来,瓦库瓦库我还是会继续更新的!!!这一次完了下一次应该会倒回来更新链表队列和栈的知识。大佬们一起努力!!!你们的的点赞就是我更新的动力!!!!
【数据结构--排序算法】超详细_第21张图片

你可能感兴趣的:(排序算法,数据结构,算法)