前言
这次复习数据结构和C++,总结了常见的几种排序算法,虽然C++中不需要自己实现排序算法,但熟悉各种排序算法的核心思想还是重要的,同时也方便自己将来复习回顾。
注意:自己是在一个项目里实现的,因此用的vector作为保存元素的容器,不过不影响大局,与数组同理
冒泡排序
核心思想:
从左到右遍历,相邻两个元素相互比较,然后按符合升序(降序)来决定是否交换两个元素,即在一轮次遍历中找到一个最大的放到最后位置,下一次找第二大的放置在倒数第二个位置上。
即是一个二重循环,外层循环表示进行多少轮的比较(一轮找出一个最大的),内层循环就在这一轮的比较中不断的比较以及交换相邻的两个元素。
优化方法:
- 每一轮找出一个最大的放在末尾,在之后的比较中就不需要再比较这个元素了,可以减少内层循环的比较次数
- 注意到,当交换发生,说明还没有达到有序;如果有一轮遍历过程中没有发生交换,说明已经有序,可以提前退出,不需要再继续进行循环比较的过程
代码
void buble_sort(vector<int>& a)
{
auto len = a.size();
for (size_t i = 0; i < len-1; ++i) //n个数只需要比较n-1次即可 也可从下面数组越界a[j+1]来考虑
{
for (int j = 0; j < len - i-1; ++j) //第一个优化方法 不需要比较后面的已经排序的数
{
if (a[j] > a[j + 1])
swap(a[j], a[j + 1]);
}
}
}
第二种优化方法的代码:
void buble_sort(vector<int>& a) //主要增加flag,其余大致相同
{
bool flag = false; //主要增加的flag标识 用来判断是否有序
while (!flag) //无序,进入算法
{
flag = true; //先假定已有序
for (size_t i = 1; i < len ; ++i)
{
if (result[i-1] > result[i])
{
swap(result[i], result[i - 1]);
flag = false; //若发生交换则说明是无序的 ,重置flag
}
}
len--;
}
}
选择排序
核心思想:
每一趟在未排序的记录中选取最小的记录作为有序序列中第i(i递增)个记录
即第一轮选出最小的元素,然后和a[0](第一个位置)交换,成为第一个有序元素,再在剩余的元素中选出最小的,和a[1]交换(第二个位置),再在剩余的数中重复上述过程。
代码核心:
在每一轮次中找出最小元素的位置,然后交换元素
代码:
void select_sort(vector<int>& a)
{
auto len = a.size();
for (size_t i = 0; i < len - 1; ++i) //每一轮次 轮次数
{
size_t min = i;
for (size_t j = i; j < len ; ++j) //找出最小元素的位置 从i开始即可 前面的是已经选择的最小的
{
if (a[j] < a[min])
{
min = j;
}
}
swap(a[min], a[i]); //找到最小位置 和第i个元素交换
}
}
直接插入排序
核心思想:
初始化为空的有序元素序列,然后依次插入每一个元素,插入时就按照关键值大小(本算法即为数值的大小)插入相应的位置中
代码核心:
序列分为有序和无序两部分,取第一个元素作为初始有序区,然后第二个开始,依次插入到有序区的合适位置,直到排好序
代码
void insert_sort(vector<int>& a)
{
auto len = a.size();
for (size_t i = 1; i < len; ++i) //从第二个数字(无序区)开始遍历,相当于第一个数作为初始有序区
{
for (size_t j = i; j > 0; --j) //从无序区的选出一个数 然后插入到有序区(不停的比较换序)
{
if (a[j] < a[j - 1])
swap(a[j], a[j - 1]);
}
}
}
快速排序
核心思想:
递归 分治
在一个序列中选出一个基准数,以基准元素把序列分成两部分(小于该元素和大于该元素的)(确定了该元素的最终位置),对着两部分再进行递归调用快排算法。
代码核心:
就是找到一个基准(轴值),并将元素序列依据基准分为大小两个部分,找到基准所在的位置
找到一篇比较好的博客 学习下(挖坑填数+分治法):博客
其主要方法:
对挖坑填数进行总结
1.i =L; j = R; 将基准数挖出形成第一个坑a[i]。
2.j–由后向前找比它小的数,找到后挖出此数填前一个坑a[i]中。
3.i++由前向后找比它大的数,找到后也挖出此数填到前一个坑a[j]中。
4.再重复执行2,3二步,直到i==j,将基准数填入a[i]中。
自己总结
实际上就是前后换位置,目的是大于基准的数都在后面,后面的坑要大数来填(从前遍历找大数),前面的坑要小数来填(从后遍历找小数)
最后就是i=j时 ,j后面的全都大于基准,i前面的全部小于基准
代码(注释很详细)
void quick_sort(vector<int>& a, int left, int right)
{
if (left >= right) { return; } //判断排序个数是否大于1 否则直接退出排序
if (left < right)
{
size_t i = left, j = right;
int X = a[i]; //将第一个数作为基准
while(i<j)
{
while (i < j&&a[j] >= X) //从后面开始遍历 大于X则不管 继续
{
--j;
}
if (i < j) //出现了a[j]<X 即有一个小于X的数出现了
{
a[i++] = a[j]; //用这个小于X的数去占坑a[i] 同时i++ 此时j的位置 需要一个大于X的数来占坑
}
while (i < j&&a[i] < X) //现在需要有一个大于X的数 从i开始遍历 小于则不管 继续
{
++i;
}
if (i < j) //出现了a[i]>X ,即有一个大于X的数出现了
{
a[j--] = a[i]; //用这个大于X的数去占刚才空出的 j 的位置的坑 同时j-- 此时又需要小于X的数去填占刚才i的坑 ,如此往复
}
}
a[i] = X; //while(i<j) 的循环出来的时候 就是i=j时 此时j后面的数 全都大于X i前面的数全部小于X 完成任务
//以上完成找基准 按基准前后分割 确定基准的位置
quick_sort(a, left, i - 1); //递归调用即可
quick_sort(a, i + 1, right);
}
}
归并排序
核心思想:
分治+递归
将一个序列分成两个子序列,(两个子序列再不断递归调用(分成更小的子序列再合并)算法),子序列排序完成后,再合并两个有序子序列
代码核心:
重点是合并操作(合并两个有序序列),分的步骤就是不停的递归调用(每次都是二分之一部分)来分,直到最后每一个子序列只有一个元素(自然就有序了),然后不停的合并。
代码:
void merge_sort(vector<int> &a, size_t first, size_t last, vector<int>& tmp)
{
if (first < last)
{
size_t mid = (first + last) / 2; //二分之一分
merge_sort(a, first, mid, tmp); //前半部分递归调用 tmp相当于一个暂时中转的容器
merge_sort(a, mid + 1, last, tmp); //后半部分
merge(a, first, mid, last, tmp); //最为关键的合并部分
}
}
void merge(vector<int>&a, size_t first, size_t mid, size_t last, vector<int>& tmp) //合并操作 将两个有序子序列合并成一个
{
size_t i = first, j = mid + 1,k=0;
while (i <= mid&&j <= last)
{
if (a[i] <= a[j]) //选择小的元素并入tmp
{
tmp[k++] = a[i++];
}
else
{
tmp[k++] = a[j++];
}
} //跳出循环时说明 有一个子序列遍历完成 只需要将另外一个续到tmp上即可
while (i <= mid)
{
tmp[k++] = a[i++];
}
while (j <= last)
{
tmp[k++] = a[j++];
}
for (size_t i = 0; i < k; ++i) //最后结果在tmp中 转移到a
{
a[first + i] = tmp[i];
}
}
总结
这次总结了常用到的几种排序算法,参考各种博文,自己总结了算法,代码核心要点以及具体的C++实现
简单排序:冒泡排序,选择排序,直接插入排序
高级排序:快速排序,归并排序
待补充:
- 还有希尔排序以及堆排序没有总结实现
- 7种算法都实现后,要放在一起进行比较总结,涉及时间复杂度分析,稳定性分析,应用场景分析