秋招面经整理

复习进度

  • C++
  • 算法
  • 操作系统
  • 计算机网络
  • 计算机图形学
  • 数据库
  • 场景题
  • 智力题
  • HR面

目录

语言基础C++

基本概念的区别

指针与引用

  • 指针是一个变量,它的内容是所指向的内存的地址, 它本身需要占用额外的内存空间(4个字节);引用对变量起的别名,类似于#define一样的功能, 本身不占用额外的内存空间,所以sizeof引用得到的是所指向的变量(对象)的大小,而sizeof指针得到的是指针本身的大小;自增自减操作得到的结果也不同
  • 引用定义时必须初始化并且不能再改变指向;指针可以不进行初始化,可以在任何时候改变它的内容.
  • 引用不能为空,指针可以为空

指针,数组,函数指针

数组名可以看成是一个常量,不能对它赋值,但可以取地址。取了地址可以赋值给一个指向数组的指针。把数组名当作函数参数的时候就退化为指针。

概念 形式 说明
指针数组 int *p[5] []优先级高于*, 因此p首先是个数组,每个数组元素的类型是int *
指向数组的指针 int (*p)[5] ()优先级最高,因此p首先是个指针,然后指向匿名数组int[5]
函数指针 int *(*p)(int x, int y) p是个指向形参为(int x, int y)返回值为int*的函数的指针
函数指针数组 int *(*p[5])(int *x) ()优先级最高,它的内容是个指针数组,每个元素都是函数指针
指向函数指针数组的指针 int *(*(*p)[5])(int *x) ()优先级最高,它的内容是一个指针,指向了一个含5各元素的数组. 数组内每个元素类型都是指向某种函数的指针, 该种函数形参为(int* x),返回值为int *

类与结构体

  • 默认的成员/数据访问权限不同
  • 默认的继承访问权限不同

可以理解为一个是数据结构的实现方式,另一个是对象的实现方式

有了类为什么还要保留结构体? 答:兼容性。

隐藏、覆盖、重载

  • 覆盖:函数同名+参数列表相同+基类virtual
  • 隐藏:函数同名+参数列表相同+基类无virtual 或 函数同名+参数不同
  • 重载:函数同名+参数列表不同+同一个类内

new/delete malloc/free

  1. new / new[]:完成两件事,先底层调用 malloc 分配了内存,然后调用构造函数(创建对象)。
  2. delete/delete[]:也完成两件事,先调用析构函数(清理资源),然后底层调用 free 释放空间。
  3. new 在申请内存时会自动计算所需字节数,而 malloc 则需我们自己输入申请内存空间的字节数。

为什么有malloc了还需要new?

浅拷贝和深拷贝

浅拷贝指的是拷贝对象的指针,会使得多个指针指向同一块内存,如果不小心就会出现野指针。深拷贝则是拷贝指针指向的内容.。

举例来说, 浅拷贝就像一个英雄使用影分身技能, 死了分身也没了. 深拷贝就像用另一个号创建同一个英雄参加到战场上.

介绍一下关键字 xxx?

1. const (高频)

这个关键字都是为了说明修饰的变量或对象都是不能被改变的。具体用法:

  • 修饰类的成员变量、类的对象时,放在前面,表示不能修改。const变量要在其他文件中访问必须加extern关键字,非const变量则默认为extern.
  • 修饰类的成员函数时,放在函数名后面,表示该函数内不会修改类的除了static成员之外的数据成员。并且类的const对象只能调用被const修饰的成员函数。
  • 修饰引用,一般用于形参类型,避免拷贝,又避免函数对值的修改.
  • 修饰指针,放在类型前面(如const int *p)表示指向常量的指针,只能读取指向的内容而不能修改,但可以改变指向,甚至可以指向非const对象。放在类型后面(如int *const p)表示指针常量,声明时必须初始化,初始化后不能改变指向,但是可以改变指向的内容。
const修饰函数时,想改变成员变量的两种方法?
  • 将成员变量声明为mutable
  • 使用const_cast将成员变量的指针转换为非const类型
  • 将成员变量以引用传值的形参类型传入const函数,调用时用this指针解引用的方式
const修饰的函数可以重载么

