C语言选择排序和快速排序(图解过程)+思路清晰

news2024/12/24 2:53:57

选择排序和快排

  • 选择排序
    • 时间复杂度和空间复杂度
  • 快排(三种方式)
    • 1.hoar
      • 时间复杂度和空间复杂度
      • 优化--三数取中
      • 优化--小区间优化
    • 2.挖坑法
    • 3.双指针(推荐)

选择排序

本篇文章的重点在快排。因为选择排序无论是在思想上面还是,代码上面,都是比较容易理解的,所以,就不说的那么详细了。

选择排序的思想(默认的实现升序)
每一次遍历数组,选取最大的(或者是最小的)与数组的最后一个数据(或者第一个数据)进行交换。

在上面的思想上进行一定的优化
优化:每一次遍历数组时,同时选取最大的和最小的,分别与数组的最后一个数据和第一个数据进行交换。

下面实现的是优化的方案(第一种太简单了,而且没什么坑,因此不写了)

#include <stdio.h>

void Swap(int* x, int* y)
{
  int tmp = *x;
  *x = *y;
  *y = tmp;
}

void Select(int* a, int n)
{
  int left = 0;
  int right = n-1;

  while(left < right)
  {
  //先默认选取数组中的第一个数据作为最大值和最小值
    int max = left;
    int min = left;
    int i = 0;
    //遍历数组选取本次遍历的最大值和最小值
    for(i = left+1; i <= right; ++i)
    {
        if(a[i] > a[max])
          max = i;
        if(a[i] < a[min])
          min = i;
    }
	//让最小值排在前面
    Swap(&a[left], &a[min]);
    if(max == left)
      max = min;

	//最大值排在后面
    Swap(&a[right], &a[max]);

    left++;
    right--;
  }
}

void Test()
{
  int arr[] = {3,2,1,-1,4,5,6,10,9,8,7};
  int sz = sizeof(arr) / sizeof(arr[0]);

  Select(arr, sz);

  int i;
  for(i = 0; i < sz; ++i)
  {
    printf("%d ", arr[i]);
  }
  printf("\n");
}

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

对于代码中,在交换完 Swap(&a[left], &a[min]); 后
为什么会增加一个if()判断,下面来解释一下。

假设我们的数组起始数据为arr[] = {6,4,3,1,2,0,3};
那么在第一次选取最大和最小值的时候,max = 0 , min = 5;

在进行完Swap(&a[left], &a[min])之后,max原本是指向最大值6的位置0,但是此刻的位置0上的数字已经变成了最小值0,而6来到了min的位置,所以当max的位置和left重复时,我们执行max = min 让max重新指向最大的值。

时间复杂度和空间复杂度

选择排序每一次都需要遍历一次数组来确定最大值或最小值,一共需要遍历n次
所以时间复杂度为O(n^2)
空间复杂度为O(1)

快排(三种方式)

快排在最早的时候是由霍尔提出的。
霍尔 (Sir Charles Antony Richard Hoare) 是一位英国计算机科学家,他是著名的快速排序 (QuickSort) 的发明者。在平均状况下,**排序 n 个项目要Ο(n log n) 次比较,而且通常明显比其他Ο(n log n) 演算法更快。**所以它是一个被广泛使用的算法。在一次采访中。

在一开始的时候,快排的实现方式只有一种,就是霍尔大佬提出的思路,后面还有两种挖坑发和双指针法,可以是说对第一种思路的优化吧,所以第一种的实现思路也是这三种方法中最复杂的一个。

1.hoar

废话不多说,下面我们直接先给出思路的文字解释,然后再结合画图的方式,帮助大家更好的理解快排的基本实现思路。

hora思路解释(文字)
我们假设以数组arr[] = {3,-1,1,2,4,5,8,7,6,9,-3}为例(以排升序为例)。
要理解快排的基本思路,要有一个前提,知道key,left,right。
首先要先选取一个数作为key,这个数字通常一开始是数组中的第一个数字,也就是3。
然后再定义两个指针,left和right,如下图:
在这里插入图片描述

