程序员面试题汇总三
面试题汇总三
1 B树B+树的区别
B 树
1什么是B树
B 树是为了磁盘或其它存储设备而设计的一种多叉(下面你会看到,相对于二叉,B树每个内结点有多个分支,即多叉)平衡查找树。B树与红黑树最大的不同在于,B树的结点可以有许多子女,从几个到几千个。B树可以在O(logn)
时间内,实现各种如插入(insert),删除(delete)等动态集合操作。
如下图所示,即是一棵B树,一棵关键字为英语中辅音字母的B树,现在要从树种查找字母R(包含n[x]
个关键字的内结点x, x有n[x]+1]个子女(也就是说,一个内结点x若含有n[x]个关键字,那么x将含有n[x]+1个子女)。所有的叶结点都处于相同的深度,带阴影的结点为查找字母R时要检查的结点):
B 树又叫平衡多路查找树。一棵m阶的B 树的特性如下:
1、 树中每个结点最多含有m个孩子(m>=2);
2、 除根结点和叶子结点外,其它每个结点至少有[ceil(m / 2)]个孩子(其中ceil(x)是一个取上限的函数);
3、 若根结点不是叶子结点,则至少有2个孩子(特殊情况:没有孩子的根结点,即根结点为叶子结点,整棵树只有一个根节点);
4、 所有叶子结点都出现在同一层,叶子结点不包含任何关键字信息
5、 每个非终端结点中包含有n个关键字信息: (n,P0,K1,P1,K2,P2,…,Kn,Pn)。其中:
a) Ki (i=1...n)
为关键字,且关键字按顺序升序排序K(i-1)< Ki。
b) Pi
为指向子树根的接点,且指针P(i-1)
指向子树种所有结点的关键字均小于Ki,但都大于K(i-1)
。
c) 关键字的个数n必须满足: [ceil(m / 2)-1]<= n <= m-1。
如下图所示:
2 B树的类型和节点定义
B树的类型和节点定义如下图所示:
3 B树的高度
B树某一非叶子节点包含N个关键字,则此非叶子节点含有N+1个孩子结点,而所有的叶子结点都在第I层,我们可以得出:
- 因为根至少有两个孩子,因此第2层至少有两个结点。
- 除根和叶子外,其它结点至少有
┌m/2┐
个孩子 - 因此在第3层至少有
2*┌m/2┐
个结点 - 在第4层至少有
2*(┌m/2┐^2)
个结点 - 在第 I 层至少有
2*(┌m/2┐^(l-2) )
个结点,于是有:N+1 ≥ 2*┌m/2┐I-2
- 考虑第L层的结点个数为N+1,那么
2*(┌m/2┐^(l-2))≤N+1
,也就是L层的最少结点数刚好达到N+1个,即:I≤ log┌m/2┐((N+1)/2 )+2;
- 当B树包含N个关键字时,B树的最大高度为l-1(因为计算B树高度时,叶结点所在层不计算在内),即:
l - 1 = log┌m/2┐((N+1)/2 )+1
。
4 B树的插入、删除操作
下面咱们以一棵5阶(即树中任一结点至多含有4个关键字,5棵子树)B树实例进行讲解(如下图所示):
备注:关键字数(2-4个)针对–非根结点(包括叶子结点在内),孩子数(3-5个)–针对根结点和叶子结点之外的内结点。当然,根结点是必须至少有2个孩子的,不然就成直线型搜索树了。
插入(insert)操作:
插入一个元素时,首先在B树中是否存在,如果不存在,即在叶子结点处结束,然后在叶子结点中插入该新的元素,注意:如果叶子结点空间足够,这里需要向右移动该叶子结点中大于新插入关键字的元素,如果空间满了以致没有足够的空间去添加新的元素,则将该结点进行“分裂”,将一半数量的关键字元素分裂到新的其相邻右结点中,中间关键字元素上移到父结点中(当然,如果父结点空间满了,也同样需要“分裂”操作),而且当结点中关键元素向右移动了,相关的指针也需要向右移。如果在根结点插入新元素,空间满了,则进行分裂操作,这样原来的根结点中的中间关键字元素向上移动到新的根结点中,因此导致树的高度增加一层。如下图所示:
1、OK,下面咱们通过一个实例来逐步讲解下。插入以下字符字母到一棵空的B 树中(非根结点关键字数小了(小于2个)就合并,大了(超过4个)就分裂):C N G A H E K Q M F W L T Z D P R X Y S,首先,结点空间足够,4个字母插入相同的结点中,如下图:
2、当咱们试着插入H时,结点发现空间不够,以致将其分裂成2个结点,移动中间元素G上移到新的根结点中,在实现过程中,咱们把A和C留在当前结点中,而H和N放置新的其右邻居结点中。如下图:
3、当咱们插入E,K,Q时,不需要任何分裂操作
4、插入M需要一次分裂,注意M恰好是中间关键字元素,以致向上移到父节点中
5、插入F,W,L,T不需要任何分裂操作
6、插入Z时,最右的叶子结点空间满了,需要进行分裂操作,中间元素T上移到父节点中,注意通过上移中间元素,树最终还是保持平衡,分裂结果的结点存在2个关键字元素。
7、插入D时,导致最左边的叶子结点被分裂,D恰好也是中间元素,上移到父节点中,然后字母P,R,X,Y陆续插入不需要任何分裂操作(别忘了,树中至多5个孩子)。
8、最后,当插入S时,含有N,P,Q,R的结点需要分裂,把中间元素Q上移到父节点中,但是情况来了,父节点中空间已经满了,所以也要进行分裂,将父节点中的中间元素M上移到新形成的根结点中,注意以前在父节点中的第三个指针在修改后包括D和G节点中。这样具体插入操作的完成,下面介绍删除操作,删除操作相对于插入操作要考虑的情况多点。
删除(delete)操作:
首先查找B树中需删除的元素,如果该元素在B树中存在,则将该元素在其结点中进行删除,如果删除该元素后,首先判断该元素是否有左右孩子结点,如果有,则上移孩子结点中的某相近元素(“左孩子最右边的节点”或“右孩子最左边的节点”)到父节点中,然后是移动之后的情况;如果没有,直接删除后,移动之后的情况。
以上述插入操作构造的一棵5阶B树(树中最多含有m(m=5)个孩子,因此关键字数最小为ceil(m / 2)-1=2。还是这句话,关键字数小了(小于2个)就合并,大了(超过4个)就分裂)为例
,依次删除H,T,R,E。
1、首先删除元素H,当然首先查找H,H在一个叶子结点中,且该叶子结点元素数目3大于最小元素数目ceil(m/2)-1=2,则操作很简单,咱们只需要移动K至原来H的位置,移动L至K的位置(也就是结点中删除元素后面的元素向前移动)
2、下一步,删除T,因为T没有在叶子结点中,而是在中间结点中找到,咱们发现他的继承者W(字母升序的下个元素),将W上移到T的位置,然后将原包含W的孩子结点中的W进行删除,这里恰好删除W后,该孩子结点中元素个数大于2,无需进行合并操作。
3、下一步删除R,R在叶子结点中,但是该结点中元素数目为2,删除导致只有1个元素,已经小于最小元素数目ceil(5/2)-1=2,而由前面我们已经知道:如果其某个相邻兄弟结点中比较丰满(元素个数大于ceil(5/2)-1=2),则可以向父结点借一个元素,然后将最丰满的相邻兄弟结点中上移最后或最前一个元素到父节点中(有没有看到红黑树中左旋操作的影子?),在这个实例中,右相邻兄弟结点中比较丰满(3个元素大于2),所以先向父节点借一个元素W下移到该叶子结点中,代替原来S的位置,S前移;然后X在相邻右兄弟结点中上移到父结点中,最后在相邻右兄弟结点中删除X,后面元素前移
4、最后一步删除E, 删除后会导致很多问题,因为E所在的结点数目刚好达标,刚好满足最小元素个数(ceil(5/2)-1=2),而相邻的兄弟结点也是同样的情况,删除一个元素都不能满足条件,所以需要该节点与某相邻兄弟结点进行合并操作;首先移动父结点中的元素(该元素在两个需要合并的两个结点元素之间)下移到其子结点中,然后将这两个结点进行合并成一个结点。所以在该实例中,咱们首先将父节点中的元素D下移到已经删除E而只有F的结点中,然后将含有D和F的结点和含有A,C的相邻兄弟结点进行合并成一个结点。
5、也许你认为这样删除操作已经结束了,其实不然,在看看上图,对于这种特殊情况,你立即会发现父节点只包含一个元素G,没达标(因为非根节点包括叶子结点的关键字数n必须满足于2=<n<=4,而此处的n=1),这是不能够接受的。如果这个问题结点的相邻兄弟比较丰满,则可以向父结点借一个元素。假设这时右兄弟结点(含有Q,X)有一个以上的元素(Q右边还有元素),然后咱们将M下移到元素很少的子结点中,将Q上移到M的位置,这时,Q的左子树将变成M的右子树,也就是含有N,P结点被依附在M的右指针上。所以在这个实例中,咱们没有办法去借一个元素,只能与兄弟结点进行合并成一个结点,而根结点中的唯一元素M下移到子结点,这样,树的高度减少一层。
B+树
B+树的定义
B+树是B树的一种变形,它更适合实际应用中操作系统的文件索引和数据库索引。
-
除根节点外的内部节点,每个节点最多有
m
个关键字,最少有ceil[m/2]
个关键字。 -
根节点要么没有子树,要么至少有2棵子树;
-
所有的叶子节点包含了全部的关键字以及这些关键字指向文件的指针,并且:
3.1所有叶子节点中的关键字按大小顺序排列
3.2 相邻的叶子节点顺序链接(相当于是构成了一个顺序链表)
3.3 所有叶子节点在同一层 -
所有分支节点的关键字都是对应子树中关键字的最大值
下图就是一个非常典型的B+树的例子。
B+树和B树相比,主要的不同点在以下3项
- 内部节点中,关键字的个数与其子树的个数相同,不像B树种,子树的个数总比关键字个数多1个
- 所有指向文件的关键字及其指针都在叶子节点中,不像B树,有的指向文件的关键字是在内部节点中。换句话说,B+树中,内部节点仅仅起到索引的作用,
- 在搜索过程中,如果查询和内部节点的关键字一致,那么搜索过程不停止,而是继续向下搜索这个分支。
B+树相比于B树,在文件系统,数据库系统当中,更有优势,原因如下:
-
B+树的磁盘读写代价更低
B+树的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对B树更小。如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多。相对来说I/O读写次数也就降低了。 -
B+树的查询效率更加稳定
由于内部结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。 -
B+树更有利于对数据库的扫描
B树在提高了磁盘IO性能的同时并没有解决元素遍历的效率低下的问题,而B+树只需要遍历叶子节点就可以解决对全部关键字信息的扫描,所以对于数据库中频繁使用的range query,B+树有着更高的性能。
1* 红黑树
红黑树,Red-Black Tree 「RBT」是一个自平衡(不是绝对的平衡)的二叉查找树(BST),树上的每个节点都遵循下面的规则:
- 每个节点都有红色或黑色
- 树的根始终是黑色的 (黑土地孕育黑树根)
- 没有两个相邻的红色节点(红色节点不能有红色父节点或红色子节点,并没有说不能出现连续的黑色节点)
- 从节点(包括根)到其任何后代NULL节点(叶子结点下方挂的两个空节点,并且认为他们是黑色的)的每条路径都具有相同数量的黑色节点(黑高相等)
红黑树有两大操作:
- 变色
- 旋转
假设我们插入的新节点为 X:
1、将新插入的节点标记为红色
2、如果 X 是根结点(root),则标记为黑色
3、如果 X 的 parent 不是黑色,同时 X 也不是 root:
3.1 如果 X 的 uncle (叔叔) 是红色
3.1.1 将 parent 和 uncle 标记为黑色
3.1.2 将 grand parent (祖父) 标记为红色
3.1.3 让 X 节点的颜色与 X 祖父的颜色相同,然后重复步骤 2、3
2 DNS介绍一下
DNS 是一个分布式数据库,提供了主机名和 IP 地址之间相互转换的服务。这里的分布式数据库是指,每个站点只保留它自己的那部分数据。
域名具有层次结构,从上到下依次为:根域名、顶级域名、二级域名。
DNS 可以使用 UDP 或者 TCP 进行传输,使用的端口号都为 53。大多数情况下 DNS 使用 UDP 进行传输,这就要求域名解析器和域名服务器都必须自己处理超时和重传从而保证可靠性。在两种情况下会使用 TCP 进行传输:
如果返回的响应超过的 512 字节(UDP 最大只支持 512 字节的数据)。
区域传送(区域传送是主域名服务器向辅助域名服务器传送变化的那部分数据)。
3 https和http的区别,https的原理
一、Http和Https的基本概念
超文本传输协议HTTP协议被用于在web服务器和网站服务器之间传递消息,HTTP协议以明文方式发送内容,不提供任何方式的数据加密.如果攻击者截取了web浏览器和网站服务器之间的传输报文,就可以直接读懂其中的信息,因此,HTTP协议不适合传输一些敏感信息,比如:信用卡号,密码等。
HTTP 有以下安全性问题:
1、使用明文进行通信,内容可能会被窃听;
2、不验证通信方的身份,通信方的身份有可能遭遇伪装;
3、无法证明报文的完整性,报文有可能遭篡改。
HTTPS 并不是新协议,而是让 HTTP 先和 SSL(Secure Sockets Layer)通信,再由 SSL 和 TCP 通信,也就是说 HTTPS 使用了隧道进行通信。
HTTPS:是以安全为目标的HTTP通道,简单讲是HTTP的安全版,即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL。
二、Http与Https的区别
1、https协议需要到CA (Certificate Authority,证书颁发机构)申请证书,一般免费证书较少,因而需要一定费用。(原来网易官网是http,而网易邮箱是https。)
2、http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。
3、http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
4、http的连接很简单,是无状态的。Https协议是由SSL+Http协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。
三 、HTTPS的工作原理:
客户端在使用HTTPS方式与Web服务器通信时有以下几个步骤,如图所示。
(1)客户使用https的URL访问Web服务器,要求与Web服务器建立SSL连接。
(2)Web服务器收到客户端请求后,会将网站的证书信息(证书中包含公钥)传送一份给客户端。
(3)客户端的浏览器与Web服务器开始协商SSL连接的安全等级,也就是信息加密的等级。
(4)客户端的浏览器根据双方同意的安全等级,建立会话密钥,然后利用网站的公钥将会话密钥加密,并传送给网站。
(5)Web服务器利用自己的私钥解密出会话密钥。
(6)Web服务器利用会话密钥加密与客户端之间的通信。
HTTPS的缺点
1)HTTPS协议握手阶段比较费时,会使页面的加载时间延长近50%,增加10%到20%的耗电;
2)HTTPS连接缓存不如HTTP高效,会增加数据开销和功耗,甚至已有的安全措施也会因此而受到影响;
3)SSL证书需要钱,功能越强大的证书费用越高,个人网站、小网站没有必要一般不会用
4)SSL证书通常需要绑定IP,不能在同一IP上绑定多个域名,IPv4资源不可能支撑这个消耗
5)HTTPS协议的加密范围也比较有限,在黑客攻击、拒绝服务攻击、服务器劫持等方面几乎起不到什么作用。最关键的,SSL证书的信用链体系并不安全,特别是在某些国家可以控制CA根证书的情况下,中间人攻击一样可行。
4 HTTP 请求响应过程
整个流程:
域名解析 —> 与服务器建立连接 —> 发起HTTP请求 —> 服务器响应HTTP请求,浏览器得到html代码 —> 浏览器解析html代码,并请求html代码中的资源(如js、css、图片) —> 浏览器对页面进行渲染呈现给用户
1. 域名解析
先查缓存是否有域名对应的IP,如果没有就使用DNS进行查询。
2. 与服务器建立连接
TCP连接的建立,三次握手。
3. 发起HTTP请求
4. 服务器响应HTTP请求,浏览器得到html代码
5. 浏览器解析html代码,并请求html代码中的资源
6. 浏览器对页面进行渲染呈现给用户
5 面向对象,封装,继承,多态,介绍一下,这三者的意义分别是什么,这样做优势在哪?
1 、如何理解面向对象
面向对象可以说是一种对现实是事物的抽象,将一类事物抽象成一个类,类里面包含了这类事物具有的公共部分,以及我们对这些部分的操作,也就是对应的数据和过程。
2 、封装
利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体。数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外的接口使其与外部发生联系。用户无需关心对象内部的细节,但可以通过对象对外提供的接口来访问该对象。
优点:
1、减少耦合:可以独立地开发、测试、优化、使用、理解和修改
2、减轻维护的负担:可以更容易被理解,并且在调试的时候可以不影响其他模块
3、有效地调节性能:可以通过剖析来确定哪些模块影响了系统的性能
4、提高软件的可重用性
5、降低了构建大型系统的风险:即使整个系统不可用,但是这些独立的模块却有可能是可用的
3、 继承
继承可以说是一种代码复用的手段,我们在一个现有类上想扩展出一些东西的时候,不需要再次重复编写上面的代码,而是采用一种继承的思想。在派生出的子类里添加一些我们想要的数据或方法,也可以理解为从一般到特殊的过程。
4、多态
多态简单理解为就是同一函数,在基类和派生类中表现出不同的效果。
多态分为编译时多态和运行时多态:
1、编译时多态主要指方法的重载
2、运行时多态指程序中定义的对象引用所指向的具体类型在运行期间才确定
这里先说运行时多态,其实是指在继承体系中父类的一个接口(必须为虚函数),在子类中有多种不同的实现,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。即通过父类指针或者引用可以访问到子类的接口(虚函数),看上去就像是一个相同的动作,会出现多种不同的结果。
就可以理解为,调用接口时,与类型无关(用父类的指针或者引用),与对象有关(父类指针或引用指向不同的对象,而调用到不同的接口)
多态是如何实现,主要还是虚表,只要类里面有虚函数,就会在静态区开辟一块空间来保存虚函数(属于整个类域),每个对象里面都有一个虚表指针,虚表是在编译的时候进行初始化的,虚表指针是在初始化列表中初始化。我们知道用父类的指针可以指向子类对象(发生切片行为),但是虚表是不变的,访问的虚函数就是子类的虚函数了。
6 重载和重写的区别
一、方法重写
在Java中覆盖继承父类的方法就是通过方法的重写来实现的。所谓方法的重写是指子类中的方法与父类中继承的方法有完全相同的返回值类型、方法名、参数个数以及参数类型。
二、方法重载
方法重载是指在一个类中,多个方法的方法名相同,但是参数列表不同。参数列表不同指的是参数个数、参数类型或者参数的顺序不同。
7 、STL中map和unordered_map 区别、
1、需要引入的头文件不同
map: #include < map >
unordered_map: #include < unordered_map >
2、内部实现机理不同
map: map内部实现了一个红黑树(红黑树是非严格平衡二叉搜索树,而AVL是严格平衡二叉搜索树),红黑树具有自动排序的功能,因此map内部的所有元素都是有序的,红黑树的每一个节点都代表着map的一个元素。因此,对于map进行的查找,删除,添加等一系列的操作都相当于是对红黑树进行的操作。map中的元素是按照二叉搜索树(又名二叉查找树、二叉排序树,特点就是左子树上所有节点的键值都小于根节点的键值,右子树所有节点的键值都大于根节点的键值)存储的,使用中序遍历可将键值按照从小到大遍历出来。
unordered_map: unordered_map内部实现了一个哈希表(也叫散列表,通过把关键码值映射到Hash表中一个位置来访问记录,查找的时间复杂度可达到O(1),其在海量数据处理中有着广泛应用)。因此,其元素的排列顺序是无序的。
3、优缺点以及适用处
map:
1、优点:
1)有序性,这是map结构最大的优点,其元素的有序性在很多应用中都会简化很多的操作
2)红黑树,内部实现一个红黑书使得map的很多操作在lgn的时间复杂度下就可以实现,因此效率非常的高
2、缺点: 空间占用率高,因为map内部实现了红黑树,虽然提高了运行效率,但是因为每一个节点都需要额外保存父节点、孩子节点和红/黑性质,使得每一个节点都占用大量的空间
3、适用处:对于那些有顺序要求的问题,用map会更高效一些
unordered_map:
1、优点: 因为内部实现了哈希表,因此其查找速度非常的快
2、缺点: 哈希表的建立比较耗费时间
3、适用处: 对于查找问题,unordered_map会更加高效一些,因此遇到查找问题,常会考虑一下用unordered_map
8 哈希表介绍一下,冲突解决
哈希表,也称散列表,从根本上来说,一个哈希表包含一个数组,通过特殊的关键码(也就是key)来访问数组中的元素。哈希表的主要思想是通过一个哈希函数, 把关键码映射的位置去寻找存放值的地方 ,读取的时候也是直接通过关键码来找到位置并存进去。
1、常见的哈希算法
1) 直接定址法
取关键字或关键字的某个线性函数值为散列地址。
即 f(key) = key 或 f(key) = a*key + b,其中a和b为常数。
2) 除留余数法 最常用
取关键字被某个不大于散列表长度 m 的数 p 求余,得到的作为散列地址。
即 f(key) = key % p, p < m。这是最为常见的一种哈希算法。
3) 数字分析法
当关键字的位数大于地址的位数,对关键字的各位分布进行分析,选出分布均匀的任意几位作为散列地址。
仅适用于所有关键字都已知的情况下,根据实际应用确定要选取的部分,尽量避免发生冲突。
4) 平方取中法
先计算出关键字值的平方,然后取平方值中间几位作为散列地址。
随机分布的关键字,得到的散列地址也是随机分布的。
5) 随机数法
选择一个随机函数,把关键字的随机函数值作为它的哈希值。
通常当关键字的长度不等时用这种方法。
2、哈希冲突
一、开放地址法
开发地址法的做法是,当冲突发生时,使用某种探测算法在散列表中寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到。按照探测序列的方法,一般将开放地址法区分为线性探查法、二次探查法、双重散列法等。
二、链地址法。
9 C++ 左值与右值
一、 左值、右值
在C++11中可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值(将亡值或纯右值)。举个例子,int a = b+c
, a
就是左值,其有变量名为a
,通过&a
可以获取该变量的地址;表达式b+c
、函数int func()
的返回值是右值,在其被赋值给某一变量前,我们不能通过变量名找到它,&(b+c)
这样的操作则不会通过编译。
二、 右值、将亡值
C++98中右值是纯右值,纯右值指的是临时变量值、不跟对象关联的字面量值。临时变量指的是非引用返回的函数返回值、表达式等,例如函数int func()
的返回值,表达式a+b
;不跟对象关联的字面量值,例如true,2,”C”等。
将亡值则是C++11新增的跟右值引用相关的表达式,这样表达式通常是将要被移动的对象(移为他用),比如返回右值引用T&&
的函数返回值、std::move
的返回值,或者转换为T&&
的类型转换函数的返回值。
将亡值可以理解为通过“盗取”其他变量内存空间的方式获取到的值。在确保其他变量不再被使用、或即将被销毁时,通过“盗取”的方式可以避免内存空间的释放和分配,能够延长变量值的生命期。
三 、左值引用、右值引用
左值引用就是对一个左值进行引用的类型。右值引用就是对一个右值进行引用的类型,事实上,由于右值通常不具有名字,我们也只能通过引用的方式找到它的存在。
右值引用和左值引用都是属于引用类型。无论是声明一个左值引用还是右值引用,都必须立即进行初始化。而其原因可以理解为是引用类型本身自己并不拥有所绑定对象的内存,只是该对象的一个别名。左值引用是具名变量值的别名,而右值引用则是不具名(匿名)变量的别名。
1 左值引用
左值引用的基本语法:type &引用名 = 左值表达式
;
2 右值引用
右值引用的基本语法type &&引用名 = 右值表达式
;
右值引用在企业开发人员在代码优化方面会经常用到。
右值引用的“&&”中间不可以有空格。
#include <iostream>
using namespace std;
int main()
{
cout << "-------引用左值--------" << endl;
int a = 5;
int &add_a(a);
cout << " a =" << a <<" "<<" &a = "<<&a<< endl;
cout << "add_a =" << add_a<<" "<< "&add_a = " << &add_a << endl;
cout << "-----------------------" << endl;
cout << "-------引用右值--------" << endl;
int b = 10;
int &&add_b(b + 1);
cout << " b =" << b << " " << " &b = " << &b << endl;
cout << "add_b =" << add_b << " " << "&add_b = " << &add_b << endl;
add_b++;
cout << "add_b++ =" << add_b << " " << "&add_b++ = " << &add_b << endl;
cout << "-----------------------" << endl;
system("pause");
return 0;
}
10 拷贝构造函数使用场景有哪些?
构造函数是一个初始化类对象的函数,即使不显示调用,编译器也会隐式调用构造函数初始化类对象。同样的,拷贝构造函数是一种特殊的构造函数,目的也是初始化类对象,同样在不声明的情况下也会隐式调用该函数。而隐式调用拷贝构造函数的时候,我们称之为“浅拷贝”。但是,请注意一点,并不是说显示调用就是“深拷贝”,而是如果要深拷贝一定要显示调用。
#include <iostream>
using namespace std;
class A{
private:
int a;
public:
A(int b):a(b){
cout<<"构造函数"<<endl;
}
A(const A& c){
a = c.a;
cout<<"拷贝构造函数"<<endl;
}
~A(){
cout<<"析构函数"<<endl;
}
};
int main(){
A a(100); //调用构造函数
A b = a; //调用拷贝构造函数
return 0;
}
当创建了一个对象a
时,编译器调用构造函数将a初始化为100
。当又创建了一个对象b,
将a赋值于b,此时调用拷贝构造函数将b初始化。最后由于调用了一次构造函数一次拷贝构造函数,所以析构函数被调用两次。
11 HashMap的原理
一、HashMap的数据结构
数组的特点是:寻址容易,插入和删除困难;而链表的特点是:寻址困难,插入和删除容易。那么我们能不能综合两者的特性,做出一种寻址容易,插入删除也容易的数据结构?答案是肯定的,这就是我们要提起的哈希表,哈希表有多种不同的实现方法,我接下来解释的是最常用的一种方法—— 拉链法,我们可以理解为“链表的数组”,如图:
从上图我们可以发现哈希表是由数组+链表组成的,一个长度为16的数组中,每个元素存储的是一个链表的头结点。那么这些元素是按照什么样的规则存储到数组中呢。一般情况是通过hash(key)%len
获得,也就是元素的key的哈希值对数组长度取模得到。
12 锁
13 了解哪些设计模式
一、单例模式
Intent:
确保一个类只有一个实例,并提供该实例的全局访问点。
Class Diagram:
使用一个私有构造函数、一个私有静态变量以及一个公有静态函数来实现。
私有构造函数保证了不能通过构造函数来创建对象实例,只能通过公有静态函数返回唯一的私有静态变量。
Implementation:
Ⅰ 懒汉式-线程不安全
以下实现中,私有静态变量 uniqueInstance 被延迟实例化,这样做的好处是,如果没有用到该类,那么就不会实例化 uniqueInstance,从而节约资源。
这个实现在多线程环境下是不安全的,如果多个线程能够同时进入 if (uniqueInstance == null)
,并且此时 uniqueInstance 为 null
,那么会有多个线程执行 uniqueInstance = new Singleton(); 语句,这将导致实例化多次 uniqueInstance。
public class Singleton {
private static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
Ⅱ 饿汉式-线程安全
线程不安全问题主要是由于 uniqueInstance 被实例化多次,采取直接实例化 uniqueInstance 的方式就不会产生线程不安全问题。但是直接实例化的方式也丢失了延迟实例化带来的节约资源的好处。
private static Singleton uniqueInstance = new Singleton();
Ⅲ 懒汉式-线程安全
只需要对 getUniqueInstance() 方法加锁,那么在一个时间点只能有一个线程能够进入该方法,从而避免了实例化多次 uniqueInstance。
但是当一个线程进入该方法之后,其它试图进入该方法的线程都必须等待,即使 uniqueInstance 已经被实例化了。这会让线程阻塞时间过长,因此该方法有性能问题,不推荐使用。
public static synchronized Singleton getUniqueInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
Ⅳ 双重校验锁-线程安全
uniqueInstance 只需要被实例化一次,之后就可以直接使用了。加锁操作只需要对实例化那部分的代码进行,只有当 uniqueInstance 没有被实例化时,才需要进行加锁。
双重校验锁先判断 uniqueInstance 是否已经被实例化,如果没有被实例化,那么才对实例化语句进行加锁。
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
if (uniqueInstance == null) {
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
二、简单工厂(Simple Factory)
Intent:
在创建一个对象时不向客户暴露内部细节,并提供一个创建对象的通用接口。
Class Diagram:
简单工厂把实例化的操作单独放到一个类中,这个类就成为简单工厂类,让简单工厂类来决定应该用哪个具体子类来实例化。
这样做能把客户类和具体子类的实现解耦,客户类不再需要知道有哪些子类以及应当实例化哪个子类。客户类往往有多个,如果不使用简单工厂,那么所有的客户类都要知道所有子类的细节。而且一旦子类发生改变,例如增加子类,那么所有的客户类都要进行修改。
Implementation:
public interface Product {
}
public class ConcreteProduct implements Product {
}
public class ConcreteProduct1 implements Product {
}
public class ConcreteProduct2 implements Product {
}
以下的 SimpleFactory 是简单工厂实现,它被所有需要进行实例化的客户类调用。
public class SimpleFactory {
public Product createProduct(int type) {
if (type == 1) {
return new ConcreteProduct1();
} else if (type == 2) {
return new ConcreteProduct2();
}
return new ConcreteProduct();
}
}
public class Client {
public static void main(String[] args) {
SimpleFactory simpleFactory = new SimpleFactory();
Product product = simpleFactory.createProduct(1);
// do something with the product
}
}
三 、工厂方法(Factory Method)
https://github.com/CyC2018/CS-Notes/blob/master/notes/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20%E5%B7%A5%E5%8E%82%E6%96%B9%E6%B3%95.md
四 、抽象工厂(Abstract Factory)
https://github.com/CyC2018/CS-Notes/blob/master/notes/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20%E6%8A%BD%E8%B1%A1%E5%B7%A5%E5%8E%82.md
14 C++的智能指针
1 普通指针(normal/raw/naked pointers)的问题?
如果不恰当处理指针就会带来许多问题,所以人们总是避免使用它。指针总是会扯上很多问题,例如指针所指向对象的生命周期,挂起引用(dangling references)以及内存泄露。
如果一块内存被多个指针引用,但其中的一个指针释放且其余的指针并不知道,这样的情况下,就发生了挂起引用。而内存泄露,就如你知道的一样,当从堆中申请了内存后不释放回去,这时就会发生内存泄露。
2 什么是智能指针?
智能指针是一个RAII(Resource Acquisition is initialization)类模型,用来动态的分配内存。它提供所有普通指针提供的接口,却很少发生异常。在构造中,它分配内存,当离开作用域时,它会自动释放已分配的内存。这样的话,程序员就从手动管理动态内存的繁杂任务中解放出来了。
3 auto_ptr
让我们来见识一下auto_ptr
如何解决上述问题的吧。
class Test
{
public:
Test(int a = 0 ) : m_a(a) { }
~Test( )
{
cout << "Calling destructor" << endl;
}
public: int m_a;
};
void main( )
{
std::auto_ptr<Test> p( new Test(5) );
cout << p->m_a << endl;
}
上述代码会智能地释放与指针绑定的内存。作用的过程是这样的:我们申请了一块内存来放Test对象,并且把他绑定到auto_ptr p
上。所以当p
离开作用域时,它所指向的内存块也会被自动释放。
但是,他也存在着很多问题:
问题1:auto_ptr不能指向一组对象,就是说它不能和操作符new[]一起使用。
问题2: auto_ptr不能和标准容器(vector,list,map…)一起使用。
因为auto_ptr容易产生错误,所以它也将被废弃了。C++11提供了一组新的智能指针,每一个都各有用武之地。
4 shared_ptr
第一种智能指针是shared_ptr,它有一个叫做共享所有权(sharedownership)的概念。shared_ptr
的目标非常简单:多个指针可以同时指向一个对象,当最后一个shared_ptr离开作用域时,内存才会自动释放。
shared_ptr
默认调用delete
释放关联的资源。
void main( )
{
shared_ptr<int> sptr1( new int );
}
5 Weak_Ptr
weak_ptr
拥有共享语义(sharing semantics)和不包含语义(not owning semantics)。这意味着,weak_ptr
可以共享shared_ptr
持有的资源。所以可以从一个包含资源的shared_ptr创建weak_ptr。
weak_ptr
不支持普通指针包含的*,->
操作。它并不包含资源所以也不允许程序员操作资源。
void main( )
{
shared_ptr<Test> sptr( new Test );
weak_ptr<Test> wptr( sptr );
weak_ptr<Test> wptr1 = wptr;
}
6 unique_ptr
unique_ptr
也是对auto_ptr的替换。unique_ptr
遵循着独占语义。在任何时间点,资源只能唯一地被一个unique_ptr
占有。当unique_ptr
离开作用域,所包含的资源被释放。如果资源被其它资源重写了,之前拥有的资源将被释放。所以它保证了他所关联的资源总是能被释放。
unique_ptr<int> uptr( new int );
7 使用哪一个?
完全取决于你想要如何拥有一个资源,如果需要共享资源使用shared_ptr,如果独占使用资源就使用unique_ptr.
15 页面置换算法,内存颠簸
一、最佳置换算法(OPT)
思想:
选择那些以后永不使用的,或在最长(未来)时间内不再被访问的页面作为淘汰的页面。
二、先进先出置换算法(FIFO)
思想:
总是淘汰最先进入内存的页面,即选择在内存中驻留时间最长的页面予以淘汰。
范例:
假设最小物理块数为3块。页面引用序列如下:
7,0,1,2,0,3,0,4,2,3,0,3,2,1,2,0,1,7,0,1
三、最近最久未使用置换算法(LRU)
思想:
赋予每个页面一个访问字段,用来记录相应页面自上次被访问以来所经历的时间t,当淘汰一个页面时,应选择所有页面中其t值最大的页面,即内存中最近一段时间内最长时间未被使用的页面予以淘汰。
LRU的硬件支持 — 栈:
可利用一个特殊的栈来保存当前使用的各个页面的页面号。每当进程访问某页面时,便将该页面的页面号从栈中移出,将它再压入栈顶。即栈顶始终是最新被访问页面的编号,而栈底则是最近最久未使用页面的页面号。
四、Clock置换算法(NRU)
Clock置换算法是一种LRU的近似算法。由于LRU算法需要较多的硬件支持,采用Clock算法只需相对较少的硬件支持。Clock也称之为最近未用算法NRU。
执行过程:
只需为每页设置一位访问位,再将内存中的所有页面都通过链接指针链接成一个循环队列。当某页被访问时,其访问位被置1。在选择页面淘汰时,只需检查页的访问位,如果是0,就选择该页换出;若为1,则重新将它置为0,暂时不换出,而给该页第二次驻留内存的机会,再按照FIFO算法检查下一个页面。当检查到队列中的最后一个页面时,若其访问位仍为1,则再返回到队首去检查第一个页面。由于该算法是循环地检查各页面的使用情况,故称为Clock算法。
页面抖动(颠簸)
在页面置换过程中的一种最糟糕的情形是,刚刚换出的页面马上又要换入主存,刚刚换入的页面马上就要换出主存,这种频繁的页面调度行为称为抖动,或颠簸。
16 进程间通信方式,哪种最快
管道,消息队列,信号量,共享内存,套接字 共享内存最快
17 vector、 map、set的实现原理
1 vector
vector
是动态空间,随着元素的加入,它的内部机制会自行扩充空间以容纳新元素。由于 vector维护的是一个连续线性空间,所以vector支持随机存取 。
注意: vector
动态增加大小时,并不是在原空间之后持续新空间(因为无法保证原空间之后尚有可供配置的空间),而是以原大小的两倍另外配置一块较大的空间,然后将原内容拷贝过来,然后才开始在原内容之后构造新元素,并释放原空间。因此, 对vector的任何操作,一旦引起空间重新配置,指向原vector的所有迭代器就都失效了 。这是程序员易犯的一个错误,务需小心。
2 map:
map技术原理:
图中所示是map容器的一个元素的数据组成,可通过pair
封装成一个结构对象。map
容器所要做的,就是将这个pair对象插入到红黑树,完成一个元素的添加。同时,也要提供一个仅使用键值进行比较的函数对象,将它传递给红黑树。由此,就可利用红黑树的操作,将map
元素数据插入到二叉树中的正确位置,也可以根据键值进行元素的删除和检索。
3 set:
Set
是关联容器,set
中每个元素都只包含一个关键字
,set支
持高效的关键字查询操作—检查每一个给定的关键字是否在set中,set
是以红黑树的平衡二叉检索树结构实现的,支持高效插入删除
,插如元素的时候会自动调整二叉树的结构,使得每个子树根节点键值大于左子树所有节点的键值,小于右子树所有节点的键值,另外还得保证左子树和右子树的高度相等。
常用操作:
1.元素插入:insert
2.中序遍历:类似vector遍历(用迭代器)
3.反向遍历:利用反向迭代器reverse_iterator
set s; set::reverse_iterator rit; for(rit=s.rbegin();rit!=s.rend();rit++)
4.元素的删除:s.erase(2); s.clear();
5.元素的检索:find(),若找到,返回该值迭代器的位置,否则返回最后一个元素后面一个位置s.end()
it=s.find(5); if(it==s.end()) cout<<“not find”<<endl;else cout<<*it<<endl;