C++ tuple元组、pair 比较、lower_bound和upper_bound

一、tuple元组

1.1、简介

C++11 标准新引入了一种类模板,命名为 tuple(元组)。tuple 最大的特点是:实例化的对象可以存储任意数量、任意类型的数据

1.2、初始化

tuple 本质是一个以可变模板参数定义的类模板,它定义在 头文件并位于 std 命名空间中。因此要想使用 tuple 类模板,头文件:

#include <tuple>

实例化 tuple 模板类对象常用的方法有两种,一种是借助该类的构造函数,另一种是借助 make_tuple() 函数。

  1. 类的构造函数
    tuple 模板类提供有很多构造函数,包括:
1) 默认构造函数
constexpr tuple();
2) 拷贝构造函数
tuple (const tuple& tpl);
3) 移动构造函数
tuple (tuple&& tpl);
4) 隐式类型转换构造函数
template <class... UTypes>
    tuple (const tuple<UTypes...>& tpl); //左值方式
template <class... UTypes>
    tuple (tuple<UTypes...>&& tpl);      //右值方式
5) 支持初始化列表的构造函数
explicit tuple (const Types&... elems);  //左值方式
template <class... UTypes>
    explicit tuple (UTypes&&... elems);  //右值方式
6) 将pair对象转换为tuple对象
template <class U1, class U2>
    tuple (const pair<U1,U2>& pr);       //左值方式
template <class U1, class U2>
    tuple (pair<U1,U2>&& pr);            //右值方式

举个例子:

#include <iostream>     // std::cout
#include <tuple>        // std::tuple
using std::tuple;
int main()
{
    std::tuple<int, char> first;                             // 1)   first{}
    std::tuple<int, char> second(first);                     // 2)   second{}
    std::tuple<int, char> third(std::make_tuple(20, 'b'));   // 3)   third{20,'b'}
    std::tuple<long, char> fourth(third);                    // 4)的左值方式, fourth{20,'b'}
    std::tuple<int, char> fifth(10, 'a');                    // 5)的右值方式, fifth{10.'a'}
    std::tuple<int, char> sixth(std::make_pair(30, 'c'));    // 6)的右值方式, sixth{30,''c}
    return 0;
}
  1. make_tuple()函数
    上面程序中,我们已经用到了 make_tuple() 函数,它以模板的形式定义在 头文件中,功能是创建一个 tuple 右值对象(或者临时对象)。

对于 make_tuple() 函数创建了 tuple 对象,我们可以上面程序中那样作为移动构造函数的参数,也可以这样用:

auto first = std::make_tuple (10,'a');   // tuple < int, char >
const int a = 0; int b[3];
auto second = std::make_tuple (a,b);     // tuple < int, int* >

程序中分别创建了 first 和 second 两个 tuple 对象,它们的类型可以直接用 auto 表示。

1.3、访问

  • 用get标准库模板进行访问其元素内容
  • 用tuple_size访问其元素个数
  • 用tuple_element访问其元素类型
#include<iostream>
#include<string>
#include<tuple>   
using namespace std;

int main()
{
    auto t1 = make_tuple(1, 2, 3, 4.4);
    cout << "第1个元素:" << get<0>(t1) << endl;//和数组一样从0开始计
    cout << "第2个元素:" << get<1>(t1) << endl;
    cout << "第3个元素:" << get<2>(t1) << endl;
    cout << "第4个元素:" << get<3>(t1) << endl;

    typedef tuple<int, float, string>TupleType;
    cout << tuple_size<TupleType>::value << endl;
    tuple_element<1, TupleType>::type fl = 1.0;

    return 0;
}

输出结果:

第1个元素:1
第2个元素:2
第3个元素:3
第4个元素:4.4
3

二、pair比较

2.1 pair的头文件

#include <utility>

2.2 pair的比较方式

pair的比较方式和元素的类型有关:
(1)当pair.first可以进行小于比较时,pair可以进行小于比较;
(2)当pair.first小于另一个pair的first时,pair小于另一个pair;
(3)当pair.first与另一个pair的first相等且pair.second小于另一个pair的second时,pair小于另一个pair。
因此,我们可以看到,在C++中比较对象,优先比较pair.first,如果相等再比较pair.second。

