字节跳动面经

答案自我整理,还请三思而信

字节跳动第一面:虐到体无完肤怀疑自我T_T

1.C++和java的区别

  • 1. Java是解释型语言,所谓的解释T_T型语言,就是源码编译为中间码中间码再被解释器解释成机器码。对于Java而言,中间码就是字节码(.class),而解释器在JVM中内置了。
  • 2. C++是编译型语言,所谓编译型语言,就是源码一次编译,直接在编译的过程中链接了,形成了机器码。
  • 3. C++比Java执行速度快,但是Java可以利用JVM跨平台。
  • 4. Java是纯面向对象的语言,所有代码(包括函数、变量)都必须在类中定义。而C++中还有面向过程的东西,比如是全局变量和全局函数。
  • 5. C++中有指针,Java中没有,但是有引用。
  • 6. C++支持多继承,Java中类都是单继承的。但是继承都有传递性,同时Java中的接口是多继承,类对接口的实现也是多实现。
  • 7. C++中,开发需要自己去管理内存,但是Java中JVM有自己的GC机制,虽然有自己的GC机制,但是也会出现OOM和内存泄漏的问题。C++中有析构函数,Java中Object的finalize方法。
  • 8. C++运算符可以重载,但是Java中不可以
  • 9.  C++中支持强制自动转型,Java中不行。

2.redis的对象及其底层

https://blog.csdn.net/u012394095/article/details/81205439

  • 字符串: 简单动态字符串,embstr字符串,整数
  • 哈希对象:,字典 压缩列表,
  • 链表:压缩列表, 双端链表
  • 集合: 整数集合, 字典
  • 有序集合: 压缩列表,跳跃表+字典,前者实现有序性,后者实现快速查找。为什么要用这种结构呢。试想如果单一用hashtable,那可以快速查找、添加和删除元素,但没法保持集合的有序性。如果单一用skiplist,有序性可以得到保障,但查找的速度太慢O(logN)

3.C++ 指针用的时候需要注意什么?

内存泄露问题

4.重载和重写

  • 重载: 两个函数名相同, 但是参数列表不同(个数, 类型) , 返回值类型没有要求, 在同一作用域中。重载分为动态和静态。静态多态主要是重载, 在编译的时候就已经确定; 动态多态是用虚函数机制实现的, 在运行期间动态绑定。 举个例子: 一个父类类型的指针指向一个子类对象时候, 使用父类的指针去调用子类中重写了的父类中的虚函数的时候, 会调用子类重写过后的函数, 在父类中声明为加了 virtual 关键字的函数, 在子类中重写时候不需要加 virtual也是虚函数。
  • 重写: 子类继承了父类, 父类中的函数是虚函数, 在子类中重新定义了这个虚函数, 这种情况是

5.linux的常用的命令:

  • 查看内存的:free

  • free 命令显示系统使用和空闲的内存情况,包括物理内存、交互区内存(swap)和内核缓冲区内存。共享内存将被忽略

    -b  以Byte为单位显示内存使用情况。 

    -k  以KB为单位显示内存使用情况。 

    -m  以MB为单位显示内存使用情况。

    -g   以GB为单位显示内存使用情况。 

  • 查看网络的:netstat, lsof

  • netstat命令参数:

      -t : 指明显示TCP端口  netstat -ntulp |grep 80 //查看所有80端口使用情况·

      -u : 指明显示UDP端口

      -l : 仅显示监听套接字(所谓套接字就是使应用程序能够读写与收发通讯协议(protocol)与资料的程序)

      -p : 显示进程标识符和程序名称,每一个套接字/端口都属于一个程序。

      -n : 不进行DNS轮询,显示IP(可以加速操作)

  • lsof -i :port就能看见所指定端口运行的程序,同时还有当前连接。

  • 查看磁盘空间大小的命令:df:df -h

  • df命令用于查看磁盘分区上的磁盘空间,包括使用了多少,还剩多少,默认单位是KB。
    • 第一列Filesystem,磁盘分区

    • 第二列Size,磁盘分区的大小

    • 第三列Used,已使用的空间

    • 第四列Avail,可用的空间

    • 第五列Use%,已使用的百分比

    • 第六列Mounted on,挂载点

  • 查看文件和目录大小的命令:du

  • 1.比如要看/data目录的总大小,可以用以下命令:-s参数就是查看总大小(区别于查看其中每个目录的大小),而-h参数是把默认的单位KB改为比较好辨认的单位

    du -sh /data
  • 2,如果要看/data目录下各个子目录的大小,只包括子目录的子目录,不包含文件, 

    du -h
  • 3.如果要看/data目录下各个子目录的大小,包括子目录的子目录,且包含/data下文件,可以用以下命令:

    du –h *
  • 如果要看/data目录下各个子目录的大小,不包括子目录的子目录,可以用以下命令:

    du -sh *
  • 如果要看/data目录下各个子目录和文件的大小,需要使用-a参数:

    du -ah
  • 查看进程的:top, ps
  • 通过netstat查找端口占用的pid,再通过pid进一步的查找程序名称,能够确认目前冲突的端口是哪个程序已经占用。