接下来我们就开始正式去按照规则走一遍:
1.right要先走,找比key要小的(不包含等于key),-3就比3小,所以right就不用动。
2.left再走,找比key要大的,那么不断的加加到4的位置,如下图:
在这里插入图片描述

等right和left都找到了对应的位置,那么交换left和right。即下图:
在这里插入图片描述

然后还是right先走,还是找比key小的,然后left再走,找比key大的。如下图:

在这里插入图片描述
然后再交换left和right,即下图:
在这里插入图片描述
然后还是right先走,找比key小的
但是注意当right <= left时,本次查找就结束。
然后再交换left和key。
如下图:
在这里插入图片描述
这称为一次查找结束,当一次查找结束后,我们可以发现几点

  1. key来到了正确的位置
  2. key的左边都是比key小的值
  3. 右边都是比key大的值

此时,精华来了!!
我们可以以key为中点,划分三个区间,如下图:
在这里插入图片描述
我们再把左区间和右区间按照上述的步骤再去走,直到全部有序了,就结束,可以把这个过程看作成一个二叉树的前序遍历!
图解:
在这里插入图片描述
不难看出这是一个递归调用的过程,递归结束的条件就是非法区间。

代码:

void Swap(int* x, int* y)
{
  int tmp = *x;
  *x = *y;
  *y = tmp;
}

void quick(int* a, int begin, int end)
{
//递归的结束条件
  if(begin >= end)
    return;

  //选key
  int key = begin;
  int left = begin;
  int right = end;

  while(left < right)
  {
    //右边先走,找比key小的
    while(right > left && a[right] >= a[key])
    {
      right--;
    }

    //左边的再走,找比key大的
    while(right > left && a[left] <= a[key])
    {
      left++;
    }
    //大的数据向后走,小的向前来
    Swap(&a[left], &a[right]);
  }
  //使得key来到了正确的位置,并且以key划分了区间[left, key-1] key [key+1, end]
  Swap(&a[key], &a[left]);
  
  //递归--前序遍历
  quick(a, begin, key-1);
  quick(a, key+1, end);
}

void Test()
{
  int arr[] = {3,-1,1,2,4,5,8,7,6,9,-3};
  int sz = sizeof(arr) / sizeof(arr[0]);

  quick(arr, 0, sz-1);

  int i = 0;
  for(; i < sz; ++i)
  {
    printf("%d ", arr[i]);
  }
  printf("\n");
}
int main()
{
  Test();
  return 0;
}

值得一提的是,在left和right寻找对应的值的时候,我们需要添加一个结束的条件即left < right。
假设我们的数据是3,4,7,8,9,6,6,10.
我们的left < right就是防止这种极端的数据出现(没有比key大的或没有比key小的),防止我们在访问数组时,出现数组越界的问题。

时间复杂度和空间复杂度

在一开始我们就提出了快排的时间复杂度是O(NlogN),为什么是O(n)我想大家能理解,每一次查找都是需要,遍历完区间的。
至于问什么有logn,是递归调用。这一点,大家学过堆排的话应该都知道,如果不清楚的话可以看一下我写的堆排的文章,里面有分析关于这块的时间复杂度。
https://blog.csdn.net/Javaxaiobai/article/details/128016533?spm=1001.2014.3001.5502

优化–三数取中

首先声明一下,上述就是快排完整的第一种实现思路和方式,下面的三数取中和小区间优化,是人们在使用时,对特殊数据的优化处理,特别是三数取中。

三数取中就是解决顺序或逆序的极端情况(递归调用的太多,可能会导致Stack Overflow)。

假设我们在处理的数据是有序的。(1,2,3,4,5,6,7,8,9,10…)
那么这就会导致left并不会移动,每一次划分的区间实际上是无效的(即交换left和key没有实际达到划分区间的目的),那么在递归调用的时候,就是造成了下图的情况。
在这里插入图片描述
递归的时间复杂度就是O(n)不再是O(logn)。
那么整体的时间复杂度就是O(n^2)!!!!

三数取中说白了就是,在三个数中,选取中间值。三个数分别对应了数组的第一个数,数组的中间值,数组的最后一个数字。之后再将第一个数字和选取的中间值进行交换。

三数取中的逻辑并不难理解,直接给出代码,就是在三个数中选取中间值。

int GetMin(int* a, int begin, int end)
{
	int min = (begin + end) / 2;

	if (a[begin] < a[min])
	{
		if (a[min] < a[end])
		{
			return min;
		}
		else if(a[begin] > a[end])
		{
			return begin;
		}
		else
		{
			return end;
		}
	}
	else //a[begin] > a[min]
	{
		if (a[min] > a[end])
		{
			return min;
		}
		else if (a[end] > a[begin])
		{
			return begin;
		}
		else
		{
			return end;
		}
	}
}

优化–小区间优化

小区间优化–最后的小区间不再继续的递归下去,而是使用插入排序进行(优化空间 %75左右)。

因为递归调用是需要在内存中进行压栈的,当时数据量不太大的时候就没必要使用递归了,直接使用快排,对剩下的数据做排序。

以下就是添加了三数取中和小区间优化的最终代码

int GetMin(int* a, int begin, int end)
{
	int min = (begin + end) / 2;

	if (a[begin] < a[min])
	{
		if (a[min] < a[end])
		{
			return min;
		}
		else if(a[begin] > a[end])
		{
			return begin;
		}
		else
		{
			return end;
		}
	}
	else //a[begin] > a[min]
	{
		if (a[min] > a[end])
		{
			return min;
		}
		else if (a[end] > a[begin])
		{
			return begin;
		}
		else
		{
			return end;
		}
	}
}
void Swap(int* x, int* y)
{
  int tmp = *x;
  *x = *y;
  *y = tmp;
}

void quick(int* a, int begin, int end)
{
//递归的结束条件
  if(begin >= end)
    return;

  int min = GetMin(a, begin, end);
  Swap(&a[min], &a[begin]);
  int key = begin;
  int left = begin;
  int right = end;
  if (end - begin + 1 < 15)
  {
	InsertSort(a + begin, end - begin + 1);//闭区间+1才是数据的个数
  }
  else
  {
	   while(left < right)
	   {
	    //右边先走,找比key小的
	    while(right > left && a[right] >= a[key])
	    {
	      right--;
	    }
	
	    //左边的再走,找比key大的
	    while(right > left && a[left] <= a[key])
	    {
	      left++;
	    }
	    //大的数据向后走,小的向前来
	    Swap(&a[left], &a[right]);
	  }
	  //使得key来到了正确的位置,并且以key划分了区间[left, key-1] key [key+1, end]
	  Swap(&a[key], &a[left]);
	  
	  //递归--前序遍历
	  quick(a, begin, key-1);
	  quick(a, key+1, end);
  }
 
}

void Test()
{
  int arr[] = {3,-1,1,2,4,5,8,7,6,9,-3};
  int sz = sizeof(arr) / sizeof(arr[0]);

  quick(arr, 0, sz-1);

  int i = 0;
  for(; i < sz; ++i)
  {
    printf("%d ", arr[i]);
  }
  printf("\n");
}
int main()
{
  Test();
  return 0;
}

接下来我们介绍第二种方法–挖坑法

2.挖坑法

挖坑法其实本质上和第一种实现的思路和方式都一样,只不过需要新增一个变量hole。

实现思路
我们还是以上一组数据arr[] = {3,-1,1,2,4,5,8,7,6,9,-3}为例。
前提仍然是需要先选取key和left和right,如下图:
在这里插入图片描述
不过我们需要在left的位置新增一个hole变量,它的作用就是取代了,交换left和right。不理解没关系,下面跟着走一遍就能清楚了。
另外一点是,key不再是记录的是位置,而是值!!

还是老规矩,让我们的right先走,找比key要小的。
再之后将right位置上的值赋值给hole位置上去,因为我们使用key记录下的是该位置的值,所以不用害怕改值丢失。

