a滴滴 && 美团面经

网络字节序与主机字节序

主机字节序就是我们平常说的大端和小端模式:不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机序。引用标准的Big-Endian和Little-Endian的定义如下:

  a) Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。

  b) Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。

网络字节序:4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这种传输次序称作大端字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,一个字节的数据没有顺序的问题了。

所以:在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,而不要假定主机字节序跟网络字节序一样使用的是Big-Endian。由于这个问题曾引发过血案!公司项目代码中由于存在这个问题,导致了很多莫名其妙的问题,所以请谨记对主机字节序不要做任何假定,务必将其转化为网络字节序再赋给socket。

滴滴

 

1.socket函数有哪些

TCP:

服务端:

socket():套接字初始化,用于创建一个socket描述符(socket descriptor),它唯一标识一个socket

bind():绑定socket和地址

listen():监听端口

accept():listen队列中接收一个连接

recv()send():读写函数

close():关闭连接

客户端:

connect():发起连接

send()recv():读写函数

close():关闭连接

总结:
TCP Server端    :create --> bind --> listen -->  accept -->  recv/send --> close
TCP Client端     :create --> conncet --> send/recv --> close.

UDP:

. 服务器端:
    1)创建套接字create;
    2)绑定端口号bind;
    3)接收/发送消息recvfrom/sendto;
    4)关闭套接字。

2. 客户端:
    1)创建套接字create;
    2)发送/接收消息sendto/recvfrom;
    3)关闭套接字.

总结:
UDP Server端 :create --> bind --> recvfrom/sendto -- >close
UDP Client端  :create --> sendto/recvfrom --> close.

2.tcp断开和连接过程中的各个状态

三次握手:

第一次握手:建立连接时,客户端发送SYN(syn = x)包到服务器,并进入到syn_sent状态。等待服务器确认;syn(Synchronize Sequence Numbers 同步序列编号)
第二次握手:服务器收到syn包,必须确认客户的syn(ack = x+1),同时自己也发送一个syn(syn = y)包,即SYN+ ACK包。此时进入syn_recv状态

第三次握手:客户端收到syn+ack包,向服务器发送确认包ack(ack=y+1)。此包发送完毕,客户端和服务器进入到established状态。完成三次握手

a滴滴 && 美团面经_第1张图片

四次挥手:

第一次握手:客户端进程发出连接释放报文,并停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u

第二次握手:服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。

第三次握手:客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。

第四次握手:客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。

a滴滴 && 美团面经_第2张图片

3.map和unordered_map区别,各自的使用场景

map,set底层实现都提供了排序功能,红黑树的存储的健值,同时红黑树可以在O(log N)时间内插入,查找和删除

  • 1.为什么提出红黑树

二叉排序树的性能取决于二叉树的层数:最好情况是O(logn),存在于完全二叉排序树情况下,其访问性能近似于折半查找;

最差时候会是 O(n),比如插入的元素是有序的,生成的二叉排序树就是一个链表,这种情况下,需要遍历全部元素才行

  • 2.红黑树的概念

本质是一种二叉查找树,在此基础上添加了一个标记(颜色),同时具有一定的规则,这些规则使得红黑树的一种平衡,使得插入,删除和查找的最坏时间复杂度都是 O(logn)

  • 3.红黑树的特点
  1. 每个结点都是红色或者黑色
  2. 根节点永远是黑色的
  3. 所有的叶子结点都是黑的
  4. 红色结点的子节点都是黑色
  5. 任一节点到子树的叶子结点的路径都包含了相同数量的黑色节点
  • 4.红黑树的优点

红黑树是牺牲了严格的高度平衡的优越条件为代价,它只要求部分地达到平衡要求,降低了对旋转的要求,从而提高了性能。红黑树能够以O(log2 n)的时间复杂度进行搜索、插入、删除操作。此外,由于它的设计,任何不平衡都会在三次旋转之内解决。当然,还有一些更好的,但实现起来更复杂的数据结构能够做到一步旋转之内达到平衡,但红黑树能够给我们一个比较“便宜”的解决方案。

相比于BST,因为红黑树可以能确保树的最长路径不大于两倍的最短路径的长度,所以可以看出它的查找效果是有最低保证的。在最坏的情况下也可以保证O(logN)的,这是要好于二叉查找树的。因为二叉查找树最坏情况可以让查找达到O(N)。

红黑树的算法时间复杂度和AVL相同,但统计性能比AVL树更高,所以在插入和删除中所做的后期维护操作肯定会比红黑树要耗时好多,但是他们的查找效率都是O(logN),所以红黑树应用还是高于AVL树的. 实际上插入 AVL 树和红黑树的速度取决于你所插入的数据.如果你的数据分布较好,则比较宜于采用 AVL树(例如随机产生系列数),但是如果你想处理比较杂乱的情况,则红黑树是比较快的

 

