【大厂面试合集】每日一刷——5. 字节跳动飞书部门2022后端工程师实习真题

每日一句

在这里插入图片描述

每日一刷

考试时间 100min

一面

1. TCP,UDP介绍,差别

TCP/UDP协议为传输层协议,传输层用于向用户提供可靠的端到端(每个进程都用一个端口号唯一标识)的通信,通过提供流量控制和差错控制保证报文的正确传输。
传输单位是报文段或用户数据报。
主要协议包括TCP协议和UDP协议。

协议TCP协议UDP协议
简介面向连接的、可靠的传输层协议。传输的数据无差错、不丢失、不重复、按序到达(有流量控制、拥塞控制、提供全双工通信)无连接的、不可靠的传输层协议。尽最大努力交付,不保证可靠性
连接面向连接(发送数据前三次握手建立连接,发送结束四次挥手释放连接)无连接的
传输数据面向字节流(把应用层传下来的报文看成字节流,把字节流组织成大小不等的数据块)面向报文(对于应用程序传下来的报文不合并也不拆分,只是添加 UDP 首部)
交互点对点(一对一)一对一,一对多,多对多
大小TCP首部(开销20字节,包括源端口号、目的端口号等以及确认ACK、同步SYN、终止FIN等用于连接建立与数据传输)+TCP数据部分UDP首部开销8字节(包括源端口号、目的端口号等)+UDP数据部分
适用情景TCP用于在传输层有必要实现可靠传输的情景UDP主要用于那些对高速传输和实时性有较高要求的通信或广播通信:1.包总量较少的通信(DNS、SNMP等)2.视频、音频等多媒体通信(即时通信)3.限定于LAN等特定网络中的应用通信4.广播通信(广播、多播)
传输可靠传输:
(1)传输信道无差错,保证传输数据正确;
(2)不管发送方以多快的速度发送数据,接收方总是来得及处理收到的数据;
可靠性原理:
(1)使用三次握手建立TCP连接,四次握手释放TCP连接,保证建立的传输信道是可靠的。
(2)使用连续ARQ协议(超时自动重传:如果一个已经发送的报文段在超时时间内没没有收到确认,那么就重传这个报文段)来保证数据传输的正确性,
(3)使用滑动窗口协议来保证接收方能够及时处理所接收到的数据,进行流量控制
(4)使用慢开始、拥塞避免、快重传和快恢复来进行拥塞控制,避免网络拥塞
不可靠传输:
UDP不提供复杂的控制机制,利用IP提供面向无连接的通信服务
并且它是将应用程序发来的数据在收到的那一刻,立即按照原样发送到网络上的一种机制。
即使是出现网络拥堵的情况,UDP也无法进行流量控制等避免网络拥塞行为。
此外传输途中出现丢包,UDP也不负责重发(发送后不管其是否会到达接收方)。甚至当包的到达顺序出现乱序也没有纠正的功能。
如果需要以上的细节控制,不得不交由采用UDP的应用程序去处理。
协议FTP、HTTP、POP3、TELNET…SMTP(网络管理)、DNS(域名转换)、TFTP(文件传输)、NFS(远程文件服务器)、DHCP…

2. 红黑树,AVL对比,引申B,B+树

  • 红黑树特点

二叉查找树(BST)特性:

  1. 左子树上节点值均小于等于根节点的值
  2. 右子树上节点值均大于等于根节点的值
  3. 左右子树均为二叉排序树

二叉查找树采用二分查找的思想,查找所需最大次数等同于二叉查找树的高度。
红黑树(Red Black Tree)是自平衡(防止高度过高)的二叉查找树,特性:

  1. 节点是红色或黑色
  2. 根节点是黑色
  3. 叶子节点是黑色的空节点
  4. 每个红色节点的2个子结点都是黑色
  5. 从任一节点到其每个叶子的所有路径都包含相同数目黑色节点

