2020年美团秋招C++精选面试题及答案(下)

  1. 布隆过滤算法
    布隆过滤器使用二进制向量结合hash函数来记录任意一条数据是否已经存在于集合中。
    布隆过滤器的执行流程为:
    首先申请包含SIZE个bit位的Bit集合,并将所有Bit置0。
    然后使用数种(k)不同的哈希函数对目标数据进行哈希计算并得到k个哈希值(确保哈希值不超过SIZE大小),然后将Bit集合中以哈希值为下标所处的bit值置为1,由于使用了k个哈希函数,因此记录一条数据的信息将在Bit集合中把k个bit值置为1。
    由于哈希函数的稳定性,任意两条相同的数据在Bit集合中所对应的k个bit位置是完全相同的。那么在检测某一条数据是否已经在Bit集合中有记录时,只需检测该条数据的k个哈希值在Bit集合中对应的位置的bit是否均已被标记为1,相反的只要其存在一个哈希值对应的bit位置未被标记为1,则证明该值未被记录过。
    使用示例
    布隆过滤器的示例如下:

在这里插入图片描述

大体过程为:
首先初始化一个二进制的数组,长度设为 L(图中为 8),同时初始值全为 0 。
当写入一个 A1=1000 的数据时,需要进行 H 次 hash 函数的运算(这里为 2 次);与 HashMap 有点类似,通过算出的 HashCode 与 L 取模后定位到 0、2 处,将该处的值设为 1。
A2=2000 也是同理计算后将 4、7 位置设为 1。
当有一个 B1=1000 需要判断是否存在时,也是做两次 Hash 运算,定位到 0、2 处,此时他们的值都为 1 ,所以认为 B1=1000 存在于集合中。
当有一个 B2=3000 时,也是同理。第一次 Hash 定位到 index=4 时,数组中的值为 1,所以再进行第二次 Hash 运算,结果定位到 index=5 的值为 0,所以认为 B2=3000 不存在于集合中。
优缺点
优点
时间复杂度为O(n),且布隆过滤器不需要存储元素本身,使用位阵列,占用空间也很小。
缺点
通过布隆过滤,我们能够准确判断一个数不存在于某个集合中,但对于存在于集合中这个结论,布隆过滤会有误报(可能存在两组不同数据但其多个哈希值完全一样的情况)。但是通过控制Bit集合的大小(即SIZE)以及哈希函数的个数,可以将出现冲突的概率控制在极小的范围内,或者通过额外建立白名单的方式彻底解决哈希冲突问题。
误判率计算公式为(1 – e(-nk/SIZE))k,其中n为目标数据的数量,SIZE为Bit集合大小,k为使用的哈希函数个数;假设现有一千万条待处理数据,Bit集合大小为2^30(约10亿,即占用内存128MB),使用9个不同的哈希函数,计算可得任意两条数据其9次哈希得到的哈希值均相同(不考虑顺序)的概率为2.6e-10,约为38亿分之一。
在这里插入图片描述
在这里插入图片描述
更多一线互联网大厂面试题以及视频资料vx关注零声学院免费领取!
在这里插入图片描述

2. 一个C++源文件从文本到可执行文件经历的过程


对于C++源文件,从文本到可执行文件一般需要四个过程:
预处理阶段:对源代码文件中文件包含关系(头文件)、预编译语句(宏定义)进行分析和替换,生成预编译文件。
编译阶段:将经过预处理后的预编译文件转换成特定汇编代码,生成汇编文件
汇编阶段:将编译阶段生成的汇编文件转化成机器码,生成可重定位目标文件
链接阶段:将多个目标文件及所需要的库连接成最终的可执行目标文件

3. 请问共享内存相关api?

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
和信号量的semctl函数类似,控制共享内存
int shmctl(int shm_id,int command,struct shmid_ds *buf);
shm_id:共享内存标识符
command: 有三个值
IPC_STAT:获取共享内存的状态,把共享内存的shmid_ds结构复制到buf中。
IPC_SET:设置共享内存的状态,把buf复制到共享内存的shmid_ds结构。
IPC_RMID:删除共享内存
buf:共享内存管理结构体。

4. 请问reactor模型组成.

