数据结构初阶最终讲:排序

news2024/9/20 0:53:51

数据结构初阶最终讲:排序

  • 1.排序的概念及其运用
    • 1.1什么是排序
    • 1.2排序的运用
    • 1.3常见排序算法
  • 2.冒泡排序
  • 3.直接插入排序
  • 4.堆排序
  • 5.测试代码:排序性能对比
    • 5.1直接插入排序时间复杂度分析
  • 6.希尔排序
    • 6.1希尔排序时间复杂度分析
  • 7.选择排序
    • 7.1初步思路
    • 7.2选择排序优化
      • 7.2.1初步实现
      • 7.2.2出错点1
      • 7.2.3出错点2
      • 7.2.4出错点3
      • 7.2.5选择排序优化最终代码
    • 7.3选择排序时间复杂度分析
  • 8.快速排序--Hoare版本
      • 8.1.1代码的初步实现
      • 8.1.2错误点1
      • 8.1.3错误点2
      • 8.1.4错误点3
      • 8.1.5错误点4
      • 8.1.6错误点5
      • 8.1.7错误点6
      • 8.1.8错误点7
    • 8.2Hoare版本快排最终代码
    • 8.3快速排序空间复杂度分析
    • 8.4快速排序时间复杂度分析
      • 8.4.1最优情况(平均情况)
      • 8.4.2最坏情况
    • 8.5观察快速排序的排序时间
  • 9.快速排序--挖坑法
  • 10.快速排序--lomuto前后指针
  • 11.快速排序--非递归版本
  • 12.归并排序
    • 12.1归并排序时间复杂度分析
    • 12.2归并排序空间复杂度分析
  • 13.非比较排序--计数排序
    • 13.1非比较函数时间复杂度分析
  • 14.排序算法复杂度以及稳定性分析

这一讲是数据结构初阶的最后一讲了,当然也是很有难度的一讲,是对于排序算法的总篇章,当然,会有其它更好的排序算法,但是对于初阶的数据结构来说,先学习这些就足够了

1.排序的概念及其运用

1.1什么是排序

排序,顾名思义,就是对一串数据,按照某个特定的顺序,将这一串数据进行有序排列的方法

1.2排序的运用

排序在我们日常生活中的使用十分广泛

当我们在网上购物,筛选自己心仪的产品时,就使用到了排序:
在这里插入图片描述
当我们在考试完后看自己的成绩排名时,也会用到排序:
在这里插入图片描述

1.3常见排序算法

下面是一些常见的排序算法,这一讲会将这些排序算法逐一实现

在这里插入图片描述

2.冒泡排序

冒泡排序对于教学来说很有意义,至少是我所学过的第一个排序算法,但是使用起来非常麻烦,它的时间复杂度为O(n^2),是一个非常大的复杂度值了

//冒泡排序
void BubbleSort(int* arr, int n)
{
	//冒泡排序十分经典,而且使用很少,所以不再讲解,这里只会作为一个对比
	for (int i = 0; i < n-1; i++)
	{
		int def = 0;
		for (int j = 0; j < n - i - 1; j++)
		{
			if (arr[j] > arr[j + 1])
			{
				Swap(&arr[j], &arr[j + 1]);
				def = 1;
			}
		}
		//当没有一个数据交换时,表示已经排序完了,直接退出即可
		if (def == 0)
		{
			break;
		}
	}
}

3.直接插入排序

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

上面是害怕大图会看不清,所以分成了三个小图,下面放大图:
blog.csdnimg.cn/direct/d0840dab5c474e618981bcc87c598e5f.png)

我们会惊奇地发现,这个算法竟然和我们打扑克牌时整牌的操作一样!(至少和我是一样的):

在这里插入图片描述

下面我们来使用代码实现这个思路:

//直接插入排序
void InsertSort(int* arr, int n)
{
	for (int i = 0; i < n-1; i++)
	{
		int end = i;
		int tmp = arr[end + 1];
		while (end >= 0)
		{
			if (arr[end] > tmp)
			{
				arr[end + 1] = arr[end];
				end--;
			}
			else
			{
				break;
			}
		}
		arr[end + 1] = tmp;
	}
}

4.堆排序

之前我们已经实现过堆排序了,所以这里也不再详细说明堆排序,直接将代码拿过来:

链接: 堆排序链接

//交换函数
void Swap(HeapDataType* x, HeapDataType* y)
{
	HeapDataType tmp = *x;
	*x = *y;
	*y = tmp;
}