在这里插入图片描述

  • 红黑树操作
    红黑树和AVL树类似,都是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。它可以在O(log n)时间内做查找,插入和删除,这里的n 是树中元素的数目。
    由于插入新结点后可能会破坏红黑树的规则,此时需要进行调整,包括变色和旋转,旋转包括左旋和右旋。
  • 红黑树后继节点
    a. 空节点,没有后继
    b. 有右子树的节点,后继就是右子树的“最左节点”
    c. 无右子树的节点,后继就是该节点所在左子树的第一个祖先节点
    有右子树的节点,节点的下一个节点,肯定在右子树中,而右子树中“最左”的那个节点则是右子树中最小的一个,那么当然是右子树的“最左节点”,就好像下图所示:
    在这里插入图片描述
    无右子树的节点,先找到这个节点所在的左子树(右图),那么这个节点所在的左子树的父节点(绿色节点),就是下一个节点。
    在这里插入图片描述
static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
    if (t == null)
        return null;
    else if (t.right != null) {
        // 有右子树的节点,后继节点就是右子树的“最左节点”
        // 因为“最左子树”是右子树的最小节点
        Entry<K,V> p = t.right;
        while (p.left != null)
            p = p.left;
        return p;
    } else {
        // 如果右子树为空,则寻找当前节点所在左子树的第一个祖先节点
        // 因为左子树找完了,根据LDR该D了
        Entry<K,V> p = t.parent;
        Entry<K,V> ch = t;
        // 保证左子树
        while (p != null && ch == p.right) {
            ch = p;
            p = p.parent;
        }
        return p;
    }
}
  • 红黑树应用
    红黑树应用很多,其中JDK的集合类TreeMap和TreeSet底层使用红黑树实现,Java8中HashMap也是用红黑树实现。

参考:漫画算法:什么是红黑树

3. 网卡收到一条数据到进程处理数据,这之间经历了什么(中断的上半部下半部,网络层协议拆包)

接收数据包是一个复杂的过程,涉及很多底层的技术细节,但大致需要以下几个步骤:

  1. 网卡收到数据包。

  2. 将数据包从网卡硬件缓存转移到服务器内存中。

  3. 通知内核处理。

  4. 经过TCP/IP协议逐层处理。

  5. 应用程序通过read()从socket buffer读取数据。
    在这里插入图片描述
    物理网卡收到数据包的处理流程如上图所示,详细步骤如下:

  6. 网卡收到数据包,先将高低电平转换到网卡fifo存储,网卡申请ring buffer的描述,根据描述找到具体的物理地址,从fifo队列物理网卡会使用DMA将数据包写到了该物理地址,,其实就是skb_buffer中.

  7. 这个时候数据包已经被转移到skb_buffer中,因为是DMA写入,内核并没有监控数据包写入情况,这时候NIC触发一个硬中断,每一个硬件中断会对应一个中断号,且指定一个vCPU来处理,如上图vcpu2收到了该硬件中断.

  8. 硬件中断的中断处理程序,调用驱动程序完成,a.启动软中断

  9. 硬中断触发的驱动程序会禁用网卡硬中断,其实这时候意思是告诉NIC,再来数据不用触发硬中断了,把数据DMA拷入系统内存即可

  10. 硬中断触发的驱动程序会启动软中断,启用软中断目的是将数据包后续处理流程交给软中断慢慢处理,这个时候退出硬件中断了,但是注意和网络有关的硬中断,要等到后续开启硬中断后,才有机会再次被触发

  11. NAPI触发软中断,触发napi系统

  12. 消耗ringbuffer指向的skb_buffer

  13. NAPI循环处理ringbuffer数据,处理完成

  14. 启动网络硬件中断,有数据来时候就可以继续触发硬件中断,继续通知CPU来消耗数据包.

其实上述过程过程简单描述为:
网卡收到数据包,DMA到内核内存,中断通知内核数据有了,内核按轮次处理消耗数据包,一轮处理完成后,开启硬中断。其核心就是网卡和内核其实是生产和消费模型,网卡生产,内核负责消费,生产者需要通知消费者消费;如果生产过快会产生丢包,如果消费过慢也会产生问题。也就说在高流量压力情况下,只有生产消费优化后,消费能力够快,此生产消费关系才可以正常维持,所以如果物理接口有丢包计数时候,未必是网卡存在问题,也可能是内核消费的太慢。

4. 大数据量(内存够用)下,快排与堆排序的对比(考察缓存命中率的对比)

