C++开发常见面试题整理(含代码题)

3 篇文章 0 订阅
1 篇文章 0 订阅

文章目录

C++

1. C和C++区别

设计思想上:

​ C++是面向对象的语言,而 C 是面向过程的结构化编程语言

语法上:

​ C++具有重载、继承和多态三种特性

​ C++相比 C,增加多许多类型安全的功能,比如强制类型转换

​ C++支持范式编程,比如模板类、函数模板等

2. static关键字

  1. 全局静态变量

    在全局变量前加上关键字 static,全局变量就定义成一个全局静态变量。 静态存储区,在整个程序运行期间一直存在。 初始化:未经初始化的全局静态变量会被自动初始化为 0(自动对象的值是任意的,除非他 被显式初始化)。 作用域:全局静态变量在声明他的文件之外是不可见的,准确地说是从定义之处开始,到文 件结尾。

  2. 局部静态变量

    在局部变量之前加上关键字 static,局部变量就成为一个局部静态变量。 内存中的位置:静态存储区。 初始化:未经初始化的全局静态变量会被自动初始化为 0(自动对象的值是任意的,除非他 被显式初始化)。 作用域:作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域结束。但 是当局部静态变量离开作用域后,并没有销毁,而是仍然驻留在内存当中,只不过我们不能再对 它进行访问,直到该函数再次被调用,并且值不变。

  3. 静态函数

    在函数返回类型前加 static,函数就定义为静态函数。函数的定义和声明在默认情况下都 是 extern 的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用。 函数的实现使用 static 修饰,那么这个函数只可在本 cpp 内使用,不会同其他 cpp 中的同 名函数引起冲突。 warning:不要再头文件中声明 static 的全局函数,不要在 cpp 内声明非 static 的全局函 数,如果你要在多个 cpp 中复用该函数,就把它的声明提到头文件里去,否则 cpp 内部声明需加 上 static 修饰。

  4. 类的静态成员

    类的静态成员可以实现多个对象之间的数据共享,并且使用静态数据成员还不会破坏隐 藏的原则,即保证了安全性。因此,静态成员是类的所有对象中共享的成员,而不是某个对象的 成员。对多个对象来说,静态数据成员只存储一处,供所有对象共用。

  5. 类的静态函数

    静态成员函数和静态数据成员一样,它们都属于类的静态成员,它们都不是对象成员。因此, 对静态成员的引用不需要用对象名

3. cast转换

1、const_cast

​ 用于将 const 变量转为非 const

2、static_cast

​ 用于各种隐式转换,比如非 const 转 const,void*转指针等, static_cast 能用于多态向上 转化,如果向下转能成功但是不安全,结果未知

3、dynamic_cast

​ 用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指 针或引用。向下转化时,如果是非法的对于指针返回 NULL,对于引用抛异常。

​ 向上转换:指的是子类向基类的转换

​ 向下转换:指的是基类向子类的转换

​ 它通过判断在执行到该语句的时候变量的运行时类型和要转换的类型是否相同来判断是否 能够进行向下转换。

4. reinterpret_cast

​ 几乎什么都可以转,比如将 int 转指针,可能会出问题,尽量少用

4. volatile关键字

​ 要求每次直接从内存中读值而不是使用保存在寄存器里的备份.(易变的、不稳定的)

5. explicit

​ explicit关键字的作用是禁止将构造函数作为转换函数。 它的作用是表明该构造函数是显示的, 而非隐式的, 跟它相对应的另一个关键字是implicit, 意思是隐藏的,类构造函数默认情况下即声明为implicit(隐式).

例如,如果一个类的构造函数中只包含一个整数参数,在构造函数前使用explicit关键字可以阻止像"CPerson person=10;”这样的语句执行。

6. C/C++指针和引用

1.指针有自己的一块空间,而引用只是一个别名;

2.使用 sizeof 看一个指针的大小是 4,而引用则是被引用对象的大小;

3.指针可以被初始化为 NULL,而引用必须被初始化且必须是一个已有对象 的引用;

4.作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引 用的修改都会 改变引用所指向的对象;

5.可以有 const 指针,但是没有 const 引用;

6.指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能 被改变;

7.指针可以有多级指针(**p),而引用至于一级;

8.指针和引用使用++运算符的意义不一样;

9.如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露。

7. C++智能指针

auto_ptr, shared_ptr, weak_ptr, unique_ptr 其中后三个是 c++11 支持,并且第一个已经被 11 弃用

为什么要使用智能指针:

智能指针的作用是管理一个指针,因为存在以下这种情况:申请的空间在函数结束时忘记释 放,造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类, 当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。所以智能指针的作 用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间

1.auto_ptr(c++98 的方案,cpp11 已经抛弃)

采用所有权模式

auto_ptr< string> p1 (new string ("hello world\n”)); 
auto_ptr p2; 
p2 = p1; //auto_ptr 

不会报错. 此时不会报错,p2 剥夺了 p1 的所有权,但是当程序运行时访问 p1 将会报错。所以 auto_ptr 的缺点是:存在潜在的内存崩溃问题

2.unique_ptr(替换 auto_ptr)

unique_ptr 实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向 该对象。它对于避免资源泄露特 别有用.

unique_ptr< string> p1 (new string ("hello world\n”)); 
unique_ptr p2; 
p2 = p1; //  报错
// 临时右值不会报错
 unique_ptr<string> p3;
p3=unique_ptr<string>(new string("hello world\n")); // 不会报错

3.shared_ptr

shared_ptr 实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源 会在“最后一个引用被销毁”时候释放。从名字 share 就可以看出了资源可以被多个指针共享, 它使用计数机制来表明资源被几个指针共享。可以通过成员函数 use_count()来查看资源的所有 者个数。除了可以通过 new 来构造,还可以通过传入 unique_ptr,weak_ptr 来构造。 当我们调用 release()时,当前指针会释放资源所有权,计数减一。当计数等于 0 时,资源会被 释放.

shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性(auto_ptr 是独占的), 在使 用引用计数的机制上提供了可以共享所有权的智能指针.

计数原理:

shared_ptr的实现是这样的: shared_ptr模板类有一个shared_count类型的成员来处理引用计数的问题。shared_count也是一个模板类,它的内部有一个指向Sp_counted_base_impl类型的指针M_pi。所有引用同一个对象的shared_ptr都共用一个_M_pi指针。

当两个对象相互使用一个 shared_ptr 成员变量指向对方,会造成循环引用,使引用计数失 效,从而导致内存泄漏。

4.weak_ptr

weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的 shared_ptr. weak_ptr 只是提供了对管理对象的一个 访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析 构不会引起引用记数的增加或减少。weak_ptr 是用来解决 shared_ptr 相互引用时的死锁问题, 如果说两个 shared_ptr 相互引用,那么这两个指针的引用计数永远不可能下降为 0,资源永远不 会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和 shared_ptr 之间可以相互转 化,shared_ptr 可以直接赋值给它,它可以通过调用 lock 函数来获得 shared_ptr.

8. 指针和数组

指针(数组):保存数据的地址(保存数据)、间接访问数据(直接):首先获得指针的内容,然后将其作为地址,从地址中提取数据(直接访问数据)、动态数据结构(固定数据数目和数据类型),Malloc分配和释放(隐式分配和释放)

9. 指针和引用

引用:引用就是某一变量的一个别名,对引用的操作与对变量直接操作完全一样。引用 的声明方法:类型标识符 &引用名=目标变量名;引用引入了对象的一个同义词。定义引用的表 示方法与定义指针相似,只是用&代替了*。

指针:指针利用地址,它的值直接指向存在电脑存储器中另一个地方的值。由于通过地址能找到所 需的变量单元,可以说,地址指向该变量单元。因此,将地址形象化的称为“指针”。意思是通 过它能找到以它为地址的内存单元。

区别:

指针有自己的一块空间,而引用只是一个别名; 
使用 sizeof 看一个指针的大小是 4,而引用则是被引用对象的大小; 
指针可以被初始化为 NULL,而引用必须被初始化且必须是一个已有对象的引用; 
作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引用的修改都会 改变引用所指向的对象; 
可以有 const 指针,但是没有 const 引用; 
指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能 被改变; 
指针可以有多级指针(**p),而引用至于一级; 
指针和引用使用++运算符的意义不一样; 
如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露。

10. 函数指针和指针函数

函数指针:本质上是一个指针,它指向的是一个函数的地址。

作用:调用函数和做函数的参数,比如回调函数

void(*p2)(int a,int b); //函数指针是专用的。格式要求很强 返回值,参数类型,个数都必须相同。

指针函数:本质上是一个函数,他的返回值是一个指针

11. 析构&构造函数

构造函数:每个类都分别定义了它的对象被初始化的方式,类通过一个或几个特殊的成员函数]来控制其对象的初始化过程,这些函数叫做构造函数。构造函数的任务是初始化对象的数据成员,构造函数最重要的作用是创建对象本身。

析构函数:析构函数释放对象使用的资源,并销毁对象非static数据成员。由于析构函数没有参数,所以它不能被重载。

一般将可能会被继承的父类的的析构函数设置为虚函数,可以保证当我们 new 一个子类,然后使用 基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。

C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的 内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此 C++默认的 析构函数不是虚函数,而是只有当需要当作父类时,设置为虚函数。

顺序:创建子类实例时,先调用父类的构造函数,再调用子类的构造函数。当要释放子类的对象时,先调用子类的析构函数,再调用父类的构造函数来销毁对象。

12. 重载和覆盖

重载:两个函数名相同,但是参数列表不同(个数,类型),返回值类型没有要求,在同一作用 域中

覆盖:子类继承了父类,父类中的函数是虚函数,在子类中重新定义了这个虚函数,这种情况是 重写

13. 虚函数和多态

  • 多态的实现主要分为静态多态和动态多态,静态多态主要是重载,在编译的时候就已经确定;动 态多态是用虚函数机制实现的,在运行期间动态绑定。例如:一个父类类型的指针指向一个 子类对象时候,使用父类的指针去调用子类中重写了的父类中的虚函数的时候,会调用子类重写 过后的函数,在父类中声明为加了 virtual 关键字的函数,在子类中重写时候不需要加 virtual 也是虚函数。
  • 虚函数的实现:在有虚函数的类中,类的最开始部分是一个虚函数表的指针,这个指针指向一个 虚函数表,表中放了虚函数的地址,实际的虚函数在代码段(.text)中。当子类继承了父类的时 候也会继承其虚函数表,当子类重写父类中虚函数时候,会将其继承到的虚函数表中的地址替换 为重新写的函数地址。使用了虚函数,会增加访问内存开销,降低效率。

14. new&malloc

new 分配内存按照数据类型进行分配,malloc 分配内存按照指定的大小分配; 
new 返回的是指定对象的指针,而 malloc 返回的是 void*,因此 malloc 的返回值一般都 需要进行类型转化。 
new 不仅分配一段内存,而且会调用构造函数,malloc 不会。 
new 分配的内存要用 delete 销毁,malloc 要用 free 来销毁;delete 销毁的时候会调用 对象的析构函数,而 free 则不会。 
new 是一个操作符可以重载,malloc 是一个库函数。
malloc 分配的内存不够的时候,可以用 realloc 扩容。扩容的原理?new 没用这样操作。
new 如果分配失败了会抛出 bad_malloc 的异常,而 malloc 失败了会返回 NULL。 
申请数组时: new[]一次分配所有内存,多次调用构造函数,搭配使用 delete[],delete[] 多次调用析构函数,销毁数组中的每个对象。而 malloc 则只能 sizeof(int) * n。

15. 初始化列表

当初始化的成员都是进本数据类型(int,float,char等)两者的效率一样。
当初始化成员包含类对象时,初始话列表的效率要高一些(拷贝构造函数的调用)  

必须使用初始化列表的情况

需要初始化的数据成员是对象的情况(这里包含了继承情况下,通过显示调用父类的构造函数对父类数据成员进行初始化);
需要初始化const修饰的类成员或初始化引用成员数据;
子类初始化父类的私有成员;
引用数据成员

16. C++11

关键字及新语法auto nullptr

STL 容器 std::array std::forward_list std::unordered_map std::unordered_set

多线程 std::thread st::atomic std::condition_variable

智能指针内存管理 std::shared_ptr std::weak_ptr

std::function、std::bind 封装可执行对象lamda 表达式

左值右值

17. vector、array、数组

数组是不安全的,array和vector是比较安全的(有效的避免越界等问题)
array对象和数组存储在相同的内存区域(栈)中,vector对象存储在自由存储区(堆)
array可以将一个对象赋值给另一个array对象,但是数组不行
vector属于变长的容器,即可以根据数据的插入和删除重新构造容器容量;但是array和数组属于定长容器
vector和array提供了更好的数据访问机制,即可以使用front()和back()以及at()(at()可以避免a[-1]访问越界的问题)访问方式,使得访问更加安全。而数组只能通过下标访问,在写程序中很容易出现越界的错误
vector和array提供了更好的遍历机制,即有正向迭代器和反向迭代器
vector和array提供了size()和Empty(),而数组只能通过sizeof()/strlen()以及遍历计数来获取大小和是否为空
vector和array提供了两个容器对象的内容交换,即swap()的机制,而数组对于交换只能通过遍历的方式逐个交换元素
array提供了初始化所有成员的方法fill()
由于vector的动态内存变化的机制,在插入和删除时,需要考虑迭代的是否有效问题
vector和array在声明变量后,在声明周期完成后,会自动地释放其所占用的内存。对于数组如果用new[ ]/malloc申请的空间,必须用对应的delete[ ]和free来释放内存

18. std::function bind

std::function是一个函数包装模板,可以包装下列这几种可调用元素类型:函数、函数指针、类成员函数指针或任意类型的函数对象

19. lambda 表达式:

定义一个匿名函数,并且可以捕获一定范围内的变量

[捕获列表] (函数参数) mutable 或 exception 声明 -> 返回值类型 {函数体}

Lambda 表达式与普通函数最大的区别就是其可以通过捕获列表访问一些上下文中的数据。 其形式如下:

[ ] 表示不捕获任何变量
[=] 表示按值传递的方法捕获父作用域的所有变量
[&] 表示按引用传递的方法捕获父作用域的所有变量
[=, &a] 表示按值传递的方法捕获父作用域的所有变量,但按引用传递的方法捕获变量a
[&, a] 表示按引用传递的方法捕获父作用域的所有变量,但按值传递的方法捕获变量a
//计算两个值的和
auto func = [](int a, int b) -> int{return a+b;};
//当返回值的类型是确定时,可以忽略返回值
auto func = [](int a, int b){return a + b;};
//调用
int sum = func(1, 3);

20. const

左定值,右定向

21 inline和define

1.define
	定义在预编译时处理的宏,只是简单的字符串替换,没有类型检查
2.inline
  1.用来定义一个内联函数,引用inline的主要原因是用它替换C语言中表示式形式的宏定义;
  2.在编译阶段完成;
  3.内联函数会做类型安全检查;
  4.内联函数是嵌入式代码,调用内联函数时,不是跳转到内联函数执行,而是把内联函数的代码直接写到调用位置。
  5.inline函数仅用于短小的函数(逻辑不复杂,且一般小于10行的函数),可以提升一定的效率,和宏相比,inline函数更加安全可靠。
  6.缺点:增加了内存空间的消耗

容器和算法

1. map &set 区别和实现

map 和 set 都是 C++的关联容器,其底层实现都是红黑树(RB-Tree)。

区别:

map 中的元素是 key-value(关键字—值)对:关键字起到索引的作用,值则表示与索 引相关联的数据;set 与之相对就是关键字的简单集合,set 中每个元素只包含一个关键字。 
set 的迭代器是 const 的,不允许修改元素的值;map 允许修改 value,但不允许修改 key。其原因是因为 map 和 set 是根据关键字排序来保证其有序性的,如果允许修改 key 的话, 那么首先需要删除该键,然后调节平衡,再插入修改后的键值,调节平衡,如此一来,严重破坏 了 map 和 set 的结构,导致 iterator 失效,不知道应该指向改变前的位置,还是指向改变后的 位置。所以 STL 中将 set 的迭代器设置成 const,不允许修改迭代器的值;而 map 的迭代器则不 允许修改 key 值,允许修改 value 值。 
map 支持下标操作,set 不支持下标操作。map 可以用 key 做下标,map 的下标运算符[ ] 将关键码作为下标去执行查找,如果关键码不存在,则插入一个具有该关键码和 mapped_type 类型默认值的元素至 map 中,因此下标运算符[ ]在 map 应用中需要慎用,const_map 不能用, 只希望确定某一个关键值是否存在而不希望插入元素时也不应该使用,mapped_type 类型没有默 认值也不应该使用。如果 find 能解决需要,尽可能用 find

2. STL 迭代器删除元素

对于序列容器 vector,deque 来说,使用 erase(itertor)后,后边的每个元素的迭代器都会失效,但是后边每个元素都会往前移动一个位 置,但是 erase 会返回下一个有效的迭代器;
对于关联容器 map set 来说,使用了 erase(iterator)后,当前元素的迭代器失效,但是其结构是红黑树,删除当前元素的,不会影 响到下一个元素的迭代器,所以在调用 erase 之前,记录下一个元素的迭代器即可。
对于 list 来说,它使用了不连续分配的内存,并且它的 erase 方法也会返回下一个有效的 iterator,因 此上面两种正确的方法都可以使用。

3.vector 和 list

Vector::
	连续存储的容器,动态数组,在堆上分配空间 
	底层实现:数组
	两倍容量增长: vector 增加(插入)新元素时,如果未超过当时的容量,则还有剩余空间,那么直接添加 到最后(插入指定位置),然后调整迭代器。 如果没有剩余空间了,则会重新配置原有元素个数的两倍空间,然后将原空间元素通过复 制的方式初始化新空间,再向新空间增加元素,最后析构并释放原空间,之前的迭代器会失效。
List::
	动态链表,在堆上分配空间,每插入一个元数都会分配空间,每删除一个元素都会释放空 间
	底层:双向链表
vector 拥有一段连续的内存空间,因此支持随机访问,如果需要高效的随即访问,而不在 乎插入和删除的效率,使用 vector。
 list 拥有一段不连续的内存空间,如果需要高效的插入和删除,而不关心随机访问,则应 使用 list

4. 迭代器、指针

迭代器 Iterator,用于提供一种方法顺序访问一个聚合 对象中各个元素, 而又不需暴露该对象的内部表示。或者这样说可能更容易理解:Iterator 模 式是运用于聚合对象的一种模式,通过运用该模式,使得我们可以在不知道对象内部表示的情况 下,按照一定顺序(由 iterator 提供的方法)访问聚合对象中的各个元素。 
由于 Iterator 模式的以上特性:与聚合对象耦合,在一定程度上限制了它的广泛运用,一 般仅用于底层聚合支持类,如 STL 的 list、vector、stack 等容器类ostream_iterator 等扩 展 iterator。

迭代器和指针的区别

迭代器不是指针,是类模板,表现的像指针。他只是模拟了指针的一些功能,通过重载了指针的一些操作符,->、*、++、--等。迭代器封装了指针,是一个“可遍历 STL容器内全部或部分元素”的对象, 本质是封装了原生指针,是指针概念的一种提升,提供了比指针更高级的行为,相当于一种智能指针,他可以根据不同类型的数据结构来实现不同的++,--等操作。迭代器返回的是对象引用而不是对象的值,所以 cout 只能输出迭代器使用*取值后的值而不能直接输出其自身

5. resize 和 reserve

resize():改变当前容器内含有元素的数量size(),例如: vectorv; v.resize(len);v 的 size 变为 len,如果原来 v 的 size 小于 len,那么容器新增(len-size)个元素,元素的值为 默认为 0.当v.push_back(3);之后,则是 3 是放在了 v 的末尾,即下标为 len,此时容器是 size 为 len+1;

reserve():改变当前容器的最大容量(capacity),它不会生成元素,只是确定这个容器允许放入多少对象,如果 reserve(len)的值大于当前的 capacity(),那么会重新分配一块能存 len 个 对象的空间,然后把之前 v.size()个对象通过 copy construtor 复制过来,销毁之前的内存

6.放入vector类的要求

自定义的类必须有默认构造函数。因为vector会调用默认构造函数来初始化元素的对象。

数据成员中没有const和reference。因为要初始化。

OS

1. 左右值引用

概念

  • 左值:能对表达式取地址、或具名对象/变量。一般指表达式结束后依然存在的持久对象。
  • 右值:不能对表达式取地址,或匿名对象。一般指表达式结束就不再存在的临时对象。

区别

  • 左值可以寻址,而右值不可以。
  • 左值可以被赋值,右值不可以被赋值,可以用来给左值赋值
  • 左值可变,右值不可变(仅对基础类型适用,用户自定义类型右值引用可以通过成员函数 改变)

右值引用是 C++11 中引入的新特性 。它的主要目的有两个方面:

  • 消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。
  • 能够更简洁明确地定义泛型函数。

2. C++源文件to可执行文件

对于 C++源文件,从文本到可执行文件一般需要四个过程:

预处理阶段:对源代码文件中文件包含关系(头文件)、预编译语句(宏定义)进行分析和 替换,生成预编译文件。 
编译阶段:将经过预处理后的预编译文件转换成特定汇编代码,生成汇编文件 
汇编阶段:将编译阶段生成的汇编文件转化成机器码,生成可重定位目标文件 
链接阶段:将多个目标文件及所需要的库连接成最终的可执行目标文件

3.头文件""和<>

编译器预处理阶段查找头文件的路径不一样

“” 查找路径:

当前头文件目录 ⟶ \longrightarrow 编译器设置的头文件路径 ⟶ \longrightarrow 系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH 指定的头文件路径

<> 查找路径:编译器设置的头文件路径系统变量 ⟶ \longrightarrow CPLUS_INCLUDE_PATH/C_INCLUDE_PATH 指定的头文件路径

4. malloc

malloc() 并不是系统调用,而是 C 库里的函数,用于动态分配内存

malloc 申请内存的时候,会有两种方式向操作系统申请堆内存。

  • 方式一:通过 brk() 系统调用从堆分配内存
  • 方式二:通过 mmap() 系统调用在文件映射区域分配内存;

方式一实现的方式很简单,就是通过 brk() 函数将「堆顶」指针向高地址移动,获得新的内存空间

方式二通过 mmap() 系统调用中「私有匿名映射」的方式,在文件映射区分配一块内存,也就是从文件映射区“偷”了一块内存。

  • 如果用户分配的内存小于 128 KB,则通过 brk() 申请内存;
  • 如果用户分配的内存大于 128 KB,则通过 mmap() 申请内存;

malloc 通过 brk() 方式申请的内存,free释放内存的时候,并不会把内存归还给操作系统,而是缓存在 malloc 的内存池中,待下次使用;

malloc 通过 mmap() 方式申请的内存,free释放内存的时候,会把内存归还给操作系统,内存得到真正的释放。

5.程序内存管理

img

6.内存泄漏、溢出

内存泄漏(memory leak)是指由于疏忽或错误造成了程序未能释放掉不再使用的内存的情况。 内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对 该段内存的控制,因而造成了内存的浪费

内存泄漏的分类:

堆内存泄漏 (Heap leak)。对内存指的是程序运行中根据需要分配通过 malloc,realloc new 等从堆中分配的一块内存,再是完成后必须通过调用对应的 free 或者 delete 删掉。如果 程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生 Heap Leak. 
系统资源泄露(Resource Leak)。主要指程序使用系统分配的资源比如 Bitmap,handle ,SOCKET 等没有使用相应的函数释放掉,导致系统资源的浪费,严重可导致系统 效能降低,系统运行不稳定。
没有将基类的析构函数定义为虚函数。当基类指针指向子类对象时,如果基类的析构函 数不是 virtual,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内 存泄露。

避免:

内存泄漏通常是由于调用了 malloc/new 等内存申请的操作,但是缺少了对应的 free/delete。 为了判断内存是否泄露,我们一方面可以使用 linux 环境下的内存泄漏检查工具 valgrind,mtrace,另一 方面我们在写代码时可以添加内存申请和释放的统计功能,统计当前申请和释放的内存是否一致, 以此来判断内存是否泄露

内存溢出指程序申请内存时,没有足够的内存供申请者使用。内存溢出就是你要的内存空间超过了系统实 际分配给你的空间,此时系统相当于没法满足你的需求,就会报内存溢出的错误

7. 进程与线程

进程是对运行时程序的封装,是系统进行资源调度和分配的的基本单位,实现了操作系统的 并发; 线程是进程的子任务,是 CPU 调度和分派的基本单位,用于保证程序的实时性,实现进程内 部的并发;
线程是操作系统可识别的最小执行和调度单位。每个线程都独自占用一个虚拟处理器: 独自的寄存器组,指令计数器和处理器状态。每个线程完成不同的任务,但是共享同一地址空间 (也就是同样的动态内存,映射文件,目标代码等等),打开的文件队列和其他内核资源

区别:

一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。线程依赖于进程而存在。
进程在执行过程中拥有独立的内存单元,而多个线程共享进程的内存。(资源分配给进程,同一进程的所有线程共享该进程的所有资源。同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量。)
进程是资源分配的最小单位,线程是 CPU 调度的最小单位;
系统开销: 由于在创建或撤消进程时,系统都要为之分配或回收资源,如内存空间、I/o 设备等。因此,操作系统所付出的开销将显著地大于在创建或撤消线程时的开销。类似地,在进行进程切换时,涉及到整个当前进程 CPU 环境的保存以及新被调度运行的进程的 CPU 环境的设置。而线程切换只须保存和设置少量寄存器的内容,并不涉及存储器管理方面的操作。可见,进程切换的开销也远大于线程切换的开销。
通信:由于同一进程中的多个线程具有相同的地址空间,致使它们之间的同步和通信的实现,也变得比较容易。进程间通信 IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。在有的系统中,线程的切换、同步和通信都无须操作系统内核的干预
进程编程调试简单可靠性高,但是创建销毁开销大;线程正相反,开销小,切换速度快,但是编程调试相对复杂。
进程间不会相互影响 ;线程一个线程挂掉将导致整个进程挂掉
进程适应于多核、多机分布;线程适用于多

多线程和多进程的不同

进程是资源分配的最小单位,而线程时 CPU 调度的最小单位。多线程之间共享同一个进程的地址空间,线程间通信简单,同步复杂,线程创建、销毁和切换简单,速度快,占用内存少,适用于多核分布式系统,但是线程间会相互影响,一个线程意外终止会导致同一个进程的其他线程也终止,程序可靠性弱。而多进程间拥有各自独立的运行地址空间,进程间不会相互影响,程序可靠性强,但是进程创建、销毁和切换复杂,速度慢,占用内存多,进程间通信复杂,但是同步简单,适用于多核、多机分布

8. 进程通信

1.管道

管道主要包括无名管道和命名管道:管道可用于具有亲缘关系的父子进程间的通信,有名管 道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信

普通管道 PIPE:

1) 它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端
2) 它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)
3) 它可以看成是一种特殊的文件,对于它的读写也可以使用普通的 read、write 等函数。但
是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。