C++中unordered_map的底层是用哈希表来实现的,通过key的哈希路由到每一个桶(即数组)用来存放内容。通过key来获取value的时间复杂度就是O(1)。因为key的哈希容易碰撞,所以需要对碰撞做处理。unordered_map里的每一个数组(桶)里面存的其实是一个链表,key的哈希冲突以后会加到链表的尾部,这是再通过key获取value的时间复杂度就变成O(n),当碰撞很多的时候查询就会变慢。为了优化这个时间复杂度,map的底层就把这个链表转换成了红黑树,这样虽然插入增加了复杂度,但提高了频繁哈希碰撞时的查询效率,使查询效率变成O(log n)

 

各自使用的场景:

AVL树:最早比较少,最早的平衡二叉树之一,应用想对于其他数据结构相对少,window对进程地址空间的管理用到了AVL树

红黑树:平衡二叉树,广泛用在STL中,如map和set使用红黑树实现的;epoll在内核中的实现,用到了红黑树

B/B+树:用在磁盘文件组织 数据索引和数据库索引

Trie(字典树):用在统计和排序大量字符串,如自动机

 

4.智能指针

share_ptr:允许多个指针指向同一个对象,使用计数对资源管理,当引用计数为0时,没有指针指向该资源,资源会释放;

unique_ptr:在同一时刻只有一个unique_ptr指向给定的对象,该智能指针是禁止使用拷贝构造函数的 禁止使用赋值运算符重载函数 

weak_ptr:weak_ptr 弱智能指针,能看到资源的引用计数,但是不会去用这个引用计数,使用weak_ptr指向资源不会使引用计数增加。

 

5.上下文切换具体切换的是什么

上下文切换是指在CPU中的状态存储于内存的PCB中,在PCB中检索下一个进程的上下文并将其在CPU的寄存器中恢复

用户级上下文: 正文、数据、用户堆栈以及共享存储区;
寄存器上下文: 通用寄存器、程序寄存器(IP)、处理器状态寄存器(EFLAGS)、栈指针(ESP);
系统级上下文: 进程控制块task_struct、内存管理信息(mm_struct、vm_area_struct、pgd、pte)、内核栈

 

6.生成可执行文件的过程

预处理阶段→编译阶段→汇编阶段→链接阶段。

a滴滴 && 美团面经_第3张图片

预处理:对源代码中的包含关系、宏定义进行分析和替换,处理源代码中的预编译指令,生成预编译文件

Tip:保留所有的"#pragma"编译器指令,编译器需要用到他们,如:"#pragma once"是为了防止有文件被重复引用

编译:预处理之后的文件转换成特定的汇编代码,生成汇编文件,进行一系列的语法分析,生成相应的汇编代码

汇编:汇编器的汇编过程相对于编译器来说更简单,没有复杂的语法,也没有语义,更不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译过来,汇编过程由汇编器完成。经过汇编之后,产生目标文件.o(Windows下)或.obj(Linux下)

链接:将多个目标文件即所需要的库用动态链接或者是静态链接 连接链接成最终的可执行目标文件;

7.进程和线程的通信方式

进程通信方式:

(1)共享内存

允许两个或者多个进程共享一定的存储区,当一个进程改变了这块地址中的内容的时候,其他进程都会察觉到这个更改,因为数据不需要来回复制,所以是一种最快的IPC。

Tip:共享内存没有任何的同步和互斥机制,所以要使用信号量来实现对共享内存的同步;

共享内存的生命周期随内核,即所有访问共享内存区域对象的进程都已经正常结束,共享内存区域对象仍然在内核中存在。简单的说,共享内存中的对象跟系统内核的生命周期是一样的

(2)管道通信:pipe,FIFO

PIPO:管道其实是一个队列,先进先出的方法从缓冲区读取数据,管道一端的进程顺序的将数据写入缓冲区,另外一端的进程顺序的读出数据;

pipe:只能用于具有血缘关系的进程,半双工的通信方式,父进程使用pipe开辟管道,fork创建子进程都指向同一管道,父进程关闭读端,子进程关闭下写端,管道会用环形队列实现,数据从写端流入到读端,这样就实现了进程的通信

FIFO:PIPO只能用于血缘关系的进程进程之间的通信,为了客服这个缺点,提出了有名管道FIFO

他和无名管道的区别在于,他提供了一个路径名相关联,即使与有名管道的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过有名管道相互通信,因此,通过有名管道不相关的进程也能交换数据。

Tip:有名管道严格遵循先进先出(first in first out),对匿名管道及有名管道的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作。有名管道的名字存在于文件系统中,内容存放在内存中。

(3)消息队列

