【C语言数据结构】排序

1.排序的概念

在深入研究各个排序算法之前,首先,我们要对排序有个大概的了解,即与排序相关的一些概念

Q:什么是排序?

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

Q:什么是内部排序与外部排序?

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

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

Q:稳定性是什么?

A:稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变

即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的

那关于排序的应用,这可太多了,可以说我们每天都在跟它打交道,这里就不展开了,接下来我们就来看看各个排序算法吧~

2.常见的排序算法的实现

a.插入排序

1.直接插入排序

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

当插入第i(i>=1)个元素时,前面的a[0],a[1],...,a[i-1]已经排好序,此时用a[i]的排序码与a[i-1],a[i-2],...的排序码顺序进行比较,找到插入位置即将a[i]插入,原来位置上的元素顺序后移

直接插入排序代码实现

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 - 2; 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 TestInsertSort()
{
	int a[] = { 5, 3, 9, 6, 2, 4, 7, 1, 8 };

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

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


int main()
{
	// 原数组为5, 3, 9, 6, 2, 4, 7, 1, 8 
	TestInsertSort();
	return 0;
}

**直接插入排序的特性总结:

  1. 元素集合越接近有序,直接插入排序算法的时间效率越高
  2. 时间复杂度:O(N^2)
  3. 空间复杂度:O(1),它是一种稳定的排序算法
  4. 稳定性:稳定**

2.希尔排序

希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成整数个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,重复上述分组和排序的工作。当到达=1时,所有记录在统一组内排好序

希尔排序代码实现

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

	}
}

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

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

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


int main()
{
	// 原数组为5, 3, 9, 6, 2, 4, 7, 1, 8 
	TestShellSort();
	return 0;
}

希尔排序的特性总结:

  1. 希尔排序是对直接插入排序的优化。

  2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。

  3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些树中给出的希尔排序的时间复杂度都不固定

4.稳定性:不稳定

b.选择排序

1.直接选择排序

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

下面讲解一下这种排序的步骤:

1.在元素集合a[i] 到 a[n-1]中选择关键码最小的数据元素

2.若它(最小数据元素)不是这组元素中的第一个元素,则将它与这组元素中的第一个元素交换

3.在剩余的a[i+1] 到 a[n-1]集合中重复上述步骤,直到集合剩余1个元素

直接选择排序代码实现

void Swap(int* p1, int* p2)//交换
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

void PrintArray(int* a, int n)//用来辅助打印的函数 
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}


void SelectSort(int* a, int n)//直接选择排序
{
	int begin = 0, end = n - 1;
	while (begin < end)
	{
		//  这里进行优化 ,每次都选出最小值和最大值两个值的下标而不是只选出最小值
		int mini = begin, maxi = begin;
		for (int i = begin + 1; i <= end; i++)
		{
			if (a[i] < a[mini])
			{
				mini = i;
			}

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

		Swap(&a[begin], &a[mini]);

		if (maxi == begin)//重叠问题
		{
			maxi = mini;
		}

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

		begin++;
		end--;
	}
}

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

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

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


int main()
{
	// 原数组为5, 3, 9, 6, 2, 4, 7, 1, 8 
	TestSelectSort();
	return 0;
}

**直接选择排序的特性总结:

  1. 直接选择排序思考非常好理解,但是效率不是很好,所以实际中很少使用
  2. 时间复杂度:O(N^2)
  3. 空间复杂度:O(1)
  4. 稳定性:不稳定**

2.堆排序

关于什么是堆:堆是一颗完全二叉树,这里的具体知识要结合二叉树部分进行讲解,就不是这篇博客的重点了,所以建议学完二叉树的小伙伴再来观看关于堆排序的内容哦~

堆排序是指利用堆这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。

下面是堆排序的具体步骤

1.建堆,需要注意的是排升序要建大堆,排降序建小堆

2.将最后一个数与堆顶进行交换,然后将除最后一个数据之外的所有数据重新向下调整

3.重复第2步,直至完全升序

堆排序代码实现

void Swap(int* p1, int* p2)//交换
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

void PrintArray(int* a, int n)//用来辅助打印的函数 
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}


//向下调整 升序要建大堆
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)
{
	// a数组直接建堆 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 TestHeapSort()
{
	int a[] = { 5, 3, 9, 6, 2, 4, 7, 1, 8 };

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

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


int main()
{
	// 原数组为5, 3, 9, 6, 2, 4, 7, 1, 8 
	TestHeapSort();
	return 0;
}

堆排序的特性总结:

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

c.交换排序

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

1.冒泡排序

冒泡排序,这是排序中我们最早接触的算法,这个排序的取名很形象,所以思想也很简单

冒泡排序代码实现

void Swap(int* p1, int* p2)//交换
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

void PrintArray(int* a, int n)//用来辅助打印的函数 
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}


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

		if (flag == 0)
			break;
	}
}


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

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

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