命名管道 FIFO:

1)FIFO 可以在无关的进程之间交换数据 
2)FIFO 有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中

2.系统IPC

消息队列

//消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列 ID)来标记。 (消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等特点)具有写权限得进程可以按照一定得规则向消息队列中添加新信息;对消息队列有读权限得进程则可以从消息队列中读取信息
特点:
1) 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。
2) 消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。
3) 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。

信号量

信号量(semaphore)与已经介绍过的 IPC 结构不同,它是一个计数器,可以用来控制多个 进程对共享资源的访问。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
特点:
1) 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
2) 信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。
3) 每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。
4) 支持信号量组。

信号

信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

共享内存

它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据得更新。这种方式需要依靠某种同步操作,如互斥锁和信号量
特点:
1) 共享内存是最快的一种 IPC,因为进程是直接对内存进行存取
2) 因为多个进程可以同时操作,所以需要进行同步
3) 信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问

3.套接字SOCKET

socket 也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同主机之间的进程通信

9. 线程通信

临界区:通过多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问;
互斥量 Synchronized/Lock:采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问
信号量 Semphare:为控制具有有限数量的用户资源而设计的,它允许多个线程在同一时刻去访问同一个资源,但一般需要限制同一时刻访问此资源的最大线程数目。
事件(信号),Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作

10.虚拟内存

为了防止不同进程同一时刻在物理内存中运行而对物理内存的争夺和践踏,采用了虚拟内存。

虚拟内存技术使得不同进程在运行过程中,它所看到的是自己独自占有了当前系统的 4G 内 存。所有进程共享同一物理内存,每个进程只把自己目前需要的虚拟内存空间映射并存储到物理 内存上。 事实上,在每个进程创建加载时,内核只是为进程“创建”了虚拟内存的布局,具体 就是初始化进程控制表中内存相关的链表,实际上并不立即就把虚拟内存对应位置的程序数据和 代码(比如.text .data 段)拷贝到物理内存中,只是建立好虚拟内存和磁盘文件之间的映射就 好(叫做存储器映射),等到运行到对应的程序时,才会通过缺页异常,来拷贝数据。还有进程 运行过程中,要动态分配内存,比如 malloc 时,也只是分配了虚拟内存,即为这块虚拟内存对 应的页表项做相应设置,当进程真正访问到此数据时,才引发缺页异常

虚拟内存的好处:

扩大地址空间;
内存保护:每个进程运行在各自的虚拟内存地址空间,互相不能干扰对方。虚存还对特定的内存地址提供写保护,可以防止代码或数据被恶意篡改。
公平内存分配。采用了虚存之后,每个进程都相当于有同样大小的虚存空间。
当不同的进程使用同样的代码时,比如库文件中的代码,物理内存中可以只存储一份这样的代码,不同的进程只需要把自己的虚拟内存映射过去就可以了,节省内存
虚拟内存很适合在多道程序设计系统中使用,许多程序的片段同时保存在内存中。当一个程序等待它的一部分读入内存时,可以把 CPU 交给另一个进程使用。在内存中可以保留多个进程,系统并发度提高
在程序需要分配连续的内存空间的时候,只需要在虚拟内存空间分配连续空间,而不需要实际物理内存的连续空间,可以利用碎片

虚拟内存的代价

虚存的管理需要建立很多数据结构,这些数据结构要占用额外的内存
虚拟地址到物理地址的转换,增加了指令的执行时间。
页面的换入换出需要磁盘 I/O,这是很耗时的
如果一页中只有一部分数据,会浪费内存。

11.缺页中断

malloc()和 mmap()等内存分配函数,在分配时只是建立了进程虚拟地址空间,并没有分配 虚拟内存对应的物理内存。当进程访问这些没有建立映射关系的虚拟内存时,处理器自动触发一 个缺页异常。

缺页中断:在请求分页系统中,可以通过查询页表中的状态位来确定所要访问的页面是否存 在于内存中。每当所要访问的页面不在内存是,会产生一次缺页中断,此时操作系统会根据页表 中的外存地址在外存中找到所缺的一页,将其调入内存。

步骤

保护 CPU 现场 
分析中断原因
转入缺页中断处理程序进行处理
恢复 CPU 现场,继续执行

12. 并发和并行

并发(concurrency):指宏观上看起来两个程序在同时运行,比如说在单核 cpu 上的多任务。但是从微观上看两个程序的指令是交织着运行的,你的指令之间穿插着我的指令,我的指令之间穿插着你的,在单个周期内只运行了一个指令。这种并发并不能提高计算机的性能,只能提高效率

并行(parallelism):指严格物理意义上的同时运行,比如多核 cpu,两个程序分别运行在两个核上,两者之间互不影响,单个周期内每个程序都运行了自己的指令,也就是运行了两条指令。这样说来并行的确提高了计算机的效率。所以现在的 cpu 都是往多核方面发展

13. 死锁

死锁是指两个或两个以上进程在执行过程中,因争夺资源而造成的下相互等待的现象。死锁 发生的四个必要条件如下:

互斥条件:进程对所分配到的资源不允许其他进程访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源;
请求和保持条件:进程获得一定的资源后,又对其他资源发出请求,但是该资源可能被其他进程占有,此时请求阻塞,但该进程不会释放自己已经占有的资源
不可剥夺条件:进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用后自己释放
环路等待条件:进程发生死锁后,必然存在一个进程-资源之间的环形

解决死锁的方法即破坏上述四个条件之一,主要方法如下:

资源一次性分配,从而剥夺请求和保持条件
可剥夺资源:即当进程新的资源未得到满足时,释放已占有的资源,从而破坏不可剥夺的条件
资源有序分配法:系统给每类资源赋予一个序号,每个进程按编号递增的请求资源,释放则相反,从而破坏环路等待的条

14. 结构体对齐

原因

1)平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2)性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作多次内存访问;而对齐的内存访问仅需要一次访问。

15. 虚拟内存页面置换

FIFO,LRU,LFU,LRU-K

FIFO(先进先出淘汰算法)
思想:最近刚访问的,将来访问的可能性比较大。
实现:使用一个队列,新加入的页面放入队尾,每次淘汰队首的页面,即最先进入的数据,最先被淘汰。
弊端:无法体现页面冷热信息
LFU(最不经常访问淘汰算法)
思想:如果数据过去被访问多次,那么将来被访问的频率也更高。
实现:每个数据块一个引用计数,所有数据块按照引用计数排序,具有相同引用计数的数据块则按照时间排序。每次淘汰队尾数据块。
开销:排序开销。
LRU(最近最少使用替换算法)
思想:如果数据最近被访问过,那么将来被访问的几率也更高。
实现:使用一个栈,新页面或者命中的页面则将该页面移动到栈底,每次替换栈顶的缓存页面。
优点:LRU 算法对热点数据命中率是很高的。
缺点:缓存污染,突然大量偶发性的数据访问,会让内存中存放大量冷数据
LRU-K(LRU-2、LRU-3)
思想:最久未使用 K 次淘汰算法。
LRU-K 中的 K 代表最近使用的次数,因此 LRU 可以认为是 LRU-1。LRU-K 的主要目的是为了解决 LRU 算法“缓存污染”的问题,其核心思想是将“最近使用过 1 次”的判断标准扩展为“最近使用过 K 次”。
相比 LRU,LRU-K 需要多维护一个队列,用于记录所有缓存数据被访问的历史。只有当数据的访问次数达到 K 次的时候,才将数据放入缓存。当需要淘汰数据时,LRU-K 会淘汰第 K 次访问时间距当前时间最大的数据

16. 锁

互斥锁:mutex,用于保证在任何时刻,都只能有一个线程访问该对象。当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒
读写锁:rwlock,分为读锁和写锁。处于读操作时,可以允许多个线程同时获得读操作。但是同一时刻只能有一个线程可以获得写锁。其它获取写锁失败的线程都会进入睡眠状态,直到写锁释放时被唤醒。 注意:写锁会阻塞其它读写锁。当有一个线程获得写锁在写时,读锁也不能被其它线程获取;写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)。适用于读取数据的频率远远大于写数据的频率的场合。
自旋锁:spinlock,在任何时刻同样只能有一个线程访问对象。但是当获取锁操作失败时,不会进入睡眠,而是会在原地自旋,直到锁被释放。这样节省了线程从睡眠状态到被唤醒期间的消耗,在加锁时间短暂的环境下会极大的提高效率。但如果加锁时间过长,则会非常浪费 CPU资源
RCU:即 read-copy-update,在修改数据时,首先需要读取数据,然后生成一个副本,对副本进行修改。修改完成后,再将老数据 update 成新的数据。使用 RCU 时,读者几乎不需要同步开销,既不需要获得锁,也不使用原子指令,不会导致锁竞争,因此就不用考虑死锁问题了。而对于写者的同步开销较大,它需要复制被修改的数据,还必须使用锁机制同步并行其它写者的修改操作。在有大量读操作,少量写操作的情况下效率非常高

17.大端小端

大端:就是高字节排放在内存的低地址端,低字节排放在内存的高地址端。小端相反

#include<stdio.h>
int main()
{
	int ret = check_sys();
	if (ret == 1)	printf("small\n");
	else		printf("big\n");
	return 0;
}
int check_sys()
{
	int a = 1;
	char* p = (char*)&a;
	if (*p == 1)	return 1;
	else		return 0;
}

18.用户、内核态

用户态和内核态是操作系统的两种运行级别,两者最大的区别就是特权级不同。用户态拥有最低的特权级,内核态拥有较高的特权级。运行在用户态的程序不能直接访问操作系统内核数据结构和程序。内核态和用户态之间的转换方式主要包括:系统调用,异常和中断。
分两种形态的原因:为了安全性。在 cpu 的一些指令中,有的指令如果用错,将会导致整个系统崩溃。分了内核态和用户态后,当用户需要操作这些指令时候,内核为其提供了 API,可以通过系统调用陷入内核,让内核去执行这些操作

19.微内核 宏内核

宏内核:除了最基本的进程、线程管理、内存管理外,将文件系统,驱动,网络协议等等 都集成在内核里面,例如 linux 内核
优点:效率高。 
缺点:稳定性差,开发过程中的 bug 经常会导致整个系统挂掉
微内核:内核中只有最基本的调度、内存管理。驱动、文件系统等都是用户态的守护进程去实现的。
优点:稳定,驱动等的错误只会导致相应进程死掉,不会导致整个系统都崩溃
缺点:效率低。典型代表 QNX,QNX 的文件系统是跑在用户态的进程,称为 resmgr 的东西,是订阅发布机制,文件系统的错误只会导致这个守护进程挂掉。不过数据吞吐量就比较不乐观了

20.僵尸孤儿进程

正常进程

正常情况下,子进程是通过父进程创建的,子进程再创建新的进程。子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程到底什么时候结束。 当一个进程完成它的工作终止之后,它的父进程需要调用 wait()或者 waitpid()系统调用取得子进程的终止状态。

孤儿进程

一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被 init 进程(进程号为 1)所收养,并由 init 进程对它们完成状态收集工作。

僵尸进程

一个进程使用 fork 创建子进程,如果子进程退出,而父进程并没有调用 wait 或 waitpid 获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵尸进程。
危害:如果进程不调用 wait / waitpid 的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程
解决:
    外部消灭:通过 kill 发送 SIGTERM 或者 SIGKILL 信号消灭产生僵尸进程的进程,它产生的僵死进程就变成了孤儿进程,这些孤儿进程会被 init 进程接管,init 进程会 wait()这些孤儿进程,释放它们占用的系统进程表中的资源
    内部消灭:1.子进程退出时向父进程发送 SIGCHILD 信号,父进程处理 SIGCHILD 信号。在信号处理函数中调用 wait 进行处理僵尸进程。
    		 2. fork 两次,原理是将子进程成为孤儿进程,从而其的父进程变为 init 进程,通过 init 进程可以处理僵尸进程。

网络

1.三次握手、四次挥手

img

2.TCP可靠性

1. 序列号、确认应答、超时重传
数据到达接收方,接收方需要发出一个确认应答,表示已经收到该数据段,并且确认序号会 说明了它下一次需要接收的数据序列号。如果发送发迟迟未收到确认应答,那么可能是发送的数 据丢失,也可能是确认应答丢失,这时发送方在等待一定时间后会进行重传。这个时间一般是 2*RTT(报文段往返时间)+一个偏差值。
2. 窗口控制与高速重发控制/快速重传(重复确认应答)
TCP 会利用窗口控制来提高传输速度,意思是在一个窗口大小内,不用一定要等到应答才能发送下一段数据,窗口大小就是无需等待确认而可以继续发送数据的最大值。如果不使用窗口控制,每一个没收到确认应答的数据都要重发。
使用窗口控制,如果数据段 1001-2000 丢失,后面数据每次传输,确认应答都会不停地发送序号为 1001 的应答,表示我要接收 1001 开始的数据,发送端如果收到 3 次相同应答,就会立刻进行重发;但还有种情况有可能是数据都收到了,但是有的应答丢失了,这种情况不会进行重发,因为发送端知道,如果是数据段丢失,接收端不会放过它的,会疯狂向它提醒
3. 拥塞控制
	如果把窗口定的很大,发送端连续发送大量的数据,可能会造成网络的拥堵(大家都在用网,你在这狂发,吞吐量就那么大,当然会堵),甚至造成网络的瘫痪。所以 TCP 在为了防止这种情况而进行了拥塞控制。
慢启动:定义拥塞窗口,一开始将该窗口大小设为 1,之后每次收到确认应答(经过一个 rtt),将拥塞窗口大小*2。
拥塞避免:设置慢启动阈值,一般开始都设为 65536。拥塞避免是指当拥塞窗口大小达到这个阈值,拥塞窗口的值不再指数上升,而是加法增加(每次确认应答/每个 rtt,拥塞窗口大小+1),以此来避免拥塞
快速重传:在遇到 3 次重复确认应答(高速重发控制)时,代表收到了 3 个报文段,但是这之前的 1 个段丢失了,便对它进行立即重传。
快恢复:先将阈值设为当前窗口大小的一半,然后将拥塞窗口大小设为慢启动阈值+3 的大小。

3.HTTP、HTTPS

HTTP:

HTTP 协议是 Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于从万维网(WWW:World Wide Web)服务器传输超文本到本地浏览器的传送协议。
HTTP 是一个基于 TCP/IP 通信协议来传递数据(HTML 文件,图片文件,查询结果等)
HTTP 是一个属于应用层的面向对象的协议,由于其简捷、快速的方式,适用于分布式超媒体信息系统。它于 1990 年提出,经过几年的使用与发展,得到不断地完善和扩展。目前在 WWW中使用的是 HTTP/1.0 的第六版,HTTP/1.1 的规范化工作正在进行之中,而且 HTTP-NG(NextGeneration of HTTP)的建议已经提出
HTTP 协议工作于客户端-服务端架构为上。浏览器作为 HTTP 客户端通过 URL 向 HTTP 服务端即 WEB 服务器发送所有请求。Web 服务器根据接收到的请求后,向客户端发送响应信息。

HTTP头

GET   /dir1/dir2/hello.html   HTTP/1.1
Host:www.test.com
Connection:close
User-agent:Mozilla/5.0
Accept-language:zh-cn

1、请求行
	请求行有三个字段:方法、URL、HTTP版本
	(1)方法:可以取不同的值,包括GET、POST、HEAD、PUT和DELETE等。绝大部分HTTP请求报文使用GET方法。
	(2)URL:请求对象的标识。示例中请求对象标识就是:/dir1/dir2/hello.html
	(3)HTTP版本:略。示例中HTTP版本为1.1。
2、首部行
	首部行由多组键值对(首部字段名:首部字段值)组成。下面分析示例:
	Host:指明请求对象所在主机。示例中主机为www.test.com。
	Connection:浏览器告知服务器是否使用持续连接。示例中close代表不使用持续连接。
	User-agent:指明用户代理,即浏览器类型。示例中浏览器类型为Mozilla/5.0。
	Accept-language:指明用户希望得到请求对象的语言版本。示例中zh-cn代表中文版本。
3、实体体
	使用GET方法时,实体体为空;
	而使用POST方法时才使用实体体,举例说明:
	当用户提交表单时,HTTP使用POST方法,则实体体内包含的就是用户在表单的输入值。
	

HTTP版本区别

http/1.0:每个TCP连接只能发送一个请求,发送数据完毕,连接就关闭,如果还要请求其他资源,就必须再新建一个连接
http/1.1:引入了持久连接(persistent connection),即TCP连接默认不关闭,可以被多个请求复用
HTTP/2:头信息和数据体都是二进制,称为头信息帧和数据帧;复用TCP连接,在一个连接里,客户端和浏览器都可以同时发送多个请求或回应,且不用按顺序;允许服务器未经请求,主动向客户端发送资源,即服务器推送

区别:

HTTP 协议和 HTTPS 协议区别如下:
1. HTTP 协议是以明文的方式在网络中传输数据,而 HTTPS 协议传输的数据则是经过 TLS 加密后的,HTTPS 具有更高的安全性
2. HTTPS 在 TCP 三次握手阶段之后,还需要进行 SSL 的 handshake,协商加密使用的对称加密密钥
3. HTTPS 协议需要服务端申请证书,浏览器端安装对应的根证书
4. HTTP 协议端口是 80,HTTPS 协议端口是 443
HTTPS 优点:
1. HTTPS 传输数据过程中使用密钥进行加密,所以安全性更高
2. HTTPS 协议可以认证用户和服务器,确保数据发送到正确的用户和服务器
HTTPS 缺点:
1. HTTPS 握手阶段延时较高:由于在进行 HTTP 会话之前还需要进行 SSL 握手,因此 HTTPS 协议握手阶段延时增加
2. HTTPS 部署成本高:一方面 HTTPS 协议需要使用证书来验证自身的安全性,所以需要购买 CA证书;另一方面由于采用 HTTPS 协议需要进行加解密的计算,占用 CPU 资源较多,需要的服务器配置或数目高

4. HTTP 返回码

1xx:指示信息--表示请求已接收,继续处理。
2xx:成功--表示请求已被成功接收、理解、接受。
3xx:重定向--要完成请求必须进行更进一步的操作。
4xx:客户端错误--请求有语法错误或请求无法实现。
5xx:服务器端错误--服务器未能实现合法的请求。
常见状态代码、状态描述的详细说明如下。
200 OK:客户端请求成功
206 partial content 服务器已经正确处理部分 GET 请求,实现断点续传或同时分片下载,
该请求必须包含 Range 请求头来指示客户端期望得到的范围
300 multiple choices(可选重定向):被请求的资源有一系列可供选择的反馈信息,由浏
览器/用户自行选择其中一个。
301 moved permanently(永久重定向):该资源已被永久移动到新位置,将来任何对该
资源的访问都要使用本响应返回的若干个 URI 之一。
302 move temporarily(临时重定向):请求的资源现在临时从不同的 URI 中获得,
304:not modified :如果客户端发送一个待条件的 GET 请求并且该请求以经被允许,而文
档内容未被改变,则返回 304,该响应不包含包体(即可直接使用缓存)。
403 Forbidden:服务器收到请求,但是拒绝提供服务。
t Found:请求资源不存在,举个例子:输入了错误的 URL。

5.OSI 7层和TCP 4层

OSI 七层模型及其包含的协议如下:

物理层: 通过媒介传输比特,确定机械及电气规范,传输单位为 bit,主要包括的协议为:IEE802.3 CLOCK RJ45
数据链路层: 将比特组装成帧和点到点的传递,传输单位为帧,主要包括的协议为 MAC VLAN PPP
网络层:负责数据包从源到宿的传递和网际互连,传输单位为包,主要包括的协议为 IP ARP ICMP
传输层:提供端到端的可靠报文传递和错误恢复,传输单位为报文,主要包括的协议为 TCP UDP
会话层:建立、管理和终止会话,传输单位为 SPDU,主要包括的协议为 RPC NFS
表示层: 对数据进行翻译、加密和压缩,传输单位为 PPDU,主要包括的协议为 JPEG ASII
应用层: 允许访问 OSI 环境的手段,传输单位为 APDU,主要包括的协议为 FTP HTTP DNS

TCP/IP四层

网络接口层:MAC VLAN
网络层:IP ARP ICMP
传输层:TCP UDP
应用层:HTTP DNS SMT

6. URL步骤

浏览器要将 URL 解析为 IP 地址,解析域名就要用到 DNS 协议,首先主机会查询 DNS 的缓存,如果没有就给本地 DNS 发送查询请求。DNS 查询分为两种方式,一种是递归查询,一种是迭代查询。如果是迭代查询,本地的 DNS 服务器,向根域名服务器发送查询请求,根域名服务器告知该域名的一级域名服务器,然后本地服务器给该一级域名服务器发送查询请求,然后依次类推直到查询到该域名的 IP 地址。DNS 服务器是基于 UDP 的,因此会用到 UDP 协议。
得到 IP 地址后,浏览器就要与服务器建立一个 http 连接。因此要用到 http 协议,http 协议报文格式上面已经提到。http 生成一个 get 请求报文,将该报文传给 TCP 层处理,所以还会用到 TCP 协议。如果采用 https 还会使用 https 协议先对 http 数据进行加密。TCP 层如果有需要先将 HTTP 数据包分片,分片依据路径 MTU 和 MSS。TCP 的数据包然后会发送给 IP 层,用到 IP协议。IP 层通过路由选路,一跳一跳发送到目的地址。当然在一个网段内的寻址是通过以太网协议实现(也可以是其他物理层协议,比如 PPP,SLIP),以太网协议需要直到目的 IP 地址的物理地址,有需要 ARP 协议
1. DNS 协议,http 协议,https 协议属于应用层
	应用层是体系结构中的最高层。应用层确定进程之间通信的性质以满足用户的需要。这里的进程就是指正在运行的程序。应用层不仅要提供应用进程所需要的信息交换和远地操作,而且还要作为互相作用的应用进程的用户代理,来完成一些为进行语义上有意义的信息交换所必须的功能。应用层直接为用户的应用进程提供服务。