一句话就是:
因为堆排序下,数据读取的开销变大。在计算机进行运算的时候,数据不一定会从内存读取出来,而是从一种叫cache的存储单位读取。
原因是cache相比内存,读取速度非常快,所以cache会把一部分我们经常读取的数据暂时储存起来,以便下一次读取的时候,可以不必跑到内存去读,而是直接在cache里面找。一般认为读取数据遵从两个原则:temporal locality,也就是不久前读取过的一个数据,在之后很可能还会被读取一遍;另一个叫spatial locality,也就是说读取一个数据,在它周围内存地址存储的数据也很有可能被读取到。因此,在读取一个单位的数据(比如1个word)之后,不光单个word会被存入cache,与之内存地址相邻的几个word,都会以一个block为单位存入cache中。另外,cache相比内存小得多,当cache满了之后,会将旧的数据剔除,将新的数据覆盖上去。在进行堆排序的过程中,由于我们要比较一个数组前一半和后一半的数字的大小,而当数组比较长的时候,这前一半和后一半的数据相隔比较远,这就导致了经常在cache里面找不到要读取的数据,需要从内存中读出来,而当cache满了之后,以前读取的数据又要被剔除。

简而言之快排和堆排读取arr[i]这个元素的平均时间是不一样的。

5. 缓存相关内容,LRU算法思想,手撕LRU的实现

LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。当限定的空间已存满数据时,应当把最久没有被访问到的数据淘汰。

以下我主要以为双向链表+HashMap的方式手撕一个时间复杂度为O(1)的LRU算法。

public class LRULinkedHashMap<K, V> extends LinkedHashMap<K, V> {

    private int capacity;

    LRULinkedHashMap(int capacity) {
        //true是表示按照访问时间排序,
        super(capacity, 0.75f, true);
        //传入指定的缓存最大容量
        this.capacity = capacity;
    }

    /**
     * 实现LRU的关键方法,如果map里面的元素个数大于了缓存最大容量,则删除链表的顶端元素
     */
    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > capacity;
    }
}

二面

1.实习项目介绍,问的很深 引申到一致性hash

2.缓存失效,替换原理

缓存是一个计算机思维,对于重复的计算,缓存其结果,下次再算这个任务的时候,不去真正的计算,而是直接返回结果,能加快处理速度。当然有些会随时间改变的东西,缓存会失效,得重新计算。

在计算中,缓存算法(通常也称为缓存替换算法或缓存替换策略)是优化指令或算法,计算机程序或硬件维护的结构可以利用这些指令或算法来管理存储在计算机上的信息缓存。高速缓存通过将最近或经常使用的数据项保存在比普通内存存储区更快或更便宜的存储位置中来提高性能。当缓存已满时,算法必须选择要丢弃的项目,以便为新项目腾出空间。

比如缓存空间只有2个,要缓存的数据有很多,1,2,3,4,5,那么当缓存空间满了,需要淘汰一个缓存出去,其中淘汰算法有 LRU,LFU,FIFO,SC二次机会,老化算法,时钟工作集算法等等。

  • FIFO:
    First In First Out,先进先出。判断被存储的时间,离目前最远的数据优先被淘汰。
  • LRU:
    Least Recently Used,最近最少使用。判断最近被使用的时间,目前最远的数据优先被淘汰。
  • LFU:
    Least Frequently Used,最不经常使用。在一段时间内,数据被使用次数最少的,优先被淘汰。

3.Java多态原理

1、定义
指允许不同类的对象对同一消息做出响应。即同一消息可以根据发送对象的不同而采用多种不同的行为方式。(发送消息就是函数调用)
现实中,关于多态的例子不胜枚举。比方说按下 F1 键这个动作,如果当前在 Flash 界面下弹出的就是 AS 3 的帮助文档;如果当前在 Word 下弹出的就是 Word 帮助;在 Windows 下弹出的就是 Windows 帮助和支持。同一个事件发生在不同的对象上会产生不同的结果。

2、三要素
(1)继承
(2)重写
(3)父类引用指向子类对象