消息队列是在两个不相关进程间传递数据的一种简单、高效方式,她独立于发送进程、接受进程而存在。消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。每个数据块都被认为是一个管道,接收进程可以独立地接收含有不同管道的数据结构。我们可以通过发送消息来避免命名管道的同步和阻塞问题。消息队列与命名管道一样,每个数据块都有一个最大长度的限制。我们可以将每个数据块当作是一种消息类型(频道),发送和接收的内容就是这个类型(频道)对应的消息(节目),每个类型(频道)相当于一个独立的管道,相互之间互不影响。

 

(4)套接字

套接字(Socket):可用于不同计算机间的进程通信

  • 优点:
    1. 传输数据为字节级,传输数据可自定义,数据量小效率高
    2. 传输数据时间短,性能高
    3. 适合于客户端和服务器端之间信息实时交互
    4. 可以加密,数据安全性强
  • 缺点:需对传输的数据进行解析,转化成应用级的数据

线程通信方式:

# 锁机制:包括互斥锁、条件变量、读写锁
   *互斥锁提供了以排他方式防止数据结构被并发修改的方法。
   *读写锁允许多个线程同时读共享数据,而对写操作是互斥的。
   *条件变量可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。
# 信号量机制(Semaphore):包括无名线程信号量和命名线程信号量
# 信号机制(Signal):类似进程间的信号处理
    线程间的通信目的主要是用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制。

8.c++11新特性

 

9.move

 

 

10.模板函数,普通函数、lambel,各自的使用场景

 

 

11.内核态和用户态

一般现在的CPU都有几种不同的指令执行级别

在高执行级别下、代码可以执行特权指令,访问任意的物理地址,这种CPU执行级别就对应着内核态

而在低级别执行状态下,代码的掌控范围会受到限制,只能在对应级别允许的范围内活动

举例:

intel x86CPU有四种不同的执行级别0-3,Linux只使用了其中的0级和3级分别表示内盒态和用户态

为什么会有权限级别的划分?防止程序员代码搞崩系统。在 CPU 的所有指令中,有些指令是非常危险的,如果错用,将导致系统崩溃,比如清内存、设置时钟等。如果允许所有的程序都可以使用这些指令,那么系统崩溃的概率将大大增加

 

区分:

cs寄存器的最低两位表明了当前代码的特权级

cpu每条指令的存取都是通过cs(代码段选择寄存器):eip(偏移量寄存器)这两个寄存器

上述判断由硬件完成

一般来说在linux中

针对 Linux 操作系统而言,最高的 1G 字节(从虚拟地址 0xC0000000 到 0xFFFFFFFF)由内核使用,称为内核空间。而较低的 3G 字节(从虚拟地址 0x00000000 到 0xBFFFFFFF)由各个进程使用,称为用户空间。

(这里说的是虚拟空间)

 

a滴滴 && 美团面经_第4张图片

中断发生的第一件事就是保护现场

处理结束前的最后一件事就是恢复现场

保护现场就是  进入中断程序   保存    需要用到的   寄存器    的数据

恢复现场就是  退出中断程序   恢复    保存寄存器  的    数据

a滴滴 && 美团面经_第5张图片

a滴滴 && 美团面经_第6张图片

0x80 系统调用

保存了  cs:eip 的值   ss(堆栈段寄存器):esp(栈顶)    eflags(当前的标志寄存器)  保存到内核堆栈去了

同时加载中断服务入口到cs:eip中  同时ss:esp加载到cpu中

CPU执行下一条的时候,就开始执行中断了

完成中断服务之后

12.static

  1. 修饰普通变量,修改变量的存储区域和生命周期,使变量存储在静态区,在 main 函数运行前就分配了空间,如果有初始值就用初始值初始化它,如果没有初始值系统用默认值初始化它。
  2. 修饰普通函数,表明函数的作用范围,仅在定义该函数的文件内才能使用。在多人开发项目时,为了防止与他人命名空间里的函数重名,可以将函数定位为 static。
  3. 修饰成员变量,修饰成员变量使所有的对象只保存一个该变量,而且不需要生成对象就可以访问该成员。
  4. 修饰成员函数,修饰成员函数使得不需要生成对象就可以访问该函数,但是在 static 函数内不能访问非静态成员。

(1)static 成员变量不随对象的创建而分配内存,也不随对象的销毁而释放内存。而普通成员变量在对象创建时分配内存,在对象销毁时释放内存

(2)static成员变量必初始化,而且只能在类外进行,如果不赋值,默认为0,全局数据区的变量都有默认的初始值0,而动态存储区的默认值值是不固定的,一般认为是垃圾值

 

静态成员函数与普通成员函数的根本区别在于:普通成员函数有 this 指针,可以访问类中的任意成员;而静态成员函数没有 this 指针,只能访问静态成员(包括静态成员变量和静态成员函数)

 

13.排序链表

 

 

二面:

1.http和tcp的区别;http1.0 1.1 1.2

