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