欢迎来到本期节目- - -
排序
前言
本期将介绍一些经典的排序算法,如果您有补充,欢迎各种奇思妙想!
排序算法的++稳定性++ :
在一组待排序的数据中,如果存在相同key元素(排序算法的关键比较元素)的两个及以上数量的数据在经过该算法的排序之后,它们的相对位置并没有改变,那么该排序算法稳定,反之,不稳定.
旅客们请注意,您乘坐的由 一脸懵逼站 始发 至 豁然开朗站 的N次列车即将发车了!
选择排序
时间复杂度:O(N^2^)
空间复杂度:O(1)
稳定性:不稳定
动图演示:
算法思路:
|-----------------|
| 从左往右排序,每趟排好一个数; |
|-----------------------------------|
| 在待排数组中,遍历一遍找出最小值,然后和待排数组的第一个位置交换; |
cpp
void Select_Sort(int* arr,int n)
{
assert(arr);
for(int begin = 0; begin < n-1;begin++)
{
int mini = begin;
for(int j = mini+1;j<n;j++)
{
if(arr[j]<arr[mini])
{
mini = j;
}
}
swap(&arr[begin],&arr[mini]);
}
}
选择排序的优缺点:
优点:空间复杂度低,算法简单
缺点:时间复杂度较高,效率低,不适合大规模数据排序。
冒泡排序
时间复杂度:O(N^2^)
空间复杂度:O(1)
稳定性:稳定
动图演示:
算法思路:
|----------------|
| 从右往左排,每趟确定一个数; |
|--------------------------------------|
| 遍历待排数组,遍历过程中,将相邻元素的关系通过交换使得前一个小于后一个; |
cpp
void Bubble_Sort(int* arr, int n)
{
assert(arr);
int flag = 0;
for (int i = 0; i < n - 1; i++)
{
flag = 1;
for (int j = 0; j < n - 1 - i; j++)
{
if (arr[j] > arr[j + 1])
{
swap(&arr[j], &arr[j + 1]);
flag = 0;
}
}
if (flag == 1)
{
break;
}
}
}
冒泡排序优缺点:
优点:算法简单,空间复杂度低
缺点:时间复杂度较高,效率低
插入排序
时间复杂度:O(N^2^)
空间复杂度:O(1)
稳定性:稳定
动图演示:
算法思路:
|----------------------------------------------|
| 从左往右排序,每趟排序确定一个数; 每趟排序将待排序数组的第一个数往已排序的数组中插入; |
|------------------------------------------------------------------------|
| 比较方式是将本次要插入的元素inserted_num 与已排序的数组依次从后往前比,如果大于inserted_num,则留出空位,否则插入; |
cpp
void Insert_Sort(int* arr, int n)
{
for(int i = 0; i < n-1; i++)
{
int end = i;
int inserted_num = arr[end+1];
while(end>=0 && arr[end]>inserted_num)
{
arr[end+1] = arr[end];
end--;
}
arr[end+1] = inserted_num;
}
}
插入排序的优劣势:
优势:
排序稳定;
数据量较小是,效率较高;
当数据接近有序时,效率较高;
劣势:
不适合大规模数据排序;
希尔排序
时间复杂度:O(N^1.3^)
空间复杂度:O(1)
稳定性:不稳定
动图演示:
算法思路:
|---------------|
| 该算法是对插入排序的优化; |
|--------------------------|
| 通过预排使得数组接近有序,最后进行一次插入排序; |
|-----------------------------|
| 每次预排是将原数组分为多个小组,每个小组进行插入排序; |
|-----------------------------------------|
| 每次预排的小组数量少于上一次,直到最后只分一个小组,既对整个数组进行插入排序; |
cpp
void shellsort(int* nums,int n)
{
int gap = n; //gap既代表本次预排的小组数量,也代表了小组中相邻元素的距离
while(gap > 1)
{
gap = gap/3+1;
for(int i = 0; i < n-gap; i++)
{
//为了对应动图演示,这里稍微和插入排序不一样,不是用插入的方式,而是用交换的方式,但是效果是一样的.
int end = i;
while(end >= 0 && nums[end] > nums[end+gap])
{
swap(&nums[end],&nums[end+gap]);
end-=gap;
}
}
}
}
希尔排序的优劣势:
优势:
适合中等规模数据排序;
当数据初始状态在一定程度的无序时,效率高于其它高级排序;
劣势:
排序不稳定;
性能受到增量序列的影响,性能不稳定;
堆排
时间复杂度:O(N*logN)
空间复杂度:O(1)
稳定性:不稳定
动图演示:
算法思路:
|-----------------------|
| 物理结构上是数组,逻辑结构上是完全二叉树; |
|-----------------------------------|
| 堆的性质:任何一个根节点都大于等于(或者小于等于)左右子树的节点; |
|-------------------------------|
| 根据该性质通过控制数组下标的方式来维护堆,既建堆和调整堆; |
cpp
void AdjustDown(int* arr, int n, int parent)
{
int child = parent*2+1;
while(child < n)
{
if(child+1 < n && arr[child+1] > arr[child])
child++;
if(arr[child] > arr[parent])
{
swap(&arr[child],&arr[parent]);
parent = child;
child = parent*2 + 1;
}
else
break;
}
}
void Heap_sort(int* arr, int n)
{
//建堆---O(N)
for(int i = (n-2)/2; i >= 0; i--)
{
AdjustDown(arr,n,i);
}
//堆排序---O(N*logN)
for(int i = n-1; i > 0; i--)
{
swap(&arr[0],&arr[i]);
AdjustDown(arr,i,0);
}
}
堆排的优劣势:
优势:
性能稳定,不依赖初始状态;
适合大规模数据排序;
劣势:
排序不稳定;
当重复元素较多时,性能不如其它高级排序;
归并排序
时间复杂度:O(N*logN)
空间复杂度:O(N)
稳定性:稳定
动图演示:
算法思路:
|-------------------------------------------------------------------------|
| 该算法采用分治的思想,将一段待排序的数据区间,对半分成左右两份区间,然后假设两份区间有序(类似二叉树的后序遍历),转化成合并两段有序区间问题; |
|-------------------------|
| 在合并两段有序区间时,需要额外的空间进行存储; |
递归版:
cpp
void _mergesort(int* nums,int* tmp,int left,int right) //左闭右闭区间
{
if(left >= right)
return;
int mid = left+(right-left)/2;
_mergesort(nums,tmp,left,mid); //由于除2问题,注意死循环问题!
_mergesort(nums,tmp,mid+1,right);
int begin1 = left;
int end1 = mid;
int begin2 = mid+1;
int end2 = right;
int t = left;
while(begin1 <= end1 && begin2 <= end2)
{
if(nums[begin1] <= nums[begin2])
tmp[t++] = nums[begin1++];
else
tmp[t++] = nums[begin2++];
}
while(begin1 <= end1)
{
tmp[t++] = nums[begin1++];
}
while(begin2 <= end2)
{
tmp[t++] = nums[begin2++];
}
memcpy(nums+left,tmp+left,(right-left+1)*sizeof(int));
}
void Merge_sort(int* nums,int numsSize)
{
int* tmp = (int*)malloc(sizeof(int)*numsSize);
if(tmp == NULL)
{
perror("malloc fail:");
return;
}
_mergesort(nums,tmp,0,numsSize-1);
free(tmp);
tmp = NULL;
}
非递归版:
该版本类似于直接实现递归版的回归过程;
cpp
void Merge_sort(int* nums,int n)
{
int* tmp = (int*)malloc(sizeof(int)*n);
if(tmp == NULL)
{
perror("malloc fail:");
return;
}
int gap = 1; //gap表示每趟排序中每个小组的个数,最后一个小组可能小于gap
while(gap < n)
{
for(int i = 0; i < n; i+=2*gap) //每次跳过两个小组,因为每次排序排的是两个小组
{
int begin1 = i;
int end1 = i+gap-1;
int begin2 = i+gap;
int end2 = i+2*gap-1;
if(begin2 >= n) //处理越界问题
break;
if(end2 >= n)
end2 = n-1;
int t = begin1;
while(begin1 <= end1 && begin2 <= end2)
{
if(nums[begin1] <= nums[begin2])
tmp[t++] = nums[begin1++];
else
tmp[t++] = nums[begin2++];
}
while(begin1 <= end1)
tmp[t++] = nums[begin1++];
while(begin2 <= end2)
tmp[t++] = nums[begin2++];
memcpy(nums+i,tmp+i,sizeof(int)*(t-i));
}
gap*=2;
}
free(tmp);
tmp = NULL;
}
归并排序的优劣势:
优势:
排序稳定;
适合大规模数据;
劣势:
空间复杂度较高;
快排
时间复杂度:O(N*logN)
空间复杂度:O(logN)
稳定性:不稳定
动图演示:
算法思路:
|-----------------------------------------------------|
| 选取一个key值,通过单趟排序,将key值位置确定,然后使用相同方式对key的左区间和右区间进行排序; |
单趟排序第一种方式:
霍尔版:
cpp
int _quicksort1(int* nums,int begin,int end) //左闭右闭
{
int keyi = begin;
int left = begin;
int right = end;
while(left < right)
{
while(left < right && nums[right] >= nums[keyi])
right--;
while(left < right && nums[left] <= nums[keyi])
left++;
swap(&nums[left],&nums[right]);
}
swap(&nums[left],&nums[keyi]);
keyi = left;
return keyi;
}
单趟排序第二种方式:
挖坑版:
cpp
int _quicksort2(int* nums,int begin,int end) //左闭右闭
{
int key = nums[begin];
int left = begin;
int right = end;
while(left < right)
{
while(left < right && nums[right] >= key)
right--;
nums[left] = nums[right];
while(left < right && nums[left] <= key)
left++;
nums[right] = nums[left];
}
nums[left] = key;
return left;
}
单趟排序第三种方式:
前后指针版:
cpp
int _quicksort3(int* nums,int begin,int end) //左闭右闭
{
int keyi = begin;
int prev = begin;
int cur = prev+1;
while(cur <= end)
{
if(nums[cur] < nums[keyi] && ++prev != cur)
swap(&nums[cur],&nums[prev]);
cur++;
}
swap(&nums[prev],&nums[keyi]);
keyi = prev;
return keyi;
}
接下来只要采用分治的思想就可以将整个数组排序完成;
cpp
void Quick_sort(int* nums, int begin,int end) //左闭右闭
{
if(begin >= end)
return;
int keyi = _quicksort3(nums,begin,end);//3种方法随便选
Quick_sort(nums,begin,keyi-1);
Quick_sort(nums,keyi+1,end);
}
在理想情况下,key值如果是每段区间的中值,那么该算法的递归深度为logN,既空间复杂度为logN,时间复杂度为N*logN ; 但实际上,当数组已经有序或接近有序时,该算法的空间复杂度增加到O(N),时间复杂度增加到O(N^2^) ,导致效率大大降低; 所以此时选key时,可以加上一个三端取中的函数,尽可能减少这种情况的发生;
bash
int GetMidIndex(int* nums,int begin,int end)
{
int mid = begin+(end-begin)/2;
if(nums[begin] > nums[end])
{
if(nums[begin] < nums[mid])
return begin;
else if(nums[end] > nums[mid])
return end;
else
return mid;
}
else
{
if(nums[begin] > nums[mid])
return begin;
else if(nums[end] < nums[mid])
return end;
else
return mid;
}
}
但是这在一些场景下,依然会有栈递归深度太深的风险,所以我们还可以通过小区间优化降低深度,以此提高效率;
cpp
void Quick_sort(int* nums, int begin,int end) //左闭右闭
{
if(begin >= end)
return;
if(end-begin+1 < 16)
{
Insert_Sort(nums+begin,end-begin+1);
return;
}
int midi = GetMidIndex(nums,begin,end);
swap(&nums[begin],&nums[midi]);
int keyi = _quicksort3(nums,begin,end);
Quick_sort(nums,begin,keyi-1);
Quick_sort(nums,keyi+1,end);
}
虽然这已近提高了快排的效率,但是当重复的数据较多时,该排序算法的效率又会下降;
所以这里我们了解一下
三路划分:
cpp
void Quick_sort(int* nums, int begin,int end) //左闭右闭
{
if(begin >= end)
return;
if(end-begin+1 < 16)
{
Insert_Sort(nums+begin,end-begin+1);
return;
}
int midi = GetMidIndex(nums,begin,end);
swap(&nums[begin],&nums[midi]);
//三路划分
int key = nums[begin];
//left和right维护值为key的区间
int left = begin;
int right = end;
int cur = begin+1;
while(cur <= right)
{
if(nums[cur] < key) //小于key和左边的key交换 ,小的扔到左边,此时cur的值为key
swap(&nums[cur++],&nums[left++]);
else if(nums[cur] > key) //大于key的和right指向的交换, 大的扔到右边,此时cur不知道大小
swap(&nums[cur],&nums[right--]);
else //值为key,继续走
cur++;
}
Quick_sort(nums,begin,left-1);
Quick_sort(nums,right+1,end);
}
虽然三路划分可以在数据重复度高时提高排序性能,但是相较于两路划分,数据重复度不高时,性能又下降了,所以总的来说,想要用好快排得看场景;
除以上写法之外,也可以使用非递归,效率不变,因为用的是栈模拟递归调用;
非递归
cpp
void Quick_sort(int* nums, int begin,int end) //左闭右闭
{
stack st;
STInit(&st);
STPush(&st,end);
STPush(&st,begin);
while(!STEmpty())
{
int left = STTop(&st);
STPop(&st);
int right = STTop(&st);
STPop(&st);
int keyi = _quicksort3(nums,left,right); //这里是两路划分,也可以三路划分,结合情况分析.
if(keyi-begin > 1)
{
STPush(&st,keyi-1);
STPush(&st,begin);
}
if(end-keyi > 1)
{
STPush(&st,end);
STPush(&st,keyi+1);
}
}
STDestroy(&st);
}
快排的优劣势:
优势:
性能高效;
适用性广;
劣势:
排序不稳定;
受数据初始状态影响,性能可能下降,性能不稳定;
计数排序
时间复杂度:O(N+range)
空间复杂度:O(range)
稳定性:稳定
动图演示:
算法思路:
|------------------------------------|
| 该算法采用哈希映射的策略,将数组的元素映射到下标,是非常高效的算法; |
|-----------------------------|
| 但是局限性也很明显,该算法只适用于数据集中的整数排序; |
cpp
void Count_sort(int* nums,int n)
{
int min = nums[0];
int max = nums[0];
for(int i = 1; i < n; i++)
{
if(min > nums[i])
min = nums[i];
if(max < nums[i])
max = nums[i];
}
int range = max-min+1;
int *tmp = (int*)calloc(range,sizeof(int));
for(int i = 0; i < n; i++)
{
tmp[nums[i]-min]++; //映射
}
int j = n-1;
for(int i = range-1; i >= 0; i--)
{
while(tmp[i]--)
{
nums[j--] = min+i; //还原,既排序
}
}
}
计数排序的优劣势:
优势:
排序稳定;
时间复杂度取决于range,如果range小于N,则效率高于所有其它排序;
无需比较,直接下标映射;
劣势:
空间复杂度高;
只适用于整数且范围较集中的数据;
希望该片文章对您有帮助,请点赞支持一下吧😘💕