2020年C/C++精选面试题及答案(三)

堆排序的思路

堆是一个完全二叉树
完全二叉树即是:若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。
堆满足两个性质: 堆的每一个父节点数值都大于(或小于)其子节点,堆的每个左子树和右子树也是一个堆。
堆分为最小堆和最大堆。最大堆就是每个父节点的数值要大于孩子节点,最小堆就是每个父节点的数值要小于孩子节点。排序要求从小到大的话,我们需要建立最大堆,反之建立最小堆。
堆的存储一般用数组来实现。假如父节点的数组下标为i的话,那么其左右节点的下标分别为:(2i+1)和 (2i+2)。如果孩子节点的下标为j的话,那么其父节点的下标为(j-1)/2。
完全二叉树中,假如有n个元素,那么在堆中最后一个父节点位置为(n/2-1)。
算法思想
建立堆
调整堆
交换堆顶元素和堆的最后一个元素
在这里插入图片描述
其他面试题 学习交流群960994558

假如现在有一个数组a[8]={100,33,3,7,11,6,8,5};首先我们要建立完全二叉树。
如下图所示:
在这里插入图片描述

然后根据各个父节点,进行一个划分。如图所示:

在这里插入图片描述

该算法的就是将各个父节点与其自己的孩子结点进行对比,然后交换的过程。根据例子,建立一个最大堆,要求每一个父节点的数值是大于孩子节点的。顺序是从最后一个父节点开始(从左至右,从下至上)。所以第一个父节点是数组下标为3的7,黄色区域中的父节点与孩子节点对比后,发现父节点已经大于孩子节点了,所以不用进行交换了。接下来的顺序就是紫色框中的二叉树,就是数组下标为2的数字3,以数字3为父节点,对比它的孩子节点,从左到右,发现右孩子的数值比父节点大,那么将右节点和父节点进行数值交换。交换后的堆如图所示:

在这里插入图片描述

交换结束后,再看蓝色框中的二叉树,就是以数组下标为1的数字33,以33为父节点,看它的孩子节点是否大于父节点,结果发现不大于,则不用交换。此时这个完全二叉树已经是一个堆。
接下来可以讲堆顶元素和堆的最后一个元素进行互换,然后再降最后一个元素至于完全二叉树外。

在这里插入图片描述

此时完全二叉树中,除去100这个元素之后,这个完全二叉树已经不满足堆的性质,所以要进行调整,此时的每次调整要从根节点(和创建堆不一样)开始进行调整,而且父节点和孩子节点交换后,要追踪到交换的孩子节点上(我用浅蓝色标志的部分)。

在这里插入图片描述

此时33和5交换了位置,那么就要从5为父节点,开始往下再和孩子节点进行比较,此时发现孩子节点和父节点中最大为11,那么将11和父节点(即数字5)互换位置。

在这里插入图片描述

此时堆已经调整好,再将堆顶元素和堆的最后一个元素互换,即将33和3互换,然后再降33至于完全二叉树外。
在这里插入图片描述

此时再进行堆的调整,从根开始。

在这里插入图片描述

在这里插入图片描述

此时调整完堆后,再将堆顶元素和最后一个元素互换位置,即将11和6互换,再将11至于完全二叉树外。

在这里插入图片描述

此时再进行完全二叉树的调整,即就是白色填充的元素进行调整。从根开始,按照上面的方法。

在这里插入图片描述

此时又是一个调整好的堆,将堆顶元素和最后一个元素互换。

在这里插入图片描述

再进行完全二叉树的调整

在这里插入图片描述

此时再进行堆顶元素和最后一个元素的互换
在这里插入图片描述

再进行完全二叉树的调整

在这里插入图片描述

最后进行堆顶元素的互换:

在这里插入图片描述

最后一次调整:

在这里插入图片描述

最后一次堆顶元素交换:

在这里插入图片描述

此时数组已经是一个有序数组,是一个生序数组,用for循环输出即可。

Linux查看文件大小命令.

  1. 使用stat命令查看
    stat命令一般用于查看文件的状态信息。stat命令的输出信息比ls命令的输出信息要更详细。
  2. 使用wc命令
    wc命令一般用于统计文件的信息,比如文本的行数,文件所占的字节数。
  3. 使用du命令
    du命令一般用于统计文件和目录所占用的空间大小。
  4. 使用ls命令
    ls 命令一般用于查看文件和目录的信息,包括文件和目录权限、拥有者、所对应的组、文件大小、修改时间、文件对应的路径等等信息。
  5. 使用ll命令(其实就是ls -l的别名)
    在大部分的Linux系统中,都已经设置了ls -l的别名为ll,所以并不存在ll的命令,ll只是一个别名命令而已。
    Linux使用ll命令查看文件大小

布隆过滤算法

布隆过滤器使用二进制向量结合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亿分之一。

给定一组非负整数组成的数组h,代表一组柱状图的高度,其中每个柱子的宽度都为1。 在这组柱状图中找到能组成的最大矩形的面积。 入参h为一个整型数组,代表每个柱子的高度,返回面积的值。 输入描述:

输入包括两行,第一行包含一个整数n(1 ≤ n ≤ 10000)
第二行包括n个整数,表示h数组中的每个值,h_i(1 ≤ h_i ≤ 1,000,000)
输出描述:
输出一个整数,表示最大的矩阵面积。

输入例子1:
6
2 1 5 6 2 3

输出例子1:
10

#include <bits/stdc++.h>
using namespace std;

const int maxn=10000+5;
typedef long long ll;
int h[maxn],l[maxn],r[maxn];