2.3 pair比较器的声明

当我们需要指定一组比较器来对pair对象进行排序或查找操作时,可以使用STL提供的std::pair_compare结构体。该结构体可以被视为一个函数对象,支持status_free、function指针等过载函数,可以被传递给STL容器算法中的比较器参数。

下面是一个声明比较器的例子,针对以pair<int, string>为元素的vector进行按照第一个元素降序排列:

#include <vector>
#include <utility>
#include <algorithm>
#include <iostream>

bool compare(const std::pair<int, std::string>& p1, const std::pair<int, std::string>& p2)
{
    return p1.first > p2.first;
}

int main()
{
    std::vector<std::pair<int, std::string>> v{ {3, "hello"}, {1, "world"}, {2, "bye"} };

    std::sort(v.begin(), v.end(), compare);

    for (auto& i : v)
        std::cout << i.first << " " << i.second << std::endl;

    return 0;
}

在声明compare函数时,我们需要按照STL的Compare规范,在参数列表中声明两个输入参数:指向pair的指针或引用。函数体中,我们根据排列需求返回true或false。

上述代码中,我们将pair的第一个元素作为排序关键字,使用std::sort()对vector进行按照第一个元素降序排列,最终输出结果如下:

3 hello
2 bye
1 world

2.4 不同STL容器中pair的比较方法

在不同的STL容器中,pair的比较方法稍有不同。下面以set和map为例说明:

  • set
    在set中,要按照一定的规则对pair进行比较,我们可以在声明set时,指定一个比较器来进行排序。默认情况下,set会对pair进行键值比较,以pair中的第一个元素作为键值。
#include <set>
#include <utility>
#include <iostream>

int main()
{
    std::set<std::pair<int, std::string>> s;

    s.insert({ 3, "hello" });
    s.insert({ 1, "world" });
    s.insert({ 2, "bye" });

    for (auto& i : s)
        std::cout << i.first << " " << i.second << std::endl;

    return 0;
}

在上述代码中,我们声明了一个set,并将三个pair对象插入到其中。由于默认情况下set按照pair的第一个元素进行排序,因此输出结果如下

1 world
2 bye
3 hello

如果我们需要按照pair的第二个元素进行排序,可以自定义比较器:

#include<set>
#include<utility>
#include<algorithm>
#include <iostream>


struct pair_comparer
{
    bool operator() (const std::pair<int, std::string>& p1, const std::pair<int, std::string>& p2) const
    {
        return p1.second > p2.second;
    }
};

int main()
{
    std::set<std::pair<int, std::string>, pair_comparer> s;

    s.insert({ 3, "hello" });
    s.insert({ 1, "world" });
    s.insert({ 2, "bye" });

    for (auto& i : s)
        std::cout << i.first << " " << i.second << std::endl;

    return 0;
}

在上述代码中,我们自定义了一个pair_comparer结构体,并在其中定义了一个成员函数operator(),实现按照pair的第二个元素进行排序。在声明set时,我们将这个结构体作为第二个模板参数传递进去,标记该set需要按照pair_comparer进行排序。最终输出结果如下:

2 bye
3 hello
1 world
  • map
    与set相似,我们也可以对map中的pair进行按照键值排序。默认情况下,map以pair的第一个元素作为键值,因此不需要特定的比较器。
#include <map>
#include <utility>
#include <iostream>


int main()
{
    std::map<std::pair<int, std::string>, double> m;

    m.insert({ {3, "hello"}, 1.2 });
    m.insert({ {1, "world"}, 3.4 });
    m.insert({ {2, "bye"}, 5.6 });

    for (auto& i : m)
        std::cout << i.first.first << "," << i.first.second << "," << i.second << std::endl;

    return 0;
}

在上述代码中,我们声明了一个map,并将三个pair对象作为键值对插入到其中。由于默认情况下map按照pair的第一个元素进行排序,因此输出结果如下:

1,world,3.4
2,bye,5.6
3,hello,1.2

如果我们需要按照pair的第二个元素进行排序,可以使用一个自定义的比较器:

#include <map>
#include <utility>
#include <iostream>

struct pair_comparer
{
    bool operator() (const std::pair<int, std::string>& p1, const std::pair<int, std::string>& p2) const
    {
        return p1.second > p2.second;
    }
};

