【进来序一序吧】呕心沥血万文详解七大排序,全程无废话

如果我们从小就开始编写程序,那么也许长大以后我们自然而然地就能够阅读它们了。

文章目录

  • 1. 直接插入排序
    • 1.1基本思想
    • 1.2 代码演示
    • 1.3 注意事项
  • 2.冒泡排序
    • 2.1基本思想
    • 2.2 代码演示
    • 2.4插入排序和冒泡排序比较
  • 3.希尔排序
    • 3.1基本思想
    • 3.2 注意事项
    • 3.3 代码演示
  • 4.直接选择排序
    • 4.1 基本思想
    • 4.2 代码演示
    • 4.3 注意事项
  • 5.堆排序
    • 5.1 基本思想
    • 5.2代码演示
  • 6.快速排序
    • 6.1基本思想
    • 6.2单趟排序的三种方法:
      • 6.2.1 hoare版本
      • 6.2.2 挖坑法
      • 6.2.3 前后指针法:
    • 6.3 代码演示
    • ⚡️6.4 优化
      • 6.4.1 三数取中
      • 6.4.2 小区间优化
    • 6.5 快速排序非递归
  • 7.归并排序
    • 7.1 基本思想
      • 7.1.2 分割
      • 7.1.3 合并
      • 7.1.4 递归代码演示
    • 7.3 归并排序非递归
      • 7.3.1 注意事项:
      • 7.3.2 代码演示

1. 直接插入排序

1.1基本思想

  1. 假设我们排升序。
  2. 如果有一个有序区间,插入一个数据,从最后一个数开始比较,比插入的数据大就把它往后挪动直到遇到第一个小于等于插入数据的数就停下,把插入的数据放在其后,保证它依旧有序。
  3. 如果是一组无序的数据,那我们就把第一个数当作一个有序区间(用end来标记),用tmp变量保存有序区间的下一个,当作插入的数据,依次按照2的思路循环迭代往后走。
    【进来序一序吧】呕心沥血万文详解七大排序,全程无废话_第1张图片

1.2 代码演示

void InsertSort(int* a, int n)
{
	//end最后一个位置是n-2,也就是倒数第二个数
	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.3 注意事项

关于最后一条语句:a[end + 1] = tmp;
插入数据可能是两种情况:

  1. tmp比a[end]大,break跳出循环插入

【进来序一序吧】呕心沥血万文详解七大排序,全程无废话_第2张图片
2. end走到-1进入不了循环时插入
【进来序一序吧】呕心沥血万文详解七大排序,全程无废话_第3张图片

2.冒泡排序

2.1基本思想

有n个数据就要比较n-1趟,第一趟比较的次数为n-1-1,以后每趟的比较次数依次减1(因为每次都把最大的数据沉到最后),且如果有一趟的比较时没有发生交换,就表明数据已经有序,此时就跳出循环。

2.2 代码演示

#include 
void BubbleSort(int* a, int size)
{
	for (int i = 0; i < size - 1; ++i)
	{
		int exchange = 0;
		for (int j = 1; j < size - i; ++j)
		{
			if (a[j - 1] > a[j])
			{
				int temp = a[j - 1];
				a[j - 1] = a[j];
				a[j] = temp;
				exchange = 1;
			}
		}
		if (exchange == 0)
			break;
	}
}

2.4插入排序和冒泡排序比较

【进来序一序吧】呕心沥血万文详解七大排序,全程无废话_第4张图片

3.希尔排序

希尔排序是插入排序的一种,又称缩小增量排序。是改进版的插入排序

3.1基本思想

  1. 选定一个增长量gap,按照增长量gap作为数据分组的依据,对数据进行分组;
  2. 对分好组的每一组数据完成插入排序;
  3. 减小增长量,最小减为1,重复第二步操作。
    【进来序一序吧】呕心沥血万文详解七大排序,全程无废话_第5张图片

3.2 注意事项

【进来序一序吧】呕心沥血万文详解七大排序,全程无废话_第6张图片

3.3 代码演示

传统写法:

void ShellSort(int* a, int n)
{
	//1. gap>1 预排序
	//2. gap==1 直接插入排序
	//3. 我们要保证最后一次gap==1
	int gap = n;
	while(gap > 1)
	{
		//gap/=2最后一定为1
		//gap /= 2;
		//当1
		gap = gap/3+1;
		//控制组数
		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 (tmp < a[end])
					{
						a[end + gap] = a[end];
						end -= gap;
					}
					else
					{
						break;
					}
				}
				//同样有两种情况
				//tmp比a[end]大,break跳出循环插入
				// end走到-1进入不了循环时插入
				a[end + gap] = tmp;
			}
		}
	}
}

