首页 > 其他分享 >124.【C语言】数据结构之快速排序的小区间优化和非递归的解决方法

124.【C语言】数据结构之快速排序的小区间优化和非递归的解决方法

时间:2025-01-10 22:03:02浏览次数:3  
标签:arr 数据结构 递归 int C语言 right 124 排序 left

目录

1.小区间优化

测试代码

运行结果

2.非递归的解决方法(重要!)

递归产生的问题

一般来说,递归改非递归有两种方法

算法分析

递归产生的二叉树

栈的示意图

先写代码框架

再填写细节部分


1.小区间优化

回顾121.【C语言】数据结构之快速排序(未优化的Hoare排序存在的问题)以及时间复杂度的分析文章的一般情况下快排的时间复杂度

类似二叉树, 下面分析二叉树的特点

注意到二叉树的最后一层节点个数为2^{n-1}近似占总节点个数2^n-1的一半(有关二叉树计算方面的知识参见100.【C语言】数据结构之二叉树的基本知识文章),因此越往二叉树的下方,递归的次数越多, 一般情况下快排递归近似二叉树,如果能适当减少递归的次数,可以提高效率

解决掉最后一层能减少一半递归次数,解决掉最后三层能减少\frac{1}{2}+ \frac{1}{4}+ \frac{1}{8} =87.5\%的递归次数!!

可以想到一个方法:越往二叉树的下方,子数组的元素个数越少,不用递归,用其他的排序方法

要注意到递归到小区间的特点,数组的大部分元素是有序的(即局部有序),选择合适的排序方法要利用好这一特点

目前讲过的排序方法有:

1.冒泡排序:118.【C语言】数据结构之排序(堆排序和冒泡排序) 点我跳转

2.选择排序:117.【C语言】数据结构之排序(选择排序) 点我跳转

3.直接插入排序:112.【C语言】数据结构之排序(详解插入排序) 点我跳转

4.希尔排序:115.【C语言】数据结构之排序(希尔排序) 点我跳转

5.堆排序:118.【C语言】数据结构之排序(堆排序和冒泡排序) 点我跳转

下面讲如何选择:

1.首先排除堆排序,堆排序要先建堆,耽误时间

2.再排除希尔排序,希尔排序是对直接插入排序的优化,由于数组的元素个数较少,没有必要使用希尔排序

3.再排除选择排序:无论最好还是最坏情况,时间复杂度都为O(N^2),而且当是小区间排序时数组的大部分元素是有序的,没有商量的余地,时间复杂度还是O(N^2)

4.冒泡排序 VS 直接插入排序

冒泡排序,最坏情况时间复杂度为O(N^2),最好情况(有序)时间复杂度为O(N)

直接插入排序,最坏情况时间复杂度为O(N^2),最好情况(有序)时间复杂度为O(N)

从时间复杂度上比较,看起来两者没有区别,但是要依据数组的实际情况来看这个问题!

从小区间的特点(数组的大部分元素是有序的(即局部有序:小段小段有序))上来看,选直接插入排序较好,原因:插入排序利用了局部有序这一特点,但是冒泡排序只是机械地将数组中最大、次大......的数依次移动,没有利用这一特点,所以直接插入排序较好

当然也可以稍加改造116.【C语言】测试排序性能的模板代码文章的测试性能的代码来比较冒泡排序和直接插入排序

例如区间长度大于10使用Hoare排序,区间长度小于等于10使用冒泡排序和直接插入排序

测试代码

void QuickSort_Hoare(int* arr, int left, int right)
{
	if (left >= right)
		return;
	if ((right - left + 1) <= 10)//添加这行代码
		return;
	//单趟快速排序
	int begin = left;
	int end = right;
	//随机选key
	srand((unsigned int)time(0));
	int rand_i = left+rand() % (right - left);
	Swap(&arr[left], &arr[rand_i]);
	
	int ret = GetMiddleNum(left, right, arr);
	if (left != ret)
	{
		Swap(&arr[left], &arr[ret]);
	}
	int key_i = left;
	while (left < right)
	{
		//由于key_i==left,因此right指针先走
		//右边找小
		while (left < right && arr[right] >= arr[key_i])
		{
			right--;
		}

		//左边找大
		while (left < right && arr[left] <= arr[key_i])
		{
			left++;
		}

		Swap(&arr[left], &arr[right]);
	}
	Swap(&arr[key_i], &arr[left]);
	key_i = left;//arr[key_i]和arr[left]交换后下标要改变,否则会对下次递归产生不利结果

	QuickSort_Hoare(arr, begin, key_i - 1);
	QuickSort_Hoare(arr, key_i + 1,end);
}