int main()
{
//freopen(“in.txt”,“r”,stdin);
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&h[i]);
l[1]=0;
for(int i=2;i<=n;i++)
{
int lp=i-1;
while(h[lp]>=h[i])
lp=l[lp];
l[i]=lp;
}
r[n]=n+1;
for(int i=n-1;i>=1;i–)
{
int hp=i+1;
while(h[hp]>=h[i])
hp=r[hp];
r[i]=hp;
}
ll ans=0;
for(int i=1;i<=n;i++)
ans=max(ans,(ll)h[i]*(r[i]-l[i]-1));
printf("%lld\n",ans);
return 0;
}

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

对于C++源文件,从文本到可执行文件一般需要四个过程:
预处理阶段:对源代码文件中文件包含关系(头文件)、预编译语句(宏定义)进行分析和替换,生成预编译文件。
编译阶段:将经过预处理后的预编译文件转换成特定汇编代码,生成汇编文件
汇编阶段:将编译阶段生成的汇编文件转化成机器码,生成可重定位目标文件
链接阶段:将多个目标文件及所需要的库连接成最终的可执行目标文件
请问malloc的原理,brk系统调用和mmap系统调用的作用分别是什么?
malloc函数用于动态分配内存。为了减少内存碎片和系统调用的开销,malloc其采用内存池的方式,先申请大块内存作为堆区,然后将堆区分为多个内存块,以块作为内存管理的基本单位。当用户申请内存时,直接从堆区分配一块合适的空闲块。malloc采用隐式链表结构将堆区分成连续的、大小不一的块,包含已分配块和未分配块;同时malloc采用显示链表结构来管理所有的空闲块,即使用一个双向链表将空闲块连接起来,每一个空闲块记录了一个连续的、未分配的地址。
当进行内存分配时,malloc会通过隐式链表遍历所有的空闲块,选择满足要求的块进行分配;当进行内存合并时,malloc采用边界标记法,根据每个块的前后块是否已经分配来决定是否进行块合并。
malloc在申请内存时,一般会通过brk或者mmap系统调用进行申请。其中当申请内存小于128K时,会使用系统函数brk在堆区中分配;而当申请内存大于128K时,会使用系统函数mmap在映射区分配。

请问共享内存相关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:共享内存管理结构体。

请问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接口,实现特定事件处理逻辑。

请问Linux虚拟地址空间

为了防止不同进程同一时刻在物理内存中运行而对物理内存的争夺和践踏,采用了虚拟内存。
虚拟内存技术使得不同进程在运行过程中,它所看到的是自己独自占有了当前系统的4G内存。所有进程共享同一物理内存,每个进程只把自己目前需要的虚拟内存空间映射并存储到物理内存上。 事实上,在每个进程创建加载时,内核只是为进程“创建”了虚拟内存的布局,具体就是初始化进程控制表中内存相关的链表,实际上并不立即就把虚拟内存对应位置的程序数据和代码(比如.text .data段)拷贝到物理内存中,只是建立好虚拟内存和磁盘文件之间的映射就好(叫做存储器映射),等到运行到对应的程序时,才会通过缺页异常,来拷贝数据。还有进程运行过程中,要动态分配内存,比如malloc时,也只是分配了虚拟内存,即为这块虚拟内存对应的页表项做相应设置,当进程真正访问到此数据时,才引发缺页异常。
请求分页系统、请求分段系统和请求段页式系统都是针对虚拟内存的,通过请求实现内存与外存的信息置换。
虚拟内存的好处:
1.扩大地址空间;
2.内存保护:每个进程运行在各自的虚拟内存地址空间,互相不能干扰对方。虚存还对特定的内存地址提供写保护,可以防止代码或数据被恶意篡改。
3.公平内存分配。采用了虚存之后,每个进程都相当于有同样大小的虚存空间。
4.当进程通信时,可采用虚存共享的方式实现。
5.当不同的进程使用同样的代码时,比如库文件中的代码,物理内存中可以只存储一份这样的代码,不同的进程只需要把自己的虚拟内存映射过去就可以了,节省内存
6.虚拟内存很适合在多道程序设计系统中使用,许多程序的片段同时保存在内存中。当一个程序等待它的一部分读入内存时,可以把CPU交给另一个进程使用。在内存中可以保留多个进程,系统并发度提高
7.在程序需要分配连续的内存空间的时候,只需要在虚拟内存空间分配连续空间,而不需要实际物理内存的连续空间,可以利用碎片

虚拟内存的代价:

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

请问操作系统中的缺页中断

malloc()和mmap()等内存分配函数,在分配时只是建立了进程虚拟地址空间,并没有分配虚拟内存对应的物理内存。当进程访问这些没有建立映射关系的虚拟内存时,处理器自动触发一个缺页异常。
缺页中断:在请求分页系统中,可以通过查询页表中的状态位来确定所要访问的页面是否存在于内存中。每当所要访问的页面不在内存是,会产生一次缺页中断,此时操作系统会根据页表中的外存地址在外存中找到所缺的一页,将其调入内存。
缺页本身是一种中断,与一般的中断一样,需要经过4个处理步骤:
1、保护CPU现场
2、分析中断原因
3、转入缺页中断处理程序进行处理
4、恢复CPU现场,继续执行
但是缺页中断是由于所要访问的页面不存在于内存时,由硬件所产生的一种特殊的中断,因此,与一般的中断存在区别:
1、在指令执行期间产生和处理缺页中断信号
2、一条指令在执行期间,可能产生多次缺页中断
3、缺页中断返回是,执行产生中断的一条指令,而一般的中断返回是,执行下一条指令。
【点击加入群聊学习交流群】

猜你喜欢

转载自blog.csdn.net/weixin_52622200/article/details/110656753