目录
一、选择排序
1.基本思路:
(1)在寻找第 i 位置的元素时,求出第 i 位到第 n 位的最小值,并记录其位置为 k;
(2)交换第 i 位元素和第 k 位元素。
2.代码实现:
#include<iostream> #include<algorithm> using namespace std; #define N 2000 int n; int a[N]; int main() { cin >> n; for (int i = 0; i < n; i++) cin >> a[i]; for (int i = 0; i < n-1; i++) { int min_pos = i; for (int j = i+1; j < n; j++) { if (a[j] < a[min_pos]) min_pos = j; } swap(a[i], a[min_pos]); } for (int i = 0; i < n; i++) cout << a[i] << ' '; return 0; }
时间复杂度为:O()
【注意】
代码段第14行:
for (int i = 0; i < n-1; i++)
这里 i 的循环条件为0~n-1,之所以不选择 n 是因为,当前 n-1 位已经确定之后,第 n 位也自动确定了。
3.介绍一下swap()函数:
因为排序算法经常用到交换操作,每一次都写一个交换操作会很麻烦,所以这里采用C++一个内置的交换函数swap()函数,主要作用就是交换两个参数的值。
有关于swap()更详细的介绍放在下面
变量值互换自定义函数swap()的使用_Apollon_krj的博客-CSDN博客https://blog.csdn.net/apollon_krj/article/details/51445885【C++ primer阅读记录】内存管理之swap交换函数与std::move - LeeSCUT - 博客园 (cnblogs.com)https://www.cnblogs.com/leeinSCUT/p/14087987.html
二、冒泡排序
总体思路为:
第 1 个阶段,通过冒泡,将 n 个元素中的最大值移动到序列的最后一位;
第 i 个阶段,通过冒泡,将前 n- i +1个元素中的最大值移动到最后一位。
冒泡过程:
从左到右,依次比较相邻的两个元素,利用如下代码,将两者中更大的元素移到右边(即交换操作):
if(a[i]>a[i+1]) swap(a[i],a[i+1]);
采用代码如下:
#include<iostream> #include<algorithm> #define N 1000 using namespace std; int a[N]; int n; int main() { cin >> n; for (int i = 0; i < n; i++) cin >> a[i]; for (int i = 0; i < n-1; i++) { for (int j = 0; j < n-1-i; j++) { if (a[j] > a[j + 1]) swap(a[j], a[j + 1]); } } for (int i = 0; i < n; i++) cout << a[i] << ' '; return 0; }
时间复杂度为:O()
【注意】
代码段的第13行:i 的循环条件是0~n-1,一共进行n-1次冒泡排序。
三、插入排序
基本思路:
(1)假设1~i-1已经有序,从 i 到 1 枚举分界线的下标 j ;
(2)如果分界线前面的元素a[ j-1 ] > x ,那么说明a[ j-1 ]应该在 x 的后面,所以将a[ j-1 ]移动到x的后面,分界线前移,变为 j-1;
(3)如果分界线前面没有元素,就将 x 放在数组的第一位;如果碰到一个 a[ j-1 ] < x ,说明分界线正确,就将x插入到 j 位。
for (j = i; j > 0 && a[j - 1] > x; j--) //其中j>0 a[j] = a[j - 1]; a[j] = x;
时间复杂度为:O()
采用代码如下:
#include<iostream> #define N 1000 using namespace std; int a[N]; int n; int main() { cin >> n; for (int i = 0; i < n; i++) cin >> a[i]; for (int i = 1; i < n; i++) { int j; int x = a[i]; for (j = i; j > 0 && a[j - 1] > x; j--) a[j] = a[j - 1]; a[j] = x; } for (int i = 0; i < n; i++) cout << a[i] << ' '; }
四、快速排序
【分治法】
快速排序用到了一种分治的思想,就是将一个复杂的问题不断分解为规模更小,更容易解决的问题,从而提升解决效率。
基本思路:
(1)假设我们要对数组
a[1..n]
排序。初始化区间[1..n]
。(2)令 l 和 r 分别为当前区间的左右端点。下面假设我们对
l
到r
子段内的数字进行划分。取pivot = a[l]
为分界线,将小于pivot
的数字移到左边,大于pivot
的数字移到右边,然后将pivot
放在中间。假设pivot
的位置是k
。(3)如果左边区间
[l..k-1]
长度大于1,则对于新的区间[l..k-1]
,重复调用上面的过程。(4)如果右边区间
[k+1..r]
长度大于1,则设置新的区间[k+1, r]
,重复调用上面的过程。(5)当整个过程结束以后,整个序列排序完毕。
【注意】对于(2)~(4)的操作可以用递归来实现。
分段代码解析如下:
设置quick_sort递归函数:
void quick_sort(int l,int r) //参数分别为当前排序子段在原序列中左右端点的位置
模拟(3)(4)操作的递归:
//k为当前分界线的位置 if(k-1>l) quick_sort(l,k-1);//如果序列的分界线左边的子段长度>1,排序 if(k+1<r) quick_sort(k+1,r);//如果序列的分界线右边的子段长度>1,排序
模拟(2)操作的移动:
int pivot=a[r];//设置最右边的数为分界线的值 int k=l;//用来记录数字已经被用来交换的位置,此时小于k的部分一定是小于pivot的部分 for(int j=l;j<r;j++) //枚举l~r内的所有数字,将小于pivot的数字依次与最前面的位置交换 { if(a[j]<pivot) swap(a[j],a[k++]); } swap(a[k],a[r]);//最后将右侧端点放到分界线的位置(利用右侧端点作为分界线)
完整代码如下:
#include<iostream> #define N 10000 using namespace std; int a[N]; int n; void quick_sort(int l, int r) { int pivot = a[r]; int k = l; for (int i = l; i < r; i++) { if (a[i] < pivot) swap(a[i], a[k++]); } swap(a[r], a[k]);//此时的k为分界线位置 if (k - 1 > l) quick_sort(l, k - 1); if (k + 1 < r) quick_sort(k + 1, r); } int main() { cin >> n; for (int i = 0; i < n; i++) cin >> a[i]; quick_sort(0, n-1); for (int i = 0; i < n; i++) cout << a[i] << ' '; return 0; }
【复杂度分析】
空间复杂度:O(n)
整个排序的过程中,元素都在原数组中移动,是一种原地排序算法,占用的内存空间一直都是原数组的大小,即为O(n)。
时间复杂度:
当我们将每个子段的中间元素作为分界线的话,一共会被划分为log n层,每一层对每一个子段扫描的合并时间复杂度为O(n),那么在这种情况下总体的时间复杂度为:O(n*log n)
下面对log n层进行简单解释:
若假设一共有n个元素,那么:
但是对于任意n个数的排序,每次划分情况取决于分界线情况,如果每次分界线刚好取最大值或者最小值,会导致划分时所有元素到另一边,此时的时间复杂度最坏,为:O(),所以要尽量避免这样的情况发生。
有如下两种避免上述情况的办法:
(1)在排序之前,随机打乱数组元素的顺序;
(2)在选取分界线时,与之前选取固定位置作为分界线的方法相比,我们可以随机选取位置作为分界线。
五、sort函数的使用
C++标准模板库(STL)提供了一种排序函数sort,因为在很多场景下都需要用到排序的操作,但是一些平方级别的排序算法不够高效,所以我们还可以选择sort函数进行排序。
sort函数基本用法:
#include<algorithm>
using namespace std;
sort(头指针,尾指针,比较函数cmp);//前两个参数必须有,比较函数可以没有,
//若没有,则按照递增进行排序
若实现从大到小的排序,可以自行定义cmp函数:
bool cmp(int a,int b)
{
return a>b;
}
sort(a,a+n,cmp);
有关于sort函数更详细的用法放在了下面
sort()函数详解_辉小歌的博客-CSDN博客_sort函数https://blog.csdn.net/qq_46527915/article/details/114597901
六、归并排序
【基本思路】
把整个序列分成大小相等的两个序列,不设置分界线,分别进行排序,然后合并两个序列。所用思想依然是分治法,归并排序是一种自底向上的逻辑,它把序列一步一步分解为最小有序子段(即长度为1),然后对相邻两个有序子段进行排序、合并,同样的方法依次使用,直到归并为整个序列。
【算法过程】
(1)假设我们要对数组
a[1..n]
排序。初始化左端点l=1,右端点r=n
。(2)下面假设我们对
l
到 r 子段内的数字进行划分。取 l 和 r 的中点mid
,将l
到mid
的元素看成第一个子段的部分,将mid+1
到r
的部分看成第二个子段的部分。两边分别进入下一层,重复调用上面的过程。直到子段长度为1,返回上一层。(3)当算法阶段返回到当前层时,使用归并操作合并下一层的左右两个有序序列,形成本层的有序序列,继续返回上一层。此时借助一个辅助数组b,现将归并排序后的数组复制到b中,用k来枚举原序列 l 到 r 的位置,依次从b数组中挑选元素,填入到k所在位置。
设置两个指针 i ,j,分别指向两个子段的最小元素,如果:j 已经移出子段末尾,或者 i 和 j 仍指向当前子段且 i 指向元素比 j 小,那么:将 i 指向元素填入到 k 所在位置,并将 i 后移,否则,将 j 指向元素填入到 k 所在位置。
(4)当整个过程结束以后,整个序列排序完毕。
【模拟算法】
模拟(1)(2)归并排序的算法操作:
void merge_sort(int l,int r) { if(l>=r) return;//如果子段为空,或者长度为1,则说明该子段有序,退出该函数 int mid=(l+r)>>1;//利用右移一位的操作来得出中间位置 merge_sort(l,mid); merge_sort(mid+1,r); /*省略了合并的过程*/ }
模拟(3)归并的算法操作:
void merge(int l, int r) { for (int i = l; i <= r; i++) b[i] = a[i]; // 将a数组对应位置复制进辅助数组 int mid = l + r >> 1; int i = l, j = mid + 1; // 初始化i和j两个指针分别指向两个子段的首位 for (int k = l; k <= r; ++k) // 枚举原数组的对应位置 { if (j > r || i <= mid && b[i] < b[j]) a[k] = b[i++]; else a[k] = b[j++]; } }
完整代码如下:
#include<iostream> using namespace std; #define N 1000 int a[N], b[N]; int n; void merge(int l, int r) { for (int i = l; i <= r; i++) b[i] = a[i]; int mid = (l + r) >> 1; int i = l, j = mid + 1; for (int k = l; k <= r; k++) { if (j > r || i <= mid && b[i] < b[j]) a[k] = b[i++]; else a[k] = b[j++]; } } void merge_sort(int l, int r) { if (l >= r) return; int mid = (l + r) >> 1; merge_sort(l, mid); merge_sort(mid + 1, r); merge(l, r); } int main() { cin >> n; for (int i = 1; i <= n; i++) cin >> a[i]; merge_sort(1, n); for (int i = 1; i <= n; i++) cout << a[i] << ' '; }
【对比】
与快速排序对比:
(1)归并排序没有设置分界线,快速排序的时间复杂度高度依赖于分界线的选取;
(2)归并排序增加了一个辅助数组,是一种非原地排序算法,快速排序是一种原地排序算法;
(3)归并排序是稳定排序,快速排序不是稳定排序;(稳定排序就是指对于有重复元素的序列,在排序前后,重复元素的相对位置不改变)
(4)二者都使用了递归操作。
【复杂度分析】
空间复杂度:O(n)
时间复杂度:O(nlogn),分析方法和快速排序类似。
七、stable_sort函数
STL库中有对归并排序的优化实现,即为stable_sort()函数,用法与sort()函数相同。
八、计数排序
【基本思路】
计数排序是一种对已知数量范围的数组进行排序的算法。给定长度为 n 的序列,假设已知序列元素的范围都是 [ 0……k ]中的整数,并且 k 的范围比较小,则可以使用计数排序的方法。如果元素的范围不是 [ 0……k ],可以转换到这个区间进行排序,或者进行扩大或缩放到这个范围。
【算法描述】
(1)使用数组 cnt 统计 [ 0……k ]范围内所有数字在序列中出现的个数;
(2)使用 i 从0~k 进行枚举,如果出现 cnt [ i ] 次 i ,那么就在序列末尾增加cnt [ i ] 个 i 。
(3)利用 i 从0~k 进行枚举,利用 j 作为答案数组所在位置,将原数组中的数字序列对应到新数组中,最后将新数组输出。
采用代码如下:
#include<iostream> using namespace std; #define N 1000005 #define K 1000000 int a[N], b[N],cnt[K]; int n; int main() { cin >> n; for (int i = 1; i <= n; i++) { cin >> a[i]; ++cnt[a[i]]; //这里通过cnt数组来维护每个数字出现的次数 } for (int i = 0, j = 0; i < K; i++) { for (int m = 1; m <= cnt[i]; m++) b[++j] = i; } for (int i = 1; i <= n; i++) cout << b[i] << ' '; return 0; }
【复杂度分析】
空间复杂度:
上述代码一共开设了三个数组,对于a和b为O(N),对于cnt为O(K),总体空间复杂度为O(N+K)。
时间复杂度:
输入和输出的时间复杂度为:O(n),内层循环为O(n),维护有序序列的最外层循环为O(K),总体时间复杂度为:O(n+K)。之所以计数排序可以达到比O(nlogn)更好的时间复杂度,就是因为它并不是基于比较的排序。
【另一种算法——对应原数组当中的位置】
大体思路如下:
(1)(2)与上述算法相同;
(3)设置一个前缀和数组sum[ ],来记录cnt[ ]各项的前缀和,利用sum[ x ]即为x 出现的最后的那一个位置,设置idx[ ]数组来与原数组元素在新数组中的位置进行对应;
(4)将idx[ ] 中的位置信息和a[ ] 进行对应,并存储到b[ ]中。
采用代码如下:
#include<iostream> #define N 100005 #define K 100000 using namespace std; int a[N], b[N]; int cnt[K],sum[K]; int idx[N]; int n; int main() { cin >> n; for (int i = 1; i <= n; i++) { cin >> a[i]; ++cnt[a[i]]; } sum[0] = cnt[0]; for (int i = 1; i < K; i++) sum[i] = sum[i - 1] + cnt[i]; for (int i = n; i >= 1; i--)//之所以倒着循环是因为,对于相等的元素, //我们是从后向前分配位置,保证排序的稳定性 idx[i] = sum[a[i]]--; for (int i = 1; i <= n; i++) b[idx[i]] = a[i]; for (int i = 1; i <= n; i++) cout << b[i] << ' '; }
有关于计数排序的拓展:基数排序和桶排序,详细的算法描述放在了下面
1.10 基数排序 | 菜鸟教程 (runoob.com)https://www.runoob.com/w3cnote/radix-sort.html1.9 桶排序 | 菜鸟教程 (runoob.com)https://www.runoob.com/w3cnote/bucket-sort.html完结撒花!这里只介绍了常用的几种排序算法,目前我就先学了这几种哈哈哈,等以后拓展一些之后,还会不断更新的!