2020年字节跳动秋招C++精选面试题及答案(下)

1.如何理解IO多路复用的三种机制Select,Poll,Epoll?

1.Select
首先先分析一下select函数

int select(
int maxfdp1,
fd_set *readset,
fd_set *writeset,
fd_set *exceptset,
const struct timeval *timeout
);

【参数说明】
int maxfdp1 指定待测试的文件描述字个数,它的值是待测试的最大描述字加1。
fd_set *readset , fd_set *writeset , fd_set *exceptset
fd_set可以理解为一个集合,这个集合中存放的是文件描述符(file descriptor),即 文件句柄。中间的三个参数指定我们要让内核测试读、写和异常条件的文件描述符集合。 如果对某一个的条件不感兴趣,就可以把它设为空指针。
const struct timeval *timeout timeout告知内核等待所指定文件描述符集合中的任 何一个就绪可花多少时间。其timeval结构用于指定这段时间的秒数和微秒数。
【返回值】
int 若有就绪描述符返回其数目,若超时则为0,若出错则为-1
select运行机制
select()的机制中提供一种fd_set的数据结构,实际上是一个long类型的数组,每一 个数组元素都能与一打开的文件句柄(不管是Socket句柄,还是其他文件或命名管道或 设备句柄)建立联系,建立联系的工作由程序员完成,当调用select()时,由内核根 据IO状态修改fd_set的内容,由此来通知执行了select()的进程哪一Socket或文件 可读。
从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还 多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select 以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注 册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程 内同时处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达 到这个目的。
select机制的问题
每次调用select,都需要把fd_set集合从用户态拷贝到内核态,如果fd_set集合很 大时,那这个开销也很大
同时每次调用select都需要在内核遍历传递进来的所有fd_set,如果fd_set集合很 大时,那这个开销也很大
为了减少数据拷贝带来的性能损坏,内核对被监控的fd_set集合大小做了限制,并且 这个是通过宏控制的,大小不可改变(限制为1024).

2.Poll
poll的机制与select类似,与select在本质上没有多大差别,管理多个描述符也是 进行轮询,根据描述符的状态进行处理,但是poll没有最大文件描述符数量的限制。 也就是说,poll只解决了上面的问题3,并没有解决问题1,2的性能开销问题。

下面是pll的函数原型:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
typedef struct pollfd {
int fd; // 需要被检测或选择的文件描述符
short events; // 对文件描述符fd上感兴趣的事件
short revents; // 文件描述符fd上当前实际发生的事件
} pollfd_t;
poll改变了文件描述符集合的描述方式,使用了pollfd结构而不是select的fd_set 结构,使得poll支持的文件描述符集合限制远大于select的1024
【参数说明】
struct pollfd *fds fds是一个struct pollfd类型的数组,用于存放需要检测其状 态的socket描述符,并且调用poll函数之后fds数组不会被清空;一个pollfd结构 体表示一个被监视的文件描述符,通过传递fds指示 poll() 监视多个文件描述符。其 中,结构体的events域是监视该文件描述符的事件掩码,由用户来设置这个域,结构 体的revents域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域
nfds_t nfds 记录数组fds中描述符的总数量
【返回值】
int 函数返回fds集合中就绪的读、写,或出错的描述符数量,返回0表示超时,返回 -1表示出错;

Epoll
epoll在Linux2.6内核正式提出,是基于事件驱动的I/O方式,相对于select来说, epoll没有描述符个数限制,使用一个文件描述符管理多个描述符,将用户关心的文件 描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一 次。
Linux中提供的epoll相关函数如下:

int epoll_create(int size);
int epoll_ctl(
int epfd, 
int op, int fd, 
struct epoll_event *event
);
int epoll_wait(
int epfd, 
struct epoll_event * events, 
int maxevents, 
int timeout
);

1).epoll_create 函数创建一个epoll句柄,参数size表明内核要监听的描述符数量。 调用成功时返回一个epoll句柄描述符,失败时返回-1。
2).epoll_ctl 函数注册要监听的事件类型。四个参数解释如下:
epfd 表示epoll句柄
op 表示fd操作类型,有如下3种
EPOLL_CTL_ADD 注册新的fd到epfd中
EPOLL_CTL_MOD 修改已注册的fd的监听事件
EPOLL_CTL_DEL 从epfd中删除一个fd
fd 是要监听的描述符
event 表示要监听的事件
epoll_event 结构体定义如下:

