lambda 表达式

关于C++Lambda表达式的学习记录

LeetCode189:轮转数组

在刷到 LeetCode189:轮转数组的地方,使用 reverse 解决这个问题很方便, 然后看题解使用了Lambda表达式,由于我对C++不是很熟,使用AI学习一下。

下面是题解。

class Solution {
public:
    void rotate(vector<int>& nums, int k) {
        int size = nums.size();
        auto reverse = [&] (int i, int j) {
            while(i < j) {
                swap(nums[i++], nums[j--]);
            }
        };
        k %= size;
        reverse(0, size-1);
        reverse(0, k-1);
        reverse(k, size-1);
    }
};

一、整体语法拆解

先看完整结构:

auto reverse = [&](int i, int j){
    while(i < j){
        swap(nums[i++], nums[j--]);
    }
};

我们把它拆成 5 个核心部分:

1. auto reverse:变量声明

  • auto:C++11 引入的自动类型推导,编译器会根据右侧 lambda 表达式的类型,自动推导 reverse 的类型(本质是 lambda 的闭包类型)。
  • reverse:给这个匿名函数起的 “名字”,后续可以像调用普通函数一样用 reverse(i, j) 调用。

2. [&]:lambda 捕获子句(捕获列表)

这是 lambda 最关键的部分之一,决定了 lambda 内部能访问哪些外部变量,以及访问方式:

  • [&]:表示以引用方式捕获所有外部可见的变量(即 lambda 定义所在作用域的变量)。

  • 这里能直接用 numsswapswap 是全局 / 命名空间函数),就是因为 [&] 捕获了外部的 nums 变量(比如外层函数的 vector / 数组)。

  • 补充捕获方式(常用):

    写法 含义
    [] 不捕获任何外部变量(空捕获)
    [=] 以值拷贝方式捕获所有外部变量
    [&x] 只以引用捕获变量 x
    [x] 只以值拷贝捕获变量 x
    [&,x] 引用捕获所有变量,唯独 x 按值捕获

3. (int i, int j):lambda 参数列表

和普通函数的参数列表完全一致:

  • 声明了两个整型参数 ij,表示要反转的区间的起始索引结束索引(闭区间 [i, j])。
  • 比如调用 reverse(0, 4),就是反转数组索引 0 到 4 的元素。

4. { ... }:lambda 函数体

核心逻辑是反转区间 [i, j] 的元素:

while(i < j){
    swap(nums[i++], nums[j--]);
}
  • 循环条件 i < j:保证只交换到区间中间(避免重复交换)。
  • swap(nums[i++], nums[j--])
    • swap:交换两个元素(C++ 标准库函数,需包含 <algorithm>,或自定义)。
    • i++/j--:交换后,起始索引右移、结束索引左移,直到相遇。

5. 分号 ;:语句结束

lambda 表达式是一个 “表达式”,但赋值给变量时,整体是一条语句,必须以分号结尾。

二、核心特性:lambda 表达式的本质

lambda 是 C++11 引入的匿名函数,本质是:

  1. 编译器会把 lambda 翻译成一个匿名类(闭包类),并创建该类的一个临时对象(闭包对象)。
  2. auto reverse 实际是把这个闭包对象赋值给变量 reverse
  3. 调用 reverse(i, j) 等价于调用闭包类的 operator() 成员函数。

三、使用示例

结合完整场景,比如反转数组的某段区间:

#include <iostream>
#include <vector>
#include <algorithm> // swap 需包含此头文件

using namespace std;

int main() {
    vector<int> nums = {1,2,3,4,5};
    
    // 定义 lambda 函数
    auto reverse = [&](int i, int j){
        while(i < j){
            swap(nums[i++], nums[j--]);
        }
    };
    
    // 调用:反转索引 1 到 3 的元素(2,3,4 → 4,3,2)
    reverse(1, 3);
    
    // 输出结果:1 4 3 2 5
    for(int num : nums) {
        cout << num << " ";
    }
    
    return 0;
}

四、关键注意事项

  1. 捕获的生命周期

    • 因为用 [&] 引用捕获 nums,所以 reverse 的生命周期不能超过 nums(比如 nums 销毁后调用 reverse 会导致野引用)。
    • 如果想避免这个问题,可以用 [=] 值捕获,但 swap 会修改拷贝后的 nums,原数组不会变(按需选择)。
  2. lambda 的可调用性

    • 无捕获的 lambda([])可以隐式转换为函数指针,有捕获的不行。
    • 本例有捕获,只能通过变量 reverse 调用。
  3. 参数和返回值

    • 本例 lambda 无返回值(默认 void),如果需要返回值,可显式指定:

      auto add = [](int a, int b) -> int { return a + b; };
      