void TestTime()
{
	const int N = 500000;
	int* a1 = (int*)malloc(sizeof(int) * N);
	if (a1 == NULL)
	{
		perror("malloc");
		return;
	}
	int* a2 = (int*)malloc(sizeof(int) * N);
	if (a2 == NULL)
	{
		perror("malloc");
		return;
	}

	for (int i = 0; i < N; i++)
	{
		a1[i] = rand();
	}
	QuickSort_Hoare(a1, 0, N - 1);
	for (int i = 0; i < N; i++)
	{
		a2[i] = a1[i];
	}
	printf("区间长度大于10的排序完毕\n");
	clock_t begin1 = clock();
	BubbleSort(a1, N);//BubbleSort函数代码省略
	clock_t end1 = clock();
	printf("BubbleSort's time=%ldms\n", end1 - begin1);
	clock_t begin2 = clock();
	InsertSort(a2, N);//InsertSort函数代码省略
	clock_t end2 = clock();
	printf("InsertSort's time=%ldms\n", end2 - begin2);

	free(a1);
	free(a2);
}

int main()
{
	TestTime();
	return 0;
}

运行结果

从运行的时间上看,插入排序占很大的优势!

则Hoare排序代码应该修改为

void QuickSort_Hoare(int* arr, int left, int right)
{
	if (left >= right)
		return;
    //小区间直接插入排序
	if ((right - left + 1) > 10)
	{
		//单趟快速排序
		int begin = left;
		int end = right;
		//随机选key
		srand((unsigned int)time(0));
		int rand_i = left + rand() % (right - left);
		Swap(&arr[left], &arr[rand_i]);

		int ret = GetMiddleNum(left, right, arr);
		if (left != ret)
		{
			Swap(&arr[left], &arr[ret]);
		}
		int key_i = left;
		while (left < right)
		{
			//由于key_i==left,因此right指针先走
			//右边找小
			while (left < right && arr[right] >= arr[key_i])
			{
				right--;
			}

			//左边找大
			while (left < right && arr[left] <= arr[key_i])
			{
				left++;
			}

			Swap(&arr[left], &arr[right]);
		}
		Swap(&arr[key_i], &arr[left]);
		key_i = left;//arr[key_i]和arr[left]交换后下标要改变,否则会对下次递归产生不利结果

		QuickSort_Hoare(arr, begin, key_i - 1);
		QuickSort_Hoare(arr, key_i + 1, end);
	}
	else
	{
		InsertSort(arr+left, right - left + 1);
	}
}

注意InsertSort(arr+left, right - left + 1);的写法!!

长度小于等于10的区间不一定都在数组的两端,可能在中间,因此需要提供该区间第一个元素的下标arr+left,一共(right-left+1)各元素,由于是闭区间注意一定要+1!

2.非递归的解决方法(重要!)

递归产生的问题

1.效率(这个影响较小)

2.深度太深,栈会溢出!(严重的问题)

一般来说,递归改非递归有两种方法

1.改循环,可以参见L25.【LeetCode笔记】 三步问题的四种解法(含矩阵精彩解法!)文章和35.【C语言】详解函数递归

2.使用辅助改成循环(★)

快速排序算法复杂,改非递归需要用到栈

回顾有关栈的一系列操作

参见99.【C语言】数据结构之栈(含栈的源码)文章

算法分析

递归产生的二叉树

先看递归产生的二叉树的一个图例,用某个算法取出key_i的值

可以看到:快速排序中递归的本质是:区间在变化! 显然可以用栈来存区间

栈的示意图

将上方二叉树图转化为栈图

可以看到类似二叉树的前序遍历:先对该区间快速排序, 相当于访问根,再左区间快速排序,相当于访问左子树,最后访问右区间

(回顾前序遍历知识点参见106.【C语言】数据结构之二叉树的三种递归遍历方式文章),因此将99.【C语言】数据结构之栈文章的栈的源码嵌入快速排序中即可

先写代码框架

栈的初始化和销毁

void QuickSort_Hoare_Use_Stack(int* arr, int left, int right)//非递归,使用栈辅助改循环
{
	ST st;
	STInit(&st);
	//do_something
	STDestory(&st);
}

再填写细节部分

注意:区间的边界值的入栈顺序和出栈顺序时是有讲究的!

如果先对区间的边界入栈,再对区间的边界入栈,那么出栈时,第一次出栈为边界.第二次出栈为边界(顺序是反着的!)

同理如果先对区间的边界入栈,再对区间边界入栈,那么出栈时,第一次出栈为边界.第二次出栈为边界

循环条件:只要栈不为空就继续快速排序

注意:

1.栈里取一段区间,单趟排序

2.单趟分割子区间(左区间和右区间)入栈

3.子区间只有一个值或不存在就不入栈

4.取区间等同于出栈

void QuickSort_Hoare_Use_Stack(int* arr, 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 left = begin;
		int right = end;

		//随机选key
		srand((unsigned int)time(0));
		int rand_i = left + rand() % (right - left);
		Swap(&arr[left], &arr[rand_i]);
		int key_i = left;
		//Hoare排序
		while (left < right)
		{
			//由于key_i==left,因此right指针先走
			//右边找小
			while (left < right && arr[right] >= arr[key_i])
			{
				right--;
			}

			//左边找大
			while (left < right && arr[left] <= arr[key_i])
			{
				left++;
			}

			Swap(&arr[left], &arr[right]);
		}
		Swap(&arr[key_i], &arr[left]);
		key_i = left;
		//左区间 [begin,key_i-1],key_i,右区间[key_i+1,end]
        //要想出栈时先对左区间排序,后对右区间排序,那么右区间先入栈,左区间后入栈
		if (key_i + 1 < end)
		{
			STPush(&st, end);
			STPush(&st, key_i + 1);
		}
		if (begin < key_i - 1)
		{
			STPush(&st, key_i - 1);
			STPush(&st, begin);
		}
	}
	STDestory(&st);
}