struct epoll_event {
    
    
 __uint32_t events; /* Epoll events */
 epoll_data_t data; /* User data variable */
};
typedef union epoll_data {
    
    
 void *ptr;
 int fd;
 __uint32_t u32;
 __uint64_t u64;
} epoll_data_t;

3). epoll_wait 函数等待事件的就绪,成功时返回就绪的事件数目,调用失败时返回 -1,等待超时返回 0。
⑴epfd 是epoll句柄
⑵events 表示从内核得到的就绪事件集合
⑶maxevents 告诉内核events的大小
⑷timeout 表示等待的超时事件
epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用 IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃 的情况下的系统CPU利用率。原因就是获取事件的时候,它无须遍历整个被侦听的描述 符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。
epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供 了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少 epoll_wait/epoll_pwait的调用,提高应用程序效率。
⑴水平触发(LT):默认工作模式,即当epoll_wait检测到某描述符事件就绪并 通知应用程序时,应用程序可以不立即处理该事件;下次调用epoll_wait时,会 再次通知此事件
⑵边缘触发(ET): 当epoll_wait检测到某描述符事件就绪并通知应用程序时, 应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次 通知此事件。(直到你做了某些操作导致该描述符变成未就绪状态了,也就是说边 缘触发只在状态由未就绪变为就绪时只通知一次)。
LT和ET原本应该是用于脉冲信号的,可能用它来解释更加形象。Level和Edge指的就 是触发点,Level为只要处于水平,那么就一直触发,而Edge则为上升沿和下降沿的 时候触发。比如:0->1 就是Edge,1->1 就是Level。

ET模式很大程度上减少了epoll事件的触发次数,因此效率比LT模式下高。

需要完整面试题集与各大一线互联网面试题公众号关注零声学院免费领取!

在这里插入图片描述

2.Linux内核调度详细说一下

1.1、调度策略
定义位于linux/include/uapi/linux/sched.h中
SCHED_NORMAL:普通的分时进程,使用的fair_sched_class调度类
SCHED_FIFO:先进先出的实时进程。当调用程序把CPU分配给进程的时候,它把该进程描述符保留在运行队列链表的当前位置。此调度策略的进程一旦使用CPU则一直运行。如果没有其他可运行的更高优先级实时进程,进程就继续使用CPU,想用多久就用多久,即使还有其他具有相同优先级的实时进程处于可运行状态。使用的是rt_sched_class调度类。
SCHED_RR:时间片轮转的实时进程。当调度程序把CPU分配给进程的时候,它把该进程的描述符放在运行队列链表的末尾。这种策略保证对所有具有相同优先级的SCHED_RR实时进程进行公平分配CPU时间,使用的rt_sched_class调度类
SCHED_BATCH:是SCHED_NORMAL的分化版本。采用分时策略,根据动态优先级,分配CPU资源。在有实时进程的时候,实时进程优先调度。但针对吞吐量优化,除了不能抢占外与常规进程一样,允许任务运行更长时间,更好使用高速缓存,适合于成批处理的工作,使用的fair_shed_class调度类
SCHED_IDLE:优先级最低,在系统空闲时运行,使用的是idle_sched_class调度类,给0号进程使用
SCHED_DEADLINE:新支持的实时进程调度策略,针对突发型计算,并且对延迟和完成时间敏感的任务使用,基于EDF(earliest deadline first),使用的是dl_sched_class调度类。
1.2、调度触发

