排序算法
将杂乱无章的数据元素,通过一定的方法按关键字顺序排列的过程叫做排序。
常见排序算法
快速排序、希尔排序、堆排序、直接选择排序不是稳定的排序算法,而基数排序、冒泡排序、直接插入排序、折半插入排序、归并排序是稳定的排序算法。
分类
◆稳定排序:假设在待排序的文件中,存在两个或两个以上的记录具有相同的关键字,在用某种排序法排序后,若这些相同关键字的元素的相对次序仍然不变,则这种排序方法是稳定的。其中冒泡,插入,基数,归并属于稳定排序,选择,快速,希尔,归属于不稳定排序。
◆就地排序:若排序算法所需的辅助空间并不依赖于问题的规模n,即辅助空间为O(1),则称为就地排序。
计数排序
1、题目详情:洛谷 P1271 【深基9.例1】选举学生会
学校正在选举学生会成员,有 n ( n ≤ 999) 名候选人,每名候选人编号分别从 1 到 n ,现在收集到了 m ( m < 2000000) 张选票,每张选票都写了一个候选人编号。现在想把这些堆积如山的选票按照投票数字从小到大排序。输入 n 和 m 以及 m 个选票上的数字,求出排序后的选票编号。
#include<iostream>
using namespace std;
int a[1000]={0},n,m,tmp;
int main(){
cin>>n>>m;
for(int i=0;i<m;i++){
cin>>tmp;
a[tmp]++;
}
for(int i=1;i<=n;i++)
for(int j=0;j<a[i];j++)
cout<<i<<' ';
cout<<endl;
return 0;
}
计数排序 顾名思义,即统计每一个元素出现的次数,再按照顺序依次排列。数列中的元素就是“票”,而一个与元素取值范围相符的数组就是“票箱”。记n为数列长度, m为取值范围。需要O(n)的时间统计每一数值出现次数。之后再用O(n + m)的时间构造出结果数列,总时间O(n + m)。
另外,需要O(m) 的额外空间作为票箱。
优点: 当m较小时,时间复杂度近似于O(n),性能强大。
缺点: 当m远大于n时,时空复杂度均取决于m,得不偿失。取值范围为非整数时,无法实现。
计数排序变种:
- 离散化计数排序
若能将不可表示的数据范围(双向)映射到较小的整数集合上,
则可以在映射后使用计数排序。这一映射过程称为离散化。- 桶排序
将数列按数值区间(而非具体数值)划分为若干个桶。桶内采用
其他排序算法。- 基数排序
从低到高对每一个(X进制)位进行一次计数排序。这样,当高位
有序时,所有低位均已有序。可以保证只使用X个桶。
冒泡排序
思路:不超过n次大循环;每次大循环,按照顺序比较相邻元素并交换,直到序列有序。
for (int i = 0; i < n - 1; i++)
for (int j = 0; j < n - i - 1; j++)
if (a[j] > a[j + 1])
swap(a[j], a[j + 1]);
特点:总交换次数恰为逆序对数。每次迭代都能保证至少一个(最大)元素的位置被确定。后 i 个元素有序,且为最大的 i 个元素。
#include <iostream>
#include <vector>
using namespace std;
/*冒泡排序*/
void bubbleSort(int* arr, int n);
int main() {
int a[100];
int n;
cin >> n;
for (int i = 0; i < n; i++) {
cin >> a[i];
}
bubbleSort(a, n);
for (int i = 0; i < n; i++) {
cout << a[i] << " ";
}
return 0;
}
void bubbleSort(int* arr, int n){
int temp;
for (int i = 0; i < n - 1; i++) { // n个数排序,只需冒泡 n-1 次
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] < arr[j + 1]) {
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
选择排序
思路: n 次大循环,第 i 次大循环中,用小循环寻找数列中第 i 小的元素。如果发现更小的数字,则交换至第 i 个位置。
for (int i = 0; i < n - 1; i++)
for (int j = i + 1; j < n; j++)
if (a[j] < a[i])
swap(a[i], a[j]);
// 事实上,由于前i-1项已经排序完毕,第i小的元素等价于第i至第n项中的最小元素。
特点:思路简单,实现更简单。
每次迭代都能保证至少一个(最小)元素的位置被确定。前i个元素有序,且为最小的i个元素。
#include <iostream>
using namespace std;
void selectSort(int* arr, int n);
int main() {
int a[100];
int n;
cin >> n;
for (int i = 0; i < n; i++) {
cin >> a[i];
}
selectSort(a, n);
for (int i = 0; i < n; i++) {
cout << a[i] << " ";
}
return 0;
}
void selectSort(int* arr, int n){
int minIndex;
int temp;
for (int i = 0; i < n; i++) {
minIndex = i;
for (int j = i; j < n; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
if (i != minIndex) {
temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
}
插入排序
思路:n次大循环;第i次大循环将第i个元素向前交换,直至左侧元素不大于它,或抵达数列首部。
for (int i = 1; i < n; i++) {
int now = a[i], j; // 记录一下待插牌,等下还要放回去
for (int j = i - 1; j >= 0; j--)
if (a[j] > now)
a[j + 1] = a[j];
else break;
a[j + 1] = now;
}
特点:前 i 个元素有序。但是直到排序完成;不能保证任何一个元素的最终位置被确定(设想最小元素在数列尾部)。可以用来动态维护前 k 小元素,单次插入时间复杂度O(k) 。在此场景下,第 k + 1小的元素将不会右移,而是被直接丢弃。
#include <iostream>
using namespace std;
void InsertSort(int *arr, int len){
int temp;
for (int i = 1; i < len; i++) {
temp = arr[i];
if (arr[i] < arr[i - 1]) {
temp = arr[i];
int j = i - 1;
for (; j >= 0 && temp < arr[j]; j--) {
arr[j + 1] = arr[j];
}
arr[j + 1] = temp;
}
}
}
int main(){
int a[100];
int n;
cin >> n;
for (int i = 0; i < n; i++) {
cin >> a[i];
}
InsertSort(a, n);
for (int i = 0; i < n; i++) {
cout << a[i] << " ";
}
cout << endl;
return 0;
}
快速排序
选择排序,每一迭代确定一个元素的位置。但是所有已确定元素均在左侧,对右侧没有指导意义。
插入排序,每一迭代只需要与大于自身的值比较,期望只需比较一半的元素。但是没有元素确定位置;新元素的插入导致所有更大的元素右移,带来额外开销。
考虑结合二者优点。以这个数列为例:3 8 4 10 6 7 2 5 9 1
尝试随机选取一个元素确认位置,称为哨兵数。
3 8 4 10 6 7 2 5 9 1
将序列中所有比哨兵数小的数字都移动到哨兵数的左边,所有比哨兵数大的数字都移动到哨兵数的右边。
3 4 2 5 1 6 8 10 7 9
显然哨兵数左右两侧的元素不再需要任何比较。因此,对两侧的子数列分别采用相同的做法,直到数列不可再分(长度为0或1)。
在最优情况下,每次选择的哨兵数均将数列对半分开。则
O(
log
2
(
n
)
\log_{2}(n)
log2(n) )次划分后数列将不可再分。每次划分的复杂度为O(n),
总复杂度O(
n
log
2
(
n
)
n\log_{2}(n)
nlog2(n) )。
在最坏情况下,每次选择的哨兵数均在数列一端。则退化为选择
排序算法,总复杂度O(
n
2
n^{2}
n2)。
// 快排模板 并不存在一种在所有情况下都最优的实现,需根据应用场景选择合适的实现。
void qsort(int a[], int l, int r) { // 引入数组的地址
int i = l, j = r, flag = a[(l + r) / 2], tmp; // flag=哨兵
do {
while (a[i] < flag) i++; // 从左找比哨兵大的数
while (a[j] > flag) j--; // 从右找比哨兵小的数
if (i <= j) { // 交换
swap(a[i], a[j]);
i++; j--;
}
} while (i <= j);
// 习惯上,上面用于分段的过程一般称作partition
if (l < j) qsort(a, l, j);
if (i < r) qsort(a, i, r);
}
快排是一种基于分治法的排序算法。
大多数基于分治法的算法均具有O(nlogn)的时间复杂度。
其余具有此复杂度的排序算法还有:
- 基数排序,基于按位处理
- 归并排序,基于分治法
- 堆排序,基于数据结构
#include <iostream>
using namespace std;
// 一个完整的快速排序代码
void quickSort(int* arr, int left, int right);
int main() {
int a[100];
int n;
cin >> n;
for (int i = 0; i < n; i++) {
cin >> a[i];
}
quickSort(a, 0,n);
for (int i = 0; i < n; i++) {
cout << a[i] << " ";
}
cout << endl;
return 0;
}
void quickSort(int* arr, int L, int R)
{
if (L > R) return;
int left = L, int right = R;
int pivot = arr[left];
while (left < right){
while (left < right && arr[right] >= pivot) {
right--;
}
if (left < right) {
arr[left] = arr[right];
}
while (left < right && arr[left] <= pivot) {
right--;
}
if (left < right) {
arr[right] = arr[left];
}
if (left >= right) {
arr[left] = pivot;
}
}
quickSort(arr, L, right - 1);
quickSort(arr, right + 1, R);
}
void Quick_Sort(int *arr, int begin, int end){
if(begin > end)
return;
int tmp = arr[begin];
int i = begin;
int j = end;
while(i != j){
while(arr[j] >= tmp && j > i)
j--;
while(arr[i] <= tmp && j > i)
i++;
if(j > i){
int t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
}
arr[begin] = arr[i];
arr[i] = tmp;
Quick_Sort(arr, begin, i-1);
Quick_Sort(arr, i+1, end);
}
2、题目详情:洛谷 P1923 【深基9.例4】求第 k 小的数
输入 n ( n < 5000000 且 n 为奇数) 个数字 a i a_{i} ai ( a i a_{i} ai < 1 0 9 10^{9} 109) ,输出这些数字的第 k 小的数。最小的数是第 0 小。
#include<bits/stdc++.h>
using namespace std;
int a[5000005],n,k;
void qs(int left,int right)//双指针快速排序,还有一种是挖坑快速排序,数据结构的书本上就是后者
{
if(left==right)return;
int l=left,r=right,mid=l;
while (l<r)
{
while(a[r]>=a[mid] && l<r)r--;
while(a[l]<=a[mid] && l<r)l++;
swap(a[l],a[r]);
}
swap(a[mid],a[r]);
if (k<l)//判断询问的位置,只要到询问的位置为整体有序即可
{
qs(left,l-1);
}else if (k>l)
{
qs(l+1,right);
}else
{
return;
}
}
int main()
{
cin >> n >>k;
for (int i = 0; i < n; i++)
{
scanf("%d",&a[i]);//scanf比cin快,用cin会超时
}
qs(0,n-1);
cout << a[k];
return 0;
}
/*
如果 k 很小(常数级别),那么可以用选择排序直接把第 k 大的数选出来。然而并没有这个保证。
如果直接排序,我们已经知道可以在O(nlnn)的时间复杂度内使用快速排序求出。然而会超时
*/
STL中的排序算法
algorithm(算法)库包含很多常用的算法,包括排序。
包含头文件:#include <algorithm>
使用排序功能:
sort(a.begin(), a.end()); // O(nlogn)
sort(a.begin(), a.end(), cmp); // O(nlogn)
begin、end分别表示需要排序位置的首末。
cmp是一个可选参数,可以自定义排序的比较方法。
排序之后,可以使用以下算法:
去重:unique(a.begin(), a.end()); // O(n)
这个函数会返回去重后的序列末尾地址(序列长度可能会变短)。
查找:find(a.begin(), a.end(), val); // O(logn)
若元素存在则返回元素地址,否则返回末尾地址(end)。
对于数组和 vector(暂时没教) 的数组坐标:
- a+0 = begin
- a+n = end
- a+(n-1) = 最后一个元素
- end – begin = n
- find(a[x]) – a = x
sort(a.begin(), a.end()); // 对一个 vector 进行排序
sort(a.begin(), a.end(), cmp); // 对一个 vector 进行【自定义】排序
sort(a, a+n); // 对一个长度为 n 的数组 a 排序
sort(a, a+n, cmp); // 对长度为 n 的数组 a【自定义】排序
3、题目详情:洛谷 P1059 [NOIP2006 普及组] 明明的随机数
给出 N ( N ≤ 100) 个 1 到 1000 的数字,输出去重后剩余数字的个数,以及去重排序后的序列。
- 解法 1:
注意到取值范围很小,可以使用计数排序。 - 解法 2:
直接使用STL中的排序、去重函数。
sort(a, a + n);
cnt = unique(a, a + n) - a;
cout << cnt << endl;
for (int i = 0; i < cnt; i++)
cout << a[i] << ' ';
sort 函数可以增加第三个参数 cmp,也就是自定义排序的基准。
cmp 函数需要两个待排序的元素类型 a、b 作为参数。返回一个 bool 值,表示 x 是否严格小于 y。
例如:实现整数从大到小排序。sort(a, a + n, cmp);
bool cmp(int x, int y){
return x > y;
}
/*
cmp 函数名称可以任取,保持上下一致即可。
*/
本题的AC代码:
#include<bits/stdc++.h>
using namespace std;
int main(){
int n,a[105],k=0;
cin>>n;
for(int i=0;i<n;i++)
cin>>a[i];
sort(a,a+n);
for(int i=0;i<n;i++)
for(int j=i+1;j<n;j++)
if(a[j]==a[i] && a[j]!=-1)
a[j]=-1;
for(int i=0;i<n;i++)
if(a[i]!=-1)
k++;
cout<<k<<endl;
for(int i=0;i<n;i++)
if(a[i]!=-1)
cout<<a[i]<<" ";
return 0;
}
4、题目详情:洛谷 P1093 [NOIP2007 普及组] 奖学金
给出 n ( n ≤ 300) 名学生的语文、数学、英语成绩,这些学生的学号依次是从 1 到 n。需要对这些学生进行排序。如果总分相同,则语文分数高者名次靠前;如果语文成绩还是一样的,学号小者靠前。输出排名前 5 的学生学号和总分。
第 1 种 :使用 STL 进行排序
构造一个结构体 student 用户存储学生的各项有用的信息(数学和
英语并不重要,可以不用存下来)。注意使用 cmp 来进行比较,
当两个学生比较时,排名比较高的学生返回 true。
#include <algorithm>
#include <iostream>
using namespace std;
int const MAXN = 310;
int n;
struct student {
int id, chinese, total;
}a[MAXN];
int cmp(student a, student b) {
if(a.total != b.total) // 总分先定胜负
return a.total > b.total;
if(a.chinese != b.chinese) // 然后比语文
return a.chinese > b.chinese;
return a.id < b.id; // 最后比学号
}
int main() {
cin >> n;
for (int i = 0; i < n; i++) {
int math, english;
cin >> a[i].chinese>>math>>english;
a[i].total=a[i].chinese+math+english;
a[i].id = i + 1;
}
sort(a, a + n, cmp);
for (int i = 0; i < 5; i++)
cout<<a[i].id<<" "<<a[i].total<<endl;
return 0;
}
第 2 种 :
#include <bits/stdc++.h>
using namespace std;
struct student {//典型结构体排序,总的来说比较简单,都是一个套路
int math;
int Chinese;
int English;
int num;
int sum;
} p[100001];
bool cmp(student a,student b) {
/*
先按总分从高到低排序,
如果两个同学总分相同,再按语文成绩从高到低排序,
如果两个同学总分和语文成绩都相同,那么规定学号小的同学 排在前面,
*/
if (a.sum!=b.sum)//总分不同
{
return a.sum>b.sum;
}else if (a.Chinese!=b.Chinese)//总分相同,语文不同
{
return a.Chinese>b.Chinese;
}else //总分相同,语文相同,按学号排
{
return a.num<b.num;
}
}
int main() {
int n;
cin>>n;
for(int i=1; i<=n; i++) {
p[i].num=i;
cin>>p[i].Chinese>>p[i].math>>p[i].English;
p[i].sum=p[i].Chinese+p[i].English+p[i].math;
}
sort(p+1,p+n+1,cmp);
for(int i=1; i<=5; i++)//只要前五
cout<<p[i].num<<" "<<p[i].sum<<endl;
return 0;
}
除此之外还有选择排序和插入排序思路的其它解法,这里就不细说了。