http1.1 主要区别在于:

1.带宽优化以及网络连接的作用:HTTP1.0中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能,HTTP1.1则在请求头引入了range头域,它允许只请求资源的某个部分,即返回码是206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接。

2.错误通知的管理,在HTTP1.1中新增了24个错误状态响应码,如409(Conflict)表示请求的资源与资源的当前状态发生冲突;410(Gone)表示服务器上的某个资源被永久性的删除

3.Host头处理,在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个IP地址。HTTP1.1的请求消息和响应消息都应支持Host头域,且请求消息中如果没有Host头域会报告一个错误(400 Bad Request)

4.长连接,HTTP 1.1支持长连接(PersistentConnection)和请求的流水线(Pipelining)处理,在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟,在HTTP1.1中默认开启Connection: keep-alive,一定程度上弥补了HTTP1.0每次请求都要创建连接的缺点

http2.0:

1.多路复用:同一个连接并发处理多个请求

2.数据压缩:HTTP1.1不支持header数据的压缩,HTTP2.0使用HPACK算法对header的数据进行压缩,这样数据体积小了,在网络上传输就会更快

3.服务器推送:当我们对支持HTTP2.0的web server请求数据的时候,服务器会顺便把一些客户端需要的资源一起推送到客户端,免得客户端再次创建连接发送请求到服务器端获取。这种方式非常合适加载静态资源。

服务器端推送的这些资源其实存在客户端的某处地方,客户端直接从本地加载这些资源就可以了,不用走网络,速度自然是快很多的

 

2.线程相对于进程的优缺点   https://cloud.tencent.com/developer/article/1416283

 


3.多线程:安全

多线程有三大特性

即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。 一个很经典的例子就是银行账户转账问题:
比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。这2个操作必须要具备原子性才能保证不出现一些意外的问题。
我们操作数据也是如此,比如i = i+1;其中就包括,读取i的值,计算i,写入i。这行代码在Java中是不具备原子性的,则多线程运行肯定会出问题,所以也需要我们使用同步和lock这些东西来确保这个特性了。
原子性其实就是保证数据一致、线程安全一部分,

什么是可见性

当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
若两个线程在不同的cpu,那么线程1改变了i的值还没刷新到主存,线程2又使用了i,那么这个i值肯定还是之前的,线程1对变量的修改线程没看到这就是可见性问题。

什么是有序性

程序执行的顺序按照代码的先后顺序执行。 一般来说处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。如下:

int a = 10;    //语句1
int r = 2;    //语句2
a = a + 3;    //语句3
r = a*a;     //语句4
复制代码

则因为重排序,他还可能执行顺序为 2-1-3-4,1-3-2-4
但绝不可能 2-1-4-3,因为这打破了依赖关系。
显然重排序对单线程运行是不会有任何问题,而多线程就不一定了,所以我们在多线程编程时就得考虑这个问题了。

 

4.事务:四大特性

  • 原子性:一个事务的所有操作,要么完全执行,要么完全不执行,不会中间结束在某个环节
  • 一致性:一个事务在执行前和执行后,都处于一个状态
  • 隔离性:一个事务未提交的结果是否对其他业务可见:级别一般有  读未提交、读提交、可重复读、幻读
  • 持久性:一个事物一旦提交了,那么对数据库的改变就是永久的、即便是在数据库系统遇到故障了也不会丢失提交事务的操作

5.虚拟内存和实存

 

6.linux基本命令

 

美团后台一面面经(04-07)

1.c++多线程,如何管理多线程

pthread、c++11引入了thread库

线程池设计:

为什么需要线程池:

可以根据系统的需求和硬件环境灵活的控制线程的数量,且可以对所有线程进行一个统一的管理和控制,从而提高系统的运行效率,降低系统的运行压力;

 

优势:

1.利用其中线程池中的一个线程,重复的是解决这个问题,提高线程的重用性

2.控制线程的并发数量、降低服务器压力,统一管理所有线程

3.提升系统响应速度,免去了创建和销毁线程的时间

线程池应用场景介绍:

  • 网购商品秒杀
  • 云盘文件上传和下载
  • 12306网上购票系统

只要有并发的地方,数量大或者小,每个任务执行时间长或段短都可以使用线程池

构造方法:

a滴滴 && 美团面经_第7张图片

  1. 核心线程数量
  2. 最大线程数
  3. 最大空闲时间
  4. 时间单位
  5. 任务队列
  6. 线程工厂
  7. 饱和处理机制

核心线程数量,只负责初始化或者提交任务的时候,允许达到的一个核心线程数量,线程数量达到核心数量之后,有一个最大线程数量,保证系统正常运行,服务器运行过在一定的压力之下

最大空闲时间:线程没有任务的时候,允许的空闲时间,过了空闲时间之后,线程池会回收线层

