快手面经

目录

  • 堆和栈区别
    • 区别
      • **2.1申请方式,栈由系统自动分配,堆需要程序员自己申请**
      • 2.2 申请后系统的响应,栈只要剩余空间大直接分配,堆需要遍历记录空闲内存的链表
      • 2.3 申请大小的限制,栈由高地址向低地址扩展,最大容量预先规定。堆由低向高,不连续,受限于虚拟内存。
      • 2.4 申请效率的比较:
      • 2.6 存取效率的比较,为什么栈比堆快,栈有专门寄存器,直接寻址;堆是间接寻址
      • 碎片问题:栈先进后出的队列,不会有碎片,堆会有
    • 多线程堆栈
  • 系统调用原理,用户程序与内核的接口
  • linux权限管理
  • fork
  • 多个父子进程不断循环申请内存,总数量超出内存大小,会发生什么
  • 多个父子进程同时对同一个文件进行修改,发生什么
  • 客户端服务端最大连接数
  • 构造函数和析构函数可以调用虚函数吗
  • CSMA/CD
  • 分页
  • LRU  最近最久未使用
  • 多个请求并发
  • 类的四种构造函数
  • 拓扑排序
  • 同步异步理解
  • 协程
  • 哪些状态码
  • 红黑树和二叉平衡树区别
  • 超时重传
  • 去掉一个字符后查看是否是回文串
  • 快乐数
  • b树 b+树
  • b树 b+
  • 构造函数有哪些必须在初始化列表完成
  • 构造函数声明为protected
  • delete 如何确定删除多少空间
  • 四种类型转化
  • cpp内存分布
  • TCP中异常关闭链接的意义 异常关闭的情况

堆和栈区别

栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
2、堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。

堆栈是一个先进后出的数据结构,栈顶地址总是小于等于栈的基地址

区别

2.1申请方式,栈由系统自动分配,堆需要程序员自己申请

stack:
由系统自动分配。 例如,声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间
heap:
需要程序员自己申请,并指明大小,在c中malloc函数
如p1 = (char *)malloc(10);
在C++中用new运算符
如p2 = (char *)malloc(10);
但是注意p1、p2本身是在栈中的。

2.2 申请后系统的响应,栈只要剩余空间大直接分配,堆需要遍历记录空闲内存的链表

栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出
堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,
会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中

2.3 申请大小的限制,栈由高地址向低地址扩展,最大容量预先规定。堆由低向高,不连续,受限于虚拟内存。

栈:在Windows下**,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的**,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大

2.4 申请效率的比较:

栈由系统自动分配,速度较快。但程序员是无法控制的
堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.
另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的地址空间中保留一快内存,虽然用起来最不方便。但是速度快,也最灵活。

2.5 堆和栈中的存储内容
栈: 在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。
当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。

2.6 存取效率的比较,为什么栈比堆快,栈有专门寄存器,直接寻址;堆是间接寻址

char s1[] = “aaaaaaaaaaaaaaa”;
char *s2 = “bbbbbbbbbbbbbbbbb”;
aaaaaaaaaaa是在运行时刻赋值的;
而bbbbbbbbbbb是在编译时就确定的;

但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。
比如:

碎片问题:栈先进后出的队列,不会有碎片,堆会有

碎片问题:栈不会存在碎片问题,因为栈是先进后出的队列,内存块弹出栈之前,在其上面的后进的栈内容已弹出。而频繁申请释放操作会造成堆内存空间的不连续,从而造成大量碎片,使程序效率降低。

#include
void main() {
    char a = 1;
    char c[] = "1234567890";
    char *p ="1234567890";
    a = c[1];
    a = p[1];
    return;
}

栈在读取时直接就把字符串中的元素读到寄存器cl中,而堆则要先把指针值读到edx中,在根据edx读取字符,显然慢了。

cpu有专门的寄存器(esp,ebp)来操作栈,堆都是使用间接寻址的。栈快点

多线程堆栈

每个线程一个栈,每个进程一个堆。

系统调用原理,用户程序与内核的接口

系统调用指运行在使用者空间的程序向操作系统内核请求需要更高权限运行的服务。系统调用提供了用户程序与操作系统之间的接口(即系统调用是用户程序和内核交互的接口)。
另外,计算机硬件的资源是有限的,为了更好的管理这些资源,所有的资源都由操作系统控制,进程只能向操作系统请求这些资源。操作系统是这些资源的唯一入口,这个入口就是系统调用。

系统调用都是通过中断实现的