2. TCP/UDP 属于传输层
	传输层的任务就是负责主机中两个进程之间的通信。因特网的传输层可使用两种不同协议:即面向连接的传输控制协议 TCP,和无连接的用户数据报协议 UDP。面向连接的服务能够提供可靠的交付,但无连接服务则不保证提供可靠的交付,它只是“尽最大努力交付”。这两种服务方式都很有用,备有其优缺点。在分组交换网内的各个交换结点机都没有传输层。
3. IP 协议,ARP 协议属于网络层
	网络层负责为分组交换网上的不同主机提供通信。在发送数据时,网络层将运输层产生的报文段或用户数据报封装成分组或包进行传送。在 TCP/IP 体系中,分组也叫作 IP 数据报,或简称为数据报。网络层的另一个任务就是要选择合适的路由,使源主机运输层所传下来的分组能够交付到目的主机
4. 数据链路层
	当发送数据时,数据链路层的任务是将在网络层交下来的 IP 数据报组装成帧,在两个相邻结点间的链路上传送以帧为单位的数据。每一帧包括数据和必要的控制信息(如同步信息、地址信息、差错控制、以及流量控制信息等)。控制信息使接收端能够知道—个帧从哪个比特开始和到哪个比特结束。控制信息还使接收端能够检测到所收到的帧中有无差错。
5. 物理层
	物理层的任务就是透明地传送比特流。在物理层上所传数据的单位是比特。传递信息所利用的一些物理媒体,如双绞线、同轴电缆、光缆等,并不在物理层之内而是在物理层的下面。因此也有人把物理媒体当做第 0 层。

7. TCP、UDP

TCP 和 UDP 区别

1. 连接
	TCP 是面向连接的传输层协议,即传输数据之前必须先建立好连接。
	UDP 无连接。
2. 服务对象
	TCP 是点对点的两点间服务,即一条 TCP 连接只能有两个端点;
	UDP 支持一对一,一对多,多对一,多对多的交互通信。
3. 可靠性
	TCP 是可靠交付:无差错,不丢失,不重复,按序到达。
	UDP 是尽最大努力交付,不保证可靠交付。
4. 拥塞控制,流量控制
	TCP 有拥塞控制和流量控制保证数据传输的安全性。
	UDP 没有拥塞控制,网络拥塞不会影响源主机的发送效率。
5. 报文长度
	TCP 是动态报文长度,即 TCP 报文长度是根据接收方的窗口大小和当前网络拥塞情况决定的。
	UDP 面向报文,不合并,不拆分,保留上面传下来报文的边界。
6. 首部开销
	TCP 首部开销大,首部 20 个字节。
	UDP 首部开销小,8 字节。(源端口,目的端口,数据长度,校验和)

TCP\UDP报文:

TCP 和 UDP 适用场景

从特点上我们已经知道,TCP 是可靠的但传输速度慢,UDP 是不可靠的但传输速度快。因此在选用具体协议通信时,应该根据通信数据的要求而决定
若通信数据完整性需让位与通信实时性,则应该选用 TCP 协议(如文件传输、重要状态的更新等);反之,则使用 UDP 协议(如视频传输、实时通信等)。
QQ微信发送消息过程中既有TCP参与还有UDP 同时出现P2P
登陆成功之后,QQ都会有一个TCP连接来保持在线状态。
QQ客户端之间的消息传送也采用了UDP模式,因为国内的网络环境非常复杂,而且很多用户采用的方式是通过代理服务器共享一条线路上网的方式,在这些复杂的情况下,客户端之间能彼此建立起来TCP连接的概率较小,严重影响传送信息的效率。而UDP包能够穿透大部分的代理服务器,因此QQ选择了UDP作为客户之间的主要通信协议。
采用UDP协议,通过服务器中转方式。因此,现在的IP侦探在你仅仅跟对方发送聊天消息的时候是无法获取到IP的。大家都知道,UDP 协议是不可靠协议,它只管发送,不管对方是否收到的,但它的传输很高效。但是,作为聊天软件,怎么可以采用这样的不可靠方式来传输消息呢?于是,腾讯采用了上层协议来保证可靠传输:如果客户端使用UDP协议发出消息后,服务器收到该包,需要使用UDP协议发回一个应答包。如此来保证消息可以无遗漏传输。之所以会发生在客户端明明看到“消息发送失败”但对方又收到了这个消息的情况,就是因为客户端发出的消息服务器已经收到并转发成功,但客户端由于网络原因没有收到服务器的应答包引起的。
微信采用TCP

8. socket函数

TCP

img

send 函数用来向 TCP 连接的另一端发送数据。客户程序一般用 send 函数向服务器发送请求,而 服务器则通常用 send 函数来向客户程序发送应答,send 的作用是将要发送的数据拷贝到缓冲区, 协议负责传输。 
recv 函数用来从 TCP 连接的另一端接收数据,当应用程序调用 recv 函数时,recv 先等待 s 的发 送缓冲中的数据被协议传送完毕,然后从缓冲区中读取接收到的内容给应用层。 accept 函数用了接收一个连接,内核维护了半连接队列和一个已完成连接队列,当队列为空的 时候,accept 函数阻塞,不为空的时候 
accept 函数从上边取下来一个已完成连接,返回一个文 件描述符

UDP

img

code-网络&OS

1. 主子轮流输出

1.子线程循环 10 次,接着主线程循环 100 次,接着又回到子线程循环 10 次,接着再回到主线程又循环 100 次,如此循环50次

#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>
pthread_mutex_t mutex;
pthread_cond_t cond;
pthread_attr_t attr;
pthread_t tid;
int flag=0;
int m_times=3;
void* func(void* args)
{
    int k=0;
    while(1)
    {
        pthread_mutex_lock(&mutex);
        printf("%d ",2*k+1);
        flag=1;
        pthread_cond_signal(&cond);
        pthread_cond_wait(&cond,&mutex);
        pthread_mutex_unlock(&mutex);
        k++;
        if(k == m_times)    
            pthread_exit(NULL);
    }
}

int main()
{
    pthread_mutex_init(&mutex,NULL);
    pthread_attr_init(&attr);
    pthread_cond_init(&cond,NULL);
    pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
    pthread_create(&tid,&attr,func,NULL);

    int k=0;
    while(1)
    {
        while(flag!=1)
            pthread_cond_wait(&cond,&mutex);
        printf("%d ",2*k+2);
        flag=0;
        pthread_cond_signal(&cond);
        pthread_mutex_unlock(&mutex);
        k++;
        if(k == m_times)    
            exit(0);
    }
    exit(0);
}

2.三线程输出ABC

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
pthread_attr_t  attr;
pthread_mutex_t  mutex;
pthread_cond_t condA2B, condB2C, condC2A;
 
 
pthread_t tid_A, tid_B;
 
 
int flagAB, flagBC;
void *tfnA(void *arg) {
    int k = 0;
    while(1) {
        pthread_mutex_lock(&mutex);
        printf("A");
        flagAB = 1;
        pthread_cond_signal(&condA2B);
        pthread_cond_wait(&condC2A, &mutex);
        pthread_mutex_unlock(&mutex);
        k++;
        if(k == 10)
            pthread_exit(NULL);
    }
}
void *tfnB(void *arg) {
    int k = 0;
    while(1) {
        pthread_mutex_lock(&mutex);
        while(flagAB != 1) {
            pthread_cond_wait(&condA2B, &mutex);
        }
        flagAB = 0;
        printf("B");
        flagBC = 1;
        pthread_cond_signal(&condB2C);
        pthread_mutex_unlock(&mutex);
        k++;
        if(k == 10)
            pthread_exit(NULL);
    }
}
int main() {
    int k = 0;
    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&condA2B, NULL);
    pthread_cond_init(&condB2C, NULL);
    pthread_cond_init(&condC2A, NULL);
    pthread_attr_init( &attr);                      /*属性*/
    pthread_attr_setdetachstate( &attr, PTHREAD_CREATE_DETACHED);
    pthread_create(&tid_A, &attr, tfnA, NULL );
    pthread_create(&tid_B, &attr, tfnB, NULL );
    while(1) {
        pthread_mutex_lock(&mutex);
        while(flagBC != 1) {
            pthread_cond_wait(&condB2C, &mutex);
        }
        flagBC = 0;
        printf("C");
        pthread_cond_signal(&condC2A);
        pthread_mutex_unlock(&mutex);
        k++;
        if(k == 10) {
            exit(0);
        }
    }
    exit(0);
}
include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
//#include "unpipc.h"
#include <semaphore.h>
pthread_t  tidA, tidB, tidC;
sem_t semA, semB, semC;
 
 
void *funcA(void *arg);
void *funcB(void *arg);
void *funcC(void *arg);
int main( ) {
    sem_init(&semA, 0, 1);
    sem_init(&semB, 0, 0);
    sem_init(&semC, 0, 0);
    pthread_create(&tidA, NULL, funcA, (void *)&tidA );
    pthread_create(&tidB, NULL, funcB, (void *)&tidB );
    pthread_create(&tidC, NULL, funcC, (void *)&tidC );
    pthread_join(tidA, NULL);
    pthread_join(tidB, NULL);
    pthread_join(tidC, NULL);
    sem_destroy(&semA);
    sem_destroy(&semB);
    sem_destroy(&semC);
    exit(0);
}
void *funcA(void *arg) {
    int i;
    for(i = 0; i< 10; i++){
        sem_wait(&semA);
        printf("A");
        fflush(stdout);
        sem_post(&semB);
    }
    return NULL;
}
void *funcB(void *arg) {
    int i;
    for(i = 0; i< 10; i++){
        sem_wait(&semB);
        printf("B");
        fflush(stdout);
        sem_post(&semC);
    }
    return NULL;
}
void *funcC(void *arg) {
    int i;
    for(i = 0; i< 10; i++){
        sem_wait(&semC);
        printf("C");
        fflush(stdout);
        sem_post(&semA);
    }
    return NULL;
}

代码题

一、链表

1. 反转链表

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

// struct ListNode
// {
//     int val
//     ListNode *next;
// }
class Solution
{
public:
    ListNode *reverseList(ListNode *head)
    {
        if(head == nullptr) return head;
        ListNode *pre = nullptr,*curr = head;
        while(curr)
        {
            ListNode *temp = curr->next;
            curr->next = pre;
            pre = curr;
            curr = temp;
        }
        return pre;
    }
};
1. 1 反转链表 II

给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。

  • class Solution {
         public:
         ListNode* reverseBetween(ListNode* head, int left, int right) {
             if(left == right)
                 return head;
             ListNode* newHead = new ListNode(-1);
             newHead->next=head;
             ListNode *pre=newHead,*curr,*next;
             for(int i=0;i<left-1;i++)
                 pre=pre->next;
             curr=pre->next;
          for(int i=0;i<right-left;i++)
          {
              next=curr->next;
              curr->next=next->next;
              next->next=pre->next;
              pre->next=next;
          }
          return newHead->next;
         }
    };
    

2. K 个一组翻转链表

给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。

k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

class Solution {
public:
    // 翻转一个子链表,并且返回新的头与尾
    pair<ListNode*, ListNode*> myReverse(ListNode* head, ListNode* tail) {
        ListNode* prev = tail->next;
        ListNode* p = head;
        while (prev != tail) {
            ListNode* nex = p->next;
            p->next = prev;
            prev = p;
            p = nex;
        }
        return {tail, head};
    }
    ListNode* reverseKGroup(ListNode* head, int k) {
        ListNode* hair = new ListNode(0);
        hair->next = head;
        ListNode* pre = hair;
       while (head) {
           ListNode* tail = pre;
​            // 查看剩余部分长度是否大于等于 k
​            for (int i = 0; i < k; ++i) {
​                tail = tail->next;
​                if (!tail) {
​                    return hair->next;
​                }
​            }
​            ListNode* nex = tail->next;
​            // 这里是 C++17 的写法,也可以写成
​            // pair<ListNode*, ListNode*> result = myReverse(head, tail);
​            // head = result.first;
​            // tail = result.second;
​            tie(head, tail) = myReverse(head, tail);
​            // 把子链表重新接回原链表
​            pre->next = head;
​            tail->next = nex;
​            pre = tail;
​            head = tail->next;
​        }
​        return hair->next;
​    }
};

3. 合并两个有序链表

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

class Solution {
public:
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
        if(list1 == nullptr) return list2;
        if(list2 == nullptr) return list1;

        ListNode *p=list1,*q=list2;
        ListNode *rear,*head;
        if(p->val<q->val)
        {
            head = p;
            rear = p;
            p=p->next;
        }else
        {
            head = q;
            rear = q;
            q=q->next;
        }
        while(p!=nullptr && q!= nullptr)
        {
            if(p->val<q->val)
            {
                rear->next = p;
                p=p->next;
            }else
            {
                rear->next = q;
                q=q->next;
            }
            rear = rear->next;
        }
        if(p == nullptr) rear->next = q;
        else rear->next = p;
        return head;
    }
};

4. 环形链表

给你一个链表的头节点 head ,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。

注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。

如果链表中存在环 ,则返回 true 。 否则,返回 false 。

class Solution {
public:
    bool hasCycle(ListNode *head) {
        ListNode *ptr1=head,*ptr2=head;
        while(ptr1!=nullptr && ptr2!=nullptr)
        {
            ptr1 = ptr1->next;
            ptr2 = ptr2->next;
            if(ptr2!=nullptr) ptr2 = ptr2->next;else return false;
            if(ptr1 == ptr2)
                return true;
        }
        return false;
    }
};
Ⅱ 返回交点

给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null

class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        ListNode *ptr1=head,*ptr2=head;
        while(ptr1!=nullptr&&ptr2!=nullptr)
        {
            ptr1=ptr1->next;
            ptr2=ptr2->next;
            if(ptr2) ptr2=ptr2->next; else return nullptr;
            if(ptr1 == ptr2)
            {
                ListNode *ptr3=head;
                while(ptr3!=ptr1)
                {
                    ptr1=ptr1->next;
                    ptr3=ptr3->next;
                }
                return ptr3;
            }
        }
        return nullptr;
    }
};

5. 相交链表

给你两个单链表的头节点 headAheadB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null

img

class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        if(headA == nullptr || headB == nullptr) return nullptr;
        int lengthA(0),lengthB(0);
        ListNode *pA = headA,*pB = headB;
        while(pA){
            lengthA++;
            pA = pA->next;
        }
        while(pB){
            lengthB++;
            pB = pB->next;
        }
        pA = headA;pB = headB;
        if(lengthA>lengthB)
            for(int i=0;i<lengthA-lengthB;i++)  pA = pA->next;
        else
            for(int i=0;i<lengthB-lengthA;i++)  pB=pB->next;
        while(pA!=nullptr)
        {
            if(pA == pB) return pA;
            pA = pA->next;
            pB = pB->next;
        }
        return nullptr;
    }
};

6. 合并K个有序链表

给你一个链表数组,每个链表都已经按升序排列。

请你将所有链表合并到一个升序链表中,返回合并后的链表。

class Solution {
public:
    ListNode* mergeTwoLists(ListNode *a, ListNode *b) {
        if ((!a) || (!b)) return a ? a : b;
        ListNode head, *tail = &head, *aPtr = a, *bPtr = b;
        while (aPtr && bPtr) {
            if (aPtr->val < bPtr->val) {
                tail->next = aPtr; aPtr = aPtr->next;
            } else {
                tail->next = bPtr; bPtr = bPtr->next;
            }
            tail = tail->next;
        }
        tail->next = (aPtr ? aPtr : bPtr);
        return head.next;
    }
    ListNode* merge(vector <ListNode*> &lists, int l, int r) {
        if (l == r) return lists[l];
        if (l > r) return nullptr;
        int mid = (l + r) >> 1;
        return mergeTwoLists(merge(lists, l, mid), merge(lists, mid + 1, r));
    }
    
    ListNode* mergeKLists(vector<ListNode*>& lists) {
        return merge(lists, 0, lists.size() - 1);
    }
};

7. 重排链表

给定一个单链表 L 的头节点 head ,单链表 L 表示为:

L0 → L1 → … → Ln - 1 → Ln

请将其重新排列后变为:

L0 → Ln → L1 → Ln - 1 → L2 → Ln - 2 → …

不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。

class Solution {
public:
    void reorderList(ListNode* head) {
        if(head == nullptr||head->next == nullptr) return;
        vector<ListNode*> temp;
        ListNode* p=head->next;
        while(p!=nullptr){
            temp.push_back(p);
            p=p->next;
            temp.back()->next = nullptr;
        }
        p=head;
        for(int i=0;i<temp.size()/2;i++)
        {
            p->next=temp[temp.size()-i-1];p=p->next;
            p->next=temp[i];p=p->next;
        }
        if(temp.size()%2 == 1)
            p->next = temp[temp.size()/2];

    }
};

8. 删除链表的倒 N 结点

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        if (head->next == nullptr)
            return nullptr;
        ListNode* p=head;
        ListNode* q=head;
        for (int i = 0; i < n; i++)
             p = p->next;

        if (p == nullptr)
            return head->next;

        while (p->next != nullptr) {
            p = p->next;
            q = q->next;
        }
        q->next = q->next->next;
        return head;
    }
};

9. 排序链表(归并)

给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表

class Solution {
    ListNode* merge(ListNode *head1,ListNode *head2)
    {
        ListNode *newHead=new ListNode(-1);
        ListNode *p1=head1,*p2=head2,*rear=newHead;
        while(p1 && p2)
        {
            if(p1->val<p2->val)
            {
                rear->next=p1;
                p1=p1->next;
            }else
            {
                rear->next=p2;
                p2=p2->next;
            }
            rear=rear->next;
        }
        if(p1==nullptr) rear->next=p2;
        else rear->next=p1;
        return newHead->next;
    }
public:
    ListNode* sortList(ListNode* head) {
        if(!head || !head->next) return head;
        ListNode *slow=head,*fast=head;
        while(fast->next != nullptr && fast->next->next !=nullptr)
        {
            slow=slow->next;
            fast=fast->next->next;
        }
        fast=slow->next;
        slow->next=nullptr;
        return merge(sortList(head),sortList(fast));
    }
};

10. 排序链表去重

给定一个已排序的链表的头 head删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表

class Solution {
public:
    ListNode* deleteDuplicates(ListNode* head) {
        if (!head) {
            return head;
        }

        ListNode* cur = head;
        while (cur->next) {
            if (cur->val == cur->next->val) {
                cur->next = cur->next->next;
            }
            else {
                cur = cur->next;
            }
        }
        return head;
    }
};
10.1 去重 II

给定一个已排序的链表的头 head , 删除原始链表中所有重复数字的节点,只留下不同的数字 。返回 已排序的链表 。

class Solution {
public:
    ListNode* deleteDuplicates(ListNode* head) {
        if(!head) return head;
        ListNode *newHead = new ListNode(head->val-1);
        newHead->next=head;
        ListNode *curr=newHead;
        while(curr->next && curr->next->next)
        {
            if(curr->next->val == curr->next->next->val)
            {
                int x=curr->next->val;
                while(curr->next && curr->next->val == x)
                {
                    //ListNode* temp=curr->next;
                    curr->next=curr->next->next;
                    //delete temp;
                }
            }
            else
                curr=curr->next;
        }
        return newHead->next;
    }
};

11. 链表中倒数第k个节点

class Solution {
public:
    ListNode* getKthFromEnd(ListNode* head, int k) {
        ListNode *p=head,*q=head;
        for(int i=0;i<k;i++)
            p=p->next;
        while(p)
        {
            p=p->next;
            q=q->next;
        }
        return q;
    }
};

12.回文链表

给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false

class Solution {
public:
    bool isPalindrome(ListNode* head) {
        vector<int> nums;
        ListNode *ptr=head;
        while(ptr)
        {
            nums.push_back(ptr->val);
            ptr=ptr->next;
        }
        for(int i=0;i<nums.size()/2;i++)
            if(nums[i] != nums[nums.size()-1-i]) return false;
        return true;
    }
};

13 两数相加-链表

给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。

请你将两个数相加,并以相同形式返回一个表示和的链表。

你可以假设除了数字 0 之外,这两个数都不会以 0 开头。

class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) 
    {
        int add=0;
        ListNode *ptr1=l1,*ptr2=l2;
        ListNode *head=new ListNode(0),*rear=head;
        while(ptr1 || ptr2 || add)
        {
            int x1,x2;
            x1 = ptr1 == nullptr?0:ptr1->val;
            x2 = ptr2 == nullptr?0:ptr2->val;
            int temp=x1+x2+add;
            rear->next=new ListNode(temp%10);
            rear=rear->next;
            ptr1=ptr1 == nullptr?ptr1:ptr1->next;
            ptr2=ptr2 == nullptr?ptr2:ptr2->next;
            add=temp/10;
        }
        return head->next;
    }
};

二、二叉树

1. 二叉树的层序遍历

给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。

class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        vector <vector <int>> ret;
        if (!root) {
            return ret;
        }
        queue <TreeNode*> q;
        q.push(root);
        while (!q.empty()) {
            int currentLevelSize = q.size();
            ret.push_back(vector <int> ());
            for (int i = 1; i <= currentLevelSize; ++i) {
                auto node = q.front(); q.pop();
                ret.back().push_back(node->val);
                if (node->left) q.push(node->left);
                if (node->right) q.push(node->right);
            }
        }
        return ret;
    }
};

2. 二叉树的最近公共祖先

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

class Solution {
public:
    unordered_map<TreeNode*, TreeNode*> fa;
    unordered_map<TreeNode*, bool> path;
    void dfs(TreeNode* root)
    {
        if(root == nullptr) return;
        if(root->left) fa[root->left] = root;
        if(root->right) fa[root->right] = root;
        dfs(root->left);dfs(root->right);
    }
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        fa[root] = nullptr;
        dfs(root);
        TreeNode* temp = p;
        while(temp)
        {
            path[temp] = true;
            temp = fa[temp];
        }
        temp = q;
        while(temp)
        {
            if(path[temp]) return temp;
            temp = fa[temp];
        }
        return nullptr;
    }
};

3. 二叉树的锯齿形层序遍历

给你二叉树的根节点 root ,返回其节点值的 锯齿形层序遍历 。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。

img

输入:root = [3,9,20,null,null,15,7]
输出:[[3],[20,9],[15,7]]
class Solution {
public:
    vector<vector<int>> zigzagLevelOrder(TreeNode* root) {
        vector<vector<int>> ret;
        if(root == nullptr) return ret;
        queue<TreeNode*> q;
        q.push(root);
        // 是否从右到左
        int level = 0;
        while(!q.empty())
        {
            level++;
            vector<int> temp;
            int size = q.size();
            for(int i=0;i<size;i++)
            {
                TreeNode* t = q.front();
                q.pop();
                temp.push_back(t->val);
                if(t->left) q.push(t->left);
                if(t->right) q.push(t->right);
            }
            if(level % 2 == 1)
                ret.push_back(temp);
            else
            {
                reverse(temp.begin(),temp.end());
                ret.push_back(temp);
            }
        }
        return ret;
    }
};

4. 二叉树中的最大路径和

路径 被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。

路径和 是路径中各节点值的总和。

给你一个二叉树的根节点 root ,返回其 最大路径和 。

class Solution {
public:
    int ret=INT_MIN;
    int maxPathSum(TreeNode* root) {
        dfs(root);
        return ret;
    }
   int dfs(TreeNode *root){
        if(root == nullptr) return 0;
        int leftMax = max(0,dfs(root->left));
        int rightMax = max(0,dfs(root->right));
        ret=max(ret,leftMax+rightMax+root->val);
        return root->val+max(leftMax,rightMax);
   }
};

5. 二叉树的遍历