调度的触发主要有两种方式,一种是本地定时中断触发调用scheduler_tick函数,然后使用当前运行进程的调度类中的task_tick,另外一种则是主动调用schedule,不管是哪一种最终都会调用到__schedule函数,该函数调用pick_netx_task,通过rq->nr_running ==rq->cfs.h_nr_running判断出如果当前运行队列中的进程都在cfs调度器中,则直接调用cfs的调度类(内核代码里面这一判断使用了likely说明大部分情况都是满足该条件的)。如果运行队列不都在cfs中,则通过优先级stop_sched_class->dl_sched_class->rt_sched_class->fair_sched_class->idle_sched_class遍历选出下一个需要运行的进程。然后进程任务切换。
处于TASK_RUNNING状态的进程才会被进程调度器选择,其他状态不会进入调度器。系统发生调度的时机如下:
à调用cond_resched()时
à显式调用schedule()时
à从中断上下文返回时
当内核开启抢占时,会多出几个调度时机如下:
à在系统调用或者中断上下文中调用preemt_enable()时(多次调用系统只会在最后一次调用时会调度)
à在中断上下文中,从中断处理函数返回到可抢占的上下文时

2、CFS调度
该部分代码位于linux/kernel/sched/fair.c中
定义了const struct sched_classfair_sched_class,这个是CFS的调度类定义的对象。其中基本包含了CFS调度的所有实现。
CFS实现三个调度策略:
1> SCHED_NORMAL这个调度策略是被常规任务使用
2> SCHED_BATCH 这个策略不像常规的任务那样频繁的抢占,以牺牲交互性为代价下,因而允许任务运行更长的时间以更好的利用缓存,这种策略适合批处理
3> SCHED_IDLE 这是nice值甚至比19还弱,但是为了避免陷入优先级导致问题,这个问题将会死锁这个调度器,因而这不是一个真正空闲定时调度器
CFS调度类:

n enqueue_task(…) 当任务进入runnable状态,这个回调将把这个任务的调度实体(entity)放入红黑树并且增加nr_running变量的值
n dequeue_task(…) 当任务不再是runnable状态,这个回调将会把这个任务的调度实体从红黑树中取出,并且减少nr_running变量的值
n yield_task(…) 除非compat_yield sysctl是打开的,这个回调函数基本上就是一个dequeue后跟一个enqueue,这那种情况下,他将任务的调度实体放入红黑树的最右端
n check_preempt_curr(…) 这个回调函数是检查一个任务进入runnable状态是否应该抢占当前运行的任务
n pick_next_task(…) 这个回调函数选出下一个最合适运行的任务
n set_curr_task(…) 当任务改变他的调度类或者改变他的任务组,将调用该回调函数
n task_tick(…) 这个回调函数大多数是被time tick调用。他可能引起进程切换。这就驱动了运行时抢占
2.1、CFS调度
Tcik 中断,主要会更新调度信息,然后调整当前进程在红黑树中的位置。调整完成以后如果当前进程不再是最左边的叶子,就标记为Need_resched标志,中断返回时就会调用scheduler()完成切换、否则当前进程继续占用CPU。从这里可以看出CFS抛弃了传统时间片概念。Tick中断只需要更新红黑树。

红黑树键值即为vruntime,该值通过调用update_curr函数进行更新。这个值为64位的变量,会一直递增,__enqueue_entity中会将vruntime作为键值将要入队的实体插入到红黑树中。__pick_first_entity会将红黑树中最左侧即vruntime最小的实体取出。

3.用UDP通讯如何保证对方百分百收到数据?

在UDP通讯中,当你的数据包发出去后,至于对方有没有正确收到数据,并不知道,那么,如何保证你发出去的数据,对方一定能收到呢???我们可以借鉴TCP协议的做法(回复+重发+编号 机制)
1)接收方收到数据后,回复一个确认包,如果你不回复,那么发送端是不会知道接收方是否成功收到数据的
比如A要发数据“{data}”到B,那B收到后,可以回复一个特定的确认包“{OK}”,表示成功收到。
但是如果只做上面的回复处理,还是有问题,比如B收到数据后回复给A的数据"{OK}“的包,A没收到,怎么办呢???
2)当A没有收到B的”{OK}“包后,要做定时重发数据,直到成功接收到确认包为止,再发下面的数据,当然,重发了一定数量后还是没能收到确认包,可以执行一下ARP的流程,防止对方网卡更换或别的原因。
但是这样的话,B会收到很多重复的数据,假如每次都是B回复确认包A收不到的话。
3)发送数据的包中加个标识符,比如A要发送的数据”{标识符|data}"到B,B收到后,先回复“{OK}"确认包,再根据原有的标识符进行比较,如果标识符相同,则数据丢失,如果不相同,则原有的标识符 = 接收标识符,且处理数据。
当A发送数据包后,没有收到确认包,则每隔x秒,把数据重发一次,直到收到确认包后,更新一下标识符,再进行后一包的数据发送。
经过上面1),2),3)点的做法,则可以保证数据百分百到达对方,当然,标识符用ID号来代替更好。