简化写法:
我们不用外层的for循环控制组数,把第二层for循环i+=gap改为++i.表示多组同时进行直接插入排序。

void ShellSort(int* a, int n)
{
	// 1、gap > 1  预排序
	// 2、gap == 1 直接插入排序
	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;
		}
	}
}

4.直接选择排序

4.1 基本思想

直接选择排序也称简单选择排序,是一种相对简单的排序算法,它的基本思想是:从一列数中找出最小的,和第一个交换;剩下的重新找出最小的,和这列数的第二个交换,…一直进行n-1次比较之后,该数列已经为有序数列了。

例如:已知一组无序数列:6 3 5 1 4 2 9

第一次:[6 3 5 1 4 2 9] 最小数为:1

第二次:1 [3 5 6 4 2 9] 最小数为:2

第三次:1 2 [5 6 4 3 9] 最小数为:3

第四次:1 2 3 [6 4 5 9] 最小数为:4

第五次:1 2 3 4 [6 5 9] 最小数为:5

第六次:1 2 3 4 5 [6 9] 最小数为:6

第七次:1 2 3 4 5 6 [9] 已经为有序数列了。

4.2 代码演示

我们这里一次选两个数,选出最小值放在第一个,最大值放在最后一个。再选出次小的值放在第二个,次大值放在倒数第二个,依次迭代往后走。

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]);
		// 如果left和maxi重叠,修正一下maxi即可
		if (left == maxi)
			maxi = mini;

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

		left++;
		right--;
	}
}

4.3 注意事项

当maxi和left重叠时,执行语句Swap(&a[left],&a[mini])后,maxi就被换到了mini的位置。
【进来序一序吧】呕心沥血万文详解七大排序,全程无废话_第7张图片
解决方法:
此时只要修正一下maxi即可

if (left == maxi)
	maxi = mini;

5.堆排序

堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。
之前详细介绍过堆和堆排序
博客链接

5.1 基本思想

  1. 假设我们排升序
  2. 将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
  3. 重新调整结构(此时最后一个数据不算堆里的),使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。

5.2代码演示

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

void AdjustDown(HPDataType* 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;
		}
	}
}

void HeapSort(int* a, int 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;
	}
}

6.快速排序

6.1基本思想

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

6.2单趟排序的三种方法:

6.2.1 hoare版本

【进来序一序吧】呕心沥血万文详解七大排序,全程无废话_第8张图片
分享一下我学习时疑惑的几个问题:

  • L、R会不会相遇不上?
    不会,因为L、R并不是一次只走一步且R找小的时候L是不动的,L找大的时候R是不动的,最后肯定有一个人要去碰上另一个不动的人。
  • 为什么相遇位置的数一定比key小?是如何保证的?
    这是由R先走保证的。
    【进来序一序吧】呕心沥血万文详解七大排序,全程无废话_第9张图片
    【进来序一序吧】呕心沥血万文详解七大排序,全程无废话_第10张图片
    注意事项,一些极端情况的处理:
    死循环
    【进来序一序吧】呕心沥血万文详解七大排序,全程无废话_第11张图片
    R一路飙出去,越界
    【进来序一序吧】呕心沥血万文详解七大排序,全程无废话_第12张图片
    正确的代码演示:
void Swap(int* pa, int* pb)
{
	int tmp = *pa;
	*pa = *pb;
	*pb = tmp;
}

// hoare
int PartSort1(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;
}

6.2.2 挖坑法

相比hoare版本,挖坑法更好理解,但本质上没有区别。
不需要理解为什么最终相遇位置比key小!
不需要理解为什么左边做key,右边先走!

代码演示:

// 挖坑法
int PartSort2(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;
}

6.2.3 前后指针法:


prev和cur的关系:

  1. cur还没遇到比key大的值时,prev紧跟cur,一前一后
  2. cur遇到比key大的值后,prev和cur之间间隔着一段比key大的值的区间

代码实现:

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

关于a[++prev] != a[cur]有两个作用:

  1. cur遇到比key小的值以后++prev。
  2. 防止自己跟自己交换。

右边做key的情况:
【进来序一序吧】呕心沥血万文详解七大排序,全程无废话_第13张图片

6.3 代码演示

【进来序一序吧】呕心沥血万文详解七大排序,全程无废话_第14张图片

void QuickSort(int* a, int begin, int end)
{
	// 子区间相等只有一个值或者不存在那么就是递归结束的子问题
	if (begin >= end)
		return;
	int keyi = PartSort(a, begin, end);//上述三种方法选1即可
	// [begin, keyi-1]keyi[keyi+1, end]
	QuickSort1(a, begin, keyi - 1);
	QuickSort1(a, keyi + 1, end);
}