int main()
{
	// 原数组为5, 3, 9, 6, 2, 4, 7, 1, 8 
	TestBubbleSort();
	return 0;
}

**冒泡排序的特性总结:

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

2.快速排序

快速排序是Hoare大佬提出的一种二叉树结构的交换排序方法,其基本思想为:

任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止

快速排序是排序里的一块重点内容,也是面试容易考察的一种排序,所以下面我们重点来讲解一下这个排序~

版本一:hoare版本

代码实现如下:y

void QuickSort1(int* a, int left, int right)//快排hoare版本
{
	if (left >= right)// 区间只有一个值或者不存在就是最小子问题
		return;

 
	 int begin = left, end = right;
	 int keyi = left;
	 while (left < right)
	 {
		// right先走,找小
		while (left < right && a[right] >= a[keyi])
		{
			right--;
		}

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

		Swap(&a[left], &a[right]);
	 }

	 //出循环的时候left = right,相遇
	 Swap(&a[left], &a[keyi]);
	 keyi = left;//keyi来到相遇点

	 // [begin, keyi-1]  keyi   [keyi+1, end] 左右进行递归
	 QuickSort1(a, begin, keyi - 1);
	 QuickSort1(a, keyi + 1, end);
}


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

	QuickSort1(a, 0, sizeof(a) / sizeof(int) - 1);

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


int main()
{
	// 原数组为5, 3, 9, 6, 2, 4, 7, 1, 8 
	TestQuickSort1();

	return 0;
}

对于hoare版本,我们有两点可以优化,分别是

1.选key的优化

2.小区间优化

先说第一个,选key的优化

我们可以采用三数取中法

所谓三数取中法,就是我们在区间的第一个,中间位置,和最后一个位置中选出中间值作为key,下面来写一下

int GetMidi(int* a, int left, int right)// 三数取中优化  left  mid  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[mid] > a[right])
		{
			return mid;
		}
		else if (a[left] < a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
}

然后我们可以把这个函数嵌入之前的快排程序了~

void QuickSort1(int* a, int left, int right)//快排hoare版本优化1.0
{
	if (left >= right)// 区间只有一个值或者不存在就是最小子问题
		return;

    
	 int begin = left, end = right;

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

	 int keyi = left;
	 while (left < right)
	 {...//为了节约空间,这里就不重复写了~}

	 //出循环的时候left = right,相遇
	 Swap(&a[left], &a[keyi]);
	 keyi = left;//keyi来到相遇点

	 // [begin, keyi-1]  keyi   [keyi+1, end] 左右进行递归
	 QuickSort1(a, begin, keyi - 1);
	 QuickSort1(a, keyi + 1, end);
}

再说一下第二个,小区间优化

因为递归要开辟栈帧,越到后面消耗越大,所以当递归到小的子区间时,我们可以使用插入排序~

插入排序的代码上面写过了,我们之间拿来用就行~,下面是优化后的代码

void QuickSort1(int* a, int left, int right)//快排hoare版本优化2.0
{
	if (left >= right)// 区间只有一个值或者不存在就是最小子问题
		return;

    
	if (right - left + 1 < 10)
    // 小区间选择走插入,可以减少90%左右的递归,一般是区间长度小于10比较合理
	{
		InsertSort(a + left, right - left + 1);
	}

	else
	{
	 int begin = left, end = right;

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

	 int keyi = left;
	 while (left < right)
	 {
		...//同样为了省空间。这里就不写了
	 }

	 //出循环的时候left = right,相遇
	 Swap(&a[left], &a[keyi]);
	 keyi = left;//keyi来到相遇点

	 // [begin, keyi-1]  keyi   [keyi+1, end] 左右进行递归
	 QuickSort1(a, begin, keyi - 1);
	 QuickSort1(a, keyi + 1, end);
   }
}

好,说完了hoare版本以及优化,

**下面我们看一下快速排序的第二个版本:**挖坑法

其实挖坑法的整体思路与hoare法类似,博主倾向于认为它是hoare版本的一个教学优化版

下面是挖坑法的过程图:

下面是代码:(注意,接下来的几个版本其实都是可以用上面的三数取中和小区间优化,这里博主就不再多写了,毕竟是一样的东西)

void QuickSort2(int* a, int left, int right)//快排挖坑法,其实是hoare版本的教学优化版
{
	if (left >= right)
		return;

	int begin = left, end = right;
	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;

	QuickSort2(a, begin, hole - 1);
	QuickSort2(a, hole + 1, end);
}

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

	QuickSort2(a, 0, sizeof(a) / sizeof(int) - 1);

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



int main()
{
	// 原数组为5, 3, 9, 6, 2, 4, 7, 1, 8 
	
	TestQuickSort2();
	

	return 0;
}

版本三:前后指针法

这种方法博主认为理解后是这些快排里代码最好写的,如果面试时要求手写一个快排代码,用前后指针法绝对是一个不二的选择!

下面是前后指针法的图示过程

下面是代码:(同样,这里的优化也不加了)

void QuickSort3(int* a, int left, int right)// 前后指针法
{
	if (left >= right)
		return;
	
	int keyi = left;
	int prev = left;
	int cur = left + 1;

	while (cur <= right)
	{
		if (a[cur] < a[keyi])  //cur < key , prev++ , 交换prev与cur的位置
		{ 
			prev++;
			Swap(&a[prev], &a[cur]);
		}

		cur++;
	}

	Swap(&a[keyi], &a[prev]);
	keyi = prev;//将key的下标换到prev

	// [left, keyi-1] keyi [keyi+1, right]	左右进行递归
	QuickSort3(a, left, keyi - 1);
	QuickSort3(a, keyi + 1, right);
}

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

	QuickSort3(a, 0, sizeof(a) / sizeof(int) - 1);

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

int main()
{
	// 原数组为5, 3, 9, 6, 2, 4, 7, 1, 8 

	TestQuickSort3();

	return 0;
}

最后一种: 非递归实现

将快排本来要用的递归变成非递归,这也是面试是喜欢考的东西

这里我们需要用栈的数据结构,因为本来递归用的是栈帧,而非递归用的是数据结构的栈

因为在C语言阶段,我们也只能自己手写,到了C++阶段就有现成的可以使用咯,这里我们CV一下(doge)

**非递归的步骤是:**取栈顶区间,单趟排序,右左子区间入栈

#include"Stack.h"

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 = begin;
		int prev = begin;
		int cur = begin + 1;

		while (cur <= end)
		{
			if (a[cur] < a[keyi] && ++prev != cur)
				Swap(&a[prev], &a[cur]);

			cur++;
		}

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

		// [begin, keyi-1] keyi [keyi+1, end] 
         //右左子区间入栈
		if (keyi + 1 < end)
		{  
			STPush(&st, end);
			STPush(&st, keyi + 1);
		}

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

	STDestroy(&st);
}

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

	QuickSortNonR(a, 0, sizeof(a) / sizeof(int) - 1);

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

}