五、为什么用这种写法?

  • 简洁:无需单独定义一个全局 / 成员函数,就地定义就地使用。
  • 闭包特性:自动捕获外部变量,无需手动传参(比如本例不用把 nums 作为参数传入 reverse)。
  • 灵活:常用于算法场景(比如排序的自定义比较、局部的小功能函数)。

LeetCode49. 字母异位词分组

官方C++题解的方法二

class Solution {
public:
    vector<vector<string>> groupAnagrams(vector<string>& strs) {
        // 自定义对 array<int, 26> 类型的哈希函数
        auto arrayHash = [fn = hash<int>{}] (const array<int, 26>& arr) -> size_t {
            return accumulate(arr.begin(), arr.end(), 0u, [&](size_t acc, int num) {
                return (acc << 1) ^ fn(num);
            });
        };

        unordered_map<array<int, 26>, vector<string>, decltype(arrayHash)> mp(0, arrayHash);
        for (string& str: strs) {
            array<int, 26> counts{};
            int length = str.length();
            for (int i = 0; i < length; ++i) {
                counts[str[i] - 'a'] ++;
            }
            mp[counts].emplace_back(str);
        }
        vector<vector<string>> ans;
        for (auto it = mp.begin(); it != mp.end(); ++it) {
            ans.emplace_back(it->second);
        }
        return ans;
    }
};

这里使用了C++更加高级的语法,又不会了,过来补充。下面内容生成自豆包。

先明确核心背景:这段代码是为了让 unordered_map 能以 array<int,26> 作为键 —— 而要实现这个目标,必须解决两个问题:

  1. unordered_map 本质是哈希表,需要 “哈希函数” 把键转换成唯一的数值(哈希值);
  2. C++ 标准库没有给 array<int,26> 提供默认的哈希函数,所以必须自己写。

下面我们一步步拆解,先补基础,再讲代码。


第一步:先补你需要的 C++ 基础概念

在看代码前,先搞懂几个核心概念,否则代码里的语法会完全看不懂:

概念 通俗解释
std::array<int,26> 固定长度的数组(长度 26,存 int),和普通数组int arr[26]类似,但更安全(支持边界检查、可拷贝)
unordered_map C++ 的哈希表,存储 “键 - 值对”,要求键必须能计算哈希值(否则无法存 / 取)
哈希函数 把任意类型(比如数组)转换成一个size_t(无符号整数)的函数,目标是 “相同的键→相同哈希值,不同的键→不同哈希值”
Lambda 表达式 匿名函数(没有名字的函数),可以直接定义、直接使用,适合写短小的回调 / 自定义逻辑
std::accumulate STL 的 “累加函数”,遍历一个容器(比如 array),按自定义规则把所有元素 “合并” 成一个值
decltype 类型推导关键字,能 “推导一个变量 / 表达式的类型”(比如 lambda 的类型是匿名的,必须用 decltype 获取)

第二步:逐行拆解代码(从易到难)

我们把代码分成两部分:自定义哈希函数创建带自定义哈希的 unordered_map,逐个细节讲透。

第一部分:自定义哈希函数 arrayHash

auto arrayHash = [fn = hash<int>{}] (const array<int, 26>& arr) -> size_t {
    return accumulate(arr.begin(), arr.end(), 0u, [&](size_t acc, int num) {
        return (acc << 1) ^ fn(num);
    });
};
1. 外层:Lambda 表达式的 “壳”
auto arrayHash = [捕获列表] (参数列表) -> 返回值类型 { 函数体 };
  • auto arrayHash:用auto自动推导变量类型(因为 lambda 的类型是匿名的,没法手动写);
  • [fn = hash<int>{}]:Lambda 的捕获列表(C++14 特性:初始化捕获):
    • hash<int>{}:创建一个 “计算 int 类型哈希值” 的函数对象(标准库提供的,比如hash<int>{}(5)能算出 5 的哈希值);
    • fn = ...:给这个函数对象起个名字fn,并捕获到 lambda 里(这样 lambda 内部能调用fn);
  • (const array<int, 26>& arr):Lambda 的参数 —— 接收一个array<int,26>类型的常量引用(加const&是为了避免拷贝,提升效率);
  • -> size_t:Lambda 的返回值类型 —— 返回size_t(无符号整数,哈希值的标准类型);
2. 内层:std::accumulate 核心逻辑
accumulate(arr.begin(), arr.end(), 0u, [&](size_t acc, int num) {
    return (acc << 1) ^ fn(num);
});

accumulate的作用是 “遍历数组,把所有元素融合成一个值”,它的 4 个参数:

