C++ map和multimap的使用

欢迎来到我的频道 点击跳转专栏

文章目录

  • [1. map类的介绍](#1. map类的介绍)
    • [1.1 pair类型的介绍](#1.1 pair类型的介绍)
      • [0. pair 核心介绍](#0. pair 核心介绍)
      • [1. 核心特性](#1. 核心特性)
      • [2. 基础用法示例](#2. 基础用法示例)
      • [3. pair与 map 的核心关系(接口部分后面会细写)](#3. pair与 map 的核心关系(接口部分后面会细写))
      • [4. 注意](#4. 注意)
    • [1.2 map的构造](#1.2 map的构造)
      • [1. 默认构造](#1. 默认构造)
      • [2. 拷贝构造](#2. 拷贝构造)
      • [3. 移动构造(C++11)](#3. 移动构造(C++11))
      • [4. 范围构造](#4. 范围构造)
      • [5. 初始化列表构造(最常用,C++11)](#5. 初始化列表构造(最常用,C++11))
    • [1.3 map的迭代器和常规接口](#1.3 map的迭代器和常规接口)
    • [1.4 修改类接口(map的增删查等)](#1.4 修改类接口(map的增删查等))
      • [1. insert:插入元素](#1. insert:插入元素)
      • [2. erase:删除元素](#2. erase:删除元素)
      • [3. swap:交换内容](#3. swap:交换内容)
      • [4. clear:清空内容](#4. clear:清空内容)
      • [5. emplace(C++11+):构造并插入元素](#5. emplace(C++11+):构造并插入元素)
      • [6. emplace_hint(C++11+):带位置提示的构造插入](#6. emplace_hint(C++11+):带位置提示的构造插入)
    • [1.5 equal_range](#1.5 equal_range)
      • [1. 核心作用](#1. 核心作用)
      • [2. 用法示例](#2. 用法示例)
      • [3. 与 find/lower_bound/upper_bound的关系](#3. 与 find/lower_bound/upper_bound的关系)
    • [1.6 map的修改获取数据](#1.6 map的修改获取数据)
  • [2. multimap](#2. multimap)
    • [2.1 基本特性](#2.1 基本特性)
    • [2.1 常用成员函数](#2.1 常用成员函数)
      • [1. 插入元素](#1. 插入元素)
      • [2. 查找元素(关键!)](#2. 查找元素(关键!))
    • [2.2 为什么没有 operator[]?](#2.2 为什么没有 operator[]?)
    • [2.3 完整示例](#2.3 完整示例)
    • [2.4 总结](#2.4 总结)
  • 3.例题
    • [3.1 随机链表的复制](#3.1 随机链表的复制)
    • [3.2 前K个高频单词](#3.2 前K个高频单词)

1. map类的介绍

map的声明如下,Key就是map底层关键字的类型,Tmap底层value的类型,set默认要求Key支持小于比较 ,如果不支持或者需要的话可以自行实现仿函数传给第二个模版参数,map底层存储数据的内存是从空间配置器申请的。一般情况下,我们都不需要传后两个模版参数。map底层是用红黑树实现,增删查改效率是 O ( l o g N ) O(logN) O(logN),迭代器遍历是走的中序,所以是按key有序顺序遍历的。

1.1 pair类型的介绍

map底层的红黑树节点中的数据,使用pair<Key, T>存储键值对数据。

0. pair 核心介绍

std::pair 是 C++ 标准库 <utility> 头文件中提供的二元组模板类,用于将两个任意类型的值"绑定"成一个整体,是处理"键值对""二元数据"的基础工具。

1. 核心特性

  • 模板参数template <class T1, class T2> struct pairT1/T2 可以是任意类型(基础类型、自定义类型、容器等)。

  • 成员变量

    • first:第一个值(通常对应 mapkey);
    • second:第二个值(通常对应 map 的 value);
      均为公有成员,可直接访问。
  • 常用构造方式

    cpp 复制代码
    #include <utility>
    #include <string>
    
    // 方式1:默认构造(值默认初始化)
    std::pair<int, std::string> p1; // p1.first=0, p1.second=""
    
    // 方式2:直接初始化 走构造函数
    std::pair<int, std::string> p2(101, "张三"); 
    
    // 方式3:std::make_pair(自动推导类型,推荐)
    auto p3 = std::make_pair(102, "李四"); 
    
    // 方式4:列表初始化(C++11+)
    std::pair<int, std::string> p4{103, "王五"};
  • 核心功能 :支持赋值、比较(默认按 first 比较,first 相等则比较 second)、C++17 结构化绑定等。

2. 基础用法示例

cpp 复制代码
#include <iostream>
#include <utility>
#include <string>

int main() {
    std::pair<int, std::string> student(101, "张三");
    
    // 访问成员
    std::cout << "学号:" << student.first << ",姓名:" << student.second << std::endl;
    
    // 结构化绑定(C++17)
    auto [id, name] = student;
    std::cout << "解构:" << id << " " << name << std::endl;
    
    // 比较
    std::pair<int, std::string> p1(1, "a"), p2(2, "b"), p3(1, "c");
    std::cout << (p1 < p2) << std::endl; // 1(true,first 1<2)
    std::cout << (p1 < p3) << std::endl; // 1(true,first相等,second "a"<"c")
    return 0;
}

3. pair与 map 的核心关系(接口部分后面会细写)

std::map 本质是存储键值对的有序关联容器 ,其底层(红黑树)的每个节点都存储一个 std::pair<const Key, T>,二者是"容器存储单元"与"容器"的强依赖关系,具体体现在以下维度:

维度 具体关联
存储载体 map 的每个元素都是 std::pair<const Key, T>: - first:const 修饰的键(保证键不可修改,符合 map 键唯一且有序的特性); - second:可修改的值。
初始化依赖 map 初始化列表的底层是接收一组 pairstd::map<int, string> m = {``{1, "a"}, {2, "b"}} (本质是隐式类型转换)等价于 {make_pair(1,"a"), make_pair(2,"b")}
遍历/访问返回值 遍历 map 时,迭代器解引用结果是 pair<const Key, T>&map::find() 返回的迭代器也指向 pair
插入操作 map::insert() 接收 pair<Key, T> 作为参数(或 emplace 直接构造 pair): m.insert(make_pair(3, "c")) / m.emplace(3, "c")

4. 注意

  1. map 中 pair 的键是 const
    由于 map 底层红黑树的有序性依赖键,因此 map 存储的 pairfirst(键)被 const 修饰,无法直接修改;若要修改键,需先删除旧键值对,再插入新的。
  2. pair 是 map 操作的基础单元
    mapinsert/emplace/equal_range 等接口均围绕 pair 设计,比如 insert 的返回值是 pair<iterator, bool>(迭代器指向插入/已存在的元素,bool 表示是否插入成功)。
  3. pair 与 map 排序的关联
    map 按键排序的本质是对 pair<const Key, T>first 进行比较;若自定义键类型,需保证 pair 能通过 first 完成比较(重载 < 或传入仿函数)。

1.2 map的构造

map的支持正向和反向迭代遍历,遍历默认按key的升序顺序,因为底层是二叉搜索树,迭代器遍历走的中序;支持迭代器就意味着支持范围formap支持修改value数据,不支持修改key数据,修改关键字数据,破坏了底层搜索树的结构。

1. 默认构造

构造一个空的 map,底层红黑树无节点,仅初始化排序规则(默认 less<Key>)和分配器。

cpp 复制代码
#include <map>
#include <string>

int main() {
    // 默认构造:空 map,键 int,值 string,默认升序排序
    std::map<int, std::string> empty_map;
    
    // 验证空:empty() 返回 true
    std::cout << "是否为空:" << (empty_map.empty() ? "是" : "否") << std::endl; // 输出:是
    return 0;
}

2. 拷贝构造

逐拷贝源 map 的所有键值对(底层红黑树深拷贝),生成一个与源 map 完全相同的新 map(排序规则、元素均一致)。

cpp 复制代码
#include <iostream>
#include <map>
#include <string>

int main() {
    std::map<int, std::string> src_map = {{1, "a"}, {2, "b"}};
    
    // 拷贝构造
    std::map<int, std::string> copy_map(src_map);
    
    // 遍历验证:与 src_map 完全一致
    for (const auto& p : copy_map) {
        std::cout << p.first << ":" << p.second << " "; // 输出:1:a 2:b
    }
    return 0;
}

3. 移动构造(C++11)

"窃取" 源 map 的底层红黑树资源(指针、节点等),源 map 会变为空(浅拷贝 + 资源转移),效率远高于拷贝构造(无内存重新分配)。

cpp 复制代码
#include <iostream>
#include <map>
#include <string>

int main() {
    std::map<int, std::string> src_map = {{1, "a"}, {2, "b"}};
    
    // 移动构造(std::move 标记为右值)
    std::map<int, std::string> move_map(std::move(src_map));
    
    std::cout << "移动后源 map 是否为空:" << (src_map.empty() ? "是" : "否") << std::endl; // 是
    for (const auto& p : move_map) {
        std::cout << p.first << ":" << p.second << " "; // 输出:1:a 2:b
    }
    return 0;
}

4. 范围构造

遍历 [first, last) 迭代器区间的元素(需是 pair<Key, T> 或可转换为该类型 ),将其插入新 map,自动按排序规则去重、排序。

cpp 复制代码
#include <iostream>
#include <map>
#include <vector>
#include <string>

int main() {
    // 先准备一个存储 pair 的 vector
    std::vector<std::pair<int, std::string>> vec = {{3, "c"}, {1, "a"}, {2, "b"}, {1, "a"}};
    
    // 范围构造:从 vec 的迭代器区间创建 map
    std::map<int, std::string> range_map(vec.begin(), vec.end());
    
    // 遍历:自动去重+升序(1:a, 2:b, 3:c)
    for (const auto& p : range_map) {
        std::cout << p.first << ":" << p.second << " "; // 输出:1:a 2:b 3:c
    }
    return 0;
}

5. 初始化列表构造(最常用,C++11)

接收 initializer_list<pair<const Key, T>>,本质是范围构造的语法糖,直接传入键值对列表,底层自动转换为 pair 并插入。

cpp 复制代码
#include <iostream>
#include <map>
#include <string>

int main() {
    // 简化写法(推荐):隐式构造 pair
    std::map<int, std::string> init_map1 = {{1, "a"}, {2, "b"}, {3, "c"}};
    
    // 等价于显式 pair 写法
     std::map<int, std::string> init_map2 = {std::make_pair(1, "a"), std::make_pair(2, "b")};
    
    //也可以传pair
    std::pair<std::string,std::string> kv1("sort", "排序");
    std::pair<std::string,std::string> kv2("left", "左边");
    std::pair<std::string,std::string> kv3("right", "右边");
    std::pair<std::string,std::string> kv4("insert", "插入");
    std::map<string, string> dict{kv1,kv2,kv3};

   //匿名对象
   std::map<std::string,std::string> dict2 {std::pair<std::string,std::string>("sort", "排序"),std::pair<std::string,std::string>("left", "左边")};

    for (const auto& p : init_map)
     {
        std::cout << p.first << ":" << p.second << " "; // 输出:1:a 2:b 3:c
     }
      //C++17 结构化绑定
      for (auto&[k, v] : init_map)
    {
        std::cout << k<< ":" << v<< endl;
    }
//// 输出:1:a 2:b 3:c
    return 0;
}

1.3 map的迭代器和常规接口

这些太常规了 就不讲了 需要可以自行查阅 点击连接转跳

这些接口可以返回 比较键 详情可以参考 set章节

对于find、count、lower_bound、upper_bound等接口 其实跟set都大差不差 也可以参考 set章节

对于value_comp接口 后面会写。

1.4 修改类接口(map的增删查等)

你提供的是std::map修改类接口(Modifiers),这些接口用于对map进行插入、删除、交换等操作,以下是每个接口的作用、用法和核心特点:

1. insert:插入元素

  • 作用 :向map中插入键值对(pair<Key, T>),若键已存在则不插入(保证键的唯一性)。

  • 常用用法

    cpp 复制代码
    // 方式1:插入pair临时对象
    map.insert(pair<int, string>(1, "a"));
    // 方式2:插入make_pair生成的pair
    map.insert(make_pair(2, "b"));
    // 方式3:列表初始化(C++11+)
    map.insert({{3, "c"}, {4, "d"}});
  • 返回值pair<iterator, bool>------

    • iterator:指向插入的元素(或已存在的同键元素);
    • booltrue表示插入成功,false表示键已存在。

补充:其实

2. erase:删除元素

  • 作用:删除map中的元素,支持按"键""迭代器""范围"删除。

  • 常用用法

    cpp 复制代码
    // 方式1:按键删除(返回删除的元素个数,map中最多为1)
    size_t cnt = map.erase(1); 
    // 方式2:按迭代器删除(需保证迭代器有效)
    auto it = map.find(2);//find是通过key去找的
    if (it != map.end()) map.erase(it);
    // 方式3:按范围删除(删除[first, last)区间的元素)
    map.erase(map.begin(), map.find(3));

3. swap:交换内容

  • 作用:交换两个map的所有元素(底层红黑树资源直接交换,效率高)。

  • 用法

    cpp 复制代码
    map<int, string> m1{{1, "a"}}, m2{{2, "b"}};
    m1.swap(m2); // 交换后m1是{2:"b"}, m2是{1:"a"}
  • 注意 :交换的两个map需是同类型(Key、T、Compare、Allocator完全一致)。

4. clear:清空内容

  • 作用:删除map中所有元素,使其变为空map(底层红黑树节点被销毁)。

  • 用法

    cpp 复制代码
    map.clear(); // 清空后map.empty()返回true

5. emplace(C++11+):构造并插入元素

  • 作用 :直接在map内部构造键值对(避免临时pair的拷贝/移动,比insert更高效)。

  • 用法 :直接传入构造pair所需的参数(无需显式创建pair):

    cpp 复制代码
    // 等价于insert(pair<int, string>(1, "a")),但更高效
    map.emplace(1, "a"); 

6. emplace_hint(C++11+):带位置提示的构造插入

  • 作用:在指定迭代器位置的"附近"构造并插入元素,若提示位置准确,可减少红黑树的查找次数,提升效率。

  • 用法 :第一个参数是"提示迭代器",后续参数是构造pair的参数:

    cpp 复制代码
    // 提示在begin()附近插入,若键1确实在begin()附近,插入效率更高
    map.emplace_hint(map.begin(), 1, "a");
  • 注意:提示位置仅作优化,即使提示错误,元素仍会被插入到正确位置(按键排序)。

1.5 equal_range

1. 核心作用

由于 map 的键是唯一 的,equal_range 会返回一个"范围"(实际是包含两个迭代器的 pair):

  • 第一个迭代器(first):指向等于目标键的元素(若存在);
  • 第二个迭代器(second):指向大于目标键的第一个元素;

若目标键不存在,则 firstsecond 都指向该键应该插入的位置 (等价于 lower_bound(key) 的返回值)。

返回值是 pair<iterator, iterator>,其中:

  • pair.first = lower_bound(key)(第一个不小于 key 的元素);
  • pair.second = upper_bound(key)(第一个大于 key 的元素);

2. 用法示例

cpp 复制代码
#include <iostream>
#include <map>
#include <string>

int main() {
    std::map<int, std::string> m = {
        {1, "a"}, {3, "c"}, {5, "e"}
    };

    // 1. 查找存在的键
    auto range1 = m.equal_range(3);
    if (range1.first != range1.second) { // 键存在
        std::cout << "找到键3:" << range1.first->first << ":" << range1.first->second << std::endl;
    }

    // 2. 查找不存在的键
    auto range2 = m.equal_range(2);
    if (range2.first == range2.second) { // 键不存在
        std::cout << "键2不存在,应插入位置:" << range2.first->first << "(之前)" << std::endl;
    }

    return 0;
}

输出

复制代码
找到键3:3:c
键2不存在,应插入位置:3(之前)

3. 与 find/lower_bound/upper_bound的关系

map 中(键唯一),equal_range 等价于:

cpp 复制代码
auto range = make_pair(m.lower_bound(key), m.upper_bound(key));

且:

  • range.first != range.second → 键存在,range.first 就是 find(key) 的返回值;
  • range.first == range.second → 键不存在,range.first 是键的插入位置。
接口 核心作用 返回值 键存在时行为 键不存在时行为 适用场景
find(key) 查找是否存在目标键 迭代器(指向匹配元素 / end() 返回指向该键的迭代器 返回 end() 仅判断键是否存在/获取对应值
lower_bound(key) 找第一个不小于 key 的元素 迭代器(指向首个≥key的元素 / end() 返回指向该键的迭代器 返回键应插入的位置(首个>key的元素) 找键的插入位置/判断键是否"不小于"
upper_bound(key) 找第一个大于 key 的元素 迭代器(指向首个>key的元素 / end() 返回首个>key的元素迭代器 返回键应插入的位置(首个>key的元素) 找目标键的"下一个"元素
equal_range(key) 一次性获取 lower_boundupper_bound pair<迭代器, 迭代器>([first, second)) first=该键,second=首个>key first=second=插入位置 同时需要"键位置+插入位置"/适配multimap

1.6 map的修改获取数据

1.operator[]

  • 这是 std::map 类的一个重载操作符,即 operator[]
  • 它允许通过键(key)来访问或插入元素,类似于数组的下标访问方式。

2.例子

cpp 复制代码
#include <iostream>
#include <map>

int main() {
    std::map<std::string, int> myMap;

    // 插入并获取值
    myMap["apple"] = 5;  // 插入 "apple" -> 5

    // 修改已存在的值
    myMap["apple"] = 10; // 修改为 10

    // 访问不存在的 key,会自动插入默认值(0)
    std::cout << myMap["banana"] << std::endl; // 输出: 0

    return 0;
}

3. 原理


cpp 复制代码
(*((this->insert(make_pair(k, mapped_type())).first)).second

这是 std::map::operator[] 的等价行为描述 ,意思是:

"对这个函数的调用,等价于执行下面这行表达式。"


我们从最外层开始,一层一层地拆解:

  1. make_pair(k, mapped_type())
  • 创建一个 std::pair<const key_type, mapped_type> 对象。
  • k 是传入的键(key)。
  • mapped_type() 是值类型的默认构造函数调用(例如:如果 mapped_typeint,则为 0;如果是 std::string,则为空字符串 "")。
  • 所以这是创建一个键值对:(k, 默认值)
    💡 示例:

如果 map<int, std::string>,那么 make_pair(5, mapped_type()) 就是 (5, "")


  1. this->insert(...)
  • 调用当前 map 对象的 insert 成员函数,插入上面创建的键值对。
  • insert 返回一个 std::pair<iterator, bool>
  • .first 是指向插入位置的迭代器(或已存在的元素)。
  • .second 是布尔值,表示是否成功插入了新元素(true 表示插入,false 表示已有该键)。
    📌 注意:即使键已经存在,insert 不会修改原值,只会返回现有元素的迭代器。

  1. .first
  • 取得 insert 返回的 pair 中的第一个元素 ------ 即一个 iterator,指向 map 中对应键的元素。

  1. *(...)
  • 对迭代器进行解引用(dereference),得到一个 pair<const key_type, mapped_type>& 引用。
  • 即得到了键值对本身。

  1. .second
  • 获取这个键值对中的"值"部分(即 mapped_type 类型的数据)。
  • 返回的是一个 引用,所以可以用来读取或修改值。

总结:

先尝试将键 k 和一个默认构造的值插入到 map 中;

然后获取插入/查找结果的迭代器;

解引用它,再取其 .second(即值部分);

最终返回这个值的引用。


4. 实际效果

这就是 std::map::operator[](const key_type& k)语义等价形式

cpp 复制代码
mapped_type& operator[](const key_type& k) {
    return (*this->insert(make_pair(k, mapped_type())).first).second;
}
  1. 如果键 k 已存在
    • 不插入新元素。
    • 返回已有元素的值的引用。
  2. 如果键 k 不存在
    • 插入一个新的键值对 (k, 默认值)
    • 返回新插入元素的值的引用(可写)。

⚠️:at本质和operator[]本质基本没区别。

2. multimap

std::multimap 是 C++ 标准模板库(STL)中的一种关联容器,与 std::map 类似,但它允许同一个键(key)对应多个值(value)。这是它与 std::map 最核心的区别

2.1 基本特性

特性 说明
键是否唯一 ❌ 不唯一(允许多个相同键)
有序性 ✅ 元素按键自动排序(默认升序)
底层实现 通常为红黑树(Red-Black Tree)
时间复杂度 插入、删除、查找:O(log n)
支持 operator[] 不支持(原因见下文)
头文件 #include <map>

2.1 常用成员函数

1. 插入元素

cpp 复制代码
std::multimap<std::string, int> mm;

mm.insert({"apple", 1});
mm.insert(std::make_pair("apple", 2));
mm.emplace("banana", 3);

所有插入操作都会成功,即使键已存在。


2. 查找元素(关键!)

find(key)

  • 返回第一个匹配键的迭代器。
  • 如果未找到,返回 end()
cpp 复制代码
auto it = mm.find("apple");
if (it != mm.end()) {
    std::cout << it->second; // 输出第一个 "apple" 的值
}

equal_range(key)

  • 返回一个 pair<iterator, iterator>,表示所有匹配键的范围
  • 这是遍历所有同键元素的标准方式。
cpp 复制代码
auto [first, last] = mm.equal_range("apple"); // C++17 结构化绑定
for (auto it = first; it != last; ++it) {
    std::cout << it->second << " ";
}
// 输出: 1 2 (假设插入了两个 "apple")

count(key)

  • 返回指定键的出现次数。
cpp 复制代码
size_t cnt = mm.count("apple"); // 返回 2

2.2 为什么没有 operator[]?

std::multimap 故意不提供 operator[],原因如下:

  1. 语义模糊
    如果 mm["apple"] 存在,它应该返回哪个值?第一个?最后一个?还是所有值?无法确定。
  2. 自动插入行为冲突
    map::operator[] 会在键不存在时插入默认值。但在 multimap 中,插入"一个默认值"会破坏"多值"的设计意图,且用户无法控制插入数量。
  3. 接口一致性
    提供 operator[] 会误导用户以为它像 map 一样是"单值映射",不符合 multimap 的本质。

💡 结论 :使用 find()equal_range() 是访问 multimap 元素的正确方式。


2.3 完整示例

cpp 复制代码
#include <iostream>
#include <map>
#include <string>

int main() {
    std::multimap<std::string, int> scores;

    // 插入多个相同键
    scores.insert({"Alice", 85});
    scores.insert({"Bob", 90});
    scores.insert({"Alice", 92}); // 同一个学生多次成绩
    scores.insert({"Alice", 88});

    // 遍历所有 "Alice" 的成绩
    auto range = scores.equal_range("Alice");
    std::cout << "Alice's scores:\n";
    for (auto it = range.first; it != range.second; ++it) {
        std::cout << "  " << it->second << "\n";
    }
    // 输出:
    // Alice's scores:
    //   85
    //   88
    //   92

    // 统计 Bob 的成绩数量
    std::cout << "Bob has " << scores.count("Bob") << " score(s).\n";

    return 0;
}

2.4 总结

容器 键唯一? 支持 [] 适用场景
std::map ✅ 是 ✅ 是 一对一映射,需要快速按 key 访问/修改
std::multimap ❌ 否 ❌ 否 一对多映射,需要按键分组查询

选择 multimap 当你需要一个键对应多个值 ,并接受使用 equal_range 等方法来访问数据。

3.例题

3.1 随机链表的复制

题目点此转跳


思路:利用maprandomcur->random建立映射关系。

cpp 复制代码
class Solution {
public:
    // 函数:复制一个带有 random 指针的链表
    // 输入:原链表的头节点 head(可能为 nullptr)
    // 返回:新链表的头节点(深拷贝)
    Node* copyRandomList(Node* head) {

        // 1. 创建一个 map,用于建立「原节点 → 新节点」的映射关系
        //    这样我们就能通过原节点的 random 指针快速找到对应的新节点
        map<Node*, Node*> nodeMap;

        // 2. 初始化新链表的头指针和尾指针(用于构建新链表)
        Node* copyhead = nullptr;  // 新链表的头节点
        Node* copytail = nullptr;  // 新链表的尾节点(用于高效尾插)

        // 3. cur 指向原链表当前遍历的节点,从 head 开始
        Node* cur = head;

        // 4. 第一遍遍历:复制所有节点的 val 和 next 关系,并建立映射
        while (cur) {  // 只要 cur 不为空,就继续遍历

            // 5. 如果 copytail 是空,说明新链表还没创建第一个节点
            if (copytail == nullptr) {
                // 6. 创建第一个新节点,值为 cur->val
                //    同时让 copyhead 和 copytail 都指向它
                copyhead = copytail = new Node(cur->val);
            } else {
                // 7. 否则,在 copytail 后面追加一个新节点
                copytail->next = new Node(cur->val);
                // 8. 移动 copytail 到新节点,保持它始终指向新链表尾部
                copytail = copytail->next;
            }

            // 9. 将「原节点 cur」和「对应的新节点 copytail」存入映射表
            //    这是关键!后续设置 random 指针全靠这个映射
            nodeMap[cur] = copytail;

            // 10. 移动 cur 到原链表的下一个节点
            cur = cur->next;
        }

        // 11. 第二遍遍历:设置每个新节点的 random 指针
        //     重置 cur 回到原链表头部
        cur = head;
        // 12. copy 指向新链表当前处理的节点,从 copyhead 开始
        Node* copy = copyhead;

        // 13. 再次遍历原链表(此时新链表结构已完整,只缺 random)
        while (cur) {

            // 14. 如果原节点的 random 是 nullptr,新节点的 random 也设为 nullptr
            if (cur->random == nullptr) {
                copy->random = nullptr;
            } else {
                // 15. 否则,通过映射表 nodeMap 找到原 random 指向节点
                //     对应的新节点,并赋值给 copy->random
                //     例如:cur->random 指向原链表的第3个节点,
                //           nodeMap[cur->random] 就是新链表中对应的第3个节点
                copy->random = nodeMap[cur->random];
            }

            // 16. 同步移动两个指针:cur(原链表)和 copy(新链表)
            cur = cur->next;
            copy = copy->next;
        }

        // 17. 返回新链表的头节点,完成深拷贝
        return copyhead;
    }
};

为什么需要两遍遍历?

必须先创建所有新节点,否则 random 指针可能指向尚未创建的节点。

3.2 前K个高频单词

题目点此转跳

1.法一

cpp 复制代码
class Solution {
public:
    // 自定义比较函数对象(仿函数)
    // 用于定义 pair<string, int> 的排序规则
    struct Compare {
        // 重载 () 运算符,使其可作为函数调用
        bool operator()(const pair<string, int>& kv1, const pair<string, int>& kv2) {
            // 按照 频次(value)降序排列:频次高的排在前面
            return kv1.second > kv2.second;
        }
    };

    // 函数:返回出现频率最高的 k 个单词
    // 要求:若频次相同,则按字典序升序排列(即 "apple" < "zoo")
    vector<string> topKFrequent(vector<string>& words, int k) {
        
        // 1. 使用 map 统计每个单词的出现次数
        //    注意:map 默认按键(string)字典序升序排序
        map<string, int> countMap;
        for (auto& str : words) {
            countMap[str]++;  // 自动初始化为0后加1
        }
        // 此时 countMap 中的元素已按键(单词)字典序排好

        // 2. 将 map 中的所有键值对拷贝到 vector 中,便于后续排序 因为sort要求随机迭代器
        //    vector 元素类型为 pair<string, int>
        vector<pair<string, int>> v(countMap.begin(), countMap.end());

        // 3. 使用 stable_sort 对 vector 进行稳定排序
        //    排序规则由 Compare 定义:按频次降序
        //    关键点:stable_sort 会保持"相等元素"的相对顺序
        //       而由于 v 初始化自 map(已按字典序排好),
        //       所以当两个单词频次相同时,它们在 v 中的原始顺序就是字典序!
        stable_sort(v.begin(), v.end(), Compare());

        // 4. 提取前 k 个单词(此时已满足:高频优先,同频则字典序小的在前)
        vector<string> ret;
        for (int i = 0; i < k; ++i) {
            ret.push_back(v[i].first);  // 只取单词(key)
        }

        return ret;
    }
};

✅ 为什么使用 stable_sort 而不是 sort

  • Compare 只比较 频次(second),不涉及单词(first)。
  • 当两个单词频次相等时,Compare 返回 false(因为 a.second > b.second 不成立),但标准 sort 不保证相等元素的顺序。
  • 而 stable_sort 会保留原始相对顺序。
  • 由于 v 是从 map 构造而来(已按字典序排好),所以:
    • 频次相同时,字典序小的单词在 v 中位置靠前;
    • stable_sort 保持这个顺序 → 最终结果满足"同频按字典序升序"。

当然 如果你偏要用sort(毕竟效率高一点)也可以 我们可以修改一下仿函数。

cpp 复制代码
struct Compare
     {
        bool operator()(const pair<string,int>& kv1,const pair<string,int>& kv2)
        {
            return kv1.second>kv2.second||(kv1.second == kv2.second&& kv1.first<kv2.first);
        }
     };

2.法二

利用priority_queue

cpp 复制代码
class Solution {
public:
    // 自定义比较器:用于构建最小堆(但目标是保留"最大"的 k 个元素)
    struct Compare {
        // 注意:priority_queue 的比较器行为与 sort 相反!
        // 若 comp(a, b) 返回 true,则 a 会被认为"优先级更低",排在堆底
        bool operator()(const pair<string, int>& kv1, const pair<string, int>& kv2) {
            // 我们希望堆顶是"当前最不想要的元素",以便弹出
            // 所以堆应按以下规则排序(小顶堆):
            // 1. 频次低的优先级低(应留在堆底)→ 但我们希望频次高的保留
            //    → 所以当 kv1.freq < kv2.freq 时,kv1 应该在堆底 → 返回 true
            // 2. 频次相同时,字典序大的优先级低(因为题目要求同频时字典序小的优先)
            //    → 所以当 kv1.word > kv2.word 时,kv1 应该在堆底 → 返回 true

            return kv1.second < kv2.second || 
                   (kv1.second == kv2.second && kv1.first > kv2.first);
        }
    };

    vector<string> topKFrequent(vector<string>& words, int k) {
        // 1. 使用 map 统计词频(map 会自动按键字典序升序排序)
        map<string, int> countMap;
        for (auto& str : words) {
            countMap[str]++;
        }
        // 此时 countMap: { "a":2, "b":1, "c":2 } 等,已按字典序排好

        // 2. 构造一个优先队列(堆),使用自定义 Compare
        //    注意:这里直接用整个 countMap 初始化堆!
        //    这会导致堆包含所有唯一单词(n 个),而不是只维护 k 个
        priority_queue<pair<string, int>, vector<pair<string, int>>, Compare> 
            pq(countMap.begin(), countMap.end());

        // 3. 从堆中取出前 k 个元素(堆顶是"当前最符合要求"的?)
        vector<string> ret;
        for (int i = 0; i < k; ++i) {
            ret.push_back(pq.top().first); // 取单词
            pq.pop();                      // 弹出
        }

        return ret;
    }
};
相关推荐
Elias不吃糖8 小时前
Java Lambda 表达式
java·开发语言·学习
guygg888 小时前
一级倒立摆MATLAB仿真程序
开发语言·matlab
暮色_年华8 小时前
随想 2:对比 linux内核侵入式链表和 STL 非侵入链表
linux·c++·链表
情缘晓梦.8 小时前
C语言指针进阶
java·开发语言·算法
世转神风-9 小时前
qt-字符串版本与数值版本互转
开发语言·qt
极客代码9 小时前
深入解析C语言中的函数指针:原理、规则与实践
c语言·开发语言·指针·状态机·函数·函数指针
w-w0w-w9 小时前
C++模板参数与特化全解析
开发语言·c++
不绝1919 小时前
C#核心:继承
开发语言·c#
大锦终10 小时前
递归回溯综合练习
c++·算法·深度优先