可以。并且参数列表可以相同。重载后,非const对象调用的是非const版本,const对象调用的是const版本。实现原理是调用成员函数时传入的第一个参数是this指针,所以实际上调用该函数时,传入的是第一个参数不相同的参数列表。

const和#define的区别

const常量具有类型,编译器可以进行安全检查;#define宏定义没有数据类型,只是简单的字符串替换,不能进行安全检查。

const和constexpr的区别

用constexpr修饰主要是为了效率, 表示这个变量或函数在编译器就可以计算出它的值. 对于函数来说, 如果其传入的参数可以在编译时期计算出来,编译器就可以将函数体直接优化成编译期常量. 如果不能, 那么就作为普通函数对待.

2. static (高频)

  1. 修饰普通变量,修改变量的存储区域和生命周期,使变量存储在静态区,在 main 函数运行前就分配了空间,如果有初始值就用初始值初始化它,如果没有初始值系统用默认值初始化它。
  2. 修饰普通函数,表明函数的作用范围,仅在定义该函数的文件内才能使用。在多人开发项目时,为了防止与他人命名空间里的函数重名,可以将函数定位为 static。
  3. 修饰成员变量,修饰成员变量使所有的对象只保存一个该变量,而且不需要生成对象就可以访问该成员。

(字节二面) static成员变量可以在类内初始化吗?
答:一般情况下必须在类外初始化,如果需要在类内初始化,那就要声明为static const类型

  1. 修饰成员函数,修饰成员函数使得不需要生成对象就可以访问该函数,但是在 static 函数内不能访问非静态成员

3. inline

inline函数是在调用时,在行内插入或替换整个函数的代码。它只是对编译器的建议,具体是否为inline取决于编译器。一般包含循环,switch/goto语句,递归,静态变量的函数会被忽略inline请求。可以用于解决重复定义的函数问题(单一定义原则), 但是否inline还是取决于编译器.

针对单一定义原则还可以使用static和匿名命名空间修饰函数.

隐式: 定义在类内的函数会被当作inline函数。

显式: 必须加在函数定义之前,声明前面是无效的。

inline函数优缺点?

优点是节省了函数调用的开销,会进行类型检查,比宏更安全。缺点是以函数复制为代价,会消耗内存;如果有循环体,代码的执行时间可能要大于函数调用的开销。

inline和宏的区别

宏是由预处理器进行的简单的代码替换。而inline是编译器来控制实现的,会进行类型安全检查或自动类型转换,并且只在调用时进行展开。

4. cast (高频)

static_cast 的作用类似于C语言中的强制类型转换,例如可以将int强制转为double, void*转为带类型的对象指针等,但不能把整个struct通过static_cast转为int或double. 当然除此以外还可以将左值转化为右值引用,来实现完美转发。

dynamic_cast用于继承体系中安全的向下转型,即父类转为子类。需要多态的支持。

const_cast只用于去除对象的const或volatile属性.

介绍一下volatile关键字的作用?

reinterpret_cast 是对转换数进行二进制的重新解释。例如说char *p; int i = reinterpret_cast<int>(p)就是将指针p的值以二进制形式重新解释为int然后赋值给i. 一般用于不同类型的函数指针的转换。

dynamic_cast的原理? 让你设计的话怎么做?

dynamic_cast最常用于父类向下转型。对象的类型信息被存在虚表的首部,运行期间,父类指针根据子类对象的虚指针找到虚表后,比对虚表首部的类型和要转换的类型是否一致。我来设计的话,我会在基类中添加一个虚函数getClassType,在运行时获取对象的类型,和转换的类型对比是否一致。

static_cast返回失败时怎么办 (不确定)

static_cast不提供运行时的检查,所以在代码编写阶段就要确认转换的安全性。返回失败是说明编译不通过,要自己检查类型是否可以转换。

cast 的缺点
  • static_cast不进行类型安全检查,转换失败时也不会返回NULL,如果实际指向父类的指针被转换为子类指针,可能会导致越界崩溃。
  • dynamic_cast由于需要通过虚表查询类型进行转换,会带来性能上的损失。
  • const_cast强制去除了对象的const属性,这与初始化const对象时的意向相违背,会导致原本可以避免的意外发生.
  • reinterpret_cast是平台依赖,所以会导致代码的移植性差.

介绍一下多态?