5.1 前
class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> res;
        if (root == nullptr) {
            return res;
        }
        stack<TreeNode*> stk;
        TreeNode* node = root;
        while (!stk.empty() || node != nullptr) {
            while (node != nullptr) {
                res.emplace_back(node->val);
                stk.emplace(node);
                node = node->left;
            }
            node = stk.top();
            stk.pop();
            node = node->right;
        }
        return res;
    }
};

5.2 中
class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> res;
        stack<TreeNode*> stk;
        while (root != nullptr || !stk.empty()) {
            while (root != nullptr) {
                stk.push(root);
                root = root->left;
            }
            root = stk.top();
            stk.pop();
            res.push_back(root->val);
            root = root->right;
        }
        return res;
    }
};
5.3 后
class Solution {
public:
    vector<int> postorderTraversal(TreeNode *root) {
        vector<int> res;
        if (root == nullptr) {
            return res;
        }
        stack<TreeNode *> stk;
        TreeNode *prev = nullptr;
        while (root != nullptr || !stk.empty()) {
            while (root != nullptr) {
                stk.emplace(root);
                root = root->left;
            }
            root = stk.top();
            stk.pop();
            if (root->right == nullptr || root->right == prev) {
                res.emplace_back(root->val);
                prev = root;
                root = nullptr;
            } else {
                stk.emplace(root);
                root = root->right;
            }
        }
        return res;
    }
};

6. 二叉树的右视图

给定一个二叉树的 根节点 root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。

class Solution {
public:
    vector<int> ret;
    void levelTravel(TreeNode *root)
    {
        queue<TreeNode*> qe;
        if(root==nullptr) return;
        qe.push(root);
        while(!qe.empty())
        {
            ret.push_back(qe.front()->val);
            int sizeQ = qe.size();
            for(int i=0;i<sizeQ;i++)
            {
                TreeNode* temp = qe.front();qe.pop();
                if(temp->right) qe.push(temp->right);
                if(temp->left) qe.push(temp->left);
            }
        } 
    }
    vector<int> rightSideView1(TreeNode* root) {
        levelTravel(root);
        return ret;
    }
};

7. 前序与中序构造二叉树

给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。

class Solution {
private:
    unordered_map<int,int> index;
public:
    TreeNode* myBuildTree(vector<int>& preorder,vector<int>& inorder,int preLeft,int preRight,int inLeft,int inRight)
    {
        if(preLeft>preRight) return nullptr;
        
        int rootValue=preorder[preLeft];
        TreeNode* root=new TreeNode(rootValue);
        int preRootIndex=preLeft;
        int inRootIndex=index[rootValue];
        int leftSize=inRootIndex-inLeft;
        root->left=myBuildTree(preorder,inorder,preLeft+1,preLeft+leftSize,inLeft,inRootIndex-1);
        root->right=myBuildTree(preorder,inorder,preLeft+leftSize+1,preRight,inRootIndex+1,inRight);
        return root;
    }
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
       for(int i=0;i<inorder.size();i++)
            index[inorder[i]]=i;
        return myBuildTree(preorder,inorder,0,inorder.size()-1,0,inorder.size()-1);
    }
};

8. 判断是否平衡二叉树

给定一个二叉树,判断它是否是高度平衡的二叉树。

本题中,一棵高度平衡二叉树定义为:

一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。

class Solution {
public:
    int height(TreeNode* root)
    {
        if(root == nullptr) return 0;
        else return max(height(root->left),height(root->right))+1;
    }
    bool isBalanced(TreeNode* root) {
        if(root == nullptr) return true;
        return abs(height(root->left)-height(root->right))<=1 && isBalanced(root->left) && isBalanced(root->right);
    }
};

9. 求根节点到叶节点数字之和

给你一个二叉树的根节点 root ,树中每个节点都存放有一个 0 到 9 之间的数字。
每条从根节点到叶节点的路径都代表一个数字:

例如,从根节点到叶节点的路径 1 -> 2 -> 3 表示数字 123 。
计算从根节点到叶节点生成的 所有数字之和 。

class Solution {
public:
    int sum=0;
    int dfs(TreeNode* root,int preSum)
    {
        if(root == nullptr) return 0;
        int sum=preSum*10+root->val;
        if(root->left == nullptr && root->right == nullptr) return sum;
        else return dfs(root->left,sum)+dfs(root->right,sum);
    }
    int sumNumbers(TreeNode* root) {
        return dfs(root,0);
    }
};

10. 二叉树的最大深度

class Solution {
public:
    int maxDepth(TreeNode* root) {
        return root?max(maxDepth(root->left),maxDepth(root->right))+1:0;
    }
};

11. 对称二叉树

给你一个二叉树的根节点 root , 检查它是否轴对称。

class Solution {
public:
    bool dfs(TreeNode* p,TreeNode* q)
    {
        if(p==nullptr && q==nullptr) return true;
        if(p==nullptr||q==nullptr) return false;
        return p->val == q->val && dfs(p->left,q->right) && dfs(q->left,p->right);
    }
    bool isSymmetric(TreeNode* root) {
        return dfs(root,root);
    }
};

12. 二叉树的直径

给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。

class Solution {
public:
    int ans;
    int dfs(TreeNode *root)
    {
        if(root == nullptr) return 0;
        int leftDepth=dfs(root->left);
        int rightDepth=dfs(root->right);
        ans=max(ans,rightDepth+leftDepth+1);
        return max(rightDepth,leftDepth)+1;
    }
    int diameterOfBinaryTree(TreeNode* root) 
    {
        ans=1;
        dfs(root);
        return ans-1;
    }
};

13. 路径总和(返回是否存在)

给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。如果存在,返回 true ;否则,返回 false 。

class Solution {
public:
    bool dfs(TreeNode* root,int target)
    {
        if(!root) return false;
        target -=root->val;
        if(!root->left && !root->right && target == 0) return true;
        return dfs(root->left,target) || dfs(root->right,target);
    }
    bool hasPathSum(TreeNode* root, int targetSum) {
        return dfs(root,targetSum);
    }
};
13.1 Ⅱ(返回所有满足要求的路径)

给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。

叶子节点 是指没有子节点的节点。

class Solution {
public:
    vector<vector<int>> ret;
    vector<int> path;
    void dfs(TreeNode* root,int target)
    {
        if(!root) return;
        path.push_back(root->val);
        target-=root->val;
        if(!root->left && !root->right && target==0)
            ret.push_back(path);
        dfs(root->left,target);
        dfs(root->right,target);
        path.pop_back();
    }
    vector<vector<int>> pathSum(TreeNode* root, int targetSum) {
       dfs(root,targetSum);
       return ret;
    }
};

14. 翻转二叉树

给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。

class Solution {
public:
    TreeNode* invertTree(TreeNode* root) {
        if(!root) return root;
        swap(root->left,root->right);
        invertTree(root->left);
        invertTree(root->right);
        return root;
    }
};

三、数组

1. 三数之和

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请

你返回所有和为 0 且不重复的三元组。

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        vector<vector<int>> res;
        if(nums.size()<3) return res;
        // 排序
        sort(nums.begin(),nums.end());
        // -4 -1 -1 0 1 2
        for(int i=0;i<nums.size();i++)
        {
            if(nums[i]>0) break;
            if(i>0 && nums[i] == nums[i-1]) continue;

            int left = i+1,right = nums.size()-1;
            while(left<right)
            {
                if(nums[i] + nums[left] + nums[right] == 0)
                {
                    res.push_back({nums[i],nums[left],nums[right]});
                    left++;right--;
                    while(left<nums.size() && nums[left] == nums[left-1]) left++;
                    while(right>0 && nums[right] == nums[right+1]) right--;
                }else if(nums[i] + nums[left] + nums[right] > 0)
                {
                    right--;
                }else
                {
                    left++;
                }
            }
        }
        return res;
    }
};

2.最大子数组和

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组 是数组中的一个连续部分。

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int result = INT_MIN;
        vector<int> dp(nums.size());
        dp[0] = nums[0];
        result = dp[0];
        for(int i=1;i<nums.size();i++)
        {
            dp[i] = max(dp[i-1]+nums[i],nums[i]);
            result = max(result,dp[i]);
        }
        return result;
    }
};

3. 搜索旋转排序数组

整数数组 nums 按升序排列,数组中的值 互不相同 。

在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], …, nums[n-1], nums[0], nums[1], …, nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。

给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。

class Solution {
public:
    int search(vector<int>& nums, int target) {
        int left = 0,right = nums.size()-1;
        if(nums[0] <= target)
        {
            // 左半段
            while(left<=right)
            {
                int mid = (left+right)/2;
                int rightValue = nums[right]>=nums[0]?nums[right]:INT_MAX;
                int midValue = nums[mid]>=nums[0]?nums[mid]:INT_MAX;
                if(midValue == target) return mid;
                else if(midValue < target) left = mid+1;
                else right = mid-1;
            }
        }else
        {
            // 右半段
            while(left<=right)
            {
                int mid = (left+right)/2;
                int leftValue = nums[left]<=nums[nums.size()-1]?nums[left]:INT_MIN;
                int midValue = nums[mid]<=nums[nums.size()-1]?nums[mid]:INT_MIN;
                if(midValue == target) return mid;
                else if(midValue<target) left = mid+1;
                else right = mid-1;
            }
        }
        return -1;
    }
};

4. 买卖股票的最佳时机

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        if(prices.size() < 2) return 0;
        int leftMin = prices[0];
        int ret=0;
        for(int i=1;i<prices.size();i++)
        {
            leftMin = min(prices[i],leftMin);
            ret = max(ret,prices[i]-leftMin);
        }
        return ret;
    }
};
Ⅱ 任意次数

给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。

在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。

返回 你能获得的 最大 利润 。

思路:

dp(i)(0)表示第 ii 天交易完后手里没有股票的最大利润,dp(i)(1)表示第 ii 天交易完后手里持有一支股票的最大利润

dp(i)(0)=max{dp(i-1)(0),dp(i-1)(1)+prices[i]}

dp(i)(1)=max{dp(i-1)(1),dp(i-1)(0)−prices[i]}

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int n = prices.size();
        int dp[n][2];
        dp[0][0] = 0, dp[0][1] = -prices[0];
        for (int i = 1; i < n; ++i) {
            dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
            dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
        }
        return dp[n - 1][0];
    }
};
Ⅲ 限制2次
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int ret=0;
        int leftMin=prices[0],rightMax=prices[prices.size()-1];
        // dp_1[i]: 第0-i天的最大利润  prices[i]-leftmin
        // dp_2[i]: 第i-n天的最大利润  rightMax-prices[i]
        vector<int> dp_1(prices.size(),0);   
        vector<int> dp_2(prices.size(),0);
        dp_1[0]=-prices[0];
        for(int i=1;i<prices.size();i++)
        {
            dp_1[i]=max(dp_1[i-1],prices[i]-leftMin);
            leftMin=min(leftMin,prices[i]);
        }
        for(int i=prices.size()-2;i>=0;i--)
        {
            dp_2[i]=max(dp_2[i+1],rightMax-prices[i]);
            rightMax=max(prices[i],rightMax);
        }
        for(int i=1;i<prices.size();i++){
            ret = max(dp_1[i-1]+dp_2[i], ret);
        }
        return max(dp_1[prices.size()-1], ret);
    }
};

5. 合并两个有序数组

给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。

请你 合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列。

class Solution {
public:
    void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
        vector<int> ret;
        if(m == 0) nums1 = nums2;
        if(n == 0) return;
        int p1(0),p2(0);
        while(true)
        {
            if(p1 == m || p2 == n) break;
            if(nums1[p1] < nums2[p2])
            {
                ret.push_back(nums1[p1]);
                p1++;
            }
            else
            {
                ret.push_back(nums2[p2]);
                p2++;
            }
        }
        if(p1 == m)
        {
            for(p2;p2<n;p2++) ret.push_back(nums2[p2]);
        }else
        {
            for(p1;p1<m;p1++) ret.push_back(nums1[p1]);
        }
        nums1 = ret;
    }
};

6. 螺旋矩阵

给你一个 mn 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。

class Solution {
public:
    vector<int> spiralOrder(vector<vector<int>>& matrix) {
        vector<int> ret;
        if(matrix.size() == 0) return ret;
        int t=0,b=matrix.size()-1,l=0,r=matrix[0].size()-1;
        while(true)
        {
            for(int i=l;i<=r;i++) ret.push_back(matrix[t][i]);
            t++;if(t>b) break;
            for(int i=t;i<=b;i++) ret.push_back(matrix[i][r]);
            r--;if(l>r) break;
            for(int i=r;i>=l;i--) ret.push_back(matrix[b][i]);
            b--;if(t>b) break;
            for(int i=b;i>=t;i--) ret.push_back(matrix[i][l]);
            l++;if(l>r) break;
        }
        return ret;
    }
};

7. 接雨水

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

class Solution {
public:
    int trap(vector<int>& height) {
        vector<int> leftMax(height.size(),0);
        vector<int> rightMax(height.size(),0);
        leftMax[0]=height[0];
        rightMax[height.size()-1] = height[height.size()-1];
        for(int i=1;i<height.size();i++)
            leftMax[i]=max(height[i],leftMax[i-1]);
        for(int i=height.size()-2;i>=0;i--)
            rightMax[i]=max(height[i],rightMax[i+1]);
        int ans(0);
        for(int i=0;i<height.size();i++)   
            ans+=(min(leftMax[i],rightMax[i])-height[i]);
        return ans;
    }
};

8. 寻找两个正序数组的中位数

class Solution {
public:
    int getKthElement(const vector<int>& nums1, const vector<int>& nums2, int k) {
        /* 主要思路:要找到第 k (k>1) 小的元素,那么就取 pivot1 = nums1[k/2-1] 和 pivot2 = nums2[k/2-1] 进行比较
         * 这里的 "/" 表示整除
         * nums1 中小于等于 pivot1 的元素有 nums1[0 .. k/2-2] 共计 k/2-1 个
         * nums2 中小于等于 pivot2 的元素有 nums2[0 .. k/2-2] 共计 k/2-1 个
         * 取 pivot = min(pivot1, pivot2),两个数组中小于等于 pivot 的元素共计不会超过 (k/2-1) + (k/2-1) <= k-2 个
         * 这样 pivot 本身最大也只能是第 k-1 小的元素
         * 如果 pivot = pivot1,那么 nums1[0 .. k/2-1] 都不可能是第 k 小的元素。把这些元素全部 "删除",剩下的作为新的 nums1 数组
         * 如果 pivot = pivot2,那么 nums2[0 .. k/2-1] 都不可能是第 k 小的元素。把这些元素全部 "删除",剩下的作为新的 nums2 数组
         * 由于我们 "删除" 了一些元素(这些元素都比第 k 小的元素要小),因此需要修改 k 的值,减去删除的数的个数
         */

        int m = nums1.size();
        int n = nums2.size();
        int index1 = 0, index2 = 0;

        while (true) {
            // 边界情况
            if (index1 == m) {
                return nums2[index2 + k - 1];
            }
            if (index2 == n) {
                return nums1[index1 + k - 1];
            }
            if (k == 1) {
                return min(nums1[index1], nums2[index2]);
            }

            // 正常情况
            int newIndex1 = min(index1 + k / 2 - 1, m - 1);
            int newIndex2 = min(index2 + k / 2 - 1, n - 1);
            int pivot1 = nums1[newIndex1];
            int pivot2 = nums2[newIndex2];
            if (pivot1 <= pivot2) {
                k -= newIndex1 - index1 + 1;
                index1 = newIndex1 + 1;
            }
            else {
                k -= newIndex2 - index2 + 1;
                index2 = newIndex2 + 1;
            }
        }
    }

    double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
        int totalLength = nums1.size() + nums2.size();
        if (totalLength % 2 == 1) {
            return getKthElement(nums1, nums2, (totalLength + 1) / 2);
        }
        else {
            return (getKthElement(nums1, nums2, totalLength / 2) + getKthElement(nums1, nums2, totalLength / 2 + 1)) / 2.0;
        }
    }
};

9. 合并区间

以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。

class Solution {
public:
    vector<vector<int>> merge(vector<vector<int>>& intervals) {
        vector<vector<int>> ret;
        sort(intervals.begin(),intervals.end());
        int L=intervals[0][0],R=intervals[0][1];
        for(int i=0;i<intervals.size();i++)
        {
            if(intervals[i][0]>R)  
            {
                ret.push_back({L,R});
                L=intervals[i][0];
                R=intervals[i][1];
                continue;
            }
            R=max(R,intervals[i][1]);
        }
        ret.push_back({L,R});
        return ret;
    }
};

10. 下一个排列

整数数组的一个 排列 就是将其所有成员以序列或线性顺序排列。

例如,arr = [1,2,3] ,以下这些都可以视作 arr 的排列:[1,2,3]、[1,3,2]、[3,1,2]、[2,3,1] 。
整数数组的 下一个排列 是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 下一个排列 就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。

例如,arr = [1,2,3] 的下一个排列是 [1,3,2] 。
类似地,arr = [2,3,1] 的下一个排列是 [3,1,2] 。
而 arr = [3,2,1] 的下一个排列是 [1,2,3] ,因为 [3,2,1] 不存在一个字典序更大的排列。

思路:

  • 首先从后向前查找第一个顺序对 (i,i+1)(i,i+1),满足 a[i] < a[i+1]a[i]<a[i+1]。这样「较小数」即为 a[i]a[i]。此时 [i+1,n)[i+1,n) 必然是下降序列。

  • 如果找到了顺序对,那么在区间 [i+1,n)[i+1,n) 中从后向前查找第一个元素 jj 满足 a[i] < a[j]a[i]<a[j]。这样「较大数」即为 a[j]a[j]。

  • 交换 a[i]a[i] 与 a[j]a[j],此时可以证明区间 [i+1,n)[i+1,n) 必为降序。我们可以直接使用双指针反转区间 [i+1,n)[i+1,n) 使其变为升序,而无需对该区间进行排序。

class Solution 
{
public:
    void nextPermutation(vector<int>& nums) 
    {
        if(nums.size() == 0 || nums.size() == 1) return;
        int i=nums.size()-2;
        while(i>=0 && nums[i]>=nums[i+1])
            i--;
        if(i==-1 && nums[0]>=nums[1]) 
        {
            reverse(nums.begin(),nums.end());
            return;
        }
        int j=nums.size()-1;
        while(j>=0&&nums[i]>=nums[j])
            j--;
        swap(nums[j],nums[i]);
        reverse(nums.begin()+i+1,nums.end());
        return ;
    }
};

11. 缺失的第一个正数

给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。

请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。

class Solution {
public:
    int firstMissingPositive(vector<int>& nums) {
        for(int i=0;i<nums.size();i++)
        {
            if(nums[i]<=0) nums[i]=nums.size()+1;
        }
        for(int i=0;i<nums.size();i++)
        {
            int num=abs(nums[i]);
            if(num<=nums.size()) nums[num-1]=-abs(nums[num-1]);
        }
        for(int i=0;i<nums.size();i++)
        {
            if(nums[i]>0) 
                return i+1;
        }
        return nums.size()+1;
    }
};

12. 求数组所有的子集

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

class Solution {
public:
    vector<vector<int>> ret;
    unordered_map<int,int> hashmap;
    void backTrace(vector<int>& nums,vector<int>& path,int index)
    {
        ret.push_back(path);        
        for(int i=index;i<nums.size();i++)
        {
            if(hashmap[nums[i]] == 1 ) continue;
            hashmap[nums[i]]=1;
            path.push_back(nums[i]);
            backTrace(nums,path,index+1);
            path.pop_back();
            hashmap[nums[i]]=0;
        }
        return;
    }
    vector<vector<int>> subsets(vector<int>& nums) {
        vector<vector<int>> res;
        res.push_back(vector<int>());
        for(int i=0;i<nums.size();i++)
        {
            vector<vector<int>> temp=res;
            for(int j=0;j<temp.size();j++)
            {
                temp[j].push_back(nums[i]);
                res.push_back(temp[j]);
            }
        }
        return res;
    }
};

13. 左上->右下路径最小路径

给定一个包含非负整数的 m x n网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

class Solution {
public:
    int minPathSum(vector<vector<int>>& grid) {
        vector<vector<int>>  dp(grid.size(),vector<int>(grid[0].size(),0));
        dp[0][0]=grid[0][0];

        for(int i=1;i<grid.size();i++)
            dp[i][0]=dp[i-1][0]+grid[i][0];

        for(int j=1;j<grid[0].size();j++)
            dp[0][j]=dp[0][j-1]+grid[0][j];
        for(int i=1;i<grid.size();i++)
        for(int j=1;j<grid[0].size();j++)
            dp[i][j]=min(dp[i][j-1],dp[i-1][j])+grid[i][j];
        return dp[grid.size()-1][grid[0].size()-1];
    }
};

14. 旋转图像

给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。

你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。

class Solution {
public:
    void rotate(vector<vector<int>>& matrix) {
        for(int i=0;i<(matrix.size()+1)/2;i++)
        {
            for(int j=0;j<matrix[0].size()/2;j++)
            {
                int temp=matrix[i][j];
                matrix[i][j]=matrix[matrix.size()-j-1][i];
                matrix[matrix.size()-j-1][i]=matrix[matrix.size()-i-1][matrix.size()-j-1];
                matrix[matrix.size()-i-1][matrix.size()-j-1]=matrix[j][matrix.size()-i-1];
                matrix[j][matrix.size()-i-1]=temp;
            }
        }
    }
};

15.组合总和(硬币)

给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。

candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。

class Solution {
public:
    vector<vector<int>> ret;
    void dfs(vector<int>& candidates,vector<int> path, int target,int begin)
    {
        if(target == 0)
        {
            ret.push_back(path);
            return ;
        }
        else if(target<0)
            return;
        for(int i=begin;i<candidates.size();i++)
        {
            path.push_back(candidates[i]);
            dfs(candidates,path,target-candidates[i],i);
            path.pop_back();
        }
        return;
    }
    
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) 
    {
        vector<int> path;
        dfs(candidates,path,target,0);
        return ret;
    }
};

16. 出现次数大于n/2的元素

给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。

class Solution {
public:
    int majorityElement(vector<int>& nums) {
        int res=nums[0];
        int count=1;
        for(int i=1;i<nums.size();i++)
        {
            if(nums[i] == res)
                count++;
            else
                count--;
            if(count<0)
            {
                res=nums[i];
                count=1;
            }
        }
        return res;
    }
};

17. 最长重复子数组

给两个整数数组 nums1nums2 ,返回 两个数组中 公共的 、长度最长的子数组的长度

class Solution {
public:
    int findLength(vector<int>& A, vector<int>& B) {
        vector<vector<int>> dp(A.size()+1,vector<int>(B.size()+1,0));
        int ans=-1;
        for(int i=A.size()-1;i>=0;i--)
        for(int j=B.size()-1;j>=0;j--)
        {
            dp[i][j]=A[i]==B[j]?dp[i+1][j+1]+1:0;
            ans=max(ans,dp[i][j]);
        }
        return ans;
    }
};

18. 在排序数组中查找元素的第一个和最后一个位置

给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 target,返回 [-1, -1]。

class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        vector<int> ret(2,-1);
        int left=0,right=nums.size()-1;
        while(left<=right)
        {
            int middle=(left+right)/2;
            if(nums[middle]==target)
            {
                ret[1]=middle;
                left=middle+1;
            }
            else if(nums[middle]>target)
            {
                right=middle-1;
            }
            else
                left=middle+1;
        }
        left=0;right=nums.size()-1;
        while(left<=right)
        {
            int middle=(left+right)/2;
            if(nums[middle]==target)
            {
                ret[0]=middle;
                right=middle-1;
            }
            else if(nums[middle]>target)
            {
                right=middle-1;
            }
            else
                left=middle+1;
        }
        return ret;
    }
};

19 . 寻找峰值

峰值元素是指其值严格大于左右相邻值的元素。

给你一个整数数组 nums,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回 任何一个峰值 所在位置即可。

你可以假设 nums[-1] = nums[n] = -∞ 。

class Solution {
public:
    int findPeakElement(vector<int>& nums) {
        int l = 0, r = nums.size() - 1;
        while(l < r){
            int m = (l+r) / 2;
            nums[m] < nums[m+1] ? l = m + 1 : r = m;
        }
        return l;
    }
};