int main()
{
	// 原数组为5, 3, 9, 6, 2, 4, 7, 1, 8 
	TestQuickSortNonR();

	return 0;
}

好了,至此,我们的快速排序的各个方法也就讲完啦,下面小小的进行一个总结

**快速排序的特性总结:

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

讲完了快速排序,再来看一下归并排序吧~

d.归并排序

归并排序是建立在归并操作上的一种有效的排序算法,它也是采用分治法的一个非常典型的应用

将已有序的子序列合并,得到完全有序的序列 ,即先使每个子序列有序,再使子序列段间有序

归并排序核心步骤:

下面是归并排序的动态图示过程:

归并排序的实现可分为递归与非递归两种类型,我们先看一下递归的实现

//归并排序子函数
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 i = begin;
	// 依次比较,取小的尾插到tmp数组
	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 fail");
		return;
	}

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

	free(tmp);
	tmp = NULL;
}

void TestMergeSort()
{

	int a[] = { 5, 3, 9, 6, 2, 4, 7, 1, 8 };

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

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

int main()
{
	// 原数组为5, 3, 9, 6, 2, 4, 7, 1, 8 

	TestMergeSort();


	return 0;
}

其实这里用递归的方式写还是比较轻松的,毕竟我们之前学了二叉树,真正难的是下面的非递归

这里的非递归与之前的快速排序不同,我们如果用一个栈就不太好操作了

于是我们想着用一个变量gap来归并每组的数据个数,让gap从1到2到4一直增长直到这个数据排到有序为止

我们可能会写出下面的代码:

void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		return;
	}

	int gap = 1;//gap是每组的数据个数

	while (gap < n)
	{
		for (int j = 0; j < n; j += 2 * gap)
		{
			int begin1 = j, end1 = begin1 + gap - 1;
			int begin2 = begin1 + gap, end2 = begin2 + gap - 1;
			int i = j;

			// 依次比较,取小的尾插到tmp数组
			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 + j, tmp + j, sizeof(int) * (2*gap));
		}

		gap *= 2;
	}

	free(tmp);
	tmp = NULL;
}

我们测试上面的代码,会得到这样的结果:

这里其实说明我们的数组越界了,这是怎么回事呢?