多态指的是相同的接口类型会因为参数或对象类型的不同而引发不同的动作. 举例来说, 调用类的某个虚函数时, 根据类实例化对象的不同, 同名的虚函数会有不同的操作.

虚函数是什么?虚表是什么?

虚函数指的是virtual修饰的成员函数。每一个包含虚函数的类,编译器都会构造一个函数表,按声明顺序存储每个虚函数的调用地址,这就是虚表。虚表是在编译时确定的,属于类而不属于某个具体的实例。虚函数表存放在可执行文件的只读数据字段,仅有一份。

多态的作用

主要是使得代码可扩充性增加,更易于维护.

举例来说的话, 比如英雄类包含法师,战士,射手等职业, 编写法师类的时候, 就要分别编写对战士射手的攻击和受伤函数, 如果增加一个新职业,就要修改每个职业的类. 但是使用多态的话, 在编写某个职业的攻击函数时,只需要传入一个英雄类指针,这是一个父类指针, 然后在运行时会具体指向某个职业的对象, 这样新增职业就不需要去修改每个职业的类

静态多态与动态多态? 实现原理?

动态多态:对于相关的对象类型,确定它们之间的一个共同功能集,然后在基类中,把这些共同的功能声明为多个公共的虚函数接口。

动态多态是虚函数,虚表, 虚指针和动态绑定来实现的。基类的函数被定义为virtual,派生类方法可以进行override定义自己的行为,其中包含定义为virtual函数的基类隐含一个指针成员,指向虚函数表,该表按照虚函数声明顺序保存地址,派生类中也包含这样一张表,如果发生重写,就更新虚函数表中的对应地址。

静态多态:对于相关的对象类型,直接实现它们各自的定义,不需要共有基类,甚至可以没有任何关系。只需要各个具体类的实现中要求相同的接口声明,这里的接口称之为隐式接口。

静态绑定通过函数重载或函数模板实现。

函数重载的原理是使用name mangling(倾轧)技术修改函数名,以区分不同参数列表的同名函数。可以对编译后的.o文件执行objdump -t命令来查看符号表。

函数名的修改有什么原则吗? 有。修改规则为: 固定前缀_Z+参数个数+函数名+参数类型首字母

函数模板的原理是编译器先对声明的函数体进行第一次编译,得到一个带有类型参数的半成品,在调用处对根据指定的参数类型再编译一次。

虚函数调用是怎么实现的?

包含虚函数的类的对象的头部含有一个虚指针,指向自己的虚表。发生动态绑定时,父类指针指向的是子类的对象,所以可以通过该对象的虚指针找到正确的虚表,再找到所调用的虚函数的地址。

虚函数的地址是按声明顺序存在虚表中的,所以调用的是第几个虚函数,就从虚表中取出第几行,也就是调用函数的实际地址。(可以理解为偏移量)

多继承的子类有几个虚函数表? 菱形继承怎么解决?

多重继承(不是多级派生)时,有多少个含虚函数的基类,子类对象就有多少个虚指针,对应着不同的虚表,排列顺序按照声明顺序。 子类的成员函数被放到了第一个父类的表中。

通过使用虚继承来解决菱形继承,这样通过多条路径间接继承虚基类的派生类中只有一份虚基类成员的拷贝。构造时调用虚基类的构造函数对虚基类初始化,而忽略虚基类的其他派生类对虚基类的构造函数。

虚函数内存泄漏怎么解决?

虚函数出现内存泄漏的情况一般是父类指针没有正确调用子类的析构函数,把父类的析构函数声明为虚函数可以避免内存泄漏。

构造函数能为虚函数吗?析构函数?

不可以。如果构造函数是虚的,就需要通过虚表来调用,可是虚表是通过对象的虚指针来访问的,此时对象还没有实例化,就无法找到对应虚表,所以构造函数不能是虚函数。在发生继承关系时,析构函数必须是虚函数,否则无法正确销毁对象。

在构造函数中调用虚函数?

父类的构造函数中调用虚函数,这时不会发生多态行为,调用的仍然是父类自己的虚函数。因为子类对象还未生成,无法通过虚指针找到对应的子类虚函数。

父类指针调用父类非虚函数? 调用子类的非虚函数?