20. 岛屿的最大面积

给你一个大小为 m x n 的二进制矩阵 grid 。

岛屿 是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在 水平或者竖直的四个方向上 相邻。你可以假设 grid 的四个边缘都被 0(代表水)包围着。

岛屿的面积是岛上值为 1 的单元格的数目。

计算并返回 grid 中最大的岛屿面积。如果没有岛屿,则返回面积为 0

class Solution {
public:
    int dfs(vector<vector<int>>&grid,int i,int j){
        if(grid[i][j]==0)return 0;
        grid[i][j]=0;//每次统计后置0
        int res=1;
        if(i-1>=0&&grid[i-1][j]==1)res+=dfs(grid,i-1,j);
        if(i+1<=grid.size()-1&&grid[i+1][j]==1)res+=dfs(grid,i+1,j);
        if(j-1>=0&&grid[i][j-1]==1)res+=dfs(grid,i,j-1);
        if(j+1<=grid[0].size()-1&&grid[i][j+1]==1)res+=dfs(grid,i,j+1);
        return res;

    }
    int maxAreaOfIsland(vector<vector<int>>& grid) {
        int Max=0;
        for(int i=0;i<grid.size();++i){
            for(int j=0;j<grid[0].size();++j){
                if(grid[i][j]==1){
                  Max=max(Max,dfs(grid,i,j));  
                }
            }
        }
        return Max;
    }
};

21. 最长连续序列

给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。

请你设计并实现时间复杂度为 O(n) 的算法解决此问题。

class Solution {
public:
    int longestConsecutive(vector<int>& nums) {
        unordered_set<int> num_set;
        for (const int& num : nums) {
            num_set.insert(num);
        }

        int longestStreak = 0;

        for (const int& num : num_set) {
            if (!num_set.count(num - 1)) {
                int currentNum = num;
                int currentStreak = 1;

                while (num_set.count(currentNum + 1)) {
                    currentNum += 1;
                    currentStreak += 1;
                }

                longestStreak = max(longestStreak, currentStreak);
            }
        }

        return longestStreak;           
    }
};

22. 不同路径

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

思路一:排列组合

因为机器到底右下角,向下几步,向右几步都是固定的,

比如,m=3, n=2,我们只要向下 1 步,向右 2 步就一定能到达终点。

所以有 C m + n − 2 m − 1 C_{m+n-2}^{m-1} Cm+n2m1种方案

思路二:动态规划

我们令 dp[i][j] 是到达 i, j 最多路径

动态方程:dp[i][j] = dp[i-1][j] + dp[i][j-1]

注意,对于第一行 dp[0][j],或者第一列 dp[i][0],由于都是在边界,所以只能为 1

class Solution {
public:
    int uniquePaths(int m, int n) {
        vector<vector<int>> f(m, vector<int>(n));
        for (int i = 0; i < m; ++i) {
            f[i][0] = 1;
        }
        for (int j = 0; j < n; ++j) {
            f[0][j] = 1;
        }
        for (int i = 1; i < m; ++i) {
            for (int j = 1; j < n; ++j) {
                f[i][j] = f[i - 1][j] + f[i][j - 1];
            }
        }
        return f[m - 1][n - 1];
    }
};

23. 寻找旋转排序数组中的最小值

已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到:
若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]
若旋转 7 次,则可以得到 [0,1,2,4,5,6,7]
注意,数组 [a[0], a[1], a[2], …, a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], …, a[n-2]] 。

class Solution {
public:
    int findMin(vector<int>& nums) {
        int low = 0;
        int high = nums.size() - 1;
        while (low < high) {
            int pivot = low + (high - low) / 2;
            if (nums[pivot] < nums[high]) {
                high = pivot;
            }
            else {
                low = pivot + 1;
            }
        }
        return nums[low];
    }
};

24. 字符串转换整数

请你来实现一个 myAtoi(string s) 函数,使其能将字符串转换成一个 32 位有符号整数(类似 C/C++ 中的 atoi 函数)。

class Solution {
public:
    int myAtoi(string s) 
    {
        int ret=0,isFu=1;
        bool flag=false;
        int i;
        for(i=0;i<s.size();i++)
        {
            // if(s[i]>='0' && s[i]<='9'&&isEnd==true) return 0;
            if(s[i] == '-' || s[i] == '+')
            {
                if(flag) return 0;
                isFu=s[i] == '-'?-1:1;
                flag=true;
            }
                
            else if(s[i] == '0' || s[i] == ' ') continue;
            else if(s[i]>='1' && s[i]<='9')
            {
                while(s[i] >= '0' && s[i]<= '9')
                {
                    if(ret>=INT_MAX/10) return isFu==1?isFu * (INT_MAX):isFu * (1L+INT_MAX);
                    ret=ret*10;
                    if(ret>=INT_MAX-(s[i]-'0')) return isFu==1?isFu * (INT_MAX):isFu * (1L+INT_MAX);
                    ret=ret+s[i]-'0';
                    i++;
                }
                break;
                // isEnd=true;
            }else
                return 0;
        }
        if(i==s.size() || i<s.size() && s[i] == '.' || s[0]>='1' && s[0]<='9')
            return isFu*ret;
        return 0;

    }
};

四、动态规划

1. 最大子数组和

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int result = INT_MIN;
        vector<int> dp(nums.size());

        dp[0] = nums[0];
        result = dp[0];
        for(int i=1;i<nums.size();i++)
        {
            dp[i] = max(dp[i-1]+nums[i],nums[i]);
            result = max(result,dp[i]);
        }
        return result;
    }
};

2. 最长回文子串

给你一个字符串 s,找到 s 中最长的回文子串。

class Solution {
public:
    pair<int, int> expandAroundCenter(const string& s, int left, int right) {
        while (left >= 0 && right < s.size() && s[left] == s[right]) {
            --left;
            ++right;
        }
        return {left + 1, right - 1};
    }

    string longestPalindrome(string s) {
        int start = 0, end = 0;
        for (int i = 0; i < s.size(); ++i) {
            auto [left1, right1] = expandAroundCenter(s, i, i);
            auto [left2, right2] = expandAroundCenter(s, i, i + 1);
            if (right1 - left1 > end - start) {
                start = left1;
                end = right1;
            }
            if (right2 - left2 > end - start) {
                start = left2;
                end = right2;
            }
        }
        return s.substr(start, end - start + 1);
    }
};

3. 最长递增子序列

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        int ret(0);
        vector<int> dp(nums.size(),1);
        for(int i=0;i<nums.size();i++)
        {
            for(int j=0;j<i;j++)
            {
                if(nums[j]<nums[i])
                {
                    dp[i]=max(dp[i],dp[j]+1);
                }
            }
            ret=max(ret,dp[i]);
        }
        return ret;
    }
};

4.斐波那契

class Solution {
public:
    int climbStairs(int n) {
        double sqrt5 = sqrt(5);
        double fibn = pow((1 + sqrt5) / 2, n + 1) - pow((1 - sqrt5) / 2, n + 1);
        return (int)round(fibn / sqrt5);
    }
};

5. 编辑距离

给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。

你可以对一个单词进行如下三种操作:

  • 插入一个字符

  • 删除一个字符

  • 替换一个字符

class Solution {
public:
    int minDistance(string word1, string word2) {
        int n=word1.length();
        int m=word2.length();
        if(n*m == 0) return n+m;
        // dp[i][j] 表示 A 的前 i 个字母和 B 的前 j 个字母之间的编辑距离。
        vector<vector<int>> dp(n+1,vector<int>(m+1));
        for(int i=0;i<n+1;i++)
            dp[i][0]=i;
        for(int j=0;j<m+1;j++)
            dp[0][j]=j;
        
        for(int i=1;i<n+1;i++)
        {
            for(int j=1;j<m+1;j++)
            {
                int left=dp[i-1][j]+1;
                int up=dp[i][j-1]+1;
                int left_up=dp[i-1][j-1];
                if(word1[i-1]!=word2[j-1]) left_up++;
                dp[i][j] = min(left_up,min(left,up));
            }
        }
        return dp[n][m];
    }
};

6.最长公共子序列

给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        vector<vector<int>> dp(text1.size()+1,vector<int>(text2.size()+1,0));

        for(int i=1;i<dp.size();i++)
        for(int j=1;j<dp[0].size();j++)
        {
            if(text1[i-1] == text2[j-1])
                dp[i][j] = dp[i-1][j-1]+1;
            else
                dp[i][j] = max(dp[i-1][j],dp[i][j-1]);
        }
        return dp[text1.size()][text2.size()];

    }
};

7. 零钱兑换-最少

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。

你可以认为每种硬币的数量是无限的。

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        int Max=amount+1;
        // 15  8
        // dp[15]=min(dp[15-2],dp[15-3],dp[15-4])+1
        // dp[15] dp[0]=0 dp[1]=MAX dp[2..7]=MAX dp[8]=dp[8-0]+1=1 dp[9...15]=dp[1...7]+1=MAX+1
        vector<int> dp(amount+1,Max);
        dp[0]=0;
        for(int i=1;i<=amount;i++)
        {
            for(int j=0;j<coins.size();j++)
            {
                if(coins[j]<=i)
                    dp[i]=min(dp[i],dp[i-coins[j]]+1);
            }
        }
        return dp[amount]>amount?-1:dp[amount];
    }
};
Ⅱ组合总数
class Solution {
public:
    int change(int amount, vector<int>& coins) {
        vector<int> dp(amount + 1);
        dp[0] = 1;
        for (int& coin : coins) {
            for (int i = coin; i <= amount; i++) {
                dp[i] += dp[i - coin];
            }
        }
        return dp[amount];
    }
};

8. 最长有效括号

给你一个只包含 '('')' 的字符串,找出最长有效(格式正确且连续)括号子串的长度。

class Solution {
public:
    int longestValidParentheses(string s) {
        int length=s.size();
        vector<int> dp(length,0);
        int ans=0;
        for(int i=1;i<length;i++)
        {
            if(s[i] == '(')
                dp[i]=0;
            else
            {
                if(s[i-1] == '(')
                {
                    if(i-2>=0)
                        dp[i]=dp[i-2]+2;
                    else
                        dp[i]=2;
                }
                else
                {
                    if(i-dp[i-1]>0 && s[i-dp[i-1]-1] == '(')
                    {
                        if(i-dp[i-1]-2>=0)
                            dp[i]=dp[i-dp[i-1]-2]+dp[i-1]+2;
                        else
                            dp[i]=dp[i-1]+2;
                    }
                }
            }
            ans=max(ans,dp[i]);
        }
        return ans;
    }
};

9. 最小路径和

给定一个包含非负整数的 *m* x *n* 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

**说明:**每次只能向下或者向右移动一步。

class Solution {
public:
    int minPathSum(vector<vector<int>>& grid) {
        vector<vector<int>>  dp(grid.size(),vector<int>(grid[0].size(),0));
        dp[0][0]=grid[0][0];

        for(int i=1;i<grid.size();i++)
            dp[i][0]=dp[i-1][0]+grid[i][0];

        for(int j=1;j<grid[0].size();j++)
            dp[0][j]=dp[0][j-1]+grid[0][j];
        for(int i=1;i<grid.size();i++)
        for(int j=1;j<grid[0].size();j++)
            dp[i][j]=min(dp[i][j-1],dp[i-1][j])+grid[i][j];
        return dp[grid.size()-1][grid[0].size()-1];
    }
};

10. 最长重复子数组

给两个整数数组 nums1nums2 ,返回 两个数组中 公共的 、长度最长的子数组的长度

class Solution {
public:
    int findLength(vector<int>& A, vector<int>& B) {
        vector<vector<int>> dp(A.size()+1,vector<int>(B.size()+1,0));
        int ans=-1;
        for(int i=A.size()-1;i>=0;i--)
        for(int j=B.size()-1;j>=0;j--)
        {
            dp[i][j]=A[i]==B[j]?dp[i+1][j+1]+1:0;
            ans=max(ans,dp[i][j]);
        }
        return ans;
    }
};

11. 最大正方形

在一个由 '0''1' 组成的二维矩阵内,找到只包含 '1' 的最大正方形,并返回其面积。

class Solution {
public:
    int maximalSquare(vector<vector<char>>& matrix) {
        int m=matrix.size(),n=matrix[0].size();
        vector<vector<int>> dp(m,vector<int>(n,0));
        int res=0;
        for(int i=0;i<m;i++)
        {
            dp[i][0]=matrix[i][0]-'0';
            res=max(dp[i][0],res);
        }
        for(int j=0;j<n;j++)
        {
            dp[0][j]=matrix[0][j]-'0';
            res=max(dp[0][j],res);
        }
        for(int i=1;i<m;i++)
        {
            for(int j=1;j<n;j++)
            {
                if(matrix[i][j] == '1')
                {
                    dp[i][j]=min(dp[i-1][j-1],min(dp[i-1][j],dp[i][j-1]))+1;
                }
                res=max(res,dp[i][j]);
            }
        }
        return res*res;
    }
};

12. 打家劫舍

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

$\textit{dp}[i] $表示前 i间房屋能偷窃到的最高总金额,那么就有如下的状态转移方程:

d p [ i ] = m a x ( d p [ i − 2 ] + n u m s [ i ] , d p [ i − 1 ] ) dp[i]=max(dp[i−2]+nums[i],dp[i−1]) dp[i]=max(dp[i2]+nums[i],dp[i1])

边界条件:

dp[0]=nums[0]

dp[1]=max(nums[0],nums[1])

class Solution {
public:
    int rob(vector<int>& nums) {
        if (nums.empty()) {
            return 0;
        }
        int size = nums.size();
        if (size == 1) {
            return nums[0];
        }
        vector<int> dp = vector<int>(size, 0);
        dp[0] = nums[0];
        dp[1] = max(nums[0], nums[1]);
        for (int i = 2; i < size; i++) {
            dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
        }
        return dp[size - 1];
    }
};
Ⅱ 房屋是圈

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。

两种情况:1. 在1~n-1范围内盗窃 2. 在2-2范围内盗窃 返回两种方案的最大值即可

class Solution {
public:
    int robRange(vector<int>& nums, int start, int end) {
        int first = nums[start], second = max(nums[start], nums[start + 1]);
        for (int i = start + 2; i <= end; i++) {
            int temp = second;
            second = max(first + nums[i], second);
            first = temp;
        }
        return second;
    }

    int rob(vector<int>& nums) {
        int length = nums.size();
        if (length == 1) {
            return nums[0];
        } else if (length == 2) {
            return max(nums[0], nums[1]);
        }
        return max(robRange(nums, 0, length - 2), robRange(nums, 1, length - 1));
    }
};

五、字符串

1. 无重复字符的最长子串

给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。

双指针

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        if(s.size() == 0) return 0;
        int pos1(0),pos2(0);
        int res(-1),nowLength(0);
        unordered_set<char> set;
        while(1)
        {
            if(set.find(s[pos2]) == set.end() && pos2 != s.size())
            {
                set.insert(s[pos2]);
                pos2++;
                nowLength++;
            }
            else
            {
                set.erase(s[pos1]);
                res = res<nowLength?nowLength:res;
                nowLength--;
                pos1++;
            }
            if(pos1 == s.size() ||  pos2 == s.size())
                return nowLength>res?nowLength:res;
        }
    }
};

2. 字符串相加

给定两个字符串形式的非负整数 num1 和num2 ,计算它们的和并同样以字符串形式返回。

你不能使用任何內建的用于处理大整数的库(比如 BigInteger), 也不能直接将输入的字符串转换为整数形式。

class Solution {
public:
    string addStrings(string num1, string num2) {
        int i=num1.length()-1,j=num2.length()-1,flag(0);
        string ans="";
        while(i>=0||j>=0||flag>0)
        {
            int x=i>=0?num1[i]-'0':0;
            int y=j>=0?num2[j]-'0':0;
            int t=x+y+flag;
            ans.push_back(t%10 + '0');
            flag=t/10;
            i--;j--;
        }
        reverse(ans.begin(),ans.end());
        return ans;
    }
};

3. 括号生成

数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。

class Solution {
public:
    vector<string> generateParenthesis(int n) {
        vector<string> res;
        backTrack(res,"",0,0,n);
        return res;
    }
 
    void backTrack(vector<string> &res,string str,int l,int r,int n)
    {
        if(l>n || r>n || r>l) return;
        if(l == n && r==n) res.push_back(str);
        backTrack(res,str+'(',l+1,r,n);
        backTrack(res,str+')',l,r+1,n);
    }
};

4. 复原 IP 地址

有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 ‘.’ 分隔。

例如:“0.1.2.201” 和 “192.168.1.1” 是 有效 IP 地址,但是 “0.011.255.245”、“192.168.1.312” 和 “192.168@1.1” 是 无效 IP 地址。
给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s 中插入 ‘.’ 来形成。你 不能 重新排序或删除 s 中的任何数字。你可以按 任何 顺序返回答案。

class Solution {
public:
    vector<string> ret;
    void backTrace(string &s,int cnt,int index,string& str)
    {
        if(cnt == 4 || index == s.size())
        {
            if(cnt == 4 && index == s.size())
                ret.push_back(str.substr(0,str.size()-1));
            return ;
        }
        for(int i=1;i<=3;i++)
        {
            if(index+i>s.size()) return;
            if(s[index]=='0'&&i!=1) return;
            if(i==3&&s.substr(index,i)>"255") return;
            str+=s.substr(index,i);
            str.push_back('.');
            backTrace(s,cnt+1,index+i,str);
            str=str.substr(0,str.size()-i-1);
        }
    }
    vector<string> restoreIpAddresses(string s) {
        string str="";
        backTrace(s,0,0,str);
        return ret;
    }
};

5. 反转字符串中的单词

给你一个字符串 s ,请你反转字符串中 单词 的顺序。

单词 是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的 单词 分隔开。

返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。

class Solution {
public:
    vector<string> split(string &s)
    {
        stringstream ss(s);
        string temp;
        vector<string> res;
        while(getline(ss,temp,' '))
        {
            if("" == temp) continue;
            res.push_back(temp);
        }
        return res;
    }
    string reverseWords(string s) 
    {
        vector<string> ret=split(s);
        reverse(ret.begin(),ret.end());
        string res="";
        for(int i=0;i<ret.size();i++)
        {
            res+=ret[i];
            res+=" ";
        }
        res=res.substr(0,res.size()-1);
        return res;
    }
};

6.最小覆盖子串

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 ""

双指针

class Solution {
public:
    bool check(unordered_map<char,int> &hashmapS,unordered_map<char,int> &hashmapT)
    {
        for(auto& kv:hashmapT)
        {
            if(hashmapS.find(kv.first)!=hashmapS.end() && hashmapS[kv.first] >=kv.second)
                continue;
            return false;
        }
        return true;
    }
    string minWindow(string s, string t) 
    {
        string res=s;
        bool flag=false;
        unordered_map<char,int> hashmapT,hashmapS;
        for(int i=0;i<t.size();i++)
        {
            if(hashmapT.find(t[i]) == hashmapT.end())
                hashmapT[t[i]]=1;
            else
                hashmapT[t[i]]++;
        }
        int ptr1=0,ptr2=0;
        while(ptr1<=ptr2)
        {
            if(check(hashmapS,hashmapT) == false)
            {
                if(hashmapS.find(s[ptr2]) == hashmapS.end()) hashmapS[s[ptr2]]=1;
                else hashmapS[s[ptr2]]++;
                if(ptr2<s.size() )ptr2++;
                continue;
            }else
            {
                if(res.size() < s.substr(ptr1,ptr2-ptr1+1).size())
                    res=s.substr(ptr1,ptr2-ptr1+1);
                hashmapS[s[ptr1]]--;
                ptr1++;
            }
        }
        if(flag) return res;
        return "";
    }
};

7. 比较版本号

给你两个版本号 version1version2 ,请你比较它们。

版本号由一个或多个修订号组成,各修订号由一个 ‘.’ 连接。每个修订号由 多位数字 组成,可能包含 前导零 。每个版本号至少包含一个字符。修订号从左到右编号,下标从 0 开始,最左边的修订号下标为 0 ,下一个修订号下标为 1 ,以此类推。例如,2.5.33 和 0.1 都是有效的版本号。

比较版本号时,请按从左到右的顺序依次比较它们的修订号。比较修订号时,只需比较 忽略任何前导零后的整数值 。也就是说,修订号 1 和修订号 001 相等 。如果版本号没有指定某个下标处的修订号,则该修订号视为 0 。例如,版本 1.0 小于版本 1.1 ,因为它们下标为 0 的修订号相同,而下标为 1 的修订号分别为 0 和 1 ,0 < 1 。

class Solution {
public:
    vector<string> split(string& str)
    {
        stringstream ss(str);
        char flag='.';
        string temp;
        vector<string> res;
        while(getline(ss,temp,flag))
        {
            if(!temp.empty())
                res.push_back(temp);
        }
        return res;
    }
    int compareVersion(string version1, string version2) {
        vector<string> vecVersion1 = split(version1),vecVersion2 = split(version2);
        int size=max(vecVersion1.size(),vecVersion2.size());
        int i=0;
        for(i=0;i<size;i++)
        {
            int x=i<vecVersion1.size()?atoi(vecVersion1[i].c_str()):0;
            int y=i<vecVersion2.size()?atoi(vecVersion2[i].c_str()):0;
            if(x == y) continue;
            if(x>y) return 1;
            if(x<y) return -1;
        }
        return 0;
    }
};

8. 字符串相乘

给定两个以字符串形式表示的非负整数 num1 和 num2,返回 num1 和 num2 的乘积,它们的乘积也表示为字符串形式。

注意:不能使用任何内置的 BigInteger 库或直接将输入转换为整数。

class Solution {
public:
    string addStrings(string num1,string num2)
    {
        int i=num1.size()-1,j=num2.size()-1,flag=0;
        string res="";
        while(i>=0 || j>=0 || flag>0)
        {
            int x=i>=0?num1[i]-'0':0;
            int y=j>=0?num2[j]-'0':0;
            int temp=x+y+flag;
            res.push_back(temp%10+'0');
            flag=temp/10;
            i--;j--;
        }
        reverse(res.begin(),res.end());
        return res;
    }
    string multiply_s_c(string num1,char num2)
    {
        int y=num2-'0';
        if(num2 == 0) return "0";
        string res="";
        int i=num1.size()-1,flag=0;
        while(i>=0 || flag >0)
        {
            int x=i>=0?num1[i]-'0':0;
            int temp=x*y+flag;
            res.push_back(temp%10+'0');
            flag=temp/10;
            i--;
        }
        reverse(res.begin(),res.end());
        return res;
    }
   
    string multiply(string num1, string num2) 
    {
        if(num1 == "0" || num2 == "0") return "0";
        string res="";
        for(int i=num2.size()-1;i>=0;i--)
        {
            string temp=multiply_s_c(num1,num2[i]);
            for(int j=num2.size()-1;j>i;j--) temp.push_back('0');
            res=addStrings(res,temp);
        }
        return res;
    }
};

9. 最长公共前缀

编写一个函数来查找字符串数组中的最长公共前缀。

如果不存在公共前缀,返回空字符串 ""

class Solution {
public:
    string longestCommonPrefix(vector<string>& strs) {
        string res=strs[0];

        for(int i=1;i<strs.size();i++)
        {
            int j=0;
            for(j=0;j<min(strs[i].size(),res.size());j++)
            {
                if(strs[i][j] == res[j])
                    continue;
                break;
            }
            res=res.substr(0,j);
        }
        return res;
    }
};

10. 电话号码字母组合

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。

class Solution {
public:
    vector<string> ret;
    void backTrace(string& digits,int pos,string str,unordered_map<char,string>& hashmap)
    {
        if(pos == digits.size()) 
        {
            ret.push_back(str);
            return;
        }

        char temp=digits[pos];
        for(int i=0;i<hashmap[temp].size();i++)
        {
            str.push_back(hashmap[temp][i]);
            backTrace(digits,pos+1,str,hashmap);
            str.pop_back();
        }
        
    }
    vector<string> letterCombinations(string digits) {
        if(digits.size() == 0) return ret;
        unordered_map<char,string> hashmap;
        hashmap['2']="abc"; 	hashmap['3']="def"; 	hashmap['4']="ghi";
        hashmap['5']="jkl";		 hashmap['6']="mno"; 	hashmap['7']="pqrs";
        hashmap['8']="tuv";		hashmap['9']="wxyz";
        backTrace(digits,0,"",hashmap);
        return ret;
    }
};