常用的系统调用:

  • 进程控制
    fork 创建一个新进程
    clone 按指定条件创建子进程

  • 文件系统控制
    1、文件读写操作
    fcntl 文件控制
    open 打开文件
    2、文件系统操作
    access 确定文件的可存取性
    chdir 改变当前工作目录

  • 系统控制
    ioctl I/O总控制函数
    time 取得系统时间

  • 内存管理

  • 网络管理

  • socket控制

  • 用户管理

  • 进程间通信

linux权限管理

Linux用户及权限管理
Linux操作系统对多用户的管理,是非常繁琐的,所以用组的概念来管理用户就变得简单,每个用户可以在一个独立的组,每个组也可以有零个用户或者多个用户。
Linux系统用户是根据用户ID来识别的,默认ID长度为32位,从默认ID编号从0开始,但是为了和老式系统兼容,用户ID限制在60000以下,Linux用户分总共分为三种,分别如下:

root用户 (ID 0)
系统用户 (ID 1-499)
普通用户 (ID 500以上)

Linux系统中的每个文件或者文件夹,都有一个所属用户及所属组,使用id命令可以显示当前用户的信息,使用passwd命令可以修改当前用户密码。Linux操作系统用户的特点如下:

每个用户拥有一个UserID,操作系统实际读取的是UID,而非用户名;

每个用户属于一个主组,属于一个或多个附属组,一个用户最多有31个附属组;

每个组拥有一个GroupID;

每个进程以一个用户身份运行,该用户可对进程拥有资源控制权限;

每个可登陆用户拥有一个指定的Shell环境。

Linux权限管理

Linux权限是操作系统用来限制对资源访问的机制,权限一般分为读、写、执行。系统中每个文件都拥有特定的权限、所属用户及所属组,通过这样的机制来限制哪些用户或用户组可以对特定文件进行相应的操作。

Linux每个进程都是以某个用户身份运行,进程的权限与该用户的权限一样,用户的权限越大,则进程拥有的权限就越大

Lnux中有的文件及文件夹都有至少权限三种权限,常见的权限如表5-1所示:
读,写,可执行
ls –l 查看peter.net目录的详细属性

Chmod用户及组权限
修改某个用户、组对文件夹的权限,用命令chmod实现,其中以代指ugo,、-、=代表加入、删除和等于对应权限

fork

一个可执行的完整的程序,包含有自己的代码正文区、数据存储区、进程执行过程中的上下文(程序计数器的值与寄存器内容)信息,总结来说就是由程序段、数据段、进程控制块(pcb)三部分组成

进程包含有一个进程控制块(PCB),PCB是进程唯一标识,其主要包含有:
(1)进程状态;
(2)进程标识信息(uid、gid);
(3)定时器;
(4)用户可见寄存器、控制状态寄存器、栈指针等;

(1)调用fork后会创建一个子进程,这个子进程相当于是父进程的一个副本,并对父进程的数据空间、堆、栈进行了备份,将备份存储在子进程对应的的地址
(2)备份中包含了父进程内设置的所有变量及锁等信息,关于子进程是否立即执行与获得的锁是否清除,这里暂且跳过;
(3)返回值方面呢,fork函数调用一次,会返回两次。两次返回的区别是:
若是父进程:返回新建子进程的进程号pid,正整数,大于0;
若是子进程,返回0。
PCB的通过链表来实现,目的应该是为了方便管理多个进程,方便删除、插入进程等
调用完fork后,此时会存在两个进程,新创建的子进程信息PCB控制信息被加入PCB链表,这时候执行顺序咱们来分析下:
(1)子进程创建后优先执行:此时父进程挂起,其指令寄存器中保存父进程要执行的下一条指令与寄存器数据,子进程从fork这一步后所执行的代码同父进程(因为它就是父进程的备份,程序正文是一样的),因为子进程中fork返回值为0(在PCB中可能存在区分父,子进程的标志,所以检查到是子进程就返回0),则执行等于0的条件;
(2)父进程未失去调度,继续执行:此时子进程挂起,指令寄存器保存的就是fork之后的下一条执行指令,寄存器保存下一个指令执行需要的数据(这个保存信息的与子进程先执行时父进程保存的信息应该是相同的),PCB判断这是父进程,fork返回子进程pid,执行大于0的条件;
(3)父、子进程均挂起:此时父、子进程各自的指令寄存器、数据寄存器保存的信息应该是相同的,都是fork后下一条指令;

“写时拷贝”。也就是当fork发生时,子进程根本不会去拷贝父进程的内存页面,而是与父进程共享。当子进程或父进程需要修改一个内存页面时,Linux就将这个内存页面复制一份给修改者,然后再去修改,这样从用户的角度看,父子进程根本就没有共享什么内存。COW也就是进程要写共享的内存页面,先复制再改写。