当父类指针指向子类对象,并调用一个父类中的非虚函数,这时不会发生多态行为。不论子类的同名函数是否为虚,此时父类指针调用的这个函数是父类中的函数,不是子类中的。父类指针无法调用子类的非虚函数。

若用子类指针调用与父类同名的非虚函数,则调用的是子类中的函数,即不发生多态行为而是隐藏。

STL

vector和list有什么区别? 适用场景?

vector是数组,底层是一段连续的内存空间,list是链表,内存空间不连续。vector可以O(1)时间访问第i个元素,而list必须遍历。但是插入删除中间某个数时,vector需要移动其他数,为O(n). 而list为O(1).

vector适用于需要在中间插入删除较少,而随机访问较多的场景;list与它相反.

介绍一下vector?

介绍一下map

STL中的map基于红黑树。它是一种接近平衡的二叉搜索树, 它的性质是任意一个节点到它的每个叶子节点的路径包含着相同数目的黑色节点, 所有叶子节点到根节点的路径上不会有连续的红色节点.

红黑树的优点: 最长路径不会超过最短路径的2倍

一些特殊树的概念
二叉搜索树 = 二叉排序树 = BST
平衡二叉树 = AVL树
红黑树 = RB树 = 近似AVL树的二叉搜索树

红黑树什么时候需要调整?

插入节点默认红色,当它的父节点也是红色时,需要向上染色,或者进行旋转操作,来保持性质.

为什么有平衡二叉树了还要红黑树?

红黑树相比于AVL树,牺牲了部分平衡性,以换取删除/插入操作时少量的旋转次数,整体来说,性能优于平衡二叉树。

由于严格的平衡性, 有时候平衡二叉树可能会需要一直调整到根节点.

介绍一下unordered_map

STL中的unordered_map基于开链地址法的哈希表。用一个vector容器存储各个链表的头指针. 每个链表称为一个桶,按照在vector中的存储顺序进行编号. 插入一个键值对的时候, 由hash函数算出hash值,再对桶总数取模得到桶的编号,然后放到这个桶里. hash函数: 设计尽量复杂的hash函数, 基于线性同余的伪随机数生成器来计算哈希值,然后再对桶数量取模.

链的长度太长了怎么办?

需要增加桶的数量,也就是vector的长度, 然后重新hash. 增加的数量来自于下一个最接近2倍的质数. 选用质数的原因是为了减少冲突的可能性. 可以举例2468模4和模5.

除了开链地址法,还有哪些冲突处理方法?
  • 开放定址法: 再往后查找一个空位
  • 再哈希法: 多个不同哈希函数
  • 溢出区法: 建立一个公共的溢出表存放溢出的元素
容器迭代器失效的情况
  • 负载因子超过默认值, 系统增加桶的数量并重新进行哈希, 此时容器的迭代器失效. 但是指向单个键值对的指针和引用仍然有效.
  • 调用erase函数时,删除元素的迭代器失效,并且不返回
让你自己设计一个哈希表要怎么设计? 为什么要引入哈希表? (米哈游)

STL中的堆?

堆结构是个特殊的完全二叉树,一般存在一个数组中。第i个数的左子节点的下标是2i+1,右是2i+2,每次插入新元素就从最后一个分支节点也就是n/2开始检查它和子节点的大小关系,然后进行调整。

在STL中一般将优先队列priority_queue当作堆来使用。

智能指针? 实现原理(怎么确保内存不泄露)?

总共三种(auto_ptr已被移除)。通过资源获取即初始化(RAII)的思想来实现的。在智能指针的构造函数中进行控制块的申请,在其析构函数中对资源进行释放.

unique_ptr 保证内存只会一个指针占有, 当它的生存期结束,就自动调用析构函数,释放指针和指针所指向的内存.

share_ptr通过引用计数, 每增加一个指向资源的指针,计数器就加1, 每销毁一个指针就减1, 在销毁最后一个指针时, 引用计数为0, 析构函数中就会自动释放资源.

weak_ptr是为了解决shared_ptr使用过程中遇到的一些问题. 例如说

  • 两个shared_ptr的循环引用, 会使得use_count永远都不为0;
  • 还有多线程程序中,如果shared_ptr提前被某个线程reset()了, 另一个线程在访问的时候就会访问到空指针.