reactor模型要求主线程只负责监听文件描述上是否有事件发生,有的话就立即将该事件通知工作线程,除此之外,主线程不做任何其他实质性的工作,读写数据、接受新的连接以及处理客户请求均在工作线程中完成。其模型组成如下:
在这里插入图片描述

1)Handle:即操作系统中的句柄,是对资源在操作系统层面上的一种抽象,它可以是打开的文件、一个连接(Socket)、Timer等。由于Reactor模式一般使用在网络编程中,因而这里一般指Socket Handle,即一个网络连接。
2)Synchronous Event Demultiplexer(同步事件复用器):阻塞等待一系列的Handle中的事件到来,如果阻塞等待返回,即表示在返回的Handle中可以不阻塞的执行返回的事件类型。这个模块一般使用操作系统的select来实现。
3)Initiation Dispatcher:用于管理Event Handler,即EventHandler的容器,用以注册、移除EventHandler等;另外,它还作为Reactor模式的入口调用Synchronous Event Demultiplexer的select方法以阻塞等待事件返回,当阻塞等待返回时,根据事件发生的Handle将其分发给对应的Event Handler处理,即回调EventHandler中的handle_event()方法。
4)Event Handler:定义事件处理方法:handle_event(),以供InitiationDispatcher回调使用。
5)Concrete Event Handler:事件EventHandler接口,实现特定事件处理逻辑。
5. 请问线程需要保存哪些上下文,SP、PC、EAX这些寄存器有什么作用?
线程在切换的过程中需要保存当前线程Id、线程状态、堆栈、寄存器状态等信息。其中寄存器主要包括SP PC EAX等寄存器,其主要功能如下:
SP:堆栈指针,指向当前栈的栈顶地址
PC:程序计数器,存储下一条将要执行的指令
EAX:累加寄存器,用于加法乘法的缺省寄存器

5. 有1千万条短信,有重复,以文本文件的形式保存,一行一条。请用5分钟时间,找出重复出现最多的前10条。

#include<iostream>
#include<map>
#include<iterator>
#include<stdio.h>
using namespace std;
 
#define HASH __gnu_cxx
#include<ext/hash_map>
#define uint32_t unsigned int
#define uint64_t unsigned long int
struct StrHash
{
    
    
 uint64_t operator()(const std::string& str) const
 {
    
    
 uint32_t b = 378551;
 uint32_t a = 63689;
  uint64_t hash = 0;
 
 for(size_t i = 0; i < str.size(); i++)
 {
    
    
 hash = hash * a + str[i];
 a = a * b;
 }
 
 return hash;
 }
 uint64_t operator()(const std::string& str, uint32_t field) const
 {
    
    
 uint32_t b = 378551;
 uint32_t a = 63689;
 uint64_t hash = 0;
 for(size_t i = 0; i < str.size(); i++)
 {
    
    
 hash = hash * a + str[i];
 a  = a * b;
 }
 hash = (hash<<8)+field;
 return hash;
 }
};
struct NameNum{
    
    
 string name;
 int num;
 NameNum():num(0),name(""){
    
    }
};
int main()
{
    
    
 HASH::hash_map< string, int, StrHash > names;
 HASH::hash_map< string, int, StrHash >::iterator it;
 NameNum namenum[10];
 string l = "";
 while(getline(cin, l))
 {
    
    
 it = names.find(l);
 if(it != names.end())
 {
    
    
 names[l] ++;
 }
 else
 {
    
    
 names[l] = 1;
 names[l] = 1;
 }
 }
 int i = 0;
 int max = 1;
 int min = 1;
 int minpos = 0;
 for(it = names.begin(); it != names.end(); ++ it)
 {
    
    
 if(i < 10)
 {
    
    
 namenum[i].name = it->first;
 namenum[i].num = it->second;
  if(it->second > max)
 max = it->second;
 else if(it->second < min)
 {
    
    
 min = it->second;
 minpos = i;
  }
 }
 else
 {
    
    
 if(it->second > min)
 {
    
    
 namenum[minpos].name = it->first;
 namenum[minpos].num = it->second;
 int k = 1;
 min = namenum[0].num;
 minpos = 0;
 while(k < 10)
  {
    
    
 if(namenum[k].num < min)
 {
    
    
 min = namenum[k].num;
 minpos = k;
 }
 k ++;
 }
 }
 }
 i++;
 
 }
 i = 0;
 cout << "maxlength (string,num): " << endl;
 while( i < 10)
 {
    
    
 cout << "(" << namenum[i].name.c_str() << "," 
<< namenum[i].num << ")" << endl;
 i++;
 }
 return 0;
}