3、好处

  1. 可替换性(substitutability)
    多态对已存在代码具有可替换性。例如,多态对圆Circle类工作,对其他任何圆形几何体,如圆环,也同样工作。
  2. 可扩充性(extensibility)
    多态对代码具有可扩充性。增加新的子类不影响已存在类的多态性、继承性,以及其他特性的运行和操作。实际上新加子类更容易获得多态功能。例如,在实现了圆锥、半圆锥以及半球体的多态基础上,很容易增添球体类的多态性。
  3. 接口性(interface-ability)
    多态是超类通过方法签名,向子类提供了一个共同接口,由子类来完善或者覆盖它而实现的。
  4. 灵活性(flexibility)
    它在应用中体现了灵活多样的操作,提高了使用效率。
  5. 简化性(simplicity)
    多态简化对应用软件的代码编写和修改过程,尤其在处理大量对象的运算和操作时,这个特点尤为突出和重要。

4.32位系统运行大于4G的程序,如何寻址(考察虚拟内存,虚拟地址空间)

一般来说,页面文件大小应该在物理内存的0.5倍到2倍之间,一般推荐的大小都是物理内存的大小。对于32位系统,如果物理内存是4G,或者已经达到系统可用的物理内存的上限,那么设置成4G或者可识别物理内存大小也都可以。
原因是:虚拟内存是用来换页用的,所以虚拟内存(页面文件)最好是大于物理内存,才能保证所有物理内存都能被换出来。如果物理内存足够大,已经超过了32位系统的寻址范围,那么虚拟内存设置成4G即可。当然,页面文件也可以大于4G,看使用场景,因为页面文件里可以不止一个地址空间(Windows系统里,每个进程都有4G地址空间),所以理论上大于4G是没有问题的,但太大的页面文件实际上不一定对系统效率有多大的提升。
所以一般来说,最大4G的页面文件已经足够用了。

发两个链接供参考:
https://www.zhihu.com/question/21008980 可以看看第一个回答。
https://www.zhihu.com/question/23247083 看轮子哥的回答以及评论。

5.手撕完全二叉树寻找最后一行的最后一个节点

首先遍历完全二叉树的左分支,求出完全二叉树的高度depth, 然后对于每个子树的根节点,先从根节点的右孩子开始,然后从此节点遍历该节点的左孩子,等遍历完成后,进行判断此时临时高度等于二叉树的高度,且节点无右孩子时候,则输出该节点,否则右侧还有节点,则遍历右子树,若临时高度小于二叉树的高度,则遍历根节点的左孩子。

class TreeNode(object):
 
    def __init__(self, x):
        self.val = x
        self.left = None
        self.right = None
 
class Solution(object):
 
    def printlastnode(self, root):
        if not root:
            return None
 
        depth = 0
        tmp = root
        # 首先先计算二叉树的高度
        while tmp:
            depth += 1
            tmp = tmp.left
 
        level = 0
        tempdepth = 0
 
        # 遍历二叉树
        while root:
            level += 1
            if level == depth:
                break
 
            curnode = root
 
            # 先遍历右孩子,若无右孩子,则玩根节点的左孩子遍历
            if curnode.right:
                parent = curnode
                curnode = curnode.right
 
                # 设置临时高度
                tempdepth = level + 1
 
                # 然后循环往左孩子遍历
                while curnode.left:
                    tempdepth = tempdepth + 1
                    parent = curnode
                    curnode = curnode.left
 
                # 若临时统计高度小于二叉树高度,则往根节点的左孩子遍历
                if tempdepth < depth:
                    root = root.left
                    
                # 若当前节点无右孩子,且高度等于二叉树高度,则输出当前节点
                elif not curnode.right or parent.right == curnode:
                    return curnode
                else:
                    root = root.right
 
            else:
                root = root.left
 
        return root
 
 
if __name__ == "__main__":
 
    sol = Solution()
    t1 = TreeNode(1)
    t2 = TreeNode(2)
    t3 = TreeNode(3)
    t4 = TreeNode(4)
    t5 = TreeNode(5)
 
    t1.left = t2
    t1.right = t3
    t2.left = t4
    t2.right = t5
 
    res = sol.printlastnode(t1)
    if res:
        print("res = %s" % res.val)

6.手撕层序遍历二叉树