weak_ptr的实现原理是通过一个lock()函数来实现的,它产生一个临时变量保存share_ptr的值,并且不影响share_ptr中的use_count的值。即使share_ptr被reset或者被销毁,它所指向的内存地址也能继续通过weak_ptr的lock()函数来获取,一直被保留到最后一个weak_ptr的生命周期为止,从而避免内存泄漏。

(字节一面) 那怎么判断什么时候要使用weak_ptr呢?

使用智能指针需要注意什么?
  • 尽量用make_shared/make_unique,少用new(数据和控制块同时申请)
  • 智能指针管理的资源它只会默认删除new分配的内存,如果不是new分配的则要传递给其一个删除器,例如malloc分配的内存 -> malloc/new的区别
  • 不要把一个原生指针给多个shared_ptr或者unique_ptr管理 (多次析构)

介绍一下C++11的新特性

右值引用

右值引用的目的是为了延长右值的生命周期, 例如 a+b这个表达式,就是一个右值, 可以被右值引用之后, 重新用于后续的计算. 右值引用最大的作用是用于实现移动语义和完美转发.

先说移动语义. 有时候我们会有一些需求, 在函数内部产生对象, 进行一些处理以后,将处理之后的对象返回到外部. 函数的返回值是一个临时对象, 它通过拷贝构造函数被创建,然后又在外部通过拷贝赋值函数复制给外部变量. 因为这个临时对象几乎转瞬即逝, 没有必要花费这些拷贝开销.

移动语义是通过右值引用和移动构造函数实现的. 移动构造函数接收一个临时对象的引用(即右值引用),然后直接转移该引用指向的内存的所有权, 而不进行内存的申请和拷贝操作.

说说移动构造函数?

完美转发指的是创建一个转发函数时,要将接收到的参数按原本的类型转发给目标函数. 其中最特别的就是右值, 通过函数形参的方式传入自然就变成了左值. 要实现完美转发就要借助forward函数返回右值引用, 再传给目标函数.

列表初始化

可变参数模板

lambda表达式

C++的垃圾回收机制?

C++没有提供垃圾回收机制,需要自己实现. 据我了解是有标记清除, 引用计数等垃圾回收算法. 一般来说使用智能指针可以规避大部分的产生内存垃圾的情况.

C++为什么没有提供?

C++的异常机制

异常指的是程序在运行过程中出现的问题,例如程序奔溃,内存泄漏,越界访问等等. C++的异常机制提供了异常抛出,异常标识和捕获,对应throw,try和catch关键字. 作用是: 不希望程序立刻奔溃,而是可以打印当前的异常,进行一些修复工作,然后继续执行后面的工作.

其他

请讲一下B继承A是一个什么流程?

B首先调用A的构造函数,完成A成员的初始化,然后调用自己的构造函数,若存在同名函数和数据成员,则会发生隐藏行为。若存在同名虚函数,则发生覆写行为。

C++程序的生成过程

在这里插入图片描述

extern “C” 的作用

是为了在C++代码中调用C语言编写的库时能够正确链接.

原因详解: C++为了实现重载, 会对函数的汇编代码进行修饰. 而C语言不会. 所以要用extern "C"告诉编译器:这是一个用 C 写成的库文件,请用 C 的方式来链接它们。

内存对齐

数据成员对齐的规则就是,而在第一个成员之后,每个成员距离struct首地址的距离 offset, 都是struct内成员自身长度(sizeof) 与 #pragma pack(n)中的n的最小值的整数倍,如果未经对齐时不满足这个规则,在对齐时就会在这个成员前填充空子节以使其达到数据成员对齐。