注意出栈的写法:先保存栈顶元素后将其pop,这一点和汇编指令的pop ax的做法是一样的

pop ax执行过程:

1.将SS:SP指向的内存单元处的数据送入寄存器AX中

2.SP+=2,SS:SP指向当前栈顶下面的单元,以当前栈顶下面的单元为新的栈顶

标签:arr,数据结构,递归,int,C语言,right,124,排序,left
From: https://blog.csdn.net/2401_85828611/article/details/144946480

相关文章

  • 冒险数据结构:峰谷序列(动态序列查找问题)
    先考虑这么一个问题:    如何求出一个序列在所有位置上的各个元素的前面和后面第一个比它小的元素位置。显然这个问题可以用单调栈来解决。        如上图所示,维护一个单调递增的序列,每当栈顶>当前元素时,就抛出栈顶,这时就找到了栈顶元素后面第一个小于它的......
  • C语言分支和循环(上)
    分⽀和循环分⽀和循环(上)1.if语句1.1if1.2else2.关系操作符3.条件操作符4.逻辑操作符:&&,||,!5.switch语句分⽀和循环(上)C语⾔是结构化的程序设计语⾔,这⾥的结构指的是顺序结构、选择结构、循环结构,C语⾔是能够实现这三种结构的,其实我们如果仔细分析,我们⽇......
  • C++并发编程之基于锁的数据结构的适用场合与需要考虑和注意的问题
    在C++多线程编程中,锁是一种常用的同步机制,用于保护共享数据,防止多个线程同时访问和修改,从而避免数据不一致或其他并发问题。基于锁的数据结构适用于多种并发编程场合,但同时也需要注意一些关键问题。1. 适用的并发编程场合锁在以下几种场合特别有用:1.1 保护共享数据当多个......
  • C/C++ 数据结构与算法【排序】 常见7大排序详细解析【日常学习,考研必备】带图+详细代
    常见7种排序算法冒泡排序(BubbleSort)选择排序(SelectionSort)插入排序(InsertionSort)希尔排序(ShellSort)归并排序(MergeSort)快速排序(QuickSort)堆排序(HeapSort)计数排序(CountingSort)算法复杂度1、冒泡排序冒泡排序是一种简单的排序算法,它重复地遍历要排序的数列,一次比......
  • 数据结构——单链表(C语言版:超详细)
    目录一、引言1.数据结构的重要性2.单链表在其中的地位二、什么是单链表1.单链表的定义2.基本概念解释三、单链表的结构特点1.与数组对比的优势2.存在的劣势四、单链表的基本操作1.节点的创建2.动态申请一个节点3.插入节点3.1尾插3.2头插3.3在pos之前插入3.4在......
  • 数据结构实验8
    7-2迪杰斯特拉方法实现最短路径用迪杰斯特拉算法实现有向网的最短路径输入格式:第一行输入有向网的顶点和边数,第二行输入各顶点值,用空格间隔,第三行开始输入各条边的两个点的及边上的权值,用空格间隔。最后一行输入要求路径的两个顶点。输出格式:输出最短路径经过的各顶点,中......
  • 数据结构实验9
    7-1整型关键字的散列映射给定一系列整型关键字和素数p,用除留余数法定义的散列函数H(key)=key%p将关键字映射到长度为p的散列表中。用线性探测法解决冲突。输入格式:输入第一行首先给出两个正整数n(≤1000)和p(≥n的最小素数),分别为待插入的关键字总数、以及散列表的长度。......
  • 数据结构实验10
    6-4快速排序本题要求实现快速排序的一趟划分函数,待排序列的长度1<=n<=1000。函数接口定义:intPartition(SqListL,intlow,inthigh);其中L是待排序表,使排序后的数据从小到大排列。类型定义:typedefintKeyType;typedefstruct{KeyTypeelem;/elem[0]一般作哨......
  • 数据结构实验1
    7-1线性表A,B顺序存储合并有两张非递增有序的线性表A,B,采用顺序存储结构,两张表合并用c表存,要求C为非递减有序的,然后删除C表中值相同的多余元素。元素类型为整型输入格式:第一行输入输入表A的各个元素,以-1结束,中间用空格分隔;第二行输入表B的各个元素,以-1结束,中间用空格分隔。输......
  • 数据结构实验二
    石家庄铁道大学实验报告课程名称:信2305-3 任课教师:刘丹 实验日期:2024.12.11班级:信2305-3 姓名:徐戌 学号:20234316实验项目名称:实验二一、 实验目的1.掌握栈的定义及......