大厂面试题含答案(一)https://developer.aliyun.com/article/1472486
2020 年小米精选 50 面试题及答案
1. 协程了解过么?
协程是更轻量级的线程。用于解决线程间切换和进程间切换的通病(对内核开销过大),协程各个状态(阻塞、运行)的切换是由程序控制,而不是内核控制,减少了 开销。
功能特点:通过应用层程序,记录上下文栈区,实现在程序执行过程中的跳跃执行。 由此可以选择不阻塞的部分执行提升运行效率。
2. 变量的声明和定义有什么区别
变量分配地址和存储空间的称为定义,不分配地址的称为声明。一个变量可以在多个 地方声明,但只能在一个地方定义。加入 extern修饰的是变量的声明,说明此变量将 在文件以外或在文件后面部分定义。
说明:很多时候一个变量,只是声明,不分配内存空间,知道具体使用时才初始化,分 配内存空间,如外部变量。
3. sizeof 和 strlen 的区别
sizeof 和 strlen 有以下区别:
sizeof 是一个操作符,strlen 是库函数。
sizeof的参数可以是数据的类型,也可以是变量,而 strlen 只能以结尾为‘\0‘的字 符串作参数。 编译器在编译时就计算出了 sizeof的结果。而 strlen 函数必须在运行时才能计算 出来。并且 sizeof 计算的是数据类型占内存的大小,而 strlen计算的是字符串实际的 长度。 数组做 sizeof 的参数不退化,传递给 strlen 就退化为指针了。
注意:有些是操作符看起来像是函数,而有些函数名看起来又像操作符,这类容易混淆的名称一定要加以区分,否则遇到数组名这类特殊数据类型作参数时就很容易出错。最 容易混淆为函数的操作符就是 sizeof。
说明:指针是一种普通的变量,从访问上没有什么不同于其他变量的特性。其保存的数值是个整型数据,和整型变量不同的是,这个整型数据指向的是一段内存地址。
4. 一个指针可以是 volatile 吗?
可以,因为指针和普通变量一样,有时也有变化程序的不可控性。常见例:子中断服务 子程序修改一个指向一个 buffer 的指针时,必须用volatile 来修饰这个指针。
5. 简述 strcpy sprintf 与 mencpy 的区别
三者主要有以下不同之处:
(1)操作对象不同,strcpy 的两个操作对象均为字符串,sprintf 的操作源对象可以是多种数据类型,目的操作对象是字符串,memcpy 的两个对象就是两个任意可操作的 内存地址,并不限于何种数据类型。
(2)执行效率不同,memcpy 最高,strcpy 次之,sprintf 的效率最低。
(3)实现功能不同,strcpy主要实现字符串变量间的拷贝,sprintf 主要实现其他数 据类型格式到字符串的转化,memcpy 主要是内存块间的拷贝。
说明:strcpy、sprintf 与 memcpy 都可以实现拷贝的功能,但是针对的对象不同,根据实际需求,来选择合适的函数实现拷贝功能。
6. 编码实现直接插入排序
#include <stdio.h> int main() { int i,temp,p; int array[10] = {2,6,1,9,4,7,5,8,3,0}; printf("Display this array:\n"); for(i=0;i<10;i++) { printf("%d ",array[i]); } for(i=1;i<10;i++) { temp = array[i]; p = i-1; while(p >= 0 && temp < array[p]) { array[p+1] = array[p]; p--; } array[p+1] = temp; } printf("\n"); printf("After sorting,this array is:\n"); for(i=0;i<10;i++) { printf("%d ",array[i]); } printf("\n"); return 0; }
7. STL 里 set 和 map 是基于什么实现的?
STL 的 set 和 map 都是基于红黑树实现的
AVL 是一种高度平衡的二叉树,所以通常的结果是,维护这种高度平衡所付出的代价比从中获得的效率收益还大,故而实际的应用不多,更多的地方是用追求局部而不是非常 严格整体平衡的红黑树。当然,如果场景中对插入删除不频繁,只是对查找特别有要求, AVL 还是优于红黑的。
8. 求两个多项式的乘积
pa=anx^n + an-1x^(n-1) + … + a1x + a0;
pb=bmx^n + bm-1x^(n-1) + … + b1x + b0;其中,an,an-1…a1,a0,bm,bm-1…b1,b0 都是整数,范围是[-1000,1000],0<=n,m<=1000。pa*pb 的结果也是多项式,请你设 计如何表示一个多项式,并写出两个多项式相乘的程序。
采用链表来表示多项式,因为如果用数组有可能遇到稀疏问题,同时链表可以动态添加节点。 相乘时,采用 hashmap来保存两两相乘的结果,最后在扫描一遍 hashmap 即可构造出多 项式
9. 一副从 1 到 n 的牌,每次从牌堆顶取一张放桌子上,再取 一张放牌堆底,直到手上没牌,最后桌子上的牌是从 1 到 n 有序,设计程序,输入 n,输出牌堆的顺序数组。
public class Main { public static void main(String[] args) { fun(20); } /** * 一副从 1 到 n 的牌, * 每次从牌堆顶取一张放桌子上, * 再取一张放牌堆底,直到手上没牌, * 最后桌子上的牌是从 1 到 n 有序, * 设计程序,输入 n,输出牌堆的顺序数组 */ public static void fun(int n) { //结果数组 List<Integer> result = new ArrayList<>(); //原始数组 List<Integer> temp = new ArrayList<>(); for (int i = 0; i < n; i++) { result.add(i + 1); } //结果数组逆序 for (int i = n - 1; i >= 0; i--) { int val = result.get(i); temp.add(0, val); if (temp.size() > 1 && temp.size() < n) { //交换位置,最后一张放到最上面 temp = change(temp); } } for (int num : temp) { System.out.print(num + " "); } } public static List<Integer> change(List<Integer> list) { List<Integer> temp = new ArrayList<>(); temp.add(list.get(list.size() - 1)); for (int i = 0; i < list.size() - 1; i++) { temp.add(list.get(i)); } return temp; } }
10. redis 数据库主从不一致问题怎么解决
(1)业务可以接受,系统不优化
(2)强制读主,高可用主库,用缓存提高读性能
(3)在 cache里记录哪些记录发生过写请求,来路由读主还是读从
11. 虚拟地址、逻辑地址、线性地址、物理地址的区别
虚拟地址:指的是由程序产生的由段选择符和段内偏移地址两个部分组成的地址。为什么叫它是虚拟的地址呢?因为这两部分组成的地址并没有直接访问物理内存,而是要通 过分段地址的变换机构处理或映射后才会对应到相应的物理内存地址。逻辑地址:指由程序产生的与段相关的偏移地址部分。不过有些资料是直接把逻辑地址 当成虚拟地址,两者并没有明确的界限。
线性地址:指的是虚拟地址到物理地址变换之间的中间层,是处理器可寻指的内存空间称为线性地址空间)中的地址。程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。如果启用了分页机制,那么线性地址可以再经过变换产生物理地址。若是没有采用分页机制,那么线性地址就是物理地址。 物理地址:指的是现在 CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的 最终结果。
12. 指针寄存器?
指针寄存器 32 位 CPU 有 2 个 32 位通用寄存器 EBP 和 ESP。其低 16 位对应先前 CPU 中的 SBP 和 SP, 对低 16 位数据的存取,不影响高 16 位的数据。 寄存器 EBP、ESP、BP 和 SP 称为指针寄存器(Pointer
Register),主要用于存放堆栈内 存储单元的偏移量,用它们可实现多种存储器操作数的寻址方式,为以不同的地址形式访问存储单元提供方便。指针寄存器不可分割成 8 位寄存器。作为通用寄存器,也可存 储算术逻辑运算的操作数和运算结果。它们主要用于访问堆栈内的存储单元,并且规定: BP 为基指针(Base Pointer)寄存器,通过它减去一定的偏移值,来访问栈中的元素;SP 为堆栈指针(Stack Pointer)寄存器,它始终指向栈顶。 说明:因栈的生长方向是从高地址向低地址生长,所以,进栈时,sp自减;出栈时, sp 自增;
13. 解释分段和分页机制
一 分段机制
1、什么是分段机制 分段机制就是把虚拟地址空间中的虚拟内存组织成一些长度可变的称为段的内存块单 元。
2、什么是段
每个段由三个参数定义:段基地址、段限长和段属性。 段的基地址、段限长以及段的保护属性存储在一个称为段描述符的结构项中。
3、段的作用
段可以用来存放程序的代码、数据和堆栈,或者用来存放系统数据结构。
4、段的存储地址
系统中所有使用的段都包含在处理器线性地址空间中。
5、段选择符
逻辑地址包括一个段选择符或一个偏移量,段选择符是一个段的唯一标识,提供了段描述符表,段描述符表指明短的大小、访问权限和段的特权级、段类型以及段的第一个字节在线性地址空间中的位置(称为段的基地址)。逻辑地址的偏移量部分到段的基地址上就可以定位段中某个字节的位置。因此基地址加上偏移量就形成了处理器线性地址空 间中的地址。
6 逻辑地址到线性地址的变换过程
如果没有开启分页,那么处理器直接把线性地址映射到物理地址,即线性地址被送到处理器地址总线上;如果对线性地址空间进行了分页处理,那么就会使用二级地址转换把 线性地址转换成物理地址。 7、虚拟地址到物理地址的变换过程
二分页机制
1、什么是分页机制
分页机制在段机制之后进行的,它进一步将线性地址转换为物理地址。
2、分页机制的存储
分页机制支持虚拟存储技术,在使用虚拟存储的环境中,大容量的线性地址空间 需 要使用小块的物理内存(RAM 或ROM)以及某些外部存储空间来模拟。当使用分页时, 每个段被划分成页面(通常每页为 4K 大小),页面会被存储于物理内存中或硬盘中。操作系统通过维护一个页目录和一些页表来留意这些页面。当程序(或任务)试图访问线性地址空间中的一个地址位置时,处理器就会使用页目录和页表把线性地址转换成一 个物理地址,然后在该内存位置上执行所要求的操作。
3、线性地址和物理地址之间的变换过程
三 分段机制和分页机制的区别
1、分页机制会使用大小固定的内存块,而分段管理则使用了大小可变的块来管理内存。
2、分页使用固定大小的块更为适合管理物理内存,分段机制使用大小可变的块更 适 合处理复杂系统的逻辑分区。
3、段表存储在线性地址空间,而页表则保存在物理地址空间。
14. Linux 管道的实现机制
在 Linux 中,管道是一种使用非常频繁的通信机制。从本质上说,管道也是一种文件,但它又和一般的文件有所不同,管道可以克服使用文件进行通信的两个问题,具体表现 为:
1)限制管道的大小。实际上,管道是一个固定大小的缓冲区。在 Linux 中,该缓冲 区的大小为 1 页,即 4K 字节,使得它的大小不像文件那样不加检验地增长。使用单个 固定缓冲区也会带来问题,比如在写管道时可能变满,当这种情况发生时,随后对管道 的 write()调用将默认地被阻塞,等待某些数据被读取,以便腾出足够的空间供 write() 调用写。
2)读取进程也可能工作得比写进程快。当所有当前进程数据已被读取时,管道变空。 当这种情况发生时,一个随后的 read()调用将默认地被阻塞,等待某些数据被写入, 这解决了 read()调用返回文件结束的问题。
注意:从管道读数据是一次性操作,数据一旦被读,它就从管道中被抛弃,释放空间以 便写更多的数据。
- 管道的结构 在 Linux 中,管道的实现并没有使用专门的数据结构,而是借助了文件系统的 file 结构和 VFS 的索引节点 inode。通过将两个 file 结构指向同一个临时的 VFS 索引 节点,而这个 VFS 索引节点又指向一个物理页面而实现的。如图 1.1 所示。
图 1.1 中有两个 file 数据结构,但它们定义文件操作例程地址是不同的,其中一个 是向管道中写入数据的例程地址,而另一个是从管道中读出数据的例程地址。这样,用 户程序的系统调用仍然是通常的文件操作,而内核却利用这种抽象机制实现了管道这一 特殊操作。
- 管道的读写 管道实现的源代码在 fs/pipe.c 中,在 pipe.c 中有很多函数,其中有两个函数比较重 要,即管道读函数 pipe_read()和管道写函数 pipe_wrtie()。管道写函数通过将字节复 制到 VFS 索引节点指向的物理内存而写入数据,而管道读函数则通过复制物理内存中的字节而读出数据。当然,内核必须利用一定的机制同步对管道的访问,为此,内核 使用了锁、等待队列和信号。
当写进程向管道中写入时,它利用标准的库函数 write(),系统根据库函数传递的文件 描述符,可找到该文件的 file 结构。file结构中指定了用来进行写操作的函数(即 写入函数)地址,于是,内核调用该函数完成写操作。写入函数在向内存中写入数据之 前,必须首先检查 VFS索引节点中的信息,同时满足如下条件时,才能进行实际的 内存复制工作:
1)内存中有足够的空间可容纳所有要写入的数据;
2)内存没有被读程序锁定。如果同时满足上述条件,写入函数首先锁定内存,然后从写进程的地址空间中复制数据 到内存。否则,写入进程就休眠在 VFS索引节点的等待队列中,接下来,内核将调用调度程序,而调度程序会选择其他进程运行。写入进程实际处于可中断的等待状态,当内存中有足够的空间可以容纳写入数据,或内存被解锁时,读取进程会唤醒写入进程,这时,写入进程将接收到信号。当数据写入内存之后,内存被解锁,而所有休眠在索引 节点的读取进程会被唤醒。管道的读取过程和写入过程类似。但是,进程可以在没有数据或内存被锁定时立即返回错误信息,而不是阻塞该进程,这依赖文件或管道的打开模式。反之,进程可以休眠在索引节点的等待队列中等待写入进程写入数据。当所有的进程完成了管道操作之后,管道的索引节点被丢弃,而共享数据页也被释放。
15. mysql 的四种隔离级别
Mysql 的四种隔离级别 SQL 标准定义了 4 类隔离级别,包括了一些具体规则,用来限定事务内外的哪些改变是可见的,哪些是不可见的。低级别的隔离级一般支持更高的并发处理,并拥有更低的系 统开销。 Read
Uncommitted(读取未提交内容)
在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为脏读 (Dirty Read)。
Read Committed(读取提交内容)
这是大多数数据库系统的默认隔离级别(但不是 MySQL 默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别 也支持所谓的不可 重复读(Nonrepeatable Read),因为同一事务的其他实例在该实例处理其间可能会有 新的 commit,所以同一 select 可能返回不同结果。Repeatable Read(可重读) 这是 MySQL 的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读 (Phantom Read)简单的说,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新 行,当用户再读取该范围的数据行时,会发现有新的“幻影”行。InnoDB 和 Falcon 存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决 了该问题。
Serializable(可串行化)
这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象 和锁竞争。
16. SQL 中 where 和 having 的区别
where 是一个约束声明,使用 where 约束来自数据库的数据,where 是在结果返回之前 起作用的,且 where中不能使用聚合函数。 having 是一个过滤声明,是在查询返回结果集以后对查询结果进行的过滤操作,在 having 中可以使用聚合函数。where 和 having 都是用来筛选的,后面跟的都是筛选条件,只不过 where 筛选的是原 始的表数据;having
筛选的是分组(group by)后的组数据,是对查询结果集的过滤; 所以如果一条查询语句中同时有 where、group by 和having,那么执行顺序会是where>group by>having。
having 不能单独使用,只能和 group by 连用,但用 group by 不一有> having (它只是 一个筛选条件用的,取决于是否要对分组数据进行筛选)。
17. 如何判断一个你打向的 ip 在国内还是国外,用什么工具?
工具类 CNIPRecognizer,用于判断某个 ip 是否为国内 ip。 判断原理很简单,如果该 ip 存在于 apnic 公开的 cn 地址段中则认为它是国内 ip。
示例代码:
System.out.println("8.8.8.8: " + CNIPRecognizer.isCNIP("8.8.8.8")); System.out.println( "114.114.114.114:"+CNIPRecognizer.isCNIP("114.114.114.114")); • 1 • 2 • 3
18. 数据的分库分表会产生什么问题,如何解决? 常见的分散存储的方法有分库和分布两大类
1.分库 分库之的是按照业务模块将数据分散到不同的数据库服务器,虽然业务 分库能够分 散存储和访问的压力,但是同时也带来了新的问题,主要存在的问题如下:
1)join 操作问题 业务分库后,原本在同一个数据库中的表分散到不同数据库中,导致无 法使用 SQL 中的 join 查询
2)事务问题原本在同一个数据库中不同的表可以在同一个事物中修改,业务分库后,表分散到不同的数据库中,无法通过事务统一修改,虽然数据库厂商针对此问题提供了一些分布式事 务解决方案(例如,MySQL 的
XA),但是性能实在太低,与高性功能存储的目标是相违 背的 3)成本问题 业务分库同时也带来了成本的代价,本来 1台服务器搞定的事情,现在需要 3 台,如果 考虑备份,那就是 2 台变成了 6 台基于上述原因,对于初始业务,并不建议一开始就这样拆分,主要有几个原因:
1)初创业务存在很大的不确定性,业务不一定能发展起来,业务开始的时候并没有真正 的存储和访问压力,业务分库并不能为业务带来价值
2)业务分库后,表之间的 join 查询,数据库事务无法简单实现了发
3)业务分库后,因为不同的数据要读写不同的数据库,代码需要增加根据数据类型映射到不同数据库的逻辑,增加了工作量,而业务初创期最重要的是快速实现,快速验证, 业务分库会拖慢业务节奏
2.分表 将不同的业务数据分散存储到不同的数据库服务器,能够支撑百万甚至千万用户规模的 业务,但是如果业务继续发展,同一个业务的单表数据也会达到单台数据库服务器的处理瓶颈,此时就需要对单表进行拆分,单表数据拆分有两种方式:垂直分表和水平分表分表能够有效的分散存储压力和带来性能提升,但是和分库一样,也会引入各种复杂性,主要存在的问题如下:
1)垂直分表 垂直分表适合将表中某些不常用而且占了大量空间的列拆分出去,垂直分表的引入的复杂性主要体现在表操作的数量会增加,例如原来只要一次查询的就可以获 取,现在要查询两次或者多次才能获得想要的数据
2)水平分表水平分表适合表行数特别大的表,如果单表行数超过 5000 万就必须进行分表,这个数字可以作为参考,但是并不是绝对的标准,关键还是要看表的访问性能水平分表相比垂直分表,会引入更多的复杂性,主要表现在以下几个方面:
3. 路由 水平分表后,某条数据具体属于哪个切分后的表,需要增加路由算法进行计算,这 个算法会引入一定的复杂性,常见的路由算法有如下几种:
1)范围路由 选择有序的数据列作为路由条件,不同分段分散到不同的数据库表中, 以常见的用户 ID 为例,路由算法可以按照 10000的范围大小进行分段 1-9999 放到数 据库 1 中的表,10000-19999 的数据放到数据库 2 中的表,依次类推,范围路由算法的复杂性主要体现在分段大小的选取上,分段太小会导致切分后的子表数据量过多,增加维护复杂度;分段太大可能会导致单表依然存在性能问题,一般建议分段大学在 100 万到 200 万之间,具体要根据业务选择合适的大小分段,路由算法的优点就是可以随着 数据的增加可以平滑的扩充新的表,原有的数据不需要懂,范围路由的一个比较隐含的 缺点就是分布不均匀
2)Hahs 路由算法选择某个列(或者某几个列组合也可以)的进行 Hash 运算,然后根 据 Hash 结果分散到不同的数据库表中,同样根据用户 ID
为例,假如一开始就规划 10 个数据库表,路由算法可以简单的用 user_id%10 的值来表示数据所属的数据库表编号, ID 为 985的用户放到编号为 5 的子表中,ID 为 10086 的用户放到编号为 6 的子表中; Hash 路由算法设计的复杂点主要体现在初始表数量的选取上,表数量太多维护比较麻 烦,表数据量太少又可能导致单表性能问题,而用了 Hash 路由后,增加表的数量非常 麻烦,所有数据都要重新分布,Hash 路由算法的优缺点和范围路由基本相反,Hash 路由算法的优点是表分布比较均匀,缺点是扩充新的表很麻烦,所有数据需要重新分布
3)配置路由
配置路由就是路由表,用一张独立的表来记录路由信息,同样根据用户 ID 为例,我们新增一张 user_router 表,这个表包含 user_id 和 table_id 两列,根据 user_id 就可以查询对应的 table_id,配置路由设计简单,使用起来非常灵活,尤其是 在扩充表的时候,只需要迁移指定书,然后修改路由表就可以。配置路由的缺点就是必须多查询一次,会影响整体的性能;而且路由表本身如果太大,性能同样可能成为瓶颈,如果我们再次将路由表分库分表,则面临一个死循环式的路由算法选择问题 分表操作和分库操作一样,同样会存在一些问题,主要体现在如下几个方面:
1)join 操作 水平分表后,数据分散到多个表中,如果需要与其他表进行 join 查询,需要在业务 代码或者数据库中间件中进行多次join 查询,然后将结果合并
2)count()操作 水平分表后,虽然物理上数据分散到多个表中,但是某些业务逻 辑上还是会将这些表当作一个表进行处理,例如,获取记录总数用于分页或展示,水平分 表之前用一个count()就能完成的操作,在分表之后就没有那么简单了,常见的处理方 式有如下两种:
⑴count()相加 具体做法就是在业务代码或者数据库中间件中对每个表进行 count() 操作,然后将结果相加,这种方式实现简单,缺点就是性能比较低
⑵记录数表 具体做法就是新建一张表,例如表名为:记录数表,包含 table_name,row_count两个字段,每次插入或删除子表数据成功后,都更新记录数表, 这种方式获取表记录数的性能要大大优于count()相加方式,因为只需要一次简单的查 询就可以获得数据,缺点是复杂度增加不少,对子表的操作要同步操作记录数表,如果一个业务逻辑遗漏了,数据就会不一致;而且针对记录数表的操作和针对子表的操作无法放在同一个事物中进行处理,异常的情况会出现操作子表成功了而操作记录数表示不,同样导致数据不一致,同时,记录数表的方式也增加了数据库的写压力,因为每次针对 子表的 insert 和 delete 操作需要 update记录数表,所以对于一些不要去记录数实 时 保持精确的业务,也可以通过后台定时更新记录数表,定时更新实际上就是 count()相加和记录数表的结合,定时通过 count()相加计算表的记录数,然后更新记录数表中的 数据
3)order by 操作水平分表后,数据分散到多个子表中,排序操作无法在数据库中 完成,只能由业务代码或数据库中间件分表查询美国子表中的数据,然后汇总进行排序.
19. HTTP2.0 的多路复用和 HTTP1.X 中的长连接复用有什么区 别?
HTTP/1.* 一次请求-响应,建立一个连接,用完关闭;每一个请求都要建立一个连接; HTTP/1.1 Pipeling解决方式为,若干个请求排队串行化单线程处理,后面的请求等待 前面请求的返回才能获得执行机会,一旦有某请求超时等,后续请求只能被阻塞,毫无办法,也就是人们常说的线头阻塞; HTTP/2 多个请求可同时在一个连接上并行执行。某个请求任务耗时严重,不会影响到 其它连接的正常执行;
具体如图:
服务器推送到底是什么? 服务端推送能把客户端所需要的资源伴随着 index.html 一起发送到客户端,省去了客户端重复请求的步骤。正因为没有发起请求,建立连接等操作,所以静态资源通过服务 端推送的方式可以极大地提升速度。具体如下:
普通的客户端请求过程:
服务端推送的过程:
20. HTTP1.0 和 HTTP1.1 和 HTTP2.0 的区别
HTTP1.0 和 HTTP1.1 的区别
1.1 长连接(Persistent Connection) HTTP1.1 支持长连接和请求的流水线处理,在一个 TCP 连接上可以传送多个 HTTP 请求 和响应,减少了建立和关闭连接的消耗和延迟,在 HTTP1.1 中默认开启长连接keep-alive,一定程度上弥补了 HTTP1.0 每次请求都要创建连接的缺点。HTTP1.0 需要 使用 keep-alive参数来告知服务器端要建立一个长连接。
1.2 节约带宽 HTTP1.0 中存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务 器却将整个对象送过来了,并且不支持断点续传功能。HTTP1.1 支持只发送 header 信 息(不带任何 body信息),如果服务器认为客户端有权限请求服务器,则返回 100, 客户端接收到 100 才开始把请求 body 发送到服务器;如果返回401,客户端就可以不 用发送请求 body 了节约了带宽。
1.3 HOST 域 在 HTTP1.0 中认为每台服务器都绑定一个唯一的 IP 地址,因此,请求消息中的 URL 并 没有传递主机名(hostname),HTTP1.0 没有 host 域。随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共 享一个 IP 地址。HTTP1.1的请求消息和响应消息都支持 host 域,且请求消息中如果没 有 host 域会报告一个错误(400 Bad Request)。
1.4 缓存处理 在 HTTP1.0 中主要使用 header 里的 If-Modified-Since,Expires 来做为缓存判断的标 准,HTTP1.1 则引入了更多的缓存控制策略例如 Entity tag,If-Unmodified-Since, If-Match,If-None-Match 等更多可供选择的缓存头来控制缓存策略。
1.5 错误通知的管理 在 HTTP1.1 中新增了 24 个错误状态响应码,如 409(Conflict)表示请求的资源与资 源的当前状态发生冲突;410(Gone)表示服务器上的某个资源被永久性的删除。 2 HTTP1.1 和 HTTP2.0 的区别
2.1 多路复用 HTTP2.0 使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的 数量比 HTTP1.1 大了好几个数量级。HTTP1.1 也可以多建立几个 TCP 连接,来支持处理 更多并发的请求,但是创建 TCP 连接本身也是有开销的。
2.2 头部数据压缩 在 HTTP1.1 中,HTTP 请求和响应都是由状态行、请求/响应头部、消息主体三部分组成。 一般而言,消息主体都会经过 gzip 压缩,或者本身传输的就是压缩过后的二进制文件,但状态行和头部却没有经过任何压缩,直接以纯文本传输。随着 Web 功能越来越复杂,每个页面产生的请求数也越来越多,导致消耗在头部的流量越来越多,尤其是每次都要 传输 UserAgent、Cookie这类不会频繁变动的内容,完全是一种浪费。 HTTP1.1 不支持 header 数据的压缩,HTTP2.0 使用 HPACK 算法对header 的数据进行压 缩,这样数据体积小了,在网络上传输就会更快。
2.3 服务器推送 服务端推送是一种在客户端请求之前发送数据的机制。网页使用了许多资源:HTML、样 式表、脚本、图片等等。在 HTTP1.1 中这些资源每一个都必须明确地请求。这是一个很 慢的过程。浏览器从获取 HTML开始,然后在它解析和评估页面的时候,增量地获取更 多的资源。因为服务器必须等待浏览器做每一个请求,网络经常是空闲的和未充分使用 的。为了改善延迟,HTTP2.0 引入了 server push,它允许服务端推送资源给浏览器,在浏览器明确地请求之前,免得客户端再次创建连接发送请求到服务器端获取。这样客户端 可以直接从本地加载这些资源,不用再通过网络。
21. 什么是动态特性?
在绝大多数情况下, 程序的功能是在编译的时候就确定下来的, 我们称之为静态特 性。 反之, 如果程序的功能是在运行时刻才能确定下来的,则称之为动态特性。 C++中, 虚函数,抽象基类, 动态绑定和多态构成了出色的动态特性。
22. 基类的有 1 个虚函数,子类还需要申明为 virtual 吗?
不申明没有关系的。 不过,我总是喜欢显式申明,使得代码更加清晰。
23. 在 C++ 程序中调用被 C 编译器编译后的函数,为什么 要加 extern “C”声明?
函数和变量被 C++编译后在符号库中的名字与 C 语言的不同,被 extern “C”修饰的 变量和函数是按照 C语言方式编译和连接的。由于编译后的名字不同,C++程序不能直 接调用 C 函数。C++提供了一个 C 连接交换指定符号extern“C”来解决这个问题。
24. 如何定义 Bool 变量的 TRUE 和 FALSE 的值。
不知道这个题有什么陷阱,写到现在神经已经大了,一般来说先要把 TURE 和 FALSE 给 定义了,使用#define 就可以:
#define TURE 1 #define FALSE 0
如果有一个变量 需要定义成 bool 型的,
举个例子:bool a=TURE;就可以了。 false/true 是标准 C++语言里新增的关键字,而 FALSE/TRUE 是通过#define,这要用 途是解决程序在 C 与 C++中环境的差异,以下是 FALSE/TRUE 在 windef.h 的定义:
#ifndef FALSE #define FALSE 0 #endif #ifndef TRUE #define TRUE 1 #endif
也就是说 FALSE/TRUE 是 int 类型,而 false/true 是 bool 类型;所以两者不一样的, 只不过我们在使用中没有这种感觉,因为 C++会帮你做隐式转换。
25. 内联函数 INline 和宏定义一起使用的区别。
内联函数是在编译的时候已经做好将对应的函数代码替换嵌入到对应的位置,适用于 代码较少的函数。宏定义是简单的替换变量,如果定义的是有参数的函数形式,参数 不做类型校验。
26. 编写 my_strcpy 函数,实现与库函数 strcpy 类似的功能, 不能使用任何库函数.
char *strcpy(char *strDest, const char *strSrc) { if ( strDest == NULL || strSrc == NULL) return NULL ; if ( strDest == strSrc) return strDest ; char *tempptr = strDest ; while( (*strDest++ = *strSrc++) != ‘’); return tempptr ; }
27. 完成程序,实现对数组的降序排序
#include void sort(int array[] ); int main() { int array[]={45,56,76,234,1,34,23,2,3}; //数字任意给出 sort( array ); return 0; } void sort( int array[] ) { int i,j,k; for (i=1;i<=7;i++) { if (array[i]>array[i-1]) { k=array[i]; j=i-1; do { array[j+1]=array[j]; j– ; } while(k>array[j]&&j>=0); array[j+1]=k; } } }
28. C 中 static 有什么作用?
(1)隐藏。 当我们同时编译多个文件时,所有未加 static 前缀的全局变量和函数都具 有全局可见性,故使用 static在不同的文件中定义同名函数和同名变量,而不必担心命名 冲突。
(2)static的第二个作用是保持变量内容的持久。存储在静态数据区的变量会在程序 刚开始运行时就完成初始化,也是唯一的一次初始化。共有两种变量存储在静态存储区:全局变量和 static 变量。
(3)static 的第三个作用是默认初始化为 0.其实全局变量也具备这一属性,因为全局变量也存储在静态数据区。在静态数据区,内存中所有的字节默认值都是 0×00,某些 时候这一特点可以减少程序员的工作量。
29. 阅读下面代码,回答问题.
void GetMemory(char **p, int num){ *p = (char *)malloc(num); } void Test(void){ char *str = NULL; GetMemory(&str, 100); strcpy(str, "hello"); printf(str); }
请问运行 Test 函数会有什么样的结果? 可以运行
30. delete []arry 和 delete arry 一样吗?不一样请说明;
delete []arry 释放的是多个同一类型的地址空间
delete arry 释放的是一个某种类型的地址空间
31. 请描述同一网段下主机 Ap 主机 B 的全过程.假定此时主 机 A 知道主机 B 的 P 地址,但不知道主机 B 的 MAC 地址
1.主机 A 广播 ARP 请求,询问主机 B 的 MAC 地址。
2.主机 B 收到 ARP 请求,以 ARP 响应形式回复自己的 MAC 地址。
3.主机 A 收到主机 B 的 MAC 地址,完成 CMP 请求报文构造,发送给 B。
4.主机 B 收到主机 A 的 ICMP 请求报文,回复一个 ICMP 应答报文。
5.主机 A 收到 CMP 应答报文,ping 结束。 Ping 过程回答正确给 5 分。ARP 过程回答正确给 5 分。
32. 请描述拥有私有地址的局域网内主机向 888 发送 DN 查 询请求报文时报文的传输层端口、网络层地址以及数据链路 层地址几个主要字段与其在传输过程中的变化。
主机构造 DNS 查询请求∪DP 报文,目标 P 地址 8.8.8.8,源 IP 地址为私有地址,报文的 MAC 目的地址为局域网网关的 MAC 地址,源 MAC 地址为主机网卡 MAC 地址(对 ARP 过程 描述不做要求),源端口任意,目的端口532.报文到达网关处,修改源 P 地址为网关公有 IP 地址,同时根据需要修改源端口,然后将报文发送到 8.8.8.8。此时一般情况下会移 除原有的 MAC 层头部,根据实际情况有可能会有其他头部加入。
答出四层细节的给 3 分
答出三次细节(包括 NAT)的给 4 分
答出二层细节的给 3 分(不要求回答进入外网后的二层情况)
33. 同一主机上有两个进程,其中各有一个变量 a 和 b。请问 a 和 b 的地址可能相同吗?请详细说明原因
虚拟地址可能相同,但物理地址不可能相同。每一个进程有独立的虚拟地址空间,一个进程中的虚拟地址需要经过转换才能转成物理地址。不同进程所对应的虚拟地址即使相同也不会被转成统一物理地址。实际上内存是按照页来组织的,每进程有一个虚拟地址到物理地址的映射表(页表),专门用于进程对应的虚拟页到物理页的映射(CPU 内部 有一个 TLB 部件专门用于加速映射过程)。当进程 A读写变量 a 时,会根据 a 的虚拟地 址找到其所在的虚拟页,通过页表找到物理页并进一步定位到物理地址。由于进程 A、 B拥有完全不同的映射表,因此物理地址不会相同。
答岀虚拟地址和物理地址不同且知道虚拟地址可以相同的给 3 分。
描述出映射过程的给 4 分。
答出页表和页的给 3 分。
34. 有一个长度为 n 的整形数组,请给出判断某整数是否在该 数组中的方法。
要求 时间复杂度低于 O(n) 不要使用标准库中的提供的数据结构 允许使用额外内存 允许对原始数组进行预处理 请在代码中添加必要注释。
使用 set 或 unordered set 红黑树或哈希
评分点
1 正确对数组做有序化处理或树处理的给 10分(如果用哈希表,要求对哈希函数进行实 现,但不对哈希函数的均匀性做要求,有效即可,但不得过于简单)
2 正确实现查找部分逻辑的给 10分(如折半查找或在树中查找);如果用哈希表则需要 哈希碰撞处理
3 代码风格糟糕可适当减分(最多可减 5 分)
35. 编写代码在一个给定的非空字符串后面追机可能少的字 符,使其成为个回串(正序与逆序完全相司),
如的追加 a 变为回串 a,如给定字符串已经是回文串,则不需 要再追加。请在代码中添加要注释。 找到以最后一个字符串结尾的最长回文子串
评分点
1可以将原始字符串转化为非最短回文串的给 5 分(比如不经判断直接在后面迫加 n-1 个字符的,n 为给定字符串的长度)
2正确实现最短回文串的的给满分(如逐步确定包含未尾字符的最长回文子串,对复杂 度不做特别要求
3 代码风格糟糕可适当减分(最多可减 5 分)
36. Session、Cookie、cache 的区别?
Session 是单用户的会话状态。当用户访问网站时,产生一个 sessionid。并存在于 cookies中。每次向服务器请求时,发送这个 cookies,再从服务器中检索是否有这 个 sessionid 保存的数据; Cookie 同session 一样是保存你个人信息的,不过是保存在客户端,也就是你使用的 电脑上,并且不会被丢掉,除非你删除浏览器 Cookie;cache 则是服务器端的缓存,是所有用户都可以访问和共享的,因为从 Cache 中读数据比较快,所以有些系统(网站)会把一些经常被使用的数据放到 Cache 里,提高访 问速度,优化系统性能。
37. 在 linux 上,创建 socket 成功时会得到一个?
fd
38. 请简述 TCP 和 UDP 的区别?
TCP 是传输控制协议,提供的是面向连接、可靠的字节流服务。通信双方彼此交换数据前,必须先通过三次握手协议建立连接,之后才能传输数据。TCP 提供超时重传,丟弃重复数据,检验数据,流量控制等功能,保证数据能从一端传到另端。∪DP 是用户数据报 协议,是—个简单的面向无连接的协议。UDP不提供可靠的服务。在数据数据前不用建立连接故而传输速度很快。∪DP 主要用户流媒体传输,IP 电话等对数据可靠性要求不 是很高的场合
39. 如果有几千个 session,怎么提高效率?
把 session 放到 redis 或 memcache 等此类内存缓存中或着把 session 存储在 SSD 硬 盘上。
session 对应的文件有一个特点就是小,一般在几 KB 左右, 如果 session 以文件方式存储,如果并发数量级有几千个,此时系统硬盘的随机 IO 早已成了系统中的最大瓶颈了,因为会话文件 是存储在多个小文件中,映射到存储空间不是一段连续的地址范围所以硬盘的随机读取能力显得非常重要,而觉机械硬盘的随机 IO 一般在 100/iops 上 下, (IOPS (Input/Output Operations Per Second),即每秒进行读写(I/O)操作的次 数) SSD 固态硬盘可以达几百至上千,所以在这么高并发读写的情况下如果无条件用 SSD 固 态盘 可以把 session 放到 redis 或 memcache 等内存缓存中,系统对内存的操作又是非常 快的, 只要你的内存足够大,再多 session 并发速度一样不会慢。s
40. session 是存储在什么地方,以什么形式存储的?
session 变量保存在网叶服务器中,你不能直接修改。当然,调用程序中的 setAttribute()方法当然可以了。cookie存储的可不是具体的数据,要不岂不是太不 安全了,谁都可以修改 session 变量了,网站也毫无安全性可言。实际,在 cookie中,存储的是一个 sessionId,它标示了一个服务器中的 session 变量,通过这种方 式,服务器就知道你到底是那个 session了。顺便说一句,如果客户端不支持 cookie,session 也是可以实现的,在服务器端通过 urlEncoder,可以实现sessionId 的传递。所以,记住客户端只存储 session 标识,实际内容在网页服务器中。
41. 用预处理指令#define 声明一个常数,用以表明 1 年中有 多少秒(忽略闰年问题)
#define SECONDS_PER_YEAR (60 * 60 * 24 * 365)UL 我在这想看到几件事情:
1). #define 语法的基本知识(例如:不能以分号结束,括号的使用,等等)
2). 懂得预处理器将为你计算常数表达式的值,因此,直接写出你是如何计算一年中 有多少秒而不是计算出实际的值,是更清晰而没有代价的。
3). 意识到这个表达式将使一个 16 位机的整型数溢出-因此要用到长整型符号 L,告诉 编译器这个常数是的长整型数。
4).如果你在你的表达式中用到 UL(表示无符号长整型),那么你有了一个好的起点。 记住,第一印象很重要。
42. 有 A、B、C、D 四个人,要在夜里过一座桥。他们通过这 座桥分别需要耗时 1、2、5、10 分钟,只有一支手电,并且 同时最多只能两个人一起过桥。请问如何安排,能够在 17 分 钟内这四个人都过桥?
A & B -->2 mins
1 mins <-- A
C & D -->10 mins
2 mins <-- B
A & B --> 2mins
一共 2 + 1 + 10 + 2 + 2 = 17 mins
43. 1-20 的两个数把和告诉 A,积告诉 B,A 说不知道是多少, B 也说不知道,这时 A 说我知道了,B 接着说我也知道了, 问这两个数是多少?
2 和 3
44. 从 300 万字符串中找到最热门的 10 条搜索的输入信息 是一个字符串,统计 300 万输入信息中的最热门的前 10 条, 我们每次输入的一个字符串为不超过 255byte,内存使用只 有 1G。请描述思想,写出算法(c 语言),空间和时间复杂 度。
300 万个字符串最多(假设没有重复,都是最大长度)占用内存 3M*1K/4=0.75G。所以 可以将所有字符串都存放在内存中进行处理。可以使用 key 为字符串(事实上是字符串的 hash 值),值为字符串出现次数的 hash 来统计每个每个字符串出现的次数。并用一个长度为10 的数组/ 链表来存储目前出现 次数最多的 10 个字符串。 这样空间和时间的复杂度都是 O(n)。
45. 如何找出字典中的兄弟单词。给定一个单词 a,如果通过 交换单词中字母的顺序可以得到另外的单词 b,那么定义 b 是 a 的兄弟单词。现在给定一个字典,用户输入一个单词, 如何根据字典找出这个单词有多少个兄弟单词?
使用 hash_map 和链表。 首先定义一个 key,使得兄弟单词有相同的 key,不是兄弟的单词有不同的 key。例如,将单词按字母从小到大重新排序后作为其 key,比如 bad 的 key 为 abd,good 的 key 为 dgoo。使用链表将所有兄弟单词串在一起,hash_map 的 key 为单词的 key,value 为链表的 起始地址。开始时,先遍历字典,将每个单词都按照 key 加入到对应的链表当中。当需要找兄弟 单词时,只需求取这个单词的 key,然后到hash_map 中找到对应 的链表即可。 这样创建 hash_map 时时间复杂度为 O(n),查找兄弟单词时时间复杂度是 O(1)。
46. 找出数组中出现次数超过一半的数,现在有一个数组, 已知一个数出现的次数超过了一半,请用 O(n)的复杂度的算法找出这个数
答案 1:
创建一个 hash_map,key 为数组中的数,value 为此数出现的次数。遍历一遍数组, 用 hash_map统计每个数出现的次数,并用两个值存储目前出现次 数最多的数和对应出 现的次数。 这样可以做到 O(n)的时间复杂度和O(n)的空间复杂度,满足题目的要求。 但是没有利用“一个数出现的次数超过了一半”这个特点。也许算法还有提高的空 间。
答案 2:
使用两个变量 A 和 B,其中 A 存储某个数组中的数,B 用来计数。开始时将 B 初始化为 0。 遍历数组,如果 B=0,则令 A等于当前数,令 B 等于 1;如果当前数与 A 相同,则 B=B+1;如果当前数与 A 不同,则令 B=B-1。遍历结束时,A 中的数就是要找的数。 这个算法的时间复杂度是 O(n),空间复杂度为 O(1)。
47. 找出被修改过的数字 n 个空间(其中 n<1M),存放 a 到 a+n-1 的数,位置随机且数字不重复,a 为正且未知。现在第 一个空间的数被误设置为-1。已经知道被修改的数不是最小 的。请找出被修改的数字是多少。
例如:n=6,a=2,原始的串为 5,3,7,6,2,4。现在被别人修改为-1,3,7,6,2,4。现在 希望找到 5。由于修改的数不是最小的,所以遍历第二个空间到最后一个空间可以得到 a 的值。 a 到 a+n-1 这 n 个数的和是total=na+(n-1)n/2。 将第二个至最后一个空间的数累加获得 sub_total。 那么被修改的数就是total-sub_total。
48. 设计 DNS 服务器中 cache 的数据结构。
要求设计一个 DNS 的 Cache 结构,要求能够满足每秒 5000 以上的查询,满足 IP 数据的快速插入,查询的速度要快。(题目还给出了一系列的数据,比如:站点数总共为 5000 万,IP 地址有 1000 万,等等) DNS 服务器实现域名到 IP 地址的转换。 每个域名的平均长度为 25 个字节(估计值),每个 IP 为 4 个字节,所以 Cache 的每个条目需要大概 30 个字节。 总共 50M 个条目,所以需要 1.5G 个字节的空间。可以放置在内存中。(考虑到每秒 5000次操作的限制,也只能放在内存中。) 可以考虑的数据结构包括 hash_map,字典树,红黑树等等。
49. 序 列 seq=[a,b, … z,aa,ab … az,ba,bb, … bz, … ,za,zb, … zz,aaa,…]类似与 excel 的排列,任意给出一个字符串 s=[az]+(由 a-z 字符组成的任意长度字符串),请问 s 是序列 seq 的第几个。
注意到每满 26 个就会向前进一位,类似一个 26 进制的问题。
比如 ab,则位置为 26x1+2;
比如 za,则位置为 26x26+1;
比如 abc,则位置为 26x26x1+26x2+3;
50. 找出第 k 大的数字所在的位置。写一段程序,找出数组 中第 k 大小的数,输出数所在的位置。例如{2,4,3,4,7} 中,第一大的数是 7,位置在 4。第二大、第三大的数都是 4, 位置在 1、3 随便输出哪一个均可。
先找到第 k 大的数字,然后再遍历一遍数组找到它的位置。所以题目的难点在于如何 最高效的找到第 k 大的数。
我们可以通过快速排序,堆排序等高效的排序算法对数组进行排序,然后找到第 k 大 的数字。这样总体复杂度为 O(NlogN)。
我们还可以通过二分的思想,找到第 k 大的数字,而不必对整个数组排序。从数组中 随机选一个数 t,通过让这个数和其它数比较,我们可以将整个数组分成了两部分并且 满足,{x,xx,…,t}<{y,yy,…}。
在将数组分成两个数组的过程中,我们还可以记录每个子数组的大小。这样我们就可 以确定第 k 大的数字在哪个子数组中。
然后我们继续对包含第 k大数字的子数组进行同样的划分,直到找到第 k 大的数字为 止。
平均来说,由于每次划分都会使子数组缩小到原来 1/2,所以整个过程的复杂度为O(N)。