//堆的向上排序
void ADJustUp(HeapDataType* arr, int child)
{
	int father = (child - 1) / 2;
	while (child > 0)
	{
		//建大堆:>
		//建小堆:<
		if (arr[child] < arr[father])
		{
			Swap(&arr[child], &arr[father]);
			child = father;
			father = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

//堆的向下排序
void ADJustDown(HeapDataType* arr, int father, int n)
{
	int child = father * 2 + 1;
	while (child < n)
	{
		//大堆:寻找左右孩子中最大的那个孩子
		//小堆:寻找左右孩子中最小的那个孩子
		if (child + 1 < n && arr[child] < arr[child + 1])
		{
			child += 1;
		}
		//如果目标数据小/大的话,将最大的那个孩子与目标数据进行交换
		if (arr[father] < arr[child])
		{
			Swap(&arr[father], &arr[child]);
			father = child;
			child = father * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

//堆排序
void HeapSort(int* arr, int sz)
{
	//首先需要一个数据一个数据地拿出来,创建一个堆
	for (int i = 0; i < sz; i++)
	{
		ADJustUp(arr, i);
	}

	//向下调整算法建堆
	int n = (sz - 1 - 1) / 2;
	for (int i = n; i >= 0; i--)
	{
		ADJustDown(arr, i, sz);
	}

	//先交换数据
	int end = sz;
	while(end > 1)
	{
		Swap(&arr[0], &arr[end - 1]);
		end--;
		//然后对堆中第一个数据进行向下排序
		ADJustDown(arr, 0, end);
	}
}

5.测试代码:排序性能对比

已经有了三种排序代码,哪一种排序的效率是最高的才是我们最想要看到的,所以我们需要一个测试排序性能的代码:

// 测试排序的性能对⽐
void TestOP()
{
	//-------------------1.----------------------
	//1,这里代表着生成十万个数据,方便后序进行排序
	//a1、a2、a3分别指代不同的排序方法
	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);
	for (int i = 0; i < N; ++i)
	{
		a1[i] = rand();
		/*a2[i] = a1[i];
		a3[i] = a1[i];*/
		a4[i] = a1[i];
		/*a5[i] = a1[i];
		a6[i] = a1[i];*/
		a7[i] = a1[i];
	}
	//-------------------1.----------------------
	//-------------------2.----------------------
	//2.clock是一个C语言中的函数,可以返回从程序开始运行到系现在所用的时间
	//用begin2 - begin1就可以得出算法运行的时间了
	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);
	int end5 = clock();

	int begin6 = clock();
	MergeSort(a6, N);
	int end6 = clock();*/

	int begin7 = clock();
	BubbleSort(a7, N);
	int end7 = clock();
	//-------------------2.----------------------
	//-------------------3.----------------------
	//3.最后将得出的时间进行打印并且销毁空间即可
	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("MergeSort:%d\n", end6 - begin6);*/
	printf("BubbleSort:%d\n", end7 - begin7);
	free(a1);
	/*free(a2);
	free(a3);*/
	free(a4);
	/*free(a5);
	free(a6);*/
	free(a7);
	//-------------------3.----------------------
}

拥有了检测排序性能的函数之后,我们就可以直观地看出不同排序算法之间的效率差异了:
在这里插入图片描述
时间单位为毫秒,可以看出,冒泡排序竟然用了15秒!真是夸张,所以冒泡排序还是不足以应对生活中的排序的

5.1直接插入排序时间复杂度分析

//直接插入排序
void InsertSort(int* arr, int n)
{
	for (int i = 0; i < n-1; i++)
	{
		//当外层循环一次时,内层循环最坏要交换i+1次,也就是下面的那样:
		//i        0  1  2  3  4  5  ...  n-1
		//循环次数 1  2  3  4  5  6  ...  n
		//循环次数是等差数列,求和并分析可以得出它的时间复杂度为O(n^2)
		//但是这个最坏情况是很难达到的,而冒泡排序两层循环,而且一一对比,为什么效率低很容易理解
		int end = i;
		int tmp = arr[end + 1];
		while (end >= 0)
		{
			if (arr[end] > tmp)
			{
				arr[end + 1] = arr[end];
				end--;
			}
			else
			{
				break;
			}
		}
		arr[end + 1] = tmp;
	}
}

直接插入排序的时间消耗已经很不错了,但是还有一个人不满足,他想要优化插入排序,让插入排序更块,它就是希尔排序,我们来看:

6.希尔排序

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

然后是大图:
在这里插入图片描述

代码实现:

//希尔排序
void ShellSort(int* arr, int n)
{
	int gap = n;
	while(gap > 1)
	{
		//当gap为3时,gap/3=1,所以刚循环一次就跳出循环,显然是不合理的,所以这里要+1
		gap = gap / 3 + 1;
		//注意:这里要考虑for循环中i的结束条件为什么是i<n-gap而不是i<n
		//当i = n-gap时,end+gap = n,此时已经发生了越界,所以i应该小于n-gap
		for (int i = 0; i < n - gap; i++)
		{
			int end = i;
			int tmp = arr[end + gap];
			while (end >= 0)
			{
				if (arr[end] > tmp)
				{
					arr[end + gap] = arr[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			arr[end + gap] = tmp;
		}
	}
}

此时我们再来看一下运行时间:
在这里插入图片描述
竟然是11秒,比堆排序花费的时间还少了,是不是很振奋!!!

6.1希尔排序时间复杂度分析

对于希尔排序的时间复杂度的求解到目前仍然是一个相对困难的事,但是它的时间复杂度大约是n的1.3次方,这是由大量数据平均得来的:
在这里插入图片描述

现在我们进行简单的推导:
在这里插入图片描述在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

7.选择排序

7.1初步思路

初步实现思路就是选择最小的数据,将最小的数据往前移

在这里插入图片描述
代码实现:

//选择排序
void SelectSort(int* arr, int n)
{
	//这里的i就能够充当begin的作用,所以不用创建变量begin
	for (int i = 0; i < n; i++)
	{
		int mini = i;
		//先寻找最小的数据的下标
		for (int j = i+1; j < n; j++)
		{
			if (arr[j] < arr[mini])
			{
				mini = j;
			}
		}
		//找到最小的元素之后,交换
		Swap(&arr[mini], &arr[i]);
	}
}

7.2选择排序优化

7.2.1初步实现

上一个实现思路的时间复杂度为o(n^2),非常的不好,所以我们要进行优化:再创建一个指针,保存最大的数据,将小数据和大数据一起排列:

在这里插入图片描述
代码实现:

//选择排序--优化后--初步实现
void SelectSort(int* arr, int n)
{
	int begin = 0;
	int end = n - 1;
	while (begin < end)
	{
		int mini = begin;
		int maxi = begin;
		//寻找最大和最小的数据
		for (int i = begin+1; i < n; i++)
		{
			if (arr[i] > arr[maxi])
			{
				maxi = i;
			}
			if (arr[i] < arr[mini])
			{
				mini = i;
			}
		}
		//然后将大小数据插入到合适的位置
		Swap(&arr[begin++], &arr[mini]);
		Swap(&arr[end--], &arr[maxi]);
	}
}

然而,当我们对数据进行排序时,我们会发现:出错了!!!为什么呢?我们来探讨一下:

7.2.2出错点1

查找时应该在begin–end范围内查找,而不是begin–n范围
在这里插入图片描述

//选择排序--优化后
void SelectSort(int* arr, int n)
{
	int begin = 0;
	int end = n - 1;
	while (begin < end)
	{
		int mini = begin;
		int maxi = begin;
		//寻找最大和最小的数据
		//更改点1:查找范围出错,i<n ---> i<end
		for (int i = begin+1; i < end; i++)
		{
			if (arr[i] > arr[maxi])
			{
				maxi = i;
			}
			if (arr[i] < arr[mini])
			{
				mini = i;
			}
		}
		//然后将大小数据插入到合适的位置
		Swap(&arr[begin++], &arr[mini]);
		Swap(&arr[end--], &arr[maxi]);
	}
}

7.2.3出错点2

在这里插入图片描述
代码实现:

//选择排序--优化后
void SelectSort(int* arr, int n)
{
	int begin = 0;
	int end = n - 1;
	while (begin < end)
	{
		int mini = begin;
		int maxi = begin;
		//寻找最大和最小的数据
		//更改点1:查找范围出错,i<n ---> i<end
		for (int i = begin+1; i < end; i++)
		{
			if (arr[i] > arr[maxi])
			{
				maxi = i;
			}
			if (arr[i] < arr[mini])
			{
				mini = i;
			}
		}
		//出错点2:考虑maxi==begin的情况
		if (maxi == begin)
		{
			maxi = mini;
		}
		//然后将大小数据插入到合适的位置
		Swap(&arr[begin++], &arr[mini]);
		Swap(&arr[end--], &arr[maxi]);
	}
}

7.2.4出错点3

这个出错点和第一个出错点的位置一样,i的范围应该包括end,i<end是错的,只能是i<=end

代码更正:

//选择排序--优化后
void SelectSort(int* arr, int n)
{
	int begin = 0;
	int end = n - 1;
	while (begin < end)
	{
		int mini = begin;
		int maxi = begin;
		//寻找最大和最小的数据
		//更改点1:查找范围出错,i<n ---> i<end
		//出错点3:i应该<=end
		for (int i = begin+1; i <= end; i++)
		{
			if (arr[i] > arr[maxi])
			{
				maxi = i;
			}
			if (arr[i] < arr[mini])
			{
				mini = i;
			}
		}
		//出错点2:考虑maxi==begin的情况
		if (maxi == begin)
		{
			maxi = mini;
		}
		//然后将大小数据插入到合适的位置
		Swap(&arr[begin++], &arr[mini]);
		Swap(&arr[end--], &arr[maxi]);
	}
}

7.2.5选择排序优化最终代码

//选择排序--优化后
void SelectSort(int* arr, int n)
{
	int begin = 0;
	int end = n - 1;
	while (begin < end)
	{
		int mini = begin;
		int maxi = begin;
		//寻找最大和最小的数据
		//更改点1:查找范围出错,i<n ---> i<end
		//出错点3:i应该<=end
		for (int i = begin+1; i <= end; i++)
		{
			if (arr[i] > arr[maxi])
			{
				maxi = i;
			}
			if (arr[i] < arr[mini])
			{
				mini = i;
			}
		}
		//出错点2:考虑maxi==begin的情况
		if (maxi == begin)
		{
			maxi = mini;
		}
		//然后将大小数据插入到合适的位置
		Swap(&arr[begin++], &arr[mini]);
		Swap(&arr[end--], &arr[maxi]);
	}
}

7.3选择排序时间复杂度分析

选择排序外循环每次循环一次,内循环都要走一遍,所以它的时间复杂度为O(n^2),时间复杂度太差,所以一般不用这个排序,只是让我们了解这个排序,并且直到这个排序算法并不理想,接着我们看一下排十万个数据的时间吧:

在这里插入图片描述
可以看出,当冒泡排序时间才为4秒时,选择排序就排了3秒,所以说不好用,下面我们就要讲一个排序的"神"算法–快速排序,也就是我们熟知的快排

8.快速排序–Hoare版本

在这里插入图片描述
看起来十分难懂啊,我们不用管这些,让我画图进行理解:
在这里插入图片描述

8.1.1代码的初步实现

//快速排序
//寻找基值的函数,返回基值的下标
int _QuickSort(int* arr, int left, int right)
{
	int key = 0;
	while (left <= right)
	{
		//找出比基值大的数据
		while(arr[left] < arr[key])
		{
			//当找到的数据比基值小时,往后找,直到找到比基值大的数据
			left++;
		}
		//找出比基值小的数据
		while (arr[right] > arr[key])
		{
			//当找到的数据比基值大时,往前找,直到找到比基值小的数据
			right--;
		}
		//然后将大小数据进行交换
		Swap(&arr[left], &arr[right]);
	}
	//最后将基值赋值到right,right就成为了基值下标
	Swap(&arr[key], &arr[right]);
	return right;
}

//与其他排序方法不同,这里应该传入数组、排序区域的左区间、排序区域的右区间
void QuickSort(int* arr, int left, int right)
{
	//递归结束条件
	//当left和right重合时,表示此时只有一个结点,退出递归
	if (left >= right)
	{
		return;
	}

	//寻找基值
	int key = _QuickSort(arr, left, right);

	//递归寻找基值
	QuickSort(arr, left, key-1);
	QuickSort(arr, key + 1, right);
}

8.1.2错误点1

第五行代码我们竟然就已经出错了,做题一定要仔细!

//快速排序
//寻找基值的函数,返回基值的下标
int _QuickSort(int* arr, int left, int right)
{
	//错误点1:key应该等于left,指向的是排序数组中的第一个数据,而不是整个数组中的第一个数据
	int key = left;

8.1.3错误点2

仍然是第五行,而且还是特别明显的错误:

//快速排序
//寻找基值的函数,返回基值的下标
int _QuickSort(int* arr, int left, int right)
{
	//错误点1:key应该等于left,指向的是排序数组中的第一个数据,而不是整个数组中的第一个数据
	int key = left;
	//错误点2:left指向的位置应该是key位置之后的数据,而不是指向key这个位置
	++left;

8.1.4错误点3

这是一个注意点,因为错误率很高:
在这里插入图片描述

//快速排序
//寻找基值的函数,返回基值的下标
int _QuickSort(int* arr, int left, int right)
{
	//错误点1:key应该等于left,指向的是排序数组中的第一个数据,而不是整个数组中的第一个数据
	int key = left;
	//错误点2:left指向的位置应该是key位置之后的数据,而不是指向key这个位置
	++left;
	//注意点3:当left=right时,仍然要进入循环
	while (left <= right)

8.1.5错误点4

while (left <= right)
{
	//错误点4:假设待排序数组中的所有数据都比基值数据大,left会一直向后++,会发生越界情况,所以这边的判断需要加上一个条件
	//找出比基值大的数据
	while (left<=right && arr[left] < arr[key])
	{
		//当找到的数据比基值小时,往后找,直到找到比基值大的数据
		left++;
	}
	//错误点4:和上边的错误点4一样
	//找出比基值小的数据
	while(left<=right && arr[right] > arr[key])
	{
		//当找到的数据比基值大时,往前找,直到找到比基值小的数据
		right--;
	}

8.1.6错误点5

这也是一个注意点

//错误点4:假设待排序数组中的所有数据都比基值数据大,left会一直向后++,会发生越界情况,所以这边的判断需要加上一个条件
//找出比基值大的数据
//注意点5:假设所有数据都为6,如果这里的判断条件为arr[left] <= arr[key]的话,left会一直向后++,right一直向前--,
//        导致基值不变,造成死循环,所以这边的判断条件不能为=
while(left<=right && arr[left] < arr[key])
{
	//当找到的数据比基值小时,往后找,直到找到比基值大的数据
	left++;
}
//错误点4:和上边的错误点4一样
//找出比基值小的数据
//注意点5:和上边注意点5相同
while(left<=right && arr[right] > arr[key])
{
	//当找到的数据比基值大时,往前找,直到找到比基值小的数据
	right--;
}

8.1.7错误点6

在交换时要加上前置条件,否则会出现下面的情况:
在这里插入图片描述

//错误点6:交换时要加上前置条件
if(left<=right)
{
	//然后将大小数据进行交换
	Swap(&arr[left], &arr[right]);
}

8.1.8错误点7

//错误点6:交换时要加上前置条件
if(left<=right)
{
	//然后将大小数据进行交换
	//错误点7:交换完之后要注意改变left和right,在思路图中想到了,但是没有实现在代码上
	Swap(&arr[left++], &arr[right--]);
}

8.2Hoare版本快排最终代码

//快速排序
//寻找基值的函数,返回基值的下标
int _QuickSort(int* arr, int left, int right)
{
	//错误点1:key应该等于left,指向的是排序数组中的第一个数据,而不是整个数组中的第一个数据
	int key = left;
	//错误点2:left指向的位置应该是key位置之后的数据,而不是指向key这个位置
	++left;
	//注意点3:当left=right时,仍然要进入循环
	while (left <= right)
	{
		//错误点4:假设待排序数组中的所有数据都比基值数据大,left会一直向后++,会发生越界情况,所以这边的判断需要加上一个条件
		//找出比基值大的数据
		//注意点5:假设所有数据都为6,如果这里的判断条件为arr[left] <= arr[key]的话,left会一直向后++,right一直向前--,
		//        导致基值不变,造成死循环,所以这边的判断条件不能为=
		while (left<=right && arr[left] < arr[key])
		{
			//当找到的数据比基值小时,往后找,直到找到比基值大的数据
			left++;
		}
		//错误点4:和上边的错误点4一样
		//找出比基值小的数据
		//注意点5:和上边注意点2相同
		while (left<=right && arr[right] > arr[key])
		{
			//当找到的数据比基值大时,往前找,直到找到比基值小的数据
			right--;
		}
		//错误点6:交换时要加上前置条件
		if(left<=right)
		{
			//然后将大小数据进行交换
			//错误点7:交换完之后要注意改变left和right,在思路图中想到了,但是没有实现在代码上
			Swap(&arr[left++], &arr[right--]);
		}
	}
	//最后将基值赋值到right,right就成为了基值下标
	Swap(&arr[key], &arr[right]);
	return right;
}

//与其他排序方法不同,这里应该传入数组、排序区域的左区间、排序区域的右区间
void QuickSort(int* arr, int left, int right)
{
	//递归结束条件
	//当left和right重合时,表示此时只有一个结点,退出递归
	if (left >= right)
	{
		return;
	}

	//寻找基值
	int key = _QuickSort(arr, left, right);

	//递归寻找基值
	QuickSort(arr, left, key-1);
	QuickSort(arr, key + 1, right);
}

8.3快速排序空间复杂度分析

函数递归每调用一次就会创建一次函数栈帧,每次return循环结束之后就会销毁函数栈帧,而快排在调用时的作用如图:
在这里插入图片描述
每一次递归就会将原数组分为两半,所以求空间复杂度,就是求这个二叉树的深度,也就是log2N,所以快速排序的空间复杂度为log2N

8.4快速排序时间复杂度分析

8.4.1最优情况(平均情况)

对于一个具有n个数据的数组来说,每一层需要交换的数据为n个,而深度为longN,所以总的排序工作量为NlogN,时间复杂度也就是O(NlogN)

8.4.2最坏情况

如果每次分区都极度不均衡,也就是每次基值都是数组中最大的数据或最小的数据,此时快速排序的时间复杂度就退化为O(n^2)

8.5观察快速排序的排序时间

快速排序在排序中是最快的,所以我们一定会很期待快排的发挥:

在这里插入图片描述
可以看出,在十万个数据中,快排只需要4ms,这次我们增大数据量,将数据增加到100万:
在这里插入图片描述
对于100万个数据,快速排序只用了39ms,相当快速,这时希尔排序只是个意外【笑哭】

9.快速排序–挖坑法

挖坑法为查找基值提供了一个新的思路,我们来看:

在这里插入图片描述

代码实现:

int _QuickSort(int* arr, int left, int right)
{
	//先在第一个值的位置挖坑
	int hole = left;
	//将第一个坑中的值保存下来
	int key = arr[left];
	while (left < right)
	{
		//先让right向前走
		//错误点1:此时要加上=,当数组中的数据全为6时,如果不加等号的话right的值不会改变,程序会死循环
		while (left < right && arr[right] >= key)
		{
			right--;
		}
		//填坑挖新坑
		arr[hole] = arr[right];
		hole = right;
		//再让left向后走
		//错误点1:看上边错误点1
		while (left < right && arr[left] <= key)
		{
			left++;
		}
		arr[hole] = arr[left];
		hole = left;
	}
	arr[hole] = key;
	return hole;
}

//与其他排序方法不同,这里应该传入数组、排序区域的左区间、排序区域的右区间
void QuickSort(int* arr, int left, int right)
{
	//递归结束条件
	//当left和right重合时,表示此时只有一个结点,退出递归
	if (left >= right)
	{
		return;
	}

	//寻找基值
	int key = _QuickSort(arr, left, right);

	//递归寻找基值
	QuickSort(arr, left, key-1);
	QuickSort(arr, key + 1, right);
}

10.快速排序–lomuto前后指针

在这里插入图片描述
代码实现:

//Lomuto法
int _QuickSort(int* arr, int left, int right)
{
	int key = left;
	int prev = left;
	int pcur = left +1;
	while (pcur < right)
	{
		if (arr[pcur] < arr[key] && ++prev!=pcur)
		{
			Swap(&arr[pcur], &arr[prev]);
		}
		pcur++;
	}
	Swap(&arr[key], &arr[prev]);
	return prev;
}

//与其他排序方法不同,这里应该传入数组、排序区域的左区间、排序区域的右区间
void QuickSort(int* arr, int left, int right)
{
	//递归结束条件
	//当left和right重合时,表示此时只有一个结点,退出递归
	if (left >= right)
	{
		return;
	}

	//寻找基值
	int key = _QuickSort(arr, left, right);

	//递归寻找基值
	QuickSort(arr, left, key-1);
	QuickSort(arr, key + 1, right);
}

11.快速排序–非递归版本

上面我们实现的快速排序都是在递归的基础上实现的,下面我们接触一个新的非递归实现快排的方法:

在这里插入图片描述
代码实现:

//快速排序--非递归版本
void QuickSortNoneD(int* arr, int left, int right)
{
	Stack s;
	Init(&s);

	//先将右值和左值入栈
	StackPush(&s, right);
	StackPush(&s, left);

	//当栈为空时,结束循环
	while (s.top != 0)
	{
		//先从栈中取左值和右值
		left = StackPrint(&s);
		StackPop(&s);
		right = StackPrint(&s);
		StackPop(&s);

		//然后在左值--右值这一块空间中寻找基值--lomuto指针法
		int keyi = left;
		int prev = left;
		int pcur = left +1;
		while (pcur <= right)
		{
			if (arr[pcur] < arr[keyi] && ++prev != pcur)
			{
				Swap(&arr[pcur], &arr[prev]);
			}
			pcur++;
		}
		Swap(&arr[keyi], &arr[prev]);
		//此时基值为prev

		//然后再次入栈,先让右数组入栈,再让左数组入栈
		//1.当只有一个数据时不用入栈
		//2.当数组的指向越界时不用入栈
		//左数组:[left, prev-1]
		//右数组:[prev+1, right]
		if (prev + 1 < right)
		{
			StackPush(&s, right);
			StackPush(&s, prev + 1);
		}
		if (left < prev - 1)
		{
			StackPush(&s, prev - 1);
			StackPush(&s, left);
		}
	}

	Destory(&s);
}

此时再使用100万个数据进行检测:
在这里插入图片描述
快速排序依旧很强势!!!

12.归并排序

在这里插入图片描述

其实就是先递归,然后排序

在这里插入图片描述
代码实现:

//归并排序
void _MergeSort(int* arr, int left, int right, int* tmp)
{
	//递归结束条件:left>=right
	if (left >= right)
	{
		return;
	}

	int mid = (left + right) / 2;
	_MergeSort(arr, left, mid, tmp);
	_MergeSort(arr, mid + 1, right, tmp);

	//递归完成,开始向上排序
	int begin1 = left, end1 = mid;

	int begin2 = mid + 1, end2 = right;
	//对数组进行排序,谁小谁插入到新的数组中
	//错位点1:这里的i应该指向left,不能指向0
	int index = begin1;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (arr[begin1] < arr[begin2])
		{
			tmp[index++] = arr[begin1++];
		}
		else
		{
			tmp[index++] = arr[begin2++];
		}
	}
	//退出循环只有两种可能:
	//1.begin1的数组为空
	//错误点2:这里应该为while,不能是if
	while (begin2 <= end2)
	{
		tmp[index++] = arr[begin2++];
	}
	//2.begin2的数组为空
	while (begin1 <= end1)
	{
		tmp[index++] = arr[begin1++];
	}
	//最后要将tmp中的数据赋值到arr数组中
	//错误点3:这里j应该初始化为left,不能为0
	for (int i = left; i <= right; i++)
	{
		arr[i] = tmp[i];
	}
}

void MergeSort(int* arr, int n)
{
	//开辟新的数组空间来存储排序好的数据
	int* tmp = (int*)malloc(sizeof(int) * n);

	//递归进行排序
	_MergeSort(arr, 0, n - 1, tmp);

	free(tmp);
}

对100万数据进行排序的结果:
在这里插入图片描述
毋庸置疑是一个非常好用的排序方法!!!

12.1归并排序时间复杂度分析

对于归并排序,假设有N个数据,那么递归的深度为logN,而每一层递归都是要将这一层的数据进行排序,所以它的时间复杂度为NlogN

12.2归并排序空间复杂度分析

归并排序使用过程中开辟了一个数组的空间,所以它的空间复杂度为O(N)

13.非比较排序–计数排序

之前所使用的所有排序算法都是通过数据之间的比较,从而将大数和小数区分出来的,现在我们要掌握一种不需要比较就能够进行排序的算法

在这里插入图片描述
代码实现:

//非比较排序
void CountSort(int* arr, int n)
{
	//先确定数组中的最大值和最小值
	int max = arr[0];
	int min = arr[0];
	for (int i = 0; i < n; i++)
	{
		if (arr[i] > max)
		{
			max = arr[i];
		}
		if (arr[i] < min)
		{
			min = arr[i];
		}
	}

	//然后通过最大值和最小值来开辟空间
	int capacity = max - min + 1;
	int* tmp = (int*)malloc(capacity * sizeof(int));
	//将开辟的空间初始化为0
	memset(tmp, 0, capacity * sizeof(int));

	//将原数组中每个数据出现的次数存储到开辟数组中
	for (int i = 0; i < n; i++)
	{
		tmp[arr[i] - min]++;
	}

	//通过开辟数组记录的数据实现排序
	int index = 0;
	for (int i = 0; i < capacity; i++)
	{
		while (tmp[i]--)
		{
			arr[index++] = i + min;
		}
	}
}

然后我们让这个排序算法来排100万个数据:
在这里插入图片描述
仅仅用了1ms!!!但是这个算法有致命的缺点:
1.无法对小数进行排序
2.对于整数的排序在特殊情况下空间浪费多

13.1非比较函数时间复杂度分析

//非比较排序--时间复杂度分析--O(N)
void CountSort(int* arr, int n)
{
	int max = arr[0];
	int min = arr[0];
	//O(N)
	for (int i = 0; i < n; i++)
	{
		if (arr[i] > max)
		{
			max = arr[i];
		}
		if (arr[i] < min)
		{
			min = arr[i];
		}
	}

	int capacity = max - min + 1;
	int* tmp = (int*)malloc(capacity * sizeof(int));
	memset(tmp, 0, capacity * sizeof(int));
	//O(N)
	for (int i = 0; i < n; i++)
	{
		tmp[arr[i] - min]++;
	}
	//此处虽然是两层循环,但是作用是遍历开辟数组,对arr数组进行赋值
	//所以此处的时间复杂度还是O(N)
	int index = 0;
	for (int i = 0; i < capacity; i++)
	{
		while (tmp[i]--)
		{
			arr[index++] = i + min;
		}
	}
}

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

在这里插入图片描述
总结:
在这里插入图片描述
分析:
在这里插入图片描述

到这里,所有排序已经讲完,之后将要学习C++的内容,也是成功地脱离新手村了,希望自己能够坚持下来!!!

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Python系列】异步编程在 Python 中的应用

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

WinCC7.5零基础教学:多功能VB/C脚本基础框架模板程序详解!报表、配方、面板类型、菜单栏切换画、IO控制模板一应俱全,0基础小白值得拥有!

了解基础框架完整功能请观看视频&#xff01; 观看完整教学视频点击这里&#xff08;腾讯视频可放心观看&#xff09; 以下是wincc多功能脚本基础框架项目功能简介&#xff1a; 功能一&#xff1a;多功能标题窗口模块 模块主要功能包括实时报警窗口信息、人员登录登出、报警消…

24暑假算法刷题 | Day30 | 贪心算法 IV | 452. 用最少数量的箭引爆气球,435. 无重叠区间,763. 划分字母区间

目录 452. 用最少数量的箭引爆气球题目描述题解 435. 无重叠区间题目描述题解 763. 划分字母区间题目描述题解 452. 用最少数量的箭引爆气球 点此跳转题目链接 题目描述 有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points &#xff0c;其中…

印刷企业实施数字工厂管理系统后能提升哪些效益

在当今这个数字化转型浪潮席卷全球的时代&#xff0c;印刷企业作为传统制造业的重要组成部分&#xff0c;正面临着前所未有的挑战与机遇。为了提升生产效率、降低成本、增强市场竞争力&#xff0c;越来越多的印刷企业开始引入数字工厂管理系统&#xff0c;这一举措不仅重塑了企…

Inno Setup根据系统的不同(32位/64位)安装不同的exe

注意事项 Inno Setup6.0及以上版本生成的可执行文件&#xff0c;可以运行在Windows7及以上系统&#xff0c;不支持WindowsXP系统。 如果要运行在WindowsXP系统上&#xff0c;需要下载Inno Setup6.0之前的版本。 Inno Setup 汉化版5.3.4下载链接&#xff1a; https://download…

从入门到精通:大学生编程技能提升全攻略

文章目录 每日一句正能量前言编程语言选择编程语言选择&#xff1a;为新手导航Python&#xff1a;初学者的友好伙伴JavaScript&#xff1a;Web开发的核心Java&#xff1a;企业级应用的经典C&#xff1a;系统编程的基石Ruby&#xff1a;优雅高效的编程Swift&#xff1a;iOS开发的…

OBS设置大揭秘:参数优化技巧与顶级录屏软件全攻略

在这个数字化的时代&#xff0c;屏幕录制已成为我们记录和分享知识、技能的重要手段。如果你还在为寻找一款既专业又易用的录屏软件而烦恼&#xff0c;那么今天的文章将为你揭开谜底。 录屏软件一、OBS studio OBS studio&#xff0c;作为录屏和直播领域的标杆&#xff0c;其功…

echarts图例旁边加百分比及百分比对齐

一、效果图 在这里插入图片描述 二、代码 import cirle from /assets/imgs/dataScree/ybp.pnglet option{tooltip: {trigger: item,formatter: function (params) {return }},legend: {orient: vertical, // 图例列表的布局朝向&#xff0c;horizontal为水平,vertical为垂直…

电话机器人能提升销售效率

经济敏捷发展的当天&#xff0c;任何行业都不离开市场&#xff0c;无法让更多人理解本人企业本人的产物。那样的话&#xff0c;像电话营销那样抢手的行业也面临很大的困难&#xff0c;员工不仅工作压力大&#xff0c;工作时间长&#xff0c;呼叫量多&#xff0c;还能忍受顾客的…

LangChain: Reduce size of tokens being passed to OpenAI

题意&#xff1a;在使用 LangChain时&#xff0c;需要减少传递给OpenAI的令牌&#xff08;tokens&#xff09;的数量 问题背景&#xff1a; I am using LangChain to create embeddings and then ask a question to those embeddings like so: 我正在使用 LangChain 来创建嵌…

记录|To run this application, you must install .NET Core.【C#,VS】

目录 前言一、问题描述二、解决办法三、.NET版本和Windows版本对接更新时间 前言 参考文章&#xff1a; 1、安装失败&#xff0c;提示“To run this application,you must install .netcore…” 2、【bug】to run this application ,you must install .net 3、在 Windows 上安装…

【亲测管用】Windows11 部署Maxkb + 本地Ollama

一、下载地址 maxkb&#xff1a;https://maxkb.cn/ ollama: https://ollama.com/download/windows 二、安装ollama 2.1 默认安装 直接install就好&#xff0c;默认是安装到C盘里面的。 安装好之后会显示在 直接打开cmd黑窗口&#xff0c;输入命令即可查看。 2.2 ollama…

什么牌子的洗地机好用?石头、添可、希亦真实使用测评对比!

随着科技的不断进步&#xff0c;许多人已经更新了家中的清洁工具。在挑选时&#xff0c;大家可能会看到很多网络攻略&#xff0c;但看得越多&#xff0c;反而越难抉择。其实&#xff0c;最直接的方法是看看这些工具的真实使用体验&#xff0c;这样就能大概知道自己使用时的感受…

5G 网络切片

5G 业务分类 增强型移动宽带(eMBB) 传统数据业务&#xff0c;特点是高带宽超高可靠性低时延业务&#xff08;URLLC&#xff09;无人驾驶、工业自动化等业务, 特点是高可靠、低时延海量机器类通信(mMTC) 物联网&#xff0c;特点是大量连接&#xff0c;时延不敏感&#xff0c;数…

练习2.19

先上代码吧。 (defn first-denomination [coin-values](first coin-values))(defn no-more? [coin-values](if (empty? coin-values) truefalse))(defn except-first-denomination [coin-values ](rest coin-values ))(defn cc[amount coin-values](cond ( amount 0) 1(or (…

ChatGPTAI指令提示工程案例

一、AI指令提示工程概述 AI指令提示工程&#xff08;AI Prompt Engineering&#xff09;是指通过设计巧妙的提示词&#xff08;Prompt&#xff09;来引导人工智能模型&#xff0c;特别是像ChatGPT这样的自然语言处理模型&#xff0c;生成符合用户需求的回答。这一过程不仅涉及…

重塑视界,流畅无界:『Levels of Detail』

在那片烽火连天的虚拟疆域——《刺激战场》中&#xff0c;你是否曾披荆斩棘&#xff0c;穿梭于一个个错落有致的城市迷宫&#xff1f;当我们飞越天际&#xff0c;俯瞰那些精心雕琢的城市场景&#xff0c;心中是否闪过一丝好奇的火花&#xff1a;在这广阔天地间&#xff0c;为何…

冷门细分才是王道,小白用AI几分钟生成,都有出了7000+单了

现在大家都在说&#xff0c;网上整点米&#xff0c;越来越难了&#xff0c;竞争太激烈。那要不看看冷门赛道&#xff0c;看这样冷门细分赛道&#xff0c;【绘本故事】&#xff0c;我视频号&#xff0c;和其他平台都搜了下&#xff0c;千赞&#xff0c;万赞很多。 这样的图片大家…

一文带你彻底搞定小程序分包和预加载

小程序分包 某些情况下&#xff0c;开发者需要将小程序划分成不同的子包&#xff0c;在构建时打包成不同的分包&#xff0c;用户在使用时按需进行加载。 在构建小程序分包项目时&#xff0c;构建会输出一个或多个分包。每个使用分包小程序必定含有一个主包。所谓的主包&#…

思码逸 信通院联合发布:《研发效能基准报告》解读

页数&#xff1a;47页 免费下载&#xff1a;https://fs80.cn/go7exq 本文共计6000字&#xff0c;建议阅读时间&#xff1a;5-10分钟。 阅读本文你将收获&#xff1a; 研发效能基准指标 研发效能基准指标数据&#xff1a;交付速率、交付质量、交付能力 研发效能综合分析&…