4.你所知道的设计模式有哪些?

有 23 种设计模式,不需要所有的回答,但是其中常用的几种设计模式应该去掌握。
总体来说设计模式分为三大类:
创建型模式共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
结构型模式共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
行为型模式共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

这里主要讲解简单工厂模式,代理模式,适配器模式,单例模式4中设计模式.
1、简单工厂模式
主要特点是需要在工厂类中做判断,从而创造相应的产品,当增加新产品时,需要修改工厂类。使用简单工厂模式,我们只需要知道具体的产品型号就可以创建一个产品。
缺点:工厂类集中了所有产品类的创建逻辑,如果产品量较大,会使得工厂类变的非常臃肿。
2、代理模式
代理模式:为其它对象提供一种代理以控制这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介作用。
优点:
职责清晰。真实的角色只负责实现实际的业务逻辑,不用关心其它非本职责的事务,通过后期的代理完成具体的任务。这样代码会简洁清晰。
代理对象可以在客户端和目标对象之间起到中介的作用,这样就保护了目标对象。
扩展性好。

3、适配器模式
适配器模式可以将一个类的接口转换成客户端希望的另一个接口,使得原来由于接口不兼容而不能在一起工作的那些类可以在一起工作。通俗的讲就是当我们已经有了一些类,而这些类不能满足新的需求,此时就可以考虑是否能将现有的类适配成可以满足新需求的类。适配器类需要继承或依赖已有的类,实现想要的目标接口。
缺点:过多地使用适配器,会让系统非常零乱,不易整体进行把握。比如,明明看到调用的是 A 接口,其实内部被适配成了 B 接口的实现,一个系统如果太多出现这种情况,无异于一场灾难。因此如果不是很有必要,可以不使用适配器,而是直接对系统进行重构。

4、单例模式
单例模式顾名思义,保证一个类仅可以有一个实例化对象,并且提供一个可以访问它的全局接口。实现单例模式必须注意一下几点:
单例类只能由一个实例化对象。
单例类必须自己提供一个实例化对象。
单例类必须提供一个可以访问唯一实例化对象的接口。
单例模式分为懒汉和饿汉两种实现方式。

5.mysql为什么要使用B+树作为索引呢?

b树的特点:
一个M阶的b树具有如下几个特征: (如下图M=3)(下文的关键字可以理解为 有效数据,而不是单纯的索引)
定义任意非叶子结点最多只有M个儿子,且M>2;
根结点的儿子数为[2, M];
除根结点以外的非叶子结点的儿子数为[M/2, M],向上取整; (儿子数:[2,3])
非叶子结点的关键字个数=儿子数-1;(关键字=2)
所有叶子结点位于同一层;
k个关键字把节点拆成k+1段,分别指向k+1个儿子,同时满足查找树的大小关系。(k=2)
有关b树的一些特性,注意与后面的b+树区分:
关键字集合分布在整颗树中;
任何一个关键字出现且只出现在一个结点中;
搜索有可能在非叶子结点结束;
其搜索性能等价于在关键字全集内做一次二分查找;
在这里插入图片描述

b+树,是b树的一种变体,查询性能更好。m阶的b+树的特征:

有n棵子树的非叶子结点中含有n个关键字(b树是n-1个),这些关键字不保存数据,只用来索引,所有数据都保存在叶子节点(b树是每个关键字都保存数据)。
所有的叶子结点中包含了全部关键字的信息,及指向含这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。
所有的非叶子结点可以看成是索引部分,结点中仅含其子树中的最大(或最小)关键字。
通常在b+树上有两个头指针,一个指向根结点,一个指向关键字最小的叶子结点。
同一个数字会在不同节点中重复出现,根节点的最大元素就是b+树的最大元素。
在这里插入图片描述