11. 字母异位词

给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。

class Solution {
public:
    vector<vector<string>> groupAnagrams(vector<string>& strs) {
        vector<vector<string>> ret;
        unordered_map<string,vector<string>> hashmap;
        for(int i=0;i<strs.size();i++)
        {
            string temp=strs[i];
            sort(temp.begin(),temp.end());
            hashmap[temp].push_back(strs[i]);
        }
        for(auto kv:hashmap)
        {
            ret.push_back(kv.second);
        }
        return ret;
    }
};

六、排序

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SgO11IMr-1663850245408)(https://pic.leetcode-cn.com/1656597367-EDjKrb-%E5%8D%81%E5%A4%A7%E7%BB%8F%E5%85%B8%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95.png)]

1. 手撕快排

class Solution {
public:
    void quickSort(vector<int>& nums,int left,int right)
    {
        if(left>=right) return;
        int low = left,high = right;
        //swap(nums[left], nums[rand()%((right-left+1)+left)]);
        int base = nums[left];

        while(low<high)
        {
            while(low<high && nums[high]>=base) high--; if(low<high) nums[low] = nums[high];
            while(low<high && nums[low]<=base) low++;   if(low<high) nums[high]=nums[low];
        }
        nums[low] = base;
        quickSort(nums,left,low-1);
        quickSort(nums,low+1,right);
    }
    vector<int> sortArray(vector<int>& nums) {
        srand((unsigned)time(NULL));
        quickSort(nums,0,nums.size()-1);
        return nums;
    }
};

2. 堆排序

class Solution {
    void maxHeapify(vector<int>& nums, int i, int len) {
        for (; (i << 1) + 1 <= len;) {
            int lson = (i << 1) + 1;
            int rson = (i << 1) + 2;
            int large;
            if (lson <= len && nums[lson] > nums[i]) {
                large = lson;
            } else {
                large = i;
            }
            if (rson <= len && nums[rson] > nums[large]) {
                large = rson;
            }
            if (large != i) {
                swap(nums[i], nums[large]);
                i = large;
            } else {
                break;
            }
        }
    }
    void buildMaxHeap(vector<int>& nums, int len) {
        for (int i = len / 2; i >= 0; --i) {
            maxHeapify(nums, i, len);
        }
    }
    void heapSort(vector<int>& nums) {
        int len = (int)nums.size() - 1;
        buildMaxHeap(nums, len);
        for (int i = len; i >= 1; --i) {
            swap(nums[i], nums[0]);
            len -= 1;
            maxHeapify(nums, 0, len);
        }
    }
public:
    vector<int> sortArray(vector<int>& nums) {
        heapSort(nums);
        return nums;
    }
};

3. 归并

class Solution {
    vector<int> tmp;
    void mergeSort(vector<int>& nums, int l, int r) {
        if (l >= r) return;
        int mid = (l + r) >> 1;
        mergeSort(nums, l, mid);
        mergeSort(nums, mid + 1, r);
        int i = l, j = mid + 1;
        int cnt = 0;
        while (i <= mid && j <= r) {
            if (nums[i] <= nums[j]) {
                tmp[cnt++] = nums[i++];
            }
            else {
                tmp[cnt++] = nums[j++];
            }
        }
        while (i <= mid) {
            tmp[cnt++] = nums[i++];
        }
        while (j <= r) {
            tmp[cnt++] = nums[j++];
        }
        for (int i = 0; i < r - l + 1; ++i) {
            nums[i + l] = tmp[i];
        }
    }
public:
    vector<int> sortArray(vector<int>& nums) {
        tmp.resize((int)nums.size(), 0);
        mergeSort(nums, 0, (int)nums.size() - 1);
        return nums;
    }
};

4. 冒泡

class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
        // bubbleSort
        int n = nums.size();
        for (int i = 0; i < n - 1; ++i) {
            bool flag = false;
            for (int j = 0; j < n - 1 - i; ++j) {
                if (nums[j] > nums[j + 1]) {
                    swap(nums[j], nums[j + 1]);
                    flag = true;
                }                 
            }
            if (flag == false) break; //无交换,代表当前序列已经最优 
        }
        return nums;
    }
};

5. 选择排序

class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
        // selectSort 选择排序
        int minIndex;
        int n = nums.size();
        for (int i = 0; i < n - 1; ++i) {
            minIndex = i;
            for (int j = i + 1; j < n; ++j) {
                if (nums[j] < nums[minIndex]) {
                    minIndex = j;
                }
            }
            swap(nums[i], nums[minIndex]);
        }
        return nums;
    }
};

6. 插入排序

class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
        // insertSort 插入排序
        int n = nums.size();
        // 第一个元素被认为已经被排序
        for (int i = 1; i < n; ++i) {
            // 两元素递增排序,则直接插入
            if (nums[i] >= nums[i - 1]) continue;
            // nums[i] < nums[i - 1]
            // 二分查找,时间复杂度logn
            int l = 0, r = i - 1;
            while (l <= r) {
                int mid = l + (r - l) / 2;
                // r右侧元素均大于 nums[i],即 r 及其左侧元素均小于等于nums[i]
                if (nums[i] < nums[mid]) r = mid - 1; 
                else l = mid + 1; 
            }
            int index = r + 1; // 稳定排序,依次排列
            // 将当前 i 元素插入在 index 位置,index ~ i - 1 元素依次后移,时间复杂度n
            int tmp = nums[i];
            for (int k = i; k >= index + 1; --k) {
                nums[k] = nums[k - 1]; // 依次后移一位
            }
            nums[index] = tmp;
        }
        return nums;
    }
};

7.希尔排序

class Solution {
    void shellSort(vector<int>&nums, int gap, int i) {
        int j, tmp = nums[i];
        for (j = i - gap; j >= 0 && tmp < nums[j]; j -= gap) {
            // 依次后移
            nums[j + gap] = nums[j];
        }
        nums[j + gap] = tmp;
    }
public:
    vector<int> sortArray(vector<int>& nums) {
        int n = nums.size();
        // 分组,最开始时,间隔 gap 为数组的一半
        for (int gap = n / 2; gap >= 1 ; gap /= 2) {
            // 对各个分组进行插入分组
            for (int i = gap; i < n; ++i) {
                shellSort(nums, gap, i);
            }
        }
        return nums;
    }
};

八、图

1.迪杰斯特拉算法

// 求start节点到所有节点的最短路径 编号:0~n-1
vector<int> Dijkstra(vector<vector<int>>& Graph, int start)
{
	vector<int> vis(Graph.size());
	vector<int> dis(Graph.size(),INT_MAX);
	dis[start] = 0;
	for (int i = 0; i < Graph.size(); i++)
	{
		int minn = INT_MAX, temp = -1;
		for (int j = 0; j < Graph.size(); j++)
		{
			if (!vis[j] && dis[j] < minn)
			{
				minn = dis[j];
				temp = j;
			}
		}
		if (temp == -1) return vector<int>();
		vis[temp] = 1;
		for (int k = 0; k < Graph.size(); k++)
		{
			if (!vis[k] && Graph[temp][k] != 0)
				dis[k] = min(dis[k], Graph[temp][k] + dis[temp]);
		}
	}
	return dis;
}

2.图中找单词

给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。

单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

思路:

设函数 check(i,j,k) 表示判断以网格的 (i, j)位置出发,能否搜索到单词word[k…],其中word[k…]表示字符串word 从第 k 个字符开始的后缀子串。如果能搜索到,则返回true,反之返回false。

函数check(i,j,k) 的执行步骤如下:

如果 board[i,j]≠s[k], 当前字符不匹配,直接返回false。

如果当前已经访问到字符串的末尾,且对应字符依然匹配,此时直接返回true。

否则,遍历当前位置的所有相邻位置。如果从某个相邻位置出发,能够搜索到子word[k+1…],则返回true,否则返回false。

九、其他

1. LRU 缓存

请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。
实现 LRUCache 类:
LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存
int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。
函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。

哈希表 + 双向链表

RU 缓存机制可以通过哈希表辅以双向链表实现,我们用一个哈希表和一个双向链表维护所有在缓存中的键值对。

双向链表按照被使用的顺序存储了这些键值对,靠近头部的键值对是最近使用的,而靠近尾部的键值对是最久未使用的。

哈希表即为普通的哈希映射(HashMap),通过缓存数据的键映射到其在双向链表中的位置。

对于 get 操作,首先判断 key 是否存在:

​ 如果 key 不存在,则返回 -1;

​ 如果 key 存在,则 key 对应的节点是最近被使用的节点。

​ 通过哈希表定位到该节点在双向链表中的位置,并将其移动到双向链表的头部,最后返回该节点的值。

对于 put 操作,首先判断 key 是否存在:

​ 如果 key 不存在,使用 key 和 value 创建一个新的节点,在双向链表的头部添加该节点,并将 key 和该节点添加进哈希表中。

​ 然后判断双向链表的节点数是否超出容量,如果超出容量,则删除双向链表的尾部节点,并删除哈希表中对应的项;

​ 如果 key 存在,则与 get 操作类似,先通过哈希表定位,再将对应的节点的值更新为 value,并将该节点移到双向链表的头部。

struct DLinkNode
{
    int key,value;
    DLinkNode *pre,*next;
    DLinkNode(){};
    DLinkNode(int _key,int _value):key(_key),value(_value){};
};

class LRUCache {
private:
    int size,capacity;
    unordered_map<int, DLinkNode*> hashmap;
    DLinkNode *head,*tail;
    void moveToHead(DLinkNode *node)
    {
        node->pre->next = node->next;
        node->next->pre = node->pre;
        insertToHead(node);
    }

    void insertToHead(DLinkNode *node)
    {
        node->next=head->next;
        node->next->pre = node;
        head->next = node;
        node->pre = head;
    }
    void deleteTail()
    {
        DLinkNode *temp = tail->pre;
        temp->pre->next=tail;
        tail->pre = temp->pre;
        delete temp;
    }

public:
    LRUCache(int _capacity):capacity(_capacity),size(0) {
        head = new DLinkNode();
        tail = new DLinkNode();
        head->next = tail;
        tail->pre=head;
    }
    
    int get(int key) {
        if(hashmap.find(key) == hashmap.end())
            return -1;
        moveToHead(hashmap[key]);
        return hashmap[key]->value;
    }
    
    void put(int key, int value) {
        if(hashmap.find(key) != hashmap.end())
        {
            // 存在
            hashmap[key]->value = value;
            moveToHead(hashmap[key]);
        }
        else
        {
            // 不存在
            DLinkNode *node = new DLinkNode(key,value);
            hashmap[key] = node;
            insertToHead(node);
            size++;
            if(size > capacity)
            {
                hashmap.erase(tail->pre->key);
                deleteTail();
                size--;
            }
        }
        
    }
};

2. 全排列

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

class Solution {
public:
    void backTrack(vector<vector<int>> &res,vector<int>& out,int first,int len)
    {
        if(first == len)
        {
            res.push_back(out);
            return;
        }
        for(int i=first;i<len;i++)
        {
            swap(out[i],out[first]);
            backTrack(res,out,first+1,len);
            swap(out[i],out[first]);
        }
    }

    vector<vector<int>> permute(vector<int>& nums) {
        vector<vector<int>> ret;
        backTrack(ret,nums,0,nums.size());
        return ret;
    }
};

3. x的平方根

class Solution {
public:
    double mySqrt(int x) {
        if (x == 0) {
            return 0;
        }
        return ans = exp(0.5 * log(x));
        // return ((long long)(ans + 1) * (ans + 1) <= x ? ans + 1 : ans);
    }
};

4. 360笔试老张修路

// 老张修路
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
vector<int> father;
int find(int x)
{
	return father[x] == x ? x : father[x] = find(father[x]);
}

bool cmp(const vector<int>& a, vector<int>& b)
{
	return a[2] < b[2];
}
int main() {
	int n, m;
	cin >> n >> m;
	father  = vector<int>(n+ 1);
	for (int i = 1; i <= n; i++) father[i] = i;
	vector<vector<int>> edges(m, vector<int>(3));
	for (int i = 0; i < 3; i++) {
		for (int j = 0; j < m; j++)
			cin >> edges[j][i];
	}
	sort(edges.begin(), edges.end(), cmp);
	int ans = 0;
	for (int i = 0; i < m; i++) {
		int f1 = find(edges[i][0]), f2 = find(edges[i][1]);
		if (f1 == f2)
			continue;
		ans += edges[i][2];
		int ff = min(f1, f2);
		father[f1] = father[f2] = ff;
	}
	cout << ans << endl;
	return 0;
}

5. 360笔试魔塔闯关

#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
int main(){
	int n;
	cin >> n;
	int res = 0;
	vector<int> nums;
	while (n-- > 0){
		int a, b;
		cin >> a >> b;
		if (b == 0) 
			res += a;
		else
			nums.push_back(a);
	}
	sort(nums.begin(), nums.end());
	for (int i = nums.size() - 1; i >= 0; i--)
		res += max(nums[i], res);
	cout << res;
	return 0;
}

6. 手写vector

#include<iostream>
using namespace std;
#ifndef VECTOR_H
#define VECTOR_H

template<typename Object>
class vector
{
private:
	int this_capcity;
	int this_size;
	Object* object;
public:
	vector() :this_capcity(5), this_size(0)
	{
		object = new Object[5];
	}
	explicit vector(int thisCapcity):this_capcity(thisCapcity), this_size(0)
	{
		object = new Object[thisCapcity];
	}
	vector(const vector& v)
	{
		*this = v;
	}
	~vector()
	{
		delete[] object;
	}
	void resize(int newSize)
	{
		if (this_size < newSize)
		{
			this_size = newSize;
			reserve(newSize * 2 + 1);
		}
	}
	void reserve(int newCapcity)
	{
		if (this_capcity < newCapcity)
		{
			Object* temp = new Object[newCapcity];
			for (int i = 0;i < this_size;i++)
			{
				temp[i] = object[i];
			}
			this_capcity = newCapcity;
			object = temp;
		}
	}
	int size()
	{
		return this_size;
	}
	int capcity()
	{
		return this_capcity;
	}
	void push_back(Object val)
	{
		if (this_size == this_capcity)
		{
			reserve(this_capcity * 2 + 1);
		}
		object[this_size] = val;
		this_size++;
	}
	Object front()
	{
		if (this_size) return object[0];
		else throw "OutOfBounds Exception!";
	}
	void pop_back()
	{
		if (this_size) this_size--;
	}
	void erase(int index)
	{
		if(index>this_size) throw "OutOfBounds Exception!";
		for (int i = index + 1;i < this_size;i++) object[i - 1] = object[i];
		this_size--;
	}
	Object back()
	{
		if (this_size) return object[this_size-1];
		else throw "OutOfBounds Exception!";
	}
	Object operator [](int index)
	{
		if (index < this_size) return object[index];
		else
		{
			throw "OutOfBounds Exception!";
		}
	}
	vector& operator = (const vector& v)
	{
		if (this != v)
		{
			this_size = v.this_size;
			this_capcity = v.this_capcity;
			object = new Object[this_capcity];
			for (int i = 0;i < this_size;i++)
			{
				object[i] = v[i];
			}
		}
		return this;
	}
};
int main()
{
	vector<int>v;
	for (int i = 0;i < 10;i++)
	{
		v.push_back(i);
		cout << i << ' ' << v.size() << ' ' << v.capcity() << endl;
	}
	v.reserve(100);
	cout << v.size() << ' ' << v.capcity() << endl;
	cout << "v.front()" << " = " << v.front() << endl;
	cout << "v.back()" << " = " << v.back() << endl;
	for (int i = 0;i < 10;i++)
	{
		cout << v[i] << ' ';
	}
	puts("");
	v.pop_back();
	v.erase(3);
	for (int i = 0;i < v.size();i++)
	{
		cout << v[i] << ' ';
	}
	puts("");

}
#endif VECTOR_H

7.手写string

// 头文件
#ifndef MYSTRING_H
#define MYSTRING_H
#include <iostream>
#include <cstring>
#include <cstdlib>
using namespace std;
 
class myString
{
    friend ostream & operator<<(ostream & out, myString &) ;
    friend istream & operator>>(istream & in, myString &) ;
 
 
    public:
        myString();  // 默认构造
        virtual ~myString();
 
 
        myString(const char *); // 由字符串构造
        myString(const myString &); // 拷贝构造函数
 
        myString & operator=(const myString &);   // 拷贝赋值运算符
        myString & operator=(const char *);   // 由字符串构造
 
 
        myString(myString &&) noexcept;  // 移动构造函数
        myString & operator=(myString &&) noexcept;  // 移动赋值运算符
 
 
 
        int getLength() const;
        int getCapacity() const;
        const char * getStr() const;  // 获取 C 字符串
 
 
        char & operator[](int);  // 获取 第 i 位字符
        bool operator==(const myString& str); // 判断两个字符串是否相等
 
        void append(char c);  // 追加一个字符
        void append(const char * s); // 追加一个字符串
 
        myString  operator+(const char * s);  // 拼接字符串
        myString  operator+(const myString & s2);  // 拼接字符串
        myString & operator+=(const char * s); // 追加字符串
        myString & operator+=(const myString & s); // 追加字符串
 
    protected:
 
    private:
        char * str;
        int length;
        int capacity;
};
#endif // MYSTRING_H
#include "MyString.h"
 
myString::myString()
{
    this->length = 0;
    this->capacity = 15;
    this->str = (char *)malloc(sizeof(char) * this->capacity);// 默认分配一个长度为15的字符数组,初始化为空串
    this->str[0] = '\0';
}
 
 
myString::~myString()
{
    //dtor
    if(this->str != nullptr) {
        free(this->str);
    }
}
 
myString::myString(const char * s)
{
    if(s != nullptr) {
        int sz = strlen(s);
        this->length = sz;
        this->capacity = sz+1;
        this->str = (char * ) malloc(sizeof(char) * this->capacity);  // 按字符串大小复制
        strcpy(this->str,s);
    } else {
        this->length = 0;
        this->capacity = 15;
        this->str = (char *)malloc(sizeof(char) * this->capacity);// 默认分配一个长度为15的字符数组,初始化为空串
        this->str[0] = '\0';
    }
}
 
myString::myString(const myString & s)
{
    this->length = s.getLength();
    this->capacity = s.getCapacity();
    this->str = (char * ) malloc(sizeof(char) * this->capacity);  // 按字符串大小复制
    strcpy(this->str,s.getStr());
 
}
 
// 移动构造函数
myString::myString(myString && s) noexcept {
    this->length = s.length;
    this->capacity = s.capacity;
    this->str = s.str;
 
    s.length = 0;
    s.capacity = 0;
    s.str = nullptr;
 
}
 
myString & myString::operator=(myString && s) noexcept {
    if(this != & s) {   // 是否自赋值
        free(this->str);
        this->length = s.length;
        this->capacity = s.capacity;
        this->str = s.str;
        s.length = 0;
        s.capacity = 0;
        s.str = nullptr;
    }
 
    return *this;
}
 
 
myString & myString::operator=(const myString & s) {  // 拷贝赋值运算符
    if(this != &s) {  // 检查自赋值
        int sz = s.getLength();
        if(sz < this->capacity) { // 如果原字符串容量够,那么直接拷贝
            this->length = sz;
            strcpy(this->str,s.getStr());
        } else { // 容量不够,释放掉原有内存,重新申请
            free(this->str);  // 释放掉原有的字符串
            this->length = s.getLength();
            this->capacity = s.getCapacity();
            this->str = (char * ) malloc(sizeof(char) * this->capacity);  // 按字符串大小复制
            strcpy(this->str,s.getStr());
        }
    }
    return *this;
}
 
 
myString & myString::operator=(const char * s) {  // 字符串构造
    if(s != nullptr) {
        int sz = strlen(s);
        if(sz < this->capacity) {  // 如果原字符串容量够,那么直接拷贝
            this->length = sz;
            strcpy(this->str,s);
        } else {  // 容量不够,释放掉原有内存,重新申请
            free(this->str);
            this->length = sz;
            this->capacity = sz+1;
            this->str = (char *)malloc(sizeof(char) * this->capacity);
            strcpy(this->str,s);
        }
 
    } else {
        this->length = 0;
        this->capacity = 15;
        this->str = (char *)malloc(sizeof(char) * this->capacity);// 默认分配一个长度为15的字符数组,初始化为空串
        this->str[0] = '\0';
    }
    return *this;
}
 
 
int myString::getLength() const
{
    return this->length;
}
 
int myString::getCapacity() const
{
    return this->capacity;
}
 
const char * myString::getStr() const  // 不允许修改 str
{
    return this->str;
}
 
 
 
char & myString::operator[](int i)
{
    if(i < 0 || i > this->length) {
        cout << "index out of range!" << endl;
        return this->str[this->length];     // 返回最后一个字符,也就是'\0'
    }
    return this->str[i];
}
 
bool myString::operator==(const myString & s)
{
    if(this->length != s.getLength()) return false;
    return strcmp(this->str,s.getStr()) == 0;
}
 
void myString::append(char c)
{
    int sz = 1 + this->length;
    if(sz  >= this->capacity) {  // 追加后字符大于字符数组长度
        while(this->capacity <= (sizeof(char) * sz)) this->capacity = (this->capacity + this->capacity >> 1);   // 容量变为1.5,直到大于拼接字符串的长度
        char * newStr = (char *)malloc(this->capacity);
        strcpy(newStr,this->str);
        newStr[sz] = c;
        newStr[sz+1] = '\0';
        free(this->str);
        this->str = newStr;
        this->length = sz;
    } else {
        this->str[sz] = c;
        this->str[sz+1] = '\0';
        this->length = sz;
    }
 
}
 
void myString::append(const char * s)
{
    int sz = strlen(s) + this->length;
    if(sz  >= this->capacity) {  // 追加后字符串大于字符数组长度
        while(this->capacity <= (sizeof(char) * sz)) this->capacity = (this->capacity + this->capacity / 2);   // 容量变为1.5,直到大于拼接字符串的长度
        char * newStr = (char *)malloc(this->capacity);
        strcpy(newStr,this->str);
        newStr = strcat(newStr,s);
        free(this->str);
        this->str = newStr;
        this->length = sz;
    } else {
        this->str = strcat(this->str, s);
        this->length = sz;
    }
 
}
 
myString myString::operator+(const char * s)  // 拼接字符串
{
    myString res(this->str);
    res += s;
    return res;
}
 
myString myString::operator+(const myString & s)  // 拼接字符串
{
    this->append(s.getStr());
    return *this;
}
 
myString & myString::operator+=(const char * s) // 追加字符串
{
    this->append(s);
    return *this;
}
 
myString & myString::operator+=(const myString & s) // 追加字符串
{
    this->append(s.getStr());
    return *this;
}
 
ostream & operator<<(ostream & out, myString & myStr)
{
    out << (myStr.str) ;
    return out;
}
 
istream & operator>>(istream & in, myString & myStr)
{
    in >> myStr.str;
    return in;
}

web服务器项目常见面试题目

项目介绍

1、为什么要做这样一个项目?

在学习C++语言的时候,发现需要做一个项目来巩固一下,网上有推荐这个项目,然后就自己尝试做了一下。这个项目综合性比较强,从中既能学习Linux环境下的一些系统调用,也能熟悉网络编程。

2、介绍下你的项目

服务器基本框架

img

此项目是基于Linux的轻量级多线程Web服务器,应用层实现了一个简单的HTTP服务器,利用多路IO复用,可以同时监听多个请求,使用线程池处理请求,使用模拟proactor模式,主线程负责监听,监听有事件之后,从socket中循环读取数据,然后将读取到的数据封装成一个请求对象放入队列。睡眠在请求队列上的工作线程被唤醒进行处理,使用状态机解析HTTP请求报文,实现同步/异步日志系统,记录服务器运行状态,并对系统进行了压力测试。

3、你的项目的技术难点是什么?

​ 1、如何提高服务器的并发能力

​ 2、由于涉及到I/O操作,当单条日志比较大的时候,同步模式会阻塞整个处理流程