答案二:
下面参考这个
fork之后exec之前两个进程用的是相同的物理空间(内存区),子进程的代码段、数据段、堆栈都是指向父进程的物理空间,也就是说,两者的虚拟空间不同,但其对应的物理空间是同一个。当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间,如果不是因为exec,内核会给子进程的数据段、堆栈段分配相应的物理空间(至此两者有各自的进程空间,互不影响),而代码段继续共享父进程的物理空间(两者的代码完全相同)。而如果是因为exec,由于两者执行的代码不同,子进程的代码段也会分配单独的物理空间。
fork之后内核会通过将子进程放在队列的前面,以让子进程先执行,以免父进程执行导致写时复制,而后子进程执行exec系统调用,因无意义的复制而造成效率的下降

fork时子进程获得父进程数据空间、堆和栈的复制,所以变量的地址(当然是虚拟地址)也是一样的。

多个父子进程不断循环申请内存,总数量超出内存大小,会发生什么

我理解的是由于虚存的原因,并不会发生什么。

多个父子进程同时对同一个文件进行修改,发生什么

父子进程对文件的操作是共享方式。因为父进程的文件描述符表被拷贝给了子进程(具体的原理参虚拟存储器的内容,私有对象写时拷贝实现了父子进程之间形成相互独立的地址空间)

文件描述符
文件偏移量(文件指针)

由于父子进程是以共享的方式控制已经打开文件的,因此对文件的操作也是相互影响的,因此读写文件的位置也会发生相应的改变。父(子)进程的文件读写位置会随着子(父)进程的文件读写位置改变而改变,因为此时改变的是文件表的文件位置项,而文件表是所有进程共享的,任何一个进程的修改都会影响到别的进程。但是父(子)进程对描述符的修改不会影响子(父)进程的描述符,因为close(fd)的操作只是改变文件表述符表中的内容,而该表是每个进程相互独立的,因此不会改变其他进程的表

客户端服务端最大连接数

最大连接数是与文件描述符相关的,
client最大tcp连接数

client每次发起tcp连接请求时,除非绑定端口,通常会让系统选取一个空闲的本地端口(local port),该端口是独占的,不能和其他tcp连接共享。tcp端口的数据类型是unsigned short,因此本地端口个数最大只有65536,端口0有特殊含义,不能使用,这样可用端口最多只有65535,所以在全部作为client端的情况下,最大tcp连接数为65535,这些连接可以连到不同的server ip。

server最大tcp连接数

server通常固定在某个本地端口上监听,等待client的连接请求。不考虑地址重用(unix的SO_REUSEADDR选项)的情况下,即使server端有多个ip,本地监听端口也是独占的,
系统用一个4四元组来唯一标识一个TCP连接:{local ip, local port,remote ip,remote port}
因此server端tcp连接4元组中只有remote ip(也就是client ip)和remote port(客户端port)是可变的,因此最大tcp连接为客户端ip数×客户端port数,对IPV4,不考虑ip地址分类等因素,最大tcp连接数约为2的32次方(ip数)×2的16次方(port数),也就是server端单机最大tcp连接数约为2的48次方

构造函数和析构函数可以调用虚函数吗

可以,虚函数底层实现原理(但是最好不要在构造和析构函数中调用) 可以**,但是没有动态绑定的效果,父类构造函数中调用的仍然是父类版本的函数,子类中调用的仍然是子类版本的函数**。 effictive c++第九条,绝不在构造和析构过程中调用virtual,因为构造函数中的base的虚函数不会下降到derived上。而是直接调用base类的虚函数。绝不在构造和析构函数中调用virtual函数:因为构造函数中的base的虚函数不会下降到derived上。而是直接调用base类的虚函数。绝不在构造和析构函数中调用virtual函数:
a) 如果有继承,构造函数会先调用父类构造函数,而如果构造函数中有虚函数,此时子类还没有构造,所以此时的对象还是父类的,不会触发多态。更容易记的是基类构造期间,virtual函数不是virtual函数。
b) 析构函数也是一样,子类先进行析构,这时,如果有virtual函数的话,子类的内容已经被析构了,C++会视其父类,执行父类的virtual函数。
c) 总之,在构造和析构函数中,不要用虚函数。如果必须用,那么分离出一个Init函数和一个close函数,实现相关功能即可。

CSMA/CD

多路访问/冲突检测

发送数据前 先侦听信道是否空闲 ,若空闲,则立即发送数据。若信道忙碌,则等待一段时间至信道中的信息传输结束后再发送数据;

若在上一段信息发送结束后,同时有两个或两个以上的节点都提出发送请求,则判定为冲突。若侦听到冲突,则立即停止发送数据,等待一段随机时间,再重新尝试。

