常见的算法有:插入类型、选择类型、交换类型和归并类型,末尾加个计数排序。细分又可分为以下几种排序思想,这篇文章将带领大家一遍解决这些排序算法,期待我们都能有所收获。
目录
一、插入排序
1.直接插入排序
大致思路
?排序思想
图解
代码实现
注意事项---插入值最小问题
解决方法
总结
2.希尔排序?( 缩小增量排序 )
预排序图解
代码实现--3层循环预排序
代码实现--两层循环希尔排序
总结
二、选择排序
1.直接选择排序?
图解
优化
代码实现
掉包问题
总结
2.堆排序
大体思路
图解
代码实现
三、交换排序
1.冒泡排序
思路
图解
代码实现
总结
2.快速排序?
递归代码实现
2.1 Hoare法
图解
实现代码
注意事项
2.2 挖坑法
图解
代码实现?
2.3 前后指针法
图解
代码实现
2.4 快排三数取中优化
代码实现
2.5 快排小区间优化
代码实现
2.6 快速排序非递归实现?
思想
图解
代码实现
四、归并排序?
递归实现
图解
思路
代码实现
分割时死循环问题
?拷贝问题
非递归实现
图解
注意事项?
代码实现
计数排序?
图解
缺点
改进图解
代码实现
一、插入排序
把待排序数按其的大小插入到一个已经排好序的有序序列中,直到所有的数插入完为止,得到一个新的有序序列
1.直接插入排序
大致思路
给定一组数据n个,
从第二个开始,先排序前两个;
再从第三个开始,排序前三个;
从第四个开始,排序前四个;
……? ? ? ……
到第n个,排序前n个。
开始的数据为要插入的数据,前面的数据都为有序的数据
?排序思想
要插入的值若比前面的数小
保留插入值,前面的值向后覆盖
遇到比插入值小的数,停止覆盖
最后将本应该被覆盖的位置替换为要插入的值
图解
代码实现
void InsertSort(int* a, int n)
{
for (int i = 0;i<n-1;i++)
{
//end为区间下标,从0开始
int end=i;
//先定义一个变量,保存要插入的值,方便覆盖
int num = a[end + 1];
//一趟排序
while (end >= 0)
{
//如果插入的值比前一个值小,区间从end开始向后覆盖
if (num<a[end])
{
a[end + 1] = a[end];
end--;
}
//否则,空位end+1=要插入的值
else
{
//考虑到边界情况,这步可直接省略
/*a[end+1] = num;*/
break;
}
}
//边界情况:若插入值最小,end小于0,while结束,插入值未放入数组中
//须手动放入
a[end + 1] = num;
}
}
注意事项---插入值最小问题
若插入值最小,end将会小于0,while循环会结束
导致最终的保存的num插入值未替换入数组中
解决方法
将替换步骤放入循环结束后,可以避免数据丢失的问题
总结
元素集合越接近有序,直接插入排序算法的时间效率越高 时间复杂度:O(N^2) 空间复杂度:O(1) ?
2.希尔排序?( 缩小增量排序 )
先选定一个整数,把待排序文件分组,间隔为这个整数的数据分为一组,对每一组的数据进行排序;
减小这个整数,再分组进行排序;
当这个整数为1时,再分组进行排序,结束排序
最后一次整数为1时,排序相当于时一层直接选择排序
前面进行多次分组排序是为了让数据愈加有序(预排序过程)
数据越有序,直接插入排序算法的时间效率越高
预排序图解
间隔为4时,预排序
间隔为2时,预排序
间隔为1时,最终排序
排序思想依旧按照直接插入排序的为准
这里的固定的整数不好取,上述只是以4为例
?实现时,我们的gap选取gap=gap/3-1
代码实现--3层循环预排序
由图我们得到:当gap为多少,那么数据就可以分为多少组
我们分别对每组进行排序,此时gap以3为例
进行间隔为3时的预排序
//三层循环,预排序
void ShellSort(int* a, int n)
{
int gap = 3;
//控制gap组
for (int j = 0;j<gap;j++)
{
//控制间隔为gap的一组数据
for (int i = j; i<n - gap; i += gap)
{
int end = i;
int num = a[end + gap];
while (end >= 0)
{
if (num<a[end])
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = num;
}
}
}
代码实现--两层循环希尔排序
嫌循环嵌套太多层,我们可以减少一层
gap=gap/3-1
减少循环gap次的循环
当gap=1时,直接就完成最后一次直接插入排序
完成预排序和最终排序
//两层循环,预排序
void ShellSort(int* a, int n)
{
//gap不是固定的值
int gap = n;
//一层循环控制gap,大于1预排序
//gap等于1时直接插入排序
while (gap>1)
{
gap = gap / 3 + 1;
for (int i = 0; i<n - gap; i++)
{
int end = i;
int num = a[end + gap];
while (end >= 0)
{
if (num<a[end])
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = num;
}
}
}
总结
是对直接插入排序的优化 gap > 1时都是预排序,目的是让数组更接近于有序 ?
二、选择排序
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
1.直接选择排序?
在元素有n的集合中选择最大或最小的元素 若它不是这组元素中的最后一个或第一个元素
则将它与这组元素中的最后一个或第一个元素交换 在剩余的n-1个元素中,重复上述步骤
直到集合剩余1个元素
图解
优化
每次不仅找到最小,而且找到最大
最小的不在第一个放第一个;
最大的不在最后一个放最后一个;
再剩余的数据中重复上述过程;
直到集合剩余一个元素为止
代码实现
//直接选择排序
//每个数放到固定的对应位置
void SelectSort(int* a, int n)
{
int left = 0, right = n - 1;
while (left<right)
{
int mini = left;
int maxi = left;
//遍历一次选出最大数和最小数下标
for (int i = left;i<=right;i++)
{
if (a[i]<a[mini])
{
mini = i;
}
if (a[i]>a[maxi])
{
maxi = i;
}
}
//将最大值和最小值放在最左边和最右边
//要注意当left=maxi时,值会被掉包
Swap(&a[left],&a[mini]);
//防掉包,如果left和maxi重叠,修正maxi
if (left==maxi)
{
maxi = mini;
}
Swap(&a[right], &a[maxi]);
//更新下标
left++;
right--;
}
}
掉包问题
因为我们时找到最大最小值得下标,对其上的元素进行交换
因此当最大值在第一个下标位置时,会出先如下图问题
找出最小,将他先和最左交换
此时最大值已经被换走了
但是下标在第一个位置处未更新?
将最大值放到最后位置
最大值下标未更新,会将最小值放最后
结果第一个不是最小,最后一个不是最大
总结
效率不是很好,很少使用 时间复杂度:O(N^2) 空间复杂度:O(1) ?
2.堆排序
之前一篇文章详解过堆排序,非常推荐大家看一下,也可以吐槽一下呀
不是动图,but很细,3000字噢:堆排序详解+TOP-K问题
大体思路
a.升序建大堆,降序建小堆
b.在原数组中通过向下调整建堆
c.交换堆顶元素和最后一个叶子结点数据
d.对除了最后一个数据外的其他数据向下调整
重复cd步骤
图解
对这组数据中向下调整建堆:升序建大堆
?? 9?? 8?? 5?? 6?? 4?? 7?? 1?? 3
从第一个非叶子节点开始向下调整建大堆
调好后从前一个节点开始,继续向下调整
找到节点更新至堆顶为止
?
? ? ? ??
?? ? ? ? ?
?? ? ? ? ?
刚接触二叉树的小伙伴可以先看
这篇文章噢!!!:堆排序详解+TOP-K问题
代码实现
// 向下调整建堆
void AdjustDown(int* a, int root, int n)
{
int child = root * 2 + 1;
while (child < n)
{
if (child + 1 <n && a[child + 1] < a[child])
{
child++;
}
if (a[child] < a[root])
{
Swap(&a[child], &a[root]);
root = child;
child = root * 2 + 1;
}
else
{
break;
}
}
}
//时间复杂度O(N*logN)空间复杂度O(1)
void HeapSort(int* a, int n)
{
//升序建大堆
//降序建小堆
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(a, i, n);
}
//最后一个数据的下标
//排序
size_t end = n - 1;
while (end>0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, 0, end);
end--;
}
}
三、交换排序
基本思想:所谓交换,依次比较元素数据,根据他们的大小情况交换他们的位置。
特点:将较大的数据向序列的尾部移动,较小的数据向序列的头部移动。
1.冒泡排序
冒泡排序,我们的老朋友了,这里我们简单复习一下
思路
相邻两个数排序
小的在左边,大的在右边
从左向右依次两两比较
一趟过后就将最大值放在了最后一位
对最大值之前的数据重复上述过程
实现整体排序
图解
0?
代码实现
// 冒泡排序
//升序为例
void BubbleSort(int* a, int n)
{
//大循环
for (int i = 0;i<n-1;i++)
{
//优化,若无交换,有序,直接结束比较
int flag = 0;
//单趟循环
for (int j = 0;j<n-i-1;j++)
{
if (a[j]>a[j+1])
{
flag = 1;
Swap(&a[j],&a[j+1]);
}
}
if (flag==0)
{
break;
}
}
}
总结
冒泡排序是一种非常容易理解的排序
时间复杂度:O(N^2) 空间复杂度:O(1)
2.快速排序?
基本思想:选一个数为标准,将整个数据分割成左右两个序列,左边的数据都小于这个数,右边的数据都大于这个数据;再左右两个序列中重复上述过程,实现整体排序。
这是一个递归思想。
递归代码实现
不管下面哪种方法
keyi接收的都是左下标和右下标相遇的位置
方便再次调用时确定新的左右序列
void QuickSort(int* a,int begin,int end)
{
if (begin>=end)
{
return;
}
//hoare第一趟找出中间
int keyi = PartSort1(a,begin,end);
//挖坑法找
/*int keyi = PartSort2(a, begin, end);*/
//前后指针法
/*int keyi = PartSort3(a, begin, end);*/
//对两边区间进行排序
QuickSort1(a,begin,keyi-1);
QuickSort1(a,keyi+1,end);
}
时间复杂度:O(N*logN) 空间复杂度:O(logN)
2.1 Hoare法
图解
?
实现代码
//hoare快速排序-单趟
int PartSort1(int* a,int left,int right)
{
int keyi = left;
while (left<right)
{
//=是防止死循环
//left<right是防止越界
//右边走找小
while (left<right&&a[right]>=a[keyi])
{
right--;
}
//左边走找大
while (left<right&&a[left] <= a[keyi])
{
left++;
}
//交换左右
Swap(&a[left],&a[right]);
}
//相等即相遇,结束,交换相遇位置和keyi上的值
Swap(&a[keyi],&a[right]);
return left;
}
注意事项
找大小时,left<right,防止越界
返回的是左下标,此时和右下标在同一位置
左边为基准,必须右边先走
确保相遇是比基准小的值
右边为基准,必须左边先走
确保相遇是比基准大的值
这样才能满足交换的条件
2.2 挖坑法
先将第一个数据存放在变量key中,形成一个坑位,从右边找比key小的填入坑位中,再从右边找比key大的,填到坑位中,重复执行上述过程,当相遇时,将key填入相遇时的坑位。
图解
?
代码实现?
?
//挖坑法快速排序-单趟
//左边或右边是一个坑,右边找小填左边,左边找大填右边
int PartSort2(int* a, int left, int right)
{
int key = a[left];
int pit = left;
while (left<right)
{
//右边先走,找小
while (left<right&&a[right]>=key)
{
right--;
}
//补上坑
a[pit] = a[right];
//更新坑
pit = right;
//左边后走,找大
while (left<right&&a[left] <= key)
{
left++;
}
a[pit] = a[left];
pit = left;
}
a[pit] = key;
return pit;
}
2.3 前后指针法
慢指针指向开头,快指针指向慢指针的后一个位置
确定一个基准值,快指针找比key小的值
慢指针找比key大的值
起始的时候慢指针得值=key的值
要先向后移动一位
图解
?
?
代码实现
//前后指针法快指针找小
//key在左
int PartSort3(int* a, int left, int right)
{
int key = a[left];
int prev = left;
int cur = left + 1;
while (cur <= right)
{
//如果相等不用交换
if (a[cur]<=key && a[++prev]!=a[cur])
{
Swap(&a[prev], &a[cur]);
}
cur++;
}
Swap(&a[prev],&a[left]);
return prev;
}
2.4 快排三数取中优化
注意:若对有序数据进行快排,选择key为 最大或最小,那么时间复杂度为O(N^2) 栈帧可能会溢出 对此进行三数取中优化
三数取中:
取最左边,最右边和中间值
选其中不大不小的值作为基准
代码实现
int GetMid(int* a, int left, int right)
{
//可能会出界
/*int mid = (left+right) / 2;*/
int mid = left + (right - left) / 2;
if (a[left]<a[mid])
{
if (a[mid] < a[right])
{
return mid;
}
else if (a[left]>a[right])
{
return left;
}
else
{
return right;
}
}
else
{
if (a[mid]>a[right])
{
return mid;
}
else if (a[left]>a[right])
{
return right;
}
else
{
return left;
}
}
}
在上述三种方法中,确定基准前,采用三数取中的方法选出基准,与最左值进行交换,依旧从最左边开始进行排序。可以有效避免栈帧溢出情况。
//选出中数交换位置
int mid = GetMid(a,left,right);
Swap(&a[mid],&a[left]);
//key在左边的数变为选的中数
int keyi = left;
2.5 快排小区间优化
区间很小时,可以不用使用递归思路排序
减少栈帧的开辟 直接使用插入排序对小区间排序, 减少递归调用?
代码实现
void QuickSort2(int* a, int begin, int end)
{
if (begin >= end)
{
return;
}
//当设置区间条件,进行插入排序
if (end - begin<10)
{
InsertSort(a+begin,end-begin+1);
}
else
{
//hoare第一趟找出中间
int keyi = PartSort1(a, begin, end);
//挖坑法找
/*int keyi = PartSort2(a, begin, end);*/
//前后指针法
/*int keyi = PartSort3(a, begin, end);*/
//对两边区间进行排序
QuickSort2(a, begin, keyi - 1);
QuickSort2(a, keyi + 1, end);
}
}
2.6 快速排序非递归实现?
使用栈来进行快速排序的非递归实现
思想
将始末下标入栈;
当栈不为空时,重复下面步骤:
保存左右下标,
后进先出原则进行下标出栈;
选择一种方法进行排序,得到中间下标;
通过中间下标确定两边区间;
再对两边区间左右下标进行入栈
图解
这里keyi是为了方便自己设的
[5,6],[4,5]设带入方法后已经有序
再算keyi时,就是两边序列排序的过程
栈的思想如图所示
?
代码实现
//快速排序---非递归
//用栈实现
void QuickSort3(int* a, int begin, int end)
{
ST st;
StackInit(&st);
//将他们的下标入栈
StackPush(&st,begin);
StackPush(&st,end);
while (!StackEmpty(&st))
{
//后进先出
int right = StackTop(&st);
StackPop(&st);
int left = StackTop(&st);
StackPop(&st);
//选一种快排方法
int keyi = PartSort3(a, left, right);
//对两边区间进行入栈[left,keyi-1][keyi+1,right]
if (left<keyi-1)
{
StackPush(&st,left);
StackPush(&st,keyi-1);
}
if (keyi+1<right)
{
StackPush(&st, keyi+1);
StackPush(&st, right);
}
}
}
四、归并排序?
递归实现
将已有序的子序列合并,得到完全有序的序列;
即先使每个子序列有序,再使子序列段间有序。
若将两个有序表合并成一个有序表,称为二路归并。
图解
思路
将序列分割成单个的有序序列
两个区间元素相互比较,归并写入临时区间中
最后用memcpy函数
将临时区间的数据拷贝到原序列中
重复上述过程,得到有序的临时序列
代码实现
void _MergeSort(int* a, int begin, int end, int* tmp)
{
if (begin>=end)
{
return;
}
int mid=(begin+end)/2;
//区间分割
//不能是[begin,mid-1]和[mid,end],会存在死循环
_MergeSort(a, begin, mid,tmp);
_MergeSort(a, mid+1, end, tmp);
int begin1 = begin, end1 = mid;
int begin2 = mid+1, end2 = end;
//确定归并时存放数据的起始位置
int index = begin;
//写的是继续的条件
//一次比较区间中数据的大小,一个区间写完后
//需要将没结束的区间中的剩余数据写入数组中
while (begin1<=end1 && begin2<=end2)
{
if (a[begin1]<a[begin2])
{
tmp[index++] = a[begin1++];
}
else
{
tmp[index++] = a[begin2++];
}
}
//没结束的区间直接归并到临时数组中
while (begin1<=end1)
{
tmp[index++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[index++] = a[begin2++];
}
//将排好序的数据拷贝回原空间
memcpy(a+begin,tmp+begin,(end-begin+1)*sizeof(int));
}
//归并排序
void MergeSort(int* a, int n)
{
//要用到临时数组
int* tmp = (int*)malloc(sizeof(int)*n);
assert(tmp);
//调用子函数进行区间的分割,合并
_MergeSort(a, 0, n - 1, tmp);
free(tmp);
}
分割时死循环问题
?
?拷贝问题
注意拷贝时的起始位置
因为拷贝区间不都是从原序列和临时序列的起始开始
必须分别加上存放数据时的起始下标begin
非递归实现
直接控制间隔进行归并排序
图解
注意事项?
?边界问题:不同gap下
两个比较区间的起始位置和末尾位置仔细控制
并且有越界问题需要判断
第一个序列起始位置不存在越界问题
第一个序列末尾位置存在越界问题
需要判断并纠正
第二个区间起始位置可能越界
需判断并修改为不存在的区间即可
第二个区间末尾位置可能越界
需要判断并纠正
?
代码实现
//归并排序非递归
//直接控制间隔进行排序归并
//间隔注意不能直接按2的倍数算,若数组个数为奇数,存在越界问题
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int)*n);
int gap = 1;
//通过调整间距gap,进行归并排序
//分组归并,间距为gap的是一组
while (gap<n)
{
//控制区间边界,对初始间隔进行排序
for (int i = 0; i<n; i += 2 * gap)
{
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
//越界,须纠正下标
//end1越界,修正
if (end1 >= n)
end1 = n - 1;
//begin2越界,右边区间不存在
//修改成不存在的区间
if (begin2 >= n)
{
begin2 = 0;
end2 = -1;
}
//end2越界,修正
if (begin2<n && end2 >= n)
end2 = n - 1;
//直接进行归并
int index = i;
//写的是继续的条件
//一次比较区间中数据的大小,一个区间写完后
//需要将没结束的区间中的剩余数据写入数组中
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1]<a[begin2])
{
tmp[index++] = a[begin1++];
}
else
{
tmp[index++] = a[begin2++];
}
}
//没结束的区间直接归并到临时数组中
while (begin1 <= end1)
{
tmp[index++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[index++] = a[begin2++];
}
}
//将排好序的数据拷贝回原空间
memcpy(a, tmp, n*sizeof(int));
//更新间距gap
gap *= 2;
}
free(tmp);
}
计数排序?
统计相同元素出现次数
根据统计的结果将序列回收到原来的序列中
适用于比较集中的数据
图解
缺点
空间浪费太大,若只有三个数:5? ?28? ?1
那就需要开辟29个空间,太过奢侈了
因此计数排序只适合数值集中的序列?
并且绝对个数进行开辟空间也是不合理的
改进图解
?
代码实现
//计数排序-相对映射
//适用于比较集中的数据
//时间复杂度O(rang+N)
//空间复杂度O(rang)
void CountSort(int* a, int n)
{
int min=a[0], max=a[0];
for (int i = 1;i<n;i++)
{
if (a[i]<min)
{
min = a[i];
}
if (a[i]>max)
{
max = a[i];
}
}
//相对个数--需要的空间大小
int rang = max - min + 1;
int* count =(int*)malloc(sizeof(int)*rang);
assert(count);
//初始化为0
memset(count,0,sizeof(int)*rang);
//计数
for (int i = 0;i<n;i++)
{
//a[i]-min是相对位置,++是计数,
//位置上都是0,出现一次++一次
count[a[i] - min]++;
}
//排序 -- 对计数的数组进行排序
int j = 0;
for (int i = 0;i<rang;i++)
{
//看count中位置上的次数,出现几次写几次
while (count[i]--)
{
//j记录原数组的位置
//i+min=原数值
a[j++] = i + min;
}
}
}
肝完了,欢迎大伙评论指正 ,感谢点赞收藏的伙伴!!!!
?
|