赋值完成后,再将hole移动到right的位置,为下一次left的值做填充准备!
如下图:
在这里插入图片描述
然后就轮到我们的left再走了,依然是找比key大的值,找到了之后将该值填入到hole的位置上去。
如下图所示:

在这里插入图片描述

然后还是再移动hole的位置到left:为下一次right的值做填充准备!
在这里插入图片描述

中间重复的过程我就不再继续的画下去了,我们来看最后结尾处:
在这里插入图片描述

此时left和right相遇了,再将hole移动到left的位置,循环结束,接下来要做的最后一步就是将key赋值给hole位置上!
如下图:
在这里插入图片描述
此时我们发现,hole左边的全是比hole小的值,右边的全部是比key大的值。
接下来的过程就和第一次的不步骤一样了,划分区间和递归的去走下去。递归结束的条件还是一样的。

代码:

void quick1(int*a , int begin, int end)
{
  if(begin >= end) return;

  //挖坑法
  int left = begin, right = end;
  int key = a[left];
  int hole = left;

  while(left < right)
  {
    //还是right先走,找比key小的
    while(left < right && a[right] >= key)
    {
      right--;
    }
    a[hole] = a[right];
    hole = right;
    //left再走,找比key大的
    while(left < right && a[left] <= key)
    {
      left++;
    }
    a[hole] = a[left];
    hole = left;
  }
  a[hole] = key; 
  quick1(a, begin, hole-1);
  quick1(a, hole+1, end);
}

关于三数取中和小区间优化也是和上面的一样,我在代码上面就不再重复的展示了。

3.双指针(推荐)

下面就是最后一种—双指针法。
双指针的代码较少,但是非常的精华,也是我个人比较喜欢的一种方式。

还是以上面的数据arr[] = {3,-1,1,2,4,5,8,7,6,9,-3}为例
前提还是要给给大家说一下的,我们定义两个指针,prev和cur,开始的时候,两个指针都是指向第一个数据的
,同样我们还是选取第一个数据作为我们的key。
在这里插入图片描述

接下来我们再来谈思路(升序)
首先cur指针先走一步,找比key小的数字

  1. 如果找到了,先再让prev走一步,此时如果prev和cur的位置不重复,就交换cur和prev的数据,否则cur 继续向前走一步,prev不动。
  2. 如果没找到,cur继续向前走,prev不动。
    根据上面给出的数据,按照以上的规则进行走,过程如下:
    在这里插入图片描述
    这一趟的循环结束之后,我们再交换,key和prev的值,结果如下:
    在这里插入图片描述
    此时左边全是比key小的值,右边全是比key大的值,此时我们再以prev为中间值划分左右区间。
    在这里插入图片描述
    之后便是和前两种方式一样的去递归调用就好了。

三者的思路总体上来说,差别并不是很大,虽然有个别的差异,但是作为我们学习者来说益处还是很大的。
代码如下:

void quick2(int* a, int begin, int end)
{
  if(begin >= end) return;

  int key = begin;
  int prev = begin;
  int cur = begin;

  while(cur <= end)
  {
    while(a[cur] < a[key] && ++prev != cur)
    {
      //满足条件,进行交换
      Swap(&a[prev], &a[cur]);
    }
    cur++;
  }

  //每一次结束的最后一次交换
  Swap(&a[key], &a[prev]);

  quick2(a, begin, prev-1);
  quick2(a, prev+1, end);
}

void Test()
{
  int arr[] = {3,-1,1,2,4,5,8,7,6,9,-3};
  int sz = sizeof(arr) / sizeof(arr[0]);

  quick2(arr, 0, sz-1);
  
  int i = 0;
  for(; i < sz; ++i)
  {
    printf("%d ", arr[i]);
  }
  printf("\n");
}

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

以上就是全部内容了,感谢支持!

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

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

相关文章

提供数百万岗位和丰厚利润,苹果却转移产业链,中国制造怎么办?

新年刚过&#xff0c;就传出消息指苹果直接代中国供应链企业向印度提出建厂申请&#xff0c;其中有14家获得了许可&#xff0c;而3家被否决&#xff0c;这凸显出苹果坚定向印度转移生产线&#xff0c;如此做对中国制造将产生深远影响。一、苹果对中国制造的影响巨大苹果为中国提…