其原理简单总结为:先听后发,边发边听,冲突停发,随机延迟后重发。

分页

分页系统的核心在于:**将虚拟内存空间和物理内存空间皆划分为大小相同的页面,**如4KB、8KB或16KB等,并以页面作为内存空间的最小分配单位,一个程序的一个页面可以存放在任意一个物理页面里。

(1)解决空间浪费碎片化问题

由于将虚拟内存空间和物理内存空间按照某种规定的大小进行分配,这里我们称之为页(Page),然后按照页进行内存分配,也就克服了外部碎片的问题。

(2)解决程序大小受限问题

程序增长有限是因为一个程序需要全部加载到内存才能运行,因此解决的办法就是使得一个程序无须全部加载就可以运行。使用分页也可以解决这个问题,只需将当前需要的页面放在内存里,其他暂时不用的页面放在磁盘上,这样一个程序同时占用内存和磁盘,其增长空间就大大增加了。而且,分页之后,如果一个程序需要更多的空间,给其分配一个新页即可(而无需将程序倒出倒进从而提高空间增长效率)。

一个虚拟地址分成两个部分,一部分存储页面号,一部分存储偏移量。
虚拟地址映射_内存管理单元(mmu)完成
内存管理单元(MMU)管理着地址空间和物理内存的转换,其中的页表(Page table)存储着页(程序地址空间)和页框(物理内存空间)的映射表
快手面经_第1张图片

LRU  最近最久未使用

最简单的方式就是在页表的记录项里增加一个计数域,一个页面被访问一次,这个计数器的值就增加1。于是,当需要更换页面时,只需要找到计数域值最小的页面替换即可,该页面即是最近最少使用的页面。另一种简单实现方式就是用一个链表将所有页面链接起来,最近被使用的页面在链表头,最近未被使用的放在链表尾。在每次页面访问时对这个链表进行更新,使其保持最近被使用的页面在链表头

我们可以总结出 cache 这个数据结构必要的条件:查找快,插入快,删除快,有顺序之分

?哈希表查找快,但是数据无固定顺序;链表有顺序之分,插入删除快,但是查找慢。所以结合一下,形成一种新的数据结构:哈希链表。

快手面经_第2张图片
思想很简单,就是借助哈希表赋予了链表快速查找的特性嘛:可以快速查找某个 key 是否存在缓存(链表)中,同时可以快速删除、添加节点。

class LRUCache {
private:
    int cap;
    // 双链表:装着 (key, value) 元组
    list<pair<int, int>> cache;
    // 哈希表:key 映射到 (key, value) 在 cache 中的位置
	    unordered_map<int, list<pair<int, int>>::iterator> map;
public:
    LRUCache(int capacity) {
        this->cap = capacity; 
    }
    
    int get(int key) {
        auto it = map.find(key);
        // 访问的 key 不存在
        if (it == map.end()) return -1;
        // key 存在,把 (k, v) 换到队头
        pair<int, int> kv = *map[key];
        cache.erase(map[key]);
        cache.push_front(kv);
        // 更新 (key, value) 在 cache 中的位置
        map[key] = cache.begin();
        return kv.second; // value
    }
    
    void put(int key, int value) {

        /* 要先判断 key 是否已经存在 */ 
        auto it = map.find(key);
        if (it == map.end()) {
            /* key 不存在,判断 cache 是否已满 */ 
            if (cache.size() == cap) {
                // cache 已满,删除尾部的键值对腾位置
                // cache 和 map 中的数据都要删除
                auto lastPair = cache.back();
                int lastKey = lastPair.first;
                map.erase(lastKey);
                cache.pop_back();
            }
            // cache 没满,可以直接添加
            cache.push_front(make_pair(key, value));
            map[key] = cache.begin();
        } else {
            /* key 存在,更改 value 并换到队头 */
            cache.erase(map[key]);
            cache.push_front(make_pair(key, value));
            map[key] = cache.begin();
        }
    }
};

多个请求并发

在服务器端编程处理时,可以设置一个全局的队列变量来存储从客户端传过来的参数。这个全局队列存储了多个客户端发送过来的参数,处理程序要处理时直接从这个队列中读取就可以了。服务器端接受客户端请求向共享队列中写参数作为一个线程,读取队列中的参数并进行处理作为另一个线程。两个线程独立执行来提高处理的并发性
参考链接

类的四种构造函数

1、无参数构造函数
2、有参数构造函数
3、赋值构造函数(copy构造函数)
4、默认构造函数

