快速入手八大排序,带你从入门到精通

Posted 蚍蜉撼树谈何易

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了快速入手八大排序,带你从入门到精通相关的知识,希望对你有一定的参考价值。

在这里插入图片描述

思维导图
在这里插入图片描述

冒泡排序

冒泡排序定义:冒泡排序(英语:Bubble Sort)是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
动图演示:
在这里插入图片描述
代码:

void bubble_sort(int arr[],int len)
{
   for(int i=0;i<len-1;i++)//i控制趟数
   {
     for(int j=0;j<len-1-i;j++)//j控制比较到哪个元素截至,因为每次冒泡都可以选出最大的值,所以没必要每次走到数组尾再停。
     {
        if(arr[j]>arr[j+1])
        { 
           swap(&arr[j],&arr[j+1]);
        }
     }
}

swap函数

void swap(int* a, int* b)
{
	int temp = *a;
	*a = *b;
	*b = temp;
}

冒泡排序的优化:设立flag标志位。

原因:冒泡排序每次冒泡都会选出该序列中的最大值或者最小值,若本次冒泡排序中没有任何元素交换的话,证明数组已经有序。避免了非必要的循环

      int flag = 0;
	for (int i = 0; i < len - 1; i++)
	{
		flag = 0;
		for (int j = 0; j < len - 1 - i; j++)
		{
			if (arr[j] < arr[j + 1])
			{
				swap(&arr[j], &arr[j + 1]);
				flag = 1;
			}
		}
		if (flag == 0)
			break;
	}

算法稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。

冒泡排序对应的解
时间复杂度O(N^2)
空间复杂度O(1)
稳定性稳定的(这块是不一定的,取决于自己判定条件,若判定条件写的是大于等于交换,则它不是稳定的)
适用场景适用于数据量较少的场景,数据量较多的话冒泡排序效率是很低的

选择排序

选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理是:第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。选择排序是不稳定的排序方法。
动图演示:
在这里插入图片描述

代码:

void select_sort(int arr[],int len)
{
    for (int i = 0; i < len - 1; i++)
	{
		int max = i;
		for (int j = i + 1; j < len; j++)
		{
			if (arr[max] < arr[j])
			{
				max = j;//更新max为最大元素的下标
			}
		}
		if (max != i)
		{
			swap(&arr[i], &arr[max]);//swap函数如冒泡排序所示
		}
	}
}

选择排序的优化:

选择排序会出现很多次重复比较的情况,他每次都是象征性的给出一个最大元素的下标,然后做对比,比到最后一个,造成很大的程序开销,所以此次优化是为了减少比较的趟数,在每次遍历的基础上既选出最大的,又选出最小的。

void select_sort(int arr[],int len)
{
   int left=0;
   int right=n-1;
   while(left<right)
   {
      int min=left;
      int max=right;
      for(int i=left;i<=right;i++)
      {
         if(arr[i]<arr[min]
          min=i;
         if(arr[i]>arr[max]
          max=i;
      }
      if(min!=left)
      {
       swap(&arr[min],&arr[left]);
      }
      if(max!=right)
      {
        swap(&arr[max],arr[right]);
      }
      left++;
      right--;
   }
}
选择排序对应的解
时间复杂度O(N^2)
空间复杂度O(1)
稳定性不稳定
适用场景适用于元素较少的情况

直接插入排序

插入排序,一般也被称为直接插入排序。对于少量元素的排序,它是一个有效的算法 [1] 。插入排序是一种最简单的排序方法,它的基本思想是将一个记录插入到已经排好序的有序表中,从而一个新的、记录数增1的有序表。在其实现过程使用双层循环,外层循环对除了第一个元素之外的所有元素,内层循环对当前元素前面有序表进行待插入位置查找,并进行移动。
动画演示:
在这里插入图片描述

void insert_sort(int arr[],int len)
{
    for (int i = 1; i < len; i++)
	{
		if (arr[i] < arr[i - 1])
		{
			int j = i - 1;
			int temp = arr[i];
			while (j >= 0 && temp < arr[j])
			{
				arr[j + 1] = arr[j];
				j--;
			}
			arr[j + 1] = temp;
		}
	}
}
插入排序对应的解
时间复杂度最优的解O(N)(就意思是不需要走while语句,不需要移动元素,自身是已经排好序的),最差的是O(N^2)假设此时需要的刚好是原数组的逆序数组,这就比较裂开了。
空间复杂度O(1)
稳定性稳定的
适用场景数据量小切数组接近有序最佳

希尔排序(缩小增量的排序)

希尔排序(Shell’s Sort)是插入排序的一种又称“缩小增量排序”(Diminishing lncrementSort),是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因D.L.Shell 于 1959年提出而得名。
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率。
但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。
动画演示:
在这里插入图片描述

示例图:
在这里插入图片描述
代码:

void shell_sort(int arr[],int len)
{
   int key,i,j;
	int gap = len;
	while (gap > 1)
	{
		gap = gap / 3 + 1;
		for ( i = gap; i < len; i++)
		{
			key = arr[i];//在一次循环中对多组进行操作
			for ( j = i; j >= gap && key < arr[j - gap]; j -= gap)
			{
				arr[j] = arr[j - gap]; 
			}
			arr[j] = key;
		}
	}
}
希尔排序对应的解
时间复杂度O(N^1.3)-O(N*N),跟gap取值有关,没有一个准确的值
空间复杂度O(1)
稳定性不稳定
适用场景数据比较杂乱,适用于大型数组

堆排

动画演示:
在这里插入图片描述
堆排建堆:
因为我们此时要求的降序,所以建小堆

void adjust_down(int arr[], int n,int root)
{
	int child = root * 2 + 1;

	while (child < n)
	{
		if (child + 1 < n && arr[child + 1] < arr[child])
		{
			child += 1;
		}
		if (arr[child] < arr[root])
		{
			swap(&arr[child], &arr[root]);
			root = child;
			child = root * 2 + 1;
		}
		else
		{
			return;
		}
	}
}
思想:先求取它第一个非叶子结点,然后依次向下调整
for (int root = (len - 2) / 2; root >= 0; root--)
	{
		adjust_down(arr, len, root);
	}

堆排代码:
利用删除思想,每次将堆顶元素置换到该堆的最后一个元素,同时再次执行向下调整,这样下次取出来的堆顶仍是当前堆的最小值

int end = len - 1;
	while (end)
	{
		swap(&arr[end], &arr[0]);
		adjust_down(arr, end, 0);
		end--;
	}

感觉这样呈现有点不清楚,还是把整个代码粘出来吧

void swap(int* a, int* b)
{
	int temp = *a;
	*a = *b;
	*b = temp;
}
void print(int arr[], int len)
{
	for (int i = 0; i < len; i++)
	{
		printf("%3d", arr[i]);
	}
	printf("\\n");
}
void adjust_down(int arr[], int n,int root)
{
	int child = root * 2 + 1;

	while (child < n)
	{
		if (child + 1 < n && arr[child + 1] < arr[child])
		{
			child += 1;
		}
		if (arr[child] < arr[root])
		{
			swap(&arr[child], &arr[root]);
			root = child;
			child = root * 2 + 1;
		}
		else
		{
			return;
		}
	}
}
void test01()
{
	int arr[] = { 1,3,4,5,2,3,0,9,10 ,11, 7,20 };
	int len = sizeof(arr) / sizeof(arr[0]);
	for (int root = (len - 2) / 2; root >= 0; root--)
	{
		adjust_down(arr, len, root);
	}
	int end = len - 1;
	while (end)
	{
		swap(&arr[end], &arr[0]);
		adjust_down(arr, end, 0);
		end--;
	}
	print(arr, len);
}

接下来分析一下时间复杂度:复杂度分为建堆的时间的复杂度+堆排的复杂度。
在这里插入图片描述
堆排时重新调整的复杂度,此时要循环n-1次,每次需要将堆顶元素向下调整log(n)步,所以堆排时重新调整的时间复杂度为O((n-1)log(n))≈O(nlogn)
总的时间复杂度等于建堆复杂度+堆排复杂度
O(n)=O(nlogn)+O(n)此时根据时间复杂度计算方法,舍弃掉较小的阶项
所以此时时间复杂度可以≈O(nlogn);

堆排序对应的解
时间复杂度O(N*log(N)
空间复杂度O(1)
稳定性不稳定
适用场景适用于内存空间较少的情况,不能全部加载入内存,比如经典的top-K问题

基数排序

基数排序概念:基数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
动画演示
在这里插入图片描述

void cal_sort(int arr[],int size,int temp[])
{
   for (int i = 0; i < len; i++)
	{
		temp[arr[i]]++;
	}
}
void test()
{
   int arr[] = { 1,2,3,1,2,3,4,1,2,3,4,8,5,6,7,9,10 };
	int len = sizeof(arr) / sizeof(arr[0]);
	int Max, Min;
	Max = Min = 0;
	for (int i = 1; i < len; i++)
	{
		if (arr[Max] < arr[i])
		{
			Max = i;
		}
		if (arr[Min] > arr[i])
		{
			Min = i;
		}
	}
	int size = arr[Max] - arr[Min] + 1;//确定辅助空间大小
	int* auxiliary_arr = (int*)calloc(sizeof(int) ,size);
	//开辟辅助空间,将其初始值置为0;
	if(auxiliary_arr==NULL)
	{
		return;
	}
	cal_sort(arr,len,auxiliary_arr);
	//数组的复制,按区间每个元素每个元素重新赋值
	int j=0;
	for (int i = 0; i < size; i++)
	{
		int k = auxiliary_arr[i];//元素个数
		while (k--)
		{
			arr[j++] = i;
		}
	}
	//free()掉该部分空间
	free(auxiliary_arr);
}

假设该部分区间的值集中在[m,n]中

计数排序对应的解
时间复杂度O(N)N为元素个数
空间复杂度O(m-n+1)
稳定性稳定
适用场景适用于数组中数值区间确定的,且数值范围不是很大的数组。不能是1到10000,那这样就得不偿失了。

同时,因为涉及到开辟额外空间,所以必须要释放掉该部分空间。

快排

快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

算法描述
快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:
从数列中挑出一个元素,称为 基准值。
重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作
递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
快排动画演示图:
在这里插入图片描述

快排的递归做法

递归做法----hoare方法

算法思想:
1、找一个基准值key(一般三数取中法),本题中基准值为数组中最右的元素,再定义两个指针begin(指向首元素)和end(指向尾元素)
2、begin从前往后走找比基准值key大的元素(找到后停下),end从后往前走找比基准值小的元素(找到后也停下),
然后,交换array[begin]和array[end],依次循环操作。
3、当begin与end相遇,将array[begin]或array[end]与基准值交换。
在这里插入图片描述
交换值的函数,后面的swap均为该swap

void swap(int* a, int* b)
{
	int temp = *a;
	*a = *b;
	*b = temp;
}
int part(int arr[], int left, int right)
{
    //将数组区间的最后一个元素作为它的基准值
	int key = arr[right-1];
	int begin = left;
	int end = right - 1;
	while (begin < end)
	{
		while (begin < end && arr[begin] >= key)
			begin++;
		while (begin < end && arr[end] <= key)
			end--;
		if (begin < end)
		{
			swap(&arr[begin], &arr[end]);
		}
	}
	//交换基准值
	if (begin != right - 1)
	{
		swap(&arr[begin], &arr[right - 1]);
	}
	return begin;
}
void Quick_sort(int arr[], int left, int rig

以上是关于快速入手八大排序,带你从入门到精通的主要内容,如果未能解决你的问题,请参考以下文章

一文带你nodejs从入门到精通

一文带你nodejs从入门到精通

云开发系列课程让你从入门到精通快速上手Serverless和云开发技术

kafka入门

kafka入门

数据结构c语言版八大算法(上)图文详解带你快速掌握——希尔排序,堆排序,插入排序,选择排序,冒泡排序!