我们看一下下面这个数组,当gap = 2 的时候,它要怎么归并呢?

这里是不是就出现了越界,所以为了阻止这个问题,我们需要增加一处地方以及修改一处地方

void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		return;
	}
    int gap = 1;//gap是每组的数据个数
    while (gap < n)
	{
		for (int j = 0; j < n; j += 2 * gap)
		{
			int begin1 = j, end1 = begin1 + gap - 1;
			int begin2 = begin1 + gap, end2 = begin2 + gap - 1;
			int i = j;

			// 这里增加了对越界的问题处理
			if (end1 >= n || begin2 >= n)
				break;

			if (end2 >= n)
				end2 = n - 1;

			while (begin1 <= end1 && begin2 <= end2)
			{
				.....
			}

			while (begin1 <= end1)
			{
				...
			}

			while (begin2 <= end2)
			{
				...
			}

            //这里将拷贝的个数从2*gap修正为 end2 - j + 1
			memcpy(a + j, tmp + j, sizeof(int) * (end2 - j + 1));
		}
		gap *= 2;
	}
	free(tmp);
	tmp = NULL;
}

在上面的代码中我们增加了对越界问题的处理以及将memcpy拷贝的个数这里进行了修改,下面我们在test.c文件测试一下吧

void TestMergeSortNonR()
{

	int a[] = { 6,1,2,7,9,3,4,5,10,8 };

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

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

}

int main()
{
	// 原数组为 6,1,2,7,9,3,4,5,10,8
	
	TestMergeSortNonR();


	return 0;
}

Perfect!

最后我们看一下非比较排序吧~

e.非比较排序

之前我们所有的排序都属于 比较排序,它们通过比较大小来排序,

下面要介绍的是非比较排序,它的排序核心用的不是比较

非比较排序主要有:计数排序,基数排序和桶排序

不过这里要说的是,非比较排序都是小众的,局限的,所以下面我就说一种计数排序来给大家感受一下,它也是这三种里面应用性最高的

计数排序的操作步骤如下:

1. 统计相同元素出现次数
2. 根据统计的结果将序列回收到原来的序列中

计数排序的代码如下:(yysy这个代码与之前几个相比就简单很多了)

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

	memset(count, 0, sizeof(int) * range);//将range个0赋给count数组,完成初始化

	// 统计次数
	for (int i = 0; i < n; i++)
	{
		count[a[i] - min]++;
	}

	// 排序
	int i = 0;
	int j = 0;
	for (i = 0;i<range;i++)
	{
		while (count[i])
		{
			count[i]--;
			a[j++] = i + min;
		}
	}

	free(count);
	count = NULL;
}



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

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

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




int main()
{
	// 原数组为5, 3, 9, 6, 2, 4, 7, 1, 8 
	TestCountSort();

	return 0;
}

有这个算法思想我们可以看出,计数排序适合的是对数据的范围集中的数据进行排序,但如果数据比较分散就不太适合了,空间浪费有点大

另外,它只能排整型,如果是什么浮点型,字符型甚至结构体类型,它都不是不能用的,这也是它的局限性

好了,至此,排序的实现我们就都完成了,鼓掌鼓掌!

最后,我们把先前在开头的图拿过来,在我们学完所有的排序之后再看一遍一定会有新的感知

结语

好啦,到此为止,今天的排序内容就结束啦,如果文中分析,题解代码有不足的地方欢迎大家在评论区讨论和指正

让我们在接下来的时间里一起学习,一起进步吧~

相关推荐
Lbs_gemini06031 小时前
C++研发笔记14——C语言程序设计初阶学习笔记12
c语言·开发语言·c++·笔记·学习
叁散2 小时前
PTA--数据结构预习报告: 考试排名汇总
数据结构
霖大侠3 小时前
Adversarial Learning forSemi-Supervised Semantic Segmentation
人工智能·算法·机器学习
阿华的代码王国4 小时前
【算法】——前缀和(矩阵区域和详解,文末附)
java·开发语言·算法·前缀和
Sunyanhui14 小时前
力扣 LCR训练计划2(剑指 Offer 22. 链表中倒数第k个节点)-140
算法·leetcode·链表
yours_Gabriel4 小时前
【力扣】3274. 检查棋盘方格颜色是否相同
算法·leetcode
Chandler244 小时前
蓝桥杯经验分享
经验分享·算法·蓝桥杯
是老余4 小时前
算法基础之链表:移除链表元素leetcode203
数据结构·算法·链表
CQU_JIAKE4 小时前
3.29【机器学习】第五章作业&实现
人工智能·算法·机器学习
CQU_JIAKE4 小时前
3.27【机器学习】第五章作业&代码实现
人工智能·算法