int main()
{
    std::map<std::pair<int, std::string>, double, pair_comparer> m;

    m.insert({ {3, "hello"}, 1.2 });
    m.insert({ {1, "world"}, 3.4 });
    m.insert({ {2, "bye"}, 5.6 });

    for(auto& i : m)
        std::cout << i.first.first << "," << i.first.second << "," << i.second << std::endl;

    return 0;
}

在上述代码中,我们自定义了一个pair_comparer结构体,并在其中定义了一个成员函数operator(),实现按照pair的第二个元素进行排序。在声明map时,我们将这个结构体作为第三个模板参数传递进去,标记该map需要按照pair_comparer进行排序。最终输出结果如下:

2,bye,5.6
1,world,3.4
3,hello,1.2

三、lower_bound和upper_bound

3.1 简介

find()、find_if()、search() 等函数的底层实现都采用的是顺序查找(逐个遍历)的方式,在某些场景中的执行效率并不高。
当指定区域内的数据处于有序状态时,如果想查找某个目标元素,更推荐使用二分查找的方法(相比顺序查找,二分查找的执行效率更高)。因此 lower_bound和upper_bound使用的前提是 **当前特定区域内数据处于有序状态**。

3.2 lower_bound()函数

lower_bound() 函数定义在头文件中,其语法格式有 2 种,分别为:

//在 [first, last) 区域内查找不小于 val 的元素
ForwardIterator lower_bound (ForwardIterator first, ForwardIterator last,
                             const T& val);
//在 [first, last) 区域内查找第一个不符合 comp 规则的元素
ForwardIterator lower_bound (ForwardIterator first, ForwardIterator last,
                            const T& val, Compare comp);
其中,first 和 last 都为正向迭代器,[first, last) 用于指定函数的作用范围;val 用于指定目标元素;comp 用于自定义比较规则,此参数可以接收一个包含 2 个形参(第二个形参值始终为 val)且返回值为 bool 类型的函数,可以是普通函数,也可以是函数对象。

实际上,第一种语法格式也设定有比较规则,只不过此规则无法改变,即使用 < 小于号比较 [first, last) 区域内某些元素和 val 的大小,直至找到一个不小于 val 的元素。这也意味着,如果使用第一种语法格式,则 [first,last) 范围的元素类型必须支持 < 运算符。

此外,该函数还会返回一个正向迭代器,当查找成功时,迭代器指向找到的元素;反之,如果查找失败,迭代器的指向和 last 迭代器相同。

再次强调,该函数仅适用于已排好序的序列。所谓“已排好序”,指的是 [first, last) 区域内所有令 element<val(或者 comp(element,val),其中 element 为指定范围内的元素)成立的元素都位于不成立元素的前面。

  • lower_bound() 示例

#include <iostream>     // std::cout
#include <algorithm>    // std::lower_bound
#include <vector>       // std::vector
#include <iostream>

using namespace std;
//以普通函数的方式定义查找规则
bool mycomp(int i, int j) { return i > j; }
//以函数对象的形式定义查找规则
class mycomp2 {
public:
    bool operator()(const int& i, const int& j) {
 
        cout << "=== i: " << i << " === j: " << j << "\n" ;
 
 
        return i > j;
    }
};
int main() {
    int a[5] = { 1,2,3,4,5 };
    //从 a 数组中找到第一个不小于 3 的元素
    int* p = lower_bound(a, a + 5, 3);
    cout << "*p = " << *p << endl;
    vector<int> myvector{ 4,5,3,1,2 };
    //根据 mycomp2 规则,从 myvector 容器中找到第一个违背 mycomp2 规则的元素
    vector<int>::iterator iter = lower_bound(myvector.begin(), myvector.end(), 3, mycomp2());
    cout << "*iter = " << *iter;
    return 0;
}

  • 底层实现
    template <class ForwardIterator, class T>
    ForwardIterator lower_bound (ForwardIterator first, ForwardIterator last, const T& val)
    {
        ForwardIterator it;
        iterator_traits<ForwardIterator>::difference_type count, step;
        count = distance(first,last);
        while (count>0)
        {
            it = first; step=count/2; advance (it,step);
            if (*it<val) {  //或者 if (comp(*it,val)),对应第 2 种语法格式
                first=++it;
                count-=step+1;
            }
            else count=step;
        }
        return first;
    }

