【算法】常用STL算法

概述

这是我对写算法题中常用的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 );

firstlast是首尾两个迭代器,然后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_boundupper_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 1n 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_elementmax_element函数调用时,若范围中有多个元素等价于最大或小元素,则返回指向首个这种元素的迭代器。若范围为空则返回 last

这两个函数都是顺序查找,因此不要用在数据量很大的情况。数据大的时候使用其他方法查找。

nth_element

nth_element是部分排序算法,它会对 [first, last) 中元素重新排序。在有序序列中,我们可以称第 n 个元素为整个序列中“第 n 大”的元素。因此,使用nth_element可以把nth元素放到自己合适的位置。然后前面的值小于它,后面的值大于它。

nth_element的使用要求如下。

  1. 容器支持的迭代器类型必须为随机访问迭代器。这意味着,nth_element() 函数只适用于 array、vector、deque 这 3 个容器。
  2. 当选用默认的升序排序规则时,容器中存储的元素类型必须支持 <小于运算符;同样,如果选用标准库提供的其它排序规则,元素类型也必须支持该规则底层实现所用的比较运算符;
  3. nth_element() 函数在实现过程中,需要交换某些元素的存储位置。因此,如果容器中存储的是自定义的类对象,则该类的内部必须提供移动构造函数和移动赋值运算符。

函数原型如下。

template< class RandomIt >
void nth_element( RandomIt first, RandomIt nth, RandomIt last );

看下面的代码。简而言之,nth_elementa+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];
    }
};
  • 11
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值