时间单位:枚举类型,里边放的时间常量

任务队列(临时缓冲区):线程数量达到了核心线程数量的时候,如果再有任务,不会立马创建新线程,会加到任务队列中去,这时候如果任务队列都满了的话,按照设定的最大线程数量去开启线程

线程工厂:允许我们自己参与创建线程的过程

饱和处理机制:任务队列满了,线程即达到了核心线程数量和最大线程数量,还有任务,线程池已经没有能力处理,就按机制处理

比如:丢弃,等一会等等


构造方法的参数:自定义的线程池才会合理

  • 1.核心线程数量:

核心线程数量需要根据任务的处理时间每秒产生的任务数量来决定

8020原则去设计,80%的情况去设计核心线程数,剩下的20利用最大线程数量去设计

  • 2.任务队列长度:

核心线程数 10,单个线程执行时间是0.1s,队列长度是 10/0.1*2 = 200

  • 3.最大线程数

  • 4.最大空闲时间

这个参数的设计完全参考系统运行环境和硬件压力而定,没有固定的参考值,用户根据经验和系统产生任务的时间间隔合理值即可

 

2.进程线程协程区别

联系:一个线程只能属于一个进程,而一个进程可以有多个线程,并且至少一个线程

区别:

  • 进程是一个资源分配的一个基本单位,线程是任务调度的一个基本单位
  • 每一个进程有自己的地址空间和资源,在进行进程切换的时候,操作系统都要为这个进程回收内存空间。i/0设备等,切换开销大,线城切换的时候,只需保存少量寄存器内容,开销很小;
  • 从同一个进程的线程切换到另一个进程的线程不会因为进程的切换,但是从一个进程的线程切换到另一个进程的线程的时候会因为进程的切换
  • 通信方面:进程的切换需要借助于同步和互斥的手段,线城可以通过读取全局变量来实现通信;

协程:

协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。

3.epoll

函数介绍

红黑树和链表原理

4.TCP怎么保证可靠传输

5.web页面请求过程

  • DNS 解析
  • TCP 连接
  • 发送 HTTP 请求
  • 服务器处理请求并返回 HTTP 报文
  • 浏览器解析渲染页面
  • 连接结束

6.C++项目中如何进行性能测试?内存泄漏怎么检测(没测。。。)(大佬原来也没测过内存泄漏  偷笑~~我也可以四五鸡蛋的说了)

7.分布式

 

8.技术职业规划(JAVA)

 

9.数据库、索引B树、聚集索引、非聚集索引

B树的优点:

  1. B+树的磁盘读写代价更低
    B+树的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对B树更小。如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多。相对来说I/O读写次数也就降低了。

  2. B+树的查询效率更加稳定
    由于内部结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。

  3. B+树更有利于对数据库的扫描
    B树在提高了磁盘IO性能的同时并没有解决元素遍历的效率低下的问题,而B+树只需要遍历叶子节点就可以解决对全部关键字信息的扫描,所以对于数据库中频繁使用的range query,B+树有着更高的性能。

B树和B+树的区别?

  • B+树内部结点不存储数据,时间复杂度为log(n),B树不固定,时间复杂度最好为O(1),根节点拿到了
  • B+树叶结点两两相连大大增加区间访问性,可使用在范围查询,而B树每个节点key和data在一起,则无法区间查找

a滴滴 && 美团面经_第8张图片

  • B+树内部无数据,每个节点能索引的范围更大更准确

a滴滴 && 美团面经_第9张图片

  • 在数据结构上,B树为有序数组+平衡多叉树,而B+树为有序数组链表+平衡多叉树(可省略)

 

聚集索引和非聚集索引:

定义:数据行的物理顺序与列值(一般是主键的那一列)的逻辑顺序相同,一个表中只能拥有一个聚集索引。

聚集索引和非聚集索引:

定义:该索引中索引的逻辑顺序与磁盘上行的物理存储顺序不同,一个表中可以拥有多个非聚集索引。

索引是通过二叉树的数据结构来描述的,我们可以这么理解聚簇索引:索引的叶节点就是数据节点。而非聚簇索引的叶节点仍然是索引节点,只不过有一个指针指向对应的数据块。如下图

 

10.数据库优化

 

 

11.确定某字符串的所有排列组合

剑指 Offer 38. 字符串的排列

输入一个字符串,打印出该字符串中字符的所有排列。

你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。

示例:

输入:s = "abc"
输出:["abc","acb","bac","bca","cab","cba"]

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/zi-fu-chuan-de-pai-lie-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

