从今年的8月13日投递了第一份简历之后,到现在收到了许多感谢信,还有一些信在路上,目前没有拿到过一个offer,基本上都是倒在一二面上。之前师傅和我说,如果一二面经常挂,说明自己的基础知识不扎实。先在这里整理,最近再投递一批,加油冲吧!
之前在学习的过程中,不去深入了解,不去自己找时间去学相关的面试内容,最终的恶果是一定会产生的,为什么要浪费当初的大好时光呢?
为什么最终选择了找工作,这些都已经不想再去思考,过去的就已经过去了。这一篇博客算是总结自己秋招的第一阶段的内容吧,没有什么经验可以值得借鉴,这里建议:
我今年已经研三了,没有实习,没有offer,八月份才开始刷题,本来打算争取一下读博士以后去教书,由于一些不可抗拒因素,决定先找工作…
找不到工作真的太焦虑了,后面还要写毕业论文,2021年一点也不比2020好过
希望能早日找到工作,祝愿各位都有满意的工作和美好的前程!加油!
后面再多投一投~ 加油~!ヾ(◍°∇°◍)ノ゙
一旦放弃的话,比赛就结束了~
有很多不太记得了,大部分都还是比较基础的;这里包括自己没答上来的,还有一些我看的别人的面经还有一些复习时感觉很重要的点。
感谢学长的博客:https://blog.csdn.net/neverever01/article/details/108237531?spm=1001.2014.3001.5501
复习的时候看了挺多面经,感谢牛客,感谢小林coding,感谢interviewtop,感谢所有分享的人
面试官会让我具体介绍项目的结构,自己做了什么,哪里是难的部分,如何解决,还可能会问数据库表的设计,为什么这样设计,这样符合数据库范式吗?
我的项目自己都感觉没东西,研究生就负责了一小部分的开发和数据库的维护,本科期间开发的项目虽然是系里还在用,但现在回忆起来只记得许多的CRUD操作。
好未来一面的面试官最后建议我,一定要深挖自己的项目,即使自己只负责了一个小部分,别人的内容也可以多了解一些,这样聊得时候才更有话题,实验室一个博士师兄面试的时候和面试官把组里的研究方向都聊了一个遍(想变强还是要努力修炼内功)
即使这个项目真的什么都没有,也可以从框架本身出发或者从语言、数据库本身出发介绍,自己梳理一下,不要简历写的很多很漂亮,结果什么都说不出来
这个部分,面试官问了我三次握手和四次挥手的具体过程,以及为什么两次握手不可以,为什么最后挥手的时候,客户端要等待 2MSL 才关闭连接
计算机网络中应用层、传输层和网络层肯定是最高频的考点,另外会结合专业来问,我是做身份认证相关的,经历过几次面试问我
HTTPS
、数字证书以及OAuth
的内容
三次握手
SYN=1, client_seq=x
的信息,发送之后客户端处于SYN-SENT
状态SYN=1, ACK=1, server_seq=y, server_ack=x+1
的信息,服务器处于SYN-RCVD
状态ACK=1, client_ack=y+1, client_seq=x+1
的信息,客户端进入ESTABLISHED
状态,服务器端收到后进行检查,也进入ESTABLISHED
状态为什么要三次握手
四次挥手 这里假设client主动断开连接
FIN=1, seq=u
的报文,client进入FIN-WAIT-1
ACK=1, seq=v, ack=u+1
报文,告知客户端它收到了报文,但服务器端仍然可以发送数据,客户端进行FIN-WAIT-2
阶段,服务器端进入CLOST-WAIT
阶段FIN=1,ACK=1,seq=w,ack=u+1
,服务器端进入LAST-ACK
阶段ACK=1,seq=u+1,acl=w+1
,服务器端收到后就进入了CLOSED
阶段,客户端进入TIME-WAIT
状态,需要等待2MSL(最大报文生命周期)后,进入CLOSED
阶段ssthresh
时,启动拥塞避免算法HTTPS需要通过TLS建立连接,客户端向服务器端发送Client Hello
并附带随机数、TLS版本信息以及支持的密码套件,服务器端收到后返回Server Hello
并附带新的随机数、对TLS版本信息的确认以及将要使用的密码套件,接下来服务器端会发送自身的服务器证书Server Certificate
(如果是双向鉴别,也会请求客户端发送它的数字证书给服务器),接下来发送Server Hello Done
表示服务器发送完毕,客户端会返回一个确认并且通过自己安装在本地的CA校验服务器的证书,并且取出其中的服务器公钥加密一个预共享密钥,并且发送给服务器端,服务器端通过私钥解密得到预共享密钥,并且生成会话密钥进行加密通信,后续两者还会更换密钥以保证通信的安全Change Cipher Spec
数字证书在其中的主要作用有两个
HTTP是超文本传输协议(HyperText Transfer Protocol),信息明文传输,有安全风险,HTTPS在TCP和HTTP之间增加了SSL/TLS协议,使得报文可以安全传输
HTTP连接建立很简单,只需要TCP三次握手之后即可发送报文;而HTTPS需要在TCP三次握手之后进行TLS握手才可以进行加密报文的传输
HTTP端口号为80,而HTTPS为443
HTTPS需要向CA申请证书来保证服务器的身份是可信的
body
中HTTP
请求信息内存管理、死锁、进程与线程是出现频率最高的内容了
内存管理重点是要从虚拟内存的管理去说,物理内存主要说一下分页管理、分段管理和段页式管理应该就可以,虚拟内存需要从虚拟内存的概念、页面置换算法和页面分配策略去说
最近在阅读《OSTEP》和《C++服务器开发精髓》,希望能尽快补充上这些知识
https://www.cnblogs.com/peterYong/p/6556619.html
https://www.cnblogs.com/peterYong/p/6556615.html
重点是虚拟内存的管理和方式,这个我后面自己再多看几遍再整理思路,这两篇博客挺详细的
死锁
死锁指的是两个线程为了保护两个不同的共享资源而使用了互斥锁,但是这两个进程又都在等待对方释放锁,在没有外力的作用下,这两个线程会一直相互等待而无法继续运行,即发生了 死锁
必要条件
死锁产生的原因
死锁的解决方法
一般情况下,死锁只需要破坏上述四个条件中的一个就可以了,最常见的是使用资源有序分配法,即让两个线程获取资源的顺序一样,必须先获取A才能获取B,来破坏环路等待条件
死锁的恢复 (上述的解决方法是最好的,恢复一般情况下采用终止进程和剥夺资源的方法)
https://mp.weixin.qq.com/s?__biz=MzUxODAzNDg4NQ==&mid=2247485318&idx=1&sn=0da0a684639106f548e9d4454fd49904&chksm=f98e432ccef9ca3ab4e10734fd011c898785f18d842ec3b148c7a8ee500790377858e0dbd8d6&scene=178&cur_album_id=1408057986861416450#rd 小林coding 图解太强了
- 进程之间的通信一定要通过内核
ps -aux | grep xxx
就是匿名管道协程可以认为是在应用层模拟的线程,它看上去像子程序,但是在执行过程中,子程序内部可以中断转而去执行别的子程序,在适当的时候返回执行;但协程不是函数调用,也不是多线程,而是程序自身控制的
这个我搜了一下好像java面试会多一些
公平锁
非公平锁
三个并发进程分别需要3 4 5台设备,当系统只有 ( 3 − 1 ) + ( 4 − 1 ) + ( 5 − 1 ) = 9 (3-1) + (4-1) + (5-1) = 9 (3−1)+(4−1)+(5−1)=9台设备时,第一特进程分配2台,第二个进程分配三台,第四个进程分配四台,这样三个进程都无法继续执行下去,发生死锁;当系统再增加1台设备时,即一共有10台设备时,最后1台设备分配给任意一个进程都可以顺利执行,即最少为10个
$ tail -n number filename
# 功能与head相反, head是取前n行
https://zhuanlan.zhihu.com/p/96934479
# uniq默认只是删除连续行的重复元素
# 可以用于过滤IP地址
$ cat filename | sort | uniq
# 希望保留原来的顺序 $0 是当前正在被处理的行的所有内容
awk '!visited[$0]++' filename
在网上看到,如果是大数据文件,可以利用数据库操作
http://www.xitongzhijia.net/xtjc/20141226/33538.html
银行一般肯定会问数据库的,包括数据库基础的语句和功能,事务、索引的具体实现原理、SQL注入等
主码是可以推导出表中其他列的列的组合
事务是一组原子性的SQL语句,当有任何一条语句因为崩溃或其他原因而无法执行时,所有语句都不会执行
事务中的语句要么全部执行成功,要么都不执行,包含四大特性:
事务的实现需要保证可靠性和并发隔离,主要通过日志恢复和并发控制来实现的(一致性一般由应用层实现)
redo log
和 undo log
两个日志,redo log
记录的是已经成功提交的事务操作信息,用于恢复数据,保证事务的持久性;undo log
是事务修改之前的信息,用于回滚数据,保证事务的原子性隔离级别越高,性能效率越低
对应的问题:
- 脏读:指一个事务在处理过程中读取了另一个还没提交的事务的数据
- A向B转账,A少了100但没提交,此时有一个事务访问B,就查不到新增的数据,A却少了
- 不可重复读:对于数据库中的某个字段,一个事务多次查询却返回了不同的值,这是因为在查询过程中字段被另一个事务修改并提交了
- 幻读:事务多次读取同一个范围的时候,查询结果的记录数目不同,这是由于在查询过程中,另一个事务新增或删除了数据
https://www.jianshu.com/p/73b19cf15e26
可以参考道哥的《白帽子讲Web安全》
sql注入是指通过构建特殊的输入作为参数传入应用程序,通过执行构造的SQL语句而完成攻击,主要原因是因为应用程序没有过滤用户输入的数据,从而导致非法数据侵入系统,包括没有正确过滤的转义字符、盲注的手段和不安全的数据库配置等
防止的方法:数据代码分离原则
哈希表的每一个位置称为一个桶
蝉的哲学
蝉的生命周期总是进化为素数,与天敌的最大公约数为1,可以最大程度避免与天敌相遇
因为当长度为质数时,可以保证哈希表覆盖地最充分,分布地最均匀
同C++
中数组与链表的区别
一些面试官会让直接写,也有面试官只需要听思路,快排出现几率挺高的
时间复杂度 O ( n 2 ) O(n^2) O(n2) 空间复杂度 O ( 1 ) O(1) O(1)
稳定排序
- 每一次排序,让最大的一个元素就位(单趟扫描交换)
// 交换函数,只写了一次
void myswap(vector& arr, int a, int b) {
if(a == b) return;
arr[a] = arr[a]^arr[b];
arr[b] = arr[a]^arr[b];
arr[a] = arr[a]^arr[b];
}
// 冒泡排序
void bubbleSort(vector& nums) {
int sz = nums.size();
bool sorted = false;
while(!sorted) {
sorted = true;
for(int i=1; i nums[i]) {
myswap(nums, i-1, i);
sorted = false;
}
}
sz--;
}
}
时间复杂度 O ( n l o g n ) O(nlog_n) O(nlogn) 空间复杂度 O ( n ) O(n) O(n)
稳定排序
- 使用分治法进行的排序操作
// 归并操作
void merge(vector& nums, int left, int mid, int right) {
vector tmp(right-left+1);
int i = left, j = mid+1, k = 0;
for(; i <= mid || j <= right;) {
if((i <= mid) && (!(j <= right) || nums[i] <= nums[j])) {
tmp[k++] = nums[i++];
}
if((j <= right) && (!(i <= mid) || nums[i] > nums[j])) {
tmp[k++] = nums[j++];
}
}
for(int p=left, k=0; p<=right; ) {
nums[p++] = tmp[k++];
}
}
// 归并排序
void mergeCore(vector& nums, int left, int right) {
if(left >= right) return;
int mid = (left+right) >> 1;
// 递归执行
mergeCore(nums, left, mid);
mergeCore(nums, mid+1, right);
// 归并操作
merge(nums, left, mid, right);
}
// 调用函数
void mergeSort(vector& nums) {
mergeCore(nums, 0, nums.size()-1);
}
时间复杂度 O ( n 2 ) O(n^2) O(n2) 空间复杂度 O ( 1 ) O(1) O(1)
稳定排序
- 遍历数组,并插入到已经排好序的数组的合适的位置
// 插入排序
void injectionSort(vector& nums) {
for(int i=1; i= 0 && nums[preIndex] > currentVal) {
nums[preIndex+1] = nums[preIndex];
preIndex--;
}
// 因为上面循环最后减去了1,所以这里要+1
nums[preIndex+1] = currentVal;
}
}
时间复杂度 O ( n 2 ) O(n^2) O(n2) 空间复杂度 O ( 1 ) O(1) O(1)
不稳定排序
- 遍历数组,选择最大或者最小的元素放在一端
// 选择排序
void selectSort(vector& nums) {
int sz = nums.size();
for(int i=sz-1; i>=0; i--) {
int maxIndex = i;
for(int j=i-1; j>=0; j--) {
if(nums[j] > nums[maxIndex]) {
maxIndex = j;
}
}
myswap(nums, i, maxIndex);
}
}
时间复杂度 O ( n l o g n ) O(nlog_n) O(nlogn) 空间复杂度 O ( 1 ) O(1) O(1)
不稳定排序
- 取一个轴点,所有小于它的元素移动到它的前面,所有大于它的元素,移动到它的后面,接下来对两个子区间分别执行操作,直到达到只剩一个元素,即可完成排序
int partition(vector& nums, int left, int right) {
int pivot = nums[left + (right-left)/2];
while(left < right) {
while(left < right && nums[left]pivot) right--;
if(left < right) myswap(nums, left, right);
}
// 返回支点
return left;
}
void quickCore(vector& nums, int left, int right) {
if(left >= right) return;
int pivot = partition(nums, left, right);
quickCore(nums, left, pivot-1);
quickCore(nums, pivot+1, right);
}
void quickSort(vector& nums) {
quickCore(nums, 0, nums.size()-1);
}
https://www.cnblogs.com/chengxiao/p/6104371.html
时间复杂度 O ( n 3 / 2 O(n^{3/2} O(n3/2) 空间复杂度 O ( 1 ) O(1) O(1)
不稳定排序
- 希尔排序把记录按照下标的一定增量分组,对每一组执行直接插入排序,随着增量减少,每组元素增多,当增量减为1时,算法终止
void shellsort(vector& nums) {
for(int gap=nums.size()/2; gap>0; gap/=2) {
for(int i=gap; i=0 && nums[j]
这个参考的学长的CSDN博客,但是找不到链接了…
时间复杂度 O ( n l o g n ) O(nlogn) O(nlogn) 空间复杂度 O ( 1 ) O(1) O(1)
不稳定排序
- 采用最大堆/最小堆进行排列
// 堆排序
// 构建堆
void adjust(vector& nums, int len, int index) {
int maxid = index;
int left = 2*index+1, right = 2*index+2;
// 寻找以index为根的子树的最大元素的下标
if(leftnums[maxid]) maxid = left;
if(rightnums[maxid]) maxid = right;
// 进行交换
if(maxid != index) {
myswap(nums, maxid, index);
adjust(nums, len, maxid);
}
}
void heapsort(vector& nums, int len) {
// 构建堆
for(int i=(len-1-1)/2; i>=0; i--) {
adjust(nums, len, i);
}
// 从最后一个下标开始遍历,每次将堆顶元素交换到当前位置,并且缩小长度
for(int i=len-1;i>=0;i--) {
myswap(nums, 0, i);
// 全部调整
adjust(nums, i, 0);
}
}
https://www.cnblogs.com/fengcc/p/5256337.html
实际中采用快速排序 结合插入排序和堆排序的方法,当快速排序分段数据量较小时,采用插入排序,如果递归层数过深,会使用堆排序
c++基础我可以说是超级菜了,不知道哪里来的勇气去找c++开发岗,这里是面试官问到过我的问题,我看前人的面经还有许多STL的问题,包括但不限于struct和class的区别,引用和指针的区别,C++内存管理(堆、栈、全局变量区、字面量存储区、代码区),const和define的区别,内联函数…
暂时先列出了自己经历过的题目
这里也可以多说一些,vector与list重载操作符的区别
vector
就是动态数组,拥有一段连续的内存空间,当插入新的元素内存不足时,通常以2倍申请更大的内存,把数据移入新的空间并释放旧空间;插入删除时会导致内存块的拷贝,时间复杂度为O(n)
,但是访存很快,为O(1)
list
是基于双向链表实现的,内存空间不连续,可以高效执行添加和删除,但是随机存取很慢为O(n)
auto
类型说明符,让编译器帮助程序员从初始化表达式中分析出变量的数据类型decltype
类型指示符,选择并返回操作数的数据类型,编译器分析表达式并得到它的返回值,但是不会实际计算表达式的值nullptr
字面量,它是一个void*
类型的,而原有的NULL
是0。一般需要初始化所有指针,如果不知道需要指向哪里,可以先赋值为 nullptr
final
和 override
关键字,final
修饰类不允许被继承,override
关键字用于标明是重写的父类方法 (注意,这两个关键字都是写在最后,如class A final {};
和 void func(int a) override {}
)lambda
表达式,类似于 javascript
中的必报,可以用于创建匿名函数对象[函数对象参数](操作符重载函数参数)mutable或exception声明->返回值类型{函数体}
=default
和=delete
语法,用于强制声明或不声明构造方法for-each
的循环遍历方式unique_ptr
和shared_ptr
智能指针thread
和mutex
类c++ 中所有的智能指针都在
头文件中,其中在 c++11中 std::auto_ptr
只能指针已经被废弃,智能指针对指针进行了封装,可以像普通指针一样使用,但是可以随着智能指针对象的释放而释放,避免造成内存泄露。
std::auto_ptr
的主要问题在于,在复制一个 auto_ptr
对象的时候(无论是拷贝构造函数还是赋值语句),原有堆内存对象都会被转移出来给新的对象,原有的 auto_ptr
就指向了空,这样在访问旧指针对象的时候可能会出问题,而且在使用 stl 的时候可能导致大量 空指针的出现std::unique_ptr
对堆内存有唯一的拥有权,它禁止实现拷贝构造函数和重载=
(标记为=delete
),保证一块堆对象只有一个指针指向它std::shared_ptr
允许一个资源对象对应多个指针,每多一个 std::shared_ptr
对资源的引用,引用计数就会+1,析构的时候会-1,当最后一个 std::shared_ptr
析构的时候,就会彻底释放资源std::weak_ptr
是对对象的弱引用,不会控制资源的生命周期,为了协助std::shared_ptr
工作,它的构造和析构不会导致计数增加或减少,用于观测std::shared_ptr
的引用计数,防止死锁auto_ptr
和unique_ptr
与裸指针的大小一样,而shared_ptr
和weak_ptr
是裸指针的2倍
悬挂指针 指向已被删除或释放的内存位置的指针
nullptr
空指针 指向空的指针
野指针 尚未初始化的指针,可能被初始化为不是有效地址的非空垃圾值
nullptr
https://blog.csdn.net/lihao21/article/details/50688337
C++的多态包括两种:不带继承的多态(即编译时多态,主要体现在函数重载和模板上)以及带继承的多态(即运行时多态,通过虚函数来实现,在单一对象中展现多种类型)
virtual
关键字,在派生类中重写该函数,运行时将根据对象的实际类型调用相应的函数*_vptr
用于指向虚函数表,它会指向自己所属类的虚函数表,虚函数表的指针会指向其继承的最近的类的虚函数运行时多态
;而普通的函数调用是静态绑定好未来二面面试官问的,这个真的听都没听过我太菜了,具体可以参考下面的链接
数据结构、操作系统、计算机网络都能考察到…
https://cloud.tencent.com/developer/article/1181632
通过发号策略,每过来一个长地址发一个号(62进制a-zA-Z0-9)即可,小型的系统可以直接用mysql的自增索引,大型系统可以使用分布式的key-value做发号器(可以采用Redis),然后通过302重定向到长链接
这样的问题或许没有最标准的答案,只要自己有思路,能说清楚就可以
一栋楼有 100 层,在第 N 层或者更高扔鸡蛋会破,而第 N 层往下则不会。给 2 个鸡蛋,求 N,要求最差的情况下扔鸡蛋的次数最少。
我面试比较少,后面估计会有很难的,我只记住了这几个…
我感觉剑指 offer 和 leetcode 100 刷个几遍应该还是够用的,关键是能从头写,ACM格式
力扣 21
https://leetcode-cn.com/problems/merge-two-sorted-lists/
#include
using namespace std;
struct ListNode {
int val;
ListNode* next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode* next) : val(x), next(next) {}
};
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
if(l1 == nullptr) return l2;
if(l2 == nullptr) return l1;
ListNode* fakeNode = new ListNode(0);
ListNode* iNode = fakeNode;
ListNode* il1 = l1;
ListNode* il2 = l2;
while(il1 != nullptr || il2 != nullptr) {
if(il1 == nullptr) {
iNode->next = il2;
break;
}
if(il2 == nullptr) {
iNode->next = il1;
break;
}
if(il1->val > il2->val) {
iNode->next = il2;
iNode = iNode->next;
il2 = il2->next;
} else {
iNode->next = il1;
iNode = iNode->next;
il1 = il1->next;
}
}
ListNode* resNode = fakeNode->next;
delete fakeNode;
fakeNode = nullptr;
return resNode;
}
力扣 剑指offer 24
https://leetcode-cn.com/problems/UHnkqh/
ListNode* reverseList(ListNode* head) {
if(head == nullptr) return nullptr;
ListNode* prev = nullptr;
ListNode* iNode = head;
ListNode* next;
while(iNode != nullptr) {
next = iNode->next;
iNode->next = prev;
prev = iNode;
iNode = next;
}
return prev;
}
力扣 141
https://leetcode-cn.com/problems/linked-list-cycle/
bool hasCycle(ListNode* head) {
if(head == nullptr) return false;
ListNode* slow = head;
ListNode* fast = head;
while(fast != nullptr && fast->next != nullptr) {
fast = fast->next->next;
slow = slow->next;
if(fast == slow) {
return true;
}
}
return false;
}
#include
#include
int main() {
char str[100] = {0}, res[100] = {0};
int maxlen = 0, left=0, right=0, maxLeft=left, maxRight=right;
scanf("%[^\n]s", str);
for(int i=0; i maxlen) {
maxlen = right-left+1;
maxLeft = left;
maxRight = right;
}
right++;
left = right;
} else {
right++;
}
}
strncpy(res, str+left, maxlen);
printf("%s\n", res);
return 0;
}
剑指offer 62
https://leetcode-cn.com/problems/yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-lcof/
常规做法会超出时间限制,可以采用数学规律的方法,详见剑指offer
class Solution {
public:
int lastRemaining(int n, int m) {
if(n<1 || m<1) return -1;
int i = 0;
list nums;
for(i=0; i::iterator cur = nums.begin();
while(nums.size() > 1) {
for(int i=1; i::iterator next = ++cur;
if(next == nums.end())
{
next = nums.begin();
}
--cur;
nums.erase(cur);
cur = next;
}
return *cur;
}
};
使用的时候需要注意函数的原型为:
const 防止源字符串被修改
char* strcpy(char* dst, const char* src);
需要考虑内存重叠的情况,从后向前拷贝,可以参考;
https://www.cnblogs.com/chenyg32/p/3739564.html
char* my_memcpy(char* dst, const char* src, int cnt) {
assert(dst != NULL && src != NULL);
char* ret = dst;
// 内存重叠的情况
if(dst >= src && dst <= src+cnt-1){
dst = dst+cnt-1;
src = src+cnt-1;
while(cnt--) {
*dst-- = *src--;
}
} else {
while(cnt--) {
*dst++ = *src++;
}
}
return ret;
}
char* strcpy(char* dst, const char* src) {
assert(dst != NULL && src != NULL);
char* ret = dst;
my_memcpy(dst, stc, strlen(src)+1);
return ret;
}
目前就只有渤海银行HR在面试的时候提问了一些问题,HR面应该要着重体现自己的软实力
除了以下的内容,还可能有自我介绍、兴趣爱好、优缺点、如何看待加班等
后来反思了一下,这其实是在问我的职业生涯规划,我当时只回答了从业务出发,提高自己的技术能力和业务能力。
可以分几个阶段来说:
第一阶段:先适应工作环境,开发的工具和流程
第二阶段:熟悉业务流程以及系统设计的原理
第三阶段:提高自己的思维能力和管理能力,尝试去负责一个大的项目
这个可以如实回答,一方面公司可能真的会考虑你选择这边的可能性,另一方面也可以用来抬价
比如有没有对象?家乡是哪里的?银行可能真的会从这个方面进行筛选