6.看网络端口的命令

7.跳跃表是那个对象用到?用跳跃表有什么优势?时间复杂度?

有序集合。提升查找速度,插入的时间复杂度是logn,空间复杂度未On.

跳跃表的构造原理:跳表是通过随机函数来维护“平衡性”。当我们往跳表中插入数据的时候,我们可以通过一个随机函数,来决定这个结点插入到哪几级索引层中,比如随机函数生成了值K,那我们就将这个结点添加到第一级到第K级这个K级索引

8.进程间通信的方式,他们的区别和优势

 

9.进程怎么去控制共享内存?整个过程。

  • Linux 允许不同进程访问同一个逻辑内存, 提供了一组 API, 头文件在 sys/shm.h 中。
  • 1) 新建共享内存 shmget

  • int shmget(key_t key,size_t size,int shmflg);

  • key: 共享内存键值, 可以理解为共享内存的唯一性标记
  • size: 共享内存大小
  • shmflag: 创建进程和其他进程的读写权限标识。
  • 返回共享内存标识符, 失败返回-1
  • 2) 连接共享内存到当前进程的地址空间 shmat

  • void *shmat(int shm_id,const void *shm_addr,int shmflg);

  • shm_id: 共享内存标识符
  • shm_addr: 指定共享内存连接到当前进程的地址, 通常为 0, 表示由系统来选择。
  • shmflg: 标志位
  • 返回值: 指向共享内存第一个字节的指针, 失败返回-1
  • 3) 当前进程分离共享内存 shmdt

  • int shmdt(const void *shmaddr);

  • 4) 控制共享内存 shmctl

  • int shmctl(int shm_id,int command,struct shmid_ds *buf);

  • shm_id: 共享内存标识符
  • IPC_STAT:获取共享内存的状态, 把共享内存的 shmid_ds 结构复制到 buf 中。
  • IPC_SET:设置共享内存的状态, 把 buf 复制到共享内存的 shmid_ds 结构。
  • IPC_RMID:删除共享内存
     

10.挥手比握手多一次,为什么多一次?

  • 2、3次挥手不能合在一次挥手中?那是因为此时A虽然不再发送数据了,但是还可以接收数据,B可能还有数据要发送给A,所以两次挥手不能合并为一次
  • 挥手次数比握手多一次,是因为握手过程,通信只需要处理连接。而挥手过程,通信需要处理数据+连接

11.time_wait阶段为什么要等两个来回,这两个来回是怎么计算的。

  • 2MSL 意义:
  • 1、 保证最后一次握手报文能到 B, 能进行超时重传
  • 2、 2MSL 后, 这次连接的所有报文都会消失, 不会影响下一次连接
    为什么是2MSL:
  • MSL是Maximum Segment Lifetime英文的缩写,中文可以译为“报文最大生存时间”,他是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃

    MSL(Maximum Segment Lifetime)报文最大生存时间

    Windows : MSL = 2 min

    linux(Ubuntu, CentOs) : MSL = 60s

    Unix : MSL = 30s

12.网络拥塞控制机制

满开始,拥塞算法,快重传,快恢复。

13.说一下这几各阶段滑动窗口的变化曲线

14.快恢复算法滑动窗口变动趋势是怎么样的。

15.二叉树是一个有序的,左>右,找出差距最小的。

 

字节跳动第二次面:哭了,被爆cao:

1.TCP实现可靠性有哪些保证:7种机制:校验和,序列号,确认回复,超时重传,拥塞控制,流量控制,连接管理。

https://blog.csdn.net/xuzhangze/article/details/80490362