6. 请问malloc的原理,brk系统调用和mmap系统调用的作用分别是什么?

malloc函数用于动态分配内存。为了减少内存碎片和系统调用的开销,malloc其采用内存池的方式,先申请大块内存作为堆区,然后将堆区分为多个内存块,以块作为内存管理的基本单位。当用户申请内存时,直接从堆区分配一块合适的空闲块。malloc采用隐式链表结构将堆区分成连续的、大小不一的块,包含已分配块和未分配块;同时malloc采用显示链表结构来管理所有的空闲块,即使用一个双向链表将空闲块连接起来,每一个空闲块记录了一个连续的、未分配的地址。
当进行内存分配时,malloc会通过隐式链表遍历所有的空闲块,选择满足要求的块进行分配;当进行内存合并时,malloc采用边界标记法,根据每个块的前后块是否已经分配来决定是否进行块合并。

malloc在申请内存时,一般会通过brk或者mmap系统调用进行申请。其中当申请内存小于128K时,会使用系统函数brk在堆区中分配;而当申请内存大于128K时,会使用系统函数mmap在映射区分配。

7. 请问Linux虚拟地址空间

为了防止不同进程同一时刻在物理内存中运行而对物理内存的争夺和践踏,采用了虚拟内存。
虚拟内存技术使得不同进程在运行过程中,它所看到的是自己独自占有了当前系统的4G内存。所有进程共享同一物理内存,每个进程只把自己目前需要的虚拟内存空间映射并存储到物理内存上。 事实上,在每个进程创建加载时,内核只是为进程“创建”了虚拟内存的布局,具体就是初始化进程控制表中内存相关的链表,实际上并不立即就把虚拟内存对应位置的程序数据和代码(比如.text .data段)拷贝到物理内存中,只是建立好虚拟内存和磁盘文件之间的映射就好(叫做存储器映射),等到运行到对应的程序时,才会通过缺页异常,来拷贝数据。还有进程运行过程中,要动态分配内存,比如malloc时,也只是分配了虚拟内存,即为这块虚拟内存对应的页表项做相应设置,当进程真正访问到此数据时,才引发缺页异常。

请求分页系统、请求分段系统和请求段页式系统都是针对虚拟内存的,通过请求实现内存与外存的信息置换。
虚拟内存的好处:
1.扩大地址空间;
2.内存保护:每个进程运行在各自的虚拟内存地址空间,互相不能干扰对方。虚存还对特定的内存地址提供写保护,可以防止代码或数据被恶意篡改。
3.公平内存分配。采用了虚存之后,每个进程都相当于有同样大小的虚存空间。
4.当进程通信时,可采用虚存共享的方式实现。
5.当不同的进程使用同样的代码时,比如库文件中的代码,物理内存中可以只存储一份这样的代码,不同的进程只需要把自己的虚拟内存映射过去就可以了,节省内存
6.虚拟内存很适合在多道程序设计系统中使用,许多程序的片段同时保存在内存中。当一个程序等待它的一部分读入内存时,可以把CPU交给另一个进程使用。在内存中可以保留多个进程,系统并发度提高
7.在程序需要分配连续的内存空间的时候,只需要在虚拟内存空间分配连续空间,而不需要实际物理内存的连续空间,可以利用碎片

虚拟内存的代价:
1.虚存的管理需要建立很多数据结构,这些数据结构要占用额外的内存
2.虚拟地址到物理地址的转换,增加了指令的执行时间。
3.页面的换入换出需要磁盘I/O,这是很耗时的
4.如果一页中只有一部分数据,会浪费内存。

猜你喜欢

转载自blog.csdn.net/lingshengxueyuan/article/details/108603022