​ 3、多线程并发的情况下,保证线程的同步

4、你是如何克服这个技术难点的?

5、你做这个项目的收获是什么?

6、为什么使用这个技术/组件?

7、如何解决项目中的BUG

(1)运行代码,发现错误,找到报错的位置。

(2)如果注释后运行正常了,那么就是注释掉的部分有误了

(3)若不是,则从main 函数里边调用的开始下手,查看定义,跳转到定义的功能,这很大程度上能让我们快速的找到bug。

8、为什么所有人都是这个服务器项目

自己接触到C++最好的练手项目就是webserver,也不知道其他人 都做这个。

9、项目的异常处理有哪些

(1)登录异常

     * 用户不存在:根据账号在数据库查,如果查不到就是
     * 用户名或密码错误:数据库查询的用户密码和http请求用户输入的密码比对,如果不一致
     * 登录成功

try-catch语句

程序先执行 try 中的代码
如果 try 中的代码出现异常, 就会结束 try 中的代码, 看和 catch 中的异常类型是否匹配.
如果找到匹配的异常类型, 就会执行 catch 中的代码
如果没有找到匹配的异常类型, 就会将异常向上传递到上层调用者.
无论是否找到匹配的异常类型, finally 中的代码都会被执行到(在该方法结束之前执行).
如果上层调用者也没有处理的了异常, 就继续向上传递
一直到 main 方法也没有合适的代码处理异常, 就会交给 JVM 来进行处理, 此时程序就会异常终止

10、项目中用到了什么协议

HTTP、TCP、DNS

线程池相关

1、为什么使用线程池

每个请求对应一个线程方法的不足之一是:为每个请求创建一个新线程的开销很大;为每个请求创建新线程的服务器在创建和销毁线程上花费的时间和消耗的系统资源要比花在处理实际的用户请求的时间和资源更多。

线程池是为了避免创建和销毁线程所产生的开销,避免活动的线程消耗的系统资源;

提高响应速度,任务到达时,无需等待线程即可立即执行;

提高线程的可管理性:线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。

2、怎么创建线程池(线程池运行逻辑)

该项目使用线程池(半同步半反应堆模式)并发处理用户请求,主线程负责读写,工作线程(线程池中的线程)负责处理逻辑(HTTP请求报文的解析等)。

具体的:主线程为异步线程,负责监听文件描述符,接收socket新连接,若当前监听的socket发生了读写事件,然后将任务插入到请求队列。工作线程从请求队列中取出任务,完成读写数据的处理。

线程池是空间换时间,浪费服务器的硬件资源,换取运行效率.

3、线程的同步机制有哪些?

(1)同步I/O

同步I/O指内核向应用程序通知的是就绪事件,比如只通知有客户端连接,要求用户代码自行执行I/O操作

a ) 阻塞IO:调用者调用了某个函数,等待这个函数返回,期间什么也不做,不停的去检查这个函数有没有返回,必须等这个函数返回才能进行下一步动作

b ) 非阻塞IO:非阻塞等待,每隔一段时间就去检测IO事件是否就绪。没有就绪就可以做其他事。非阻塞I/O执行系统调用总是立即返回,不管时间是否已经发生,若时间没有发生,则返回-1,此时可以根据errno区分这两种情况,对于accept,recv和send,事件未发生时,errno通常被设置成eagain

c ) 信号驱动IO:linux用套接口进行信号驱动IO,安装一个信号处理函数,进程继续运行并不阻塞,当IO时间就绪,进程收到SIGIO信号。然后处理IO事件。

d ) IO复用:linux用select/poll函数实现IO复用模型,这两个函数也会使进程阻塞,但是和阻塞IO所不同的是这两个函数可以同时阻塞多个IO操作。而且可以同时对多个读操作、写操作的IO函数进行检测。知道有数据可读或可写时,才真正调用IO操作函数

(2)异步I/O

异步I/O是指内核向应用程序通知的是完成事件,比如读取客户端的数据后才通知应用程序,由内核完成I/O操作

linux中,可以调用aio_read函数告诉内核描述字缓冲区指针和缓冲区的大小、文件偏移及通知的方式,然后立即返回,当内核将数据拷贝到缓冲区后,再通知应用程序。

4、线程池中的工作线程是一直等待吗?

在run函数中,我们为了能够处理高并发的问题,将线程池中的工作线程都设置为阻塞等待在请求队列是否不为空的条件上,因此项目中线程池中的工作线程是处于一直阻塞等待的模式下的。

5、你的线程池工作线程处理完一个任务后的状态是什么?

(1) 当处理完任务后如果请求队列为空时,则这个线程重新回到阻塞等待的状态

(2) 当处理完任务后如果请求队列不为空时,那么这个线程将处于与其他线程竞争资源的状态,谁获得锁谁就获得了处理事件的资格。

6、如果同时1000个客户端进行访问请求,线程数不多,怎么能及时响应处理每一个呢?

本项目是通过对子线程循环调用来解决高并发的问题的。

首先在创建线程的同时就调用了pthread_detach将线程进行分离,不用单独对工作线程进行回收,资源自动回收。

我们通过子线程的run调用函数进行while循环,让每一个线程池中的线程永远都不会停止,访问请求被封装到请求队列(list)中,如果没有任务线程就一直阻塞等待,有任务线程就抢占式进行处理,直到请求队列为空,表示任务全部处理完成。

7、如果一个客户请求需要占用线程很久的时间,会不会影响接下来的客户请求呢,有什么好的策略呢?

会,因为线程池内线程的数量时有限的,如果客户请求占用线程时间过久的话会影响到处理请求的效率,当请求处理过慢时会造成后续接受的请求只能在请求队列中等待被处理,从而影响接下来的客户请求。

应对策略:

我们可以为线程处理请求对象设置处理超时时间, 超过时间先发送信号告知线程处理超时,然后设定一个时间间隔再次检测,若此时这个请求还占用线程则直接将其断开连接。

8、什么是虚假唤醒?

举个例子,我们现在有一个生产者-消费者队列和三个线程。

1) 1号线程从队列中获取了一个元素,此时队列变为空。

2) 2号线程也想从队列中获取一个元素,但此时队列为空,2号线程便只能进入阻塞(cond.wait()),等待队列非空。

3) 这时,3号线程将一个元素入队,并调用cond.notify()唤醒条件变量。

4) 处于等待状态的2号线程接收到3号线程的唤醒信号,便准备解除阻塞状态,执行接下来的任务(获取队列中的元素)。

5) 然而可能出现这样的情况:当2号线程准备获得队列的锁,去获取队列中的元素时,此时1号线程刚好执行完之前的元素操作,返回再去请求队列中的元素,1号线程便获得队列的锁,检查到队列非空,就获取到了3号线程刚刚入队的元素,然后释放队列锁。

6) 等到2号线程获得队列锁,判断发现队列仍为空,1号线程“偷走了”这个元素,所以对于2号线程而言,这次唤醒就是“虚假”的,它需要再次等待队列非空。

9、介绍一下几种典型的锁?

线程池的实现还需要依靠锁机制以及信号量机制来实现线程同步,保证操作的原子性

(1)读写锁

多个读者可以同时进行读
写者必须互斥(只允许一个写者写,也不能读者写者同时进行)
写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)

(2)互斥锁

一次只能一个线程拥有互斥锁,其他线程只有等待

互斥锁是在抢锁失败的情况下主动放弃CPU进入睡眠状态直到锁的状态改变时再唤醒,而操作系统负责线程调度,为了实现锁的状态发生改变时唤醒阻塞的线程或者进程,需要把锁交给操作系统管理,所以互斥锁在加锁操作时涉及上下文的切换。
(3)条件变量

互斥锁一个明显的缺点是他只有两种状态:锁定和非锁定。而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,他常和互斥锁一起使用,以免出现竞态条件。当条件不满足时,线程往往解开相应的互斥锁并阻塞线程然后等待条件发生变化。一旦其他的某个线程改变了条件变量,他将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。总的来说互斥锁是线程间互斥的机制,条件变量则是同步机制。

(4)自旋锁

如果进线程无法取得锁,进线程不会立刻放弃CPU时间片,而是一直循环尝试获取锁,直到获取为止。如果别的线程长时期占有锁,那么自旋就是在浪费CPU做无用功,但是自旋锁一般应用于加锁时间很短的场景,这个时候效率比较高。

10、如何销毁线程

1、通过判断标志位,主动退出
2、通过Thread类中成员方法interrupt(),主动退出
3、通过Thread类中成员方法stop(),强行退出

11、detach和join有什么区别

(1)当调用join(),主线程等待子线程执行完之后,主线程才可以继续执行,此时主线程会释放掉执行完后的子线程资源。主线程等待子线程执行完,可能会造成性能损失。

(2)当调用detach(),主线程与子线程分离,他们成为了两个独立的线程遵循cpu的时间片调度分配策略。子线程执行完成后会自己释放掉资源。分离后的线程,主线程将对它没有控制权。

当你确定程序没有使用共享变量或引用之类的话,可以使用detch函数,分离线程。

12、每个线程占多大的内存

32位系统,分配4G的虚拟内存给进程,每个线程约占10M的内存

13、线程池中有多少个线程,线程池数量如何设定

默认8个

调整线程池中的线程数量的最主要的目的是为了充分并合理地使用 CPU 和内存等资源,从而最大限度地提高程序的性能。

Ncpu 表示 CPU的数量。

如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 Ncpu+1。

如果是IO密集型任务,参考值可以设置为 2 * Ncpu。因为线程间竞争的不是CPU的计算资源而是IO,IO的处理一般较慢,多于cores数的线程将为CPU争取更多的任务,不至在线程处理IO的过程造成CPU空闲导致资源浪费。

最佳线程数量 = ((线程等待时间+线程CPU时间)/ 线程CPU时间)* CPU个数。

由公式可得,线程等待时间所占比例越高,需要越多的线程,线程CPU时间所占比例越高,所需的线程数越少。

14、socket 通信的基本流程

img

15、listen 函数第二个参数 backlog 参数作用

int listen(int sockfd, int backlog);
backlog是accept阻塞队列的长度,即等待accept的socket的最大数量。

16、listen底层用的是什么队列

a.半连接队列(Incomplete connection queue),又称 SYN 队列。……

b.全连接队列(Completed connection queue),又称 Accept 队列。……

17、send函数在发送的数据长度大于发送缓冲区大小,或者大于发送缓冲区剩余大小时,socket会怎么反应

不管是windows还是linux,阻塞还是非阻塞,send都会分帧发送,分帧到缓冲区能够接收的大小

并发模型相关

1、IO是什么

在计算机中,输入/输出(即IO)是指信息处理系统(比如计算机)和外部世界(可以是人或其他信息处理系统)的通信。输入是指系统接收的信号或数据,输出是指从系统发出的数据或信号。

(数据从网卡或硬盘读到内核缓冲区)

2、几种I/O模型

(1)阻塞 blocking (BIO)

调用者调用了某个函数, 等待这个函数返回 ,期间什么也不做,不停的去检查这个函数有没有返回,必须等这个函数返回才能进行下一步动作。
(2)非阻塞 non-blocking(NIO)

非阻塞等待,每隔一段时间就去检测 IO 事件是否就绪。 没有就绪就可以做其他事。 非阻塞 I/O 执行系统调 用总是立即返回,不管事件是否已经发生,若事件没有发生,则返回 -1 ,此时可以根据 errno 区分这两 种情况,对于 accept , recv 和 send ,事件未发生时, errno 通常被设置成 EAGAIN 。
(3)IO复用(IO multiplexing)

Linux 用 select/poll/epoll 函数实现 IO 复用模型,这些函数也会使进程阻塞,但是和阻塞 IO 所不同的是这些函数可以同时阻塞多个 IO 操作。而且可以同时对多个读操作、写操作的 IO 函数进行检测。直到有数 据可读或可写时,才真正调用 IO 操作函数。
(4)信号驱动(signal-driven)

信号驱动 IO , 安装一个信号处理函数,进程继续运行并不阻塞, 当 IO 事件就绪,进程收到 SIGIO 信号,然后处理 IO 事件。
与非阻塞 IO 的区别在于它提供了消息通知机制,不需要用户进程不断的轮询检查,减少了系统 API 的调用次数,提高了效率。
(5)异步(asynchronous)

Linux 中,可以调用 aio_read 函数告诉内核描述字缓冲区指针和缓冲区的大小、文件偏移及通知的方式,然后立即返回,当内核将数据拷贝到缓冲区后,再通知应用程序。

3、简单说一下服务器使用的并发模型?两种高效的事件并发处理模式reactor、proactor?

事件:I/O事件、信号及定时事件

(1)reactor模式中,主线程(I/O处理单元)只负责监听文件描述符上是否有事件发生,有的话立即通知工作线程(逻辑单元 ),将socket可读写事件放入请求队列,交给工作线程处理,即读写数据、接受新连接及处理客户请求均在工作线程中完成。通常由同步I/O实现(epoll_wait)。Reactor模式主要是提高系统的吞吐量,在有限的资源下处理更多的事情。

(2)proactor模式中,主线程和内核负责处理读写数据、接受新连接等I/O操作,工作线程仅负责业务逻辑,如处理客户请求。通常由异步I/O实现(aio_read/aio_write)。

本服务器采用:同步I/O模拟Proactor模式

Reactor和Proactor模式的主要区别就是 真正的读取和写入操作是有谁来完成的。

常见的 Reactor 实现方案有三种。

第一种方案单 Reactor 单进程 / 线程,不用考虑进程间通信以及数据同步的问题,因此实现起来比较简单,这种方案的缺陷在于无法充分利用多核 CPU,而且处理业务逻辑的时间不能太长,否则会延迟响应,所以不适用于计算机密集型的场景,适用于业务处理快速的场景,比如 Redis 采用的是单 Reactor 单进程的方案。

第二种方案单 Reactor 多线程,通过多线程的方式解决了方案一的缺陷,但它离高并发还差一点距离,差在只有一个 Reactor 对象来承担所有事件的监听和响应,而且只在主线程中运行,在面对瞬间高并发的场景时,容易成为性能的瓶颈的地方。

第三种方案多 Reactor 多进程 / 线程,通过多个 Reactor 来解决了方案二的缺陷,主 Reactor 只负责监听事件,响应事件的工作交给了从 Reactor,Netty 和 Memcache 都采用了「多 Reactor 多线程」的方案,Nginx 则采用了类似于 「多 Reactor 多进程」的方案。

Reactor 可以理解为「来了事件操作系统通知应用进程,让应用进程来处理」,而 Proactor 可以理解为「来了事件操作系统来处理,处理完再通知应用进程」。

因此,真正的大杀器还是 Proactor,它是采用异步 I/O 实现的异步网络模型,感知的是已完成的读写事件,而不需要像 Reactor 感知到事件后,还需要调用 read 来从内核中获取数据。

不过,无论是 Reactor,还是 Proactor,都是一种基于「事件分发」的网络编程模式,区别在于 Reactor 模式是基于「待完成」的 I/O 事件,而 Proactor 模式则是基于「已完成」的 I/O 事件。

4、你用了epoll,说一下为什么用epoll,还有其他复用方式吗?区别是什么?

(1)epoll的优点:epoll 是一种更加高效的 IO 复用技术

1、没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口);
2、效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;
即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。

3、 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。

epoll 的使用步骤及原理如下:

1)调用epoll_create()会在内核中创建一个指示epoll内核事件表的文件描述符,该描述符将用作其他epoll系统调用的第一个参数。

在这个结构体中有 2 个比较重要的数据成员:一个是需要检测的文件描述符的信息 struct_root rbr (红黑树),还有一个是就绪列表struct list_head rdlist,存放检测到数据发送改变的文件描述符信息 (双向链表);

2)调用epoll_ctl() 用于操作内核事件表监控的文件描述符上的事件:注册、修改、删除

3)调用epoll_wait() 可以让内核去检测就绪的事件,并将就绪的事件放到就绪列表中并返回,通过返回的事件数组做进一步的事件处理。

epoll 的两种工作模式:

a)LT 模式(水平触发)LT(Level - Triggered)是缺省的工作方式,并且同时支持 Block 和 Nonblock Socket。 在这种做法中,内核检测到一个文件描述符就绪了,然后应用程序可以对这个就绪的 fd 进行 IO 操作。应用程序可以不立即处理该事件,如果不作任何操作,内核还是会继续通知。

b)ET 模式(边缘触发) ET(Edge - Triggered)是高速工作方式,只支持 Nonblock socket。 在这种模式下,epoll_wait检测到文件描述符有事件发生,则将其通知给应用程序,应用程序必须立即处理该事件。必须要一次性将数据读取完,使用非阻塞I/O,读取到出现EAGAIN。但是,如果一直不对这个 fd 进行 IO 操作(从而导致它再次变成未就绪 ),内核不会发送更多的通知(only once)。

ET 模式在很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。epoll 工作在 ET 模式的时候,必须使用非阻塞套接口,以避免由于一个文件描述符的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

3)EPOLLONESHOT

一个线程读取某个socket上的数据后开始处理数据,在处理过程中该socket上又有新数据可读,此时另一个线程被唤醒读取,此时出现两个线程处理同一个socket

我们期望的是一个socket连接在任一时刻都只被一个线程处理,通过epoll_ctl对该文件描述符注册epolloneshot事件,一个线程处理socket时,其他线程将无法处理,当该线程处理完后,需要通过epoll_ctl重置epolloneshot事件

(2)I/O 多路复用是一种使得程序能同时监听多个文件描述符的技术,从而提高程序的性能。 Linux 下实现 I/O 复用的系统调用主要有select、poll 和 epoll。

(3)select/poll/epoll区别

1)调用函数

select和poll都是一个函数,epoll是一组函数

2)文件描述符数量

select通过线性表描述文件描述符集合,文件描述符有上限(与系统内存关系很大),32位机默认是1024个,64位机默认是2048。

poll是链表描述,突破了文件描述符上限,最大可以打开文件的数目

epoll通过红黑树描述,最大可以打开文件的数目

3)将文件描述符从用户传给内核

select和poll通过将所有文件描述符拷贝到内核态,每次调用都需要拷贝

epoll通过epoll_create建立一棵红黑树,通过epoll_ctl将要监听的文件描述符注册到红黑树上

4)内核判断就绪的文件描述符

select和poll通过线性遍历文件描述符集合,判断哪个文件描述符上有事件发生

epoll_create时,内核除了帮我们在epoll文件系统里建了个红黑树用于存储以后epoll_ctl传来的fd外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。

epoll是根据每个fd上面的回调函数(中断函数)判断,只有发生了事件的socket才会主动的去调用 callback函数,其他空闲状态socket则不会,若是就绪事件,插入list

5)应用程序索引就绪文件描述符

select/poll只返回发生了事件的文件描述符的个数,若知道是哪个发生了事件,同样需要遍历

epoll返回的发生了事件的个数和结构体数组,结构体包含socket的信息,因此直接处理返回的数组即可

6)工作模式

select和poll都只能工作在相对低效的LT模式下

epoll则可以工作在ET高效模式,并且epoll还支持EPOLLONESHOT事件,该事件能进一步减少可读、可写和异常事件被触发的次数。

7)应用场景

当监测的fd数目较小,且全部fd都比较活跃,建议使用select或者poll

当监测的fd数目非常大,且单位时间只有其中的一部分fd处于就绪状态,这个时候使用epoll能够明显提升性能

条件编译:

#ifdef 标识符

程序段1

#else

程序段2

#endif

5、LT和ET的使用场景

LT适用于并发量小的情况,ET适用于并发量大的情况。

ET在通知用户之后,就会将fd从就绪链表中删除,而LT不会,它会一直保留,这就会导致随着fd增多,就绪链表越大,每次都要从头开始遍历找到对应的fd,所以并发量越大效率越低。ET因为会删除所以效率比较高。

(LT模式下只读一次,ET模式下是无限循环读)

6、怎么解决LT的缺点?

LT模式下,可写状态的fd会一直触发事件,该怎么处理这个问题

数据量很少时直接send数据,数据量很多时每次要写数据时,将fd绑定EPOLLOUT事件,写完后将fd同EPOLLOUT从epoll中移除。

7、为什么ET模式一定要设置非阻塞?

因为ET模式下是无限循环读,直到出现错误为EAGAIN或者EWOULDBLOCK,这两个错误表示socket为空,然后就停止循环。如果是阻塞,循环读在socket为空的时候就会阻塞到那里,主线程的read()函数一旦阻塞住,当再有其他监听事件过来就没办法读了,给其他事情造成了影响,所以必须要设置为非阻塞。

8、epoll 如何判断数据已经读取完成

epoll ET(Edge Trigger)模式,才需要关注数据是否读取完毕了。使用select或者epoll的LT模式,不用关注,select/epoll检测到有数据可读去读就OK了。
两种做法:
1、针对TCP,调用recv方法,根据recv的返回值。如果返回值小于我们设定的recv buff的大小,那么就认为接收完毕。
2、TCP、UDP都适用,将socket设为NOBLOCK状态(使用fcntl函数),然后select该socket可读的时候,使用read/recv函数读取数据。当返回值为-1,并且errno是EAGAIN或EWOULDBLOCK的时候,表示数据读取完毕。

9、epoll为什么要用红黑树

epoll内核中维护了一个内核事件表,它是将所有的文件描述符全部都存放在内核中,系统去检测有事件发生的时候触发回调,当你要添加新的文件描述符的时候也是调用epoll_ctl函数使用EPOLL_CTL_ADD宏来插入,epoll_wait也不是每次调用时都会重新拷贝一遍所有的文件描述符到内核态。当我现在要在内核中长久的维护一个数据结构来存放文件描述符,并且时常会有插入,查找和删除的操作发生,这对内核的效率会产生不小的影响,因此需要一种插入,查找和删除效率都不错的数据结构来存放这些文件描述符,那么红黑树当然是不二的人选。

HTTP报文解析相关

1、用了状态机啊,为什么要用状态机?

在逻辑处理模块中,响应HTTP请求采用主从状态机来完成

传统的控制流程都是按照顺序执行的,状态机能处理任意顺序的事件,并能提供有意义的响应—即使这些事件发生的顺序和预计的不同。

项目中使用主从状态机的模式进行解析,从状态机(parse_line)负责读取报文的一行,主状态机负责对该行数据进行解析,主状态机内部调用从状态机,从状态机驱动主状态机。每解析一部分都会将整个请求的m_check_state状态改变,状态机也就是根据这个状态来进行不同部分的解析跳转的。

  1. 当一个程序有多个状态时,规范了状态机的状态转换,避免了一些引入一些复杂的判断逻辑。
  2. 规范了程序在不同状态下所能提供的能力。
  3. 在能力上可以进行横向扩展,提供新的状态来完善现有逻辑

2、状态机的转移图画一下

从状态机负责读取报文的一行,主状态机负责对该行数据进行解析,主状态机内部调用从状态机,从状态机驱动主状态机。

img

主状态机

三种状态,标识解析位置。

CHECK_STATE_REQUESTLINE,解析请求行

CHECK_STATE_HEADER,解析请求头

CHECK_STATE_CONTENT,解析消息体,仅用于解析POST请求

从状态机

三种状态,标识解析一行的读取状态。

LINE_OK,完整读取一行,该条件涉及解析请求行和请求头部

LINE_BAD,报文语法有误

LINE_OPEN,读取的行不完整

3、状态机的缺点

状态机的缺点就是性能比较低,一般一个状态做一个事情,性能比较差,在追求高性能的场景下一般不用,高性能场景一般使用流水线设计

4、HTTPS协议为什么安全?

https验证过程

https共经历了两个阶段,一个是证书验证阶段,在本阶段通过非对称加密会验证ca证书的合法性,当证书合法之后,第二步是数据传输的阶段,本阶段的加密是使用对称加密,以用于高频传输数据,因为非对称加密相对于对称加密的效率是非常低的,高频传输根本无法接受这种情况

5、HTTPS的SSL连接过程

6、GET和POST的区别

(1)get主要用来获取数据,post主要用来提交或修改数据。

(2)get的参数有长度限制,最长2048字节,而post没有限制。

(3)get是明文传输,可以直接通过url看到参数信息,post是放在请求体中,除非用工具才能看到。

