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

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

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

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

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

结语

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

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

c0fe1378f4b1464abb37998a472b5961.jpg

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/484945.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Rust egui(3) 增加多个tab

话说不知道咋写&#xff0c;要不直接上git patch吧。代码都是移植的官方demo&#xff0c;核心改动就是把原来的line_demo换成了plot_demo&#xff0c;里面实现多个ui&#xff0c;然后点击tab标题会切换不同的ui。 如下图&#xff0c;Lines和Markers两个不同的标签对应不同的ui。…

pytest框架的封装以及用例管理框架

pytest框架的封装以及用例管理框架 公共类统一封装requests_util02.pytest_api01.py 自动化测试的基础自动化测试的介入点自动化测试和手工测试占比自动化实施过程 pytest元素定位元素定位查找元素定位的方式通过 ID 定位通过 Name 定位通过 Class Name 定位通过 Tag Name 定位…

数据结构面试常见问题之串的模式匹配(KMP算法)系列-简单解决方案

&#x1f600;前言 字符串匹配是计算机科学中一个常见的问题&#xff0c;指的是在一个长字符串中查找一个短字符串的出现位置。在文本编辑、生物信息学、数据挖掘等领域都有着广泛的应用。 本文将介绍 KMP 算法&#xff0c;一种用于解决字符串匹配问题的经典算法。KMP 算法可以…

鸿蒙网络开发学习:【ylong_http】

简介 ylong_http 构建了完整的 HTTP 能力&#xff0c;支持用户使用 HTTP 能力完成通信场景的需求。 ylong_http 使用 Rust 编写&#xff0c;为 OpenHarmony 的 Rust 能力构筑提供支持。 ylong_http 在 OpenHarmony 中的位置 ylong_http 向 OpenHarmony 系统服务层中的网络协…

flutter实现视频播放器,可根据指定视频地址播放、设置声音,进度条拖动,下载等

需要装依赖&#xff1a; gallery_saver: ^2.3.2video_player: ^2.8.3 AndroidManifest.xml <uses-permission android:name"android.permission.INTERNET"/> 实现代码 import dart:async; import dart:io;import package:flutter/material.dart; import pa…

深入理解栈和队列(二):队列

个人主页&#xff1a;17_Kevin-CSDN博客 专栏&#xff1a;《数据结构》 一、队列的概念和结构 队列是只允许在一端进行插入数据操作&#xff0c;在另一端进行删除数据操作的特殊线性表&#xff0c;队列具有先进先出 FIFO(First In First Out) 入队列&#xff1a;进行插入操作的…

OC对象 - 关联对象(如何给分类添加成员变量)

文章目录 OC对象 - 关联对象&#xff08;如何给分类添加成员变量&#xff09;1. 基本使用1.1 提供的API1.1.1 添加关联对象1.1.2 获得关联对象1.1.3 移除所有关联对象1.1.3 修饰符 1.2 使用方法1.2 Key的常见用法1.2.1 使用的get方法的selecor作为key1.2.2 使用指针的地址作为k…

【Node.js】zlib

gzip 和 deflate 的基本使用 const zlib require("zlib"); const fs require(fs)// 压缩 1. createGzip .gz 2. createDeflate .deflate // const readStream fs.createReadStream(index.txt) // const writeStream fs.createWriteStream(index.txt.gz) // rea…

Python Flask 自定义404错误

from flask import Flask, abort, make_response, request, render_templateapp Flask(__name__)# 重定向到百度 app.route(/index, methods["GET", "POST"]) def index():if request.method "GET":return render_template("index.html&q…

Python Flask 自定义过滤器

{{ data.list | li2 }} li2就是自定义的 from flask import Flask, render_templateapp Flask(__name__)app.route("/index") def index():data {name: "张三","age": 18,list: [123123, 41, 123]}return render_template("index2.html…

nodejs+vue高校二手商品交易平台的设计与实现python-flask-django-php

当今社会已经步入了科学技术进步和经济社会快速发展的新时期&#xff0c;国际信息和学术交流也不断加强&#xff0c;计算机技术对经济社会发展和人民生活改善的影响也日益突出&#xff0c;人类的生存和思考方式也产生了变化。传统高校二手商品交易采取了人工的管理方法&#xf…

BUG未解之谜01-指针引用之谜

在leetcode里面刷题出现的问题&#xff0c;当我在sortedArrayToBST里面给root赋予初始值NULL之后&#xff0c;问题得到解决&#xff01; 理论上root是未初始化的变量&#xff0c;然后我进入insert函数之后&#xff0c;root引用的内容也是未知值&#xff0c;因此无法给原来的二叉…

array go 语言的数组 /切片

内存地址通过& package mainimport "fmt"func main() {var arr [2][3]int16fmt.Println(arr)fmt.Printf("arr的地址是: %p \n", &arr)fmt.Printf("arr[0]的地址是 %p \n", &arr[0])fmt.Printf("arr[0][0]的地址是 %p \n"…

【Linux 09】进程优先级

文章目录 &#x1f308; Ⅰ 什么是优先级&#x1f308; Ⅱ 为什么存在优先级&#x1f308; Ⅲ 查看进程优先级&#x1f308; Ⅳ 修改进程优先级 &#x1f308; Ⅰ 什么是优先级 优先级本质 指定进程获取某种资源的先后顺序。在 Linux 中&#xff0c;使用 task_struct 进程控制…

电子电器架构 —— 诊断数据DTC具体故障

电子电器架构 —— 诊断数据DTC具体故障 我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师 (Wechat:gongkenan2013)。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 本就是小人物,输了就是输了,不要在意别人怎么看自己。江湖一碗茶,喝完再挣…

【CDA二级数据分析备考思维导图】

CDA二级数据分析备考思维导图 CDA二级复习备考资料共计七个章节&#xff0c;如需资料&#xff0c;请留言&#xff0c;概览如下图&#xff1a;一、数据采集与处理1.数据采集方法2.市场调研和数据录入3、数据探索与可视化4、数据预处理方法 总结&#xff1a;以上为自己学习数据分…

现代化应用部署工具-Docker

Docker 简介 什么是Docker Docker 是一个开源的应用容器引擎&#xff0c;可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中&#xff0c;然后发布到任何流行的 Linux 机器上。 Docker部署的优势 通过使用Docker等容器技术&#xff0c;可以将应用程序及其依赖项…

R-Linux 免费ext2 / 3 / 4文件系统恢复软件简介(推荐)

R-Linux&#xff1a;只适合 3种类型的发行版使用。 R-Linux 免费ext2 / 3 / 4文件系统恢复软件简介&#xff08;推荐&#xff09; https://blog.csdn.net/allway2/article/details/102632495 R-Linux for Linux Technical Documentation https://www.r-studio.com/free-linux…

【docker常用命令】

1. 什么是docker Docker 是一种开源的容器化平台&#xff0c;用于开发、交付和运行应用程序。它允许开发人员将应用程序及其依赖项&#xff08;如库、环境变量、配置文件等&#xff09;打包到一个被称为容器的标准化单元中。这个容器包含了一切应用程序需要运行的所有内容&…

漫画之家”系统设计与实现|SpringBoot+ Mysql+Java+ B/S结构(可运行源码+数据库+设计文档)

本项目包含可运行源码数据库LW&#xff0c;文末可获取本项目的所有资料。 推荐阅读100套最新项目 最新ssmjava项目文档视频演示可运行源码分享 最新jspjava项目文档视频演示可运行源码分享 最新Spring Boot项目文档视频演示可运行源码分享 2024年56套包含java&#xff0c;…