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

选择排序

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

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

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

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

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

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

  • 3
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
好的,我来为您介绍一下 C 语言中的快速排序和直接选择排序快速排序是一种常用的排序算法,其基本思想是采用分治策略,将待排序序列分成两个子序列,其中一个子序列的所有元素都小于另一个子序列的所有元素,然后再对这两个子序列分别进行快速排序,以此类推,直到整个序列有序为止。快速排序的时间复杂度为 O(nlogn),是一种比较高效的排序算法。 以下是 C 语言中实现快速排序的代码: ```c void quick_sort(int arr[], int left, int right) { if (left < right) { int i = left, j = right, x = arr[left]; while (i < j) { while (i < j && arr[j] >= x) j--; if (i < j) arr[i++] = arr[j]; while (i < j && arr[i] < x) i++; if (i < j) arr[j--] = arr[i]; } arr[i] = x; quick_sort(arr, left, i - 1); quick_sort(arr, i + 1, right); } } ``` 直接选择排序是一种简单的排序算法,其基本思想是每次找到最小的元素并将其放到序列的起始位置,然后再在剩余的元素中找到最小的元素并将其放到已排序部分的末尾,以此类推,直到整个序列有序为止。直接选择排序的时间复杂度为 O(n^2),是一种比较低效的排序算法。 以下是 C 语言中实现直接选择排序的代码: ```c void select_sort(int arr[], int len) { int i, j, min, temp; for (i = 0; i < len - 1; i++) { min = i; for (j = i + 1; j < len; j++) { if (arr[j] < arr[min]) min = j; } if (min != i) { temp = arr[i]; arr[i] = arr[min]; arr[min] = temp; } } } ``` 希望这些代码能够帮助您了解快速排序和直接选择排序的基本思想和实现

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

南山忆874

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值