选用B+树作为数据库的索引结构的原因有
B+树的中间节点不保存数据,是纯索引,但是B树的中间节点是保存数据和索引的,相对来说,B+树磁盘页能容纳更多节点元素,更“矮胖”;
B+树查询必须查找到叶子节点,B树只要匹配到即可不用管元素位置,因此b+树查找更稳定(并不慢);
对于范围查找来说,B+树只需遍历叶子节点链表即可,B树却需要重复地中序遍历,在项目中范围查找又很是常见的
增删文件(节点)时,效率更高,因为B+树的叶子节点包含所有关键字,并以有序的链表结构存储,这样可很好提高增删效率。

6.如何用socket编程实现ftp协议?

以linux为例,使用socket编程中的read()函数和write()函数已可实现文件的发送接收,为啥还要专门建立ftp协议呢?单单使用read()函数和write()函数,数据接口和命令接口未分开,效率低。而ftp将数据接口和命令接口分开,提高了文件传输效率和安全性。
ftp协议的实现仍是使用socket编程,首先是实现tcp连接。
Socket 客户端编程主要步骤如下:
socket() 创建一个 Socket
connect() 与服务器连接
write() 和 read() 进行会话
close() 关闭 Socket

Socket 服务器端编程主要步骤如下:
socket() 创建一个 Socket
bind()
listen() 监听
accept() 接收连接的请求
write() 和 read() 进行会话
close() 关闭 Socket
建立tcp连接代码简示如下:

SOCKET control_sock;
struct hostent *hp;
struct sockaddr_in server;
memset(&server, 0, sizeof(struct sockaddr_in))
/* 初始化socket */
control_sock = socket(AF_INET, SOCK_STREAM, 0);
hp = gethostbyname(server_name);
memcpy(&server.sin_addr, hp->h_addr, hp->h_length);
server.sin_family = AF_INET;
server.sin_port = htons(port);
/* 连接到服务器端 */
connect(control_sock,(struct sockaddr *)&server, sizeof(server));
/* 客户端接收服务器端的一些欢迎信息 */
read(control_sock, read_buf, read_len);

ftp客户端与服务器建立起tcp连接后,然后向服务器发送命令。(这一步建立的是命令接口的tcp连接,数据接口连接尚未建立。)
通常第一步发送USER和PASS命令验证账号和密码后登陆服务器。若是匿名服务器则另当别论。
登陆服务器代码简示如下:

/* 命令 ”USER username\r\n” */
sprintf(send_buf,"USER %s\r\n",username);
/*客户端发送用户名到服务器端 */
write(control_sock, send_buf, strlen(send_buf));
/* 客户端接收服务器的响应码和信息,正常为
 ”331 User name okay, need password.” 
*/
read(control_sock, read_buf, read_len);
/* 命令 ”PASS password\r\n” */
sprintf(send_buf,"PASS %s\r\n",password);
/* 客户端发送密码到服务器端 */
write(control_sock, send_buf, strlen(send_buf));
/* 客户端接收服务器的响应码和信息,正常为 ”230 User logged in, proceed.” */
read(control_sock, read_buf, read_len);
 

接下来是选择发送PORT命令选择主动模式还是发送PASV命令选择被动模式。
主动模式还是被动模式是相对服务器来说的。被动模式即命令接口连接和数据接口连接都由客户端主动建立;主动模式是数据接口连接由服务器主动建立。这里仅简述被动模式的建立,主动模式可依理建立。
被动模式建立连接代码:

/* 命令 ”PASV\r\n” */
sprintf(send_buf,"PASV\r\n");
/* 客户端告诉服务器用被动模式 */
write(control_sock, send_buf, strlen(send_buf));
/*客户端接收服务器的响应码和新开的端口号,* 正常为 
”227 Entering passive mode (<h1,h2,h3,h4,p1,p2>)” 
*/
read(control_sock, read_buf, read_len);

客户端发送PASV命令让服务器进入被动模式。服务器会打开数据端口并监听。并返回响应码 227 和数据连接的端口号。
接下来通过数据端口下载上传查看文件就不赘述了。