参数 含义
arr.begin(), arr.end() 遍历范围:从数组开头到结尾
0u 累加的初始值:0u表示 “无符号的 0”(u是无符号标记,和size_t类型匹配)
[&](size_t acc, int num) { ... } 自定义的 “累加规则”(又是一个 Lambda):- acc:累加器(保存当前计算的哈希值);- num:遍历到的数组元素(比如字母 'a' 的出现次数);
3. 累加规则的细节:(acc << 1) ^ fn(num)

这是哈希函数的核心,目的是把数组每个元素的特征融合成一个值:

  • acc << 1:把当前哈希值左移 1 位(相当于乘以 2)—— 扩大哈希值的范围,减少 “不同数组算出相同哈希值” 的概率(哈希冲突);
  • fn(num):调用之前捕获的fn,计算当前元素num的哈希值(比如num=2fn(2)会算出 2 对应的哈希值);
  • ^:异或运算符 —— 把 “左移后的哈希值” 和 “当前元素的哈希值” 做异或,融合两者的特征;

举个例子:假设数组是[1,0,0,...,0](对应字符串 "a"),计算过程:

  • 初始acc=0

  • 遍历第一个元素num=1acc = (0 << 1) ^ fn(1) = 0 ^ 哈希(1) = 哈希(1)

  • 遍历剩下的元素num=0acc = (哈希(1) << 1) ^ fn(0),依此类推;

    最终得到一个唯一的哈希值,代表这个数组。

第二部分:创建带自定义哈希的 unordered_map

unordered_map<array<int, 26>, vector<string>, decltype(arrayHash)> mp(0, arrayHash);
1. unordered_map 的模板参数(尖括号里的内容)

unordered_map 的完整模板是:

template<class Key, class T, class Hash = hash<Key>, class KeyEqual = equal_to<Key>, class Allocator = allocator<pair<const Key, T>>>
class unordered_map;

我们这里用到了前 3 个参数:

参数 含义
array<int, 26> Key(键的类型):哈希表的键是长度 26 的 int 数组
vector<string> T(值的类型):哈希表的值是字符串数组(比如存一组字母异位词)
decltype(arrayHash) Hash(哈希函数的类型):- lambda 的类型是匿名的,没法手动写,所以用decltype(arrayHash)推导arrayHash的类型;- 如果不指定这个,编译器会用默认的hash<array<int,26>>,但标准库没有这个特化版本,会编译报错;
2. unordered_map 的构造函数参数(圆括号里的内容)
mp(0, arrayHash)
  • 0:哈希表的 “初始桶数”(bucket count)—— 哈希表内部是用 “桶” 来存数据的,设为 0 表示让系统自动选择初始大小(不影响功能,只是性能调优);
  • arrayHash:传入我们自定义的哈希函数实例 —— 告诉unordered_map:“计算键的哈希值时,用这个 lambda 函数”;
关键为什么必须自定义哈希?

如果直接写:

// 编译报错!!!
unordered_map<array<int,26>, vector<string>> mp;

原因是:C++ 标准库只为intstringdouble等基础类型提供了hash特化版本,没有为array<int,26>提供。所以必须手动告诉编译器 “怎么计算 array 的哈希值”—— 也就是我们写的arrayHash


第三步:用 “人话” 总结整个逻辑(场景化)

假设你要做 “字母异位词分组”(把 eat/tea/ate 分到一组),核心思路是:

  1. 对每个字符串,统计 26 个字母的出现次数(比如 eat→a:1, e:1, t:1,其他 0 → 对应 array<int,26>);
  2. 把 “字母次数数组” 作为键,字符串作为值,存入哈希表 —— 这样相同次数的字符串会自动分到一组;
  3. 但哈希表不认 “字母次数数组” 这个键,所以需要写一个函数(arrayHash),把数组转换成哈希表能识别的数字(size_t);
  4. 最后创建哈希表时,把这个函数传给它,让它知道怎么处理数组键。

第四步:补充新手容易踩的坑

  1. 为什么用const array<int,26>& arr而不是array<int,26> arr

    &是引用,避免拷贝整个数组(26 个 int 虽然少,但养成习惯);加const是保证不会修改传入的数组(只读)。

  2. 0u为什么加u

    0是有符号 int,0u是无符号 unsigned int,和size_t(无符号)类型匹配,避免类型转换警告。

  3. Lambda 的[&]是什么?

    内层 lambda 的[&]是 “按引用捕获外部变量”—— 这里捕获的是外层 lambda 的fn,所以内层能调用fn(num)

  4. 为什么不用普通函数代替 lambda?

    可以,但 lambda 更简洁(不用单独定义函数),且能直接捕获fn,不用额外传参。

posted @ 2025-12-10 14:19  BuerH  阅读(15)  评论(0)    收藏  举报