class test
{
public:
test()
{
m_a = 1; m_b = 2;
cout << "这是无参数构造函数" << endl;
}
test(int a)
{
m_a = a;
cout << "这是有一个参数构造函数" << endl;
}
test(int a, int b)
{
m_a = a; m_b = b;
cout << "这是有两个参数构造函数" << endl;
}
test(const test& obj)
{
m_a = obj.m_a;
m_b = obj.m_b;
cout << "这是赋值构造函数" << endl;
}
private :
int m_a;
int m_b;
};

c++11 move

#include 
 
using namespace std;
 
class A
{
public:
    A(){cout<<"construct1..............."<<endl;}
    A& operator = (const A&&) {cout<<" operator move"<<endl;}
    A(const A&&){cout<<"move construct "<<endl;}
 
    A& operator = (const A&){cout<<"copy operator"<<endl;}
    A(const A&){cout<<"copy construct"<<endl;}
};
int main()
{   
    A a1 = A();  // 这里调用的到底是哪一个构造函数?
    A a2 = std::move(A()); // 调用什么呢?
       a2 = A();//调用operator =(const A&&)
   
    A&& a = A();//只调用默认构造
    return 0;
}

继承,析构函数

class A
{
public:
    A(){cout<<"A constructor"<<endl;}
    virtual ~A(){cout<<"A destructor"<<endl;}
};

class B: public A
{
public:
    B(){cout<<"B constructor"<<endl;}
    ~B(){cout<<"B destructor"<<endl;}
};

拓扑排序

#include
#include 
#include 
using namespace std;

/************************类声明************************/
class Graph
{
    int V;             // 顶点个数
    list<int> *adj;    // 邻接表
    queue<int> q;      // 维护一个入度为0的顶点的集合
    int* indegree;     // 记录每个顶点的入度
public:
    Graph(int V);                   // 构造函数
    ~Graph();                       // 析构函数
    void addEdge(int v, int w);     // 添加边
    bool topological_sort();        // 拓扑排序
};

/************************类定义************************/
Graph::Graph(int V)
{
    this->V = V;
    adj = new list<int>[V];

    indegree = new int[V];  // 入度全部初始化为0
    for(int i=0; i<V; ++i)
        indegree[i] = 0;
}

Graph::~Graph()
{
    delete [] adj;
    delete [] indegree;
}

void Graph::addEdge(int v, int w)
{
    adj[v].push_back(w); 
    ++indegree[w];
}

bool Graph::topological_sort()
{
    for(int i=0; i<V; ++i)
        if(indegree[i] == 0)
            q.push(i);         // 将所有入度为0的顶点入队

    int count = 0;             // 计数,记录当前已经输出的顶点数 
    while(!q.empty())
    {
        int v = q.front();      // 从队列中取出一个顶点
        q.pop();

        cout << v << " ";      // 输出该顶点
        ++count;
        // 将所有v指向的顶点的入度减1,并将入度减为0的顶点入栈
        list<int>::iterator beg = adj[v].begin();
        for( ; beg!=adj[v].end(); ++beg)
            if(!(--indegree[*beg]))
                q.push(*beg);   // 若入度为0,则入栈
    }

    if(count < V)
        return false;           // 没有输出全部顶点,有向图中有回路
    else
        return true;            // 拓扑排序成功
}

同步异步理解

进程同步:就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回,这时程序是出于阻塞的,只有接收到返回的值或消息后才往下执行其他的命令。

当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者

协程

协程(Coroutines)是一种比线程更加轻量级的存在,正如一个进程可以拥有多个线程一样,一个线程可以拥有多个协程。

协程不是被操作系统内核所管理的,而是完全由程序所控制,也就是在用户态执行。这样带来的好处是性能大幅度的提升,因为不会像线程切换那样消耗资源

协程不是进程也不是线程,而是一个特殊的函数,这个函数可以在某个地方挂起,并且可以重新在挂起处外继续运行。所以说,协程与进程、线程相比并不是一个维度的概念。

一个线程内可以由多个这样的特殊函数在运行,但是有一点必须明确的是,一个线程的多个协程的运行是串行的。

哪些状态码

1XX:消息
2XX:成功
3XX:重定向
4XX:请求错误
5XX、6XX:服务器错误

200 OK:客户端请求成功。
400 Bad Request:客户端请求有语法错误,不能被服务器所理解。
401 Unauthorized:请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用。
403 Forbidden:服务器收到请求,但是拒绝提供服务。
404 Not Found:请求资源不存在,举个例子:输入了错误的URL。
500 Internal Server Error:服务器发生不可预期的错误。
503 Server Unavailable:服务器当前不能处理客户端的请求,一段时间后可能恢复正常,举个例子:HTTP/1.1 200 OK(CRLF)。