/* 连接服务器新开的数据端口 */
connect(data_sock,(struct sockaddr *)&server, sizeof(server));
/* 命令 ”CWD dirname\r\n” */
sprintf(send_buf,"CWD %s\r\n", dirname);
/* 客户端发送命令改变工作目录 */
write(control_sock, send_buf, strlen(send_buf));
/* 客户端接收服务器的响应码和信息,正常为 ”250 Command okay.” */
read(control_sock, read_buf, read_len);
/* 命令 ”SIZE filename\r\n” */
sprintf(send_buf,"SIZE %s\r\n",filename);
/* 客户端发送命令从服务器端得到下载文件的大小 */
write(control_sock, send_buf, strlen(send_buf));
/* 客户端接收服务器的响应码和信息,正常为 ”213 <size>” */
read(control_sock, read_buf, read_len);
/* 命令 ”RETR filename\r\n” */
sprintf(send_buf,"RETR %s\r\n",filename);
/* 客户端发送命令从服务器端下载文件 */
write(control_sock, send_buf, strlen(send_buf));
/* 客户端接收服务器的响应码和信息,正常为 ”150 Opening data connection.” */
read(control_sock, read_buf, read_len);
/* 客户端创建文件 */
file_handle = open(disk_name, CRFLAGS, RWXALL);
for( ; ; ) {
    
    
... ...
/* 客户端通过数据连接 从服务器接收文件内容 */
read(data_sock, read_buf, read_len);
/* 客户端写文件 */
write(file_handle, read_buf, read_len);
... ... 
}
/* 客户端关闭文件 */
rc = close(file_handle);
 
下载完成后客户端退出服务器,关闭连接。
/* 客户端关闭数据连接 */
close(data_sock);
/* 客户端接收服务器的响应码和信息,正常为 ”226 Transfer complete.” */
read(control_sock, read_buf, read_len);
/* 命令 ”QUIT\r\n” */
sprintf(send_buf,"QUIT\r\n");
/* 客户端将断开与服务器端的连接 */
write(control_sock, send_buf, strlen(send_buf));
/* 客户端接收服务器的响应码,正常为 ”200 Closes connection.” */
read(control_sock, read_buf, read_len);
/* 客户端关闭控制连接 */
close(control_sock);

断点续传的实现
由于网络不稳定,在传输文件的过程中,可能会发生连接断开的情况,这时候需要客户端支持断点续传的功能,下次能够从上次终止的地方开始接着传送。需要使用命令 REST。如果在断开连接前,一个文件已经传输了 512 个字节。则断点续传开始的位置为 512,服务器会跳过传输文件的前 512 字节。

7.手写冒泡排序算法,计算冒泡排序算法的时间复杂度?

public void swap(int[] array, int i, int j) {
    
    
 array[i] = array[i] + array[j];
 array[j] = array[i] - array[j];
 array[i] = array[i] - array[j];
}
public void sort(int[] array) {
    
    
 int n = array.length;
 for (int i = 0; i < n - 1; i++) {
    
    
 //在这里j不需要遍历到n-1了,因为n-1-i~n-1之间的元素
//已经排好序了,不需要再比较
 for (int j = 0; j < n - 1 - i; j++) {
    
    
 //将最大元素移动到数组末尾
 if (array[j] > array[j + 1]) {
    
    
 swap(array, j, j + 1);
 }
 }
 }
}

从代码中可以看出一共遍历了n-1 + n-2 + … + 2 + 1 = n * (n-1) / 2 = 0.5 * n ^ 2 - 0.5 * n,
那么时间复杂度是O(N^2)。

8.redis中的网络IO有了解过吗,它是单线程的还是多线程的,为什么要用单线程。

redis 采用网络IO多路复用技术来保证在多连接的时候,系统的高吞吐量。
多路-指的是多个socket连接,复用-指的是复用一个线程。多路复用主要有三种技术:select,poll,epoll。epoll是最新的也是目前最好的多路复用技术。
这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路I/O
复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),且Redis在内存中操作数据的速度非常快(内存内的操作不会成为这里的性能瓶颈),主要以上两点造就了Redis具有很高的吞吐量。

在这里插入图片描述

因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,所以采用单线程的方案。

猜你喜欢

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