排序算法在我们的现实生活中应用非常广泛,我们应该很好的掌握。下面我们将对7种常见算法逐个进行介绍:
常见的排序算法:
1.插入排序
(1)直接插入排序
思路分析:
思路分析:
①在长度为N的数组,将数组中第i [1~(N-1) ] 个元素,插入到数组 [0~i] 适当的位置上。
②在排序的过程中当前元素之前的数组元素已经是有序的了。
③在插入的过程中,有序的数组元素,需要向右移动为更小的元素腾出空间,直到为当前元素找到合适的位置。
过程动态图展示:
代码实现:
//插入排序
void InsertSort(int *a,size_t n)
{
assert(a);
for(size_t i = 0;i < n-1;i++)//注意这里的边界控制,如果不是 n-1,就会出现越界
{
int end = i; //每一个有序区间的最后一个元素所在的位置为end
int tmp = a[end+1]; //每次需要排序的元素先保存在tmp里面
while(end >= 0)
{
if(a[end] > tmp)
{
a[end+1] = a[end];
--end;
}
else
{
break;
}
}
a[end+1] = tmp;
}
}
总结:时间复杂度平均为O(n^2),最坏为O(n^2),最好为O(n),空间复杂度为O(1),排序算法稳定。
(2)希尔排序
希尔排序是对插入排序最坏的情况的改进,主要是减少数据移动次数,增加算法的效率。
思路分析:
先比较距离远的元素,而不是像简单交换排序算法那样先比较相邻的元素,这样可以快速减少大量的无序情况,从而减轻后续的工作。被比较的元素之间的距离逐步减少,直到减少为1,这时的排序变成了相邻元素的互换。
代码实现:
//希尔排序
void ShellSort(int *a,size_t n)
{
assert(a);
int gap = n;
while(gap > 1)
{
gap = gap/3+1; //控制分组
for(size_t i = 0;i < n-gap;i++)//注意边界条件,避免越界
{
int end = i;
int tmp = a[end+gap];
while(end >= 0)
{
if(a[end] > tmp)
{
a[end+gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end+gap] = tmp;
}
}
}
总结:希尔排序通过将比较的全部元素分为几个区域来提升插入排序的性能。这样让一个元素可以一次性的朝最终的位置前进一大步。然后算法再取越来越小的步长进行排序,算法最后一步就是普通的插入排序,但是到了这步,需要排序的数据几乎接近有序了(此时插入排序较快),时间复杂度平均为O(nlog2n),最坏为O(nlog2n),空间复杂度为O(1),排序算法不稳定。
步长的选择是希尔排序的重要部分,只要最终的步长为1任何步长序列都是可以工作的(且步长要小于数组长度)。算法开始以一定的步长进行排序。然后会继续以一定的步长进行排序,最终算法以步长为1进行排序。当步长为1进行排序时,算法就变为插入排序,这就保证了数据一定会被排序。
2.选择排序算法
(1)选择排序
思路分析:第一趟从n个元素的数据序列中选出关键字最小/大的元素并放在最前/后位置,下一趟从n-1个元素中选出最小/大的元素并放在最前/后位置。以此类推,经过n-1趟完成排序。
过程动态图:
代码实现(此处代码对直接排序进行了有优化,遍历一次同时选出最大的和最小的,最大的放在最右边,最小的放在最左边,排序范围缩减)
//选择排序
void SelectSort(int *a,size_t n)
{
assert(a);
size_t left = 0,right = n-1;
while(left < right)
{
size_t min = left,max = left;
for(size_t i = left;i <= right;i++)
{
if(a[i] > a[max])
{
max = i;
}
if(a[i] < a[min])
{
min = i;
}
}
swap(a[min],a[left]);
if(max == left) //最大值在最左边
max = min;
swap(a[max],a[right]);
--right;
++left;
}
}
总结:直接选择排序的最好时间复杂度号最坏时间复杂度都是O(n^2),因为即使数组一开始就是正序的,也需要将两重循环进行完,平均时间复杂度也是O(n^2),最好的时间复杂度为O(n^2),空间复杂度为O(1),因为不占用多余的空间。直接选择排序是一种原地排序并且不稳定的排序算法,优点是实现简单,占用空间小,缺点是效率低,时间复杂度高,对于大规模的数据耗时较长。
(2)堆排序
思路分析:
①将长度为n的待排序的数组进行堆有序化构造成一个大顶堆
②将根节点与尾节点交换并输出此时的尾节点
③将剩余的n -1个节点重新进行堆有序化
④重复步骤2,步骤3直至构造成一个有序序列
(升序构建小堆,降序构建大堆)
过程动态图
代码实现:
//堆排序
void AdjustDown(int *a,size_t root,size_t n)//向下调整算法
{
size_t parent = root;
size_t child = parent*2+1;//下标为0的为第一个孩子,所以parent*2+1为下标为左孩子
while(child < n)
{
if(child+1 < n && a[child+1] > a[child])
{
++child;//让child指向较大的那个孩子
}
if(a[child] > a[parent])
{
swap(a[child],a[parent]);
parent = child;
child = child*2+1;
}
else
{
break;
}
}
}
void HeapSort(int *a,size_t n)//升序,建大堆
{
assert(a);
int i;
//从第一个非叶子节点开始,做向下调整,调整完第一个元素为最大元素
for(i = (n-2)/2;i >= 0;--i)
{
AdjustDown(a,i,n);
}//大堆建成
int end;
//交换第一个元素和最后一个元素,然后把交换后的第一个元素往下调整,再成大堆
//--end,即是缩小范围,每次把最大的换到end的位置
for(end = n-1;end >= 0;--end)
{
swap(a[end],a[0]);
AdjustDown(a,0,end);
}
}
总结:堆排序的最好和最差情况时间复杂度都为O(nlog2n),平均时间复杂度为O(nlog2n),空间复杂度为O(1),排序算法不稳定,无需使用多余的空间帮助排序。优点是占用空间较小,时间复杂度较低,缺点是实现较为复杂,并且当待排序序列发生改动时,哪怕是特别小的改动,都需要调整整个堆来维护堆的性质,维护开销较大。
3.交换排序
(1)冒泡排序
思路分析:
①比较相邻的元素。如果第一个比第二个大,就交换他们两个。
②对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
③针对所有的元素重复以上的步骤,除了最后一个。
④持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
过程动态图:
代码实现:
//冒泡排序
void BubbleSort(int *a, size_t n)
{
for(int i = 0; i < n; i++)
{
for(int j = 0; j < n-1-i; j++)
{
if(a[j] > a[j+1])
{
swap(a[j],a[j+1]);
}
}
}
}
//优化版本
void BubbleSort(int *a, size_t n)
{
for(int i = 0; i < n; i++)
{
bool flag = true;//优化标记
for(int j = 0; j < n-1-i; j++)
{
if(a[j] > a[j+1])
{
swap(a[j],a[j+1]);
flag = false;
}
}
if(flag)//本次排序都没有发生交换,说明已经有序了
{
break;
}
}
}
总结:冒泡排序算法最坏的情况和平均复杂度是O(n^2),最好时间复杂度为O(n),空间复杂度为O(1),排序算法稳定。
(2)快速排序
本质上快速排序把数据划分成几份,所以快速排序通过选取一个关键数据,再根据它的大小,把原数组分成两个子数组:第一个数组里的数都比这个主元数据小或等于,而另一个数组里的数都比这个主元数据要大或等于。
快排有三种方法:①左右指针法 ②挖坑法 ③前后指针法
void QuickSort(int *a,int left,int right)
{
assert(a);
if(left >= right)
return;
//当区间比较小时,用插入排序以优化程序
//if(right - left < 5)
//{
// InsertSort(a,right-left+1);
//}
else
{
int div = PartSort3(a,left,right);//分区间
//[left,div-1] [div+1,right]
QuickSort(a,left,div-1);//递归子问题
QuickSort(a,div+1,right);
}
}
①左右指针法
//1.左右指针法
int PartSort1(int *a,int left,int right)
{
int& key = a[right]; //此处必须用引用才能达到后面用key作为交换值的效果
int begin = left;
int end = right;
while(begin < end)
{
while(begin < end && a[begin] <= key)//特别需要注意边界条件
++begin;
while(begin < end && a[end] >= key)
--end;
//走到此处,begin所指向的值比key大,end所指向的值比key小
if(begin < end)
swap(a[begin],a[end]);
}
//走到此处,begin和end相遇
swap(a[begin],key);//上面的key需要加上引用
return begin;
}
②挖坑法
//挖坑法
int PartSort2(int *a,int left,int right)
{
int begin = left;
int end = right;
int key = a[end];
int dig = right; //标记当前坑的位置
while(begin < end)
{
while(begin < end && a[begin] <= key)
++begin;
if(begin < end)
{
a[dig] = a[begin];
dig = begin;
}
while(begin < end && a[end] >= key)
--end;
if(begin < end)
{
a[dig] = a[end];
dig = end;
}
}
a[dig] = key;
return dig;
}
③前后指针法
//前后指针法
//选出key,若a[cur]<key,prev++;cur继续往后走,
//若a[cur]>key,prev不动,cur继续往后走,
//若再次遇到a[cur]<key,prev++;此时的cur和prev已经不指向同一个位置了,
//交换prev和cur所对应的值。
int PartSort3(int *a,int left,int right)
{
int cur = left;
int prev = left-1;
int& key = a[right];
while(cur < right)
{
//prev == cur时,其实也要交换的,不过此时它俩是指向同一个值
if(a[cur] < key && ++prev != cur)
{
swap(a[prev],a[cur]);
}
++cur;//不管怎样,cur都要往后走
}
//cur走到了key所在的位置了
swap(a[++prev],key);
return prev;
}
总结:平均时间复杂度为O(nlog2n),最坏时间复杂度为O(n^2),空间复杂度为O(nlog2n),排序算法不稳定。
(3)快速排序优化
①三数取中法
当我们每次选取key时,如果key恰好是最大或者最小值,此时快排效率会很低,为了避免这种情况,我们对快排选取key值进行优化。
优化思路:依旧选取最右边的值作为key,但是在选取前,我们把数组中最左边,中间,最右边位置的三个数取出来。找到这三个数中值排在中间的一个。把该值与最右边位置的值进行交换。此时key的值不可能是最大值或者最小值。
②随机值法。
num = rand()%N 把num位置的数与最右边的值交换,key依旧去最右边的值。这种方法也可以,但是太随机了,特殊场景会导致不可控的结果。
③小区间优化
快排是利用递归栈帧完成的,如果递归深度太深会影响效率。切割区间时,当区间内元素数量比较少时就不用切割区间了,这时候就直接对这个区间采用直接插入法,可以进一步提高算法效率。
4.归并排序
思路分析:
当一个数组左边有序,右边也有序,那合并这两个有序数组就完成了排序。如何让左右两边有序了?用递归!这样递归下去,合并上来就是归并排序。
代码实现:
//归并排序
void _MergeSort(int *a, int left, int right)
{
assert(a);
if(left >= right)
return;
int mid = left + ((right-left) >> 1);
_MergeSort(a, left, mid);
_MergeSort(a, mid+1, right);
int *tmp = new int[right-left+1];//开辟同原数组大小的辅助空间
int begin1 = left;//左区间的开头
int begin2 = mid+1;//右区间的开头
int cur = 0;//辅助空间的开头
while(begin1 <= mid && begin2 <= right)
{
if(a[begin1] < a[begin2])
{
tmp[cur++] = a[begin1++];
}
else
{
tmp[cur++] = a[begin2++];
}
}
//将余下的直接放入辅助空间
while(begin1 <= mid)
{
tmp[cur++] = a[begin1++];
}
while(begin2 <= right)
{
tmp[cur++] = a[begin2++];
}
//将tmp数组的值全部拷贝到a数组里面
for(int i = 0; i < cur; i++)
{
a[left+i] = tmp[i];
}
delete[] tmp;//释放自己申请的空间
}
void MergeSort(int *a, size_t n)
{
assert(a);
_MergeSort(a, 0, n-1);
}
总结:平均和最坏、最好时间复杂度为O(nlog2n),空间复杂度O(n),排序算法稳定。
排序算法大总结: