排序和去重是数据分析中对数据常见的两种操作。

排序

简介 sort()\texttt{sort()}

algorithm 头文件里,有一个 sort 函数,十分的方便。
运用了快速排序,其基本思想是分治,也可以加上二分的优化;
速度很快,达到 O(nlog2n)O(n\log_2 n)
至少比冒泡快多了。

函数有三个参数:sort(start, end, cmp)
start 是排序起始地址,end 是排序结束地址的下一位
cmp 支持用户自定义排序方法,也可以不填,就默认升序。

实际应用

具体地,我有一个数组 aa。对它进行以下操作:

int n, a[114514];
cin >> n;
for(int i = 1; i <= n; i++){
    cin >> a[i];
}

没错,其实就是个输入。
现在,输入的这些数据无序,如何让他们升序排列?

sort(a + 1, a + n + 1);

起始下标是 11,所以第一个参数是 a+1a+1
终止下标是 nn,所以第二个参数是 a+n+1a + n + 1
当然你也可以多加一个取址:

sort(&a[1], &a[n + 1]);

两种方法等价

快速排序是通过比较的方式来排序。
类比于刚刚的方法,降序排列就需要使用自定义 cmp 来实现。
定义一个 bool 函数,名字随意,参数是要比较的两个东西:

bool jntm(int kun1, int kun2){ // 瞎起名字
	/*
	返回 true → 后面的放在前面
	返回 false → 不动
	*/
	return kun1 > kun2; // 较大的放在前面
}
sort(a + 1, a + n + 1, jntm);

这个函数如果要是全部返回 true 值的话,整个数组将会以最中间的元素左右对称;如果全部返回 false 的话,整个数组不变。bool 的函数默认返回 false
因此,如果想要实现排序的效果,需要比较前一个和后一个。


问题来了,如果对于一个 struct 结构体,cmp 如何使用?
Luogu P1093 / NOIp 2007 PJ\texttt{Luogu P1093 / NOIp 2007 PJ}【奖学金】

// Author:PanDaoxi
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

const int INF = 301;
struct stu{
	int id, total, s1, s2, s3;
} a[INF]; // 结构体数组
int n;

bool cmp(stu x, stu y){ // 比较函数
	if(x.total != y.total) return x.total > y.total; // 先按总分降序排列
	else if(x.s1 != y.s1) return x.s1 > y.s1; // 再按照语文成绩降序排列
	else return x.id < y.id; // 最后是学号
}

int main(){
	ios :: sync_with_stdio(false);
	
	cin >> n;
	for(int i = 1; i <= n; i++){
		a[i].id = i; // 记录学号
		cin >> a[i].s1 >> a[i].s2 >> a[i].s3;
		a[i].total = a[i].s1 + a[i].s2 + a[i].s3; // 计算总分
	}
	sort(a + 1, a + n + 1, cmp); // 排序
	for(int i = 1; i <= 5; i++){ // 输出
		cout << a[i].id << " " << a[i].total << endl;
	}
	
	return 0;
}

去重

集合(set\texttt{set})——升序排序 ++ 去重

什么是字符串 Hash\texttt{Hash}

对于数据规模较小的一组数,我们可以通过桶排序的方法去重。简单快捷,好写。

推广到一般情况,有的时候字符串 Hash\texttt{Hash} 生成的数会很大(或者本身的数据规模就很大),这时候就不能再利用桶了,可能会爆空间。

有一种数据集——集合 set\texttt{set} 可以一边升序排序,一边去重。
头文件:set
同样,我有一个数组 aa,离线输入了一些数据。

int n, a[114514];
cin >> n;
for(int i = 1; i <= n; i++){
    cin >> a[i];
}

我们就可以创建一个整数型集合储存、去重。

set <int> s(a + 1, a + n + 1);
for(auto i : s){
	cout << i << " ";
}
// 也可以创建一个迭代器,但是我不喜欢,因为太麻烦,也可以直接用 auto 代替
for(set <int> :: iterator i = s.begin(); i != s.end(); i++){
	cout << *i << " ";
}
// 倒着遍历
for(auto i = s.rbegin(); i != s.rend(); i++){
	cout << i << " ";
}
// 转存到 vector
vector <int> v(s.begin(), s.end());
for(auto i : v){
	cout << i << " ";
}

unique\texttt{unique} 函数——只去重

头文件:algorithm
本质是将重复的元素移动到数组的末尾,最后再将迭代器指向第一个重复元素的下标。
这个移动的过程也需要比较。

int n, a[114514];
cin >> n;
for(int i = 1; i <= n; i++){ // 还是照常的输入
    cin >> a[i];
}
// 一般使用前需要先排序!!
sort(a + 1, a + n + 1);
// 获取去重后数组的新长度
int len = unique(a + 1, a + n + 1) - a - 1;
// 输出
for(int i = 1; i <= len; i++){
	cout << a[i] << " ";
}

如果使用前需要先排序,那么就出现了一个新问题——如何按照输入顺序?
——可以通过结构体解决。

const int INF = 114514;
struct node {int num, id;} a[INF];

bool cmp(node kun1, node kun2){
	return kun1.id < kun2.id; // 输入早的放在前面
}

// ......
int n;
cin >> n;
for(int i = 1; i <= n; i++){
    cin >> a[i].num;
    a[i].id = i; // 记录输入顺序
}
sort(a + 1, a + n + 1);
int len = unique(a + 1, a + n + 1) - a - 1;
// 再次排序(按照输入顺序)
sort(a + 1, a + len + 1, cmp);
// 输出
for(int i = 1; i <= len; i++){
	cout << a[i].num << " ";
}

美滋滋写完,发现编译器报错:

[Error] no match for 'operator==' (operand types are 'node' and 'node')
[Error] no match for 'operator<' (operand types are 'node' and 'node')

为啥?咋嘛咋嘛滋味儿,我突然发现结构体有两个变量。
电脑和 unique 函数咋知道我要比较哪个?
好的,它不知道,那咱们帮它知道。一拳头淦电脑上。
通过重载运算符

struct node {
	int num, id;
	bool operator == (node k){ // 判等,判的是储存的数等
		return num == k.num;
	}
	bool operator < (node k){ // 排序,让数小的在前面,让输入编号小的在前
		return num < k.num || num == k.num && id < k.id;
	}
} a[INF];

如果有更多的结构体成员也没关系,核心就是两次排序和重载运算符。

现在就可以实现不排序的去重了。

Luogu P3370\texttt{Luogu P3370}【模板】字符串哈希

很简单的水了一道题。(喜)

// Author:PanDaoxi
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

const int INF = 1e4 + 1;
set <string> s;

int main(){
	ios :: sync_with_stdio(false);

	int n;
	cin >> n;
	while(n--){
		string str;
		cin >> str;
		s.insert(str);
	}
	cout << s.size();
	
	return 0;
}