3.3 upper_bound()函数

upper_bound() 函数定义在头文件中,用于在指定范围内查找大于目标值的第一个元素。该函数的语法格式有 2 种,分别是:

//查找[first, last)区域中第一个大于 val 的元素。
ForwardIterator upper_bound (ForwardIterator first, ForwardIterator last,
                             const T& val);
//查找[first, last)区域中第一个不符合 comp 规则的元素
ForwardIterator upper_bound (ForwardIterator first, ForwardIterator last,
                             const T& val, Compare comp);

其中,first 和 last 都为正向迭代器,[first, last) 用于指定该函数的作用范围;val 用于执行目标值;comp 作用自定义查找规则,此参数可接收一个包含 2 个形参(第一个形参值始终为 val)且返回值为 bool 类型的函数,可以是普通函数,也可以是函数对象。

实际上,第一种语法格式也设定有比较规则,即使用 < 小于号比较 [first, last) 区域内某些元素和 val 的大小,

直至找到一个大于 val 的元素,只不过此规则无法改变。这也意味着,如果使用第一种语法格式,则 [first,last) 范围的元素类型必须支持 < 运算符。

同时,该函数会返回一个正向迭代器,当查找成功时,迭代器指向找到的元素;反之,如果查找失败,迭代器的指向和 last 迭代器相同。

另外,由于 upper_bound() 底层实现采用的是二分查找的方式,因此该函数仅适用于“已排好序”的序列。注意,这里所说的“已排好序”,并不要求数据完全按照某个排序规则进行升序或降序排序,而仅仅要求 [first, last) 区域内所有令 element<val(或者 comp(val, element)成立的元素都位于不成立元素的前面(其中 element 为指定范围内的元素)。

  • upper_bound()示例
#include <iostream>     // std::cout
#include <algorithm>    // std::upper_bound
#include <vector>       // std::vector
#include <iostream>

using namespace std;
//以普通函数的方式定义查找规则
bool mycomp(int i, int j) { return i > j; }
//以函数对象的形式定义查找规则
class mycomp2 {
public:
    bool operator()(const int& i, const int& j) {
        cout << "=== i: " << i << " === j: " << j << "\n";
        return i > j;
    }
};
int main() {
    int a[5] = { 1,2,3,4,5 };
    //从 a 数组中找到第一个大于 3 的元素
    int* p = upper_bound(a, a + 5, 3);
    cout << "*p = " << *p << endl;
    vector<int> myvector{ 4,5,3,1,2 };
    //根据 mycomp2 规则,从 myvector 容器中找到第一个违背 mycomp2 规则的元素
    vector<int>::iterator iter = upper_bound(myvector.begin(), myvector.end(), 3, mycomp2());
    cout << "*iter = " << *iter;
    return 0;
}

此程序中演示了 upper_bound() 函数的 2 种适用场景,其中 a[5] 数组中存储的为升序序列;而 myvector 容器中存储的序列虽然整体是乱序的,但对于目标元素 3 来说,所有符合 mycomp2(3, element) 规则的元素都位于其左侧,不符合的元素都位于其右侧,因此 upper_bound() 函数仍可正常执行。

  • 底层实现
    template <class ForwardIterator, class T>
    ForwardIterator upper_bound (ForwardIterator first, ForwardIterator last, const T& val)
    {
        ForwardIterator it;
        iterator_traits<ForwardIterator>::difference_type count, step;
        count = std::distance(first,last);
        while (count>0)
        {
            it = first; step=count/2; std::advance (it,step);
            if (!(val<*it))  // 或者 if (!comp(val,*it)), 对应第二种语法格式
                { first=++it; count-=step+1;  }
            else count=step;
        }
        return first;
    }

资料:
https://deepinout.com/cpp/cpp-questions/g_how-to-declare-comparator-for-set-of-pair-in-cpp.html
https://c.biancheng.net/view/8600.html
https://blog.csdn.net/qq_37529913/article/details/125139815
https://blog.csdn.net/m0_51913750/article/details/128368665

posted @ 2023-11-20 17:27  小海哥哥de  阅读(349)  评论(0编辑  收藏  举报