2.C++大小端和字节对齐,有没有一字节对齐。什么时候设置字节对齐?

 

有一字节对齐。一字节对齐就是结构体原来有多大,就是多大,不做优化。

  •  

C++的字节对齐

  • 对齐原因:
  • 1.因为某些硬件平台只能在某些地址处取某些特定类型的数据
  • 2.为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问
  • 在设计不同CPU下的通信协议时,或者编写硬件驱动程序时寄存器的结构这两个地方都需要按一字节对齐。即使看起来本来就自然对齐的也要使其对齐,以免不同的编译器生成的代码不一样.

对齐原则 

  •  VS 中提供了#pragma pack(n)来设定变量以n字节对齐方式
  • 整个sizeof(struct)的最终结果必然是 min[n,结构内部最大成员] 的整数倍,不够补齐。
  • struct内部各个成员的首地址必然是min[n,自身大小]的整数倍。
  • 在写结构体时,成员先后应遵循从大到小的原则,这样有助于节省空间
  • 跨平台数据结构可考虑1字节对齐,节省空间但影响访问效率

字节对齐设置:

  • 使用伪指令#pragma pack (n),C编译器将按照n个字节对齐。
    · 使用伪指令#pragma pack (),取消自定义字节对齐方式。
  • · __attribute((aligned (n))),让所作用的结构成员对齐在n字节自然边界上。如果结构中有成员的长度大于n,则按照最大成员的长度来对齐。

 

