赞
踩
本文为博主2020年秋招提前批的c/c++后端开发面经整理,包括C/C++语言基础,计网,数据库,linux,操作系统,场景题,智力题和hr常问题。面试问题来自前人的工作和博主面试时遇到的值得记录的问题,其中面试题答案多为博主自行解答(并且面试的时候也基本是这样回答的),回答中都附上了参考资料的链接,全文共七万余字,仅供大家作为面试准备材料的参考。
希望大家在准备面试的时候都能建立一个属于自己的题库,自己从网上寻找问题,然后自己进行解答,并且记录下来,这样在面试的时候就能行云流水般的回答上来。因此我把这份面经的源文件 markdown格式分享给大家:网盘链接
提取码:229s
希望大家在这份面经上添砖加瓦或从头开始,建一个属于自己的面试题库!
附上博主2020秋招进展:
TP-LINK提前批 软开深圳:一面二面三面hr面 offer
shopee 后端深圳:笔试一面二面hr面 offer
快手提前批 客户端 深圳:一面二面三面hr面 offer
腾讯提前批 c++ teg 深圳:一面二面hr面 offer
百度提前批 c++ 北京:一面二面三面+hr面测评 offer
阿里C++ 北京:笔试一面二面三面交叉面一面二面三面hr面 offer
字节跳动提前批 后端深圳:一面二面 挂
招银网络提前批 软开深圳:一面二面三面 挂
京东提前批 后端 京东零售:简历 挂
猿辅导 服务端:笔试 挂
京东正式批 c++:笔试一面二面 放弃
网易互娱 游戏研发广州: 笔试 放弃
哔哩哔哩 后端上海:笔试 放弃
虎牙提前批 后端广州:放弃
拼多多提前批 后端:笔试 放弃
博主在投递秋招提前批的时候简历上是没有实习经历的。大三的暑假在参加保研夏令营;研一春招的时候手头有一个做的差不多了的科研课题,当时选择了相信导师跟着他改论文投会议,但是事实证明这个选择是错误的,既没有投中,又错过了春招找实习的机会。不过在春招的末尾匆匆面了几家,都挂了,最接近offer的是腾讯WXG的面试,已经hr面结束,但是最后泡池子泡挂了。听说实习对秋招十分重要,当时投递秋招提前批的时候我也是很绝望的,不过求生欲逼着我不断投递不断面试,最后也拿到了好几个offer。因此没有实习并不是致命缺点,只不过有的话更好,跟面试官聊实习和项目就可以聊很久,因为一场技术面试一般一个小时,聊项目聊掉半个小时,剩下半个小时手撕算法,就不会问那么多奇奇怪怪的基础知识啦。
因为一般情况下不会每个公司能够通过简历筛选的,很多人会因为害怕被挂或者嫌麻烦而不敢投递简历, 所以我推荐首先在心里给自己定下一个数量上的指标,先投满20个,但凡有机会的都无脑投就是了,其他的情况以后再考虑。先就业再择业。
我在秋招之前用了两个月的时间进行准备,但并不是两个月过去了才投简历,而是准备的过程中一有机会就投,因为从投递到发起笔试/面试通常要隔两个星期,等到你完全准备好了,别人已经面的热火朝天了。
栈快一点。因为操作系统会在底层对栈提供支持,会分配专门的寄存器存放栈的地址,栈的入栈出栈操作也十分简单,并且有专门的指令执行,所以栈的效率比较高也比较快。而堆的操作是由C/C++函数库提供的,在分配堆内存的时候需要一定的算法寻找合适大小的内存。并且获取堆的内容需要两次访问,第一次访问指针,第二次根据指针保存的地址访问内存,因此堆比较慢。
在new一个对象的时候,首先会调用malloc为对象分配内存空间,然后调用对象的构造函数。delete会调用对象的析构函数,然后调用free回收内存。
new与malloc都会分配空间,但是new还会调用对象的构造函数进行初始化,malloc需要给定空间大小,而new只需要对象名
详见:https://blog.csdn.net/leikun153/article/details/80612130
包括但不限于:
delete只会调用一次析构函数,而delete[]会调用每个成员的析构函数
用new分配的内存用delete释放,用new[]分配的内存用delete[]释放
包括但不限于:
包括但不限于:
联系:它们都是定义常量的一种方法。
区别:
static的意思是静态的,可以用来修饰变量,函数和类成员。
变量:被static修饰的变量就是静态变量,它会在程序运行过程中一直存在,会被放在静态存储区。局部静态变量的作用域在函数体中,全局静态变量的作用域在这个文件里。
函数:被static修饰的函数就是静态函数,静态函数只能在本文件中使用,不能被其他文件调用,也不会和其他文件中的同名函数冲突。
类:而在类中,被static修饰的成员变量是类静态成员,这个静态成员会被类的多个对象共用。被static修饰的成员函数也属于静态成员,不是属于某个对象的,访问这个静态函数不需要引用对象名,而是通过引用类名来访问。
【note】静态成员函数要访问非静态成员时,要用过对象来引用。局部静态变量在函数调用结束后也不会被回收,会一直在程序内存中,直到该函数再次被调用,它的值还是保持上一次调用结束后的值。
注意和const的区别。const强调值不能被修改,而static强调唯一的拷贝,对所有类的对象都共用。
class A {};
int main(){
cout<<sizeof(A)<<endl;// 输出 1;
A a;
cout<<sizeof(a)<<endl;// 输出 1;
return 0;
}
空类的大小是1, 在C++中空类会占一个字节,这是为了让对象的实例能够相互区别。具体来说,空类同样可以被实例化,并且每个实例在内存中都有独一无二的地址,因此,编译器会给空类隐含加上一个字节,这样空类实例化之后就会拥有独一无二的内存地址。当该空白类作为基类时,该类的大小就优化为0了,子类的大小就是子类本身的大小。这就是所谓的空白基类最优化。
空类的实例大小就是类的大小,所以sizeof(a)=1字节,如果a是指针,则sizeof(a)就是指针的大小,即4字节。
class A { virtual Fun(){} };
int main(){
cout<<sizeof(A)<<endl;// 输出 4(32位机器)/8(64位机器);
A a;
cout<<sizeof(a)<<endl;// 输出 4(32位机器)/8(64位机器);
return 0;
}
因为有虚函数的类对象中都有一个虚函数表指针 __vptr,其大小是4字节
class A { static int a; };
int main(){
cout<<sizeof(A)<<endl;// 输出 1;
A a;
cout<<sizeof(a)<<endl;// 输出 1;
return 0;
}
静态成员存放在静态存储区,不占用类的大小, 普通函数也不占用类大小
class A { int a; };
int main(){
cout<<sizeof(A)<<endl;// 输出 4;
A a;
cout<<sizeof(a)<<endl;// 输出 4;
return 0;
}
class A { static int a; int b; };;
int main(){
cout<<sizeof(A)<<endl;// 输出 4;
A a;
cout<<sizeof(a)<<endl;// 输出 4;
return 0;
}
静态成员a不占用类的大小,所以类的大小就是b变量的大小 即4个字节
C++ STL从广义来讲包括了三类:算法,容器和迭代器。
STL中的hash表就unordered_map。使用的是哈希进行实现(注意与map的区别)。它记录的键是元素的哈希值,通过对比元素的哈希值来确定元素的值。
unordered_map的底层实现是hashtable,采用开链法(也就是用桶)来解决哈希冲突,当桶的大小超过8时,就自动转为红黑树进行组织。
STL中的vector是封装了动态数组的顺序容器。不过与动态数组不同的是,vector可以根据需要自动扩大容器的大小。具体策略是每次容量不够用时重新申请一块大小为原来容量两倍的内存,将原容器的元素拷贝至新容器,并释放原空间,返回新空间的指针。
在原来空间不够存储新值时,每次调用push_back方法都会重新分配新的空间以满足新数据的添加操作。如果在程序中频繁进行这种操作,还是比较消耗性能的。
如果需要频繁插入,最好先指定vector的大小,因为vector在容器大小不够用的时候会重新申请一块大小为原容器两倍的空间,并将原容器的元素拷贝到新容器中,并释放原空间,这个过程是十分耗时和耗内存的。频繁调用push_back()会使得程序花费很多时间在vector扩容上,会变得很慢。这种情况可以考虑使用list。
vector和数组类似,拥有一段连续的内存空间。vector申请的是一段连续的内存,当插入新的元素内存不够时,通常以2倍重新申请更大的一块内存,将原来的元素拷贝过去,释放旧空间。因为内存空间是连续的,所以在进行插入和删除操作时,会造成内存块的拷贝,时间复杂度为o(n)。
list是由双向链表实现的,因此内存空间是不连续的。只能通过指针访问数据,所以list的随机存取非常没有效率,时间复杂度为o(n); 但由于链表的特点,能高效地进行插入和删除。
vector拥有一段连续的内存空间,能很好的支持随机存取,因此vector::iterator支持“+”,“+=”,“<”等操作符。
list的内存空间可以是不连续,它不支持随机访问,因此list::iterator则不支持“+”、“+=”、“<”等
vector::iterator和list::iterator都重载了“++”运算符。
总之,如果需要高效的随机存取,而不在乎插入和删除的效率,使用vector;
如果需要大量的插入和删除,而不关心随机存取,则应使用list。
详见:https://blog.csdn.net/weixin_30379911/article/details/99497160
详见:https://blog.csdn.net/qq_43152052/article/details/98889139
在C++中,内存分成5个区,他们分别是堆、栈、全局/静态存储区和常量存储区和代码区。
关于这个有很多种说法,有的会增加一个自由存储区,存放malloc分配得到的内存,与堆相似。
面向对象的三大特性是:封装,继承和多态。
C++ 多态包括编译时多态和运行时多态,编译时多态体现在函数重载和模板上,运行时多态体现在虚函数上。
C++的虚函数是实现多态的机制。它是通过虚函数表实现的,虚函数表是每个类中存放虚函数地址的指针数组,类的实例在调用函数时会在虚函数表中寻找函数地址进行调用,如果子类覆盖了父类的函数,则子类的虚函数表会指向子类实现的函数地址,否则指向父类的函数地址。一个类的所有实例都共享同一张虚函数表。
详见:C++虚函数表剖析
编译器处理虚函数的方法是:
如果类中有虚函数,就将虚函数的地址记录在类的虚函数表中。派生类在继承基类的时候,如果有重写基类的虚函数,就将虚函数表中相应的函数指针设置为派生类的函数地址,否则指向基类的函数地址。
为每个类的实例添加一个虚表指针(vptr),虚表指针指向类的虚函数表。实例在调用虚函数的时候,通过这个虚函数表指针找到类中的虚函数表,找到相应的函数进行调用。
详见:虚函数的作用及其底层实现机制
首先析构函数可以为虚函数,当析构一个指向子类的父类指针时,编译器可以根据虚函数表寻找到子类的析构函数进行调用,从而正确释放子类对象的资源。
如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除指向子类的父类指针时,只会调用父类的析构函数而不调用子类析构函数,这样就会造成子类对象析构不完全造成内存泄漏。
1)因为创建一个对象时需要确定对象的类型,而虚函数是在运行时确定其类型的。而在构造一个对象时,由于对象还未创建成功,编译器无法知道对象的实际类型,是类本身还是类的派生类等等
2)虚函数的调用需要虚函数表指针,而该指针存放在对象的内存空间中;若构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,更没有虚函数表地址用来调用虚函数即构造函数了
在构造函数中调用虚函数,由于当前对象还没有构造完成,此时调用的虚函数指向的是基类的函数实现方式。
在析构函数中调用虚函数,此时调用的是子类的函数实现方式。
纯虚函数是只有声明没有实现的虚函数,是对子类的约束,是接口继承
包含纯虚函数的类是抽象类,它不能被实例化,只有实现了这个纯虚函数的子类才能生成对象
使用场景:当这个类本身产生一个实例没有意义的情况下,把这个类的函数实现为纯虚函数,比如动物可以派生出老虎兔子,但是实例化一个动物对象就没有意义。并且可以规定派生的子类必须重写某些函数的情况下可以写成纯虚函数。
静态绑定也就是将该对象相关的属性或函数绑定为它的静态类型,也就是它在声明的类型,在编译的时候就确定。在调用的时候编译器会寻找它声明的类型进行访问。
动态绑定就是将该对象相关的属性或函数绑定为它的动态类型,具体的属性或函数在运行期确定,通常通过虚函数实现动态绑定。
浅拷贝就是将对象的指针进行简单的复制,原对象和副本指向的是相同的资源。
而深拷贝是新开辟一块空间,将原对象的资源复制到新的空间中,并返回该空间的地址。
深拷贝可以避免重复释放和写冲突。例如使用浅拷贝的对象进行释放后,对原对象的释放会导致内存泄漏或程序崩溃。
对象复用指得是设计模式,对象可以采用不同的设计模式达到复用的目的,最常见的就是继承和组合模式了。
零拷贝指的是在进行操作时,避免CPU从一处存储拷贝到另一处存储。在Linux中,我们可以减少数据在内核空间和用户空间的来回拷贝实现,比如通过调用mmap()来代替read调用。
用程序调用mmap(),磁盘上的数据会通过DMA被拷贝的内核缓冲区,接着操作系统会把这段内核缓冲区与应用程序共享,这样就不需要把内核缓冲区的内容往用户空间拷贝。应用程序再调用write(),操作系统直接将内核缓冲区的内容拷贝到socket缓冲区中,这一切都发生在内核态,最后,socket缓冲区再把数据发到网卡去。
C++中的构造函数主要有三种类型:默认构造函数、重载构造函数和拷贝构造函数
如
void func(Dog dog){};
如
Dog func(){ Dog d; return d;}
详见:C++拷贝构造函数详解
因为结构体的成员可以有不同的数据类型,所占的大小也不一样。同时,由于CPU读取数据是按块读取的,内存对齐可以使得CPU一次就可以将所需的数据读进来。
对齐规则:
动态分配内存所开辟的空间,在使用完毕后未手动释放,导致一直占据该内存,即为内存泄漏。
造成内存泄漏的几种原因:
1)类的构造函数和析构函数中new和delete没有配套
2)在释放对象数组时没有使用delete[],使用了delete
3)没有将基类的析构函数定义为虚函数,当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确释放,因此造成内存泄露
4)没有正确的清楚嵌套的对象指针
避免方法:
C++中的智能指针有auto_ptr,shared_ptr,weak_ptr和unique_ptr。智能指针其实是将指针进行了封装,可以像普通指针一样进行使用,同时可以自行进行释放,避免忘记释放指针指向的内存地址造成内存泄漏。
coredump是程序由于异常或者bug在运行时异常退出或者终止,在一定的条件下生成的一个叫做core的文件,这个core文件会记录程序在运行时的内存,寄存器状态,内存指针和函数堆栈信息等等。对这个文件进行分析可以定位到程序异常的时候对应的堆栈调用信息。
以下例子在Linux上编写一段代码并导致segment fault 并产生core文件
mkdir coredumpTest
vim coredumpTest.cpp
在编辑器内键入
#include<stdio.h>
int main(){
int i;
scanf("%d",i);//正确的应该是&i,这里使用i会导致segment fault
printf("%d\n",i);
return 0;
}
编译
g++ coredumpTest.cpp -g -o coredumpTest
运行
./coredumpTest
使用gdb调试coredump
gdb [可执行文件名] [core文件名]
inline是内联的意思,可以定义比较小的函数。因为函数频繁调用会占用很多的栈空间,进行入栈出栈操作也耗费计算资源,所以可以用inline关键字修饰频繁调用的小函数。编译器会在编译阶段将代码体嵌入内联函数的调用语句块中。
1、内联函数在编译时展开,而宏在预编译时展开
2、在编译的时候,内联函数直接被嵌入到目标代码中去,而宏只是一个简单的文本替换。
3、内联函数可以进行诸如类型安全检查、语句是否正确等编译功能,宏不具有这样的功能。
4、宏不是函数,而inline是函数
5、宏在定义时要小心处理宏参数,一般用括号括起来,否则容易出现二义性。而内联函数不会出现二义性。
6、inline可以不展开,宏一定要展开。因为inline指示对编译器来说,只是一个建议,编译器可以选择忽略该建议,不对该函数进行展开。
7、宏定义在形式上类似于一个函数,但在使用它时,仅仅只是做预处理器符号表中的简单替换,因此它不能进行参数有效性的检测,也就不能享受C++编译器严格类型检查的好处,另外它的返回值也不能被强制转换为可转换的合适的类型,这样,它的使用就存在着一系列的隐患和局限性。
用template <typename T>关键字进行声明,接下来就可以进行模板函数和模板类的编写了
编译器会对函数模板进行两次编译:第一次编译在声明的地方对模板代码本身进行编译,这次编译只会进行一个语法检查,并不会生成具体的代码。在第二次时对代码进行参数替换后再进行编译,生成具体的函数代码。
成员初始化列表就是在类或者结构体的构造函数中,在参数列表后以冒号开头,逗号进行分隔的一系列初始化字段。如下:
class A{
int id;
string name;
FaceImage face;
A(int& inputID,string& inputName,FaceImage& inputFace):id(inputID),name(inputName),face(inputFace){} // 成员初始化列表
};
因为使用成员初始化列表进行初始化的话,会直接使用传入参数的拷贝构造函数进行初始化,省去了一次执行传入参数的默认构造函数的过程,否则会调用一次传入参数的默认构造函数。所以使用成员初始化列表效率会高一些。
另外,有三种情况是必须使用成员初始化列表进行初始化的:
自动类型推导auto:auto的自动类型推导用于从初始化表达式中推断出变量的数据类型。通过auto的自动类型推导,可以大大简化我们的编程工作
nullptr
:nullptr是为了解决原来C++中NULL的二义性问题而引进的一种新的类型,因为NULL实际上代表的是0,而nullptr是void*类型的
lambda表达式:它类似Javascript中的闭包,它可以用于创建并定义匿名的函数对象,以简化编程工作。Lambda的语法如下:
[函数对象参数](操作符重载函数参数)mutable或exception声明->返回值类型{函数体}
thread类和mutex类
新的智能指针 unique_ptr和shared_ptr
更多详见:https://blog.csdn.net/caogenwangbaoqiang/article/details/79438279
函数的调用过程:
1)从栈空间分配存储空间
2)从实参的存储空间复制值到形参栈空间
3)进行运算
形参在函数未调用之前都是没有分配存储空间的,在函数调用结束之后,形参弹出栈空间,清除形参空间。
数组作为参数的函数调用方式是地址传递,形参和实参都指向相同的内存空间,调用完成后,形参指针被销毁,但是所指向的内存空间依然存在,不能也不会被销毁。
当函数有多个返回值的时候,不能用普通的 return 的方式实现,需要通过传回地址的形式进行,即地址/指针传递。
四种强制类型转换操作符分别为:static_cast、dynamic_cast、const_cast、reinterpret_cast
特性与要点:
string继承自basic_string,其实是对char*进行了封装,封装的string包含了char*数组,容量,长度等等属性。
string可以进行动态扩展,在每次扩展的时候另外申请一块原空间大小两倍的空间(2*n),然后将原字符串拷贝过去,并加上新增的内容。
预处理,编译,汇编,链接
set,map的插入复杂度就是红黑树的插入复杂度,是log(N)。
unordered_set,unordered_map的插入复杂度是常数,最坏是O(N).
vector的插入复杂度是O(N),最坏的情况下(从头插入)就要对所有其他元素进行移动,或者扩容重新拷贝
声明是告诉编译器变量的类型和名字,不会为变量分配空间
定义就是对这个变量和函数进行内存分配和初始化。需要分配空间,同一个变量可以被声明多次,但是只能被定义一次
#define是预处理命令,在预处理是执行简单的替换,不做正确性的检查
typedef是在编译时处理的,它是在自己的作用域内给已经存在的类型一个别名
https://blog.csdn.net/YMY_mine/article/details/81180168
不是的,被free回收的内存会首先被ptmalloc使用双链表保存起来,当用户下一次申请内存的时候,会尝试从这些内存中寻找合适的返回。这样就避免了频繁的系统调用,占用过多的系统资源。同时ptmalloc也会尝试对小块内存进行合并,避免过多的内存碎片。
对比值传递,引用传参的好处:
1)在函数内部可以对此参数进行修改
2)提高函数调用和运行的效率(因为没有了传值和生成副本的时间和空间消耗)
如果函数的参数实质就是形参,不过这个形参的作用域只是在函数体内部,也就是说实参和形参是两个不同的东西,要想形参代替实参,肯定有一个值的传递。函数调用时,值的传递机制是通过“形参=实参”来对形参赋值达到传值目的,产生了一个实参的副本。即使函数内部有对参数的修改,也只是针对形参,也就是那个副本,实参不会有任何更改。函数一旦结束,形参生命也宣告终结,做出的修改一样没对任何变量产生影响。
用引用作为返回值最大的好处就是在内存中不产生被返回值的副本。
但是有以下的限制:
1)不能返回局部变量的引用。因为函数返回以后局部变量就会被销毁
2)不能返回函数内部new分配的内存的引用。虽然不存在局部变量的被动销毁问题,可对于这种情况(返回函数内部new分配内存的引用),又面临其它尴尬局面。例如,被函数返回的引用只是作为一 个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由new分配)就无法释放,造成memory leak
3)可以返回类成员的引用,但是最好是const。因为如果其他对象可以获得该属性的非常量的引用,那么对该属性的单纯赋值就会破坏业务规则的完整性。
https://www.cnblogs.com/zhuguanhao/p/6286145.html
友元提供了不同类的成员函数之间、类的成员函数和一般函数之间进行数据共享的机制。通过友元,一个不同函数或者另一个类中的成员函数可以访问类中的私有成员和保护成员。友元的正确使用能提高程序的运行效率,但同时也破坏了类的封装性和数据的隐藏性,导致程序可维护性变差。
1)友元函数
有元函数是定义在类外的普通函数,不属于任何类,可以访问其他类的私有成员。但是需要在类的定义中声明所有可以访问它的友元函数。
#include <iostream> using namespace std; class A { public: friend void set_show(int x, A &a); //该函数是友元函数的声明 private: int data; }; void set_show(int x, A &a) //友元函数定义,为了访问类A中的成员 { a.data = x; cout << a.data << endl; } int main(void) { class A a; set_show(1, a); return 0; }
一个函数可以是多个类的友元函数,但是每个类中都要声明这个函数。
2)友元类
友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。
但是另一个类里面也要相应的进行声明
#include <iostream> using namespace std; class A { public: friend class C; //这是友元类的声明 private: int data; }; class C //友元类定义,为了访问类A中的成员 { public: void set_show(int x, A &a) { a.data = x; cout<<a.data<<endl;} }; int main(void) { class A a; class C c; c.set_show(1, a); return 0; }
使用友元类时注意:
(1) 友元关系不能被继承。
(2) 友元关系是单向的,不具有交换性。若类B是类A的友元,类A不一定是类B的友元,要看在类中是否有相应的声明。
(3) 友元关系不具有传递性。若类B是类A的友元,类C是B的友元,类C不一定是类A的友元,同样要看类中是否有相应的申明
volatile的意思是“脆弱的”,表明它修饰的变量的值十分容易被改变,所以编译器就不会对这个变量进行优化(CPU的优化是让该变量存放到CPU寄存器而不是内存),进而提供稳定的访问。每次读取volatile的变量时,系统总是会从内存中读取这个变量,并且将它的值立刻保存。
STL中的sort是用快速排序和插入排序结合的方式实现的,stable_sort()是归并排序。
https://www.cnblogs.com/qingjiaowoxiaoxioashou/p/5874572.html
建立TCP服务器连接的过程中主要通过以下系统调用序列来获取某些函数,这些系统调用主要包括:socket(),bind(),listen(),accept(),send()和recv()。
详见:建立TCP 服务器的系统调用
socket() 创建套接字
bind() 绑定本机端口
connect() 建立连接 (TCP三次握手在调用这个函数时进行)
listen() 监听端口
accept() 接受连接
recv(), read(), recvfrom() 数据接收
send(), write(), sendto() 数据发送
close(), shutdown() 关闭套接字
使用close()时,只有当套接字的引用计数为0的时候才会终止连接,而用shutdown()就可以直接关闭连接
详见:网络编程Socket之TCP之close/shutdown详解
TCP连接与断开详解: https://www.cnblogs.com/felixzh/p/8359066.html
RIP“路由信息协议(Route Information Protocol)”的简写,主要传递路由信息,通过每隔30秒广播一次路由表,维护相邻路由器的位置关系,同时根据收到的路由表信息使用动态规划的方式计算自己的路由表信息。RIP是一个距离矢量路由协议,最大跳数为16跳,16跳以及超过16跳的网络则认为目标网络不可达。
OSPF:详见:https://zhuanlan.zhihu.com/p/41341540
因为UDP是无连接的协议,所以在传输层上无法保证可靠传输,要想实现可靠传输,只能从应用层实现。需要实现seq/ack机制,重传机制和窗口确认机制。
就要接收方收到UDP之后回复个确认包,发送方有个机制,收不到确认包就要重新发送,每个包有递增的序号,接收方发现中间丢了包就要发重传请求,当网络太差时候频繁丢包,防止越丢包越重传的恶性循环,要有个发送窗口的限制,发送窗口的大小根据网络传输情况调整,调整算法要有一定自适应性。
作者:姚冬
链接:https://www.zhihu.com/question/283995548/answer/661809748
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
注:单凭TCP是不能保证完整性的,要是有黑客伪造TCP包,是无法识别的。
TCP族的协议有HTTP,HTTPS,SMTP,TelNet,FTP等,UDP族的协议有DNS,DHCP等等。
详见:https://blog.csdn.net/qq_22080999/article/details/81105051
TCP的头部大致包括:源端口,目的端口,序号,确认号,偏移位,标志位,校验和等等
UDP的头部则包括:源端口,目的端口,长度,校验和。
IP数据包的头部包括:源IP地址,目的IP地址,协议,校验和,总长度等等
详见:https://blog.csdn.net/zhangliangzi/article/details/52554439
这里仅展示浏览器解析服务器响应的过程,URL解析和交互的完整过程在(9)
因为在链路层中帧的大小通常都有限制,比如在以太网中帧的最大大小(MTU)就是1500字节。如果IP数据包加上头部后大小超过1500字节,就需要分片。
IP分片和完整IP报文差不多拥有相同的IP头,16位ID域对于每个分片都是一致的,这样才能在重新组装的时候识别出来自同一个IP报文的分片。在IP头里面,16位识别号唯一记录了一个IP包的ID,具有同一个ID的IP分片将会重新组装;而13位片偏移则记录了某IP片相对整个包的位置;而这两个表中间的3位标志则标志着该分片后面是否还有新的分片。这三个标志就组成了IP分片的所有信息(将在后面介绍),接受方就可以利用这些信息对IP数据进行重新组织。
详见:https://blog.csdn.net/gettogetto/article/details/72851734
第一次握手:首先client给server发送连接请求报文,在这个报文中,包含了SYN=1,client_seq=任意值i,发送之后处于SYN-SENT状态,这是第一次握手
第二次握手:server端接收到了这个请求,并分配资源,同时给client返回一个ACK报文,这个报文中呢包含了这些字段,标志位SYN和ACK都为1,而小ack为i+1,此时位于SYN-RCVD状态,这是第二次握手
第三次握手:client收到server发来的ACK信息后呢,他会看到server发过来的小ack是i+1,这时他知道了server收到了消息,也给server回一个ACK报文,报文中同样包含了ACK=1这样的消息,同时呢,还包括了client_ack=k+1这样的字段,这样呢三次握手之后,连接就建立了,client进入established(已建立连接)状态
TCP断开连接通常是由一方主动,一方被动的,这里我们假设client主动,server被动
第一次挥手:当client没有数据要发送给server了,他会给server发送一个FIN报文,告诉server:“我已经没有数据要发给你了,但是你要是还想给我发数据的话,你就接着发,但是你得告诉我你收到我的关闭信息了”,这是第一次挥手,挥手之后client进入FIN_WAIT_1的第一阶段
第二次挥手:当server收到client发来的FIN报文后,告诉client:“我收到你的FIN消息了,但是你等我发完的”此时给client返回一个ACK信息,并且呢ack=seq+1,这是第二次挥手,挥手之后呢server进入CLOSE_WAIT阶段,而client收到之后处于FIN_WAIT_2第二阶段
第三次挥手:当server发完所有数据时,他会给client发送一个FIN报文,告诉client说“我传完数据了,现在要关闭连接了”,然后呢server变成LAST_ACK状态,等着client最后的ACK信息,这是第三次挥手
第四次挥手:当client收到这个FIN报文时,他会对这个消息进行确认,即给server发ACK信息,但是它不相信网络,怕server收不到信息,它会进入TIME_WAIT状态,万一server没收到ACK消息它可以可以重传,而当server收到这个ACK信息后,就正式关闭了tcp连接,处于CLOSED状态,而client等待了2MSL这样长时间后还没等到消息,它知道server已经关闭连接了,于是乎他自己也断开了,这是第四次挥手,这样tcp连接就断开了
见上
如果使用两次握手的话,三次握手中的最后一次缺失,服务器不能确认客户端的接收能力。
举两个例子,第一种是黑客会伪造大量SYN请求发送给服务器,服务器立即确认并建立连接,分配资源,但是这一系列连接并不是真实存在的,这大大浪费了服务器的资源并且阻塞了正常用户的连接,这种也叫SYN洪泛攻击。第二种是服务器返回给客户端的ACK数据包可能会在传输的过程中丢失,而客户端没有收到该ACK数据包而拒绝接收服务器接下来发送的数据,于是服务器一直在发送,客户端一直在拒绝,形成死锁。
TIME_WAIT是指四次挥手中客户端接收了服务端的FIN报文并发送ACK报文给服务器后,仍然需要等待2MSL时间的过程。虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可以最后一个ACK丢失。如果客户端发送的ACK发生丢失,服务器会再次发送FIN报文给客户端,所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。
(校序重流拥)
校验和
发送的数据包的二进制相加然后取反,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP将丢弃这个报文段和不确认收到此报文段。
确认应答+序列号
TCP给发送的每一个包进行编号,接收方对数据包进行排序,把有序数据传送给应用层。
超时重传
当TCP发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。
流量控制
TCP连接的每一方都有固定大小的缓冲空间,TCP的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。TCP使用的流量控制协议是可变大小的滑动窗口协议。
接收方有即时窗口(滑动窗口),随ACK报文发送
拥塞控制
当网络拥塞时,减少数据的发送。
发送方有拥塞窗口,发送数据前比对接收方发过来的即使窗口,取小
慢启动、拥塞避免、快速重传、快速恢复
所谓流量控制就是让发送方发送速率不要过快,让接收方来得及接收。利用TCP报文段中的窗口大小字段来控制发送方的发送窗口不大于接收方发回的窗口大小就可以实施流量控制。
考虑一种特殊的情况,就是接收方若没有缓存足够使用,就会发送零窗口大小的报文,此时发送放将发送窗口设置为0,停止发送数据。之后接收方有足够的缓存,发送了非零窗口大小的报文,但是这个报文在中途丢失的,那么发送方的发送窗口就一直为零导致死锁。
解决这个问题,TCP为每一个连接设置一个持续计时器(persistence timer)。只要TCP的一方收到对方的零窗口通知,就启动该计时器,周期性的发送一个零窗口探测报文段。对方就在确认这个报文的时候给出现在的窗口大小(注意:TCP规定,即使设置为零窗口,也必须接收以下几种报文段:零窗口探测报文段、确认报文段和携带紧急数据的报文段)。
详见 TCP-IP详解:滑动窗口SlidingWindow和TCP滑动窗口
TCP的滑动窗口用来控制接收方和发送方的发送速率,避免拥塞的发生。滑动窗口其实就是接收端的缓冲区大小,用来告诉发送方对它发送的数据有多大的缓冲空间。在接收方的滑动窗口已知的情况下,当接收方确认了连续的数据序列之后,发送方的滑动窗口向后滑动,发送下一个数据序列。
接收方会在每个ACK数据包中附带自己当前的接受窗口(滑动窗口)的大小,方便发送方进行控制。
拥塞控制是防止过多的数据注入到网络中,导致网络发生拥塞;而流量控制是防止发送方一下子发送过多的数据到接收方,导致接收方缓存放不下。两种算法都是对发送方的行为进行控制的。
防止过多的数据注入到网络中,这样可以使网络中的路由器或链路不致过载,拥塞控制自然也是控制发送者的流量,拥塞控制有四种算法,慢启动、拥塞避免,快速重传和快速恢复
发送方维持一个拥塞窗口 cwnd ( congestion window )的状态变量。拥塞窗口的大小取决于网络的拥塞程度,并且动态地在变化。发送方让自己的发送窗口等于拥塞窗口和接受窗口的较小值。
(1)慢启动。慢启动算法的思路是当主机开始发送数据时,先以比较小的拥塞窗口进行发送,然后每次翻倍,也就是说,由小到大逐渐增加拥塞窗口的大小,而这个大小是指数增长的,即1、2、4、8、16
*为了防止拥塞窗口cwnd增长过大引起网络拥塞,还要另外设置一个慢启动阈值ssthresh状态变量,当拥塞窗口的大小超过慢启动阈值的时候( cwnd > ssthresh 时),停止使用慢开始算法而改用拥塞避免算法
(2)拥塞避免。拥塞避免算法的思路是让拥塞窗口cwnd缓慢地增大,即每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1,而不是加倍。
(3)快速重传。当发送端连续收到三个重复的ack时,表示该数据段已经丢失,需要重发。此时慢启动阈值ssth变为原来一半,拥塞窗口cwnd变为ssth+3,然后+1+1的发(每一轮rtt+1)
(4)快速恢复。当超过设定的时间没有收到某个报文段的ack时,表示网络拥塞,慢启动阈值ssth变为原来一半,拥塞窗口cwnd=1,进入慢启动阶段
联系:Http协议是建立在TCP协议基础之上的,当浏览器需要从服务器获取网页数据的时候,会发出一次Http请求。Http会通过TCP建立起一个到服务器的连接通道,当本次请求需要的数据传输完毕后,Http会立即将TCP连接断开,这个过程是很短的。
区别:HTTP和TCP位于不同的网络分层。TCP是传输层的协议,定义的是数据传输和连接的规范,而HTTP是应用层的,定义的是数据的内容的规范。
建立一个TCP请求需要进行三次握手,而由于http是建立在tcp连接之上的,建立一个http请求通常包含请求和响应两个步骤。
HTTP 协议老的标准是 HTTP/1.0 ,目前最通用的标准是 HTTP/1.1 。
HTTP1.0 只保持短暂的连接,浏览器的每次请求都需要与服务器建立一个 TCP 连接,但是最新的http/1.0加入了长连接,只需要在客户端给服务器发送的http报文头部加入Connection:keep-alive
HTTP 1.1 支持持久连接,默认进行持久连接,在一个 TCP 连接上可以传送多个 HTTP 请求和响应,减少了建立和关闭连接的消耗和延迟。
HTTP的请求方法包括GET,POST,PUT,DELETE四种基本方法。(四种方法中只有POST不是操作幂等性的)
get和post的区别:
详见 HTTP状态码的含义
常见的状态码有:
- 200 - 请求成功
- 301 - 资源(网页等)被永久转移到其它URL
- 404 - 请求的资源(网页等)不存在
- 500 - 内部服务器错误
- 400 - 请求无效
- 403 - 禁止访问
http 是超文本传输协议,信息是明文传输, https 则是具有安全性的 ssl 加密传输协议
http 和 https 使用的是完全不同的连接方式,用的端口也不一样,前者是 80 ,后者是 443
http 的连接很简单,是无状态的; HTTPS 协议是由 SSL+HTTP 协议构建的可进行加密传输、身份认证的网络协议,比http 协议安全。
https 协议需要到 ca 申请证书,一般免费证书较少,因而需要一定费用
https://www.cnblogs.com/wqhwe/p/5407468.html
SSL是传输层的协议
https包括非对称加密和对称加密两个阶段,在客户端与服务器建立连接的时候使用非对称加密,连接建立以后使用的是对称加密。
服务器第一次传给客户端的公钥其实是CA对网站信息进行加密的数字证书
客户端的对称加密密钥其实是三个随机数的哈希(1. 客户端第一次给服务端发送请求时附带的随机数 2. 服务器返回时的随机数 3. 客户端收到返回时的随机数)
第一次的序号是随机序号,但也不是完全随机,它是使用一个ISN算法得到的。
seq = C + H (源IP地址,目的IP地址,源端口,目的端口)。其中,C是一个计时器,每隔一段时间值就会变大,H是消息摘要算法,输入是一个四元组(源IP地址,目的IP地址,源端口,目的端口)。
65536.因为TCP的报文头部中源端口号和目的端口号的长度是16位,也就是可以表示2^16=65536个不同端口号,因此TCP可供识别的端口号最多只有65536个。但是由于0到1023是知名服务端口,所以实际上还要少1024个端口号。
而对于服务器来说,可以开的端口号与65536无关,其实是受限于Linux可以打开的文件数量,并且可以通过MaxUserPort来进行配置。
https://blog.csdn.net/qq_29689487/article/details/81634057
权威CA使用私钥将网站A的信息和消息摘要(签名S)进行加密打包形成数字证书。公钥给客户端。
网站A将自己的信息和数字证书发给客户端,客户端用CA的公钥对数字证书进行解密,得到签名S,与手动将网站的信息进行消息摘要得到的结果S*进行对比,如果签名一致就证明网站A可以信任。
close_wait状态是在TCP四次挥手的时候收到FIN但是没有发送自己的FIN时出现的,服务器出现大量close_wait状态的原因有两种:
处理方法:
消息摘要算法有MD家族(MD2,MD4,MD5),SHA家族(SHA-1,SHA-256)和CRC家族(CRC8,CRC16,CRC32)等等
MD5算法介绍:
MD5以512位分组来处理输入的信息,且每一分组又被划分为若干个小分组(16个32位子分组),经过一些列的处理后,算法输出由四个散列值(32位分组组成的128位散列值。)
详见:https://blog.csdn.net/weixin_39640298/article/details/84555814
为什么不可逆:因为MD5在进行消息摘要的过程中,数据与原始数据相比发生了丢失,所以不能由结果进行恢复。
加强安全性:加盐(加随机数)
服务器端:
数据库端:
ping是使用ICMP协议来进行工作的。 ICMP:网络控制报文协议
目的主机接收到数据帧后,就会检查包上的mac地址与本机mac是否相符,如果相符,就接收并把其中的信息提取出来交给IP协议,IP协议就会将其中的信息提取出来交给ICMP协议。然后构建一个ICMP应答包,用相同的过程发送回去。
因为TCP为了减少额外开销,采取的是流式传输,所以接收端在一次接收的时候有可能一次接收多个包。而TCP粘包就是发送方的若干个数据包到达接收方的时候粘成了一个包。多个包首尾相接,无法区分。
导致TCP粘包的原因有三方面:
避免粘包的措施:
因为TCP是无边界的流传输,所以需要对TCP进行封包和拆包,确保发送和接收的数据不粘连。
DNS解析有两种方式:递归查询和迭代查询
OSI七层协议模型主要是:应用层(Application)、表示层(Presentation)、会话层(Session)、传输层(Transport)、网络层(Network)、数据链路层(Data Link)、物理层(Physical)。
五层体系结构包括:应用层、传输层、网络层、数据链路层和物理层。
通过MAC地址寻找主机是MAC地址寻址,通过IP地址寻找主机叫IP地址寻址。它们适用于不同的协议层,IP寻址是网络层,Mac寻址是数据链路层。
http://c.biancheng.net/view/6388.html
https://blog.csdn.net/wxy_nick/article/details/9190693
IP寻址的过程(ARP协议):主机A想通过IP地址寻找到目标主机,首先分析IP地址确定目标主机与自己是否为同一网段。如果是则查看ARP缓存,或者使用ARP协议发送广播。如果不是,则寻找网关发送ARP数据包
非关系型数据库也叫nosql,采用键值对的形式进行存储。它的读写性能很高,易于扩展。例如Redis,Mongodb,hbase等等。
适合使用非关系型数据库的场景:
数据库的索引类型分为逻辑分类和物理分类
逻辑分类:
物理分类:
https://blog.csdn.net/u013256816/article/details/103966510
https://www.cnblogs.com/takumicx/p/9998844.html
事务就是一组逻辑操作的集合。实现事务就是要保证可靠性和并发隔离,或者说,能够满足ACID特性的机制。而这些主要是靠日志恢复和并发控制实现的。
MySQL建立索引有两种方式:用alter table或者create index。
alter table table_name add primary key(column_list) #添加一个主键索引
alter table table_name add index (column_list) #添加一个普通索引
alter table table_name add unique (column_list) #添加一个唯一索引
create index index_name on table_name (column_list) #创建一个普通索引
create unique index_name on table_name (column_list) #创建一个唯一索引
Mysql删除索引同样也有两种方式:alter table 和 drop index
alter table table_name drop index index_name #删除一个普通索引
alter table table_name drop primary key #删除一个主键索引
drop index index_name on table table_name
https://www.cnblogs.com/wezheng/p/8399305.html
哪些列不适合建索引?
数据库的索引是使用B+树来实现的。
(为什么要用B+树,为什么不用红黑树和B树)
B+树是一种特殊的平衡多路树,是B树的优化改进版本,它把所有的数据都存放在叶节点上,中间节点保存的是索引。这样一来相对于B树来说,减少了数据对中间节点的空间占用,使得中间节点可以存放更多的指针,使得树变得更矮,深度更小,从而减少查询的磁盘IO次数,提高查询效率。另一个是由于叶节点之间有指针连接,所以可以进行范围查询,方便区间访问。
而红黑树是二叉的,它的深度相对B+树来说更大,更大的深度意味着查找次数更多,更频繁的磁盘IO,所以红黑树更适合在内存中进行查找。
这都是由于B+树和B具有不同的存储结构所造成的区别,以一个m阶树为例。
B+树优点:由于B+树的数据都存储在叶子结点中,分支结点均为索引,方便扫库,只需要扫一遍叶子结点即可,但是B树因为其分支结点同样存储着数据,我们要找到具体的数据,需要进行一次中序遍历按序来扫,所以B+树更加适合在区间查询的情况,所以通常B+树用于数据库索引,而B树则常用于文件索引。
假如我们对a b c三个字段建立了联合索引,在联合索引中,从最左边的字段开始,任何连续的索引都能匹配上,当遇到范围查询的时候停止。比如对于联合索引index(a,b,c),能匹配a,ab,abc三组索引。并且对查询时字段的顺序没有限制,也就是a,b,c; b,a,c; c,a,b; c,b,a都可以匹配。
高频访问:
并发优化:
数据库事务是指逻辑上对数据的一种操作,这个事务要么全部成功,要么全部失败。
A: atom 原子性
数据库事务的原子性是指:事务是一个不可分割的工作单位,这组操作要么全部发生,要么全部不发生。
C: consistency 一致性
数据库事务的一致性是指:在事务开始以前,数据库中的数据有一个一致的状态。在事务完成后,数据库中的事务也应该保持这种一致性。事务应该将数据从一个一致性状态转移到另一个一致性状态。
比如在银行转账操作后两个账户的总额应当不变。
I: isolation 隔离性
数据库事务的隔离性要求数据库中的事务不会受另一个并发执行的事务的影响,对于数据库中同时执行的每个事务来说,其他事务要么还没开始执行,要么已经执行结束,它都感觉不到还有别的事务正在执行。
D:durability 持久性
数据库事务的持久性要求事务对数据库的改变是永久的,哪怕数据库发生损坏都不会影响到已发生的事务。
如果事务没有完成,数据库因故断电了,那么重启后也应该是没有执行事务的状态,如果事务已经完成后数据库断电了,那么重启后就应该是事务执行完成后的状态。
比如A向B转账100,A的账户减少了100,而B的账户还没来得及修改,此时一个并发的事务访问到了B的账户,就是脏读
比如A第一次查询自己的账户有1000元,此时另一个事务给A的账户增加了1000元,所以A再次读取他的账户得到了2000的结果,跟第一次读取的不一样。
不可重复读与脏读的不同之处在于,脏读是读取了另一个事务没有提交的脏数据,不可重复读是读取了已经提交的数据,实际上并不是一个异常现象。
比如A公司一共有100个人,第一次查询总人数得到100条记录,此时另一个事务新增了一个人,所以下一次查询得到101条记录。
不可重复度和幻读的不同之处在于,幻读是多次读取的结果行数不同,不可重复度是读取结果的值不同。
避免不可重复读需要锁行,避免幻读则需要锁表。
脏读,不可重复读和幻读都是数据库的读一致性问题,是在并行的过程中出现的问题,必须采用一定的隔离级别解决。
详见脏读、不可重复读和幻读的区别
为了保证数据库事务一致性,解决脏读,不可重复读和幻读的问题,数据库的隔离级别一共有四种隔离级别:
Oracle的默认隔离级别是读已提交,实现了四种隔离级别中的读已提交和串行化隔离级别
MySQL的默认隔离级别是可重复读,并且实现了所有四种隔离级别
https://www.cnblogs.com/linjiqin/archive/2012/04/01/2428695.html
比如 学生 选课(包括很多课程) 就不符合第一范式
比如一张学生信息表,由主键(学号)可以唯一确定一个学生的姓名,班级,年龄等信息。但是主键 (学号,班级) 与列 姓名,班主任,教室 就不符合第二范式,因为班主任跟部分主键(班级)是依赖关系
比如一张学生信息表,主键是(学号)列包括 姓名,班级,班主任 就不符合第三范式,因为非主键的列中 班主任 依赖于 班级
以MYSQL为例,
共享锁是读操作的时候创建的锁,一个事务对数据加上共享锁之后,其他事务只能对数据再加共享锁,不能进行写操作直到释放所有共享锁。
排他锁是写操作时创建的锁,事务对数据加上排他锁之后其他任何事务都不能对数据加任何的锁(即其他事务不能再访问该数据)
https://blog.csdn.net/qq_42743933/article/details/81236658
一般的数据库都会支持并发操作,在并发操作中为了避免数据冲突,所以需要对数据上锁,乐观锁和悲观锁就是两种不同的上锁方式。
悲观锁假设数据在并发操作中一定会发生冲突,所以在数据开始读取的时候就把数据锁住。而乐观锁则假设数据一般情况下不会发生冲突,所以在数据提交更新的时候,才会检测数据是否有冲突。
悲观锁有行级锁和页级锁两种形式。行级锁对正在使用的单条数据进行锁定,事务完成后释放该行数据,而页级锁则对整张表进行锁定,事务正在对该表进行访问的时候不允许其他事务并行访问。
悲观锁要求在整个过程中一直与数据库有一条连接,因为上一个事务完成后才能让下一个事务执行,这个过程是串行的。
乐观锁有三种常用的实现形式:
https://blog.csdn.net/sqsltr/article/details/92762279
https://www.cnblogs.com/euphie/p/6376508.html
(IO过程包括两个阶段:(1)内核从IO设备读写数据和(2)进程从内核复制数据)
阻塞:调用IO操作的时候,如果缓冲区空或者满了,调用的进程或者线程就会处于阻塞状态直到IO可用并完成数据拷贝。
非阻塞:调用IO操作的时候,内核会马上返回结果,如果IO不可用,会返回错误,这种方式下进程需要不断轮询直到IO可用为止,但是当进程从内核拷贝数据时是阻塞的。
IO多路复用就是同时监听多个描述符,一旦某个描述符IO就绪(读就绪或者写就绪),就能够通知进程进行相应的IO操作,否则就将进程阻塞在select或者epoll语句上。
同步IO:同步IO模型包括阻塞IO,非阻塞IO和IO多路复用。特点就是当进程从内核复制数据的时候都是阻塞的。
异步IO:在检测IO是否可用和进程拷贝数据的两个阶段都是不阻塞的,进程可以做其他事情,当IO完成后内核会给进程发送一个信号。
https://zhuanlan.zhihu.com/p/56486633
https://www.jianshu.com/p/397449cadc9a
https://blog.csdn.net/davidsguo008/article/details/73556811
Epoll是Linux进行IO多路复用的一种方式,用于在一个线程里监听多个IO源,在IO源可用的时候返回并进行操作。它的特点是基于事件驱动,性能很高。
epoll将文件描述符拷贝到内核空间后使用红黑树进行维护,同时向内核注册每个文件描述符的回调函数,当某个文件描述符可读可写的时候,将这个文件描述符加入到就绪链表里,并唤起进程,返回就绪链表到用户空间,由用户程序进行处理。
Epoll有三个系统调用:epoll_create(),epoll_ctl()和epoll_wait()。
eoll_create()函数在内核中初始化一个eventpoll对象,同时初始化红黑树和就绪链表。
epoll_ctl()用来对监听的文件描述符进行管理。将文件描述符插入红黑树,或者从红黑树中删除,这个过程的时间复杂度是log(N)。同时向内核注册文件描述符的回调函数。
epoll_wait()会将进程放到eventpoll的等待队列中,将进程阻塞,当某个文件描述符IO可用时,内核通过回调函数将该文件描述符放到就绪链表里,epoll_wait()会将就绪链表里的文件描述符返回到用户空间。
(1)select的方法介绍:select把所有监听的文件描述符拷贝到内核中,挂起进程。当某个文件描述符可读或可写的时候,中断程序唤起进程,select将监听的文件描述符再次拷贝到用户空间,然select后遍历这些文件描述符找到IO可用的文件。下次监控的时候需要再次拷贝这些文件描述符到内核空间。select支持监听的描述符最大数量是1024.
(2)poll使用链表保存文件描述符,其他的跟select没有什么不同。
(3)epoll将文件描述符拷贝到内核空间后使用红黑树进行维护,同时向内核注册每个文件描述符的回调函数,当某个文件描述符可读可写的时候,将这个文件描述符加入到就绪链表里,并唤起进程,返回就绪链表到用户空间。
详见 https://www.cnblogs.com/Anker/p/3265058.html
详见:https://blog.csdn.net/qq_36357820/article/details/76606113
chmod 777 (177 277 477 等,权限组合是 1 2 4,分别代表r x w )
详见: http://blog.sina.com.cn/s/blog_7b4ce6b101018l8l.html
cat的功能是将文件从第一行开始连续的将内容输出在屏幕上。当文件大,行数比较多时,屏幕无法全部容下时,只能看到一部分内容。所以通常使用重定向的方式,输出满足指定格式的内容
cat语法:cat [-n] 文件名 (-n : 显示时,连行号一起输出)
tac的功能是将文件从最后一行开始倒过来将内容数据输出到屏幕上。我们可以发现,tac实际上是cat反过来写。这个命令不常用。
tac语法:tac 文件名。
more的功能是将文件从第一行开始,根据输出窗口的大小,适当的输出文件内容。当一页无法全部输出时,可以用“回车键”向下翻行,用“空格键”向下翻页。退出查看页面,请按“q”键。另外,more还可以配合管道符“|”(pipe)使用,例如:ls -al | more more的语法:more 文件名 Enter 向下n行,需要定义,默认为1行; Ctrl f 向下滚动一屏; 空格键 向下滚动一屏; Ctrl b 返回上一屏; = 输出当前行的行号; :f 输出文件名和当前行的行号; v 调用vi编辑器; ! 命令 调用Shell,并执行命令; q 退出more less的功能和more相似,但是使用more无法向前翻页,只能向后翻。 less可以使用【pageup】和【pagedown】键进行前翻页和后翻页,这样看起来更方便。 less的语法:less 文件名
head和tail通常使用在只需要读取文件的前几行或者后几行的情况下使用。head的功能是显示文件的前几行内容
head的语法:head [n number] 文件名 (number 显示行数)
tail的功能恰好和head相反,只显示最后几行内容
tail的语法:tail [-n number] 文件名
nl的功能和cat -n一样,同样是从第一行输出全部内容,并且把行号显示出来
nl的语法:nl 文件名
这个用的太普遍了,主要是用于编辑。
coredump是程序由于异常或者bug在运行时异常退出或者终止,在一定的条件下生成的一个叫做core的文件,这个core文件会记录程序在运行时的内存,寄存器状态,内存指针和函数堆栈信息等等。对这个文件进行分析可以定位到程序异常的时候对应的堆栈调用信息。
coredump产生的条件
用简单的话来定义tcpdump,就是:dump the traffic on a network,根据使用者的定义对网络上的数据包进行截获的包分析工具。 tcpdump可以将网络中传送的数据包的“头”完全截获下来提供分析。它支持针对网络层、协议、主机、网络或端口的过滤,并提供and、or、not等逻辑语句来帮助你去掉无用的信息。
实用命令实例
将某端口收发的数据包保存到文件
sudo tcpdump -i any port 端口 -w 文件名.cap
打印请求到屏幕
sudo tcpdump -i any port 端口 -Xnlps0
默认启动
tcpdump
普通情况下,直接启动tcpdump将监视第一个网络接口上所有流过的数据包。
监视指定网络接口的数据包
tcpdump -i eth1
如果不指定网卡,默认tcpdump只会监视第一个网络接口,一般是eth0,下面的例子都没有指定网络接口。
详见:https://www.cnblogs.com/peida/archive/2013/01/08/2850483.html
corntab命令是用来指定用户计划任务的。用户将需要定时执行的任务写入crontab文件中,提交给crond进程定期执行。
1.命令格式:
crontab [-u user] file
crontab [-u user] [ -e | -l | -r ]
2.命令功能:
通过crontab 命令,我们可以在固定的间隔时间执行指定的系统指令或 shell script脚本。时间间隔的单位可以是分钟、小时、日、月、周及以上的任意组合。这个命令非常设合周期性的日志分析或数据备份等工作。
3.命令参数:
-u user:用来设定某个用户的crontab服务,例如,“-u ixdba”表示设定ixdba用户的crontab服务,此参数一般有root用户来运行。
file:file是命令文件的名字,表示将file做为crontab的任务列表文件并载入crontab。如果在命令行中没有指定这个文件,crontab命令将接受标准输入(键盘)上键入的命令,并将它们载入crontab。
-e:编辑某个用户的crontab文件内容。如果不指定用户,则表示编辑当前用户的crontab文件。
-l:显示某个用户的crontab文件内容,如果不指定用户,则表示显示当前用户的crontab文件内容。
-r:从/var/spool/cron目录中删除某个用户的crontab文件,如果不指定用户,则默认删除当前用户的crontab文件。
-i:在删除用户的crontab文件时给确认提示。
crond是Linux下的周期性执行系统任务的守护进程,他会根据/etc下的crontab配置文件的内容执行。用户需要将计划任务写入crontab文件中才能执行。
用户所建立的crontab文件中,每一行都代表一项任务,每行的每个字段代表一项设置,它的格式共分为六个字段,前五段是时间设定段,第六段是要执行的命令段,格式如下:
minute hour day month week command
其中:
minute: 表示分钟,可以是从0到59之间的任何整数。
hour:表示小时,可以是从0到23之间的任何整数。
day:表示日期,可以是从1到31之间的任何整数。
month:表示月份,可以是从1到12之间的任何整数。
week:表示星期几,可以是从0到7之间的任何整数,这里的0或7代表星期日。
command:要执行的命令,可以是系统命令,也可以是自己编写的脚本文件。
在以上各个字段中,还可以使用以下特殊字符:
星号(*):代表所有可能的值,例如month字段如果是星号,则表示在满足其它字段的制约条件后每月都执行该命令操作。
逗号(,):可以用逗号隔开的值指定一个列表范围,例如,“1,2,5,7,8,9”
中杠(-):可以用整数之间的中杠表示一个整数范围,例如“2-6”表示“2,3,4,5,6”
正斜线(/):可以用正斜线指定时间的间隔频率,例如“0-23/2”表示每两小时执行一次。同时正斜线可以和星号一起使用,例如*/10,如果用在minute字段,表示每十分钟执行一次。
查看当前控制台的后台进程
想要停止后台进程,使用jobs命令查看其进程号(比如为num),然后kill %num即可
查看后台进程
查看所有进程和资源使用情况,类似Windows中的任务管理器
停止进程:界面是交互式的,在窗口输入k 之后输入PID,会提示输入停止进程模式 有SIGTERM和 SIGKILL 如果留空不输入,就是SIGTERM(优雅停止)
退出top:输入q即可
答:32768. 因为进程的pid是用pid_t来表示的,pid_t的最大值是32768.所以理论上最多有32768个进程。
至于线程。进程最多可以创建的线程数是根据分配给调用栈的大小,以及操作系统(32位和64位不同)共同决定的。Linux32位下是300多个。
进程之间的通信方式主要有六种,包括管道,信号量,消息队列,信号,共享内存,套接字。
管道:管道是半双工的,双方需要通信的时候,需要建立两个管道。管道的实质是一个内核缓冲区,进程以先进先出的方式从缓冲区存取数据:管道一端的进程顺序地将进程数据写入缓冲区,另一端的进程则顺序地读取数据,该缓冲区可以看做一个循环队列,读和写的位置都是自动增加的,一个数据只能被读一次,读出以后再缓冲区都不复存在了。当缓冲区读空或者写满时,有一定的规则控制相应的读进程或写进程是否进入等待队列,当空的缓冲区有新数据写入或慢的缓冲区有数据读出时,就唤醒等待队列中的进程继续读写。管道是最容易实现的
匿名管道pipe和命名管道除了建立,打开,删除的方式不同外,其余都是一样的。匿名管道只允许有亲缘关系的进程之间通信,也就是父子进程之间的通信,命名管道允许具有非亲缘关系的进程间通信。
管道的底层实现 https://segmentfault.com/a/1190000009528245
信号量:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。信号量只有等待和发送两种操作。等待(P(sv))就是将其值减一或者挂起进程,发送(V(sv))就是将其值加一或者将进程恢复运行。
信号:信号是Linux系统中用于进程之间通信或操作的一种机制,信号可以在任何时候发送给某一进程,而无须知道该进程的状态。如果该进程并未处于执行状态,则该信号就由内核保存起来,知道该进程恢复执行并传递给他为止。如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程。 信号是开销最小的
共享内存:共享内存允许两个或多个进程共享一个给定的存储区,这一段存储区可以被两个或两个以上的进程映射至自身的地址空间中,就像由malloc()分配的内存一样使用。一个进程写入共享内存的信息,可以被其他使用这个共享内存的进程,通过一个简单的内存读取读出,从而实现了进程间的通信。共享内存的效率最高,缺点是没有提供同步机制,需要使用锁等其他机制进行同步。
消息队列:消息队列就是一个消息的链表,是一系列保存在内核中消息的列表。用户进程可以向消息队列添加消息,也可以向消息队列读取消息。
消息队列与管道通信相比,其优势是对每个消息指定特定的消息类型,接收的时候不需要按照队列次序,而是可以根据自定义条件接收特定类型的消息。
可以把消息看做一个记录,具有特定的格式以及特定的优先级。对消息队列有写权限的进程可以向消息队列中按照一定的规则添加新消息,对消息队列有读权限的进程可以从消息队列中读取消息。
套接字:套接口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同设备及其间的进程通信。
https://blog.csdn.net/u011080472/article/details/51217754
https://blog.csdn.net/leex_brave/article/details/51638300
进程的执行需要经过三大步骤:编译,链接和装入。
https://blog.csdn.net/qq_38623623/article/details/78306498
将进程装入内存时,通常使用分页技术,将内存分成固定大小的页,进程分为固定大小的块,加载时将进程的块装入页中,并使用页表记录。减少外部碎片。
通常操作系统还会使用虚拟内存的技术将磁盘作为内存的扩充。
https://www.cnblogs.com/peterYong/p/6556619.html
https://zhuanlan.zhihu.com/p/141602175
操作系统的内存管理包括物理内存管理和虚拟内存管理
(面试官这样问的时候,其实是希望你能讲讲虚拟内存)
用到两个数据结构:哈希+双向链表
unordered_map<int,list<pair<int,int> > > cache ;// 存放键,迭代器
list<pair<int,int>> auxlist; // 存放 <键,值>
class LRUCache { int cap; list<pair<int,int>> l;// front:new back:old 存放值 新的放前面,因为前面的可以取得有效的迭代器 map<int,list<pair<int,int> >::iterator > cache;// 存放键,迭代器 public: LRUCache(int capacity) { cap=capacity; } int get(int key) { auto mapitera = cache.find(key); if(mapitera==cache.end()){ return -1; }else{// found list<pair<int,int>>::iterator listItera = mapitera->second; int value = (*listItera).second; l.erase(listItera); l.push_front({key,value}); cache[key]=l.begin(); return value; } } void put(int key, int value) { auto itera = cache.find(key); if(itera!=cache.end()){// exist list<pair<int,int>>::iterator listItera = itera->second; l.erase(listItera); l.push_front({key,value}); cache[key]=l.begin(); }else{// not exist if(cache.size()>=cap){ pair<int,int> oldpair = l.back(); l.pop_back(); cache.erase(oldpair.first); } l.push_front({key,value}); cache[key]=l.begin(); } } }; /** * Your LRUCache object will be instantiated and called as such: * LRUCache* obj = new LRUCache(capacity); * int param_1 = obj->get(key); * obj->put(key,value); */
(1) 互斥:一个资源每次只能被一个进程使用。
(2) 占有并请求:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3) 不可剥夺:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4) 循环等待:若干进程之间形成一种头尾相接的循环等待资源关系。
产生死锁的原因主要是:
(1) 因为系统资源不足。
(2) 进程运行推进的顺序不合适。
(3) 资源分配不当等。
饥饿是由于资源分配策略不公引起的,当进程或线程无法访问它所需要的资源而不能继续执行时,就会发生饥饿现象。
https://blog.csdn.net/kid551/article/details/84338619
实现mutex最重要的就是实现它的lock()方法和unlock()方法。我们保存一个全局变量flag,flag=1表明该锁已经锁住,flag=0表明锁没有锁住。
实现lock()时,使用一个while循环不断检测flag是否等于1,如果等于1就一直循环。然后将flag设置为1;unlock()方法就将flag置为0;
static int flag=0;
void lock(){
while(TestAndSet(&flag,1)==1);
//flag=1;
}
void unlock(){
flag=0;
}
因为while有可能被重入,所以可以用TestandSet()方法。
int TestAndSet(int *ptr, int new) {
int old = *ptr;
*ptr = new;
return old;
}
线程之间通信:
进程之间同步:
https://www.cnblogs.com/sonic4x/archive/2011/07/05/2098036.html
https://blog.csdn.net/yu876876/article/details/82810178
但是实际中更常见的是进程加线程的结合方式,并不是非此即彼的。
https://www.cnblogs.com/Anker/p/3271773.html
https://blog.csdn.net/qq_38499859/article/details/80057427
PCB就是进程控制块,是操作系统中的一种数据结构,用于表示进程状态,操作系统通过PCB对进程进行管理。
PCB中包含有:进程标识符,处理器状态,进程调度信息,进程控制信息
进程地址空间内有:
在Linux中虚拟地址空间范围为0到4G,最高的1G地址(0xC0000000到0xFFFFFFFF)供内核使用,称为内核空间,低的3G空间(0x00000000到0xBFFFFFFF)供各个进程使用,就是用户空间。
内核空间中存放的是内核代码和数据,而进程的用户空间中存放的是用户程序的代码和数据。
https://blog.csdn.net/s_lisheng/article/details/74278765
线程的栈空间是自己独有的
在Linux下栈空间通常是8M,Windows下是1M
https://www.cnblogs.com/Przz/p/6876988.html
在运行一个进程的时候,它所需要的内存空间可能大于系统的物理内存容量。通常一个进程会有4G的空间,但是物理内存并没有这么大,所以这些空间都是虚拟内存,它的地址都是逻辑地址,每次在访问的时候都需要映射成物理地址。
当进程访问某个逻辑地址的时候,会去查看页表,如果页表中没有相应的物理地址,说明内存中没有这页的数据,发生缺页异常,这时候进程需要把数据从磁盘拷贝到物理内存中。如果物理内存已经满了,就需要覆盖已有的页,如果这个页曾经被修改过,那么还要把它写回磁盘。
应用数据与静态资源分离
将静态资源(图片,视频,js,css等)单独保存到专门的静态资源服务器中,在客户端访问的时候从静态资源服务器中返回静态资源,从主服务器中返回应用数据。
客户端缓存
因为效率最高,消耗资源最小的就是纯静态的html页面,所以可以把网站上的页面尽可能用静态的来实现,在页面过期或者有数据更新之后再将页面重新缓存。或者先生成静态页面,然后用ajax异步请求获取动态数据。
集群和分布式
(集群是所有的服务器都有相同的功能,请求哪台都可以,主要起分流作用)
(分布式是将不同的业务放到不同的服务器中,处理一个请求可能需要使用到多台服务器,起到加快请求处理的速度。)
可以使用服务器集群和分布式架构,使得原本属于一个服务器的计算压力分散到多个服务器上。同时加快请求处理的速度。
反向代理
在访问服务器的时候,服务器通过别的服务器获取资源或结果返回给客户端。
协程和微线程是一个东西。
协程就是子程序在执行时中断并转去执行别的子程序,在适当的时候又返回来执行。
这种子程序间的跳转不是函数调用,也不是多线程执行,所以省去了线程切换的开销,效率很高,并且不需要多线程间的锁机制,不会发生变量写冲突。
协程进行中断跳转时将函数的上下文存放在其他位置中,而不是存放在函数堆栈里,当处理完其他事情跳转回来的时候,取回上下文继续执行原来的函数。
三态模型
三态模型包括三种状态:
五态模型
七态模型
https://blog.csdn.net/yusiguyuan/article/details/39496057
从操作系统层面上看,malloc是通过两个系统调用来实现的: brk和mmap
通常,分配的内存小于128k时,使用brk调用来获得虚拟内存,大于128k时就使用mmap来获得虚拟内存。
进程先通过这两个系统调用获取或者扩大进程的虚拟内存,获得相应的虚拟地址,在访问这些虚拟地址的时候,通过缺页中断,让内核分配相应的物理内存,这样内存分配才算完成。
https://www.cnblogs.com/broglie/p/5645200.html
字节序是对象在内存中存储的方式,大端即为最高有效位在前面,小端即为最低有效位在前面。
判断大小端的方法:使用一个union数据结构
union{
short s;
char c[2]; // sizeof(short)=2;
}un;
un.s=0x0102;
if(un.c[0]==1 and un.c[1]==2) cout<<"大端";
if(un.c[0]==2 and un.c[1]==1) cout<<"小端";
在网络编程中不同字节序的机器发送和接收的顺序不同。
面试中90%的算法题都从leetcode hot100和剑指offer中出 刷两遍非常有必要
// C++ version
class Singleton{
private:
static Singleton* instance;
Singleton(){
// initialize
}
public:
static Singleton* getInstance(){
if(instance==nullptr) instance=new Singleton();
return instance;
}
};
#include<iostream> #include<thread> #include<mutex> #include<condition_variable> using namespace std; mutex mymutex; condition_variable cv; int flag=0; void printa(){ unique_lock<mutex> lk(mymutex); int count=0; while(count<10){ while(flag!=0) cv.wait(lk); cout<<"thread 1: a"<<endl; flag=1; cv.notify_all(); count++; } cout<<"my thread 1 finish"<<endl; } void printb(){ unique_lock<mutex> lk(mymutex); for(int i=0;i<10;i++){ while(flag!=1) cv.wait(lk); cout<<"thread 2: b"<<endl; flag=2; cv.notify_all(); } cout<<"my thread 2 finish"<<endl; } void printc(){ unique_lock<mutex> lk(mymutex); for(int i=0;i<10;i++){ while(flag!=2) cv.wait(lk); cout<<"thread 3: c"<<endl; flag=0; cv.notify_all(); } cout<<"my thread 3 finish"<<endl; } int main(){ thread th2(printa); thread th1(printb); thread th3(printc); th1.join(); th2.join(); th3.join(); cout<<" main thread "<<endl; }
void swap(int& a,int& b){
a=a^b;
b=a^b;
a=a^b;
}
void swap(vector<int>& vec,int a,int b){ vec[a]=vec[a]^vec[b]; vec[b]=vec[a]^vec[b]; vec[a]=vec[a]^vec[b]; } int partition(vector<int>& vec,int start,int end){ int pivot=vec[start+(end-start)/2]; while(start<end){ while(start<end and vec[start]<pivot) start++; while(start<end and vec[end]>pivot) end--; if(start<end) swap(vec,start,end); } return start; } void quickSort(vector<int>& vec,int start,int end){ if(start>end) return; int pivot=partition(vec,start,end); quickSort(vec,start,pivot-1); quickSort(vec,pivot+1,end); }
堆排序的基本过程:
整体时间复杂度为nlogn
#include<iostream> #include<vector> using namespace std; void swap(vector<int>& arr, int a,int b){ arr[a]=arr[a]^arr[b]; arr[b]=arr[a]^arr[b]; arr[a]=arr[a]^arr[b]; } void adjust(vector<int>& arr,int len,int index){ int maxid=index; // 计算左右子节点的下标 left=2*i+1 right=2*i+2 parent=(i-1)/2 int left=2*index+1,right=2*index+2; // 寻找当前以index为根的子树中最大/最小的元素的下标 if(left<len and arr[left]<arr[maxid]) maxid=left; if(right<len and arr[right]<arr[maxid]) maxid=right; // 进行交换,记得要递归进行adjust,传入的index是maxid if(maxid!=index){ swap(arr,maxid,index); adjust(arr,len,maxid); } } void heapsort(vector<int>&arr,int len){ // 初次构建堆,i要从最后一个非叶子节点开始,所以是(len-1-1)/2,0这个位置要加等号 for(int i=(len-1-1)/2;i>=0;i--){ adjust(arr,len,i); } // 从最后一个元素的下标开始往前遍历,每次将堆顶元素交换至当前位置,并且缩小长度(i为长度),从0处开始adjust for(int i=len-1;i>0;i--){ swap(arr,0,i); adjust(arr,i,0);// 注意每次adjust是从根往下调整,所以这里index是0! } } int main(){ vector<int> arr={3,4,2,1,5,8,7,6}; cout<<"before: "<<endl; for(int item:arr) cout<<item<<" "; cout<<endl; heapsort(arr,arr.size()); cout<<"after: "<<endl; for(int item:arr)cout<<item<<" "; cout<<endl; return 0; }
https://blog.csdn.net/left_la/article/details/8656425
void insertSort(vector<int>& nums){
int len=nums.size();
for(int i=1;i<len;i++){
int key=nums[i];
int j=i-1;
while(j>=0 and nums[j]>key){
nums[j+1]=nums[j];
j--;
}
nums[j+1]=key;
}
}
随机(rand函数)、固定(队首、队尾)、三数取中(队首、队中和队尾的中间数)
优化1:当待排序序列的长度分割到一定大小后,使用插入排序
优化2:在一次分割结束后,可以把与Key相等的元素聚在一起,继续下次分割时,不用再对与key相等元素分割
优化3:优化递归操作
优化4:使用并行或多线程处理子序列
ListNode* reverse(ListNode* root){
ListNode* pre=nullptr,cur=root,nxt;
while(cur!=nullptr){
nxt=cur->next;
cur->next=pre;
pre=cur;cur=nxt;
}
return pre;
}
Top K 问题的常见形式:
给定10000个整数,找第K大(第K小)的数
给定10000个整数,找出最大(最小)的前K个数
给定100000个单词,求前K词频的单词
解决Top K问题若干种方法
note:最小堆的插入时间复杂度为log(n),n为堆中元素个数,在这里是K。最小堆的初始化时间复杂度是nlog(n)
C++中的最大最小堆要用标准库的priority_queue来实现。
struct Node {
int value;
int idx;
Node (int v, int i): value(v), idx(i) {}
friend bool operator < (const struct Node &n1, const struct Node &n2) ;
};
inline bool operator < (const struct Node &n1, const struct Node &n2) {
return n1.value < n2.value;
}
priority_queue<Node> pq; // 此时pq为最大堆
这个算法与快排最大的区别是,每次划分后只处理左半边或者右半边,而快排在划分后对左右半边都继续排序。
//此为Java实现 public int findKthLargest(int[] nums, int k) { return quickSelect(nums, k, 0, nums.length - 1); } // quick select to find the kth-largest element public int quickSelect(int[] arr, int k, int left, int right) { if (left == right) return arr[right]; int index = partition(arr, left, right); if (index - left + 1 > k) return quickSelect(arr, k, left, index - 1); else if (index - left + 1 == k) return arr[index]; else return quickSelect(arr, k - (index - left + 1), index + 1, right); }
我们可以使用外部排序来对它进行处理。首先将整个文件分成许多份,比如说m份,划分的依据就是使得每一份的大小都能放到内存里。然后我们用快速排序或者堆排序等方法对每一份数据进行一个内部排序,变成有序子串。接着对这m份有序子串进行m路归并排序。取这m份数据的最小元素,进行排序,输出排序后最小的元素到结果中,同时从该元素所在子串中读入一个元素,直到所有数据都被输出到结果中为止。
https://blog.csdn.net/ailunlee/article/details/84548950
在写二叉树相关算法的时候,如果需要自己构造测试用例(自己构造一棵二叉树),往往是一件很麻烦的事情,我们可以用一个带有null标记的前序遍历序列来进行构造。 需要注意的是vec2tree()参数中的start是引用传递,而不是简单的参数值传递。
#include<iostream> #include<vector> #include<queue> using namespace std; struct treeNode{ string val; treeNode* left,*right; treeNode(string val):val(val){ left=nullptr; right=nullptr; } }; treeNode* vec2tree(vector<string>& vec,int& start){ treeNode* root; if(vec[start]=="null"){ start+=1; root=nullptr; }else{ root=new treeNode(vec[start]); start+=1; root->left=vec2tree(vec,start); root->right=vec2tree(vec,start); } return root; } void tree2vec(treeNode *root,vector<string>& vec){ if(root==nullptr){ vec.push_back("null"); }else{ vec.push_back(root->val); tree2vec(root->left,vec); tree2vec(root->right,vec); } } int main(){ vector<string> vec={"2","4","5","7","null","null","null","null","3","6","null","null","2","null","null"}; int index=0,&start=index; treeNode* root=vec2tree(vec,start); //displaytree(root); vector<string> mvec; tree2vec(root,mvec); for(string item:mvec) cout<<item<<" "; cout<<endl; return 0;
B树也叫做B-树,或者平衡多路树,它是每个节点最多有m个子树的平衡树。一个m阶的B树具有如下几个特征:
b树主要应用于文件系统中,在数据库中(mongoDB)也有应用,与B+树相比好处应该是有时不需要访问到叶节点就可以获取数据。
查询时间复杂度是logN
B+树是一种特殊的B树,它把数据都存储在叶子节点,并且叶节点间有指针连接。内部只存关键字(其中叶子节点的最小值作为索引)和孩子指针,简化了内部节点。
应用场景主要是数据库的索引
查询时间复杂度也是logN
https://zhuanlan.zhihu.com/p/110202102
https://blog.csdn.net/hguisu/article/details/7786014
红黑树是一种特殊的二叉查找树,它在每一个节点上都使用红色或黑色进行标记,通过一些性质确保它是始终平衡的。
它的性质是这样的:
红黑树的插入,查询,删除在一般情况和最坏情况下的时间复杂度都是O(log(n))
应用场景主要是STL中map,set的实现,优点在于支持频繁的修改,因为查询删除插入时间复杂度都是logN
select * limit 1000
from t1
https://www.cnblogs.com/chengxiao/p/6104371.html
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
模拟STL中vector的实现即可,去看一下vector的源码。
bitmap算法就是使用一个比特映射一个值,它可以用在整数排序和数据压缩上,因为使用一个比特位去存储一个数,所以它可以大大节省空间。
它的具体过程是:先根据数组中元素最大的数N计算需要分配多大的空间。
如果使用int型数组的形式来保存的话,一个int = 4字节 =4*8比特 = 32比特。也就是一个int数可以映射32个数据(图1),然后需要找到最大的数Max,表示最多需要的位数,所以需要开辟的数组空间为int a[1+Max/32]。
然后需要推导一个整数a内如何映射32个数据,方法是将待存储的数据模32,然后将a中相应位置的比特置为1。
依此方法映射每一个元素,待读取的时候扫描每个比特位,遇到值为1的就还原该数字。
移位计算公式:
N/32就是将N的二进制右移log32(也就是5)位 : N>>5
N%32就是求N的后5位:N& 0x1F (0x1F = 00011111)
模32然后相应位置置为1: a[i] |= 1<< N & 0x1F
所以总的公式为: a[ N>>5 ] |= 1<< N & 0x1F
BitMap算法评价
布隆过滤器是一个比特向量或者比特数组,它本质上是一种概率型数据结构,用来查找一个元素是否在集合中,支持高效插入和查询某条记录。常作为针对超大数据量下高效查找数据的一种方法。
它的具体工作过程是这样子的:
假设布隆过滤器的大小为m(比特向量的长度为m),有k个哈希函数,它对每个数据用这k个哈希函数计算哈希,得到k个哈希值,然后将向量中相应的位设为1。在查询某个数据是否存在的时候,对这个数据用k个哈希函数得到k个哈希值,再在比特向量中相应的位查找是否为1,如果某一个相应的位不为1,那这个数据就肯定不存在。但是如果全找到了,则这个数据有可能存在。
为什么说有可能存在呢?
因为不同的数据经过哈希后可能有相同的哈希值,在比特向量上某个位置查找到1也可能是由于某个另外的数据映射得到的。
支持删除操作吗
目前布隆过滤器只支持插入和查找操作,不支持删除操作,如果要支持删除,就要另外使用一个计数变量,每次将相应的位置为1则计数加一,删除则减一。
布隆过滤器中哈希函数的个数需要选择。如果太多则很快所有位都置为1,如果太少会容易误报。
布隆过滤器的大小以及哈希函数的个数怎么选择?
k 为哈希函数个数,m 为布隆过滤器长度,n 为插入的元素个数,p 为误报率
//评测题目: class FIFOQueue { vector<int> vec(initCap,0); int start=0,end=0; condition_variable cv; mutex m; bool flag=false;// isFull bool enqueue(int v) { unique_lock<mutex> lk(m); while(flag==true) cv.wait(lk); end=(end+1)%initCap; vec[end]=v; cv.notifyall(); return true; } } int dequeue() { unique_lock<mutex> lk(m); if(start!=end){ int val = vec[start]; start=(start+1)%initCap; flag=false; cv.notifyall(); return val; }else{ flag=false; cv.notifyall(); return -1; } } }
以上代码是面试时写的,并没有运行,也许有错误,请客观参考
用二进制的思路解决问题。2的十次方是1024,使用十只小鼠喝一次即可。方法是先将每瓶水编号,同时10个小鼠分别表示二进制中的一个位。将每瓶水混合到水瓶编号中二进制为1的小鼠对应的水中。喝完后统计,将死亡小鼠对应的位置为1,没死的置为0,根据死亡小鼠的编号确定有毒的是哪瓶水,如0000001010表示10号水有毒。
寻找每个回合固定的拿取模式。最后一次是我拿,那么上个回合最少剩下6本。那么只要保持每个回合结束后都剩下6的倍数,并且在这个回合中我拿的和对方拿的加起来为6(这样这个回合结束后剩下的还是6的倍数),就必胜。关键是第一次我必须先手拿(100%6=4)本(这不算在第一回合里面)。
碰到就当没发生,继续走,相当于碰到的两个蚂蚁交换了一下身体。其实就是每个蚂蚁从当前位置一直走直到停止的总距离或者时间。
拿走3瓶,换回1瓶,相当于减少2瓶。但是最后剩下4瓶的时候例外,这时只能换1瓶。所以我们计算1000减2能减多少次,直到剩下4.(1000-4=996,996/2=498)所以1000减2能减498次直到剩下4瓶,最后剩下的4瓶还可以换一瓶,所以总共是1000+498+1=1499瓶。
24小时中时针走2圈,而分针走24圈,时针和分针重合24-2=22次,而只要时针和分针重合,秒针一定有机会重合,所以总共重合22次
至少2次:第一次,一边3个,哪边轻就在哪边,一样重就是剩余的3个;
第二次,一边1个,哪边轻就是哪个,一样重就是剩余的那个;
砝码分组1~10,第一组拿一个,第二组拿两个以此类推。。第十组拿十个放到秤上称出克数x,则y = 550 - x,第y组就是轻的那组
思路:由大的生成小的容易,比如由Rand7()生成Rand5(),所以我们先构造一个大于7的随机数生成函数。
记住下面这个式子:
RandNN= N( RandN()-1 ) + RandN() ;// 生成1到N^2之间的随机数
可以看作是在数轴上撒豆子。N是跨度/步长,是RandN()生成的数的范围长度,RandN()-1的目的是生成0到N-1的数,是跳数。后面+RandN()的目的是填满中间的空隙
比如Rand25= 5( Rand5()-1 ) + Rand5()
可以生成1到25之间的随机数。我们可以只要1到21(3*7)之间的数字,所以可以这么写
int rand7(){
int x=INT_MAX;
while(x>21){
x=5*(rand5()-1)+rand5();
}
return x%7+1;
}
(说了求15分钟,没说开始的15分钟还是结束的15分钟,这里是可以求最后的15分钟)点燃一根A,同时点燃另一根B的两端,当另一根B烧完的时候就是半小时,这是再将A的另一端也点燃,从这时到A燃烧完就正好15分钟。
每次拿起一块巧克力,掰一下(无论横着还是竖着)都会变成两块,因为所有的巧克力共有N*M块,所以要掰N*M-1次,减1是因为最开始的一块是不用算进去的。
每一场辩论赛参加两个人,淘汰一个人,所以可以看作是每一场辩论赛减少一个人,直到最后剩下1个人,所以是1000-1=999场。
Hadoop是一套大数据解决方案,提供了一套分布式的系统基础架构,包括HDFS,MapReduce和YARN。
HDFS是主从架构的,包括namenode,secondarynamenode和datanode。datanode负责存储数据,namenode负责管理HDFS的目录树和文件元信息。
MapReduce包括jobtracker,tasktracker和client。Jobtracker负责进行资源调度和作业监控。tasktracker会周期性的通过心跳向jobtracker汇报资源使用情况。
MapReduce包括输入分片、map阶段、combine阶段、shuffle阶段和reduce阶段。分布式计算框架包括client,jobtracker和tasktracker和调度器。
https://blog.csdn.net/qq_29186199/article/details/80827085
https://blog.csdn.net/student__software/article/details/81486431
kafka是一个分布式消息队列,包括producer、broker和consumer。kafka会对每个消息根据topic进行归类,每个topic又会分成多个partition,消息会根据先进先出的方式存储。消费者通过offset进行消费。
kafka的特点是吞吐量高,可以进行持久化,高可用。
kafka吞吐量高是因为一个利用了磁盘顺序读写的特性,速度比随机读写要快很多,另一个是使用了零拷贝,数据直接在内核进行输入和输出,减少了用户空间和内核空间的切换。
零拷贝:传统文件读取并发送至网络的步骤是:先将文件从磁盘拷贝到内核空间,然后内核空间拷贝到用户空间的缓冲区,再从用户空间拷贝到内核空间的socket缓冲区,最后拷贝到网卡并发送。而零拷贝技术是先将文件从磁盘空间拷贝到内核缓冲区,然后直接拷贝至网卡进行发送,减少了重复拷贝操作。
https://blog.csdn.net/u011204847/article/details/51010205
spark是一个通用内存并行计算框架。它可以在内存中对数据进行计算,效率很高,spark的数据被抽象成RDD(弹性分布式数据集)并且拥有DAG执行引擎,兼容性和通用性很好。可以和Hadoop协同工作。
https://blog.csdn.net/yu0_zhang0/article/details/80569946
spark-streaming是spark的核心组件之一。主要提供高效的流计算能力。spark-streaming的原理是将输入数据流以时间片进行拆分,然后经过spark引擎以类似批处理的方式处理每个时间片数据。
spark-streaming将输入根据时间片划分成一段一段的Dstream(也就是离散数据流),然后将每一段数据转换成RDD进行操作。
spark的算子分成transformation和action两类
spark的算子分为两类:transformation和action
常用的transformation算子:
// union 求并集
val rdd8 = rdd6.union(rdd7)
// intersection 求交集
val rdd9 = rdd6.intersection(rdd7)
// join 将rdd进行聚合连接,类似数据库的join
val rdd3 = rdd1.join(rdd2)
// map flatMap mapPartition 传入一个函数对数据集中的每一个数据进行操作
val arr1 = Array(1,2,3,4,5)
val arr2 = rdd1.map(_+1)
// countByKey reduceByKey partitionByKey 统计每个key有多少个键值对
常用的action算子
// reduce 按照一定的方法将元素进行合并
val rdd2 = rdd1.reduce(_+_)
// collect 将RDD转换为数组
rdd1.collect
// top 返回最大的k个元素
rdd1.top(2)
https://blog.csdn.net/liudashuang2017/article/details/88576274
我们可以从三个方面保证kafka不丢失消息
首先启动的broker在zookeeper中创建一个临时节点并让自己称为leader,其他的节点会创建watch对象进行监听并成为follower,当broker宕机的时候,其他follower会尝试创建这个临时节点,但是只有一个能够创建成功,创建成功的broker就会成为leader。
https://blog.csdn.net/a1043498776/article/details/54889922
https://zhuanlan.zhihu.com/p/57124273
spark中的stage其实是一组并行的任务,spark会将多个RDD根据依赖关系划分成有向无环图DAG,DAG会被划分成多个stage,划分的依据是RDD之间的宽窄依赖。遇到宽依赖就划分stage。因为宽依赖与窄依赖的区别之一就是宽依赖会发生shuffle操作,所以也可以说stage的划分依据是是否发生shuffle操作。
https://www.jianshu.com/p/4f1e551553ae
https://www.cnblogs.com/wzj4858/p/8204282.html
spark的内存包括静态内存管理和统一内存管理两种机制。静态内存管理中存储和执行两块内存区域是分开的,统一内存管理中两块内存之间可以相互借用
https://blog.csdn.net/dengxing1234/article/details/73613484
spark的容错机制是通过血统(lineage)和checkpoint来实现的 。
(HR面试的自我介绍可以侧重软实力部分,项目技术方面介绍可以适当少一些)
在项目中曾经遇到了新的框架不知道该如何上手的问题,以及面对新的概念,新的技术不知道从何学起。解决的办法是在官网寻找说明文档和demo,按照说明文档上的内容一步步了解,以及咨询身边有用过这个框架的同学,或者在CSDN上寻找相关博客。
项目的时间比较紧迫,没有那么多的时间可以用。解决方法是把还没有完成的项目分一个轻重缓急,在有限的时间里,先做重要而且紧急的,然后完成紧急的,再做重要的。利用轻重缓急做一个取舍。
一个是了解了相关框架的使用方法(比如Dataframe的使用,xgboost的使用等等),这些框架或者技术可以在以后的开发中使用到。和对自己开发能力的锻炼。
一个是锻炼了与他人的交流能力,因为在团队项目里经常会跟别人汇报自己的想法和进度,同时也会跟其他成员沟通模块之间的交互,所以在这个过程中对自己的表达能力和理解能力都是一个很大的提升。
一定要往长了说!半年起步,最好七八个月,因为实习生是可以随时跑路的。而且实习时间越长HR越青睐。
hr问这个是因为人一般都不会愿意离开家乡去太远的地方工作,比如你是北方人,可能就不太可能到深圳来发展。所以如果自己的籍贯家乡跟base地太远,记得解释一下你为什么选择来这么远的地方发展,表明你有意愿到这个地方长久发展。
我是比较内向谨慎的人,平时做的多说的少。比较善于总结,在与人交流的时候更倾向于倾听别人的意见后才发言。并且别人都说我办事认真靠谱。
我的缺点是容易在一些细节的地方花费太多的时间,有时候过分追求细节。并且我的实习经验比较缺乏,对于实际项目的业务流程和工作流程不是很了解。(所以我打算通过实习来熟悉实际的软件开发的流程和技术。)
我的优点是责任心比较强,做事比较负责,在校期间我负责的大创项目进展很顺利,我经常组织组员们进行讨论和推进项目的开发,最后这个项目得到了92的评分,在同级别里面是比较高的。
平时的爱好是画画打游戏,在CSDN写写博客,还有就是看书,我很喜欢学到新知识掌握新技能的感觉。
技术类:编程之美 机器学习西瓜书 STL源码剖析 剑指offer C++primer plus
非技术类:明朝那些事儿 香水(聚斯金德) 解忧杂货店 人类简史 沉默的大多数 与时间做朋友(李笑来) 千年历史千年诗
我觉得 任何一家单位都有可能要加班。如果自己的工作没有按时完成,那自觉加班是理所当然的,当然,自己要不断提高工作效率,避免这种原因导致的加班。如果遇到紧急任务或者突发状况时,为了顺利配合团队完成任务,我会尽自己所能加班共同完成。
在工作的第一个阶段,先尽快适应工作的环境,包括开发环境开发工具和工作流程等,把自己负责的部分快速的完成,不能出差错。第二个阶段要熟悉整个项目的业务流程,所有模块的结构和依赖关系,知道每个模块为什么要这么设计,以及它们的实现细节。第三个阶段要培养独立设计一个项目的能力,可以独立或者在别人的协作下设计项目的模块分工和架构。
在工作和项目中多写博客或者笔记,积累技术影响力,将经验总结成文档。同时与同事搞好关系,尝试培养领导能力和组织能力。
可以如实说
踏实 认真
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。