采用队列实现。
仔细看看层序遍历过程,其实就是从上到下,从左到右依次将每个数放入到队列中,然后按顺序依次打印就是想要的结果。

实现过程
1、首先将二叉树的根节点push到队列中,判断队列不为NULL,就输出队头的元素,
2、判断节点如果有孩子,就将孩子push到队列中,
3、遍历过的节点出队列,
4、循环以上操作,直到Tree == NULL。

void FloorPrint_QUEUE(pTreeNode &Tree) //层序遍历_队列实现
{
    queue < pTreeNode> q;
    if (Tree != NULL)
    {
        q.push(Tree);   //根节点进队列
    }

    while (q.empty() == false)  //队列不为空判断
    {
        cout << q.front()->data << " → "; 

        if (q.front()->leftPtr != NULL)   //如果有左孩子,leftChild入队列
        {
            q.push(q.front()->leftPtr);   
        }

        if (q.front()->rightPtr != NULL)   //如果有右孩子,rightChild入队列
        {
            q.push(q.front()->rightPtr);
        }
        q.pop();  //已经遍历过的节点出队列
    }
}

三面

1.项目介绍,实习收获了什么

2.平时看什么书,如何评价自己

3.STL vector扩容,map实现原理,红黑树

  • vector 扩容
    在标准类库中,当我们采用push_back操作,往vector中添加元素时,若此时vector的容量已满,而不得不获取新的空间时,vector通常会分配比新的空间需求更大的内存空间,然后将原来的数据复制过来,然后再插入新的元素。扩容的方式在不同的编译器中有不一样的实现。一般是2倍或者1.5倍进行扩容。在GCC中是2倍扩容。

  • map实现原理
    hashmap的底层数据结构散列表,即:数组+链表,创建的时候初始化一个数组,每个节点可以为一个链表
    在这里插入图片描述
    当一键值对发生put操作时,

首先根据key的hash值得到这个元素在数组中的位置(即下标),如果这个位置上已经存在其他元素,将进行下一步操作。

由于同一点是链表方式存储,会将原来的元素向后推
然后新的元素放在这个位置上
put操作可能会出现冲突,冲突分两种:

不同的key值,通过hash函数得出相同的index,这种冲突通过上面所说的链表方式存储。
相同的key值,直接覆盖。
所以为了减少冲突,尽量将hashmap 的长度设置为2的次方,因为如果不是2的次方,经过hash & 操作,最后一位总是0如下图,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,而且这样可以使用的位置比数组长度小了很多,增加了冲突的几率,故减慢的查询的效率(如果每一个节点都不存在链表,则不需要循环,查询效率会高,所以尽量均匀分布)。

同理,当一键值对发生get操作时,会经过hash函数计算得到index,如果节点为链表有多个元素,则迭代用key.equals()比较获取。

在这里插入图片描述

4.手撕给二叉树先序,中序序列,求后序序列

前序遍历

public void preOrderTraverse2(TreeNode root) {
		LinkedList<TreeNode> stack = new LinkedList<>();
		TreeNode pNode = root;
		while (pNode != null || !stack.isEmpty()) {
			if (pNode != null) {
				System.out.print(pNode.val+"  ");
				stack.push(pNode);
				pNode = pNode.left;
			} else { //pNode == null && !stack.isEmpty()
				TreeNode node = stack.pop();
				pNode = node.right;
			}
		}
	}

中序遍历

public void inOrderTraverse2(TreeNode root) {
		LinkedList<TreeNode> stack = new LinkedList<>();
		TreeNode pNode = root;
		while (pNode != null || !stack.isEmpty()) {
			if (pNode != null) {
				stack.push(pNode);
				pNode = pNode.left;
			} else { //pNode == null && !stack.isEmpty()
				TreeNode node = stack.pop();
				System.out.print(node.val+"  ");
				pNode = node.right;
			}
		}
	}

后序遍历

public void postOrderTraverse1(TreeNode root) {
		if (root != null) {
			postOrderTraverse1(root.left);
			postOrderTraverse1(root.right);
			System.out.print(root.val+"  ");
		}
	}

5.随便聊一些发展前景啦,城市啦有的没的

希望各位都拿到心仪的offer~~~

在这里插入图片描述

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李一恩

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值