作用是什么?
提高CPU 的内存访问速度。有些平台每次读都是从偶地址开始,如果一个 int 型(假设为 32 位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出这 32 位,而如果存放在奇地址开始的地方,就需要 2 个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该 32 位数据。显然,这在读取效率上下降很多。

如何关闭? #pragma pack(1)

行读取和列读取的效率

与二重循环内外大小无关,尽量保证内循环操作连续数据即可。

CPU读取内存某地址处的值,并不是每次都去内存中取出来,有时候会从cache里读取。当初次访问数组的时候,会把连续一块(chunk)内存地址上的值都读到cache里(比如,64字节),后续CPU接受到一个内存地址要读取数据时,先看cache里有没有,没有的话再去内存上取。

数据结构

二叉树

概念题

二叉排序和堆排序的区别?

在二叉排序树中,某结点的右孩子结点的值一定大于该结点的左孩子结点的值;在堆中却不一定,堆只是限定了某结点的值大于(或小于)其左右孩子结点的值,但没有限定左右孩子结点之间的大小关系。

二叉排序树是为了实现动态查找而设计的数据结构,它是面向查找操作的,在二叉排序树中查找一个结点的平均时间复杂度是O(log n);

堆是为了实现排序而设计的一种数据结构,它不是面向查找操作的,因而在堆中查找一个结点需要进行遍历,其平均时间复杂度是O(n)。

二叉树的节点数计算?

二叉树的节点数: 遍历, 前中后层之

满二叉数的节点数: 2 k − 1 2^k-1 2k1 (1 << depth) - 1

(网易一面)等比数列的推导? 错位相减

完全二叉树节点数:

  • d l e f t = = d r i g h t d_{left} == d_{right} dleft==dright 左子树为满二叉树 结果为 ( 1 < < d l e f t ) − 1 + g e t N ( r i g h t ) + 1 (1<< d_{left} )-1+getN(right)+1 (1<<dleft)1+getN(right)+1
  • d l e f t > d r i g h t d_{left}>d_{right} dleft>dright 右子树为满二叉树 结果为 ( 1 < < d r i g h t ) − 1 + g e t N ( l e f t ) + 1 (1<< d_{right})-1+getN(left)+1 (1<<dright)1+getN(left)+1

时间复杂度 O ( l o g 2 N ) O(log^2N) O(log2N)

前中后序遍历迭代版

leetcode相关练习:前序 中序 后序

stack<TreeNode*> s; auto p = root;
// 前序
while(s.size() || p)
    if(p) s.push(p), visit(p), p=p->left;
    else  p=s.top(), s.pop() , p=p->right;
// 中序 (和前序区别是visit位置不同)
while(s.size() || p)
    if(p) s.push(p), p=p->left;
    else  p=s.top(), s.pop(), visit(p), p=p->right;
// 后序 (根右左,暂存在r栈中,O(n)空间)
while(s.size() || p)
    if(p) s.push(p), r.push(p), p=p->right;
    else  p=s.top(), s.pop()  , p=p->left;
while(r.size()) visit(r.top()), r.pop(); //从r栈中取出

// 后序 O(1)空间
TreeNode* r = nullptr; // r记录前一步visit的节点
while(s.size() || p) 
    if(p) s.push(p), p=p->left;
    else{
    //用 r来检查 p的 right是否访问过
        p = s.top();
        if(!p->right || p->right == r)
        //访问后要把 p置为空,因为要去取栈中的点
            visit(p), s.pop(), r=p, p=nullptr;
        else p = p->right;
    } 

之字形遍历

如果不是要求返回res而是输出,level数组可以进一步优化:队列中同时出现两个nullptr时说明树已遍历完。

// 层序基础上,加上nullptr分隔
queue<TreeNode*> q; q.push(root); q.push(nullptr); 
while(q.size()){
   
    auto t = q.front(); q.pop();
    if(!t){
   
        q.push(nullptr); // 因此补上下一层的结尾
        if(level.empty()) break; // 树已遍历完
        // 偶数层时翻转
        if(res.size()&1) reverse(level.begin(), level.end()); 
        res.push_back(level), level.clear();
    }
    else{
    //不为空时,访问+扩展
        level.push_back(t->val);
        if(t->left) q.push(t->left);
        if(t->right) q.push(t->right);
    }
}

二叉搜索树的Kth

找第k大就右根左,第k小就左根右

void dfs(TreeNode* root, int& k){
   
    if(!root) return;
    dfs(root->right, k); // 先右
    if(!--k) {
    res = root->val; return; }
    dfs(root->left, k); // 再左
}

二叉搜索树转双向链表

leetcode习题

Node* pre = nullptr, *head = nullptr;
Node* treeToDoublyList(Node* root) {
   
    if(!root) return nullptr;
    dfs(root); 
    // 本题要求返回一个循环的双向链表
    pre->right = head, head->left = pre;
    return head;
}
void dfs(Node* root){
   
    if(!root) return;
    dfs(root->left); // 先转换左子树,完成后pre是左子树最右边的节点
    root->left = pre; // 左子树已完成,把left直接指向pre
    pre? pre->right=root : head=root; // pre为空时,说明是最左边的节点
    pre = root; // 更新pre
    dfs(root->right);
}

最低公共祖先LCA

TreeNode* LCA(TreeNode* root, TreeNode* p, TreeNode* q) {
   
    if(!root) return nullptr;
    if(root==p || root==q) return root;

    auto left = LCA(root->left, p, q);
    auto right = LCA(root->right, p, q);
    
    if(!left) return right; // 左子树中没找到,那就是右子树中找到的那个节点
    if(!right) return left; // 同理

    // 如果左右子树中都找到了【包含p或q的节点】
    return root;
}
//二叉搜索树拥有一些性质
TreeNode* LCA_BST(TreeNode* root, TreeNode* p, TreeNode* q){
   
    if(!root) return nullptr;
    if(root->val>p->val && root->val>q->val)
        return LCA_BST(root->left, p, q);
    if(root->val<p->val && root->val<q->val)
        return LCA_BST(root->right, p, q);
    return root;
}

含根二叉树求后继

给定一个节点,求后继

if(p->right){
    // 存在右子树,则右子树最左下节点就是后继节点
    p = p->right;
    while(p->left) p=p->left;
    return p;
}
// 否则沿着father往上找到第一个满足 p == p->father->left 的点
while(p->father && p == p->father->left)
    p = p->father;

从输入构建二叉树

层序输入,空节点用任意非整数字符表示

// 待补充

哈希表

找0~n-1重复数(原地哈希)

while(i<num.size()){
   
    x = num[i];
    if(num[i] != i)
        if(num[x] == x) return x; //出现重复
        else swap(num[x], nums[i]); // 换到正确的位置
    else i++;
}

最长连续序列(不要求原位置连续)

int longestConsecutive(vector<int>& nums) {
   
   unordered_set<int> m; //记录出现的数
   for(int x:nums) m.insert(x);
   int res = 0; 
   for(int x:nums){
   
       //只有x-1没出现过,包含x的区间才没被计算过
       if(!m.count(x-1)){
   
           int cur = x, len = 1;
           //cur+1存在,就可以往前走
           while(m.count(cur+1)) cur++, len++;
           res = max(len, res);    
       }            
   } return res;
}

链表

排序链表删重(不是去重)

// q向前探索,p记录非重位置
ListNode* solve(ListNode* head){
   
    ListNode dummy = ListNode(-1);
    dummy.next = head; 
    ListNode* p = &dummy;
    while(p->next){
    // 用虚拟头节点,可以处理head为空的情况
    	auto q = p->next;
        while(q&&p->next->val==q->val) // 一直探索重复数
            q = q->next;
        // q只移动一次,说明下一个数非重
        if(q == p->next->next) p = p->next;
        else{
    // 仅仅跳过重复元素会内存泄漏
            auto t1 = p->next; p->next = q;
            while(t1!=q){
    // 一直删到q
                auto t2 = t1->next; delete t1; t1 = t2;
            }
        }
    } return dummy.next;
}

翻转

迭代版

ListNode* reverseList(ListNode* head) {
   
    ListNode* cur = head, *pre = nullptr;
    while(cur) swap(cur->next, pre), swap(pre, cur);
    return pre;
}

递归版

// 递归版本1,加一个辅助函数比较好理解
ListNode* reverseList(ListNode* head) {
   
    return reverse(nullptr, head);
}
ListNode* reverse(ListNode* p, ListNode* c){
   
    if(!c) return p; //终点,返回pre
    swap(c->next, p); //把pre换给next
    return reverse(c, p);
}

// 递归版本2,不加辅助函数稍微难理解一点,要画图
ListNode* reverseList(ListNode* head) {
   
    if(!head || !head->next) return head;
    // 当前节点之后的部分翻转完成,并返回了最后一个节点
    auto res = reverseList(head->next);
    // 然后再处理当前节点,就是cur.next.next -> cur
    head->next->next = head;
    head->next = nullptr;
    return res;
}
部分翻转

翻转链表的第m到n个节点

ListNode* reverseBetween(ListNode* head, int m, int n) {
   
    auto dummy = new ListNode(-1); dummy->next = head;
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值