(4)get的参数会附加在url中,以 " ?"分割url和传输数据,多个参数用 "&"连接, 而post会把参数放在http请求体中。

(5)get请求会保存在浏览器历史记录中,也可以保存在web服务器日志中。

(6)get请求会被浏览器主动缓存,而post不会,除非手动设置。

(7)get在浏览器回退时是无害的,而post会再次提交请求。

(8)get请求只能进行url编码,而post支持多种编码方式。

(9)get请求的参数数据类型只接受ASCII字符,而post没有限制。

(10)get是幂等的,而post不是幂等的。 幂等性:对同一URL的多个请求应该返回同样的结果。

7、HTTP报文格式

(1)HTTP请求报文:请求行、请求头部、请求空行、请求数据

img

1)请求行:用来说明请求方法,要访问的资源以及所使用的HTTP版本。

格式: 请求方法|空格|URL|空格|协议版本|回车符|换行符

2)请求头部:用来说明服务器要使用的附加信息

HTTP常见字段有哪些?

HOST,给出请求资源所在服务器的域名。

User-Agent,HTTP客户端程序的信息,该信息由你发出请求使用的浏览器来定义,并且在每个请求中自动发送等。

connection,连接管理,可以是Keep-Alive或close。

content-length字段,这里用于读取post请求的消息体长度

Content-Type 字段:用于服务器回应时,告诉客户端,本次数据是什么格式

Content-Type: text/html; charset=utf-8

*POST /audiolibrary/music?ar=1595301089068&n=1p1 HTTP/1.1\r\n*
*Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, application/x-silverlight, application/x-shockwave-flash\r\n*
*Referer: http://www.google.cn\r\n*
*Accept-Language: zh-cn\r\n*
*Accept-Encoding: gzip, deflate\r\n*
*User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; TheWorld)\r\n*
*content-length:28\r\n*
*Host: www.google.cn\r\n*
*Connection: Keep-Alive\r\n*
*Cookie: PREF=ID=80a06da87be9ae3c:U=f7167333e2c3b714:NW=1:TM=1261551909:LM=1261551917:S=ybYcq2wpfefs4V9g; NID=31=ojj8d-IygaEtSxLgaJmqSjVhCspkviJrB6omjamNrSm8lZhKy_yMfO2M4QMRKcH1g0iQv9u\r\n"*
*\r\n*
*hl=zh-CN&source=hp&q=domety

// 上面的类型表明,发送的是网页,而且编码是UTF-8。
在HTTP报文中,每一行的数据由\r\n作为结束字符,空行则是仅仅是字符\r\n。因此,可以通过查找\r\n将报文拆解成单独的行进行解析

在报文中,请求头和空行的处理使用的同一个函数,这里通过判断当前的text首位是不是\0字符,若是,则表示当前处理的是空行,若不是,则表示当前处理的是请求头。判断是空行还是请求头,若是空行,进而判断content-length是否为0,如果不是0,表明是POST请求,则状态转移到CHECK_STATE_CONTENT,否则说明是GET请求,则报文解析结束。

(2)HTTP响应报文:状态行、响应头部、响应空行、响应正文

状态行:协议版本|空格|状态码|空格|状态码描述|回车符|换行符

8、HTTP常用的请求方法

(1)HTTP1.0定义了三种请求方法: GET, POST 和 HEAD方法。

GET:请求获取资源

POST:提交或修改数据

HEAD:获得报文首部,与 GET 方法类似,只是不返回报文主体,一般用于验证 URI 是否有效。

(2)HTTP1.1新增了五种请求方法:OPTIONS, PUT, DELETE, TRACE 和 CONNECT 方法。

OPTIONS:可使服务器传回该资源所支持的所有 HTTP 请求方法。

PUT:从客户端向服务器上传的数据取代指定的文件

DELETE:请求服务器删除指定的文件。

TRACE :追踪路径。回显服务器收到的请求,主要用于测试或诊断。

CONNECT:HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器,通常用于SSL加密服务器的链接。

(3)HTTP2.0 新的二进制格式(Binary Format),HTTP1.x的解析是基于文本。基于文本协议的格式解析存在缺陷:文本的表现形式有多样性,要做到健壮性考虑的场景必然很多,二进制则不同,只认0和1的组合。

9、HTTP的协议版本

http协议目前有4个版本,其中1.0和1.1版本在互联网上被广泛使用,2.0版本目前应用很少,是下一代的http协议。

http/0.9版本:原型版本,功能简陋,只有一个命令GET,服务器只能回应HTML格式字符串,该版本已过时。

http/1.0版本:短连接

  • 任何格式的内容都可以发送,这使得互联网不仅可以传输文字,还能传输图像、视频、二进制等文件。
  • 除了GET命令,还引入了POST命令和HEAD命令。
  • 头信息是 ASCII码,而后面数据可为任何格式。服务器回应时会告诉客户端,数据是什么格式,即Content-Type字段的作用。Content-Type值:text/xml image/jpeg audio/mp3
  • 每个TCP连接只能发送一个请求,发送数据完毕,连接就关闭,如果还要请求其他资源,就必须再新建一个连接

HTTP1.1版本:是目前最为主流的http协议版本

  • 引入了持久连接( persistent connection),即TCP连接默认不关闭,可以被多个请求复用
  • 引入了管道机制(pipelining),即在同一个TCP连接里,客户端可以同时发送多个请求
  • 新增方法:PUT、 PATCH、 OPTIONS、 DELETE

HTTP2.0版本:发布于2015年,目前应用还比较少

  • http/2是一个彻底的二进制协议,头信息和数据体都是二进制,并且统称为"帧"(frame):头信息帧和数据帧。
  • 复用TCP连接,在一个连接里,客户端和浏览器都可以同时发送多个请求或回应,且不用按顺序一一对应,避免了队头堵塞的问题。
  • HTTP/2 允许服务器未经请求,主动向客户端发送资源,即服务器推送。
  • 引入头信息压缩机制(header compression),头信息使用gzip或compress压缩后再发送;客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,不发送同样字段,只发送索引号,提高速度。

10、HTTP常见状态码及使用场景

1xx消息——请求已被服务器接收,继续处理

         101 切换请求协议,从 HTTP 切换到 WebSocket

2xx成功——请求已成功被服务器接收、理解、并接受

         200 请求成功,有响应体

3xx重定向——需要后续操作才能完成这一请求

         300 可选重定向
         301 永久重定向:会缓存
         302 临时重定向:不会缓存
         304 协商缓存命中

4xx请求错误——请求含有语法错误或者无法被执行

         400 请求报文存在语法错误
         404 资源未找到
         403 服务器禁止访问

5xx服务器错误——服务器在处理某个正确请求时发生错误

         500 服务器端错误
         503 服务器繁忙
         504 网关超时

11、HTTP状态码301和302的区别

301 永久重定向:页面永久性转移,表示为资源或页面永久性地转移到了另一个位置。

(1)用于防止收藏夹中的旧地址因网页扩展名改变而出错

(2)用于多个域名跳转至同一域名

302 临时重定向:页面暂时性转移,表示资源或页面暂时转移到另一个位置

用作网址劫持,容易导致网站降权,严重时网站会被封掉,不推荐使用

12、一次完整 HTTP 请求所经历的步骤

(当我们在 web 浏览器的地址栏中输入:www.baidu.com,然后回车,到底发生了什么?)

由域名→ IP 地址 寻找 IP 地址的过程依次经过了浏览器缓存、系统缓存、hosts 文件、路由器缓存、 递归搜索根域名服务器(DNS解析)。

建立 TCP/IP 连接(三次握手具体过程)。

由浏览器发送一个 HTTP 请求。

经过路由器的转发,通过服务器的防火墙,该 HTTP 请求到达了服务器。

服务器处理该 HTTP 请求,返回一个 HTML 文件。

浏览器解析该 HTML 文件,并且显示在浏览器端。

服务器关闭 TCP 连接(四次挥手具体过程)。

13、HTTP与HTTPS的缺点,以及区别

(1)HTTP 的不足

窃听风险: 通信使用明文(不加密),内容可能会被窃听;

冒充风险: 不验证通信方的身份,因此有可能遭遇伪装;

篡改风险: 无法证明报文的完整性,所以有可能已遭篡改;

(2)HTTPS 的缺点

HTTPS 协议多次握手,导致页面的加载时间延长近 50%;

HTTPS 连接缓存不如 HTTP 高效,会增加数据开销和功耗;

SSL 涉及到的安全算法会消耗 CPU 资源,对服务器资源消耗较大;

(3)区别

端口不同:HTTP 和 HTTPS 使用的是完全不同的连接方式,用的端口也不一样,前者是 80,后者是 443;

资源消耗:HTTP 是超文本传输协议,信息是明文传输,HTTPS 则是具有安全性的 ssl 加密传输协议,需要消耗更多的 CPU 和内存资源

开销:HTTPS 协议需要到 CA 申请证书,一般免费证书很少,需要交费;

安全性:HTTP 的连接很简单,是无状态的;HTTPS 协议是由 TLS+HTTP 协议构建的可进行加密传输、身份认证的网络协议,比 HTTP 协议安全

14、HTTP报文处理流程

· 浏览器端发出http连接请求,主线程创建http对象接收请求并将所有数据读入对应buffer,将该对象插入任务队列,工作线程从任务队列中取出一个任务进行处理。

· 工作线程取出任务后,调用process_read函数,通过主、从状态机对请求报文进行解析。

· 解析完之后,跳转do_request函数生成响应报文,通过process_write写入buffer,返回给浏览器端。

定时器相关

1、为什么要用定时器?

为了定期删除非活跃事件,防止连接资源的浪费。

非活跃,是指浏览器与服务器端建立连接后,长时间不交换数据,一直占用服务器端的文件描述符,导致连接资源的浪费。

定时事件,是指固定一段时间之后触发某段代码,由该段代码处理一个事件,如从内核事件表删除事件,并关闭文件描述符,释放连接资源。

2、说一下定时器的工作原理

定时器利用结构体将定时事件进行封装起来。定时事件,即定期检测非活跃连接。

服务器主循环为每一个连接创建一个定时器,并对每个连接进行定时。另外,利用升序双向链表将所有定时器串联起来,利用alarm函数周期性地触发SIGALRM信号,信号处理函数利用管道通知主循环,主循环接收到该信号后对升序链表上所有定时器进行处理,若该段时间内没有交换数据,则将该连接关闭,释放所占用的资源。(信号处理函数仅仅发送信号通知程序主循环,将信号对应的处理逻辑放在程序主循环中,由主循环执行信号对应的逻辑代码。)

信号通知的逻辑:创建管道,其中管道写端写入信号值,管道读端通过I/O复用系统监测读事

为什么管道写端要非阻塞?

send是将信息发送给套接字缓冲区,如果缓冲区满了,则会阻塞,这时候会进一步增加信号处理函数的执行时间,为此,将其修改为非阻塞。

3、定时任务处理函数的逻辑

使用统一事件源,SIGALRM信号每次被触发,主循环中调用一次定时任务处理函数,处理链表容器中到期的定时器。

(1)链表容器是升序排列,当前时间小于定时器的超时时间,后面的定时器也没有到期

(2)当前定时器到期,则调用回调函数,执行定时事件

(3)将处理后的定时器从链表容器中删除,并重置头结点

若有数据传输,则将定时器往后延迟3个单位

4、升序双向链表,删除和添加的时间复杂度说一下?还可以优化吗?

删除定时器的时间复杂度是O(1),添加定时器的时间复杂度是O(n)(刚好添加在尾节点时)。

缺点:每次以固定的时间间隔触发SIGALRM信号,调用定时任务处理函数处理超时连接会造成一定的触发浪费。举个例子,若当前的TIMESLOT=5,即每隔5ms触发一次SIGALRM,跳出循环执行定时任务处理函数,这时如果当前即将超时的任务距离现在还有20ms,那么在这个期间,SIGALRM信号被触发了4次,定时任务处理函数也被执行了4次,可是在这4次中,前三次触发都是无意义的。

(1)在双向链表的基础上优化:

在添加新定时器时,除了检测新定时器是否小于头节点定时器,还应该检测是否大于尾节点定时器的时间,都不符合再使用常规插入。

(2)不使用双向链表优化:最小堆。

5、最小堆优化?说一下时间复杂度和工作原理

时间复杂度:添加:O(logn), 删除:O(1)

工作原理:

将所有定时器中超时时间最小的一个定时器的超时值作为定时任务处理函数的定时值。这样,一旦定时任务处理函数被调用,超时时间最小的定时器必然到期,我们就可以在定时任务处理函数中处理该定时器。然后,再次从剩余的定时器中找出超时时间最小的一个(堆),并将这段最小时间设置为下一次定时任务处理函数的定时值。如此反复,就实现了较为精确的定时。

日志相关

1、说下你的日志系统的运行机制?

使用单例模式创建日志系统,对服务器运行状态、错误信息和访问数据进行记录,该系统可以实现按天分类,超行分类功能,可以根据实际情况分别使用同步和异步写入两种方式。

其中异步写入方式,将生产者-消费者模型封装为阻塞队列,创建一个写线程,工作线程将要写的内容push进队列,写线程从队列中取出内容,写入日志文件。

超行、按天分文件逻辑,具体的,

日志写入前会判断当前day是否为创建日志的时间,行数是否超过最大行限制
若为创建日志时间,写入日志,否则按当前时间创建新log,更新创建时间和行数
若行数超过最大行限制,在当前日志的末尾加count/max_lines为后缀创建新log

2、为什么要异步?和同步的区别是什么?

生产者-消费者模型,并发编程中的经典模型。

以多线程为例,为了实现线程间数据同步,生产者线程与消费者线程共享一个缓冲区,其中生产者线程往缓冲区中push消息,消费者线程从缓冲区中pop消息。

阻塞队列,将生产者-消费者模型进行封装,使用循环数组实现队列,作为两者共享的缓冲区。

异步日志,将所写的日志内容先存入阻塞队列,写线程从阻塞队列中取出内容,写入日志。

可以提高系统的并发性能。

同步日志,日志写入函数与工作线程串行执行,由于涉及到I/O操作,当单条日志比较大的时候,同步模式会阻塞整个处理流程,服务器所能处理的并发能力将有所下降,尤其是在峰值的时候,写日志可能成为系统的瓶颈。

写入方式通过初始化时是否设置队列大小(表示在队列中可以放几条数据)来判断,若队列大小为0,则为同步,否则为异步。

若异步,则将日志信息加入阻塞队列,同步则加锁向文件中写

3、关于该项目用到的设计模式

(1)单例模式:单例对象的类只能允许一个实例存在,并提供一个访问它的全局访问点,该实例被所有程序模块共享。主要解决一个全局使用的类频繁的创建和销毁的问题,是一种创建型模式,提供了一种创建对象的最佳方式。

(2)单例模式三要素:

    1)单例类只能有一个实例。

    2)单例类必须自己创建自己的唯一实例。

    3)单例类必须给所有其他对象提供这一实例。

(3)单例设计模式的优缺点
优点:
1 )单例模式可以保证内存里 只有一个实例 , 减少了内存的开销 。
2 )可以避免对资源的 多重占用 。 (比如写文件操作)
3 )单例模式设置全局访问点,可以优化和共享资源的访问。
缺点:
1 )单例模式一般没有接口, 不能继承, 扩展困难 。 如果要扩展,则除了修改原来的代码,没有第二种途径,违背开闭原则。
2 )在并发测试中,单例模式 不利于代码调试 。 在调试过程中,如果单例中的代码没有执行完,也不能模拟生成一个新的对象。
3 )单例模式的功能代码通常写在一个类中, 如果功能设计不合理,则很容易违背单一职责原则。
(4)C++ 单例设计模式的实现 两步骤
1 ) 私有化 构造函数 ,这样别处的代码就无法通过调用该类的构造函数来实例化该类的对象,只 有通过该类提供的静态方法来得到 该类的唯一实例 ;
2 ) 通过局部静态变量,利用其只初始化一次的特点,返回静态对象成员。
(5) 单例设计模式的种类
1 )懒汉式:获取该类的对象 时 才创建该类的实例
2 )饿汉式:获取该类的对象之 前 已经创建好该类的实例
(6)手撕单例模式
(懒汉模式)

class single{
private:
    single(){}   // 私有化构造函数
    ~single(){}  
public:
    // 公有静态方法获取实例
    static single* getinstance();
};
single* single::getinstance(){
    static single obj;
    return &obj;
}

使用函数内的局部静态对象无需加锁和解锁,因为C++11后编译器可以保证内部静态变量的线程安全性

(懒汉模式)加锁版本

class single{
private:
    static pthread_mutex_t lock;
    single(){
        pthread_mutex_init(&lock, NULL);
    }
    ~single(){}

public:
    static single* getinstance();

};
pthread_mutex_t single::lock;
single* single::getinstance(){
    pthread_mutex_lock(&lock);
    static single obj;
    pthread_mutex_unlock(&lock);
    return &obj;
}

(饿汉模式)

class single{
private:
    static single* p;
    single(){}
    ~single(){}
public:
    static single* getinstance();
};

single* single::p = new single();
single* single::getinstance(){
    return p;
}

饿汉模式不需要用锁,就可以实现线程安全。原因在于,在程序运行时就定义了对象,并对其初始化。之后,不管哪个线程调用成员函数getinstance(),都只是返回一个对象的指针

4、现在你要监控一台服务器的状态,输出监控日志,请问如何将该日志分发到不同的机器上?(消息队列)

数据库登录注册相关

1、 什么是数据库连接池,为什么要创建连接池?

(1)池是资源的容器,本质上是对资源的复用。

当系统开始处理客户请求的时候,如果它需要相关的资源,可以直接从池中获取,无需动态分配;当服务器处理完一个客户连接后,可以把相关的资源放回池中,无需执行系统调用释放资源。

(2)若系统需要频繁访问数据库,则需要频繁创建和断开数据库连接,而创建数据库连接是一个很耗时的操作,也容易对数据库造成安全隐患。

在程序初始化的时候,集中创建多个数据库连接,并把他们集中管理,供程序使用,可以保证较快的数据库读写速度,更加安全可靠。

(3)使用单例模式和链表创建数据库连接池,实现对数据库连接资源的复用。

连接池的功能主要有:初始化,获取连接、释放连接,销毁连接池

连接池中的多线程使用信号量进行通信,使用互斥锁进行同步。

数据库连接的获取与释放通过RAII机制封装,避免手动释放。

RAII机制

RAII全称是“Resource Acquisition is Initialization”,直译过来是“资源获取即初始化”.
RAII的核心思想是将资源或者状态与对象的生命周期绑定,通过C++的语言机制,实现资源和状态的安全管理,智能指针是RAII最好的例子
具体来说:构造函数的时候初始化获取资源,析构函数释放资源

2、登录说一下?(登录注册是POST请求)

将数据库中的用户名和密码载入到服务器的map中来,map中的key为用户名,value为密码

服务器端解析浏览器的请求报文,当解析为POST请求时,提取出请求报文的消息体的用户名和密码。

POST请求中最后是用户名和密码,用&隔开。分隔符&,前是用户名,后是密码。

登录:将浏览器输入的用户名和密码在数据库中查找,直接判断。

注册:往数据库中插入数据,需要判断是否有重复的用户名。

最后进行页面跳转

通过m_url定位/所在位置,根据/后的第一个字符,使用分支语句实现页面跳转。具体的,

0 — 跳转注册页面,GET
1 — 跳转登录页面,GET
5 — 显示图片页面,POST
6 — 显示视频页面,POST
7 — 显示关注页面,POST

3、登录验证怎么写的?用户登录需要考虑什么?

4、你这个保存状态了吗?如果要保存,你会怎么做?(cookie和session)

5、登录中的用户名和密码你是load到本地,然后使用map匹配的,如果有10亿数据,即使load到本地后hash,也是很耗时的,你要怎么优化?

6、用的mysql啊,redis了解吗?用过吗?

压测相关

1、服务器并发量测试过吗?怎么测试的?

补充知识:

系统吞吐量几个重要参数:QPS(TPS)、并发数、响应时间

TPS:Transactions Per Second,即服务器每秒响应的事务数

QPS:每秒查询率,每秒的响应请求数量

并发数: 系统同时处理的request/事务数

响应时间: 一般取平均响应时间

关系:QPS(TPS)= 并发数/平均响应时间

压力测试:每分响应请求数pages/min 和 每秒传输数据量bytes/sec

使用Webbench对服务器进行压力测试,创建1000个客户端,并发访问服务器10s,正常情况下有接近8万个HTTP请求访问服务器。

2、webbench是什么?介绍一下原理

WebBench是一款在Linux下使用非常简单的压力测试工具。

原理:WebBench首先fork出多个子进程,每个子进程都循环做web访问测试。子进程把访问的结果通过pipe告诉父进程,父进程做最终的统计结果。Webbench最多可以模拟3万个并发连接去测试网站的负载能力。

-c :子进程的个数,即并发数

-t :运行webbench的时间

3、测试的时候有没有遇到问题?

Bug:使用Webbench对服务器进行压力测试,创建1000个客户端,并发访问服务器10s,正常情况下有接近8万个HTTP请求访问服务器。

结果显示仅有7个请求被成功处理,0个请求处理失败,服务器也没有返回错误。此时,从浏览器端访问服务器,发现该请求也不能被处理和响应,必须将服务器重启后,浏览器端才能访问正常。

解决办法:

排查:

通过查询服务器运行日志,对服务器接收HTTP请求连接,HTTP处理逻辑两部分进行排查。

日志中显示,7个请求报文为:GET / HTTP/1.0的HTTP请求被正确处理和响应,排除HTTP处理逻辑错误。重点放在接收HTTP请求连接部分。其中,服务器端接收HTTP请求的连接步骤为socket -> bind -> listen -> accept

错误原因:错误使用epoll的ET模式。

ET边缘触发模式

epoll_wait检测到文件描述符有事件发生,则将其通知给应用程序,应用程序必须立即处理该事件。

必须要一次性将数据读取完,使用非阻塞I/O,读取到出现eagain。

当连接较少时,队列不会变满,即使listenfd设置成ET非阻塞,不使用while一次性读取完,也不会出现Bug。

若此时1000个客户端同时对服务器发起连接请求,连接过多会造成established 状态的连接队列变满。但accept并没有使用while一次性读取完,只读取一个。因此,连接过多导致TCP就绪队列中剩下的连接都得不到处理,同时新的连接也不会到来。

解决方案

将listenfd设置成LT阻塞,或者ET非阻塞模式下while包裹accept即可解决问题。

综合能力

1、你的项目解决了哪些其他同类项目没有解决的问题?

2、说一下前端发送请求后,服务器处理的过程,中间涉及哪些协议?

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
高级开发面试题一般包括更加细致和高级的技术问,涉及到开发者在实际项目中的运用和解决问的能力。以下是一个可能的回答: 在高级开发面试中,面试官可能会问到关于具体开发框架或者技术栈的问。例如,他们可能会要求我解释MVC框架以及我在实际项目中的应用经验。我会回答MVC的全称是Model-View-Controller,它是一种软件开发架构模式,将应用程序分为三个主要部分:模型,视图和控制器。模型负责处理数据逻辑,视图负责显示数据和界面,控制器处理用户的请求并负责协调模型和视图之间的交互。对于MVC的应用经验,我可以分享我在过去项目中使用MVC框架的经历,以及如何通过它来组织和管理代码的优势。 除了框架和技术栈问,高级开发面试还可能涉及到代码质量保证和性能优化方面的问。例如,面试官可能会问我在实际项目中如何确保代码的质量和可维护性。我会谈到我在代码开发过程中遵循SOLID原则,编写可读性强的代码,并使用单元测试和集成测试来确保代码的正确性和可靠性。另外,我还会提到我在代码评审中的经验,以及如何利用代码静态分析工具和自动化测试工具来帮助检测潜在的问。 性能优化也是一个重要的话。如果被问到如何提高应用程序的性能,我会谈到我在过去项目中使用的一些策略,如对数据库进行索引优化,减少网络传输量,优化算法和数据结构等。 总的来说,高级开发面试题旨在测试开发者在实际项目中的运用和解决问的能力。通过提出关于框架、技术栈、代码质量保证和性能优化等方面的问,面试官可以更全面地了解开发者的能力和经验,并决定是否适合担任高级开发职位。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值