2021.01.05 字节跳动客户端实习一面面经
主题一:C++
t1. 拷贝构造函数:
-
定义:拷贝构造函数是一种特殊的构造函数,函数的名称必须和类名称一致,它必须的一个参数是本类型的一个引用变量。
-
作用:用来复制对象,使用这个对象的实例来初始化这个对象的一个新的实例。
-
调用时机:(1)当函数的参数为类的对象时(2)函数的返回值是类的对象(3)对象需要通过另外一个对象进行初始化。
-
拷贝有两种:深拷贝,浅拷贝。当出现类的等号赋值时,会调用拷贝函数,在未定义显示拷贝构造函数的情况下,系统会调用默认的拷贝函数即浅拷贝,它能够完成静态成员的复制。当数据成员中没有指针时,浅拷贝是可行的。但当数据成员中有指针时,如果采用简单的浅拷贝,则两类中的两个指针将指向同一个地址,当对象快结束时,会调用两次析构函数,而导致指针悬挂现象。所以这时必须采用深拷贝。
-
深拷贝与浅拷贝的区别就在于深拷贝会在堆内存中另外申请空间来储存数据,从而也就解决了指针悬挂的问题。简而言之,当数据成员中有指针时,必须要用深拷贝。
-
拷贝构造函数必须是引用传递,不能是值传递,这是为了防止递归引用。
t2. 智能指针:
-
c++里面的四个智能指针,auto_ptr,unique_ptr,shared_ptr,weak_ptr,其中后三个是c++11支持,并且第一个已经被c++11弃用。
-
使用原因:智能指针的作用是管理一个指针,因为存在以下这种情况:申请的空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,因为智能指针是一个类,当超出了类的实例对象的作用域时,会自动调用对象的析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。
-
auto_ptr:采用所有权模式。p2剥夺了p1的所有权,但是当程序运行时访问p1将会报错。所以auto_ptr的缺点是:存在潜在的内存崩溃问题。
-
unique_ptr:实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象。它对于避免资源泄露(例如“以new创建对象后因为发生异常而忘记调用delete”)特别有用,可以通过标准库的move()函数实现指针转移。
-
shared_ptr:实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。从名字share就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。
-
weak_ptr:是一种不控制对象生命周期的智能指针, weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。
t3. HashMap的实现,以及时间复杂度:
-
如何避免哈希碰撞:
(1)开放地址法:开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入
(2)再哈希法:当哈希地址发生冲突用其他的函数计算另一个哈希函数地址,直到冲突不在产生为止
(3)链地址法:将哈希表的每个单元作为链表的头结点,所有哈希地址为 i 的元素构成一个同义词链表。即发生冲突时就把该关键字链在以该单元为头节点的链表的尾部
(4)建立公共溢出区:将哈希表分为基本表和溢出表两部分,发生冲突的元素都放在溢出表中
-
不管插入还是查找,由key获取hash值然后定位到桶的时间复杂度都是O(1),那么真正决定时间复杂度的实际上是桶里面链表/红黑树的情况。如果桶里面没有元素,那么直接将元素插入或者直接返回未查找到,时间复杂度就是O(1),如果里面有元素,那么就沿着链表进行遍历,时间复杂度就是O(n),链表越短时间复杂度越低,如果是红黑树的话那就是O(logn)。
-
因此HashMap的查找时间复杂度只有在最理想的情况下才会为O(1)。
主题二:操作系统
t1. 进程的通信方式有哪些:
-
管道:简单,只能在父子进程间单向传输,效率低下。
-
消息队列:容量受到系统限制,发送消息(拷贝)需要花很多时间读内存,不适合频繁通信。
-
共享内存:能够很容易控制容量,速度快,但要解决多进程竞争内存问题。
-
信号量:信号量的本质就是一个计数器,用来实现进程之间的互斥与同步。
-
嵌套字:不仅可以用于本地进程通信,还可以用于不同主机进程之间的通信。
t2. 消息队列的实现:
-
消息队列是一种异步的服务间通信方式,是分布式系统中重要的组件,主要解决应用耦合,异步消息,流量削锋等问题,实现高性能,高可用,可伸缩和最终一致性架构。
-
消息队列的实现:
1 #include <sys/msg.h>
2 // 创建或打开消息队列:成功返回队列ID,失败返回-1
3 int msgget(key_t key, int flag);
4 // 添加消息:成功返回0,失败返回-1
5 int msgsnd(int msqid, const void *ptr, size_t size, int flag);
6 // 读取消息:成功返回消息数据的长度,失败返回-1
7 int msgrcv(int msqid, void *ptr, size_t size, long type,int flag);
8 // 控制消息队列:成功返回0,失败返回-1
9 int msgctl(int msqid, int cmd, struct msqid_ds *buf);
t3.内核态和用户态的区分:
-
内核态:cpu可以访问内存的所有数据,包括外围设备,例如硬盘,网卡,cpu也可以将自己从一个程序切换到另一个程序。
-
用户态:只能受限的访问内存,且不允许访问外围设备,占用cpu的能力被剥夺,cpu资源可以被其他程序获取。
-
为什么要有内核态和用户态:由于需要限制不同的程序之间的访问能力,防止他们获取别的程序的内存数据, 或者获取外围设备的数据,并发送到网络,造成安全问题。
-
用户态和内核态的三种切换方式:
(1)系统调用:用户进程通过系统调用申请使用操作系统提供的服务程序来完成工作,比如read()、fork()等。系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现的,例如Linux的int 80h中断。
(2)异常:当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。
(3)中断:当外围设备完成用户请求的操作后,会想CPU发送中断信号。这时CPU会暂停执行下一条指令(用户态)转而执行与该中断信号对应的中断处理程序(内核态)
主题三:计算机网络
t1. HTTPS请求的过程:
-
浏览器发起往服务器的 443 端口发起请求,请求携带了浏览器支持的加密算法和哈希算法。
-
服务器收到请求,选择浏览器支持的加密算法和哈希算法。
-
服务器下将数字证书返回给浏览器,这里的数字证书可以是向某个可靠机构申请的,也可以是自制的。
-
浏览器进入数字证书认证环节,这一部分是浏览器内置的 TSL 完成的:
(1)首先浏览器会从内置的证书列表中索引,找到服务器下发证书对应的机构,如果没有找到,此时就会提示用户该证书是不是由权威机构颁发,是不可信任的。如果查到了对应的机构,则取出该机构颁发的公钥。
(2)用机构的证书公钥解密得到证书的内容和证书签名,内容包括网站的网址、网站的公钥、证书的有效期等。浏览器会先验证证书签名的合法性。签名通过后,浏览器验证证书记录的网址是否和当前网址是一致的,不一致会提示用户。如果网址一致会检查证书有效期,证书过期了也会提示用户。这些都通过认证时,浏览器就可以安全使用证书中的网站公钥了。
(3)浏览器生成一个随机数 R,并使用网站公钥对 R 进行加密。
-
浏览器将加密的 R 传送给服务器。
-
服务器用自己的私钥解密得到 R。
-
服务器以 R 为密钥使用了对称加密算法加密网页内容并传输给浏览器。
-
浏览器以 R 为密钥使用之前约定好的解密算法获取网页内容。
t2. TCP如何保证可靠传输:
-
确认和超时重传:接收方收到报文就会确认,发送方发送一段时间后没有收到确认就重传。
-
数据校验。
-
数据合理分片和排序:tcp会按MTU合理分片,接收方会缓存未按序到达的数据,重新排序后再交给应用层。
-
流量控制:当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。
-
拥塞控制:当网络拥塞时,减少数据的发送。
t3. TCP的拥塞机制:
-
慢启动和拥塞避免:当拥塞窗口大小达到慢启动门限的初始值前,以慢启动的方式指数增长;达到后,按拥塞避免的方式线性增长。
-
快重传:收到3个同样的确认就立刻重传,不等到超时。
-
快恢复:拥塞窗口不是从1重新开始。
主题四:算法题
t1. A为升序链表,B为降序链表,合并两个链表为升序链表:
-
思路:看过本公众号链表技术文的同学,应该可以很快地的看出本题是由反转链表和合并链表两个简单的模板题组合而成。对于反转链表,可以使用双指针法或递归法;对于合并链表,则是使用尾插法。通过两个模板函数,我们就可以先反转链表,再合并,达到需要的效果。
-
代码:
ListNode *mergeList(ListNode *headA, ListNode *headB){
ListNode *dummyHead=new ListNode(-1);
ListNode *now=dummyHead;
while(headA&&headB){
if(headA->val<headB->val){
now->next=headA;
headA=headA->next;
}else{
now->next=headB;
headB=headB->next;
}
now=now->next;
}
if(headA){
now->next=headA;
}
if(headB){
now->next=headB;
}
return dummyHead->next;
}
ListNode *reverseList(ListNode *head){
ListNode *now=head;
ListNode *pre=NULL;
ListNode *tmp;
while(now){
tmp=now->next;
now->next=pre;
pre=now;
now=tmp;
}
return pre;
}
ListNode *mergeReverseList(ListNode *headA, ListNode *headB) {
headB=reverseList(headB);
return mergeList(headA,headB);
}
-
复杂度分析:
(1)时间复杂度:O(n),一遍反转链表,一遍合并链表。
(2)空间复杂度:O(1)。
t2. 求给定二叉树的宽度和高度:
-
思路:在层序遍历这篇文章中,我们给出了可以区分二叉树每一层遍历过程的模板,在本题中,我们只需要在遍历某一层之前,根据该层结点个数更新最大宽度,并将高度加1即可。
-
代码:
pair<int,int> countMaxLevelWidth(TreeNode *root){
queue<TreeNode*> q;
if(root == NULL) return make_pair(0, 0);
int width = 1; // 宽度
int height = 0; // 高度
q.push(root);
while(1) {
int cnt = q.size();
if(cnt == 0) break;
width = max(width, cnt);
height++;
while(cnt--) {
TreeNode* cur = q.front();
q.pop();
if(cur->left) q.push(cur->left);
if(cur->right) q.push(cur->right);
}
}
return make_pair(width,height); //返回最大宽度和总层数
}
-
复杂度分析:
(1)时间复杂度:O(n)。
(2)空间复杂度:O(n)。