class Solution {
public:
    void dfs(vector &res,vector flag,string temp,string s){
        if(temp.size()==s.size()){
            res.push_back(temp);
        }

        for(int i=0;i0&&s[i]==s[i-1]&&flag[i-1]==1)continue;

            temp+=s[i];
            flag[i]=1;
            dfs(res,flag,temp,s);
            flag[i]=0;
            temp.pop_back();
        }
    }
    vector permutation(string s) {
        
        sort(s.begin(),s.end());
        
        vector res;
        vector flag(s.size());
        string temp;

        dfs(res,flag,temp,s);

        return res;
    }
};

 

美团两面面经(8.19)

一面:

1.linux内存模型

 

 

 

2.linux线程进程模型

 

 

3.IO多路复用

select poll epoll

4.代码题:

在数组中找到一个局部最小的位置
限定语言:C、Python、C++、Javascript、Python 3、Java、Go
定义局部最小的概念。arr长度为1时,arr[0]是局部最小。arr的长度为N(N>1)时,如果arr[0]

5.RPC,数据库,服务间调用,消息队列,分布式相关

分布式相关

 

(1)单机结构

我想大家最最最熟悉的就是单机结构,一个系统业务量很小的时候所有的代码都放在一个项目中就好了,然后这个项目部署在一台服务器上就好了。整个项目所有的服务都由这台服务器提供。这就是单机结构。

那么,单机结构有啥缺点呢?我想缺点是显而易见的,单机的处理能力毕竟是有限的,当你的业务增长到一定程度的时候,单机的硬件资源将无法满足你的业务需求。此时便出现了集群模式,往下接着看。

(2)集群结构

集群模式在程序猿界有各种装逼解释,有的让你根本无法理解,其实就是一个很简单的玩意儿,且听我一一道来。

单机处理到达瓶颈的时候,你就把单机复制几份,这样就构成了一个“集群”。集群中每台服务器就叫做这个集群的一个“节点”,所有节点构成了一个集群。每个节点都提供相同的服务,那么这样系统的处理能力就相当于提升了好几倍(有几个节点就相当于提升了这么多倍)。

但问题是用户的请求究竟由哪个节点来处理呢?最好能够让此时此刻负载较小的节点来处理,这样使得每个节点的压力都比较平均。要实现这个功能,就需要在所有节点之前增加一个“调度者”的角色,用户的所有请求都先交给它,然后它根据当前所有节点的负载情况,决定将这个请求交给哪个节点处理。这个“调度者”有个牛逼了名字——负载均衡服务器。

集群结构的好处就是系统扩展非常容易。如果随着你们系统业务的发展,当前的系统又支撑不住了,那么给这个集群再增加节点就行了。但是,当你的业务发展到一定程度的时候,你会发现一个问题——无论怎么增加节点,貌似整个集群性能的提升效果并不明显了。这时候,你就需要使用微服务结构了。

(3)分布式结构

先来对前面的知识点做个总结。

从单机结构到集群结构,你的代码基本无需要作任何修改,你要做的仅仅是多部署几台服务器,每台服务器上运行相同的代码就行了。但是,当你要从集群结构演进到微服务结构的时候,之前的那套代码就需要发生较大的改动了。所以对于新系统我们建议,系统设计之初就采用微服务架构,这样后期运维的成本更低。但如果一套老系统需要升级成微服务结构的话,那就得对代码大动干戈了。所以,对于老系统而言,究竟是继续保持集群模式,还是升级成微服务架构,这需要你们的架构师深思熟虑、权衡投入产出比。

OK,下面开始介绍所谓的分布式结构。

分布式结构就是将一个完整的系统,按照业务功能,拆分成一个个独立的子系统,在分布式结构中,每个子系统就被称为“服务”。这些子系统能够独立运行在web容器中,它们之间通过RPC方式通信。

举个例子,假设需要开发一个在线商城。按照微服务的思想,我们需要按照功能模块拆分成多个独立的服务,如:用户服务、产品服务、订单服务、后台管理服务、数据分析服务等等。这一个个服务都是一个个独立的项目,可以独立运行。如果服务之间有依赖关系,那么通过RPC方式调用。

这样的好处有很多:

  1. 系统之间的耦合度大大降低,可以独立开发、独立部署、独立测试,系统与系统之间的边界非常明确,排错也变得相当容易,开发效率大大提升。
  2. 系统之间的耦合度降低,从而系统更易于扩展。我们可以针对性地扩展某些服务。假设这个商城要搞一次大促,下单量可能会大大提升,因此我们可以针对性地提升订单系统、产品系统的节点数量,而对于后台管理系统、数据分析系统而言,节点数量维持原有水平即可。
  3. 服务的复用性更高。比如,当我们将用户系统作为单独的服务后,该公司所有的产品都可以使用该系统作为用户系统,无需重复开发。

RPC

远程过程调用,两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上提供的函数和方法,由于不在一个内存空间,不能直接调用,需要通过网络表达调用的语义和传达调用的数据。

