├── C++面经 ├── C++面经.assets │ ├── image-20220624165105805.png │ ├── image-20220722123357635.png │ └── image-20220722130957400.png └── C++面经.md ├── README.md ├── SLAM知识点汇总 ├── SLAM相关问题答案部分.assets │ ├── image-20220609194504641.png │ ├── image-20220612113516403.png │ ├── image-20220613205633074.png │ ├── image-20220623160559132.png │ ├── image-20220623210720807.png │ ├── image-20220623210740171.png │ ├── image-20220623213330203.png │ ├── image-20220623214547408.png │ └── image-20220623223009837.png └── SLAM相关问题答案部分.md ├── leetcode刷题笔记 ├── 刷题笔记.assets │ ├── image-20220725170827235.png │ └── image-20220725172355076.png └── 刷题笔记.md ├── 选择题等其他零碎知识点 └── 综合知识点.md └── 项目经历 ├── 陈嘉皓.pdf └── 项目经历及简历介绍.md /C++面经/C++面经.assets/image-20220624165105805.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfkiwl/slam_and_leetcode/47120bfea76f6f53bb2a003aeae16ce1318c4432/C++面经/C++面经.assets/image-20220624165105805.png -------------------------------------------------------------------------------- /C++面经/C++面经.assets/image-20220722123357635.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfkiwl/slam_and_leetcode/47120bfea76f6f53bb2a003aeae16ce1318c4432/C++面经/C++面经.assets/image-20220722123357635.png -------------------------------------------------------------------------------- /C++面经/C++面经.assets/image-20220722130957400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfkiwl/slam_and_leetcode/47120bfea76f6f53bb2a003aeae16ce1318c4432/C++面经/C++面经.assets/image-20220722130957400.png -------------------------------------------------------------------------------- /C++面经/C++面经.md: -------------------------------------------------------------------------------- 1 | ## C++ 面经整理 2 | 3 | [TOC] 4 | 5 | #### C++程序编译过程 6 | 7 | 1. 编译过程分为四个过程:预处理 + 编译 + 汇编 + 链接 8 | 9 | + 预处理:生成翻译单元(后缀名为`.i`,源文件 + 相关头文件(直接/ 间接)- 应忽略的预处理语) 10 | + 编译:将源码 .cpp 文件翻译成 .s 汇编代码; 11 | + 汇编:将汇编代码翻译成机器指令 .o 文件(目标文件); 12 | + 链接:将这些文件对应的目标文件连接成一个整体,从而生成可执行的程序文件。 13 | 14 | 2. 链接分为两种: 15 | + 静态链接:以.lib或者.a为后缀。在链接阶段将各种库文件和相关文件集成到可执行文件中,本质上是在编译链接时直接将需要的执行代码拷贝到调用处 16 | + 优点:装载速度很快,运行速度比动态链接快; 17 | + 缺点:可执行文件很大;执行文件较大 ; 18 | + 动态链接:以.so为后缀。把对一些库函数的链接载入推迟到程序运行的时期,可以实现进程之间的资源共享,本质上在编译时记录一系列符号和参数,将动态库加载到内存中,在运行到指定代码时,在共享执行内存中加载的动态库可执行代码,实现运行时链接; 19 | + 优点:可执行文件较小;开发耦合性较小,程序更新部署不需要全部编译; 20 | + 缺点:速度没有静态链接快;不具有自完备性,如果用户缺少动态库则无法运行; 21 | 22 | #### 指针和引用的区别是什么 23 | 24 | + 指针可以为空;引用必须绑定对象,总的来说比指针安全。 25 | + 指针所指向的内存空间在程序运行过程中可以改变;而引用所绑定的对象一旦绑定就不能改变。 26 | + 指针可以有多级,但是引用只能一级,即不存在引用的引用。(是否能为多级) 27 | + 引用属于编译期概念,在底层还是通过指针实现 28 | + 如果返回动态内存分配对象,必须用指针,否则可能引起内存泄漏。 29 | 30 | #### 变量的声明和定义有什么区别 31 | 32 | 变量的定义为变量分配地址和存储空间, 变量的声明不分配地址。一个变量可以在多个地方声明,加入extern 修饰的是变量的声明,说明此变量将在翻译单元外部进行定义; 33 | 34 | #### nullptr 比 NULL 优势 35 | 36 | nullptr 比 NULL 更安全,它是有类型的,而`NULL`是预处理变量,是一个宏,值是 0;在函数重载等情况下NULL容易被误解 37 | 38 | #### 讲述下C++中的左值和右值,移动语义和完美转发 39 | 40 | 1. 左右值的基础类型: 41 | 42 | image-20220624165105805 43 | 44 | + glvalue:泛左值,标识一个对象、位或者函数,可以是`lvalue`和`xvalue`; 45 | + prvalue:纯右值,用于初始化对象或作为操作数 46 | + rvalue:将亡值,表示其资源可以被重新使用; 47 | + const 对应的变量也是一个左值;T&&对应右值引用; 48 | 49 | 2. 右值引用:形如`int && a`,右值最大的特点就是无名且不需要存储空间。 50 | 51 | 3. 移动语义:`std::move()`,将一个泛左值修改为一个将亡值,后续将不再继续修改该数值。特别适合对独占指针的移动; 52 | 53 | 4. 完美转发:`std::forward`,它的作用是保持原来的值属性不变,如果原来的值是左值,经std::forward处理后该值还是左值;如果原来的值是右值,处理后它还是右值。[参考资料](https://zhuanlan.zhihu.com/p/161039484) 54 | 55 | #### 完美转发是用来解决什么问题的? 56 | 57 | 完美转发为了解决引用折叠问题,引用折叠就是:如果间接创建一个引用的引用,那么这些引用就会折叠. 一个右值引用是左值。 58 | 59 | #### 什么是浅拷贝和深拷贝? 60 | 61 | 浅拷贝就是增加了一个新指针指向原来的地址,那么改变原有对象也会改变新对象。而深拷贝则是开辟了新的内存空间,并增加一个指向该空间的指针。 62 | 63 | #### 类型的自动推导:auto 和 decltype 64 | 65 | + auto: 最常用的形式,但会产生类型退化。特别是在引用中,当定义另外一个引用等于当前引用时,自动推导实际上是新建了一个变量,而非对原始变量的另外一个引用;auto不能用于含有递归的匿名函数。 66 | 67 | + decltype(.):不会产生类型退化的类型推导。如果是左值则加引用 ,如果左值是变量名称则不加 68 | 69 | + decltype(exp/val):返回表达式/变量的类型,可以通过括号来设计是否引用 70 | 71 | ```c++ 72 | *ptr -> int; 73 | decltype(*ptr) -> int&; 74 | // 通过括号来设计是否引用 75 | decltype((int)) -> int&; 76 | decltype(int) -> int; 77 | ``` 78 | 79 | + decltype(auto) :从 c++14 开始支持,简化 decltype 使用 80 | 81 | + concept auto :从 C++20 开始支持,表示一系列类型( std::integral auto x = 3; 自动类型推导会被限制在`std::integral`类型中) 82 | 83 | #### C++有什么编译优化参数? 84 | 85 | + O0 不做任何优化,这是默认的编译选项。 86 | + O1 优化会消耗少多的编译时间,它主要对代码的*分支,常量以及表达式*等进行优化。 87 | + O2 会尝试更多的*寄存器级的优化以及指令级的优化*,它会在编译期间占用更多的内存和编译时间。 88 | + O3 在O2的基础上进行更多的优化,例如使用伪寄存器网络,*普通函数的内联*,以及*针对循环的更多优化*。 89 | 90 | #### 什么时候会发生段错误 91 | 92 | + 访问了不存在的地址,比如试图修改null指针的值 93 | + 访问了受保护的地址:`int *p = 0; (*p) = 1;` 94 | + 试图修改只读区,比如修改字面值常量 95 | + 栈溢出,无限递归 96 | + new一次但是delete多次。 97 | 98 | #### 讲一下内存溢出和内存泄露 99 | 100 | 1. 内存溢出:是指申请内存时申请的空间不够,比如申请的int型的内存空间,但是存进去一个long型的数据。 101 | + 内存中加载的数据量过于庞大,比如一次性从数据库中读取大量数据。 102 | + 代码中出现死循环或者循环产生过多重复的实体。 103 | + 启动参数内存值设定的过小。 104 | 105 | 2. 内存泄露:是指申请内存空间之后,没有释放申请的内存空间。 106 | + new创建出来的对象没有及时的delete掉,导致了内存的泄露; 107 | + delete一个`void*`的指针可能会造成内存上的泄露。因为delete一个`void*`的对象指针,它不会调用析构函数,导致内存泄漏。 108 | + new创建了一组对象数组,内存回收的时候却只调用了delete而非delete []来处理,导致内存泄露; 109 | 110 | #### 堆和栈有什么区别? 111 | 112 | + 堆是动态分配的,其空间的分配和释放都由程序员控制,栈是栈是由编译器自动管理的; 113 | + 堆来说,频繁使用分配内存会造成内存空间的不连续,产生大量碎片,栈不会,因为栈具有先进后出的特性; 114 | + 栈顶和栈底是预设好的,大小固定,堆是不连续的内存区域,其大小可以灵活调整; 115 | + 堆是从内存的低地址向高地址方向增长,栈是从内存的高地址向低地址方向增长; 116 | 117 | #### 内存的分配方式有几种? 118 | 119 | + 在**栈**上分配:**局部变量**在栈上分配,函数结束时会自动释放。**效率很高,但内存容量有限**; 120 | + 从**堆**上分配:由程序去控制; 121 | + 从**常量存储区**分配:特殊的存储区,存放的是常量,不可修改; 122 | + 从**全局/静态存储区**分配:全局变量和静态变量被分配到同一块内存中,在该区定义的变量若没有初始化,则会被自动初始化 123 | 124 | #### 静态内存分配和动态内存分配有什么区别 125 | 126 | + 静态内存分配是在编译时期完成的,动态内存分配是在运行时期完成的; 127 | + 静态内存分配是在栈上分配的(或者全局变量、常量);动态内存分配是在堆上分配的; 128 | + 静态内存分配是按计划分配的,在编译前确定内存块的大小;动态内存分配是按需要分配的; 129 | + 静态内存分配的运行效率比动态内存分配高 130 | + 静态内存分配不需要指针或引用类型的支持;动态内存分配需要; 131 | 132 | #### 指针常量和常量指针有什么区别? 133 | 134 | 以`*`为界,const出现在左侧,内容不改变;const出现在右侧,指针不修改; 135 | 136 | ```c++ 137 | int x = 4; 138 | int* const ptr = &x; // ptr本身不能改变 139 | const int* ptr = &x; // ptr指向的内容不改变 140 | ``` 141 | 142 | #### this指针 143 | 144 | + this指针指向被调用的成员函数所属的对象 145 | 146 | + this指针隐式的加在每个成员函数中 147 | 148 | #### lambda表达式 149 | 150 | lambda 表达式:小巧灵活,功能强大 151 | 152 | + C++11 ~ C++20 持续更新: 153 | 154 | + C++11 引入lambda 表达式 155 | + C++14 支持初始化捕获、泛型lambda 156 | + C++17 引入constexpr lambda ,*this 捕获 157 | + C++20 引入concepts ,模板lambda 158 | 159 | + **底层实现**: lambda 表达式会被编译器翻译成类进行处理 160 | 161 | + **基本组成部分**:参数与函数体、返回类型、捕获、说明符、模板形参(C++20 ) 162 | 163 | + example: 164 | 165 | ```c++ 166 | // 显示的指出返回类型 167 | auto x = [](int val) -> bool 168 | { 169 | return val > 3 && val < 10; 170 | }; 171 | // 隐式自动推导返回类型 172 | auto x = [](int val){ return val > 3;}; 173 | std::cout << x(5) << std::endl; 174 | ``` 175 | 176 | + 捕获:针对函数体中使用的局部自动对象进行捕获。存在值捕获、引用捕获与混合捕获,缺省捕获;this 捕获;初始化捕获(C++14 );*this 捕获(C++17 )多种方式 177 | 178 | ```c++ 179 | // 局部值捕获, static不需要捕获 180 | int y = 10; 181 | auto x = [y](int val) -> bool { return val > y; }; 182 | // 引用捕获 183 | auto x = [&y](int val) -> bool { return val > y; }; 184 | // 编译器分析用到了哪些变量,传递方式为值捕获,适用于传入值比较多的情况; 185 | auto x = [=](int val) -> bool { return val > y; }; 186 | // 编译器分析用到了哪些变量,传递方式为引用捕获,适用于传入值比较多的情况; 187 | auto x = [&](int val) -> bool { return val > y; }; 188 | // 一般在类中使用,调用类 189 | auto x = [&, this](int val) -> bool { return val > y; }; 190 | // this可能会内存不安全,但是会有复制的开销 191 | auto x = [&, *this](int val) -> bool { return val > y; }; 192 | // 初始化捕获,初始化只需要执行一次,而如果不使用初始化构造则会多次操作; 193 | auto res = [z = x + y](int val) -> bool { return val > z; }; 194 | ``` 195 | 196 | + 说明符:mutable / constexpr (C++17) / consteval (C++20) 197 | 198 | ```c++ 199 | // 编译不通过,无法修改y,因为相当于 函数const 200 | auto x = [y](int val) 201 | { 202 | y++; 203 | return val > y; 204 | }; 205 | // 编译通过,函数内部参数可改 206 | auto x = [y](int val) mutable 207 | { 208 | y++; 209 | return val > y; 210 | }; 211 | // constexpr保证优先在编译期阶段进行调用,consteval 保证只能在编译期进行调用 212 | auto lam = [](int val) constexpr 213 | { 214 | return val++; 215 | }; 216 | constexpr int val = lam(100); 217 | ``` 218 | 219 | + 模板形参(c++20):之前可以用auto解决 220 | 221 | ```c++ 222 | // 模板 223 | auto lam = [](T val){ return val++; }; 224 | int val = lam(100); 225 | // auto 226 | auto lam = [](auto val){ return val++; }; 227 | ``` 228 | 229 | + 深入用法: 230 | 231 | 1. 即调用函数表达式:一般用于初始化常量表达式计算上 232 | 233 | ```c++ 234 | const auto val = [z = x + y](int val){ return z; }(); 235 | ``` 236 | 237 | #### 为什么要引入泛型算法?有什么特点? 238 | 239 | + 为什么要引入泛型算法?如`std::sort(x.begin(),x.end())`而不采用方法`x.sort()`的形式: 240 | 241 | 1. 方法是指类里面实现的函数,但是内建数据类型不支持方法 242 | 2. 计算逻辑存在相似性,采用了模板的方式,避免重复定义 243 | 244 | + 泛型算法的特点: 245 | 246 | + 使用迭代器作为算法与数据的桥梁,因此实现支持多种类型 247 | + 泛型算法通常来说都不复杂,但优化足够好 248 | + 一些泛型算法与方法同名,实现功能类似,此时建议调用方法而非算法。如:std::find V.S. std::map::find 249 | 250 | #### bind操作 251 | 252 | bind :基于已有的逻辑灵活适配,但描述复杂逻辑时语法可能会比较复杂难懂 253 | 254 | + **功能**:C++11 引入,用于修改可调用对象的调用方式 255 | + **example**:如`std::bind(fun, 0, std::placeholders::_1, 2)`表示函数固定了函数的第1个参数为0,第3个参数为2;第2个参数(也是占位符第1个参数)需要填写; 256 | + **缺点**:调用std::bind 时,传入的参数会被复制,这可能会产生一些调用风险;只能绑定一个函数,因此不支持重载(lambda可以用auto或者模板解决); 257 | 258 | #### 说一说extern“C” 259 | 260 | extern “C”的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern “C”后,会指示编译器这部分代码按C语言(而不是C++)的方式进行编译。 261 | 262 | #### C 语言的关键字 static 和 C++ 的关键字 static 有什么区别 263 | 264 | 在 C 中 static 用来修饰局部静态变量和外部静态变量、函数。而 C++中除了上述功能外,还用来定义类的成员变量和函数。即静态成员和静态成员函数。 265 | 266 | #### 悬挂指针与野指针有什么区别? 267 | 268 | - 悬挂指针:当指针所指向的对象被释放,但是该指针没有任何改变,以至于其仍然指向已经被回收的内存地址,这种情况下该指针被称为悬挂指针; 269 | - 野指针:未初始化的指针被称为野指针。 270 | 271 | #### static的作用是什么,什么情况下用到static? 272 | 273 | + static修饰类变量:该变量仅在每个类(而不是每个实例)中存在一次,并且仅会在.cpp文件中被定义。该变量可以在类中声明,但是必须要在类外定义。 274 | + static修饰类函数:不需要通过类即可访问该函数。 275 | + 局部变量用static声明,则是为该变量分配的空间在整个程序的执行期内都始终存在;外部变量用static来声明,则该变量的作用只限于本文件模块,且编译器会自动对其初始化。 276 | 277 | #### 初始化列表的价值: 278 | 279 | 如果类中存在一个const 变量,如果不使用初始化列表方法,那么将会因为已经预先创建而报错。 280 | 281 | #### 智能指针解决的问题: 282 | 283 | + 堆中指针导致的**内存泄露**:当指向堆的指针改变时,由于无法自动管理堆中数据,且该部分数据空间无法再次通过指针等其他形式再次访问和删除,将造成内存泄露 284 | 285 | + 堆中指针导致的**指针悬空**:当两个堆指针指向同一块内存,删除某一个指针,另一个指针不清楚内存已经被释放,仍然试图访问,造成悬空 286 | 287 | #### 内存对齐的概念?为什么会有内存对齐? 288 | 289 | 1. 内存对齐:编译器将程序中的每个“数据单元”安排在字的整数倍的地址指向的内存之中 290 | 291 | 2. 内存对齐的原则: 292 | 293 | + 结构体变量的首地址能够被其最宽基本类型成员大小与对齐基数中的较小者所整除; 294 | + 结构体每个成员相对于结构体首地址的偏移量 (offset) 都是该成员大小与对齐基数中的较小者的整数倍,如有需要编译器会在成员之间加上填充字节 (internal padding); 295 | + 结构体的总大小为结构体最宽基本类型成员大小与对齐基数中的较小者的整数倍,如有需要编译器会在最末一个成员之后加上填充字节 (trailing padding)。 296 | 297 | 3. 内存对齐的优点: 298 | 299 | + 便于在不同的平台之间进行移植,因为有些硬件平台不能够支持任意地址的数据访问; 300 | 301 | + 提高内存的访问效率,因为 CPU 在读取内存时,是一块一块的读取。 302 | 303 | #### inline 内联函数的特点有哪些?与define相比如何? 304 | 305 | 1. 特点: 306 | 307 | + 编译器在编译期直接对函数进行了类似替换的行为,只用于简单的函数,避免了栈的生成和销毁造成性能的提升; 308 | 309 | + 使用`inline`进行声明时,需要在翻译单元内进行定义,最好是只有定义;inline是将程序级别的一次定义变成了翻译单元的一次定义。 310 | 311 | + 它可以避免相同函数重写多次的麻烦,编译器在链接的时候就不会重复进行定义,而是会将重复定义的函数自动删除;它减少了运行时间但是增加了空间开销。 312 | + 如果函数体内有循环,那么执行函数代码时间比调用开销大。 313 | 314 | 2. 与define相比:内联函数在编译时展开,而宏是由预处理器对宏进行展开;内联函数会检查参数类型,宏定义不检查函数参数 ,所以内联函数更安全。 315 | 316 | #### C++的容器原理及优缺点 317 | 318 | 容器包括:**序列容器,关联容器,适配器生成器** 319 | 320 | ##### 序列容器 321 | 322 | 1. **array** :元素**个数固定**的序列容器; 323 | 324 | + **底层实现**:底层依靠数组实现; 325 | + **容器的功能特点**:由于维护数组而非指针,swap速度较慢 326 | 327 | 2. **vector** :元素**连续存储**的序列容器; 328 | 329 | + **底层实现**:vector底层是一个动态数组,包含三个迭代器,start和finish之间是已经被使用的空间范围,end_of_storage是整块连续空间包括备用空间的尾部; 330 | + **内存增长机制**:当空间不足时,会自动申请另一片更大的空间(1.5倍或者2倍),然后把原来的数据拷贝到新的内存空间,接着释放原空间。注意:一旦引起了空间的重新配置(主要在写操作中),指向原vector的所有迭代器会都失效了; 331 | + **内存清空机制**:使用clear()命令时,其存储空间不释放,仅仅是清空了里面的数据;使用shrink_to_fit()命令时可以降低其capacity和size匹配。同时使用两条指令可以清空内容并释放内存; 332 | + **容器的功能特点**:vector 不提供push_front / pop_front,因为效率太差;swap 效率较高,因为值交换指针,并不会因为长度变化而变化; 333 | + **注意事项**:vector的底层实现要求连续的对象排列,引用并非对象,没有实际地址,因此vector的元素类型不能是引用。 334 | 335 | + **vector迭代器失效的情况** 336 | 337 | + 当插入一个元素到vector中,由于引起了内存重新分配,所以指向原内存的迭代器全部失效。 338 | + 当删除容器中一个元素后,该迭代器所指向的元素已经被删除,那么也造成迭代器失效。erase方法会返回下一个有效的迭代器,所以当我们要删除某个元素时,需要it=vec.erase(it);。 339 | 340 | + **vector内存的释放方式** 341 | 342 | + vec.clear():清空内容,但是不释放内存。 343 | 344 | + vec.shrink_to_fit():请求容器降低其capacity和size匹配。 345 | 346 | + vec.clear();vec.shrink_to_fit();:清空内容,且释放内存。 347 | 348 | 3. **forward_list / list** :基于**链表 / 双向链表**的容器; 349 | 350 | + **list 底层实现**:list的底层是一个双向链表,以结点为单位存放数据,结点的地址在内存中不一定连续,每次插入或删除一个元素,就配置或释放一个元素空间。 351 | + **list 容器的功能特点**:list不支持随机存取,插入、删除成本较低,但随机访问成本较高;写操作通常不会改变迭代器的有效性;支持`splice`等切片插入等; 352 | + **forward_list容器特点**:单向链表,是一个成本较低的线性表实现,因此只支持递增操作,不支持size操作,不支持pop_back / push_back; 353 | 354 | 4. **deque** :**vector 与list 的折衷** 355 | 356 | + **底层实现**:deque是一个双向开口的连续线性空间(双端队列),相当于分成连续空间段,每段之间用链表连接。 357 | + **容器的功能特点**:push_back / push_front 速度较快;在序列中间插入、删除速度较慢;deque的迭代器比vector迭代器复杂很多; 358 | 359 | 5. **basic_string** :提供了对**字符串**专门的支持; 360 | 361 | ##### 关联容器 362 | 363 | 1. map 、set、multiset、multimap的特点 364 | 365 | + **底层实现**:set / map / multiset / multimap 底层使用红黑树实现, 366 | 367 | + 有序,查找的时间复杂度O(logn); 368 | 369 | + 红黑树特点:搜查树(左子树小于根节点,右子树大于根节点)+平衡树(左子树和右子树最多相差1层);遍历时采用了中序遍历,因此是有序的; 370 | 371 | + map/set 迭代器所指向的对象是const 的,不能通过其修改元素; 372 | 373 | + multiset / multimap:使用 lower_bound / upper_bound / equal_range 返回查找到的区间 374 | 375 | 2. unordered_xx: 376 | 377 | + **底层实现**:是一个防冗余的哈希表(采用除留余数法)。哈希表最大的优点,就是把数据的存储和查找消耗的时间大大降低,时间复杂度为O(1);而代价仅仅是消耗比较多的内存。 378 | + **容器特点**:与set / map 相比查找性能更好,但插入操作一些情况下会慢; 379 | 380 | 3. **为何map和set的插入删除效率比其他序列容器高,而且每次insert之后,以前保存的iterator不会失效**? 381 | 382 | 因为存储的是结点,不需要内存拷贝和内存移动。插入操作只是结点指针换来换去,结点内存没有改变。而iterator就像指向结点的指针,内存没变,指向内存的指针也不会变 383 | 384 | ##### 适配器 385 | 386 | 功能:把不同类型统一到相同类型进行处理 387 | 388 | 1. **类型适配器** 389 | 390 | + **basic_string_view** (C++17 ): 391 | 392 | 统一了c++(string)和c字符串(char*),使接口更加灵活,代价也非常cheap;但是拒绝进行写操作,仅仅是string的一个窗口,一般也不会把string_view作为函数返回值; 393 | 394 | ```c++ 395 | void fun(std::string_view s); 396 | .... 397 | fun("123456"); 398 | fun(std::String("123456")); 399 | // 一个非常好的局部处理操作,不需要重新构建string 400 | std::string s = "123456"; 401 | fun(std::String_view(s.begin(), s.begin()+3)); 402 | ``` 403 | 404 | + **span** (C++20 ):可基于C 数组、array,vector等构造(连续空间),并且可读写 405 | 406 | 2. **接口适配器** 407 | 408 | + **stack**(栈,后进先出,底层实现是deque),**queue**(队列,先进先出,底层实现是deque),**priority_queue**(优先队列,底层实现是vector) 409 | + 对底层序列容器进行封装,对外展现栈、队列与优先级队列的接口 410 | 411 | 3. **数值适配器**(c++20,gcc10):可以将一个输入区间中的值变换后输出 412 | 413 | + ranges库相关函数,std::ranges::XXX_view, std::ranges::views::XXX, std::views::XXX 414 | 415 | + 评价:比较好用,但是不需要gcc10不太现实诶 416 | 417 | + example: 418 | 419 | ```c++ 420 | bool isEven(int i){ return i%2 == 0 }; 421 | ... 422 | // 对vec中每个元素执行isEven,只有当结果为true时候才执行下一步 423 | for (auto p:std::ranges::filter_view(vec, isEven)){ 424 | cout << p << endl; 425 | } 426 | ``` 427 | 428 | ##### 生成器(c++20) 429 | 430 | + 可以在运行期生成无限长或有限长的数值序列 431 | 432 | + std::ranges::itoa_view, std::ranges::views::itoa, std::views::itoa 433 | 434 | + example: 435 | 436 | ```c++ 437 | // 生成1-9 438 | for (int i : std::ranges::itoa_view{1,10}){ 439 | ... 440 | } 441 | // or 442 | for (int i : std::views::itoa(1,10)){ 443 | ... 444 | } 445 | ``` 446 | 447 | #### 智能指针有几种?分别介绍一下他们的底层实现 448 | 449 | + 指针函数: 450 | 451 | 1. `ptr.get()`:返回智能指针管理的原始指针 452 | 2. `ptr.reset(raw_ptr)`:指针与指向原来对象对应的引用计数减1,若引用计数减为0则释放该对象内存;指针与指向新对象对应的引用计数加1; 453 | 3. `ptr.reset()`:指针与指向原来对象对应的引用计数减1,若引用计数减为0则释放该对象内存; 454 | 4. `ptr.use_count()`:查看计数器。 455 | 456 | + `shared_ptr`:基于计数的共享指针解决方案 457 | 458 | 1. 包含一个指针和一个计数器,允许拷贝。但是每拷贝一次,计数器加一,每析构一次,计数器减一,为零时销毁。 459 | 460 | 2. 不同方式构建函数: 461 | 462 | ```c++ 463 | #include 464 | // Using default constructor Type(); 465 | auto p = std::shared_ptr(new Type); 466 | auto p = std::make_shared(); // Preferred 467 | // Using constructor Type(); 468 | auto p = std::shared_ptr(new Type()); 469 | auto p = std::make_shared(); // Preferred 470 | std::shared_ptr p(new int[10],std::default_delete()); 471 | ``` 472 | 473 | 3. 指定函数析构 474 | 475 | ```c++ 476 | std::shared_ptr y(new int(100),fun); 477 | 478 | void fun(int* ptr){ 479 | std::cout << "Delete Ptr" << std::endl; 480 | delete ptr; 481 | } 482 | ``` 483 | 484 | + `unique_ptr`:独占内存的解决方案 485 | 486 | 1. 没有拷贝构造函数,可以使用`std::move()`进行移动,但是不能赋值 487 | 488 | 2. 指定析构函数: 489 | 490 | ```c++ 491 | std::unique_ptr x(new int(3), fun); 492 | ``` 493 | 494 | + `weak_ptr`:防止循环引用而引入的智能指针 495 | 496 | 1. 循环引用:内存不会被销毁。一般用于类似于图的结构 497 | 498 | ```c++ 499 | // 错误用法:x,y,m_nei都是shared_ptr 500 | x->m_nei = y; 501 | y->m_nei = x; 502 | // 错误用法:x,y是shared_ptr, m_nei是weak_ptr 503 | ``` 504 | 505 | 2. 基于shared_ptr 构造,但是并不会额外增加计数值:具体使用详见 lock 方法 506 | 507 | + 数组智能指针: 508 | 509 | 1. unique_ptr的数组智能指针,没有*和->操作,但支持下标操作[] 510 | 511 | 2. shared_ptr的数组智能指针,有*和->操作,但不支持下标操作[],只能通过get()去访问数组的元素。 512 | 513 | 3. shared_ptr的数组智能指针中:C++11必须要自定义deleter,C++17 支持`shared_ptr `,C++20 支持`make_shared`分配数组 514 | 515 | + 常见错误: 516 | 517 | ```c++ 518 | int a = 0; 519 | // Same happens with std::shared_ptr. 520 | auto a_ptr = std::unique_ptr(&a); 521 | ``` 522 | 523 | 因为:此时a是位于栈上的而不是位于堆上。使用堆指针的最佳方法还是new一个新的; 524 | 525 | #### 讲一下四种类型转换符 526 | 527 | + `static_cast(T2)` :将T2转换为T1,但是不是所有类型都能完全转换;它可以在编译期执行; 528 | + `dynamic_cast(T2)`:在运行期执行,更加安全,但是性能稍差;可以用于在类的继承层次之间进行类型转换,它既允许向上转型,就是把继承类指针转换为基类指针;也允许向下转型。向上转型是无条件的,不会进行任何检测;向下转型的前提必须是安全的,只有一部分能成功。 529 | + `const_cast`:增加或者去除常量性(注意,可以去除); 530 | + `reinterpret_cast`:对对应内存空间进行重新解释,类似于eigen中的Eigen::Map,但是也要遵循相关规范,一般只支持指针; 531 | 532 | #### const和constexpr有什么区别? 533 | 534 | 传统const的问题在于“双重语义”,既有“只读”的含义,又有“常量”(不可改变)的含义,而constexpr严格定义了常量。 535 | 536 | #### emplace_back()和push_back()哪个更好,为什么? 537 | 538 | emplace_back()更好,因为它调用的是移动构造函数。而push_back()调用的是拷贝构造函数。移动构造函数不需要分配新的内存空间,所以更快一些。特别体现在对类的处理上。 539 | 540 | #### 讲一下capacity(), size(), reserve(), resize() 函数的区别。 541 | 542 | + size()用于返回容器当前的元素个数。而capacity()返回容器的容量。 543 | 544 | + reserve只是开辟空间并不创建元素。而resize重新开辟空间并自动初始化元素。reserve()改变了capacity,size保持不变;resize()既改变了capacity,又改变了size。 545 | 546 | + resize(x,val): 547 | + x > capacity,那么会在原容器内补充x-capacity个值为val的元素; 548 | + x <= capacity,那么容器内前x个元素值变为为val,其余不变 549 | 550 | #### 谈一谈你对zero overhead(零开销原则)的理解 551 | 552 | 1. 不需要为没有使用的语言特性付出成本(虚函数) 553 | 2. 使用了一些语言特性不等于付出运行期成本(`consteval`:编译期执行,c++20特性) 554 | 555 | #### 什么是STL?STL由哪些组件组成 556 | 557 | STL由6个组件和13个头文件组成: 558 | 559 | + **容器**:一些封装数据结构的模板类,比如vector,list等。 560 | + **算法**:它们被设计为一个个模板函数,大部分位于 ,小部分位于。 561 | + **函数对象**:如果一个类将()重载为成员函数,那么这个类称为函数对象类,类的对象称为仿函数。 562 | + **迭代器**:容器对数据的读写是通过迭代器完成的,它充当容器和算法之间的胶合剂。 563 | + **适配器**:将一个类的接口设计成用户指定形式。 564 | 565 | #### 内存管理 566 | 567 | C++ 内存分区:**栈、堆、全局/静态存储区、常量存储区、代码区**。 568 | 569 | + 栈内存的特点:更好的局部性,对象自动销毁 570 | + 堆内存的特点:运行期动态扩展,需要显式释放 571 | 572 | #### STL 的迭代器失效,怎么解决 573 | 574 | 1. 插入操作: 575 | + 对于vector和string,如果容器内存被重新分配,iterators,pointers,references失效;如果没有重新分配,那么插入点之前的iterator有效,插入点之后的iterator失效; 576 | + 对于deque,如果插入点位于除front和back的其它位置,iterators,pointers,references失效;当我们插入元素到front和back时,deque的迭代器失效,但reference和pointers有效; 577 | 2. 删除操作: 578 | + 对于vector和string,删除点之前的iterators,pointers,references有效;off-the-end迭代器总是失效的; 579 | + 对于deque,如果删除点位于除front和back的其它位置,iterators,pointers,references失效;当我们插入元素到front和back时,off-the-end失效,其他iterators,pointers,references有效; 580 | 581 | #### new 和 malloc 的区别 582 | 583 | + new包括三个步骤:operator new( )分配内存,调用构造函数,返回指针 584 | + malloc 继承自c语言,相当于C++中的分配器(allocator ),只能够分配内存,但是不能构建对象;33 585 | 586 | #### 为什么析构函数必须是虚函数?为什么默认的析构函数不是虚函数? 587 | 588 | + 效果:类似于`Father* p = new Son;`,如果基类不是虚函数,那么析构只会调用基类;如果基类是虚函数,则会先析构派生类再析构基类; 589 | + 为什么不默认虚函数:虚函数是有代价的,额外生成了虚函数表和虚表指针,无需清除时这种内存开销无疑是浪费的,因此一般是**使用基类指针**进行删除时才需要声明虚函数。 590 | 591 | #### 面向对象的三大特征是哪些 592 | 593 | 封装:将客观事物封装成抽象的类,而类可以把自己的数据和方法暴露给可信的类或者对象,对不可信的类或对象则进行信息隐藏。 594 | 595 | 继承:可以使用现有类的所有功能,并且无需重新编写原来的类即可对功能进行拓展; 596 | 597 | 多态:一个类实例的相同方法在不同情形下有不同的表现形式,使不同内部结构的对象可以共享相同的外部接口。 598 | 599 | #### 多态的实现有哪几种? 600 | 601 | 多态分为静态多态和动态多态。其中,静态多态是通过重载和模板技术实现的,在编译期间确定;动态多态是通过虚函数和继承关系实现的,执行动态绑定,在运行期间确定。 602 | 603 | #### 动态绑定是如何实现的? 604 | 605 | 1. 虚表:具有一个基础的typeinfo,可以通过`dynamic_cast`来进行基类到派生类的类型转换(派生类到基类直接引用和指针指向即可),而其他派生不支持多态; 606 | 2. 当编译器发现类中有虚函数时,会创建一张虚函数表存放在全局数据区中,把虚函数的函数入口地址放到虚函数表中,并且在对象中增加一个指针`vptr`,用于指向类的虚函数表。当派生类覆盖基类的虚函数时,会将虚函数表中对应的指针进行替换,从而调用派生类中覆盖后的虚函数,从而实现动态绑定。 607 | 608 | #### 虚函数表是针对类的还是针对对象的? 609 | 610 | + 虚函数表是针对类的,类的所有对象共享这个类的虚函数表; 611 | + 每个对象内部都保存一个指向该类虚函数表的指针`vptr`,每个对象的`vptr`的存放地址都不同,但都指向同一虚函数表。 612 | 613 | #### 动态多态有什么作用?有哪些必要条件? 614 | 615 | 1. 动态多态的作用: 616 | + 隐藏实现细节,使**代码模块化**; 617 | + 接口重用,使派生类的功能可以被基类的指针/引用所调用,即**向后兼容**; 618 | 2. 动态多态的必要条件:**继承、虚函数、需要有基类指针/引用指向子类对象**; 619 | 620 | #### 如果类A是一个空类,那么sizeof(A)的值为多少? 621 | 622 | `sizeof(A)`的值为1,因为编译器需要区分这个空类的不同实例,分配一个字节,可以使这个空类的不同实例拥有独一无二的地址。 623 | 624 | #### 覆盖(重写)和重载之间有什么区别? 625 | 626 | + 重写的函数名、参数列表、返回类型与父类完全相同,只是函数体不同;覆盖只发生在派生类的成员函数中; 627 | + 重载是指两个函数具有相同的函数名,不同的参数列表,不关心返回值; 628 | 629 | #### 模板特化 630 | 631 | 对于某种特定类型,如果要实现其特有的功能,单一模板就无法做到,这时就需要模板特例化;模板类特化可以选择部分特化、完全特化等; 632 | 633 | #### RTTI是什么?其原理是什么? 634 | 635 | RTTI即运行时类型识别,其功能由两个运算符实现: 636 | 637 | + `typeid`运算符,用于返回表达式的类型,可以通过基类的指针获取派生类的数据类型; 638 | + `dynamic_cast`运算符,具有类型检查的功能,用于将基类的指针或引用安全地转换成派生类的指针或引用。 639 | 640 | #### C++如何实现只在堆/栈上实例化对象 641 | 642 | 1. 只在堆上实例化对象:**将析构函数设置为保护**/私有。编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性。若析构函数不可访问,则不能在栈上创建对象。**然后,设置静态成员变量进行创建和析构;** 643 | 644 | 2. 只能在栈上实例化对象:**将 new 和 delete 重载为私有**。 645 | 646 | ```c++ 647 | class A 648 | { 649 | private: 650 | void* operator new(size_t t){} // 注意函数的第一个参数和返回值都是固定的 651 | void operator delete(void* ptr){} // 重载了new就需要重载delete 652 | public: 653 | A(){} 654 | ~A(){} 655 | }; 656 | ``` 657 | 658 | #### new/delete和malloc/free之间有什么关系? 659 | 660 | 1. new一般分为两步:内存分配和调用构造;而malloc只有内存分配,不会调用构造;delete与free同理。 661 | 2. new与delete直接带具体类型的指针;malloc和free返回void类型的指针。 662 | 3. new类型是安全的,会进行编译器检查;而malloc不是。 663 | 664 | #### 友元 friend 665 | 666 | 1. 功能:外部函数在类中被标记为友元时,可以访问类内所有成员函数和变量。 667 | 2. 隐藏友元:即使友元在类内进行定义,但是作用域仍然是全局的(类外的); 668 | 3. 继承与友元:**友元关系无法继承,但基类的友元可以访问派生类中基类的相关成员** 669 | 670 | #### 模板形参的万能引用(T &&) 671 | 672 | 1. 不再像普通函数中表示右值:同时处理传入参数是左值或右值的情形,推荐使用; 673 | 674 | + 如果实参表达式是右值,那么模板形参被推导为去掉引用的基本类型 675 | + 如果实参表达式是左值,那么模板形参被推导为左值引用,触发引用折叠 676 | 677 | 2. 完美转发应用:函数中使用万能引用作为函数形参时,右值引用的变量是左值,只有使用完美转发才能避免 678 | 679 | ![image-20220722123357635](C++面经.assets/image-20220722123357635.png) 680 | 681 | #### 了解RAII吗?介绍一下? 682 | 683 | + RAII:资源获取初始化,是C++语言的一种管理资源、避免泄漏的用法。编程使用系统资源时,都必须遵循一个步骤:申请资源、使用资源、释放资源。 684 | 685 | + RAII典型案例: 686 | 1. 资源管理:智能指针 shared_ptr与weak_ptr 自动内存管理。 687 | 2. 状态管理:线程锁、unique_lock、lock_guard\、mutex互斥量、 688 | 689 | #### 变长模板(Variadic Template ) 690 | 691 | 1. 形参包 692 | 693 | ```c++ 694 | // case1: 695 | template 696 | void fun(){}; 697 | fun<1,3,5>(); 698 | 699 | // case2: 700 | template 701 | void fun(T... args){ 702 | std::cout << sizeof...(args); // 包含了多少个参数 703 | }; 704 | fun(3, 5.3, 'c'); 705 | 706 | // case3: 707 | template class... T> 708 | void fun(){}; 709 | 710 | // case4: c++11标准包展开操作 711 | void fun_ref() { 712 | /* 结尾为空时退出 */ 713 | cout << "pargs size = 0\t Do nothing, quit." << endl; 714 | } 715 | template void fun_ref(Ts& value, Us&... pargs) { 716 | /* 每次读取最前侧的值 */ 717 | cout << "pargs size = " << sizeof...(pargs) + 1 718 | << "\tcurrent value type = " << typeid(Ts).name() 719 | << "\tcarrent value = " << value 720 | << "\n"; 721 | // 递归调用,每次调用后 value 取代 pargs 第一个参数,直至为空 722 | fun_ref(pargs...); 723 | } 724 | 725 | // case5: c++17折叠表达式 726 | template void fun_ref(Us&... pargs) { 727 | ((cout << "\tcarrent value = " << pargs << endl), ...); 728 | } 729 | ``` 730 | 731 | 2. 通过包展开技术操作变长模板参数:参考cppreference 732 | 733 | 3. [折叠表达式](https://en.cppreference.com/w/cpp/language/fold) 734 | 735 | ```c++ 736 | template 737 | int sum(Args&&... args) 738 | { 739 | // Error: operator with precedence below cast 740 | // return (args + ... + 1 * 2); 741 | 742 | // Right: 743 | return (args + ... + (1 * 2)); 744 | } 745 | ``` 746 | 747 | + 基于逗号的折叠表达式应用 748 | + 折叠表达式用于表达式求值,无法处理输入(输出)是类型与模板的情形 749 | 750 | #### C++11为什么引入enum class? 751 | 752 | 枚举值不能隐式地转换成其他类型 753 | 754 | #### 平时会用到function、bind、lambda吗,都什么场景下会用到? 755 | 756 | 1. **function:代替回调函数中的函数指针** 757 | 758 | 2. **bind :用于修改可调用对象的调用方式** 759 | 760 | + **example**:如`std::bind(fun, 0, std::placeholders::_1, 2)`表示函数固定了函数的第1个参数为0,第3个参数为2;第2个参数(也是占位符第1个参数)需要填写; 761 | + **缺点**:调用std::bind 时,**传入的参数会被复制**,这可能会产生一些调用风险;可以使用std::ref 或std::cref 避免复制的行为;**只能绑定一个函数**,因此不支持重载(lambda可以用auto或者模板解决); 762 | 763 | 3. **lambda 表达式**: 764 | 765 | + 底层实现:会被编译器翻译成类进行处理 766 | 767 | + 组成:参数与函数体、返回类型、捕获、说明符 768 | 769 | + 捕获:针对函数体中使用的局部自动对象进行捕获。存在值捕获、引用捕获与混合捕获,缺省捕获;this 捕获;初始化捕获(C++14 );*this 捕获(C++17 )多种方式 770 | 771 | ```c++ 772 | // 局部值捕获, static不需要捕获 773 | int y = 10; 774 | auto x = [y](int val) -> bool { return val > y; }; 775 | // 引用捕获 776 | auto x = [&y](int val) -> bool { return val > y; }; 777 | // 编译器分析用到了哪些变量,传递方式为值捕获,适用于传入值比较多的情况; 778 | auto x = [=](int val) -> bool { return val > y; }; 779 | // 编译器分析用到了哪些变量,传递方式为引用捕获,适用于传入值比较多的情况; 780 | auto x = [&](int val) -> bool { return val > y; }; 781 | // 一般在类中使用,调用类 782 | auto x = [&, this](int val) -> bool { return val > y; }; 783 | // this可能会内存不安全,但是会有复制的开销 784 | auto x = [&, *this](int val) -> bool { return val > y; }; 785 | // 初始化捕获,初始化只需要执行一次,而如果不使用初始化构造则会多次操作; 786 | auto res = [z = x + y](int val) -> bool { return val > z; }; 787 | ``` 788 | 789 | #### 多线程的锁 790 | 791 | 1. **mutex互斥锁** 792 | 793 | + 使用:互斥锁是一种简单的通过加锁的方式控制多个线程对共享资源的访问,互斥锁有两个状态,上锁与解锁(lock和unlock)。 794 | + 特点:互斥锁是一个原子操作,不会出现同时上锁的情况,同时,互斥锁具有唯一性,一旦上锁,其他线程不能够再将其锁住。 795 | + 缺点:如果在lock和unlock之间发生了异常,则可能永远不会执行到unlock,另一个进程将永远被挂起在那里等待。 796 | 797 | 2. **std::lock_guard** 798 | 799 | + 使用:自动释放锁,其原理是:声明一个局部的lock_guard对象,在其构造函数中进行加锁,在其析构函数中进行解锁。 800 | 801 | + example: 802 | 803 | ```c++ 804 | mutex mut; 805 | void thread1() 806 | { 807 | while(share<20) 808 | { 809 | std::lock_guard mtx_locker(mut); //用lock_guard实现互斥锁 810 | if(share>=20) 811 | break; 812 | share++; 813 | cout << "this is thread1! share is " << share << endl; 814 | Sleep(100); 815 | } 816 | } 817 | ``` 818 | 819 | 3. **unique_lock** 820 | 821 | + unique_lock基本用法和lock_guard一致,在构造函数和析构函数中进行锁操作,不同的地方在于它提供了非常多构造函数。 822 | + image-20220722130957400 823 | 824 | #### 大端小端的区别、应用、如何判断大端小端 825 | 826 | + 大端:big endian,先高位再低位(高位在地址的前端, 67 45 23 01) 827 | + 小端:little endian,先低位再高位( 01 23 45 67) 828 | + C++根据硬件需求自动选择大端还是小端;Java根据规范确定大端还是小端,具有易用性但是性能会牺牲 829 | 830 | #### C++17 新特性: 831 | 832 | + 结构化绑定:一种语法糖,如`auto [u,v] = fun(a, b);` 833 | 834 | + 带初始化的 if 和 switch 语句: 835 | 836 | ```c++ 837 | // 带有初始化的if语句应用 838 | if (status s = check(); s != status::success) { 839 | return s; 840 | } 841 | // 初始化switch的应用 842 | namespace fs = std::filesystem; 843 | ... 844 | switch (fs::path p{name}; status(p).type()) { 845 | case fs::file_type::not_found: 846 | std::cout << p << " not found\n"; 847 | break; 848 | case fs::file_type::directory: 849 | std::cout << p << ":\n"; 850 | for (const auto& e : std::filesystem::directory_iterator{p}) { 851 | std::cout << "- " << e.path() << '\n'; 852 | } 853 | break; 854 | default: 855 | std::cout << p << " exists\n"; 856 | break; 857 | } 858 | ``` 859 | 860 | + 自从 C++17 开始,可以在头文件中以 inline 的方式定义全局变量/对象 861 | 862 | ```c++ 863 | class MyClass { 864 | inline static std::string msg{"OK"}; // OK(自C++17起) 865 | ... 866 | }; 867 | 868 | inline MyClass myGlobalObj; // 即使被多个CPP文件包含也OK 869 | ``` 870 | 871 | + 在 C++ 里不允许在类里初始化非常量静态成员,可以在类定义的外部定义并初始化非常量静态成员,但如果被多个 CPP 文件同时包含的话又会引发新的错误; 872 | 873 | + 新属性和属性特性 874 | 875 | + `[[nodiscard]] `属性:保证返回值一定被调用。常用于函数中开辟内存空间,避免不使用返回值造成内存泄露 876 | 877 | ```C++ 878 | [[nodiscard]] int fun(int a, int b); 879 | ``` 880 | 881 | + ` [[maybe_unused]]`属性:该变量可能不会被使用; 882 | 883 | + `[[fallthrough]]`属性:避免编译器在 switch 语句中某一个标签缺少 break 语句时发出警告; 884 | 885 | + 属性现在可以用来标记命名空间,如用来舍弃命名空间: 886 | 887 | ```c++ 888 | namespace [[deprecated]] DraftAPI { 889 | ... 890 | } 891 | ``` 892 | 893 | + 嵌套命名空间: 894 | 895 | ```c++ 896 | namespace A::B::C { 897 | ... 898 | } 899 | 等价于: 900 | namespace A { 901 | namespace B { 902 | namespace C { 903 | ... 904 | } 905 | } 906 | } 907 | ``` 908 | 909 | 910 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # slam_and_leetcode 2 | 一个SLAM算法方向学生的痛苦之旅,包括leetcode刷题、c++面经和SLAM相关知识点。 3 | 4 | 目前项目正在完善中,欢迎各位共同协作!我的联系方式(微信):nsdl1035 5 | -------------------------------------------------------------------------------- /SLAM知识点汇总/SLAM相关问题答案部分.assets/image-20220609194504641.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfkiwl/slam_and_leetcode/47120bfea76f6f53bb2a003aeae16ce1318c4432/SLAM知识点汇总/SLAM相关问题答案部分.assets/image-20220609194504641.png -------------------------------------------------------------------------------- /SLAM知识点汇总/SLAM相关问题答案部分.assets/image-20220612113516403.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfkiwl/slam_and_leetcode/47120bfea76f6f53bb2a003aeae16ce1318c4432/SLAM知识点汇总/SLAM相关问题答案部分.assets/image-20220612113516403.png -------------------------------------------------------------------------------- /SLAM知识点汇总/SLAM相关问题答案部分.assets/image-20220613205633074.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfkiwl/slam_and_leetcode/47120bfea76f6f53bb2a003aeae16ce1318c4432/SLAM知识点汇总/SLAM相关问题答案部分.assets/image-20220613205633074.png -------------------------------------------------------------------------------- /SLAM知识点汇总/SLAM相关问题答案部分.assets/image-20220623160559132.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfkiwl/slam_and_leetcode/47120bfea76f6f53bb2a003aeae16ce1318c4432/SLAM知识点汇总/SLAM相关问题答案部分.assets/image-20220623160559132.png -------------------------------------------------------------------------------- /SLAM知识点汇总/SLAM相关问题答案部分.assets/image-20220623210720807.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfkiwl/slam_and_leetcode/47120bfea76f6f53bb2a003aeae16ce1318c4432/SLAM知识点汇总/SLAM相关问题答案部分.assets/image-20220623210720807.png -------------------------------------------------------------------------------- /SLAM知识点汇总/SLAM相关问题答案部分.assets/image-20220623210740171.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfkiwl/slam_and_leetcode/47120bfea76f6f53bb2a003aeae16ce1318c4432/SLAM知识点汇总/SLAM相关问题答案部分.assets/image-20220623210740171.png -------------------------------------------------------------------------------- /SLAM知识点汇总/SLAM相关问题答案部分.assets/image-20220623213330203.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfkiwl/slam_and_leetcode/47120bfea76f6f53bb2a003aeae16ce1318c4432/SLAM知识点汇总/SLAM相关问题答案部分.assets/image-20220623213330203.png -------------------------------------------------------------------------------- /SLAM知识点汇总/SLAM相关问题答案部分.assets/image-20220623214547408.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfkiwl/slam_and_leetcode/47120bfea76f6f53bb2a003aeae16ce1318c4432/SLAM知识点汇总/SLAM相关问题答案部分.assets/image-20220623214547408.png -------------------------------------------------------------------------------- /SLAM知识点汇总/SLAM相关问题答案部分.assets/image-20220623223009837.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfkiwl/slam_and_leetcode/47120bfea76f6f53bb2a003aeae16ce1318c4432/SLAM知识点汇总/SLAM相关问题答案部分.assets/image-20220623223009837.png -------------------------------------------------------------------------------- /SLAM知识点汇总/SLAM相关问题答案部分.md: -------------------------------------------------------------------------------- 1 | ## SLAM相关问题答案 2 | 3 | [TOC] 4 | 5 | ### IMU测量方程是什么?噪声模型是什么? 6 | 7 | 1. 测量方程: 8 | $$ 9 | \phi = \frac{\omega_{k-1}+\omega_k}{2}(t_k-t_{k-1})\\ 10 | R_{wb_k} = R_{wb_{k+1}}\left(I+\frac{\sin\phi}{\phi}\phi_{\times}+\frac{\cos\phi}{\phi^{2}}(\phi_{\times})^2\right) \\ 11 | q_{wb_k} =q_{wb_{k+1}}\otimes\begin{bmatrix}\cos\frac{\phi}{2}\\\frac{\Phi}{\phi}\sin\frac{\phi}{2} \end{bmatrix}\\ 12 | v_k = v_{k-1}+(\frac{R_{wb_k}a_k+R_{wb_{k-1}}a_{k-1}}{2})(t_k-t_{k-1})\\ 13 | p_k = p_{k-1} + \frac{v_k+v_{k-1}}{2}(t_k-t_{k-1}) 14 | $$ 15 | 16 | 2. 噪声模型: 17 | 18 | IMU误差可以分为:确定性误差,随机误差。 19 | 20 | + **确定性误差**可以事先标定确定,包括:**bias**(为0时的恒定偏置 b), **scale**(实际/理想),**非对齐误差**(scale + Misalignment :3*3矩阵,对角阵为尺度因子)等; 21 | + **随机误差**通常假设噪声服从高斯分布,包括:**高斯白噪声**,**bias随机游走等**; 22 | 23 | 总体而言: 24 | 25 | + **加速度计的误差模型**:$a^b_m=S_aR_{g}^a(a^G-g^G)+n_a+b_a$,其中$a^b_m$表示body系下加速度测量量,$g^G = [0,0, −9.81]^{\top}$。 26 | + **陀螺仪的误差模型**:$\omega^b_m=S_gw^b+n_g +b_g+s^g_{a}a^b$,最后一项仅仅在低端产品中出现。 27 | 28 | ### 惯导的误差模型推导?(15维) 29 | 30 | 1. 推导方法: 31 | 32 | + 写出不考虑误差的微分方程;(观测) 33 | + 写出考虑误差的微分方程;(预测) 34 | + 写出带误差的值与理想值之间的关系;(预测 = 观测 + 变化) 35 | + 将3) 中的关系带入 2) 36 | + 把1) 中的关系带入 4) 37 | 38 | 2. 推导结果: 39 | 40 | image-20220609194504641 41 | 42 | ### 多传感器之间如何对时? 43 | 44 | 这个问题相对比较复杂,首先分为两部分吧,时间戳的表述方式,和时间对齐方式。 45 | 46 | 首先是时间戳的表达方式,一般比较常见的包括Unix 时间戳,它记录的是UTC时间,或者是GPS时间戳,常用在GPS对时上,表达的是原子钟时间,然后根据时区的不同,可能还有相应的加减处理等; 47 | 48 | 对齐方式,首先说说实验室级别的对齐,这个要根据传感器不同来分类: 49 | 50 | + 激光雷达:通常采用PPS脉冲信号+NMEA消息。GPS时钟源的PPS端口每秒发送一次硬件脉冲(PPS信号),随后数据端口发送一次对应这个脉冲上升沿的时间信息(GPRMC格式);当激光雷达接收到PPS信号时,会去置零微秒定时器。当合法有效的GPRMC信号到来的时候,系统抽取GPRMC信号中的时间,换算成整秒,修正激光雷达整秒的时间戳 51 | + 相机同步: 52 | + IMU、相机使用同一个时钟晶振:这样做的好处是不用考虑太多额外的因素,但是要求IMU和相机距离足够接近(这在自动驾驶场景下是不现实的),没有其他干扰,也不需要其他设备进行同步。 53 | + 以IMU时钟触发Camera曝光,适合比较简单的VIO系统 54 | + 工业级别: 55 | + 有一个专门的时钟源,晶振非常高,作为稳定时钟源,并采用卫星信号进行矫正;同时构造一个触发装置,在指定的时刻,发送触发信号,让所有的传感器触发成像,减少成像时刻误差。 56 | + NTP,PTP网络时间同步:利用网络报文进行矫正,前者能达到毫秒级,后者能够达到微秒级别; 57 | 58 | ### GPS双天线安装标定 59 | 60 | 车辆走直线后,采用GPS计算航迹,最小二乘算一下偏角。然后平移直接用尺子量就行。 61 | 62 | ### GPS存在延时,而IMU和轮速计都是实时的,如何处理? 63 | 64 | 这个方法没有亲身实现过,但是听别人讲过:大概就是把IMU当成预测,GPS或者Lidar里程计结果当成观测,用简单的卡尔曼滤波进行融合一下;这个也适用于SLAM计算过慢的情况。 65 | 66 | ### 常见的点云注册(registration)方法 67 | 68 | #### 1. ICP算法系列 : 69 | 70 | + **点到点的ICP**: 71 | + **公式**:$(R,t)=\arg\min\sum_{i=0}^N\omega_i||Rp_i+t-q_i||^2$,详细推导见资料:[SVD分解法](D:\网络课程\多传感器融合和定位 深蓝学院\第2章 3D激光里程计 I\svd_rot.pdf)。其中,权重$\omega_i$表示$p_i,q_i$之间的欧式距离是否小于手动阈值$d_{max}$,若小于则认为1,否则为0; 72 | + **优点**:定位精度高; 73 | + **缺点**:计算开销大且速度慢,对初始变换敏感,容易陷入局部最优解,会有离群点及噪声;采样场景不均匀时(如长走廊等环境)容易被产生误差造成发散;此外,由于需要记录点云,内存消耗大,不能适用于超大规模场景; 74 | + **点到线的ICP**:找到最近邻的两点,两点连线,以点到线的距离作为误差,[相关资料](https://mp.weixin.qq.com/s/r0YQa4uMUamIla2-dDy3fw) 75 | + **公式**:$(R,t)=\arg\min\sum_{i=0}^N\omega_i||n_i^{\top}(Rp_i+t-q_i)||^2$。其中,$n_i^{\top}$是最近邻两点连线的法线; 76 | + **优点**:误差度量方式更符合结构化场景中的雷达点云的实际情况,因此具有更小的误差; 77 | + **缺点**:它对非常大的初始位移误差的鲁棒性较差,因此需要比较精确的初始值。 78 | 79 | + **点到面的ICP**: 80 | 81 | + **公式**:$(R,t)=\arg\min\sum_{i=0}^N\omega_i||\eta_i(Rp_i+t-q_i)||^2$;式中,$\eta_i$是$q_i$处的表面法线 82 | + **优点**:点云的局部结构,精度更高,不容易陷入局部最优 83 | + **缺点**:优化是一个非线性问题,速度比较慢; 84 | 85 | + **广义迭代最近邻(GICP)**:ICP只考虑每一个点的残差,并没有考虑每个点的局部特征。要把ICP改为GICP只需要将欧氏距离替换为马氏距离。因此,也可以将GICP看做面到面的ICP 86 | 87 | + **公式**:$T = \arg_T\min\sum_i^Nd_{i}^{(T)}\ ^{\top}(C_i^B+T^{*}C_i^A(T^{*})^{\top})^{-1}d_{i}^{(T)}$ 88 | 89 | + **流程**: 90 | 91 | 1. 设点集B为点A1的K临近(K=20),计算B均值; 92 | 93 | 2. 将B中每一个量减去均值即可计算出error矩阵: 94 | 95 | image-20220612113516403 96 | 97 | 3. 构造协方差矩阵来描述A1的局部特征:$\Sigma=\frac{ee^{\top}}{K}$,并对协方差$\Sigma$进行SVD分解; 98 | 99 | 4. 找到3个量中最小的量$\xi$,即是法向量的特征值,令其等于一个超小量,比如1e-3,再将其他两个量设为1,得到新的$\Sigma^{1}$; 100 | 101 | 5. 得到新的$\Sigma^{1}$后,利用SVD重构源协方差矩阵$\Sigma$,得到$C_i^A$或者$C_i^B$; 102 | 103 | 6. 利用GICP代价公式求解; 104 | 105 | + **改进**:FastGICP,[文献解析](https://zhuanlan.zhihu.com/p/453290973),[代码开源1](https://github.com/SLAMWang/fasterGICP),[代码开源2(推荐)](https://github.com/SMRT-AIST/fast_gicp);主要是剔除了平面性不强的点云(GICP基于平面假设近似协方差矩阵),数据关联点云滤波(残差较小的关联结果对位姿优化的贡献较少)。 106 | 107 | + **NICP**:充分利用实际曲面的特征来对错误点进行了滤除,主要使用的特征为法向量和曲率;[文献解析](https://blog.csdn.net/shoufei403/article/details/102972842),[代码开源](https://github.com/yorsh87/nicp): 108 | 109 | + **公式**:$(R,t)=\arg\min\sum_{i=0}^N\omega_i\left\|\begin{bmatrix}Rp_i+t\\ Rn_i\end{bmatrix}-\begin{bmatrix}p_i^{'}\\n_i^{'}\end{bmatrix}\right\|^2_{\Sigma}$ 110 | + **流程**: 111 | 1. 前几步同GICP的1-3步; 112 | 2. 定义曲率$\sigma=\lambda_1/(\lambda_1+\lambda_2)$; 113 | 3. 点云过滤:如果没有well define的法向量,则拒绝(即选择比较结构化的点,如果对应点周围过于杂乱就丢弃该点);两点间的欧式距离($p$)大于阈值,则拒绝;两点的曲率($\log\sigma$)之差距大于阈值,则拒绝;两点的法向量角度($n$)之差大于阈值,则拒绝。 114 | + **优点**:可以提前排除 一些明显是错误的匹配。这样就减少了计算量并且提高了计算结果的精度 115 | 116 | + **其他**:[VGICP](),[tdr-gicp](https://zhuanlan.zhihu.com/p/433141609) 117 | 118 | #### 2. NDT算法系列: 119 | 120 | + **P2D-NDT**:点到分布的NDT。将目标点云栅格化,每个栅格内的点云使用高斯分布来拟合。点到分布的NDT的目标函数是,找到一个变换T,使得待配准的点云经过变换后落入Target的栅格的高斯分布的似然概率最高。 121 | 122 | + **公式**:$\arg\min\sum_i^{N}(Ry_i+t-\mu)^{\top}\Sigma^{-1}(Ry_i+t-\mu)$ 123 | + **流程**: 124 | 1. 地图网格生成:将地图点云按照固定的分辨率划分到不同的三维网格中 125 | 2. 计算每个网格点云的均值与协方差 126 | 3. 投影与近邻关系搜索:根据初始位姿将待匹配点云投影到NDT地图中,并搜索得到与每个匹配点最近的地图网格; 127 | 4. 计算代价值:根据每个待匹配点及其搜索到的地图网格,计算代价值,并且使用非线性迭代计算优化 128 | + **参数**:P2D-NDT对初值和栅格大小敏感,通常使用一个粗分辨率;NDT迭代次数少,一般5步内即可收敛,可以完全实时; 129 | + **优缺点**:比ICP更加鲁棒,计算开销更小;长于定位任务,弱于里程计任务;擅长处理平移,但在处理旋转方面表现一般; 130 | + [相关源码解析](https://mp.weixin.qq.com/s/TW50l7lThIogPV9H5qeAnw) 131 | 132 | + **D2D-NDT**: 133 | 134 | + **文献**:*Fast and Accurate Scan Registration through Minimization of the Distance between Compact 3D NDT Representations*,文献没下载成功因此没有仔细看过 135 | + **优点**:D2D相比P2D耗时更少,对初始估计不佳,分辨率过小的敏感性更低; 136 | + **缺点**:精度有所下降; 137 | 138 | + **NDT-Transform**: 139 | 140 | + NDT-Transform是一种三维点云的*实时、大规模地点识别*方法。 141 | 142 | + 3D NDT提供几何形状描述。NDT-transformer网络从一组3D NDT单元表示学习全局描述符。最后,使用查询数据库实现位置识别的描述符检索。 143 | + [代码开源](https://github.com/dachengxiaocheng/NDT-Transformer) 144 | + 其他相似算法:*You Only Hypothesize Once: Point Cloud Registration with Rotation-equivariant Descriptors* 145 | 146 | + **比较与分析**: 147 | 148 | + 优化思路与方向,参考[NDT文献汇总](https://blog.csdn.net/weixin_37669024/article/details/121853056): 149 | + 向优化目标添加运动约束,因此在低信息环境(类似无尽的走廊)上的性能要高得多; 150 | + 引入三线性插值用于处理边界效应; 151 | + 引入三线性插值,导致更强的鲁棒性; 152 | + 迭代细分提高鲁棒性 153 | + 用于处理异常值/缺失数据的链接节点 154 | 155 | #### 3. TEASER/TEASER++ 156 | 157 | + **相关学习资料**: 158 | 159 | 1. [论文翻译](https://mp.weixin.qq.com/s/kGF9cX7iUhq5x_1-KAiDSA) 160 | 2. [开源代码](https://github.com/MIT-SPARK/TEASER-plusplus) 161 | 162 | 2. [论文解析](https://mp.weixin.qq.com/s/eV4L5KbQWLHWa0tt48YHZA) 163 | 3. [基础知识补充1](https://mp.weixin.qq.com/s/SGzIp_e33ExnrLERhopnPQ) 164 | 4. [基础知识补充2](https://mp.weixin.qq.com/s/889jbDBR14xzDHBOztmRLQ) 165 | 166 | + **算法优点**: 167 | 168 | + 在尺度已知的情况下可以对99%的异常值鲁棒不受干扰; 169 | + 可以在毫秒级别完成计算,是现有的最快的鲁棒的配准算法; 170 | + 可以处理没有点对匹配的情况,即假定全集对全集的匹配,在这种情况下性能能够大幅度领先ICP; 171 | + 如果与基于深度学习的关键点检测算法搭配使用,可以大幅度提升点云配准的整体性能 172 | 173 | + **算法缺点**: 174 | 175 | + 不适合大尺度场景匹配 176 | 177 | ### 有哪些经典的激光SLAM算法? 178 | 179 | #### 1. LOAM: 180 | 181 | + **特征表示**:根据激光单根线上前后各5个点与当前点的长度(激光点到雷达的距离),计算曲率大小: 182 | $$ 183 | c = \frac{1}{\|X\|}\sum_{i=0}(X-X_i) 184 | $$ 185 | 点的排列越平直差距越小,反之点排列越弯曲差距越大,曲率c越大。因此可以分类为以下4种:曲率特别大的点(sharp,角点或者线点)、曲率大的点(less_sharp,线点)、曲率小的点(less_flat,平面) 、曲率特别小的点(flat,面点或者平面) 186 | 187 | + 当$p_i$为 sharp 时,在上一帧中搜索离$p_i$最近的线特征点( sharp 和 less_sharp),并在相邻线上再找一个线特征点,组成直线。 188 | + 当$p_i$为 flat 时,在上一帧中搜索离$p_i$最近的面特征点(flat和less_flat),并在相邻线上找两个面特征点,组成平面。 189 | + 不能超过设定的size,每个集合平面点4个,边缘点2个;已选取的点周围不能有点,使得点可以分布的更加均匀; 190 | 191 | + **代价函数**: 192 | 193 | 1. 点到线:$d=\frac{(p_i-p_a)\times(p_i-p_b)}{|p_a-p_b|}$ 194 | 2. 点到面:$d=(p_i-p_j)\times\frac{(p_l-p_j)\times(p_m-p_j)}{|(p_l-p_j)\times(p_m-p_j)|}$ 195 | 196 | + **扩展**: 197 | 198 | + [论文解读1](https://zhuanlan.zhihu.com/p/111388877),[论文解读2](https://blog.csdn.net/robinvista/article/details/104379087) 199 | + A-LOAM:去掉了和IMU相关的部分;使用Ceres做迭代优化,简化了代码,但降低了效率 200 | + F-LOAM:使用残差函数的雅可比进行解析式求导 201 | 202 | #### 2. LeGO-LOAM: 203 | 204 | + **主要贡献**: 205 | 206 | + 通过竖直角度的阈值判定进行地面分割(上下两根线点的距离在10度之内),且所有用于匹配的平面点仅使用地面点。因此减小了特征搜索范围; 207 | + 在非地面点中,使用BFS聚类后,簇中点的数量大于30才用来线特征匹配,因此提高了特征质量和匹配速度; 208 | + 里程计环节中,使用地面面特征优化高度和水平角,使用线特征优化水平位移和航向角; 209 | + 建图环节中,采用六自由度模型计算,需要提取稀疏化submap。同时以帧为单位进行优化,使得全局地图可以多次调整,而不像LOAM那样不可修改; 210 | 4) 增加了回环修正模块:匹配用的是ICP,在SC-LeGO-LOAM也可以换成SC算法; 211 | 212 | + **后端优化公式**: 213 | 214 | + 基于回环的位姿修正误差扰动方程: 215 | $$ 216 | \hat{e}_{ij}=\ln(T_{ij}^{-1}T_i^{-1}\exp((-\delta\xi_i)^{\wedge})\exp(\delta\xi_j^{\wedge})T_j)^{\vee} 217 | $$ 218 | 219 | + 基于先验观测(RTK或者组合导航)的位姿修正: 220 | $$ 221 | \hat{e}_{ij}=\ln(Z_i^{-1}\exp(\delta\xi_i^{\wedge})T_i)^{\vee} 222 | $$ 223 | 224 | + **算法分析**: 225 | 226 | + 应对可变地面进行了地面优化,同时保证了轻量级。它是专门为地面车辆设计的SLAM算法,要求在安装的时候Lidar能以水平方式安装在车辆上; 227 | + [论文解读](https://zhuanlan.zhihu.com/p/115986186),[论文解读2](https://zhuanlan.zhihu.com/p/426280500) 228 | 229 | #### 3. T-LOAM 230 | 231 | + **多范围地面提取**: 232 | 233 | 1. 原因:使用单一平面模型不足以精确表示复杂地形中的分布 234 | 235 | 2. 流程: 236 | 237 | + 根据极径和方位角将点云划分为4象限,每个象限将进一步划分为几个相等的子区域 238 | 239 | image-20220613205633074 240 | 241 | + 提取种子点:计算每个区域中所有小于一定高度阈值的点,这就是提取平面所需要的点; 242 | 243 | + 对区域内点的协方差做分解,提取主方向,对每个主方向赋予权重后得到法向量 244 | 245 | + 利用平面拟合公式拟合平面; 246 | 247 | + **动态体素聚类(DCVC)**: 248 | 249 | 1. 目的: 250 | 251 | + 不同深度点云的稀疏程度不同:考虑与传感器的距离,体素分割为扇形,距离越远,区域越大。 252 | 253 | + 横向和纵向尺度不同:考虑方向分辨率,可调整$\Delta\theta$和$\Delta\phi$的大小 254 | 255 | + 相邻点云深度突变: 考虑点的稀有性,调整$\Delta\rho$的大小 256 | 257 | 2. 流程: 258 | 259 | + 首先将点云的直角坐标转换为极坐标,同时建立动态弯曲体素放入哈希表中; 260 | + 根据哈希表映射关系搜索当前点目标体素周围最近的体素,并将其合并到同一标签中; 261 | + 滤出微小的组和潜在的动态对象,以获得最终的点 262 | 263 | + **提取四种特征**: 264 | 265 | 1. 目的:LOAM系列在几何退化情况下容易退化 266 | 267 | 2. 特征分类 : 268 | 269 | 协方差矩阵后进行PCA主成分分析,依次计算曲率值,平整度和球度系数: 270 | $$ 271 | \gamma=\frac{\lambda_3}{\lambda_1+\lambda_2+\lambda_3};\ 272 | \sigma=\frac{\lambda_2-\lambda_3}{\lambda_1};\ \psi=\frac{\lambda_3}{\lambda_1} 273 | $$ 274 | 275 | + 直边:相当于LOAM中的直线,可以通过曲率值来判断;注意这里没有用LOAM系利中的角点; 276 | + 平面:平面度系数$\sigma$大于阈值$\tau$,则提取点作为平面特征的垂直部分; 277 | + 球型:球面度系数$\psi$大于阈值$\pi$,则提取点作为球面特征的垂直部分;主要是提升鲁棒性用的 278 | + 地面:地面分割提取后的平面; 279 | 280 | + **截断最小二乘(TLS)**: 281 | 282 | 1. 目的:抵抗每个特征残差的异常值 283 | 284 | #### 4. LINS 285 | 286 | + **主要贡献**: 287 | 1. 基于IESKF滤波器(ESKF + IEKF)实现Lidar与IMU,6自由度紧耦合估计的前端里程计。以IMU做状态预测,以特征中的点-面距离、点-线距离为约束(观测),修正误差。 288 | 2. scan2map匹配后就会利用gtsam对所有关键帧进行一次全局优化,而不仅仅是检测到回环的时候; 289 | 3. [代码开源](https://github.com/ChaoqinRobotics/LINS---LiDAR-inertial-SLAM) 290 | + **相似工作**: 291 | 1. FAST-LIO: A Fast, Robust LiDAR-inertial Odometry Package by Tightly-Coupled Iterated Kalman Filter 292 | 2. [代码开源](https://github.com/hku-mars/FAST_LIO) 293 | 294 | #### 5. LIO-SAM 295 | 296 | + **主要贡献**: 297 | 1. 两步走策略:先通过点云特征计算出相对位姿,再利用相对位姿、IMU预积分和GPS做 298 | 融合。相比于直接一步做紧耦合,大大提高了效率,而且实测性能也很优于直接紧耦合。 299 | 2. 帧-局部地图匹配:因为IMU预计积分的存在,依赖帧帧匹配获取一个较好初始值的需求已经不存在了,并且帧-局部地图匹配减少了对全局地图的处理时间,精度也不会受到太大损失。 300 | 3. [代码开源](https://github.com/TixiaoShan/LIO-SAM),[论文讲解](https://zhuanlan.zhihu.com/p/153394930) 301 | 302 | #### 6. LIO-Mapping 303 | 304 | + **主要贡献**: 305 | 1. 基于滑动窗口方法,把雷达线/面特征、IMU预积分等的约束放在一起进行优化。 306 | 2. 后端和VINS差不多; 307 | 308 | ### 常用三维点云采样方法有哪些? 309 | 310 | + **体素下采样**:最为常用的下采样方法。算法效率高,采样点分布均匀,点间距可控,但是不能精确控制采样点个数; 311 | + **均匀下采样**:以球体空间进行划分,点的位置不发生移动。准确度较高,但耗时提升; 312 | + **几何下采样**(曲率采样为例):点云曲率越大的地方,采样点数越多。它的计算效率高,且局部点云是均匀采样的,稳定性高。该采样适用于不规则的且丰富表面特征的点云数据计算; 313 | + **增采样**:增加点云数据。适合用于解决曲面重建时点云数量缺少的问题; 314 | + **滑动最小二乘法**:对点云数量的扩充,但主要是对点云形状进行平滑处理,所以更适合用来对点云结构进行优化。 315 | 316 | ### 点云如何产生的?如何进行畸变补偿? 317 | 318 | 1. **点云畸变产生的原因**: 319 | 320 | 本质上每个激光点的坐标都是相对于雷达的,雷达运动时(一般是顺时针运动),不同激光点的坐标原点会不同,因此产生了点云畸变。比如:平移导致的畸变(扫描标准圆为螺旋线),旋转导致的畸变(扫描标准圆为缺口圆); 321 | 322 | 2. **点云畸变矫正方法**:[参考](https://zhuanlan.zhihu.com/p/109379384) 323 | 324 | + 获取载体运动信息:一般情况下,imu测量角速度,轮式里程计测量线速度; 325 | + 获取该激光点相对于起始时刻的时间差:有时间戳时候可以用时间戳,没有可以计算得到; 326 | + 坐标系对齐:将点云与雷达起始扫描位置进行对齐,包括雷达点云坐标、加速度和角速度。 327 | + 坐标系反推:利用时间差获得相应的变换矩阵,并对点云进行逆变换; 328 | 329 | 3. **点云去畸变效果**: 330 | 331 | 畸变补偿除了提高定位精度以外,对地图质量,尤其是对消除路口转弯导致的地图重影有明显效果。 332 | 333 | ### 匹配问题有哪些解决方法? 334 | 335 | + **最近邻匹配** 336 | 337 | 1. Kdtree在邻域查找上比较有优势,在小数据量的情况下,其搜索效率比较高,但在数据量增大的情况下,其效率会有一定的下降,一般是线性上升的规律。 338 | 2. Octree算法实现简单,但大数据量点云数据下,其使用比较困难的是最小粒度(叶节点)的确定,粒度较大时,有的节点数据量可能仍比较大,后续查询效率仍比较低,反之,粒度较小,八叉树的深度增加,需要的内存空间也比较大(每个非叶子节点需要八个指针),效率也降低。 339 | 3. 如果将Octree和Kdtree结合起来的应用,应用八叉树进行大粒度的划分和查找,而后使用Kdtree树进行细分,效率会有一定的提升, 340 | 341 | + **匈牙利算法** 342 | 343 | 1. 属于二分图问题:简单来说就是两组集合U与V,其中U与V集合内部的点不能相互连通,但是U与V的点之间是可以联通的。特点为:无向图、交集为空、单集合内部禁止组合; 344 | 345 | 2. 匈牙利算法核心思路:增广路取反,[参考文章](http://data.biancheng.net/view/150.html),[参考文章2](https://blog.csdn.net/lx_ros/article/details/123980953) 346 | 347 | image-20220623160559132 348 | 349 | 3. 匈牙利算法特点:基于最大流算法简化,速度较快,但是匹配不稳定; 350 | 351 | 4. 改进:KM算法(Kuhn-Munkres 算法),相当于带有权重的匈牙利算法; 352 | 353 | + **高斯混合模型** 354 | 355 | 1. 背景:K Means的算法缺点 356 | + 簇的半径为固定截断值,缺乏鲁棒性; 357 | + 拟合出来的簇为圆形,可能与实际数据分布(如椭圆)差别很大; 358 | 2. 核心: 359 | + 高斯混合模型可以看作是由 K 个单高斯模型组合而成的模型; 360 | + 模型表述: 361 | 1. 单高斯分布:$\log L(\theta) =\sum_{j=1}^{N}\log P(x_j|\theta)$,可以通过最大似然法求解 362 | 2. 混合高斯:$\log L(\theta) =\sum_{j=1}^{N}\log(\sum_{k=1}^{K} \alpha_k\phi(x|\theta_k))$,对于每个观测数据点来说并不知道它是属于哪个子分布的,因此需要采用EM算法求解; 363 | + EM算法求解高斯混合模型: 364 | 1. E-step:求期望。依据当前参数,计算每个数据来自子模型的可能性; 365 | 2. M-step:求极大。根据上一轮的结果计算相应参数 366 | 3. 重复迭代,直到结果收敛; 367 | + 超参数$K$可以通过手动调参确定,部分算法提供了诸如AIC或BIC评价方法; 368 | + 参考文献:[高斯混合模型(GMM)](https://zhuanlan.zhihu.com/p/30483076),[最佳聚类实践:高斯混合模型](https://zhuanlan.zhihu.com/p/81255623) 369 | 370 | ### 卡尔曼滤波的推导 371 | 372 | 1. 贝叶斯相关知识:贝叶斯推断可以理解为贝叶斯公式的运用,它是指,如果已知先验概率密度函数 ,以及传感器模型 ,那么就可以根据贝叶斯公式推断出后验概率密度。 373 | 374 | 2. 贝叶斯视角推导过程: 375 | 376 | image-20220623210720807 377 | 378 | image-20220623210740171 379 | 380 | 3. 理解卡尔曼:[相关参考视频](https://www.bilibili.com/video/BV1ez4y1X7eR?spm_id_from=333.1007.top_right_bar_window_custom_collection.content.click&vd_source=0dc435c19d2243d812952aa7d93b3c74) 381 | 382 | 4. 扩展卡尔曼: 383 | 384 | image-20220623213330203 385 | 386 | image-20220623214547408 387 | 388 | 5. 多传感器融合框架下的ESKF算法: 389 | 390 | **流程框架** 391 | 392 | + 初始化:状态量、初始方差、过程噪声和观测噪声 393 | + 惯性解算:包括姿态、速度、位置 394 | + Kalman 预测更新:执行kalman五个步骤中的前两步,即预测状态量和预测方差; 395 | + 无观测时的量测更新: 396 | + 直接赋值等价; 397 | + 有观测时的量测更新 398 | + 执行kalman滤波后面的三个步骤,得到后验状态量 399 | + 更新后验位姿; 400 | + 状态量清零,方差不变; 401 | 402 | 其他相关细节详见PPT(多传感器融合定位); 403 | 404 | ### 卡尔曼滤波调参 405 | 406 | 首先根据IMU或者其他传感器的误差给出一个大概的估值,然后放大或者缩小相关参数,和真值比较; 407 | 408 | ### 平均旋转与插值的方法 409 | 410 | + 结论:这个问题目前不存在解析解,可以通过数值求解的方式计算;或者如果采样的姿态较为接近,可以采用四元数求平均的方式近似计算。 411 | + 为什么不使用欧拉角分别插值:姿态空间插值的一个主要问题是姿态空间的三个自由度是相互耦合的,而不像位置空间一样三个自由度完全解耦正交。所以,直接对欧拉角三个角度分别插值,可能存在奇异性或者错误结果,且插补后的刚体角速度不恒定 412 | + 使用四元数slerp插值:Slerp插值可以认为是最短路径插值,类似于位置空间的直线插值。但是,Slerp插值只能保证一阶连续,过渡点处的角速度方向会发生突变。公式表述为:$\text{Slerp}(q_0,q_1,t)=q_0(q_0^{-1}q_1)^{t}$。更多资料参考[wiki资料](https://en.wikipedia.org/wiki/Slerp); 413 | + 使用Eigen库的实现如下:`Eigen::Quaternionf q = q1.slerp(t, q2);` 414 | + 为提高速度,利用角度进行插值:[参考资料](https://zhuanlan.zhihu.com/p/87418561) 415 | + 使用数值求解方法进行旋转平均:使用黎曼度量下的距离进行处理,表示最短测地线的弧长。计算方法如下(参考论文为:*Rotation averaging* ): 416 | + 初始化:令$\bar{x}\gets x_i$; 417 | + 迭代计算:$\omega=\frac{1}{N}\sum_{i=1}^{N}\log(\bar{x}^{\top}x_i)$ 418 | + 迭代更新:$\bar{x}\gets\bar{x}\exp(\omega),\ \text{if}\ \| \omega\|>\xi$,其中$\xi$为给定阈值; 419 | + 使用数值方法进行旋转插值:[参考资料](https://zhuanlan.zhihu.com/p/88780398) 420 | + 公式表述为:$R_1\exp(t\log(R^{-1}_1R_2))$ 421 | + 多个四元数之间插值求解:**Squad插值算法** 422 | + 其他思考:计算两个 SO(3) 元素之间的距离虽然是能求旋转矩阵的平均,但是忽略了不同导致姿态变化的因素对旋转矩阵的影响的权重以及影响方式是不一样的,究其根本原因在于这些方法本质上假设机器人的姿态在欧拉角空间中随机分布。重点在于你的原始数据是什么,在传感器直接采样的参数的空间里取平均才是最准确的。 423 | 424 | ### 矩阵数值求逆的方法 425 | 426 | ### SVD分解 427 | 428 | ### 最小二乘法的推导 429 | 430 | -------------------------------------------------------------------------------- /leetcode刷题笔记/刷题笔记.assets/image-20220725170827235.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfkiwl/slam_and_leetcode/47120bfea76f6f53bb2a003aeae16ce1318c4432/leetcode刷题笔记/刷题笔记.assets/image-20220725170827235.png -------------------------------------------------------------------------------- /leetcode刷题笔记/刷题笔记.assets/image-20220725172355076.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfkiwl/slam_and_leetcode/47120bfea76f6f53bb2a003aeae16ce1318c4432/leetcode刷题笔记/刷题笔记.assets/image-20220725172355076.png -------------------------------------------------------------------------------- /leetcode刷题笔记/刷题笔记.md: -------------------------------------------------------------------------------- 1 | # 面试汇总 2 | 3 | [TOC] 4 | 5 | ## 1 基础算法代码题解 6 | 7 | ### 1.1 0-1背包相关问题 8 | 9 | 1. **物品只被放入一次,求最大价值和**。即纯0-1背包问题 10 | 11 | ```cpp 12 | vector dp(bagWeight + 1, 0); 13 | for(int i = 0; i < weight.size(); i++) { // 遍历物品 14 | for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量 15 | dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); 16 | } 17 | } 18 | ``` 19 | 20 | + [分割等和子集](https://leetcode.cn/problems/partition-equal-subset-sum/)、[最后一块石头的重量 II](https://leetcode.cn/problems/last-stone-weight-ii/):`vector dp(sum/2+1);` 21 | 22 | [目标和](https://leetcode.cn/problems/target-sum/):`int bagWeight = (target + sum)/2; vector dp(bagWeight+1);` 23 | 24 | 2. **物品被放入多次,求最大价值和**。即完全背包问题,完全背包物体顺序无所谓; 25 | 26 | ```cpp 27 | for(int i = 0; i < weight.size(); i++) { // 遍历物品 28 | for(int j = weight[i]; j <= bagWeight ; j++) { // 遍历背包容量 29 | dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); 30 | } 31 | } 32 | ``` 33 | 34 | 3. **背包容量固定,求多少种可能性**。经典题目:[目标和](https://leetcode.cn/problems/target-sum/),[零钱兑换 II](https://leetcode.cn/problems/coin-change-2/), 35 | 36 | ```c++ 37 | vector dp(bagWeight+1); 38 | dp[0] = 1; 39 | for (int i=0; i::max(); 65 | vector dp(n + 1, INT_MAX); 66 | dp[0] = 0; 67 | for (int i=1; i= zeroNum; i--) { // 遍历背包容量且从后向前遍历! 78 | for (int j = n; j >= oneNum; j--) { 79 | dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1); 80 | } 81 | } 82 | ``` 83 | 84 | ### 1.2 公共子序列/子串相关题目 85 | 86 | 1. 基础类型:[1143最长公共子序列](https://leetcode.cn/problems/longest-common-subsequence/),[不相交的线](https://leetcode.cn/problems/uncrossed-lines/) 87 | 88 | ```cpp 89 | vector> dp(text1.size() + 1, vector(text2.size() + 1, 0)); 90 | for (int i = 1; i <= text1.size(); i++) { 91 | for (int j = 1; j <= text2.size(); j++) { 92 | if (text1[i - 1] == text2[j - 1]) { 93 | dp[i][j] = dp[i - 1][j - 1] + 1; 94 | } else { 95 | dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]); 96 | } 97 | } 98 | } 99 | return dp[text1.size()][text2.size()]; 100 | ``` 101 | 102 | + [392判断子序列](https://leetcode.cn/problems/is-subsequence/):`return dp[m-1][n-1] == s.size() ? true : false;` 103 | 104 | + [583两个字符串的删除操作](https://leetcode.cn/problems/delete-operation-for-two-strings/):`return m+n-2-2*dp[m-1][n-1];` 105 | 106 | + [718最长重复子数组](https://leetcode.cn/problems/maximum-length-of-repeated-subarray/):注意这里变成了*子串/子数组*,表示选择是连续的。 107 | 108 | ```cpp 109 | int results = 0; 110 | for (int i = 1; i <= A.size(); i++) { 111 | for (int j = 1; j <= B.size(); j++) { 112 | if (A[i - 1] == B[j - 1]) { 113 | dp[i][j] = dp[i - 1][j - 1] + 1; 114 | } 115 | result = max(dp[i][j], result); 116 | } 117 | } 118 | ``` 119 | 120 | ### 1.3 二分查找相关算法 121 | 122 | 1. 二分法: 123 | 124 | + **无重复有序数组,区间左闭右闭** 125 | 126 | ```c++ 127 | int search(vector& nums, int target) { 128 | int left = 0; 129 | // 定义target在左闭右闭的区间里,[left, right] 130 | int right = nums.size() - 1; 131 | // 当left==right,区间[left, right]依然有效,所以用 <= 132 | while (left <= right) { 133 | int middle = left + ((right - left) / 2); 134 | if (nums[middle] > target) { 135 | right = middle - 1; // target 在左区间,所以[left, middle - 1] 136 | } else if (nums[middle] < target) { 137 | left = middle + 1; 138 | } else { 139 | return middle; 140 | } 141 | } 142 | return -1; 143 | } 144 | ``` 145 | 146 | + **无重复有序数组,区间左闭右开** 147 | 148 | ```c++ 149 | int search(vector& nums, int target) { 150 | int left = 0; 151 | // 定义target在左闭右开的区间里,即:[left, right) 152 | int right = nums.size(); 153 | // 因为left == right的时候,在[left, right)是无效的空间,所以使用 < 154 | while (left < right) { 155 | int middle = left + ((right - left) >> 1); 156 | if (nums[middle] > target) { 157 | right = middle; // target 在左区间,在[left, middle)中 158 | } else if (nums[middle] < target) { 159 | left = middle + 1; 160 | } else { 161 | return middle; 162 | } 163 | } 164 | return -1; 165 | } 166 | ``` 167 | 168 | + **有重复有序数组,寻找左边界/右边界**。如[34在排序数组中查找元素的第一个和最后一个位置](https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/) 169 | 170 | + c++STL库: 171 | 172 | ```c++ 173 | vector searchRange(vector& nums, int target) { 174 | // 二分查找第一个大于或等于num的数字 175 | auto left = lower_bound(nums.begin(), nums.end(), target); 176 | // 二分查找第一个大于num的数字 177 | auto right = upper_bound(nums.begin(), nums.end(), target); 178 | if (left == right) 179 | return vector{-1, -1}; 180 | else 181 | return vector{(int)(left - nums.begin()), (int)(right - nums.begin() - 1)}; 182 | } 183 | ``` 184 | 185 | + 不调用库函数:相似题目[35搜索插入位置](https://leetcode.cn/problems/search-insert-position/) 186 | 187 | ```c++ 188 | int lowerBound(vector& nums, int target) { 189 | int left = 0, right = nums.size() - 1; 190 | while (left <= right) { 191 | int mid = (left + right) / 2; 192 | if (nums[mid] >= target) // 这里选择等于 >= 193 | { 194 | right = mid - 1; 195 | }else{ 196 | left = mid + 1; 197 | } 198 | } 199 | return left; 200 | } 201 | int upperBound(vector& nums, int target) { 202 | int left = 0, right = nums.size() - 1; 203 | while (left <= right) { 204 | int mid = (left + right) / 2; 205 | if (nums[mid] > target) // 这里选择等于 > 206 | { 207 | right = mid - 1; 208 | }else{ 209 | left = mid + 1; 210 | } 211 | } 212 | return left; 213 | } 214 | ``` 215 | 216 | + 相似题目: 217 | 218 | 1. [x 的平方根 ](https://leetcode.cn/problems/sqrtx/):对于``0 <= x <= 2^31 - 1`,有: 219 | 220 | ```c++ 221 | long long base = (long long)mid * mid; 222 | ``` 223 | 224 | 2. [搜索二维矩阵](https://leetcode.cn/problems/search-a-2d-matrix/):选择`lowerBound`和`upperBound`至关重要 225 | 226 | 2. 查找的扩展: 227 | 228 | + 隐式查找:数字位置关系映射。如:[540有序数组中的单一元素](https://leetcode.cn/problems/single-element-in-a-sorted-array/) 229 | 230 | ```c++ 231 | int singleNonDuplicate(vector& nums) { 232 | if (nums.size() == 1) 233 | return nums[0]; 234 | int left = 0, right = nums.size()-1; 235 | while(left <= right){ 236 | int mid = (right - left)/2 + left; 237 | if (mid %2 == 1){ 238 | if (nums[mid] == nums[mid - 1]){ 239 | left = mid + 1; 240 | }else{ 241 | right = mid - 1; 242 | } 243 | }else{ 244 | if (nums[mid] == nums[mid + 1]){ 245 | left = mid + 1; 246 | }else{ 247 | right = mid - 1; 248 | } 249 | } 250 | } 251 | return nums[left]; 252 | } 253 | ``` 254 | 255 | + [找到 K 个最接近的元素](https://leetcode.cn/problems/find-k-closest-elements/) 256 | 257 | ```c++ 258 | // case1 259 | vector findClosestElements(vector& arr, int k, int x) { 260 | sort(arr.begin(),arr.end(), 261 | [&](int &a,int &b){return abs(a-x)==abs(b-x)?a res(arr.begin(), arr.begin() + k); 263 | sort(res.begin(), res.end()); 264 | return res; 265 | } 266 | // case2 267 | vector findClosestElements(vector& arr, int k, int x) { 268 | int left = 0; 269 | int right = arr.size() - k; 270 | while(left < right) 271 | { 272 | int mid = (right - left)/2 + left; 273 | // 关键:x = (arr[mid] + arr[mid+k])/2 274 | if(2*x > arr[mid + k] + arr[mid]) left = mid + 1; 275 | else right = mid; 276 | } 277 | return vector(arr.begin() + left, arr.begin() + k + left); 278 | } 279 | ``` 280 | 281 | + 旋转数组系列题目(基本都是左闭右开类型扩展):[153、154寻找旋转排序数组中的最小值](https://leetcode.cn/problems/find-minimum-in-rotated-sorted-array/),[162寻找峰值](https://leetcode.cn/problems/find-peak-element/),[81搜索旋转排序数组 II](https://leetcode.cn/problems/search-in-rotated-sorted-array-ii/) 282 | 283 | ```c++ 284 | // 无重复元素 285 | int findMin(vector& nums) { 286 | int left = 0, right = nums.size() - 1; 287 | while (left < right) { 288 | int mid = left + (right - left) / 2; 289 | // 翻转符号就能够得到最大值 290 | if (nums[mid] < nums[right]) right = mid; 291 | else left = mid + 1; 292 | } 293 | return nums[left]; 294 | } 295 | // 有重复元素 296 | int findMin(vector& nums) { 297 | int left = 0, right = nums.size() - 1; 298 | while (left < right) { 299 | int mid = left + (right - left) / 2; 300 | if (nums[mid] < nums[right]) right = mid; 301 | else if (nums[mid] > nums[right]) left = mid + 1; 302 | else right--; 303 | } 304 | return nums[left]; 305 | } 306 | // 寻找峰值 307 | int findPeakElement(vector& nums) { 308 | int left = 0, right = nums.size() - 1; 309 | while (left < right) { 310 | int mid = left + (right - left) / 2; 311 | if (nums[mid] > nums[mid+1]) right = mid; 312 | else left = mid + 1; 313 | } 314 | return left; 315 | } 316 | /* TODO 81 */ 317 | ``` 318 | 319 | ### 1.4 回溯问题模板 320 | 321 | 1. 组合问题1:**无重复**数组中,找出**固定数量(k个)数字**,**不允许重复**。[组合总和 III](https://leetcode.cn/problems/combination-sum-iii/) 322 | 323 | ```c++ 324 | public: 325 | vector> result; // 存放符合条件结果的集合 326 | vector path; // 用来存放符合条件结果 327 | 328 | // 回溯函数 329 | void backtracking(int targetSum, int k, int sum, int startIndex) { 330 | if (path.size() == k) { 331 | if (sum == targetSum) result.push_back(path); 332 | return; // 如果path.size() == k 但sum != targetSum 直接返回 333 | } 334 | for (int i = startIndex; i <= 9; i++) { 335 | sum += i; // 处理 336 | path.push_back(i); // 处理 337 | backtracking(targetSum, k, sum, i + 1); // 注意i+1调整startIndex 338 | sum -= i; // 回溯 339 | path.pop_back(); // 回溯 340 | } 341 | } 342 | 343 | // 主函数 344 | vector> combinationSum3(int k, int n) { 345 | backtracking(n, k, 0, 1); 346 | return result; 347 | } 348 | ``` 349 | 350 | 2. 组合问题2:**无重复**元素数组中,选取**未定个数**,**允许重复** 351 | 352 | ```c++ 353 | for (int i = startIndex; i < candidates.size(); i++) { 354 | sum += candidates[i]; 355 | path.push_back(candidates[i]); 356 | backtracking(candidates, target, sum, i); // 关键点:不用i+1了,表示可以重复读取当前的数 357 | sum -= candidates[i]; // 回溯 358 | path.pop_back(); // 回溯 359 | } 360 | ``` 361 | 362 | 3. 组合问题3:有**重复元素数组**中,选取**未定个数**,**不允许重复**。[组合总和 II](https://leetcode.cn/problems/combination-sum-ii/) 363 | 364 | ```c++ 365 | public: 366 | vector> result; 367 | vector path; 368 | 369 | void backtracking(vector& candidates, int target, int sum, int startIndex, vector& used) { 370 | if (sum == target) { 371 | result.push_back(path); 372 | return; 373 | } 374 | 375 | for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) 376 | { 377 | if (i > 0 && candidates[i] == candidates[i - 1] \ 378 | && used[i - 1] == false) 379 | continue; 380 | 381 | sum += candidates[i]; path.push_back(candidates[i]); used[i] = true; 382 | backtracking(candidates, target, sum, i + 1, used); 383 | used[i] = false; sum -= candidates[i]; path.pop_back(); 384 | } 385 | } 386 | 387 | public: 388 | vector> combinationSum2(vector& candidates, int target) { 389 | vector used(candidates.size(), false); 390 | // 首先把给candidates排序,让其相同的元素都挨在一起。 391 | sort(candidates.begin(), candidates.end()); 392 | backtracking(candidates, target, 0, 0, used); 393 | return result; 394 | } 395 | ``` 396 | 397 | 4. 变形:**切割**问题。[131分割回文串](https://leetcode.cn/problems/palindrome-partitioning/) 398 | 399 | ``` c++ 400 | for (int i = startIndex; i < s.size(); i++) { 401 | // 符合要求,存储;如果不是则直接跳过 402 | if (isPalindrome(s, startIndex, i)) { 403 | string str = s.substr(startIndex, i - startIndex + 1); 404 | path.push_back(str); 405 | } else { 406 | continue; 407 | } 408 | backtracking(s, i + 1); 409 | path.pop_back(); 410 | } 411 | ``` 412 | 413 | 5. 变形:**子集**问题 414 | 415 | ```c++ 416 | void backtracking(vector& nums, int startIndex) { 417 | // 收集子集,要放在终止添加的上面,否则会漏掉自己 418 | result.push_back(path); 419 | // 终止条件可以不加 420 | ........ 421 | } 422 | ``` 423 | 424 | + [子集](https://leetcode.cn/problems/subsets/),[子集 II](https://leetcode.cn/problems/subsets-ii/),[递增子序列](https://leetcode.cn/problems/increasing-subsequences/) 425 | 426 | 6. 全排列问题: 427 | 428 | + C++标准库: 429 | 430 | ```c++ 431 | vector> res = {A}; 432 | while (next_permutation(A.begin(),A.end()) 433 | { 434 | res.push_back(A); 435 | } 436 | ``` 437 | 438 | + 回溯写法(类似组合问题3) 439 | 440 | ```c++ 441 | for (int i = 0; i < nums.size(); i++) { 442 | if (used[i] == true) continue; // path里已经收录的元素,直接跳过 443 | used[i] = true; 444 | path.push_back(nums[i]); 445 | backtracking(nums, used); 446 | path.pop_back(); 447 | used[i] = false; 448 | } 449 | ``` 450 | 451 | + 去重的全排列(和组合问题3一样) 452 | 453 | ```c++ 454 | if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) { 455 | continue; 456 | } 457 | if (used[i] == true) continue; // path里已经收录的元素,直接跳过 458 | ``` 459 | 460 | ### 1.5 二叉树遍历相关题目 461 | 462 | 1. 基础递归法: 463 | 464 | ```c++ 465 | // 前序遍历 466 | void traversal(TreeNode* cur, vector& vec) { 467 | if (cur == NULL) return; 468 | vec.push_back(cur->val); // 中 469 | traversal(cur->left, vec); // 左 470 | traversal(cur->right, vec); // 右 471 | } 472 | // 中序遍历 473 | void traversal(TreeNode* cur, vector& vec) { 474 | if (cur == NULL) return; 475 | traversal(cur->left, vec); // 左 476 | vec.push_back(cur->val); // 中 477 | traversal(cur->right, vec); // 右 478 | } 479 | // 后序遍历 480 | void traversal(TreeNode* cur, vector& vec) { 481 | if (cur == NULL) return; 482 | traversal(cur->left, vec); // 左 483 | traversal(cur->right, vec); // 右 484 | vec.push_back(cur->val); // 中 485 | } 486 | ``` 487 | 488 | + 二叉搜索树: 489 | 490 | ``` 491 | void searchBST(TreeNode* cur) { 492 | if (cur == NULL) return ; 493 | searchBST(cur->left); // 左 494 | (处理节点) // 中 495 | searchBST(cur->right); // 右 496 | return ; 497 | } 498 | ``` 499 | 500 | 2. 迭代遍历法: 501 | 502 | ```c++ 503 | // 前序遍历 504 | vector preorderTraversal(TreeNode* root) { 505 | stack st; 506 | vector result; 507 | if (root == NULL) return result; 508 | st.push(root); 509 | while (!st.empty()) { 510 | TreeNode* node = st.top();// 中 511 | st.pop(); 512 | result.push_back(node->val); 513 | if (node->right) st.push(node->right);// 右(空节点不入栈) 514 | if (node->left) st.push(node->left);// 左(空节点不入栈) 515 | } 516 | return result; 517 | } 518 | // 中序遍历 519 | vector inorderTraversal(TreeNode* root) { 520 | vector result; 521 | stack st; 522 | TreeNode* cur = root; 523 | while (cur != NULL || !st.empty()) { 524 | if (cur != NULL) { // 指针来访问节点,访问到最底层 525 | st.push(cur); // 将访问的节点放进栈 526 | cur = cur->left; // 左 527 | }else{ 528 | cur = st.top(); 529 | st.pop(); 530 | result.push_back(cur->val); // 中 531 | cur = cur->right; // 右 532 | } 533 | } 534 | return result; 535 | } 536 | // 后序遍历 537 | vector postorderTraversal(TreeNode* root) { 538 | stack st; 539 | vector result; 540 | if (root == NULL) return result; 541 | st.push(root); 542 | while (!st.empty()) { 543 | TreeNode* node = st.top(); 544 | st.pop(); 545 | result.push_back(node->val); 546 | if (node->left) st.push(node->left); // 相对于前序遍历更改入栈顺序 547 | if (node->right) st.push(node->right); // 空节点不入栈 548 | } 549 | reverse(result.begin(), result.end()); // 将结果反转之后就是左右中的顺序了 550 | return result; 551 | } 552 | ``` 553 | 554 | 3. 层序遍历 555 | 556 | + 无返回值的基础型: 557 | 558 | ```c++ 559 | // 非递归法 560 | vector> levelOrder(TreeNode* root) { 561 | queue que; 562 | if (root != NULL) que.push(root); 563 | vector> result; 564 | while (!que.empty()) { 565 | int size = que.size(); 566 | vector vec; 567 | // 这里一定要使用固定大小size,不要使用que.size() 568 | for (int i = 0; i < size; i++) { 569 | TreeNode* node = que.front(); 570 | que.pop(); 571 | vec.push_back(node->val); 572 | if (node->left) que.push(node->left); 573 | if (node->right) que.push(node->right); 574 | } 575 | result.push_back(vec); 576 | } 577 | return result; 578 | } 579 | // 递归法 580 | void order(TreeNode* cur, vector>& result, int depth) 581 | { 582 | if (cur == nullptr) return; 583 | if (result.size() == depth) result.push_back(vector()); 584 | result[depth].push_back(cur->val); 585 | order(cur->left, result, depth + 1); 586 | order(cur->right, result, depth + 1); 587 | } 588 | vector> levelOrder(TreeNode* root) { 589 | vector> result; 590 | int depth = 0; 591 | order(root, result, depth); 592 | return result; 593 | } 594 | ``` 595 | 596 | + [二叉树的最大深度](https://leetcode.cn/problems/maximum-depth-of-binary-tree/): 597 | 598 | ```c++ 599 | return results.size(); 600 | ``` 601 | 602 | + [222完全二叉树个数](https://leetcode.cn/problems/count-complete-tree-nodes/),[513找左下角的值](https://leetcode.cn/problems/find-bottom-left-tree-value/) 603 | 604 | + 带有返回值的层次遍历: 605 | 606 | + [对称二叉树](https://leetcode.cn/problems/symmetric-tree/) 607 | 608 | ```c++ 609 | bool compare(TreeNode* left, TreeNode* right) { 610 | if (left == NULL && right != NULL) return false; 611 | else if (left != NULL && right == NULL) return false; 612 | else if (left == NULL && right == NULL) return true; 613 | else if (left->val != right->val) return false; 614 | 615 | // 此时就是:左右节点都不为空,且数值相同的情况 616 | // 此时才做递归,做下一层的判断 617 | bool outside = compare(left->left, right->right); 618 | bool inside = compare(left->right, right->left); 619 | bool isSame = outside && inside; 620 | return isSame; 621 | } 622 | 623 | bool isSymmetric(TreeNode* root) { 624 | if (root == NULL) return true; 625 | return compare(root->left, root->right); 626 | } 627 | ``` 628 | 629 | + [二叉树最小深度](https://leetcode.cn/problems/minimum-depth-of-binary-tree/),[平衡二叉树](https://leetcode.cn/problems/balanced-binary-tree/submissions/) 630 | 631 | ```c++ 632 | int getdepth(TreeNode* cur){ 633 | if (cur->left == nullptr && cur->right == nullptr) 634 | return 0; 635 | if (cur->left != nullptr && cur->right == nullptr) 636 | return 1+getdepth(cur->left); 637 | if (cur->left == nullptr && cur->right != nullptr) 638 | return 1+getdepth(cur->right); 639 | return 1+min(getdepth(cur->left), getdepth(cur->right)); 640 | } 641 | int minDepth(TreeNode* root) { 642 | if (root == nullptr) return 0; 643 | return getdepth(root)+1; 644 | } 645 | ``` 646 | 647 | + 带回溯模板的二叉树遍历: 648 | 649 | ```c++ 650 | public: 651 | void traversal(TreeNode* cur, string path, vector& result) { 652 | path += to_string(cur->val); // 中 653 | if (cur->left == NULL && cur->right == NULL) { 654 | result.push_back(path); 655 | return; 656 | } 657 | // path的值没有改变:隐藏了回溯 658 | if (cur->left) traversal(cur->left, path + "->", result); // 左 659 | if (cur->right) traversal(cur->right, path + "->", result); // 右 660 | } 661 | 662 | vector binaryTreePaths(TreeNode* root) { 663 | vector result; 664 | string path; 665 | if (root == NULL) return result; 666 | traversal(root, path, result); 667 | return result; 668 | 669 | } 670 | ``` 671 | 672 | + 例题:[112路径总和](https://leetcode.cn/problems/path-sum/),[113路径总和2](https://leetcode.cn/problems/path-sum-ii/), [257二叉树所有路径](https://leetcode.cn/problems/binary-tree-paths/),[左叶子之和](https://leetcode.cn/problems/sum-of-left-leaves/) 673 | 674 | + 返回节点的二叉树遍历: 675 | 676 | ```c++ 677 | public: 678 | TreeNode* insertIntoBST(TreeNode* root, int val) { 679 | if (root == NULL) { 680 | TreeNode* node = new TreeNode(val); 681 | return node; 682 | } 683 | if (root->val > val) root->left = insertIntoBST(root->left, val); 684 | if (root->val < val) root->right = insertIntoBST(root->right, val); 685 | return root; 686 | } 687 | ``` 688 | 689 | + [701二叉搜索树中的插入操作](https://leetcode.cn/problems/insert-into-a-binary-search-tree/),[二叉树的最近公共祖先](https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree/),[617合并二叉树](https://leetcode.cn/problems/merge-two-binary-trees/) 690 | 691 | + 模板:普通二叉树的删除方式。代码中目标节点(要删除的节点)被操作了两次:第一次是和目标节点的右子树最左面节点交换;第二次直接被NULL覆盖了。 692 | 693 | ```c++ 694 | TreeNode* deleteNode(TreeNode* root, int key) { 695 | if (root == nullptr) return root; 696 | if (root->val == key) { 697 | if (root->right == nullptr) { // 这里第二次操作目标值:最终删除的作用 698 | return root->left; 699 | } 700 | TreeNode *cur = root->right; 701 | while (cur->left) { 702 | cur = cur->left; 703 | } 704 | swap(root->val, cur->val); // 这里第一次操作目标值:交换目标值其右子树最左面节点。 705 | } 706 | root->left = deleteNode(root->left, key); 707 | root->right = deleteNode(root->right, key); 708 | return root; 709 | } 710 | ``` 711 | 712 | 4. 总结: 713 | 714 | 在递归函数有返回值的情况下: 715 | 716 | + 如果要搜索一条边,递归函数返回值不为空的时候,立刻返回; 717 | 718 | ```c++ 719 | if (递归函数(root->left)) return ; 720 | if (递归函数(root->right)) return ; 721 | ``` 722 | 723 | + 如果搜索整个树,直接用一个变量left、right接住返回值,是后序遍历中处理中间节点的逻辑 724 | 725 | ```c++ 726 | left = 递归函数(root->left); 727 | right = 递归函数(root->right); 728 | left与right的逻辑处理; 729 | ``` 730 | 731 | 732 | ### 1.4 链表 733 | 734 | ### 1.5 双指针 735 | 736 | 1. 快慢指针:如[27移除元素](https://leetcode.cn/problems/remove-element/) 737 | 738 | ```c++ 739 | int removeElement(vector& nums, int val) { 740 | int slowIndex = 0; 741 | for (int fastIndex = 0; fastIndex < nums.size(); fastIndex++) { 742 | if (val != nums[fastIndex]) { 743 | nums[slowIndex++] = nums[fastIndex]; 744 | } 745 | } 746 | return slowIndex; 747 | } 748 | ``` 749 | 750 | 2. 前后指针:示例[977有序数组的平方](https://leetcode.cn/problems/squares-of-a-sorted-array/) 751 | 752 | ```c++ 753 | vector sortedSquares(vector& A) { 754 | int k = A.size() - 1; 755 | vector result(A.size(), 0); 756 | // 注意这里要i <= j,因为最后要处理两个元素 757 | for (int i = 0, j = A.size() - 1; i <= j;) { 758 | if (A[i] * A[i] < A[j] * A[j]) { 759 | result[k--] = A[j] * A[j]; 760 | j--; 761 | } 762 | else { 763 | result[k--] = A[i] * A[i]; 764 | i++; 765 | } 766 | } 767 | return result; 768 | } 769 | ``` 770 | 771 | 3. 滑动窗口:初始位置不同或者区间内满足一定要求 772 | 773 | + **区间内满足一定要求**:示例[209长度最小的子数组](https://leetcode.cn/problems/minimum-size-subarray-sum/)(注意要求是正整数) 774 | 775 | ```c++ 776 | int minSubArrayLen(int s, vector& nums) { 777 | int result = std::numeric_limits::max(); 778 | int sum = 0; 779 | int i = 0; // 滑动窗口起始位置 780 | int subLength = 0; // 滑动窗口的长度 781 | for (int j = 0; j < nums.size(); j++) { 782 | sum += nums[j]; 783 | // 注意这里使用while,不断比较子序列是否符合条件 784 | while (sum >= s && i <) { 785 | subLength = (j - i + 1); // 取子序列的长度 786 | result = result < subLength ? result : subLength; 787 | sum -= nums[i++]; // i而不是j,并不断变更i(子序列的起始位置) 788 | } 789 | } 790 | // 如果result没有被赋值的话,就返回0,说明没有符合条件的子序列 791 | return result == std::numeric_limits::max() ? 0 : result; 792 | } 793 | ``` 794 | 795 | + 相似题目:[713乘积小于 K 的子数组](https://leetcode.cn/problems/subarray-product-less-than-k/) 796 | 797 | ```c++ 798 | while (product >= k && i <= j) { 799 | product /= nums[i++]; 800 | } 801 | // 当区间范围为大于时候,在外部记录区间长度,等价于符合条件的组合可能性 802 | result += j - i + 1; 803 | ``` 804 | 805 | + 相似题目:[930和相同的二元子数组](https://leetcode.cn/problems/binary-subarrays-with-sum/) 。这里题目数组有了0,就不能像209那样求解,而是利用差获得等于goal的数组: 806 | 807 | ```c++ 808 | for (int j = 0; j < nums.size(); j++) { 809 | sum1 += nums[j]; 810 | sum2 += nums[j]; 811 | while (sum1 > goal && i1 <= j) sum1 -= nums[i1++]; 812 | while (sum2 >= goal && i2 <= j) sum2 -= nums[i2++]; 813 | result += i2 - i1; 814 | } 815 | ``` 816 | 817 | + 相似题目变形:[3无重复字符的最长子串](https://leetcode.cn/problems/longest-substring-without-repeating-characters/), [904水果成篮](https://leetcode.cn/problems/fruit-into-baskets/) 818 | 819 | ```c++ 820 | int lengthOfLongestSubstring(string s) { 821 | int i = 0; 822 | int window_len = 0; 823 | int max_len = 0; 824 | unordered_set record; 825 | for (int j = 0; j < s.size(); j++) { 826 | while (record.count(s[j])) { 827 | window_len = (j - i); // 不加1是因为当前位置是重复的,不计算在内 828 | max_len = max_len > window_len ? max_len : window_len; 829 | record.erase(s[i++]); 830 | } 831 | record.insert(s[j]); 832 | } 833 | window_len = s.size() - i; // 等于 j-i+1. 使用+1是因为最后一个是无重复字符 834 | max_len = max_len > window_len ? max_len : window_len; 835 | return max_len; 836 | } 837 | ``` 838 | 839 | + 相似题目:[438找到字符串中所有字母异位词](https://leetcode.cn/problems/find-all-anagrams-in-a-string/),[567字符串的排列](https://leetcode.cn/problems/permutation-in-string/) 840 | 841 | 尽管这道题(438)乍一看上去是固定大小的滑窗,但是由于“异位”的概念用常规方法维护的复杂度较高,产生超时,因此将固定大小设置为条件使用; 842 | 843 | + **固定大小的滑动窗口**: 844 | 845 | 示例[219存在重复元素 II](https://leetcode.cn/problems/contains-duplicate-ii/) 846 | 847 | ```c++ 848 | bool containsNearbyDuplicate(vector& nums, int k) { 849 | unordered_set record; 850 | for (int我 i = 0; i < nums.size(); i++) { 851 | if (i > k) record.erase(nums[i - k - 1]); 852 | if (record.count(nums[i])) return true; 853 | record.insert(nums[i]); 854 | } 855 | return false; 856 | } 857 | ``` 858 | 859 | 示例[239滑动窗口最大值](https://leetcode.cn/problems/sliding-window-maximum/) 860 | 861 | ```c++ 862 | vector maxSlidingWindow(vector& nums, int k) { 863 | int n = nums.size(); 864 | priority_queue> q; 865 | for (int i = 0; i < k; ++i) q.emplace(nums[i], i); 866 | 867 | vector ans = {q.top().first}; 868 | for (int i = k; i < n; ++i) { 869 | q.emplace(nums[i], i); 870 | while (q.top().second <= i - k) { 871 | q.pop(); 872 | } 873 | ans.push_back(q.top().first); 874 | } 875 | return ans; 876 | } 877 | ``` 878 | 879 | + **初始位置不同**:示例[替换空格](https://leetcode.cn/problems/ti-huan-kong-ge-lcof/) 880 | 881 | ```c++ 882 | int count = 0; // 统计空格的个数 883 | int sOldSize = s.size(); 884 | for (int i = 0; i < s.size(); i++) { 885 | if (s[i] == ' ') { 886 | count++; 887 | } 888 | } 889 | // 扩充字符串s的大小,也就是每个空格替换成"%20"之后的大小 890 | s.resize(s.size() + count * 2); 891 | int sNewSize = s.size(); 892 | // 从后先前将空格替换为"%20" 893 | for (int i = sNewSize - 1, j = sOldSize - 1; j < i;) { 894 | if (s[j] != ' ') { 895 | s[i--] = s[j--]; 896 | }else { 897 | s[i--] = '0'; s[i--] = '2'; s[i--] = '%'; 898 | } 899 | } 900 | return s; 901 | ``` 902 | 903 | + **N数之和**: 904 | 905 | 示例[15三数之和](https://leetcode.cn/problems/3sum/),[18四数之和](https://leetcode.cn/problems/4sum/) 906 | 907 | ```c++ 908 | vector> threeSum(vector& nums) { 909 | vector> result; 910 | sort(nums.begin(), nums.end()); 911 | // a = nums[i], b = nums[left], c = nums[right] 912 | for (int i = 0; i < nums.size(); i++) { 913 | if (nums[i] > 0) return result; 914 | if (i > 0 && nums[i] == nums[i - 1]) continue; // 去重 915 | 916 | int left = i + 1; 917 | int right = nums.size() - 1; 918 | while (left < right) { 919 | if (nums[i] + nums[left] + nums[right] > 0) right--; 920 | else if (nums[i] + nums[left] + nums[right] < 0) left++; 921 | else { 922 | result.push_back(\ 923 | ector{nums[i], nums[left], nums[right]}); 924 | while (right > left && nums[right] == nums[right - 1]) 925 | right--; 926 | while (right > left && nums[left] == nums[left + 1]) 927 | left++; 928 | // 找到答案时,双指针同时收缩 929 | right--; 930 | left++; 931 | } 932 | } 933 | } 934 | return result; 935 | } 936 | ``` 937 | 938 | + 左右指针与哈希表集合: 939 | 940 | 941 | 相似题目: [424替换后的最长重复字符](https://leetcode.cn/problems/longest-repeating-character-replacement/) 942 | 943 | ```c++ 944 | int characterReplacement(string s, int k) { 945 | int n = s.size(); int total = 0; 946 | int ans = 0, left = 0, right = 0; 947 | vector window(26,0); 948 | while (right < n) { 949 | window[s[right] - 'A']++; 950 | int max_nums = *max_element(window.begin(),window.end()); 951 | if (right - left + 1 - max_nums <= k) { 952 | ans = max(ans, right - left + 1); 953 | }else{ 954 | window[s[left] - 'A']--; 955 | left++; 956 | } 957 | right++; 958 | } 959 | return ans; 960 | } 961 | ``` 962 | 963 | 相似题目:[395至少有 K 个重复字符的最长子串](https://leetcode.cn/problems/longest-substring-with-at-least-k-repeating-characters/),利用字符串有限的隐藏特性 964 | 965 | ```c++ 966 | int longestSubstring(string s, int k) { 967 | int n = s.size(); 968 | int ans = 0; 969 | for (int kind_limit = 1; kind_limit <= 26; kind_limit++) { 970 | int left = 0; 971 | int right = 0; 972 | unordered_map window; 973 | int total = 0; // 窗口内字符种类 974 | int sat_total = 0; // 满足「出现次数不少于 k」条件的,窗口内字符种类 975 | 976 | while (right < n) { 977 | // 右边界入窗 978 | window[s[right]]++; 979 | if (window[s[right]] == 1) total++; 980 | if (window[s[right]] == k) sat_total++; 981 | // 当窗口内字符种类数超过限制时,左边界收缩 982 | while (total > kind_limit) { 983 | if (window[s[left]] == 1) total--; 984 | if (window[s[left]] == k) sat_total--; 985 | window[s[left]]--; 986 | left++; 987 | } 988 | // 窗口内字符种类 等于 满足条件的字符种类时,取此时的窗口长度 989 | if (total == sat_total) { 990 | ans = max(ans, right - left + 1); 991 | } 992 | right++; 993 | } 994 | } 995 | return ans; 996 | } 997 | ``` 998 | 999 | 1000 | 1001 | ### 1.6 回文相关题目 1002 | 1003 | 1. 回文**子串**可能的**个数**:[647回文子串](https://leetcode.cn/problems/palindromic-substrings/) 1004 | 1005 | ```c++ 1006 | for (int i = s.size() - 1; i >= 0; i--) { // 注意遍历顺序 1007 | for (int j = i; j < s.size(); j++) { 1008 | if (s[i] == s[j]) { 1009 | if (j - i <= 1) { // 情况一 和 情况二 1010 | result++; 1011 | dp[i][j] = true; 1012 | } else if (dp[i + 1][j - 1]) { // 情况三 1013 | result++; 1014 | dp[i][j] = true; 1015 | } 1016 | } 1017 | } 1018 | } 1019 | ``` 1020 | 1021 | 2. 回文**子串**的**最大长度**:[5最长回文子串](https://leetcode.cn/problems/longest-palindromic-substring/) 1022 | 1023 | ```c++ 1024 | class Solution { 1025 | public: 1026 | string longestPalindrome(string s) { 1027 | vector> dp(s.size(), vector(s.size(), 0)); 1028 | int maxlenth = 0; 1029 | int left = 0; 1030 | int right = 0; 1031 | for (int i = s.size() - 1; i >= 0; i--) { 1032 | for (int j = i; j < s.size(); j++) { 1033 | if (s[i] == s[j]) { 1034 | if (j - i <= 1) { // 情况一 和 情况二 1035 | dp[i][j] = true; 1036 | } else if (dp[i + 1][j - 1]) { // 情况三 1037 | dp[i][j] = true; 1038 | } 1039 | } 1040 | if (dp[i][j] && j - i + 1 > maxlenth) { 1041 | maxlenth = j - i + 1; 1042 | left = i; 1043 | right = j; 1044 | } 1045 | } 1046 | 1047 | } 1048 | return s.substr(left, right - left + 1); 1049 | } 1050 | }; 1051 | ``` 1052 | 1053 | 3. 回文**子序列**的**最大长度**:[516最长回文子序列](https://leetcode.cn/problems/longest-palindromic-subsequence/) 1054 | 1055 | ```c++ 1056 | int longestPalindromeSubseq(string s) { 1057 | vector> dp(s.size(), vector(s.size(), 0)); 1058 | for (int i = 0; i < s.size(); i++) dp[i][i] = 1; 1059 | for (int i = s.size() - 1; i >= 0; i--) { 1060 | for (int j = i + 1; j < s.size(); j++) { 1061 | if (s[i] == s[j]) { 1062 | dp[i][j] = dp[i + 1][j - 1] + 2; 1063 | } else { 1064 | dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]); 1065 | } 1066 | } 1067 | } 1068 | return dp[0][s.size() - 1]; 1069 | } 1070 | ``` 1071 | 1072 | 4. **分割回文子串**,**罗列**所有分割方案:[131分割回文串](https://leetcode.cn/problems/palindrome-partitioning/)。解答详见回溯法章节 1073 | 1074 | 5. **分割回文子串**,计算最小**次数**: 1075 | 1076 | ```c++ 1077 | for (int i = s.size() - 1; i >= 0; i--) { 1078 | for (int j = i; j < s.size(); j++) { 1079 | if (s[i] == s[j]) { 1080 | if (j - i <= 1) { // 情况一 和 情况二 1081 | isPalindromic[i][j] = true; 1082 | } else if (isPalindromic[i + 1][j - 1]) { // 情况三 1083 | isPalindromic[i][j] = true; 1084 | } 1085 | } 1086 | } 1087 | } 1088 | // 初始化 1089 | vector dp(s.size(), 0); 1090 | for (int i = 0; i < s.size(); i++) dp[i] = i; 1091 | 1092 | for (int i = 1; i < s.size(); i++) { 1093 | if (isPalindromic[0][i]) { 1094 | dp[i] = 0; 1095 | continue; 1096 | } 1097 | for (int j = 0; j < i; j++) { 1098 | if (isPalindromic[j + 1][i]) { 1099 | dp[i] = min(dp[i], dp[j] + 1); 1100 | } 1101 | } 1102 | } 1103 | return dp[s.size() - 1]; 1104 | ``` 1105 | 1106 | ### 1.7 优先队列/堆的相关题目 1107 | 1108 | 1. 优先队列自定义排序算法 1109 | 1110 | ```c++ 1111 | // case1 1112 | struct cmp{ 1113 | bool operator()(ListNode* a, ListNode* b){ 1114 | return a->val > b->val; 1115 | } 1116 | }; 1117 | priority_queue, cmp> pq; 1118 | 1119 | // case2 1120 | auto cmp = [&](ListNode* a, ListNode* b) -> bool{ 1121 | return a->val > b->val; 1122 | }; 1123 | priority_queue, decltype(cmp)> pq(cmp); 1124 | ``` 1125 | 1126 | 2. 优先队列维护归并算法:[378有序矩阵中第 K 小的元素](https://leetcode.cn/problems/kth-smallest-element-in-a-sorted-matrix/)、[347前 K 个高频元素](https://leetcode.cn/problems/top-k-frequent-elements/)、[692前K个高频单词](https://leetcode.cn/problems/top-k-frequent-words/) 1127 | 1128 | ```c++ 1129 | int kthSmallest(vector>& matrix, int k){ 1130 | auto cmp = [&](pair &a, pair &b) -> bool 1131 | { return matrix[a.first][a.second] > matrix[b.first][b.second]; } 1132 | 1133 | priority_queue, vector>, \ 1134 | decltype(cmp)> que(cmp); 1135 | 1136 | for (int i=0; i cur_pos = que.top(); 1145 | return matrix[cur_pos.first][cur_pos.second]; 1146 | } 1147 | ``` 1148 | 1149 | ## 2. 面试算法 1150 | 1151 | ### 排序算法 1152 | 1153 | 1. **综合比较** 1154 | 1155 | + 选择:快排代码紧凑,常数因子小,局部性良好;归并需要额外空间大,稳定性好; 1156 | 1157 | + 性能表: 1158 | 1159 | image-20220725170827235 1160 | 1161 | 2. **归并排序**: 1162 | 1163 | + **算法步骤**:参考[知乎资料](https://zhuanlan.zhihu.com/p/452169920) 1164 | 1165 | 1. 归并排序是一种递归算法,持续地将一个数组分成两半。 1166 | 1167 | 2. 如果数组是空的或者只有一个元素,那么根据定义,它就被排序好了。 1168 | 1169 | 3. 如果数组里的元素超过一个,我们就把数组拆分,然后分别对两个部分调用递归排序, 1170 | 1171 | 4. 一旦这两个部分被排序好了,就对这两个部分进行归并。 1172 | 1173 | image-20220725172355076 1174 | 1175 | + **代码实现** 1176 | 1177 | ```c++ 1178 | void merge(vector& v, int left, int mid, int right){ 1179 | vector temp = v; 1180 | int i = left, j = mid + 1; 1181 | int index = left; 1182 | while(i <= mid || j <= right){ 1183 | if(i > mid){ 1184 | // 左侧数组到头了,只存放右侧数组 1185 | v[index++] = temp[j]; 1186 | j++; 1187 | } 1188 | else if(j > right){ 1189 | // 右侧数组到头了,只存放左侧数组 1190 | v[index++] = temp[i]; 1191 | i++; 1192 | } 1193 | else if(temp[i] < temp[j]){ 1194 | // 左侧数组更小,放入数组中,索引向后延展一位 1195 | v[index++] = temp[i]; 1196 | i++; 1197 | } 1198 | else{ 1199 | // 右侧数组更小,放入数组中,索引向后延展一位 1200 | v[index++] = temp[j]; 1201 | j++; 1202 | } 1203 | } 1204 | 1205 | } 1206 | void merge_Sort(vector& v, int left, int right){ 1207 | // 左值大于右值,终止拆分 1208 | if(left >= right) return; 1209 | int mid = (left + right) / 2; 1210 | merge_Sort(v, left, mid); 1211 | merge_Sort(v, mid + 1, right); 1212 | if(v[mid] > v[mid + 1]){ 1213 | // 优化:证明已经有序了,无需再合并 1214 | merge(v, left, mid, right); 1215 | } 1216 | } 1217 | void mergeSort(vector& v){ 1218 | int n = v.size(); 1219 | merge_Sort(v, 0, n - 1); 1220 | } 1221 | ``` 1222 | 1223 | + 相关题目 1224 | 1225 | [合并K个升序链表](https://leetcode.cn/problems/merge-k-sorted-lists/):关键思路提示 1226 | 1227 | ```c++ 1228 | ListNode* mergeTwoLists(ListNode* a, ListNode* b){ 1229 | if ((!a) || (!b)) return a ? a : b; 1230 | ListNode* cur = new ListNode(); 1231 | ListNode* head = cur; 1232 | ListNode* aPtr = a, *bPtr = b; 1233 | while(aPtr && bPtr){ 1234 | if (aPtr->val > bPtr->val){ 1235 | cur->next = bPtr; bPtr = bPtr->next; 1236 | }else{ 1237 | cur->next = aPtr; aPtr = aPtr->next; 1238 | } 1239 | cur = cur->next; 1240 | } 1241 | if (aPtr) cur->next = aPtr; 1242 | else cur->next = bPtr; 1243 | return head->next; 1244 | } 1245 | ListNode* merge(vector &lists, int left, int right) { 1246 | if (left == right) return lists[left]; 1247 | if (left > right) return nullptr; 1248 | int mid = left + (right-left) >> 1; 1249 | return mergeTwoLists(merge(lists, left, mid), merge(lists, mid + 1, right)); 1250 | } 1251 | ``` 1252 | 1253 | 3. **快速排序**:[知乎资料](https://zhuanlan.zhihu.com/p/452168666) 1254 | 1255 | + **算法步骤** 1256 | 1257 | 1. 从数组中挑出一个元素,称为基准; 1258 | 1259 | 2. 重新排序数组,所有比基准小的数放在基准前面,所有比基准大的元素放在基准后面,这称为分区(partition)操作; 1260 | 1261 | 3. 递归地对基准两边的子数组进行排序。 1262 | 1263 | + **代码实现** 1264 | 1265 | ```c++ 1266 | void quick_Sort(vector& v, int left, int right){ 1267 | if(left >= right) return; 1268 | int index = rand()%(right-left+1) + left; // 随机产生一个下标 1269 | swap(v[left], v[index]); // 注意:先把它放在最前面 1270 | int i = left, j = right, base = v[left]; //取最左边的数为基数 1271 | while(i < j){ 1272 | //降序是先<=再>=,升序相反; j永远在前,i永远在后 1273 | // 找到从右侧第一个小于base的值 1274 | while(v[j] >= base && i < j) j--; 1275 | // 找到左侧第一个大于base的值 1276 | while(v[i] <= base && i < j) i++; 1277 | // 左右指针进行交换 1278 | if(i < j) swap(v[i], v[j]); 1279 | } 1280 | // 和头指针进行交换 1281 | swap(v[left], v[i]); 1282 | // 递归的实现 1283 | quick_Sort(v, left, i - 1); 1284 | quick_Sort(v, i + 1, right); 1285 | } 1286 | void quickSort(vector& v){ 1287 | int n = v.size(); 1288 | quick_Sort(v, 0, n - 1); 1289 | } 1290 | ``` 1291 | 1292 | 4. **选择排序**: 1293 | 1294 | + **算法步骤** 1295 | 1296 | 1. 每一次遍历一次数组只会进行一次交换,即在遍历过程中记录最大项的索引, 1297 | 2. 完成遍历后,再把它换到正确的位置,同样若数组有n项,它也需要遍历n-1次。 1298 | 1299 | + **代码实现** 1300 | ```c++ 1301 | void selectSort(vector& v){ 1302 | int n = v.size(); 1303 | for(int i=0; i v[index]){ 1307 | index = j; 1308 | } 1309 | } 1310 | swap(v[index], v[n-1-i]); 1311 | } 1312 | } 1313 | ``` 1314 | 1315 | 5. **排序算法的扩展**: 1316 | 1317 | + [347前 K 个高频元素](https://leetcode.cn/problems/top-k-frequent-elements/)、[973最接近原点的 K 个点](https://leetcode.cn/problems/k-closest-points-to-origin/)、[面40: 最小K个数](https://leetcode.cn/problems/smallest-k-lcci/) 1318 | 1319 | ```c++ 1320 | class Solution { 1321 | public: 1322 | vector topKFrequent(vector& nums, int k) { 1323 | unordered_map mp; 1324 | for(auto num : nums) mp[num]++; 1325 | vector> vals; 1326 | for(auto a : mp) vals.push_back(a); 1327 | vector ans; 1328 | quicksort(vals, 0, vals.size()-1, ans, k); 1329 | return ans; 1330 | } 1331 | private: 1332 | void quicksort(vector> &v, int left, int right, 1333 | vector&ans, int k) 1334 | { 1335 | // 不带等号,以保证相等时能够被访问 1336 | if(left > right) return; 1337 | // 以下和快排相同 1338 | int i = left, j = right; 1339 | auto base = v[left]; 1340 | while(i < j){ 1341 | //降序是先<=再>=,升序相反; j永远在前,i永远在后 1342 | while(i < j && v[j].second <= base.second) j--; 1343 | while(i < j && v[i].second >= base.second) i++; 1344 | swap(v[i], v[j]); 1345 | } 1346 | swap(v[left], v[i]); 1347 | // 如果分组个数k小于左侧分组个数,则一定在左半部分分组里,问题缩小 1348 | if(k <= i-left) 1349 | quicksort(v, left, i-1, ans, k); 1350 | else{ 1351 | // 如果分组个数大于左侧分组个数,则左侧分组一定全部被包括 1352 | for(int m = left; m <= i; m++) 1353 | ans.push_back(v[m].first); 1354 | // 右侧分组只需要找剩下的k-(i - left + 1)个数即可 1355 | if(k > i - left + 1) 1356 | quicksort(v,i+1, right, ans, k-(i - left + 1)); 1357 | } 1358 | } 1359 | }; 1360 | ``` 1361 | 1362 | 1363 | 1364 | -------------------------------------------------------------------------------- /选择题等其他零碎知识点/综合知识点.md: -------------------------------------------------------------------------------- 1 | ## 综合知识点 2 | 3 | [TOC] 4 | 5 | ### 1 Linux系统指令 6 | 7 | #### shell传参: 8 | 9 | + \$# 是传给脚本的参数个数 10 | + \$0 是脚本本身的名字 11 | + \$1 是传递给该shell脚本的第一个参数 12 | + \$2 是传递给该shell脚本的第二个参数 13 | + \$@ 是传给脚本的所有参数的列表 14 | + \$* 是以一个单字符串显示所有向脚本传递的参数,与位置变量不同,参数可超过9个 15 | + $$ 是脚本运行的当前进程ID号 16 | + $? 是显示最后命令的退出状态,0表示没有错误,其他表示有错误 17 | 18 | #### 文件系统权限 19 | 20 | 1. 用二进制表示 rwx,r 代表可读,w 代表可写,x 代表可执行。 21 | 22 | + 如果可读,权限二进制为 100,十进制是4; 23 | 24 | + 如果可写,权限二进制为 010,十进制是2; 25 | 26 | + 如果可执行,权限二进制为 001,十进制是1; 27 | 28 | 2. chmod(change mode)命令:控制用户对文件的权限的命令。 29 | 30 | + \+ 表示增加权限,x 表示可执行权限,r 表示可读权限。 31 | 32 | #### grep指令: 33 | 34 | + -E:表示使用扩展的正则表达式 35 | + ^:匹配正则表达式的开始行 36 | + $: 匹配正则表达式的结束行 -------------------------------------------------------------------------------- /项目经历/陈嘉皓.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfkiwl/slam_and_leetcode/47120bfea76f6f53bb2a003aeae16ce1318c4432/项目经历/陈嘉皓.pdf -------------------------------------------------------------------------------- /项目经历/项目经历及简历介绍.md: -------------------------------------------------------------------------------- 1 | ## 项目经历 2 | 3 | ### 一、Apollo项目概述 4 | 5 | 1. **总述**: 6 | 7 | 这个项目主要将Apollo系统部署在公司的实际车辆上,调试底盘、激光雷达、相机、组合惯导等传感器,对定位、感知、规控等单元进行功能性测试,最终实现小场景内无人驾驶DEMO。我们使用pix车厂购置的汽车底盘以及相关的传感器作为硬件基础,并使用Apoll6.0这一主流的自动驾驶框架作为软件系统。 8 | 9 | 2. **系统框架**: 10 | 11 | 给我影响最深的可能是整个自动驾驶框架,让我感觉到一个工业级或者近工业级的工程的魅力。整个系统开阔了我的眼界,除了之前有接触过的日志系统、参数系统之外,还有时钟和定时器,单元测试、线程池管理、序列化和反序列化、系统监控等。因为时间关系,其中部分没有深入了解,但是它的确是我当前阶段的学习方向;同时,从驱动、定位、感知再到routing和规划,最后到控制,使我对自动驾驶有一种全貌性的了解,比如定位就需要为感知提供定位、为规划提供定位和速度等;因为本质上这些模块都是相互依赖的,对整个工程的理解的确加深了。 12 | 13 | 3. **定位部分**: 14 | 15 | 我主要负责定位、标定和感知部分。Apollo系统的定位系统其实很简陋,特别是最困难的多传感器融合部分还被闭源了,但是并不是说在这部分并没有什么收获。相反,通过阅读代码,包括debug,在我脑海里面建立起了一套比较清晰的时间、空间和坐标系统一框架,理解了定位遇到的各种困难:包括信号丢失、同一topic的消息到两个节点之间的顺序不一致的等情况; 16 | 17 | 后来我们把一个完成的多传感器融合框架迁移到了Apollo上面来。我们采用的是动态库的方案,这里面牵扯非常多的东西。首先就是库的引用与封装,因为Apollo是用的bazel封装的,很多库需要自己写或者换,比如说VINS里面用的那个地理库就被我们换成了Proj.4,Sophus库被我们用自己写的类给替换掉了等等;然后原始代码和ros的耦合度比较高,需要把代码进行解耦,还需要对接口进行分离,避免过多的库依赖。收获还是蛮多的。 18 | 19 | 4. **标定部分** 20 | 21 | 标定部分我理解在正式大公司是一个非常复杂的系统性工程,到最后需要实现标定车间的自动化标注。但是由于要求不是那么高。总的来说,我们的标定分为两部分,时钟对齐和内外参标定。 22 | 23 | 内参参标定又分为内参标定和外参标定,内参标定包括用张正友标定相机内参,轮式里程计标定,车辆动力学标定等;外参标定包括激光和相机之间的标定(这个采用了边缘对齐的方法),还有组合惯导和激光的标定(这个先用手眼标定AX=XB获取初始,再用ICP对齐实现的)。代码实现部分其实是参考了一些开源框架和mentor自己写的程序。 24 | 25 | 时钟对齐由于相机不是那种定制的相机,因此只考虑了激光时钟和组合惯导通过PPS脉冲信号+NMEA消息进行对齐,当激光雷达接收到PPS信号时,会去置零微秒定时器。当合法有效的GPRMC信号到来的时候,系统抽取GPRMC信号中的时间,换算成整秒,修正激光雷达整秒的时间戳 26 | 27 | ### 二、物体级别SLAM系统 28 | 29 | 当时选这个课题的时候过程也是很坎坷的,因为导师的一些原因,也是有很多折磨和妥协的。之所以选择物体级别的语义,而不是选择点云级别的语义,是因为物体其实具有天然的语义属性,它所具有的形状、朝向、大小等信息,更容易和语义地图、动态场景去除甚至后续的感知规划环节进行结合。 30 | 31 | 整个项目是建立在EAO-SLAM基础上进行的。由于这个系统其实只能算是半开源,因此我首先对这个系统进行了大量的改进:修正了很多bug,插入了基于TR的YOLOX作为在线的目标检测,增加了可视化,增加了ROS接口和可视化,增加了优化部分代码,提供基于IMU的初始化等等 32 | 33 | 在实践中逐渐发现它采用的物体估计算法并不是很准确,尤其是在机器人应用中,由于视角受限,我们只能看到物体的一部分,因此可能很难构建合适的模型。然后查了查资料,发现北航有这方面的研究,都是基于对称性来检测的。但是对称性耗时比较大。因此就想到了,既然观测不完全,能否使用退化的模型代替?于是就有了这个可进化的SLAM,用圆柱模型这种退化模型描述物体。 34 | 35 | 同时,我也测试了一些数据关联算法,这方面算法比较多,我迁移了几个算法试了试,部分算法速度比较慢,有的 需要用深度学习,比较麻烦,最后选择了bytetracker作为二维追踪器 ,配上之前的EAO那种基于统计学的方法,完成了比较准确的数据关联。 36 | 37 | + 缺点:圆柱其实不是一个很好的特征表示,圆柱拟合更依赖于点云获取,在室内需要深度相机的参与,然而常见的深度相机视野过于小,远远不如激光或者鱼眼相机带来的增益,因此我理想去做物体slam更多的应该关注单目+imu这种形式;此外,关于物体的位姿估计,是否深度学习方案能代替传统的位姿估计方案也是一个很值得思考的问题,我的回答是肯定会,但是受限于一些外在原因,这个方向也没法深入下去;最后,物体位姿是否用于优化,这是一个非常值得思考的问题,大部分结果显示,物体位姿对slam精度和鲁棒性提高并没有想象那么多,甚至远远不如多基元来的多。更多的物体slam中优化机器人本身位姿倒是一个副产品,而对物体位姿估计成了主要目标;从我个人来看,我更倾向于语义点云slam建图,然后后处理获得物体位姿,然后再定位部分引入物体slam,这样可能更加合理一点; --------------------------------------------------------------------------------