红黑树和二叉平衡树区别

这些约束强制了红黑树的关键性质: 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这个树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树。

平衡二叉树的性质:

它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。这个方案很好的解决了二叉查找树退化成链表的问题,把插入,查找,删除的时间复杂度最好情况和最坏情况都维持在O(logN)。但是频繁旋转会使插入和删除牺牲掉O(logN)左右的时间,不过相对二叉查找树来说,时间上稳定了很多。

区别:

1、红黑树放弃了追求完全平衡,追求大致平衡,在与平衡二叉树的时间复杂度相差不大的情况下,保证每次插入最多只需要三次旋转就能达到平衡,实现起来也更为简单。

2、平衡二叉树追求绝对平衡,条件比较苛刻,实现起来比较麻烦,每次插入新节点之后需要旋转的次数不能预知。

超时重传

快手面经_第3张图片
 由于不同的网络情况不一样,不可能设置一样的RTO,实际中RTO是根据网络中的RTT(传输往返时间)来自适应调整的。

去掉一个字符后查看是否是回文串

首尾双指针i,j。
当i、j指向的元素相等时,同时往前走。知道碰头或者不相等。
当不相等时,判断去掉i或者去掉j之后的字符串是否为回文串。
快手面经_第4张图片

快乐数

//参考英文网站热评第一。这题可以用快慢指针的思想去做,有点类似于检测是否为环形链表那道题
//如果给定的数字最后会一直循环重复,那么快的指针(值)一定会追上慢的指针(值),也就是
//两者一定会相等。如果没有循环重复,那么最后快慢指针也会相等,且都等于1。

  public  bool IsHappy(int n)
        {
            int fast = n;
            int slow = n;
            do
            {
                slow = squareSum(slow);
                Console.Write(slow+"——————");
                fast = squareSum(fast);
                fast = squareSum(fast);
                Console.WriteLine(fast);
            }
            while (slow!=fast) ;
            if (fast==1)
            {
                return true;
            }
            else
            {
                return false;
            }
        }
        private int squareSum(int m)
        {
            int squaresum = 0;
            while (m != 0)
            {
                squaresum = squaresum + (m % 10) * (m % 10);
                //这一轮进来是为了算,每个位的平方和的。
                m = m / 10;
                //为了换位。
            }
            return squaresum;
        }

b树 b+树

  • 内积(点乘)的几何意义包括:

表征或计算两个向量之间的夹角
b向量在a向量方向上的投影

  • 叉积 ,法向量

  • 与点积不同,它的运算结果是一个向量而不是一个标量。并且两个向量的叉积与这两个向量和垂直
    叉乘的概念非常有用,可以通过两个向量的叉乘,生成第三个垂直于a,b的法向量,从而构建X、Y、Z坐标系。如下图所示:

  • 绕x轴旋转
    快手面经_第5张图片

  • 齐次坐标就是将一个原本是n维的向量用一个n+1维向量来表示

b树 b+

B 树相对于平衡二叉树,每个节点存储了更多的键值(key)和数据(data),并且每个节点拥有更多的子节点,子节点的个数一般称为阶,上述图中的 B 树为 3 阶 B 树,高度也会很低

①B+ 树非叶子节点上是不存储数据的,仅存储键值,而 B 树节点中不仅存储键值,也会存储数据。
之所以这么做是因为在数据库中页的大小是固定的,InnoDB 中页的默认大小是 16KB。
如果不存储数据,那么就会存储更多的键值,相应的树的阶数(节点的子节点树)就会更大,树就会更矮更胖,如此一来我们查找数据进行磁盘的 IO 次数又会再次减少,数据查询的效率也会更快。

②因为 B+ 树索引的所有数据均存储在叶子节点,而且数据是按照顺序排列的。

么 B+ 树使得范围查找,排序查找,分组查找以及去重查找变得异常简单。而 B 树因为数据分散在各个节点,要实现这一点是很不容易的。

构造函数有哪些必须在初始化列表完成

有const成员
有引用类型成员
成员对象没有默认构造函数
基类对象没有默认构造函数

构造函数声明为protected

如果你不想让外面的用户直接构造一个类(假设这个类的名字为A)的对象,而希望用户只能构造这个类A的子类,那你就可以将类A的构造函数/析构函数声明为protected,而将类A的子类的构造函数/析构函数声明为public

delete 如何确定删除多少空间

原来会在申请的空间首位置之前的4个字节内记录申请了多少空间(可以理解为数组下标为-1的位置,但是这样理解并不总是正确):


基础架构

四种类型转化

static_cast, dynamic_cast, const_cast, reinterpret_cast

1、const_cast

