概述
这是我对写算法题中常用的STL算法的笔记,仅仅包含最基础的一些算法。
文章目录
大小写转换
判断一个字母是不是小写:islower
判断一个字母是不是大写:isupper
转换一个字符为小写:tolower
转换一个字符为大写:tupper
下面是使用方法。使用库函数的好处是方便理解,而且不需要考虑这个字符是不是字母,因为库函数会判断。而且如果是tupper
这样的函数,如果是字母的小写那么才会转为大写,如果是大写就不动。
char ch = 'a';
if (islower(ch)){
// ch是小写
}
if (isupper(ch)){
// ch是大写
}
char loch = tolower(ch); // 转换为小写
char upch = toupper(ch); // 转换为大写
排序
sort使用的是优化版的快速排序,时间复杂度大约是 O ( n l o g n ) O(nlogn) O(nlogn)。
sort三个参数分别是起始地址,结束地址的下一个地址,比较函数。
sort默认是利用小于号进行排序,因此默认是升序。而下面传入一个cmp
函数就变成了降序。
bool cmp(const int& u, const int& v)
{
return u > v;
}
int main()
{
vector<int> v = {5,214,43,6,57,68,3,43};
sort(v.begin(), v.end(), cmp);
// lambda的写法
sort(v.begin(), v.end(), [](const int& u, const int& v){
return u > v;
});
return 0;
}
对于lambda匿名函数写法,如果涉及到上下文中的变量,那么这种最好使用自己定义一个cmp
函数的写法,然后把这个上下文中的变量变为全局变量,然后在cmp
函数中使用。
对结构体或者类进行排序,重载<
这个运算符,或者是写比较函数。
struct Node {
int u,v;
bool operator <(const Node& m) const{
//以u为第一关键字,v为第关键字排序
return u==m.u ? v<m.V : u<m.u;
}
};
练习题:https://www.lanqiao.cn/problems/1265/learning/
#include <bits/stdc++.h>
using namespace std;
int n;
int v[1000000];
int main()
{
scanf("%d", &n);
for(int i=0;i<n;i++){
scanf("%d", &v[i]);
}
// 先升序排
sort(v,v+n);
for(int i=0;i<n;i++){
printf("%d ", v[i]);
}
printf("\n");
// 再降序排
sort(v,v+n,[](int lhs,int rhs){
return lhs>rhs;
});
for(int i=0;i<n;i++){
printf("%d ", v[i]);
}
printf("\n");
return 0;
}
其实这里拿到升序以后直接反转就行。
#include <bits/stdc++.h>
using namespace std;
int n;
int v[1000000];
int main()
{
scanf("%d", &n);
for(int i=0;i<n;i++){
scanf("%d", &v[i]);
}
// 先升序排
sort(v,v+n);
for(int i=0;i<n;i++){
printf("%d ", v[i]);
}
printf("\n");
// 再降序排
reverse(v,v+n);
for(int i=0;i<n;i++){
printf("%d ", v[i]);
}
printf("\n");
return 0;
}
不过还有更投机取巧的方法,就是不要排了,倒着输出就行。
#include <bits/stdc++.h>
using namespace std;
int n;
int v[1000000];
int main()
{
scanf("%d", &n);
for(int i=0;i<n;i++){
scanf("%d", &v[i]);
}
// 先升序排
sort(v,v+n);
for(int i=0;i<n;i++){
printf("%d ", v[i]);
}
printf("\n");
for(int i=n-1;i>=0;i--){
printf("%d ", v[i]);
}
printf("\n");
return 0;
}
不过要注意的是数据的范围,一般来说4MB内存,差不多够开一百万个int。不过,我这里代码都是遵循C++标准库内惯用方法,即左闭右开的写法,所以都是小于或者是大于,不带等号。
二分查找
二分查找的数据必须是有序的。而且二分查找的类型很多,如果要自己实现是比较麻烦的,尤其是带有相等值的数据,例如1 2 3 3 4 5 8
这样的数据,想要找到首个不小于4的值,如果是自己背二分查找的模板就很麻烦。
这里就使用库函数的方法解决。
首先是binary_search
函数,用于检查在一个范围内是否有一个值为value
的元素,其函数签名如下。
template< class ForwardIt, class T >
bool binary_search( ForwardIt first, ForwardIt c, const T& value );
first
和last
是首尾两个迭代器,然后value
是需要找的值。使用方法如下。
#include <bits/stdc++.h>
using namespace std;
int main()
{
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
bool f = binary_search(arr, arr + 10, 2);
cout << f;
return 0;
}
其实很简单,就是传入首尾的地址和要查找的值。返回是否有这个值。不过要注意的是binary_search
函数的查找范围是[first, last)
,也就是说last
迭代器指向的元素是不会被当成里面的元素的。因此,范围是[first, last - 1]
。
接下来这两个是用的最多的。分别是lower_bound
和upper_bound
函数。
template< class ForwardIt, class T >
ForwardIt lower_bound( ForwardIt first, ForwardIt last, const T& value );
template< class ForwardIt, class T >
ForwardIt upper_bound( ForwardIt first, ForwardIt last, const T& value );
lower_bound
函数返回指向范围 [first, last)
中首个不小于(即大于或等于) value
的元素的迭代器,或若找不到这种元素则返回last
。
upper_bound
函数返回指向范围 [first, last)
中首个大于 value
的元素的迭代器,或若找不到这种元素则返回 last
。
要注意的是这两个函数都是需要一个升序的数组,因为是调用<
来实现条件判断的。下面是一个例子,用来查找首个不大于3的元素。
#include <bits/stdc++.h>
using namespace std;
int main()
{
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int arr[7] = {1, 2, 3, 3, 4, 5, 8};
int* p = lower_bound(arr, arr+7, 3);
cout << *p;
return 0;
}
当然,如果一个数据是降序,这种时候如果数据很大,那么使用sort
重新排序为升序反而效果不好。可以利用前面提到过的reverse
进行数组反转,但是这里可以利用这个函数的第四个参数来实现。第四个参数就是比较函数,在前面sort
的时候,我们就知道可以自定义一个函数,而且标准库默认使用<
比较的事实。那么我们可以定义一个>
实现的函数,进行传入。
#include <bits/stdc++.h>
using namespace std;
bool cmp(const int& lhs, const int& rhs)
{
return lhs > rhs;
}
int main()
{
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int arr[7] = {8, 5, 4, 3, 3, 2, 1};
int* p = lower_bound(arr, arr+7, 3, cmp);
cout << *p;
return 0;
}
杂项
memset
有的时候,需要把一个数组都重新清零,这个时候如果要写循环就很麻烦,就可以使用memset
函数,函数的参数如下。
#include <bits/stdc++.h>
using namespace std;
int main()
{
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int buf[64][1024];
// ...
// 使用 buf
// 清零
memset(buf, sizeof(buf), 0);
// 复用 buf
return 0;
}
因为编译器可以自动计算数组大小,所以直接使用sizeof
,或者自己手动算sizeof(int)*64*1024
。
swap
接下来是交换函数swap
。我们在写程序的时候经常需要交换两个变量。这个函数就实现了这个功能,因为是一个模板函数。所以对于任意的类,只要实现=
赋值运算符就能使用。
int a=5, b=10;
swap(a, b);
reverse
反转数组也是常用功能,如果有一个升序的数组需要变成降序,这个时候最佳方法不是使用自定义的sort
排序,因为不如直接反转数组快。reverse
就是实现这样的功能,其函数原型如下。
template< class BidirIt >
void reverse( BidirIt first, BidirIt last );
unique
有时候,我们有可能需要对一个数组去重,这个时候,如果利用map
数据结构或者是手动来实现也是可以的,不过有unique
这个方便的函数。它可以把重复的元素放在后面,前面都是不重复的元素。函数的原型如下。
template< class ForwardIt >
ForwardIt unique( ForwardIt first, ForwardIt last );
写一个去除数组内重复元素的代码。unique
函数会返回新的结束位置的迭代器。
#include <bits/stdc++.h>
using namespace std;
int main()
{
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int arr[7] = {8, 5, 4, 3, 3, 2, 1};
int* p = unique(arr, arr+7);
cout << *p << endl;
for(int i = 0; i < 7; i++){
cout << arr[i] << ' ';
}
return 0;
}
输出结果是
1
8 5 4 3 2 1 1
当然,如果不能确定返回的是什么地址。可以看下面的内容,如果没有疑惑,那就跳过去。
可以使用一句cout << (p-arr) << endl;
代替前面的cout << *p << endl;
就能知道p
相对于arr
多了几个元素的距离。这个时候会输出6
。如果觉得这个时候应该输出的字节数,那么可以选择改成cout << (&arr[1]-arr) << endl;
这一句代码,测试一下,下标为1的元素地址减去首地址是多少。这个就能发现,算出来的其实不是绝对的地址相差几个字节。而是要除以类型的4,所以就是几个元素的距离。而且这个时候,unique
的返回值也绝对不是有些人认为的新数组结束地址,而是逻辑结束。我这里说的逻辑结束,指的是跟标准库的last
那个结束。
例如一个数组1 2 3
,那么last
应该是元素3后面的一个元素地址。而不是元素3的地址。
要注意unique
只不过是把重复的元素放到末尾了,想要真正去除需要配合erase
函数。
auto end_unique = unique(result.begin(), result.end());
result.erase(end_unique, result.end());
或者干脆就直接记住下面这个模板,定义去重的数组名字叫做v
,定义为vector<int> v;
这样一个数组。
sort(v.begin(), v.end());
v.erase(unique(v.begin(), v.end()), v.end());
全排列
对于全排列一般使用搜索,即dfs的方法写。这里可以参考acwing的一个题。要求从 1 ∼ n 1 \sim n 1∼n这 n n n个整数排成一行后随机打乱顺序,输出所有可能的次序。本质上就是求一个全排列。题目链接:https://www.acwing.com/problem/content/96/。如果dfs的话比较繁琐,可以参考下面这个代码。
#include <iostream>
using namespace std;
int n;
bool f[10]={false};
int path[10];
void dfs(int u)
{
if(u > n) {
for(int i = 1; i <= n; i++){
cout << path[i] << ' ';
}
cout << endl;
return;
}
for(int i = 1; i <= n; i++){
if(!f[i]){
path[u] = i;
f[i] = true;
dfs(u+1);
f[i] = false;
}
}
}
int main()
{
cin >> n;
dfs(1);
return 0;
}
next_permutation
next_permutation
函数用于生成当前序列的下一个排列。它按照字典序对序列进行重新排
列,如果存在下一个排列,则将当前序列更改为下一个排列,并返回true;如果当前序列已
经是最后一个排列,则将序列更改为第一个排列,并返回false。函数的原型如下。
template< class BidirIt >
bool next_permutation( BidirIt first, BidirIt last );
说了那么多,next_permutation
其实很简单,就是传染一个容器的首尾迭代器。换成数组,就是数组的起始地址和结束地址的下一个地址。下面使用next_permutation
输出数组的全排列。
#include <bits/stdc++.h>
using namespace std;
int main()
{
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int a[3]={1, 2, 3};
while(next_permutation(a, a+3)){
for(int i = 0; i < 3; i++){
cout << a[i] << ' ';
}
cout << endl;
}
return 0;
}
输出如下。
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
下面使用next_permutation
函数重写前面那个全排列的题目。
#include <bits/stdc++.h>
using namespace std;
int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
int main()
{
int n;
cin >> n;
do{
for(int i = 0; i < n; i++){
cout << a[i] << ' ';
}
cout << endl;
}while(next_permutation(a, a+n));
return 0;
}
不过这里要注意,不能使用while
,因为当
n
n
n为1的时候,这个时候next_permutation
会因为只有一个全排列,认为这已经是最后一个了,所以返回false
,导致没有用输出。而do while
可以保证至少进去一次。
prev_permutation
prev_permutation
函数与next _permutation
函数相反,它用于生成当前序列的上-一个排列。它按照字典序对序列进行重新排列,如果存在上-一个排列,则将当前序列更改为上-一个排列,并返回true;如果当前序列已经是第一个排列,则将序列更改为最后一个排列,并返回false。函数原型如下。
template< class BidirIt >
bool prev_permutation( BidirIt first, BidirIt last);
还是前面那个全排列的例子。因为是反向的,所以一开始数组应该是降序,也就是字典序最大的开始才能全排列成功。
#include <bits/stdc++.h>
using namespace std;
int main()
{
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int a[3]={3, 2, 1};
while(prev_permutation(a, a+3)){
for(int i = 0; i < 3; i++){
cout << a[i] << ' ';
}
cout << endl;
}
return 0;
}
输出内容如下。
3 1 2
2 3 1
2 1 3
1 3 2
1 2 3
最值查找
min和max
min和max函数,我们都知道,因为我们经常需要拿到两个变量中较小或者较大的值。这很容易理解。下面是两个函数的原型。
template< class T >
const T& min( const T& a, const T& b );
template< class T >
const T& max( const T& a, const T& b );
min_element和max_element
min_element
用来获取一个容器内最小的元素的迭代器。而max_element
用来获取最大的元素的迭代器。函数原型如下。
template< class ForwardIt >
ForwardIt min_element( ForwardIt first, ForwardIt last );
min_element
和max_element
函数调用时,若范围中有多个元素等价于最大或小元素,则返回指向首个这种元素的迭代器。若范围为空则返回 last
。
这两个函数都是顺序查找,因此不要用在数据量很大的情况。数据大的时候使用其他方法查找。
nth_element
nth_element
是部分排序算法,它会对 [first, last)
中元素重新排序。在有序序列中,我们可以称第 n 个元素为整个序列中“第 n 大”的元素。因此,使用nth_element
可以把nth
元素放到自己合适的位置。然后前面的值小于它,后面的值大于它。
nth_element
的使用要求如下。
- 容器支持的迭代器类型必须为随机访问迭代器。这意味着,nth_element() 函数只适用于 array、vector、deque 这 3 个容器。
- 当选用默认的升序排序规则时,容器中存储的元素类型必须支持 <小于运算符;同样,如果选用标准库提供的其它排序规则,元素类型也必须支持该规则底层实现所用的比较运算符;
- nth_element() 函数在实现过程中,需要交换某些元素的存储位置。因此,如果容器中存储的是自定义的类对象,则该类的内部必须提供移动构造函数和移动赋值运算符。
函数原型如下。
template< class RandomIt >
void nth_element( RandomIt first, RandomIt nth, RandomIt last );
看下面的代码。简而言之,nth_element
把a+3
也就是4这个元素放到自己合适的位置上了。而且左边的值都是小于4,右边的值大于4。但是不保证是有序的,因为这个函数只是部分排序。
#include <bits/stdc++.h>
using namespace std;
int main()
{
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int a[8]={2, 5, 4, 6, 9, 3, 2, 1};
nth_element(a, a+3, a+8);
for(int i = 0; i < 8; i++){
cout << a[i] << ' ';
}
return 0;
}
输出结果如下。
2 2 1 3 4 5 9 6
LeetCode的一道题使用这个nth_element
函数就很方便。题目链接:https://leetcode.cn/problems/kth-largest-element-in-an-array/。可以看出来,针对于要第K个这种情况就很适合这个函数。
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
nth_element(nums.begin(), nums.begin()+k-1, nums.end(),std::greater<int>());
return nums[k-1];
}
};
这里涉及到一种东西叫做仿函数std::greater
,这个就像是应该宏函数,实际上是类模板。说到底就是一种比较方法,这是nth_element
函数的第四个参数。如果不用标准库的仿函数,那就自己写一个比较函数。下面这样也是可以通过的。
bool cmp(const int& lhs, const int& rhs)
{
return lhs > rhs;
}
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
nth_element(nums.begin(), nums.begin()+k-1, nums.end(), cmp);
return nums[k-1];
}
};