左半区间递归图右半区间略:
【进来序一序吧】呕心沥血万文详解七大排序,全程无废话_第15张图片

⚡️6.4 优化

6.4.1 三数取中

为什么要优化,快排存在着一种最坏情况:
【进来序一序吧】呕心沥血万文详解七大排序,全程无废话_第16张图片
优化方法:
【进来序一序吧】呕心沥血万文详解七大排序,全程无废话_第17张图片
代码演示:

int GetMidIndex(int* a, int left, int right)
{
	int mid = left + (right - left) / 2;
	// left mid right
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
		{
			return mid;
		}
		//此时mid最大,剩下两个谁大谁就是中间的值
		else if (a[left] > a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
	else // a[left] > a[mid]
	{
		if (a[mid] > a[right])
		{
			return mid;
		}
		//此时mid是最小的,剩下两个谁小谁就是中间的值
		else if (a[left] < a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
}

// 前后指针法
int PartSort3(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);//上述三种方法选1即可
	// [begin, keyi-1]keyi[keyi+1, end]
	QuickSort1(a, begin, keyi - 1);
	QuickSort1(a, keyi + 1, end);
}

6.4.2 小区间优化

【进来序一序吧】呕心沥血万文详解七大排序,全程无废话_第18张图片
代码演示:

void QuickSort2(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);
		// [begin, keyi-1]keyi[keyi+1, end]
		QuickSort2(a, begin, keyi - 1);
		QuickSort2(a, keyi + 1, end);
	}
}

6.5 快速排序非递归

我们发现快速排序递归版本,递归调用时建立栈帧实际上是在存储要处理的区间。那我们也可以用一个栈来存储区间来模拟递归调用。C语言没有栈,我们可以把之前写的Stack.c,Stack.h文件复制到现有项。
【进来序一序吧】呕心沥血万文详解七大排序,全程无废话_第19张图片

代码实现:

#include "Stack.h"
// 非递归
void QuickSort3(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+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);
}

7.归并排序

7.1 基本思想

  1. 假设左右区间有序,直接合并,两个有序数组,归并成一个数组,取小的尾插到新数组。
  2. 如果左右区间无序,那么我们把8个分成4个,4个还是无序,再把4个分为2个,2个分为1个,直到分到一个数会区间不存在为止。此时可认为左右区间有序,这一步骤用递归去实现。
  3. 按照1的思路合并回去
    【进来序一序吧】呕心沥血万文详解七大排序,全程无废话_第20张图片

7.1.2 分割

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]
	printf("归并[%d,%d][%d,%d]\n", begin, mid, mid + 1, end);
}

运行结果:
【进来序一序吧】呕心沥血万文详解七大排序,全程无废话_第21张图片

【进来序一序吧】呕心沥血万文详解七大排序,全程无废话_第22张图片
注意事项:
我们分割的时候注意保证均分,不然递归的时候容易出现死循环。

【进来序一序吧】呕心沥血万文详解七大排序,全程无废话_第23张图片
递归展开图(部分):
【进来序一序吧】呕心沥血万文详解七大排序,全程无废话_第24张图片

7.1.3 合并

归并到tmp,再拷贝回去。
一一归下来,拷贝回去。
两两归下来,拷贝回去。
四四归下来,拷贝回去。
整个归下来,拷贝回去。
【进来序一序吧】呕心沥血万文详解七大排序,全程无废话_第25张图片

7.1.4 递归代码演示

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

7.3 归并排序非递归

我们不用递归帮我们分割,直接用循环来分割。gap>=n,我们就不分了。
【进来序一序吧】呕心沥血万文详解七大排序,全程无废话_第26张图片
错误代码示范:

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;		
			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));
		//PrintArray(a, n);
		gap *= 2;
	}
	free(tmp);
}

7.3.1 注意事项:

之所以说上面的代码是错误代码,是因为当数据的个数不是2的倍数时,程序就会崩溃。
我们按照之前的方法打印归并的区间,发现有些边界出现了越界。
【进来序一序吧】呕心沥血万文详解七大排序,全程无废话_第27张图片
【进来序一序吧】呕心沥血万文详解七大排序,全程无废话_第28张图片

7.3.2 代码演示

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 越界,第二个区间不存在,保证begin2>end2即可
			if (begin2 >= n)
			{
				begin2 = n;
				end2 = n - 1;
			}

			// begin2 ok, end2越界,修正end2即可
			if (begin2 < n && end2 >= n)
				end2 = n - 1;
			//printf("归并[%d,%d][%d,%d] -- gap=%d\n", begin1, end1, begin2, end2, gap);
			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));
		//PrintArray(a, n);

		gap *= 2;
	}

	free(tmp);
}

你可能感兴趣的:(数据结构(C语言实现),c语言,数据结构)