首先:解决通讯的问题,客户端和服务端建立TCP链接,远程过程调用的是所有交换的数据都在这个链接中传输,链接可以是按需链接,调用结束后就断掉,也可以是长链接,多个过程调用共享同一个链接。

第二:解决通讯的问题,也就是说,A服务器上的应用怎么告诉底层的RPC框架,如何连接到B服务器以及特定的端口,方法是什么以及怎么完成调用,比如基于Web服务协议栈的RPC,就要提供一个endpoint URI,或者是从UDDI服务哈桑查找,如果是RMI调用的话,还需要一个RMI Redistry来注册服务的地址

第三:当发起调用的时候,方法的参数需要通过底层的网络协议,如果tcp网络协议,由于网络协议是基于二进制的,内存中的参数的值要序列化成二进制的形式,也就是序列化,通过寻址和传输将二进制发送给B服务器

第四:B服务器收到请求后,需要对参数进行反序列化,恢复为内存中的表达方式,然后找到对应的方法,进行本地调用,然后得到返回值

第五:返回值还要发送到A的应用,也是经过序列化的方式发送,服务器A接到后,再反序列化,恢复为内存中的表达方式,交给A上的应用;

 

消息队列

消息队列管道通信的区别

 

美团二面(8.24)

1.合并两个有序单链表

. 合并两个有序链表

难度简单1239收藏分享切换为英文关注反馈

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 

示例:

输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
        
        if(l1==NULL)return l2;
        if(l2==NULL)return l1;

        ListNode *ans = new ListNode(0);
        ListNode *res = ans;

        while(l1!=NULL && l2!=NULL){
            if(l1->val <= l2->val){
                ans->next = l1;
                ans = ans->next;
                l1=l1->next;
            }
            else{
                ans->next=l2;
                ans = ans->next;
                l2=l2->next;
            }
        }

        ans->next = (l1==NULL)?l2:l1;

        return res->next;
    }
};

 

 

2.在1的基础上两个链表可能相交

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
        
        if(l1==NULL)return l2;
        if(l2==NULL)return l1;

        ListNode *ans = new ListNode(0);
        ListNode *res = ans;
        
        int flag=0;
        while(l1!=NULL && l2!=NULL){
            if(l1==l2){flag=1;break;}
            if(l1->val <= l2->val){
                ans->next = l1;
                ans = ans->next;
                l1=l1->next;
            }
            else{
                ans->next=l2;
                ans = ans->next;
                l2=l2->next;
            }
        }
        if(flag==1)ans->next=l1;
        else {
            ans->next = (l1==NULL)?l2:l1;
        }

        return res->next;
    }
};

 

3.n个有序链表合并,内存较小,不能直接加载到内存

(合并k个有序链表?)

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */


struct cmp{
    bool operator()(ListNode*a,ListNode*b){
        return a->val>b->val;
    }
};

class Solution {
public:
    ListNode* mergeKLists(vector& lists) {
        if(lists.size()==0)return NULL;

        priority_queue,cmp> Q;
        for(int i=0;inext = temp;
            res = res->next;
            
            if(temp->next)Q.push(temp->next);
        }

        return ans->next;
    }
};

4.堆排序的时间复杂度、稳定性

O(nlogn) 不稳定

5.堆排序过程口述

 

1.创建一个堆

2.取出堆顶数据,把堆首和堆尾互换;然后把待排序范围-1;

3.尺寸缩小为1,对剩下的元素重新构建堆

4.重复上述步骤

 

6.有哪些排序算法时间复杂度是O(n*logn)

希尔排序、归并排序、快速排序、堆排序

 

内排序:所有排序操作都在内存中完成;
外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;

Tip:十大排序的分析

O(n2):冒泡排序、选择排序、插入排序

O(nlogn):希尔排序、归并排序、快速排序、堆排序

O(n+k):计数排序、桶排序

O(nxk):基数排序

稳定排序:冒泡、插入、归并、计数、桶、基数

不稳定:选择、希尔、快速、堆

稳定排序和不稳定排序的区别:https://zhuanlan.zhihu.com/p/42586566

排序算法的稳定性大家应该都知道,通俗地讲就是能保证排序前2个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。

(1)冒泡:如果相等,就不动,所以位置相同,不变化

(2)选择:给每个位置选择最小的,如果小的元素在相等元素的后边就会破坏稳定性,比如 5 8 5 2 9,第一趟排序就会把5和2交换 破坏了稳定性

(3)插入:把当前元素之前的元素认定为已经有序,然后把当前元素插进去,从有序数据的末尾开始插的,如果碰见和他元素相等的话,就会放在相等元素的后边, 所以是稳定的

(4)希尔:希尔排序是按照不同的步长进行排序,无序的时候步长很大,所以插入排序的元素个数少,速度快,但是基本有序的话,步长很小,插入排序对于有序的效率很高。一次插入排序是稳定的,不会改变相对位置,但是不同的插入排序的过程,相同的排序可能在各自的插入排序中移动,最后稳定性被打乱

