第一阶段笔试面试经历 0 offer 2021年9月21日

2021.09.21

从今年的8月13日投递了第一份简历之后,到现在收到了许多感谢信,还有一些信在路上,目前没有拿到过一个offer,基本上都是倒在一二面上。之前师傅和我说,如果一二面经常挂,说明自己的基础知识不扎实。先在这里整理,最近再投递一批,加油冲吧!

之前在学习的过程中,不去深入了解,不去自己找时间去学相关的面试内容,最终的恶果是一定会产生的,为什么要浪费当初的大好时光呢?

为什么最终选择了找工作,这些都已经不想再去思考,过去的就已经过去了。这一篇博客算是总结自己秋招的第一阶段的内容吧,没有什么经验可以值得借鉴,这里建议:

  • 大家如果是想进互联网相关的企业去工作,能早点实习去接触真实的项目还是尽早去吧,经过半年到一年的实习,竞争力、眼界和学习能力都会非常不一样
  • 如果学校或者导师不允许实习,也可以投一些简历,远程或者现场面试感受一下氛围,也可以帮助尽早找到自己学习的方向

我今年已经研三了,没有实习,没有offer,八月份才开始刷题,本来打算争取一下读博士以后去教书,由于一些不可抗拒因素,决定先找工作…

找不到工作真的太焦虑了,后面还要写毕业论文,2021年一点也不比2020好过
希望能早日找到工作,祝愿各位都有满意的工作和美好的前程!加油!

笔试和面试经历

  • 2021.08.15 好未来笔试 2021.08.22 好未来1、2面 挂
    • 一面的时候就感觉凉了,但是今年一二面一起进行,还有二面的机会,二面结束的时候面试官就给了感谢信,还是挺好的,这个时候就意识到自己可能会非常难了
  • 2021.08.16 阿里笔试 挂
    • 脑瓜子嗡嗡的
  • 2021.08.17 BIGO笔试 2021.08.24 BIGO 面试 凉
    • 临时被安排了整理RFC的任务,整理到了下午七点,错过了面试时间…没有后续
  • 2021.08.24 联想天津 笔试 2021.09.13 联想一面 挂
  • 2021.08.29 渤海银行笔试 2021.09.06 渤海银行面试 无结果
    • 面试超级快,好像有个十一二三分钟,而且HR面了挺久
  • 2021.08.31 深信服笔试 2021.09.04 深信服一面 2021.09.12 深信服二面 无结果
    • (问了一下HR,她说可能下面是线下面试,不知道是不是泡池子了)
  • 2021.09.06 农行天津研发 笔试 2021.09.15 农行天津研发 已凉
    • 面试 六分钟,而且巨简单的问题答错了
  • 2021.09.15 华为笔试 无结果

后面再多投一投~ 加油~!ヾ(◍°∇°◍)ノ゙

一旦放弃的话,比赛就结束了~

笔试和面试的内容记录

有很多不太记得了,大部分都还是比较基础的;这里包括自己没答上来的,还有一些我看的别人的面经还有一些复习时感觉很重要的点。

感谢学长的博客:https://blog.csdn.net/neverever01/article/details/108237531?spm=1001.2014.3001.5501

复习的时候看了挺多面经,感谢牛客,感谢小林coding,感谢interviewtop,感谢所有分享的人

项目简历深挖

面试官会让我具体介绍项目的结构,自己做了什么,哪里是难的部分,如何解决,还可能会问数据库表的设计,为什么这样设计,这样符合数据库范式吗?

我的项目自己都感觉没东西,研究生就负责了一小部分的开发和数据库的维护,本科期间开发的项目虽然是系里还在用,但现在回忆起来只记得许多的CRUD操作。