用于将const变量转为非const

2、static_cast

用于各种隐式转换,比如非const转const,void*转指针等, static_cast能用于多态向上转化如果向下转能成功但是不安全,结果未知

3、dynamic_cast

用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。向下转化时,如果是非法的对于指针返回NULL,对于引用抛异常。要深入了解内部转换的原理。

向上转换:指的是子类向基类的转换

向下转换:指的是基类向子类的转换

它通过判断在执行到该语句的时候变量的运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。

4、reinterpret_cast

几乎什么都可以转,比如将int转指针,可能会出问题,尽量少用

为什么不使用C的强制转换?

C的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错

由于 auto_ptr 基于排他所有权模式:两个指针不能指向同一个资源,复制或赋值都会改变资源的所有权。
在 STL 容器中无法使用auto_ptr ,因为容器内的元素必需支持可复制(copy constructable)和可赋值(assignable)

unique_ptr对auto_ptr的改进如下:
1, auto_ptr支持拷贝构造与赋值操作,但unique_ptr不直接支持,会报错。原来的auto_ptr对象就不再有效,这点不符合人的直觉。unique_ptr则直接禁止了拷贝构造与赋值操作。
2. unique_ptr可做为容器元素,虽然unique_ptr同样不能直接做为容器元素,但可以通过move语意实现。

我们知道指针或引用在离开作用域时是不会进行析构的,但是类在离开作用域时会自动执行析构函数,所以我们可以用一个类来实现指针指针(unique_ptr本质上是一个类,只是可以像一个指针一样使用)。因此我们可以通过析构函数调用delete去释放资源**。那么如何实现“独占”呢?我们可以在类中把拷贝构造函数和拷贝赋值声明为private,这样就不可以对指针指向进行拷贝了,也就不能产生指向同一个对象的指针。**

cpp内存分布

bss段什么作用,为什么要有bss段,什么时候完成初始化?(1. 自动初始化为0;2. 减小程序文件大小,程序加载时进行初始化,在exe程序文件中没有为其创建内存空间,仅存储变量符号和类型)

bbs段是未初始化的数据,不占用磁盘空间,是在程序执行前由内核初始化完成

TCP中异常关闭链接的意义 异常关闭的情况

参考

终止一个连接的正常方式是发送FIN。 在发送缓冲区中 所有排队数据都已发送之后才发送FIN,正常情况下没有任何数据丢失。

但我们有时也有可能发送一个RST报文段而不是F IN来中途关闭一个连接。这称为异常关闭.

值得注意的是RST报文段不会导致另一端产生任何响应,另一端根本不进行确认。收到RST的一方将终止该连接。

当TCP连接的进程在忘记关闭Socket而退出、程序崩溃、或非正常方式结束进程的情况下(Windows客户端),会导致TCP连接的对端进程产生“104: Connection reset by peer”(Linux下)或“10054: An existing connection was forcibly closed by the remote host”(Windows下)错误

当TCP连接的进程机器发生死机、系统突然重启、网线松动或网络不通等情况下,连接的对端进程可能检测不到任何异常,并最后等待“超时”才断开TCP连接

当TCP连接的进程正常关闭Socket时,对端进程在检查到TCP关闭事件之前仍然向TCP发送消息,则在Send消息时会产生“32: Broken pipe”(Linux下)或“10053: An established connection was aborted by the software in your host machine”(Windows下)错误。

当TCP连接的对端进程已经关闭了Socket的情况下,本端进程再发送数据时,第一包可以发送成功(但会导致对端发送一个RST包过来):
之后如果再继续发送数据会失败,错误码为“10053: An established connection was aborted by the software in your host machine”(Windows下)或“32: Broken pipe,同时收到SIGPIPE信号”(Linux下)错误;
之后如果接收数据,则Windows下会报10053的错误,而Linux下则收到正常关闭消息

TCP连接的本端接收缓冲区中还有未接收数据的情况下close了Socket,则本端TCP会向对端发送RST包,而不是正常的FIN包,这就会导致对端进程提前(RST包比正常数据包先被收到)收到“10054: An existing connection was forcibly closed by the remote host”(Windows下)或“104: Connection reset by peer”(Linux下)错误

  • top指令
    top   //每隔5秒显式所有进程的资源占用情况
    top -d 2 //每隔2秒显式所有进程的资源占用情况
    top -c //每隔5秒显式进程的资源占用情况,并显示进程的命令行参数(默认只有进程名)
    top -p 12345 -p 6789//每隔5秒显示pid是12345和pid是6789的两个进程的资源占用情况
    top -d 2 -c -p 123456 //每隔2秒显示pid是12345的进程的资源使用情况,并显式该进程启动的命令行参

