🍉一、十大排序算法汇总
- 分类
- 时空复杂度
🍇二、插入排序
💉把待排序的记录按照其关键码值插入到已经有序的序列列中,循环反复,直到所有的记录全部插入完毕,得到一个新的序列,该序列就是有序序列。
void InsertSort(int* arr, int sz)
{
for (int i = 0; i < sz - 1; i++)//注意这里i相当于就等end,i必须是小于sz-1
{
//此for循环是要排序的趟数
int end = i;
int tmp = arr[end + 1];
//此while循环是一趟下来要比较的次数
while (end >= 0)//只要end没出界就继续比较
{
if (tmp < arr[end])
{
//在比较的过程中只要tmp值比arr[end]小,就向后移动arr[end]
arr[end + 1] = arr[end--];
//end--;
}
else
{
//一旦出现相等或者比arr[end]大,就将tmp插入到arr[end+1]处
//这里break掉是因为,如果循环结束,说明要插入的值比所以的值都要小,
//要插入到头部,所有到while后面一并处理
break;
}
}
arr[end + 1] = tmp;
}
}
总结:
- 元素集合越接近有序,直接插入排序算法的时间效率越高
- 时间复杂度:O(N^2)
- 空间复杂度:O(1),它是一种稳定的排序算法
- 稳定性:稳定
🍓三、希尔排序
🌈💨希尔排序本质上也是直接插入排序,但是会先进行预排序,使原序列更接近有序序列,最后将预排之后的序列进行直接插入排序。
//希尔排序
void ShellSort(int* arr, int sz)
{
int count=0;//这是我为了看看希尔排序和直接插入排序的性能比较而设置的计数
int gap = sz;//设置排序的间隔
while ( gap > 1 )
{
//这里一定要保证gap最后进来循环后为1,所以对此加1
gap = gap / 3 + 1;//gap>1为与排序,gap==1,为直接插入排序
for (int i = 0; i < sz - gap; i++)//这里并不是一次性把一组排完,而是挨个往后,一组一个轮流排
{
int end = i;
int tmp = arr[end + gap];
while (end >= 0)
{
if (tmp < arr[end])
{
arr[end + gap] = arr[end];
end -= gap;
count++;
}
else
{
break;
}
}
arr[end + gap] = tmp;
}
}
printf("希尔插入排序后移次数count=%d", count);
}
总结:
- 希尔排序是对直接插入排序的优化。
- 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的 了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的 对比。
- 希尔排序的时间复杂度不好计算,需要进行推导,推导出来平均时间复杂度: O(N^1.3— N^2)
- 稳定性:不稳定
🥭四、选择排序
⛳直接选择排序就是遍历整个数组,每遍历一遍的目的是找出该数组中的最大数和最小数对应的下标,然后将最小数和数组的第一个数进行交换,最大数和数组的最后一个数进行交换,然后缩小范围再次遍历。(已排好的最大数和最小数不再参与后续的遍历)
//交换函数
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//直接选择排序
void SelectSort(int* arr,int n)
{
int begin = 0;
int end = n - 1;
while (begin < end)
{
//max和min分别代表最大值和最小值的下标
int max = begin;
int min = begin;
for (int i = begin; i <= end; i++)
{
if (arr[i] < arr[min])
{
min = i;
}
if (arr[i] > arr[max])
{
max = i;
}
}
Swap(&arr[begin], &arr[min]);
//如果begin和maxindex重合了,必须调整一下maxindex
if (begin == max)
{
max = min;
}
Swap(&arr[end], &arr[max]);
begin++;
end--;
}
}
总结:
- 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:不稳定
🥒五、堆排序
堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//向下调整算法
void AdjustDown(int* arr, int n, int root)
{
int parent = root;
int child = parent * 2 + 1;//默认先对左孩子进行操作
while (child<n)
{
//选出左右孩子较小的那一个
//防止没有右孩子,数组越界
if (child+1<n && arr[child + 1] > arr[child]) //建小堆用小于,建大堆用大于
{
child += 1;
}
if (arr[child] > arr[parent]) //建小堆用小于,建大堆用大于
{
Swap(&arr[child], &arr[parent]);//交换父子的值
parent = child;//父亲到孩子的位置,继续向下调整
child = parent * 2 + 1;//默认又从左孩子开始
}
else
{
break;
}
}
}
void HeapSort(int* arr, int n)
{
//建堆,排升序建大堆,排降序建小堆
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(arr, n, i);
}
//排升序
int end = n - 1;//end为最后一个数的下标
while (end > 0)//剩余超过一个数就交换
{
Swap(&arr[0], &arr[end]);
AdjustDown(arr, end, 0);
end--;
}
}
总结:
- 堆排序使用堆来选数,效率就高了很多。
- 时间复杂度:O(N*logN)
- 空间复杂度:O(1)
- 稳定性:不稳定
🍒六、冒泡排序
🌴冒泡排序(Bubble Sort):是常用排序算法中比较简单的一种。其基本思路是将待排序的序列从头遍历到尾,遍历n趟(n为序列元素个数)。每一趟依次比较相邻的两个数,如果顺序错误就交换,然后继续往后遍历,这样一趟下来,最大(最小)的数就被冒到了序列尾部。
//交换函数
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//冒泡排序
void BubbleSort(int* arr, int n)
{
//趟数
for (int i = 0; i < n; i++)
{
//单趟排序
int Ischange = 0;//标志是否发生交换,如果没有就代表已经有序,不需要再排
for (int j = 0; j < n-i-1; j++)
{
if (arr[j] > arr[j + 1])
{
Swap(&arr[j], &arr[j + 1]);
Ischange = 1;
}
}
if(Ischange == 0)
{
break;
}
}
}
总结:
- 冒泡排序是一种非常容易理解的排序
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:稳定
🍐七、快速排序
- 递归挖坑法
⛳ 单趟排序:单趟排序的目的就是使当前的key值放到它最终的位置,左边全是比它小的数,右边全是比它大的数。我们一般选取第一个值或者最后一个值做key,pivot初始值为初始的key值的位置,这里也就是第一个位置。当pivot在begin的位置时,end从右往左开始找比key小的值,找到后将它放到pivot的地方,也就是填坑,填完之后自己形成新的坑位;pivot在end的位置时,begin从左往右开始找比key大的数,找到之后进行填坑,直到begin和end相遇时,最后将key放至相遇点即可。
void QuickSort(int* arr, int left,int right)
{
//当区间被分到只有1个元素时,则返回
//=代表只有一个元素,>代表没有右区间,为什么会出现大于可以看下面递归图
if (left >= right)
{
return;
}
int begin = left;
int end = right;
int key = arr[begin];
int pivot = begin;
while (begin < end)
{
//pivot在begin那边,则end这边找比key小
while (begin < end&&arr[end] >= key)
{
end--;
}
//循环结束则为找到该小值,将之赋值给arr[pivot]
arr[pivot] = arr[end];
//自己形成新的坑位
pivot = end;
//piovt在end那边,则begin这边找比key大
while (begin < end&&arr[begin] <= key)
{
begin++;
}
//循环结束则为找到该大值,将之赋值给arr[pivot]
arr[pivot] = arr[begin];
//自己形成新的坑位
pivot = begin;
}
//循环结束代表begin和end相遇,并且相遇在坑(pivot)
pivot = begin;//这里给begin和end都可以
//将key放到pivot
arr[pivot] = key;
//将区间分为[left,pivot-1] pivot [pivot+1,right]
//采用分治递归,左边有序了,右边有序了,则整体有序
QuickSort(arr, left, pivot - 1);
QuickSort(arr, pivot + 1, right);
}
- 非递归挖坑法
//交换函数
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//三数取中
int GetMinIndex(int* arr, int left, int right)
{
int mid = (left + right) >> 1;
if (arr[left] < arr[mid])
{
if (arr[mid] < arr[right])
{
return mid;
}
if (arr[left] < arr[right] && arr[right] < arr[mid])
{
return right;
}
return left;
}
else//arr[left] >= arr[mid]
{
if (arr[left] < arr[right])
{
return left;
}
if (arr[mid] < arr[right] && arr[right] < arr[left])
{
return right;
}
return mid;
}
}
//快排非递归
void QuickSort(int* arr, int n)
{
ST st;
StackInit(&st);
//把左右区间压栈,先压右边
StackPush(&st, n - 1);
//后压左边
StackPush(&st, 0);
//只要栈不为空,就继续分割排序
while (!StackEmpty(&st))
{
//从栈里面取出左右区间
int left = StackTop(&st);
StackPop(&st);
int right = StackTop(&st);
StackPop(&st);
int index = GetMinIndex(arr, left, right);
//因为我们下面的逻辑都是把第一个数作为key,
//为了避免改动代码,这里我们直接交换就可以
Swap(&arr[left], &arr[index]);
//开始单趟排序
int begin = left;
int end = right;
int pivot = begin;
int key = arr[begin];
while (begin < end)
{
//end开始找小
while (begin < end && arr[end] >= key)
{
end--;
}
arr[pivot] = arr[end];
pivot = end;
//begin开始找大
while (begin < end && arr[begin] <= key)
{
begin++;
}
arr[pivot] = arr[begin];
pivot = begin;
}
pivot = begin;
arr[pivot] = key;
//区间分为[left,pivot-1]pivot[pivot+1,right]
//利用循环继续分割区间
//先入右子区间
if (pivot + 1 < right)
{
//说明右子区间不止一个数
//先入右边边界
StackPush(&st, right);
//再入左边边界
StackPush(&st, pivot+1);
}
//再入左子区间
if (left < pivot-1)
{
//说明左子区间不止一个数
//先入右边边界
StackPush(&st, pivot-1);
//再入左边边界
StackPush(&st, left);
}
}
StackDestory(&st);
}
总结:
- 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
- 时间复杂度:O(N*logN)
- 空间复杂度:O(logN)
- 稳定性:不稳定
🥕八、归并排序
🌈💨归并排序:是建立在归并操作上的一种有效,稳定的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
- 递归实现
void _MergeSort(int* arr, int left, int right,int* tmp)
{
if (left >= right)//=为只有一个数,>为区间存在
{
return;
}
//左右区间如果没有序,分治递归分割区间,直到最小,
int mid = (left + right) >> 1;
//区间被分为[left,mid] 和 [mid+1,right]
//开始递归
_MergeSort(arr, left, mid, tmp);
_MergeSort(arr, mid + 1, right, tmp);
//此时通过递归已经能保证左右子区间有序
//开始归并
int begin1 = left;
int end1 = mid;
int begin2 = mid + 1;
int end2 = right;
//归并时放入临时数组的位置从left开始
int index = left;
while (begin1 <= end1 && begin2 <= end2)
{
if (arr[begin1] < arr[begin2])
{
tmp[index] = arr[begin1];
index++;
begin1++;
//这三行代码可以写成一行
//tmp[index++] = arr[begin1++];
}
else
{
tmp[index++] = arr[begin2++];
}
}
//循环结束,将还没遍历完的那个区间剩下的数拷贝下来
while (begin1 <= end1)
{
tmp[index++] = arr[begin1++];
}
while (begin2 <= end2)
{
tmp[index++] = arr[begin2++];
}
//将排归并完的数拷贝回原数组
for (int i = left; i <=right ; i++)
{
arr[i] = tmp[i];
}
}
void MergeSort(int* arr, int n)
{
//申请一个空间用来临时存放数据
int* tmp = (int*)malloc(sizeof(int)*n);
//归并排序
_MergeSort(arr, 0, n - 1, tmp);
//释放空间
free(tmp);
}
- 非递归实现
//交换函数
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//三数取中
int GetMinIndex(int* arr, int left, int right)
{
int mid = (left + right) >> 1;
if (arr[left] < arr[mid])
{
if (arr[mid] < arr[right])
{
return mid;
}
if (arr[left] < arr[right] && arr[right] < arr[mid])
{
return right;
}
return left;
}
else//arr[left] >= arr[mid]
{
if (arr[left] < arr[right])
{
return left;
}
if (arr[mid] < arr[right] && arr[right] < arr[left])
{
return right;
}
return mid;
}
}
//快排非递归
void QuickSort(int* arr, int n)
{
ST st;
StackInit(&st);
//把左右区间压栈,先压右边
StackPush(&st, n - 1);
//后压左边
StackPush(&st, 0);
//只要栈不为空,就继续分割排序
while (!StackEmpty(&st))
{
//从栈里面取出左右区间
int left = StackTop(&st);
StackPop(&st);
int right = StackTop(&st);
StackPop(&st);
int index = GetMinIndex(arr, left, right);
//因为我们下面的逻辑都是把第一个数作为key,
//为了避免改动代码,这里我们直接交换就可以
Swap(&arr[left], &arr[index]);
//开始单趟排序
int begin = left;
int end = right;
int pivot = begin;
int key = arr[begin];
while (begin < end)
{
//end开始找小
while (begin < end && arr[end] >= key)
{
end--;
}
arr[pivot] = arr[end];
pivot = end;
//begin开始找大
while (begin < end && arr[begin] <= key)
{
begin++;
}
arr[pivot] = arr[begin];
pivot = begin;
}
pivot = begin;
arr[pivot] = key;
//区间分为[left,pivot-1]pivot[pivot+1,right]
//利用循环继续分割区间
//先入右子区间
if (pivot + 1 < right)
{
//说明右子区间不止一个数
//先入右边边界
StackPush(&st, right);
//再入左边边界
StackPush(&st, pivot+1);
}
//再入左子区间
if (left < pivot-1)
{
//说明左子区间不止一个数
//先入右边边界
StackPush(&st, pivot-1);
//再入左边边界
StackPush(&st, left);
}
}
StackDestory(&st);
}
总结:
- 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题
- 时间复杂度:O(N*logN)
- 空间复杂度:O(N)
- 稳定性:稳定
🌽九、桶排序
桶排序 (Bucket sort)或所谓的箱排序,是一个排序算法,工作的原理是将数组分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。桶排序是鸽巢排序的一种归纳结果。当要被排序的数组内的数值是均匀分配的时候,桶排序使用线性时间(Θ(n))。但桶排序并不是 比较排序,他不受到 O(n log n) 下限的影响。
#include<stdio.h>
int main()
{
int a[100],t,max=sizeof(a)/sizeof(int);
for(int i=0;i<max;i++)
a[i]=0;
for(int j=0;j<5;j++)
{
scanf("%d",&t);
a[t]++;
}
for(int k=0;k<max;k++)
for(int y=0;y<a[k];y++)
printf("%d ",k);
return 0;
}
🍑十、计数排序
⛳计数排序:是一个非基于比较的排序算法,该算法于1954年由 Harold H. Seward 提出。它的优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k)(其中k是整数的范围),快于任何比较排序算法。当然这是一种牺牲空间换取时间的做法。
//计数排序(优化后)
void CountSort(int* arr, int n)
{
//找到序列中的最大值和最小值
int max = arr[0];
int min = arr[0];
for (int i = 0; i < n; i++)
{
if (arr[i] > max)
{
max = arr[i];
}
if (arr[i] < min)
{
min = arr[i];
}
}
int range = max - min + 1;//开辟空间的数量
int* countArr = (int*)malloc(sizeof(int)*range);//开辟空间
//初始化数组全部为0
memset(countArr, 0, sizeof(int)*range);
//开始计数
for (int i = 0; i < n; i++)
{
countArr[arr[i]-min]++;
}
//开始排序
int j = 0;
for (int i = 0; i < range; i++)
{
while (countArr[i]--)
{
arr[j] = i + min;
j++;
}
}
free(countArr);
}
总结
缺点1:不能对小数进行排序;
缺点2:空间复杂度O(range),空间浪费不能彻底的解决。
优点1:时间复杂度为:O(N+range),对集中的数据排序效率极高。
🌶️十一、基数排序
基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或binsort,顾名思义,它是透过键值的部份资讯,将要排序的元素分配至某些“桶”中,藉以达到排序的作用,基数排序法是属于稳定性的排序,在某些时候,基数排序法的效率高于其它的稳定性排序法。
//基数排序
void RadixSort(int* arr, int n)
{
//max为数组中最大最小值
int max = arr[0];
int min = arr[0];
int base = 1;
//找出数组中的最大值
for (int i = 0; i < n; i++)
{
if (arr[i] > max)
{
max = arr[i];
}
if (arr[i] < min)
{
min = arr[i];
}
}
//循环结束max就是数组最大最小值
//循环将数组的元素全部变为正数
//所有元素加上最小值的绝对值
for (int i = 0; i < n; i++)
{
arr[i] += abs(min);
}
//临时存放数组元素的空间
int* tmp = (int*)malloc(sizeof(int)*n);
//循环次数为最大数的位数
while (max / base > 0)
{
//定义十个桶,桶里面装的不是数据本身,而是每一轮排序对应(十、白、千...)位的个数
//统计每个桶里面装几个数
int bucket[10] = { 0 };
for (int i = 0; i < n; i++)
{
//arr[i] / base % 10可以取到个位、十位、百位对应的数字
bucket[arr[i] / base % 10]++;
}
//循环结束就已经统计好了本轮每个桶里面应该装几个数
//将桶里面的元素依次累加起来,就可以知道元素存放在临时数组中的位置
for (int i = 1; i < 10; i++)
{
bucket[i] += bucket[i - 1];
}
//循环结束现在桶中就存放的是每个元素应该存放到临时数组的位置
//开始放数到临时数组tmp
for (int i = n - 1; i >= 0; i--)
{
tmp[bucket[arr[i] / base % 10] - 1] = arr[i];
bucket[arr[i] / base % 10]--;
}
//不能从前往后放,因为这样会导致十位排好了个位又乱了,百位排好了十位又乱了
/*for (int i = 0; i < n; i++)
{
tmp[bucket[arr[i] / base % 10] - 1] = arr[i];
bucket[arr[i] / base % 10]--;
}*/
//把临时数组里面的数拷贝回去
for (int i = 0; i < n; i++)
{
arr[i] = tmp[i];
}
base *= 10;
}
free(tmp);
//还原原数组
for (int i = 0; i < n; i++)
{
arr[i] -= abs(min);
}
}
总结:
- 时间复杂度:O(n+k)
- 空间复杂度:O(n+k)
- 不能对小数进行排序,但是速度极快