新的一年,这份高级测试人的职业素养请收好~

软件测试工程师需要的专业技能计算机领域的专业技能是测试工程师应该必备的一项素质&#xff0c;是做好测试工作的前提条件。尽管没有任何IT背景的人也可以从事测试工作&#xff0c;但是一名要想获得更大发展空间或者持久竞争力的测试工程师&#xff0c;则计算机专业技能是必不…

5G R16+C-V2X赋能下一代智能T-Box,助力智能驾驶时代加速到来

█ 5G技术助力C-V2X持续进化&#xff0c;智能网联新生态逐步建立 汽车行业正面临百年未有之变局&#xff0c;智能汽车已经成为全球汽车产业发展的战略方向。发改委、工信部、交通部等11部委联合印发的《智能汽车创新发展战略》中指出&#xff1a;汽车产业与相关产业全面融合&a…

(二十四)深入理解蓝牙BLE之“H5协议”

前言&#xff1a;蓝牙产品在实际落地中&#xff0c;很多时候采用hostcontroller的通信模型&#xff0c;其中host负责实现协议栈profile是运行在主控cpu上的。controller为另外一颗单独的蓝牙芯片&#xff0c;负责蓝牙link layer的处理&#xff0c;两个芯片通过hci消息来交互数据…

SD卡提示格式化后怎么办?可尝试这种数据恢复方法快速找回!

生活中&#xff0c;虽然我们更习惯用手机、U盘来存储数据&#xff0c;但是对于摄影爱好者&#xff0c;SD卡还是非常刚需。 在使用SD卡存储文件时&#xff0c;经常遇到SD卡无法读取&#xff0c;要求我们格式化后才可以使用。此时&#xff0c;该如何备份里面的数据&#xff0c;或…

IB课程为何号称全球最难国际课程?

在读国际学校的同学们&#xff0c;一定对大名鼎鼎的IB课程不陌生&#xff0c;可是他为什么被称作是它号称最难的国际课程呢&#xff1f;今天就来给大家全面解析一下IB课程&#xff5e; IB课程最开始是IBO为外交官子女开设全球统一标准的课程。IB课程为全球学生开设从幼儿园到大…

【论文速递】TMM2023 - FECANet:用特征增强的上下文感知网络增强小样本语义分割

【论文速递】TMM2023 - FECANet:用特征增强的上下文感知网络增强小样本语义分割 【论文原文】&#xff1a;FECANet: Boosting Few-Shot Semantic Segmentation with Feature-Enhanced Context-Aware Network 获取地址&#xff1a;https://ieeexplore.ieee.org/stamp/stamp.js…

这款小巧精致的 Keychron K7 满足了我对键盘的所有想象

&#x1f53d; 前言 博主是一个“练习”时长两年半的前端码农&#xff0c;在练习期间打交道最多的就是键盘&#xff0c;敲得多了懂得也就多了对键盘的要求也变多了。 之前认为&#xff0c;不就一块键盘嘛&#xff0c;能打字就行。 现在认为&#xff0c;键盘必须要有好的手感&a…

Vue前端基于模板实现word导出功能

目录一、依赖二、模板三、代码一、依赖 // 核心依赖 cnpm i docxtemplater3.32.5 cnpm i file-saver2.0.5 cnpm i jszip-utils0.1.0 cnpm i pizzip3.1.3// ui cnpm i element-ui2.15.8二、模板 public 下新建 test.docx 三、代码 <template><div><el-form …

Maven依赖冲突

An attempt was made to call a method that does not exist 依赖冲突完整报错如下 Description:An attempt was made to call a method that does not exist. The attempt was made from the following location:com.baomidou.mybatisplus.extension.plugins.inner.Paginati…

Springboot抑郁症测试系统的设计与实现