情景题,100万的数取top k,堆的底层实现,堆排序复杂度。(建立堆O(n),堆调整O(nlogn))
求一亿个数据中最大的100个数.
这个问题一脸懵逼我.
后来查了资料说使用HASH函数以及分治的思想来解决**.将这1亿个数根据HASH去重然后根据hash值分别存储到1000个分区内,然后每个分区都使用一个容量为100的最小堆得到每个区最大的100个数.
最后将1000个分区内得到的最小堆再合并处理即可.**

海量数据的前n大/前n小/Top k问题
海量数据求top k的问题要用到容器和堆

大根堆/小根堆
大根堆(求最小的前n个数)

小根堆(求最大的前n个数)

堆的实质其实就是一颗完全二叉树

最大堆特点:父节点值均大于子节点;(堆顶元素最大)

最小堆特点:父节点值均小于子节点;(堆顶元素最小)

总结:找top最大的用小根堆;找top最小的用大根堆

最大堆找top k小代码的基本思路
求前n个最小的数 先建立最大堆 把海量数据向最大堆添加n个数,然后堆的内部会自动排序 我们不用管它的内部是怎么操作的 接下来我们往最大堆进行插入操作 因为最大堆的特点就是堆顶元素最大 若插入元素大于堆顶元素 则它大于堆中任何一个元素 所以摒弃该元素 遍历下一个元素 若插入元素小于堆顶元素则进行插入 插入后堆内自动排序 堆顶元素又成为该堆的最大元素 原堆顶元素出堆 持续遍历完 得到的就是所有元素中最小的前n个数

最小堆找top k大代码的基本思路
求前n个最大的数 先建立最小堆 把海量数据向最小堆添加n个数,堆的内部会自动排序 不用管它的内部是什么操作 然后我们往最小堆进行插入操作 因为最小堆的特点就是堆顶元素最小 若插入元素小于堆顶元素 则它小于堆中任何一个元素 所以摒弃该元素 遍历下一个元素 若插入元素大于堆顶元素则进行插入 插入后堆内自动排序 堆顶元素又成为该堆的最小元素 原堆顶元素出堆 持续遍历完 得到的就是所有元素中最大的前n个数

int main()
{
	//在最短时间内找到所有整数中最大/最小的n个数并且打印;
	//找top最大的用小根堆;找top最小的用大根堆
	//时间复杂度 O(n)*log2 n
	//===================最大堆找最小值======================
	vector<unsigned int>vec;
	for (unsigned int i = 0; i < 20000; ++i)
	{
		vec.push_back(rand() + i); //随机产生两万个数据放在vector容器中
	}
	priority_queue<int> maxHeap; //声明最大堆
	int k = 10;//求20000个数据中前10小个数
	for (int i = 0; i < k; i++)
	{
		maxHeap.push(vec[i]);//先往最大堆中放10个数
	}
	for (int i = k; i < 20000; ++i)
	{
	    //遍历判断容器中剩余的每个数与堆顶元素的大小
	    //若小于堆顶元素 则把堆顶元素出堆 把该元素入堆
		if (vec[i] < maxHeap.top())
		{
			maxHeap.pop();
			maxHeap.push(vec[i]);
		}
	}
	while (!maxHeap.empty())
	//打印最大堆里边的十个最小的数
	{
		cout << maxHeap.top() << " ";
		maxHeap.pop();
	}
	cout << endl;

//==========================================================
//==========================最小堆找最大值===================
	for (unsigned int i = 0; i < 20000; ++i)
	{
		vec.push_back(rand() + i);
	}
	priority_queue<int, vector<int>, greater<int>> minHeap; //建立最小堆
	//注意:最小堆要加上greater
	int j = 10;
	for (int i = 0; i < j; i++)
	{
		minHeap.push(vec[i]);
	}
	for (int i = j; i < 20000; ++i)
	{
	    //遍历判断容器中剩余的每个数与堆顶元素的大小
	    //若大于堆顶元素 则把堆顶元素出堆 把该元素入堆
		if (vec[i] > minHeap.top())
		{
			minHeap.pop();
			minHeap.push(vec[i]);
		}
	}
	while (!minHeap.empty())
	{
		//打印最大堆里边的十个最小的数
		cout << minHeap.top() << " ";
		minHeap.pop();
	}
	cout << endl;
	return 0}

  • 函数返回局部变量
    1.局部变量的特点:随函数调用时创建,随函数结束时析构(销毁)。

2.如果函数内部有用运算符new 申请的堆空间,是可以返回的。

3.函数不能通过返回指向栈内存的指针。

你可能感兴趣的:(面试)