(5)归并:归并排序是递归的分成子序列,将各个子序列排序最后合并成一个有序的长序列,两个元素相等的时候不会交换,在合并的过程中处在序列前边的元素保存在结果序列的前边,这样保证了稳定性

(6)快排:快排有两个方向,左边的元素一直往右走,右边的元素一直往左走,i==j  对当前位置的左右子数组,重复上述过程 ,元素交换的过程中有可能 把右边的元素交换到相等元素的前边,比如序列为5 3 3 4 3 8 9 10 11,现在中枢元素5和3(第5个元素,下标从1开始计)交换就会把元素3的稳定性打乱,所以快速排序是一个不稳定的排序算法,不稳定发生在中枢元素和a[j] 交换的时刻。所以是不稳定的

(7)堆排序:有可能第n / 2个父节点交换把后面一个元素交换过去了,而第n / 2 - 1个父节点把后面一个相同的元素没 有交换,那么这2个相同的元素之间的稳定性就被破坏了。所以,堆排序不是稳定的排序算法。

(8)计数排序:稳定

(9)桶排序:桶X内的所有元素,是一直有序的;插入排序是稳定的,因此桶内元素顺序也是稳定的;

(10)基数排序:基数是按照最低位排序,然后收集,然后按照最高位排序,以此类推,基数排序是高优先级的在前,低优先级的在后,

基数排序是分别排序,分别收集。所以是稳定的排序算法

 

7.堆除了用来排序还能用来做什么

查找第k大元素

优先队列

 

8.为什么堆排序和快排时间复杂度一样,但是都用快排不用堆排序呢?

1.对于同样的数据,在排序过程中,堆排序算法的数据交换次数要多于快速排序。排序有 有序度和逆序度两个概念。对于基于比较的排序算法来说,整个排序过程就是由两个基本的操作组成的,比较和交换(移动)。快速排序数据交换的次数不会比逆序度多。但是地排序的第一步是建堆,建堆过程会打乱数据原有的相对先后顺序,导致数据的有序度降低。比如,对于一组已经有序的数据来说,经过建堆之后,数据反而变得更无序了

2.堆排序数据访问的方式没有快速排序号,对于快速排序老说,数据是跳着访问的,而不是相对于快速排序那种,顺序访问,堆cpu缓存不友好

 

9.三个线程T1,T2和T3,线程T3必须等到T1,T2都结束之后才能开始,应该怎么做?

join:阻塞当前线程直到线程执行完毕

#include   
#include
#include
#include

using namespace std;


void print1() {
	for (int i = 0; i < 5; i++) {
		cout <<"thread1:"<< i << endl;
	}
}


void print2() {
	for (int j = 6; j < 10; j++) {
		cout << "thread2:" << j << endl;
	}
}

void print3() {
	for (int j = 11; j < 15; j++) {
		cout << "thread3:" << j << endl;
	}
}


int main(){
	
	thread t1(print1);
    thread t2(print2);
	

	t1.join();
	t2.join();

    thread t3(print3);
	t3.join();

	system("pause");
	return 0;
}

detach:Detach 线程。 将当前线程对象所代表的执行实例与该线程对象分离,使得线程的执行可以单独进行。一旦线程执行完毕,它所分配的资源将会被释放。

#include   
#include
#include
#include

using namespace std;


void print1() {
	for (int i = 0; i < 5; i++) {
		cout <<"thread1:"<< i << endl;
	}
}


void print2() {
	for (int j = 6; j < 10; j++) {
		cout << "thread2:" << j << endl;
	}
}

void print3() {
	for (int j = 11; j < 15; j++) {
		cout << "thread3:" << j << endl;
	}
}


int main(){
	
	thread t1(print1);
    thread t2(print2);
	

	t1.detach();
	t2.detach();

    thread t3(print3);
	t3.detach();

	system("pause");
	return 0;
}

10.线程T1有三个代码段,线程T2有两个代码段,线程T3必须等待T1执行完第二个代码段,T2执行完第一个代码段之后才能开始,应该怎么做?

#include 
#include 
#include 
#include

/* 屏障控制块 */
pthread_barrier_t barrier;
using namespace std;

/*线程1入口函数*/
void* thread1_entry(void* parameter)
{

        //代码段1
        for(int i=0;i<5;i++){
              cout<<"thread1 code1:"<

 

 

11.8个运动员,1个发令员,1个裁判员,发令员要在8个运动员都准备好之后才能法令,运动员在发令员法令之后才能跑,裁判员要在8个运动员都跑完之后才能评分,如果每个人看作一个线程(一共10个线程)的话,这个系统该如何设计,可以保证这种顺序关系?

 

 

你可能感兴趣的:(面经答案,网络)