在各大医院的教学过程中&#xff0c;用户的抑郁症测试是一项非常重要的事情。随着计算机多媒体技术的发展和网络的普及&#xff0c;“基于网络的学习模式”正悄无声息的改变着传统的抑郁症测试系统&#xff0c;“在线视频、案例展示”的研究和设计也成为教育技术领域的热点课题…

Spring-IOC相关内容

Spring-IOC相关内容 4&#xff0c;IOC相关内容 4.1 bean基础配置 对于bean的配置中&#xff0c;主要会讲解bean基础配置,bean的别名配置,bean的作用范围配置(重点),这三部分内容&#xff1a; 4.1.1 bean基础配置(id与class) 对于bean的基础配置&#xff0c;在前面的案例中…

Chapter2:ROS基础

ROS1{\rm ROS1}ROS1的基础及应用&#xff0c;基于古月的课&#xff0c;各位可以去看&#xff0c;基于hawkbot{\rm hawkbot}hawkbot机器人进行实际操作。 ROS{\rm ROS}ROS版本&#xff1a;ROS1{\rm ROS1}ROS1的Melodic{\rm Melodic}Melodic&#xff1b;实际机器人&#xff1a;Ha…

Magisk内部实现原理

Android10以后&#xff0c;Android系统限制了System分区的修改&#xff0c;结果就是&#xff0c;即使你i是自己编译的Android系统&#xff0c;即使是有做高的root权限&#xff0c;你依然无法挂载System分区并对其内容进行修改,尽管网上有各种帖子说可以使用mount -o rw,remount…

SpringBoot+Vue项目企业客户管理系统

文末获取源码 开发语言&#xff1a;Java 框架&#xff1a;springboot JDK版本&#xff1a;JDK1.8 服务器&#xff1a;tomcat7 数据库&#xff1a;mysql 5.7/8.0 数据库工具&#xff1a;Navicat11 开发软件&#xff1a;eclipse/myeclipse/idea Maven包&#xff1a;Maven3.3.9 浏…

Linux(centos7)基本操作---文件管理和用户管理

文件管理 文件的目录结构 文件的目录结构从根&#xff08;/&#xff09;目录开始&#xff0c;主要由一下几个目录组成&#xff0c;之间的作用也是不同的&#xff0c;具体作用如下&#xff1a; bin目录&#xff1a;主要存放系统中的一些基本的有执行权限&#xff08;x&#…

动态AOP 自动以标签 源码解析

Spring AOP 是一个简化版的AOP 实现&#xff0c;并没有提供完整的AOP功能&#xff0c;通常情况下&#xff0c;Spring AOP 是能够满足我们日常开发过程中的大多数场景的&#xff0c;但在某些情况下&#xff0c; 我们可能需要使用Spring AOP 范围外的某些AOP 功能。 AspectJ是一…

【JAVA程序设计】(C00101)基于Servlet的在线鞋店销售管理系统

基于Servlet的在线鞋店销售管理系统项目简介项目获取开发环境项目技术运行截图项目简介 本项目是基于J2EE的servlet的在线鞋店销售管理系统&#xff0c;网上鞋店&#xff0c;球鞋&#xff0c;篮球鞋&#xff0c;跑步鞋&#xff0c;本项目有三种权限&#xff1a;游客、用户、管…

LeetCode 1480. 一维数组的动态和

有人相爱 有人深夜看海 有人LeetCode第一题都做不出来 小趴菜就是我 女神镇楼压压惊 文章目录LeetCode 1480. 一维数组的动态和题目描述&#xff1a;示例1&#xff1a;示例2&#xff1a;示例3&#xff1a;提示&#xff1a;解题思路题解结果展示大神题解执行消耗内存为 0 kb&am…

轻量级 K8S 环境、本地 K8S 环境Minikube,一键使用 (史上最全)

文章很长&#xff0c;而且持续更新&#xff0c;建议收藏起来&#xff0c;慢慢读&#xff01;疯狂创客圈总目录 博客园版 为您奉上珍贵的学习资源 &#xff1a; 免费赠送 :《尼恩Java面试宝典》 持续更新 史上最全 面试必备 2000页 面试必备 大厂必备 涨薪必备 免费赠送 经典…