好未来一面的面试官最后建议我,一定要深挖自己的项目,即使自己只负责了一个小部分,别人的内容也可以多了解一些,这样聊得时候才更有话题,实验室一个博士师兄面试的时候和面试官把组里的研究方向都聊了一个遍(想变强还是要努力修炼内功

即使这个项目真的什么都没有,也可以从框架本身出发或者从语言、数据库本身出发介绍,自己梳理一下,不要简历写的很多很漂亮,结果什么都说不出来

计算机网络

这个部分,面试官问了我三次握手和四次挥手的具体过程,以及为什么两次握手不可以,为什么最后挥手的时候,客户端要等待 2MSL 才关闭连接

计算机网络中应用层、传输层和网络层肯定是最高频的考点,另外会结合专业来问,我是做身份认证相关的,经历过几次面试问我 HTTPS、数字证书以及OAuth的内容

TCP 三次握手?为什么两次握手不可以?

三次握手

  • 第一次握手,client分配相关资源并向server发送连接请求报文,在此报文中,包含了SYN=1, client_seq=x的信息,发送之后客户端处于SYN-SENT状态
  • 第二次握手,server收到了client的请求,分配相关的资源,给client返回一个ACK报文,包含SYN=1, ACK=1, server_seq=y, server_ack=x+1的信息,服务器处于SYN-RCVD状态
  • 第三次握手,client收到了server的请求,检查相关字段,确认server收到了报文,于是给server返回一个ACK报文,包含ACK=1, client_ack=y+1, client_seq=x+1的信息,客户端进入ESTABLISHED状态,服务器端收到后进行检查,也进入ESTABLISHED状态

为什么要三次握手

  • 服务器端需要确认客户端可以接收到自己发送的消息,否则容易产生死锁
  • 如果只要两次握手容易遭受SYN泛洪攻击
TCP四次挥手的过程?为什么四次挥手最后要等待 2MSL?

四次挥手 这里假设client主动断开连接

  • 第一次挥手:当client没有数据发送给server了,给服务器端发送一个FIN=1, seq=u的报文,client进入FIN-WAIT-1
  • 第二次挥手:server收到了client的报文,会返回一个ACK报文,包括ACK=1, seq=v, ack=u+1报文,告知客户端它收到了报文,但服务器端仍然可以发送数据,客户端进行FIN-WAIT-2阶段,服务器端进入CLOST-WAIT阶段
  • 第三次挥手:server发送完数据之后,会发送给客户端一个FIN报文,包括FIN=1,ACK=1,seq=w,ack=u+1,服务器端进入LAST-ACK阶段
  • 第四次挥手:client收到FIN报文之后,返回给服务器端一个ACK报文,包括ACK=1,seq=u+1,acl=w+1,服务器端收到后就进入了CLOSED阶段,客户端进入TIME-WAIT状态,需要等待2MSL(最大报文生命周期)后,进入CLOSED阶段
介绍一下拥塞控制的算法?
  • 慢启动:TCP刚建立连接之后,会一点一点提高发送数据报的数量,即每当发送方收到一个ACK,就会将拥塞窗口的大小变为原来的2倍;当增长到慢启动门限ssthresh时,启动拥塞避免算法
  • 拥塞避免:为了避免拥塞,超过慢启动门限后,拥塞窗口的大小每收到一个ACK,加1,为线性增长
  • 快重传:当出现拥塞的时候,发生了丢包的现象,快速重传算法会将拥塞窗口变为原来的一半,并且让慢启动门限变为拥塞窗口大小
    • 如果不进行快速重传,进行超时重传,则是将慢启动门限设置为拥塞窗口大小的一半,并且将拥塞窗口大小置为1
  • 快恢复:让拥塞窗口大小 +3 (即确认收到了三个数据报),并执行拥塞避免的线性增长
TCP和UDP的区别
  • TCP是面向连接的协议,提供可靠传输,在收发数据之前需要通过三次握手建立连接,并且通过校验、序号等机制保证传输数据的正确性;UDP是无连接的协议,提供尽最大努力交付,适合实时应用
  • TCP提供流量控制和拥塞控制,而UDP没有
  • TCP对系统资源要求较高,速度比UDP慢
  • TCP数据报没有边界,可能出现粘包问题,而UDP是一个独立的数据报,不会出现粘包现象
  • TCP提供点到点的通信,而UDP支持一对一、一对多、多对多的通信
HTTPS的原理?数字证书在其中是用来干什么的?

HTTPS需要通过TLS建立连接,客户端向服务器端发送Client Hello并附带随机数、TLS版本信息以及支持的密码套件,服务器端收到后返回Server Hello并附带新的随机数、对TLS版本信息的确认以及将要使用的密码套件,接下来服务器端会发送自身的服务器证书Server Certificate(如果是双向鉴别,也会请求客户端发送它的数字证书给服务器),接下来发送Server Hello Done表示服务器发送完毕,客户端会返回一个确认并且通过自己安装在本地的CA校验服务器的证书,并且取出其中的服务器公钥加密一个预共享密钥,并且发送给服务器端,服务器端通过私钥解密得到预共享密钥,并且生成会话密钥进行加密通信,后续两者还会更换密钥以保证通信的安全Change Cipher Spec

数字证书在其中的主要作用有两个

  • 包含了服务器的身份信息,用于验证服务器的身份信息
  • 包含了服务器的公钥用于加密会话密钥
HTTPS 与 HTTP的区别
  • HTTP是超文本传输协议(HyperText Transfer Protocol),信息明文传输,有安全风险,HTTPS在TCP和HTTP之间增加了SSL/TLS协议,使得报文可以安全传输

  • HTTP连接建立很简单,只需要TCP三次握手之后即可发送报文;而HTTPS需要在TCP三次握手之后进行TLS握手才可以进行加密报文的传输

  • HTTP端口号为80,而HTTPS为443

  • HTTPS需要向CA申请证书来保证服务器的身份是可信的

HTTP中 POST 和 GET的区别
  • GET是请求从服务器获取数据;POST是向URI上指定的资源提交数据,数据放在报文的body
  • GET是HTTP安全的,不会破坏服务器上的资源,而POST则是不安全的,因为它会修改服务器上的资源
  • GET是幂等的,多次提交的结果是一样的,而POST多次提交就会创建多份资源,不是幂等的
  • GET请求的数据附加在URL中,传输数据大小受到URL大小限制
  • POST请求会先把请求头发送给服务器确认,然后才会发送真正的数据
HTTP 状态码
  • 1xx 提示信息,代表当前是协议被处理的阶段,还需要后续操作
  • 2xx 请求成功,请求报文已经收到并且被成功处理
    • 200 ok 表明请求成功
  • 3xx 重定向,资源位置发生了变动,需要客户端重新发送请求
    • 301 Moved Permanently 永久重定向
    • 302 Moved Temporarily 临时重定向
  • 4xx 客户端错误,客户端发送的报文有误,服务器端无法处理
    • 400 Bad Request 客户端请求有误
    • 403 Forbidden 服务器禁止访问资源
    • 404 Not Found 请求的资源不存在
  • 5xx 服务器错误,服务器处理请求的时候内部出了错误
    • 500 Interval Server Error 服务器端出现了错误
    • 502 Bad Gateway 网关返回的错误码
输入一条URL到显示出页面的过程?
  • 浏览器对URL进行解析,生成发送给服务器的HTTP请求信息
  • 向DNS服务器查询服务器域名对应的IP地址
  • 通过三次握手建立TCP连接
  • 客户端向服务器端发送HTTP连接请求
  • 服务器端接收客户端发来的HTTP请求并处理,返回响应
  • 释放TCP连接
  • 客户端收到HTTP响应并将结果渲染给用户
DNS协议的过程?(迭代过程)
  • 客户端发送DNS请求,询问域名对应的IP,发送给本地域名服务器
  • 本地域名服务器收到客户端请求后,先查询缓存,如果查询到就直接返回IP,如果没有,就会查询根域名服务器
  • 根域名服务器根据请求告知本地域名服务器应该查询的顶级域名服务器
  • 顶级域名服务器同样根据请求告知本地域名服务器应该查询的权威DNS服务器
  • 权威域名服务器会将查询后的IP地址告知本地域名服务器
  • 本地域名服务器再将IP地址返回客户端,客户端与目标建立连接
笔试题
  1. 计算机网络是通信技术计算机技术相结合的产物
操作系统

内存管理、死锁、进程与线程是出现频率最高的内容了

内存管理重点是要从虚拟内存的管理去说,物理内存主要说一下分页管理、分段管理和段页式管理应该就可以,虚拟内存需要从虚拟内存的概念、页面置换算法和页面分配策略去说

最近在阅读《OSTEP》和《C++服务器开发精髓》,希望能尽快补充上这些知识

内存管理

https://www.cnblogs.com/peterYong/p/6556619.html

https://www.cnblogs.com/peterYong/p/6556615.html

重点是虚拟内存的管理和方式,这个我后面自己再多看几遍再整理思路,这两篇博客挺详细的

死锁是什么?它产生的必要条件是什么?死锁的解决方法?

死锁

死锁指的是两个线程为了保护两个不同的共享资源而使用了互斥锁,但是这两个进程又都在等待对方释放锁,在没有外力的作用下,这两个线程会一直相互等待而无法继续运行,即发生了 死锁

必要条件

  • 互斥条件:多个线程不能同时拥有一个资源
  • 拥有并等待条件:线程A在等待资源2的同时不会放弃自己的资源1
  • 不可剥夺条件:线程A持有的资源在它释放之前不能被别的资源获取
  • 环路等待条件:两个线程获取资源的次序构成了环形链

死锁产生的原因

  • 系统资源不足
  • 资源的分配和线程的运行方式不恰当

死锁的解决方法

一般情况下,死锁只需要破坏上述四个条件中的一个就可以了,最常见的是使用资源有序分配法,即让两个线程获取资源的顺序一样,必须先获取A才能获取B,来破坏环路等待条件

死锁的恢复 (上述的解决方法是最好的,恢复一般情况下采用终止进程和剥夺资源的方法)

  • 重新启动
  • 终止进程,回收资源
  • 剥夺资源
  • 进程回退
进程与线程的区别和联系
  • 进程是资源分配(内存、打开的文件等)的单位,而线程是CPU调度的单位
  • 进程的创建需要系统分配内存、文件句柄等,销毁时也需要回收,而线程只需要创建自己的寄存器和栈等少部分资源,管理的开销很小
  • 进程之间不会相互影响,但是进程中的一个线程崩溃,进程的线程也会同时崩溃
  • 一个进程可以有多个线程,而一个线程只属于一个进程
进程通信方式

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 就是匿名管道
    • 命名管道允许非亲缘关系的进程通信
  • 消息队列 不适合较大数据的传输,数据通信过程中存在用户态与内核态之间的数据拷贝开销
    • 消息队列时保存在内核中的消息链表,发送数据时会分成一个个独立的数据单元(用户自定义的数据类型),每个消息体具有固定的大小和类型;当进程从消息队列中读取了消息体,内核就会把这个消息体删除
  • 共享内存
    • 拿出一块虚拟地址空间来,映射到相同的物理内存中,一个进程的写入可以被另一个进程看到
  • 信号量
    • 信号量是一种计数器,用来控制多个进程对于共享资源的访问,P是将资源值减1或挂起进程,V是将资源值加1或将进程恢复运行。包括互斥信号量和同步信号量
  • 信号
    • 信号是进程间通信机制中唯一的异步通信机制,可以在任意时刻发送信号给一进程,一旦有信号产生,用户进程就需要对信号进行处理
      • 执行默认操作
      • 捕捉信号
      • 忽略信号
  • Socket
    • 可以用于不同设备之间的进程通信
协程

协程可以认为是在应用层模拟的线程,它看上去像子程序,但是在执行过程中,子程序内部可以中断转而去执行别的子程序,在适当的时候返回执行;但协程不是函数调用,也不是多线程,而是程序自身控制的

  • 它没有多线程的线程切换的开销
  • 不需要多线程的锁机制,因为只有一个线程,不会产生冲突
公平锁与非公平锁

这个我搜了一下好像java面试会多一些

公平锁

  • 多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,按照队列的顺序依次获得锁
    • 优点:所有线程都能获得资源,不会饿死在队列中
    • 缺点:吞吐量会下降很多,队列中除了拿到锁的线程,其他线程都会阻塞

非公平锁

  • 多个线程去获取锁的时候,会直接尝试获取,获取不到再去等待队列,如果可以直接获取就直接获取锁
    • 优点:可以减少CPU唤醒线程的开销,整体吞吐量高
    • 缺点:导致队列中间的一些线程一直或长期获取不到锁而饿死
笔试题目
  1. 某系统有 n 台互斥使用的同类设备,3 个并发进程需要 3,4,5 台设备,可确保系统不发生死锁的设备数 n 最小为多少?

三个并发进程分别需要3 4 5台设备,当系统只有 ( 3 − 1 ) + ( 4 − 1 ) + ( 5 − 1 ) = 9 (3-1) + (4-1) + (5-1) = 9 (31)+(41)+(51)=9台设备时,第一特进程分配2台,第二个进程分配三台,第四个进程分配四台,这样三个进程都无法继续执行下去,发生死锁;当系统再增加1台设备时,即一共有10台设备时,最后1台设备分配给任意一个进程都可以顺利执行,即最少为10个

Linux相关
如何读取一个文本文件的最后几行?
$ 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

  • 首先把数据导入一个数据表中
  • 创建一个新的数据表,对于每一个重复的字段可以只导出一行到新表中
  • 然后重新导出文件
MySQL数据库

银行一般肯定会问数据库的,包括数据库基础的语句和功能,事务、索引的具体实现原理、SQL注入等

数据库的三大范式

主码是可以推导出表中其他列的列的组合

  • 1NF是数据库最基本的范式,要求数据库表中所有字段值都是不可以分解的原子值
  • 2NF是在1NF的基础上,确保表中的每一列都完全函数依赖于主码,必须由主码唯一确定,而不能是主码的一部分
  • 3NF是在2NF的基础上,确保非主码的列没有传递函数依赖,即非主码的列不能推导出另一个非主码的列,每一列必须和主码相关
执行查询语句的过程
  • 连接器:客户端通过连接器进行身份和权限的验证,验证通过即连接成功
  • 缓存:如果缓存中有相同的语句,则会直接返回,没有命中缓存的话进入下一步
  • 分析器:SQL语句经过分析器,进行语法语义的检查,判断SQL语句是否正确,如果错误就直接返回给客户端错误信息,如果语法正确则进入优化器
  • 优化器:优化器对SQL语句进行优化处理,比如判断哪个索引的性能更好
  • 执行器:优化器优化文成进入执行器,执行语句进行查询,查询到符合条件的数据记录然后返回
事务是什么以及事务的特性?

事务是一组原子性的SQL语句,当有任何一条语句因为崩溃或其他原因而无法执行时,所有语句都不会执行

事务中的语句要么全部执行成功,要么都不执行,包含四大特性:

  • 原子性 Atomic:事务是一个不可以分割的工作单位,这组操作要么全部完成,要么全部不发生
  • 一致性 Consistency:事务总是从一个一致性状态转向另一个一致性状态
    • 一致性依赖于应用层,主要体现为数据库表字段约束的一致性,比如空值约束、UNIQUE约束,如果执行成功后不满足约束,则刚刚的操作需要回滚
  • 隔离性 Isolation:数据库事务不会受到另一个并发执行的事务的影响
    • 对于数据库中正在执行的事务来说,其他事务要么还没开始,要么已经执行结束
  • 持久性 Durability:事务对数据库的改变时永久性的,数据库崩溃对于已经提交的事务不会产生影响
事务的实现原理?

事务的实现需要保证可靠性和并发隔离,主要通过日志恢复和并发控制来实现的(一致性一般由应用层实现)

  • 日志恢复:利用数据库的 redo logundo log两个日志,redo log记录的是已经成功提交的事务操作信息,用于恢复数据,保证事务的持久性undo log是事务修改之前的信息,用于回滚数据,保证事务的原子性
  • 并发控制:通过读写锁和MVCC(多版本并发控制)来保证隔离性
数据库的隔离级别以及对应可能出现的问题?

隔离级别越高,性能效率越低

对应的问题:

  • 脏读:指一个事务在处理过程中读取了另一个还没提交的事务的数据
    • A向B转账,A少了100但没提交,此时有一个事务访问B,就查不到新增的数据,A却少了
  • 不可重复读:对于数据库中的某个字段,一个事务多次查询却返回了不同的值,这是因为在查询过程中字段被另一个事务修改并提交了
  • 幻读:事务多次读取同一个范围的时候,查询结果的记录数目不同,这是由于在查询过程中,另一个事务新增或删除了数据
  • 读未提交 Read Uncommited 一个事务没有提交时,它所做的变更可以被其他事务看到
    • 可能导致脏读、不可重复读、幻读
  • 读提交 Read Commited 一个事务提交之后,它做的更改才可以被其他事务看到
    • 可能导致 不可重复读、幻读
    • Oracle 的默认隔离级别
  • 可重复读 Repeatable Read 一个事务执行过程中看到的数据与此事务启动时看到的数据是一致的
    • MySQL InnoDB的默认隔离级别,可能导致 幻读
    • 可以加 next-key lock 避免幻读
  • 串行化 Serializable 事务会对记录加读写锁,如果对某条记录进行读写操作时发生了读写冲突,后面访问的事务必须要等待前面事务执行完成后才可以继续执行
sql注入了解吗?怎么防止sql注入?

https://www.jianshu.com/p/73b19cf15e26

可以参考道哥的《白帽子讲Web安全》

sql注入是指通过构建特殊的输入作为参数传入应用程序,通过执行构造的SQL语句而完成攻击,主要原因是因为应用程序没有过滤用户输入的数据,从而导致非法数据侵入系统,包括没有正确过滤的转义字符、盲注的手段和不安全的数据库配置等

防止的方法:数据代码分离原则

  • 预编译语句,绑定变量,让攻击者无法执行插入的语句
  • 避免使用原始的SQL语句执行,尽量使用安全地函数来执行数据库的命令
  • 使用最小权限原则,避免在应用程序中直接使用具有特殊权限的用户来连接
数据结构
哈希表如果产生了哈希冲突应该怎么处理?

哈希表的每一个位置称为一个桶

  • 开链法:每个桶存放一个指针,当有冲突的词条的时候,就把元素插入到相应的链表位置
  • 线性探查:当元素的哈希值对应的桶不能存元素时,依次向后试探每个桶直到找到空桶;查找的时候也是,依次向后试探,试探到空桶则失败(这样可能导致一个向后试探会产生连锁反应)
  • 平方探查:当元素的哈希值对应的桶不能存元素时,以平方值的距离( 1 2 , 2 2 , 3 2 , 4 2 . . . 1^2, 2^2, 3^2, 4^2... 12,22,32,42...)依次试探下一个元素(这样可能导致大量的不能遍历到的空间)
  • 双向平方探查:当元素的哈希值对应的桶不能存元素时,以平方值的距离( 1 2 , 2 2 , 3 2 , 4 2 . . . 1^2, 2^2, 3^2, 4^2... 12,22,32,42...)依次向前向后试探
  • 双散列函数法:当第一个散列函数发生冲突时,使用第二个散列函数进行哈希作为移动步长
哈希表为什么要选择一个质数作为长度?

蝉的哲学

蝉的生命周期总是进化为素数,与天敌的最大公约数为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++基础我可以说是超级菜了,不知道哪里来的勇气去找c++开发岗,这里是面试官问到过我的问题,我看前人的面经还有许多STL的问题,包括但不限于struct和class的区别,引用和指针的区别,C++内存管理(堆、栈、全局变量区、字面量存储区、代码区),const和define的区别,内联函数…

暂时先列出了自己经历过的题目

vector 和 list 的区别

这里也可以多说一些,vector与list重载操作符的区别

  • vector就是动态数组,拥有一段连续的内存空间,当插入新的元素内存不足时,通常以2倍申请更大的内存,把数据移入新的空间并释放旧空间;插入删除时会导致内存块的拷贝,时间复杂度为O(n),但是访存很快,为O(1)
  • list是基于双向链表实现的,内存空间不连续,可以高效执行添加和删除,但是随机存取很慢为O(n)
c++ 11 的特性了解吗?
  • auto类型说明符,让编译器帮助程序员从初始化表达式中分析出变量的数据类型
  • decltype类型指示符,选择并返回操作数的数据类型,编译器分析表达式并得到它的返回值,但是不会实际计算表达式的值
  • nullptr字面量,它是一个void*类型的,而原有的NULL是0。一般需要初始化所有指针,如果不知道需要指向哪里,可以先赋值为 nullptr
  • finaloverride关键字,final修饰类不允许被继承,override关键字用于标明是重写的父类方法 (注意,这两个关键字都是写在最后,如class A final {};void func(int a) override {}
  • lambda表达式,类似于 javascript中的必报,可以用于创建匿名函数对象

[函数对象参数](操作符重载函数参数)mutable或exception声明->返回值类型{函数体}

  • 增加了 =default=delete语法,用于强制声明或不声明构造方法
  • 新增了for-each的循环遍历方式
  • 新增了unique_ptrshared_ptr智能指针
  • 新增了threadmutex
谈一下c++中的智能指针?

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_ptrunique_ptr与裸指针的大小一样,而shared_ptrweak_ptr是裸指针的2倍

野指针、悬挂指针和空指针是什么?

悬挂指针 指向已被删除或释放的内存位置的指针

  • 一般释放对象后把指针指向 nullptr

空指针 指向空的指针

野指针 尚未初始化的指针,可能被初始化为不是有效地址的非空垃圾值

  • 初始化时将指针指向 nullptr
多态的含义与实现?

https://blog.csdn.net/lihao21/article/details/50688337

C++的多态包括两种:不带继承的多态(即编译时多态,主要体现在函数重载和模板上)以及带继承的多态(即运行时多态,通过虚函数来实现,在单一对象中展现多种类型)

  • 虚函数是指在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将根据对象的实际类型调用相应的函数
  • 虚函数是通过虚函数表实现的,每一个具有虚函数的类或继承自具有虚函数的类都有自己的一个虚函数表,虚函数表是一个指针数组,每个元素对应一个虚函数的函数指针
    • 每个对象都有一个虚函数表的指针*_vptr用于指向虚函数表,它会指向自己所属类的虚函数表,虚函数表的指针会指向其继承的最近的类的虚函数
    • 这种调用方式称为动态绑定,表现出来的现象是运行时多态;而普通的函数调用是静态绑定
场景题
如何缩短url的长度并做唯一映射?如何保证高并发时的稳定性?

好未来二面面试官问的,这个真的听都没听过我太菜了,具体可以参考下面的链接

数据结构、操作系统、计算机网络都能考察到…

https://cloud.tencent.com/developer/article/1181632

通过发号策略,每过来一个长地址发一个号(62进制a-zA-Z0-9)即可,小型的系统可以直接用mysql的自增索引,大型系统可以使用分布式的key-value做发号器(可以采用Redis),然后通过302重定向到长链接

有25匹马和5条赛道,赛马过程中无法进行计时,只能知道相对快慢。问最少需要几场赛马可以知道前3名?

这样的问题或许没有最标准的答案,只要自己有思路,能说清楚就可以

  • 首先,25匹马平均分成5组,每组5匹,分别赛跑进行排名(5次)
  • 每一组的第一名进行比赛,按照这一次的排名对上述五组进行排序 (1次)
    • 得出最快的一名,假设为 A 1 A_1 A1
      • A 1 A 2 A 3 A 4 A 5 A_1A_2A_3A_4A_5 A1A2A3A4A5
      • B 1 B 2 B 3 B 4 B 5 B_1B_2B_3B_4B_5 B1B2B3B4B5
      • C 1 C 2 C 3 C 4 C 5 C_1C_2C_3C_4C_5 C1C2C3C4C5
      • D 1 D 2 D 3 D 4 D 5 D_1D_2D_3D_4D_5 D1D2D3D4D5
      • E 1 E 2 E 3 E 4 E 5 E_1E_2E_3E_4E_5 E1E2E3E4E5
  • 这样可以得出第一名是 A 1 A_1 A1,第二、三名只能是 A 2 A 3 B 1 B 2 C 1 A_2A_3B_1B_2C_1 A2A3B1B2C1中最快的两个,这五匹马重赛即可(1次)
扔鸡蛋问题

一栋楼有 100 层,在第 N 层或者更高扔鸡蛋会破,而第 N 层往下则不会。给 2 个鸡蛋,求 N,要求最差的情况下扔鸡蛋的次数最少。

  • 先把楼层分成多个区间,第一个鸡蛋 E 1 E1 E1用于确定N在哪个区间,第二个鸡蛋用于在区间内遍历找到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;
    }
};
写一个strcpy()函数

使用的时候需要注意函数的原型为:

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在面试的时候提问了一些问题,HR面应该要着重体现自己的软实力

除了以下的内容,还可能有自我介绍、兴趣爱好、优缺点、如何看待加班等

你认为你来我们这能干什么?

后来反思了一下,这其实是在问我的职业生涯规划,我当时只回答了从业务出发,提高自己的技术能力和业务能力。

可以分几个阶段来说:

第一阶段:先适应工作环境,开发的工具和流程

第二阶段:熟悉业务流程以及系统设计的原理

第三阶段:提高自己的思维能力和管理能力,尝试去负责一个大的项目

有没有其他offer?为什么不去大厂?

这个可以如实回答,一方面公司可能真的会考虑你选择这边的可能性,另一方面也可以用来抬价

一些关于个人的问题?

比如有没有对象?家乡是哪里的?银行可能真的会从这个方面进行筛选

你可能感兴趣的:(总结归纳,面试,java,数据库)