3.什么时候不需要字节对齐?

   字、双字和四字在自然边界上不需要在内存中对齐。(对于字、双字和四字来说,自然边界分别是偶数地址,可以被4整除的地址,和可以被8整除的地址。

4.网络传输的大小端

  • C++的大小端:小端低字节在低址,高高低低。    不会头重脚轻,所以是小端口。
  • Java和网络通讯协议都是使用 大端   的编码。

5.网络序

  •   对于可移植的代码来说,将接收的网络数据转换成主机的字节序是必须的,一般会有成对的函数用于把网络数据转换成相应的主机字节序或反之(若主机字节序与网络字节序相同,通常将函数定义为空宏)。
  • Htonl、htons用于主机序转换到网络序;ntohl、ntohs用于网络序转换到本机序。ls表示长和短整形

5.传输多大的时候不需要大小序转换?区别什么时候不用字节对齐

https://www.cnblogs.com/prettyshuang/p/5553140.html

在大小字节序转换时,必须考虑待转换数据的长度。另外对于单字符或小于单字符的几个bit数据,是不必转换的,因为在机器存储和网络发送的一个字符内的bit位存储顺序是一致的。

6.const为什么不可修改?底层做了什么,怎么去修改const?

  •   const int i=0;
  •   int *p=(int*)&i;
  •   p=100;

7.tcpip一直要求重传怎么办?--

数据被重发以后若还是收不到应答,则进行再次发送。此时等待确认应答时间会以2倍、4倍的指数函数延长
此外,数据也不会被无限、反复的重发。达到一定的重发次数之后,如果仍然没有任何确认应答返回,就会判断为网络或者对端主机发生了异常,强制关闭连接。

linux的设置

最小重传时间是200ms
最大重传时间是120s = 7200ms
重传次数为15

map和unorder_map的特性和区别。

4.堆可以用来干什么?如何让建立一个堆。

从第一个开始从上往下调整,直到全部完成。
 public static void downAdjust(int[] array, int parentIndex,int length) {
    保存父节点值, 用于最后的赋值
     int temp = array[parentIndex];
    左子节点的下标为父节点标识的2被加1;
     int childIndex = 2 * parentIndex + 1;
        直到子节点的超出范围,否则一直调整。
     while (childIndex < length) {
         如果有右孩子, 且右孩子大于左孩子的值, 则定位到右孩子
         if (childIndex + 1 < length && array[childIndex + 1] >array[childIndex]) {
             childIndex++;
         }
         如果父节点大于任何一个孩子的值, 无需调整。
         if (temp >= array[childIndex])
             break;
        否则,交换父子节点。
        无须真正交换, 单向赋值即可,让子节点换成父节点。子索引成为新的父索引,自索引的两倍为新子索引。
         array[parentIndex] = array[childIndex];
         parentIndex = childIndex;
         childIndex = 2 * childIndex + 1;
     }
     array[parentIndex] = temp;
 }

 /**
 * 堆排序(升序)
 * @param array 待调整的堆
 */
 public static void heapSort(int[] array) {
     无序数组构建成最大堆,逐个节点插入已经有序
     for (int i = (array.length-2)/2; i >= 0; i--) {
        从最后一个父节点开始,执行一次向下调整。共n次,每次执行logn复杂度。共计nlogn。
         downAdjust(array, i, array.length);
     }
     System.out.println(Arrays.toString(array));

    循环删除堆顶元素, 移到集合尾部, 调整堆产生新的堆顶
    删除n次得到了一个有序数组,但是每次调整可能需要logn复杂度。
    所以堆排序的复杂度为O(nlogn )+O(nlogn) = O(logn);
     for (int i = array.length - 1; i > 0; i--) {
         // 最后1个元素和第1个元素进行交换
         int temp = array[i];
         array[i] = array[0];
         array[0] = temp;
         // “下沉”调整最大堆,只需要执行一次调整。
         downAdjust(array, 0, i);
     }
 }

 public static void main(String[] args) {
     int[] arr = new int[] {1,3,2,6,5,7,8,9,10,0};
     heapSort(arr);
     System.out.println(Arrays.toString(arr));
 }

进程

fork什么时候需要拷贝,拷贝了什么?父进程打开的文件描述符要不要拷贝?发生修改怎么办?文件描述符怎么处理?

以前没有COW的时候,fork和vfork的区别是子进程会不会复制一份父进程内存空间的拷贝

后来有了COW,都只复制页表,等到要修改页面的时候才触发中断,拷贝一份新的页面
有了COW之后,fork和vfork的区别是会不会阻塞父进程
vfork然后阻塞父进程,等到子进程exec或exit父进程才能被唤醒


堆栈肯定会被拷贝,只不过先拷贝页表,父子进程映射到同样的物理内存。当父子进程其中一个要修改该页面时,会触发中断,内核这时候会复制一份新的内存然后进行映射到要修改内存的进程内存空间中

 

父子进程的异同点:

  1. 两者的虚拟空间不同,但其对应的物理空间是同一个
  2. 子进程的代码段、数据段、堆栈都是指向父进程的物理空间,复制了页表,没有复制物理页面。

当父子进程中发生更改相应段的行为时

  1. 如果不是因为exec,内核会给子进程的数据段、堆栈段分配相应的物理空间 ,而代码段继续共享父进程的物理空间
  2. 如是因为exec,       由于两者执行的代码不同,数据段、堆栈段和代码段都复制。

具体过程是这样的:

  1. fork复制了页表,但没有复制物理页面
  2. 但是会把父子共享的页面标记为“只读”(类似mmap的private的方式),如果父子进程一直对这个页面是同一个页面,直到其中任何一个进程要对共享的页面“写操作”,这时内核会复制一个物理页面给这个进程使用,同时(修改子进程的)页表。而把原来的只读页面标记为“可写”,留给另外一个进程使用。这样,父进程的仍然是只读,子进程的是可写。

对文件描述符的处理:

在fork之后处理文件描述符有两种常见的情况:

  1. 文件描述符的继承:共享打开的文件描述符,可以分别关闭。
  2. (1)如果在父进程在fork()之前打开my.dat,子进程都会继承,与父进程共享相同的文件偏移量fork()时需要对这个计数进行维护, 以体现子进程对应的新的文件描述符表也指向它。程序关闭文件时,也是将系统文件表条目内部的计数减一,当计数值减为0时,才将其删除。
  3. (2)如果父进程fork之后打开my.dat,这时父子进程关于my.dat 的文件描述符表指向不同的系统文件表条目,也不再共享文件偏移量(fork以后2个进程分别open,在系统文件表中创建2个条目);但是关于标准输入, 标准输出,标准错误,父子进程还是共享的。

算法题1:二叉树的层序遍历并且打印层节点。

#include <iostream>
#include <queue>
#include <vector>
using namespace std;
struct Node{
    int val;
    Node* left;
    Node* right;
    Node(int x):val(x),left(nullptr),right(nullptr){}
};
第一种写法
Node* BuildTree(Node* root, vector<int>& arr, int length, int index){   
    if(index >= length){
        return nullptr;     
    }else{
            root = new Node(arr[index]);  
            在返回节点的时候,令root->left/right指向新创建的子节点。连接其整个树
            root->left = BuildTree(root->left, arr, length, 2*index+1);
            root->right = BuildTree(root->right, arr, length, 2*index+2);  
    }
    return root;
}
第二种写法,不返回根节点,所以需要传入引用。因为如果不传递引用,每次调用的都是一样的root的子节点,root的右节点。会覆盖之前的值。
*只可以改变指针指向的内容
*&既可以改变指针指向的内容,亦可以让指向指向其他地址。
void BuildTree(Node*& root, vector<int>& arr, int length, int index){   
    if(index >= length){
        return ;     
    }else{
            root = new Node(arr[index]);  
            BuildTree(root->left, arr, length, 2*index+1);
            BuildTree(root->right, arr, length, 2*index+2);  
    }
}

void levelTraversal(Node* root){
        queue<Node*> p;
        queue<Node*> q;
        bool flag = false;
        p.push(root);
        while(!p.empty() || !q.empty()){
            if(!flag){
                while(!p.empty()){
                    Node* temp = p.front();
                    cout << temp->val;
                    p.pop();
                    if(temp->left){
                        q.push(temp->left);
                    }
                    if(temp->right){
                        q.push(temp->right);
                    }  
                }
                flag = !flag;
                cout << endl;
            }else{
                while(!q.empty()){
                    Node* temp = q.front();
                    cout << temp->val;
                    q.pop();
                    
                    if(temp->left){
                        p.push(temp->left);
                    }
                    if(temp->right){
                        p.push(temp->right);
                    }
                }
                flag = !flag;
                cout << endl;
            }
    }
}
void dfs(Node* root){
    if(root != nullptr){
        cout << root->val;
        dfs(root->left);
        dfs(root->right);
    }
}
int main() {
    Node* root = nullptr;
    vector<int> t = {1,2,3,4,5};
    只需要遍历节点一次。
    第一种写法,BuildTree返回根节点。
    root = BuildTree(root,t,t.size(),0);
    第二种写法,BuildTree不返回根节点。
    BuildTree(root, t,t.size(),0);

    levelTraversal(root);
    return 0;    
}

算法题2: 再排序二叉树中寻找和指定值最接近的树

#include <iostream>
#include<vector>
#include<math.h>
using namespace std;
struct TreeNode{
    TreeNode* left;
    TreeNode* right;
    int val;
    TreeNode(int x):val(x){
        left = nullptr;
        right = nullptr;
    }
};
//构建二叉树。
TreeNode* buildTree(TreeNode* root, int val){
    如果当前子树根节点为空,新建一个根节点。
    if(root == nullptr){
        root = new TreeNode(val);
        return root;
    }    
     如果根节点不为空,并且如果当前节点的值小于根节点,那么去左子树插入这个值。
    if(val < root->val) 
        root->left = buildTree(root->left,val);
    else if(val > root->val)
     如果根节点不为空,并且如果当前节点的值dayu根节点,那么去you子树插入这个值。
        root->right = buildTree(root->right,val);
    最后返回父节点。
    return root;
}

vector<int> res;
void GetInorderSequence(TreeNode* root){
    //中序遍历
    if(root == nullptr) return;
     GetInorderSequence(root->left);   
     if(root != nullptr){
        res.push_back(root->val);      
     }
     GetInorderSequence(root->right);
}
int main() {
    //构建一棵树
    TreeNode* root = nullptr;
    int t[] = {5, 4,6 , 3,7};
    for(int i = 0;i < 5; i++){
        调用比较函数N次
        root = buildTree(root, t[i]);  
    }
    //输入要比较的节点
    int compareVal = 9;
    
    //调用比较函数
    GetInorderSequence(root);
  
    //遍历节点并比较
    int index = 0;
    for(int i = 0; i < res.size(); i++){
        if(compareVal > res[i]){
            index = i;
        }
    }
    //如果要比较数字大于最大值,直接返回和最大值的差值
    //否则输出和这个值接近的两个值的差的最小值。
    if(index  == res.size())
        cout << compareVal - res[index];
    else{
        int diff1 = abs(res[index+1] - compareVal);
        int diff2 = abs(compareVal - res[index]);
        int result = diff1 < diff2 ? diff1 : diff2;
        cout << result << endl;
    }
    return 0;
}

 

 

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客
应支付0元
点击重新获取
扫码支付

支付成功即可阅读