备战百度笔试(C++后端开发学习日记番外篇)

2022.4.18 day 10 - 2022.4.19 day 11

前言

周一周二是复习的番外篇,另行总结如下。

八股文

声明:此部分参考徐学长的面试基础学习笔记

一、数据结构

AVL树

AVL 树是一种平衡二叉树,得名于其发明者的名字( Adelson-Velskii 以及 Landis)。AVL树是平衡二叉查找树,增加和删除节点后通过树形旋转重新达到平衡。右旋是以某个节点为中心,将它沉入当前右子节点的位置,而让当前的左子节点作为新树的根节点,也称为顺时针旋转。同理左旋是以某个节点为中心,将它沉入当前左子节点的位置,而让当前的右子节点作为新树的根节点,也称为逆时针旋转。

红黑树

红黑树是1972年发明的,称为对称二叉B树,1978年正式命名红黑树。主要特征是在每个节点上增加一个属性表示节点颜色,可以红色或黑色,红黑树和AVL树类似,都是在进行插入和删除时通过旋转保持自身平衡,从而获得较高的查找性能。与AVL树相比,红黑树不追求所有递归子树的高度差不超过1,保证从根节点到叶尾的最长路径不超过最短路径的2倍,所以最差时间复杂度是O(logn)。红黑树通过重新着色和左右旋转,更加高效地完成了插入和删除之后的自平衡调整。

红黑树在本质上还是二叉查找树,它额外引入了5个约束条件:

  • 节点只能是红色或黑色

  • 根节点必须是黑色

  • 所有NIL节点都是黑色的

  • 一条路径上不能出现相邻的两个红色节点

  • 在任何递归子树中,根节点到叶子节点的所有路径上包含相同数目的黑色节点

这五个约束条件保证了红黑树的新增、删除、查找的最坏时间复杂度均为O(logn)。如果一个树的左子节点和右子节点不存在,则均认定为黑色。红黑树的任何旋转在3次之内均可完成。

AVL树和红黑树的区别

红黑树的平衡性不如AVL树,它维持的只是一种大致的平衡,不严格保证左右子树的高度差不超过1。这导致节点数相同的情况下,红黑树的高度可能更高,也就是说平均查找次数会高于相同情况的AVL树。

在插入时,红黑树和AVL树都能在至多两次旋转内恢复平衡,在删除时由于红黑树只追求大致平衡,因此红黑树至多三次旋转可以恢复平衡,而AVL树最多需要O(logn)次。AVL树在插入和删除时,将向上回溯确定是否需要旋转,这个回溯的时间成本最差为O(logn),而红黑树每次向上回溯的步长为2,回溯成本低。因此面对频繁地插入与删除,红黑树更加合适。

数据库的索引为什么要用B+树,为什么不用红黑树或者B树?

B+树是一种特殊的平衡多路树,是B树的优化改进版本,它把所有的数据都存放在叶节点上,中间节点保存的是索引。这样一来相对于B树来说,减少了数据对中间节点的空间占用,使得中间节点可以存放更多的指针,使得树变得更矮,深度更小,从而减少查询的磁盘IO次数,提高查询效率。另一个是由于叶节点之间有指针连接,所以可以进行范围查询,方便区间访问。

而红黑树是二叉的,它的深度相对B+树来说更大,更大的深度意味着查找次数更多,更频繁的磁盘IO,所以红黑树更适合在内存中进行查找。

B树和B+树的区别

B树中每个节点同时存储key和data,而B+树中只有叶子节点才存储data,非叶子节点只存储key。且B+树的叶子节点上增加了一个指向相邻叶子节点的链表指针,形成了带有顺序指针的B+树,提高了区间访问的性能。由于B+树的数据都存储在叶子节点中,分支节点均为索引,方便扫库,只需要扫一遍叶子节点即可,但是B树因为其分支节点同样存储着数据,我们要找到具体的数据,需要进行一次中序遍历按序来扫,所以B+树更加适合在区间查询的情况,所以通常B+树用于数据库索引,而B树则常用于文件索引。

B+树的优点在于:

1、 由于B+树在非叶子节点上不含数据信息,因此在内存页中能够存放更多的key,数据存放得更加紧密,具有更好的空间利用率,访问叶子节点上关联的数据也具有更好的缓存命中率。

2、 B+树的叶子节点都是相连的,因此对整棵树的遍历只需要一次性遍历叶子节点即可。而B树则需要进行每一层的递归遍历,相邻的元素可能在内存中不相邻,所以缓存命中性没有B+树好。

但是B树也有优点,由于每个节点都包含key和value,因此经常访问的元素可能离根节点更近,访问也更迅速。

B-树,一个m阶的B树具有如下几个特征:

在这里插入图片描述

1、 根节点至少有两个子女

2、 每个中间节点都包含k-1个元素和k个孩子,其中m/2 <= k <= m

3、 每一个叶子节点都包含k-1个元素,其中m/2 <= k <= m

4、 所有的叶子节点都位于同一层

5、 每个节点中的元素从小到大排序,节点当中k-1个元素正好是k个孩子包含的元素的值域分划。

B-树在查询中的比较次数其实不比二叉查找树少,尤其当单一节点中的元素数量很多时。但是相比磁盘IO的速度,内存中的比较耗时几乎可以忽略,所以只要树的高度足够低,IO次数足够少,就可以提升查找性能。相比之下节点内部元素多一些也没有关系,仅仅是多了几次内存交互,只要不超过磁盘页的大小即可。这就是B-树的优势之一。

B+树,一个m阶的B+树具有如下几个特征:

在这里插入图片描述

1、 有k个子树的中间节点包含有k个元素(B树中是k-1个元素),每个元素不保存数据,只用来索引,所有数据都保存在叶子节点。

2、 所有的叶子节点中包含了全部元素的信息,及指向含这些元素记录的指针,且叶子节点本身依关键字的大小自小而大顺序链接。

3、 所有的中间节点元素都同时存在于子节点中,在子节点元素中是最大(或最小)元素。

需要注意的是,根节点的最大元素,也就等同于整个B+树的最大元素。以后无论插入删除多少元素,始终要保持最大元素在根节点当中。

在B-树中,无论中间节点还是叶子节点都带有卫星数据。而在B+树中,只有叶子节点带有卫星数据,其余中间节点仅仅是索引,没有任何数据关联。

  • 首先,B+树的中间节点没有卫星数据,所以同样大小的磁盘页可以容纳更多的节点元素。这就意味着,数据量相同的情况下,B+树的结构比B-树更加“矮胖”,因此查询时IO次数也更少。

  • 其次,B+树的查询必须最终查找到叶子节点,而B-树只要找到匹配元素即可,无论匹配元素处于中间节点还是叶子节点。因此,B-树的查找性能并不稳定(最好情况是只查根节点,最坏情况是查到叶子节点)。而B+树的每一次查找都是稳定的。

  • 最后,B-树的范围查询只能依靠繁琐的中序遍历。而B+树只要先找到下限,再通过链表指针遍历就可以了。

B+树的优点:

1、 单一节点存储更多的元素(这样该节点下分支变多了,树变矮胖了),使得查询的IO次数更少。

2、 所有查询都要查找到叶子节点,查询性能稳定。

3、 所有叶子节点形成有序链表,便于范围查询。

快速排序(分治思想)

  • 选pivot,i = 0
  • j = size - 1,将比pivot小的数放在pivot,j–,大于或等于它的数就留在右边
  • 再对左右区间重复第二步,直到各区间只有一个数
//快速排序
void quick_sort(int s[], int l, int r)
{
    if (l < r)
    {
        //Swap(s[l], s[(l + r) / 2]); //将中间的这个数和第一个数交换 参见注1
        int i = l, j = r, x = s[l];
        while (i < j)
        {
            while(i < j && s[j] >= x) // 从右向左找第一个小于x的数
                j--;  
            if(i < j) 
                s[i++] = s[j];  
            while(i < j && s[i] < x) // 从左向右找第一个大于等于x的数
                i++;  
            if(i < j) 
                s[j--] = s[i];
        }
        s[i] = x;
        quick_sort(s, l, i - 1); // 递归调用 
        quick_sort(s, i + 1, r);
    }
}

归并排序(分治思想)

归并排序是用分治思想,分治模式在每一层递归上有三个步骤:

  • 分解(Divide):将n个元素分成个含n/2个元素的子序列。
  • 解决(Conquer):用合并排序法对两个子序列递归的排序。
  • 合并(Combine):合并两个已排序的子序列已得到排序结果。

递归法

  • 开辟一个辅助数组用来存放临时结果,然后将其给原数组赋值
template<typename T>
void merge_sort_recursive(T arr[], T reg[], int start, int end) {
    if (start >= end)
        return;
    int len = end - start, mid = (len >> 1) + start;//移位操作,等效为除以2
    int start1 = start, end1 = mid;
    int start2 = mid + 1, end2 = end;//将一个数组划分成两个子区间,分别调用归并排序
    merge_sort_recursive(arr, reg, start1, end1);
    merge_sort_recursive(arr, reg, start2, end2);
    int k = start;
    while (start1 <= end1 && start2 <= end2)//将两个数组较小的值填入辅助数组
        reg[k++] = arr[start1] < arr[start2] ? arr[start1++] : arr[start2++];
    while (start1 <= end1)//第二块子区间元素已填完,将第一子区间整体填入辅助数组
        reg[k++] = arr[start1++];
    while (start2 <= end2)//第一块子区间元素已填完,将第二子区间整体填入辅助数组
        reg[k++] = arr[start2++];
    for (k = start; k <= end; k++)//给原数组赋值
        arr[k] = reg[k];
}

// merge_sort
template<typename T>
void merge_sort(T arr[], const int len) {
    T reg[len];//开辟临时数组
    merge_sort_recursive(arr, reg, 0, len - 1);
}

堆排序

在堆的数据结构中,堆中的最大值总是位于根节点(在优先队列中使用堆的话堆中的最小值位于根节点)。堆中定义以下几种操作:

最大堆调整(Max_Heapify):将堆的末端子节点作调整,使得子节点永远小于父节点
创建最大堆(Build_Max_Heap):将堆所有数据重新排序
堆排序(Heap_Sort):移除位在第一个数据的根节点,并做最大堆调整的递归运算

#include <iostream>
#include <algorithm>
using namespace std;

void max_heapify(int arr[], int start, int end) {
    // 建立父節點指標和子節點指標
    int dad = start;
    int son = dad * 2 + 1;
    while (son <= end) { // 若子節點指標在範圍內才做比較
        if (son + 1 <= end && arr[son] < arr[son + 1]) // 先比較兩個子節點大小,選擇最大的
            son++;
        if (arr[dad] > arr[son]) // 如果父節點大於子節點代表調整完畢,直接跳出函數
            return;
        else { // 否則交換父子內容再繼續子節點和孫節點比較
            swap(arr[dad], arr[son]);
            dad = son;
            son = dad * 2 + 1;
        }
    }
}

void heap_sort(int arr[], int len) {
    // 初始化,i從最後一個父節點開始調整即len/2,-1是因为数组下标从0开始
    for (int i = len / 2 - 1; i >= 0; i--)
        max_heapify(arr, i, len - 1);
    // 先將第一個元素和已经排好的元素前一位做交換,再從新調整(刚调整的元素之前的元素),直到排序完畢
    for (int i = len - 1; i > 0; i--) {
        swap(arr[0], arr[i]);//大顶堆,最后的输出是升序排列
        max_heapify(arr, 0, i - 1);
    }
}

int main() {
    int arr[] = { 3, 5, 3, 0, 8, 6, 1, 5, 8, 6, 2, 4, 9, 4, 7, 0, 1, 8, 9, 7, 3, 1, 2, 5, 9, 7, 4, 0, 2, 6 };
    int len = (int) sizeof(arr) / sizeof(*arr);
    heap_sort(arr, len);
    for (int i = 0; i < len; i++)
        cout << arr[i] << ' ';
    cout << endl;
    return 0;
}

折半查找

class Solution {
public:
    int search(vector<int>& nums, int target) {
        int left = 0;
        int right = nums.size() - 1; // 定义target在左闭右闭的区间里,[left, right]
        while (left <= right) { // 当left==right,区间[left, right]依然有效,所以用 <=
            int middle = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2
            if (nums[middle] > target) {
                right = middle - 1; // target 在左区间,所以[left, middle - 1]
            } else if (nums[middle] < target) {
                left = middle + 1; // target 在右区间,所以[middle + 1, right]
            } else { // nums[middle] == target
                return middle; // 数组中找到目标值,直接返回下标
            }
        }
        // 未找到目标值
        return -1;
    }
};

关键路径

整个工程只有一个开始点和一个完成点,所以在正常的情况(无环)下,网中只有一个入度为零的点,称为源点,也只有一个出度为零的点,称为汇点。在整个AOE(Activity On Edge)网中,一条路径各弧上的权值之和称为该路径的带权路径长度。要估算整项工程完成的最短时间,就是要找一条从源点到汇点的 带权路径最长 的路径,称为关键路径。关键路径上的活动叫做关键活动,这些活动是影响工程进度的关键,他们提前或拖延将使整个工程提前或拖延。

在这里插入图片描述

  • 若网中有多条关键路径,则需要加快同时在几条关键路径上的活动。
    如:a6、a9、a7、a10同时加速。

  • 如果一个活动处于所有的关键路径上,那么提高这个活动的速度,才能缩短整个工程的完成时间。
    如:a0、a3加速

  • 处于所有关键路径上的活动完成时间不能缩短太多,否则会是的原来的关键路径变成非关键路径。这时,必须重新寻找关键路径。
    如:a0 由 6天变成 1天,就会改变关键路径。

二、计算机网络

名词

  • OSI(Open System Interconnection,开放式系统互联)
  • HTTP协议 (超文本传输协议HyperText Transfer Protocol)
  • TCP协议(传输控制协议,Transmission Control Protocol)
  • IP协议(Internet Protocol,互联网协议)
  • UDP协议, User Datagram Protocol, 中文名是用户数据报协议

OSI七层模型

国际标准化组织ISO提出了OSI开放互连的七层计算机网络模型,从上到下分别是应用层、表示层、会话层、运输层、网络层、数据链路层和物理层。OSI模型的概念清楚,理论也比较完善,但是既复杂又不实用。还有一种是TCP/IP体系结构,它分为四层,从上到下分别是应用层、运输层、网际层和网络接口层,不过从实质上将只有三层,因为最下面的网络接口层并没有什么具体内容。因特网的协议栈使用一种五层的模型结构,从上到下依次是应用层、运输层、网络层、链路层和物理层,其中下层是为上层提供服务的,每层执行某些动作或使用下层的服务来提高服务。

  • 物理层

物理层负责将信息编码成电流脉冲或其它信号用于网上传输。

RJ45等将数据转化成0和1

  • 数据链路层

数据链路层通过物理网络链路提供数据传输。不同的数据链路层定义了不同的网络和协议特征,其中包括物理编址、网络拓扑结构、数据校验、数据帧序列以及流控。

可以简单的理解为:规定了0和1的分包形式,确定了网络数据包的形式。

  • 网络层

网络层负责在源和终点之间建立连接。

可以理解为,此处需要确定计算机的位置,怎么确定?IPV4,IPV6!

  • 传输层

传输层向高层提供可靠的端到端的网络数据流服务。

可以理解为:每一个应用程序都会在网卡注册一个端口号,该层就是端口与端口的通信!常用的(TCP/IP)协议。

  • 会话层

会话层建立、管理和终止表示层与实体之间的通信会话。

建立一个链接(自动的手机信息、自动的网络寻址)

  • 表示层

表示层提供多种功能用于应用层数据编码和转化,以确保以一个系统应用层发送的信息可以被另一个系统应用层识别。

可以理解为:解决不同系统之间的通信,Linux下的QQ和Windows下的QQ可以通信

  • 应用层

OSI的应用层协议包括文件的传输、访问及管理协议(FTAM),以及文件虚拟终端协议(VIP)和公用管理系统信息(CMIP)等。

规定数据的传输协议

TCP特点

TCP是面向连接的运输层协议,一个应用进程在向另一个进程发送数据之前,两个进程必须先建立TCP连接,发送某些预备报文段,建立确保数据传输的参数。作为TCP连接建立的一部分,连接双方都将初始化与TCP连接相关的许多状态变量。这种连接不是电路交换网络中的端到端电路这种物理连接,而是一种逻辑连接,TCP报文要先传送到IP层加上IP首部后,再传到数据链路层,加上数据链路层的首部和尾部后才离开主机发送到物理层。

TCP连接提供全双工服务,允许通信双方的应用进程在任何时候都能发送数据。TCP连接的两端都有各自的发送缓存和接收缓存,用来临时存放通信数据。在发送前,应用程序把数据传送给TCP缓存后就可以做自己的事,而TCP在合适的时候会把数据发送出去。在接收时,TCP把收到的数据放入缓存,上层应用程序会在合适的时候读取缓存数据。

TCP连接是点对点的,每一条TCP连接只能有两个端点,即只能是单个发送方和单个接收方之间的连接。

TCP提供可靠的交付服务,通过TCP连接传送的数据无差错、不丢失、不重复,按序到达。

TCP是面向字节流的,流是指流入到进程或从进程中流出的字节序列。面向字节流的含义是:虽然应用程序和TCP的交互是一次一个数据块,但是TCP把应用程序交下来的数据仅仅看成一连串无结构的字节流。TCP不保证接收方应用程序收到的数据块和发送方应用程序发出的数据块具有对应大小的关系,但是接收方应用程序收到的字节流必须和发送方发出的字节流完全一样。接收方应用程序必须有能力识别收到的字节流,并把它还原成有意义的应用层数据。

TCP可靠原理

TCP的可靠传输包含很多机制,例如使用检验和来检测一个传输分组中的比特错误、使用定时器来用于超时重传一个分组、使用序号来检测丢失的分组和冗余副本、使用确认来告诉发送方确认的分组信息、使用否定确认来告诉发送方某个分组未被正确接收。

TCP有三次握手建立连接,四次挥手关闭连接的机制。除此之外还有滑动窗口和拥塞算法。最关键的是还保留超时重传的机制。对于每份报文也存在校验,保证每份报文可靠性。

除此之外,TCP还是用流量控制拥塞控制来保证可靠性。

流量控制

如果某个应用程序读取数据的速度较慢,而发送方发送得太多、太快,发送的数据就会很容易使连接的接收缓存溢出,TCP 为它的应用程序提供了流量控制以消除发送方使接收方缓存溢出的可能性。流量控制是一个速度匹配服务,即发送方的发送速率与接收方的应用程序读取速率相匹配。

拥塞控制

网络中对资源需求超过了资源可用量的情况就叫做拥塞。当吞吐量明显小于理想的吞吐量时就出现了轻度拥塞,当吞吐量随着负载的增加反而下降时,网络就进入了拥塞状态。当吞吐量降为 0 时,网络已无法正常工作并陷入死锁状态。拥塞控制就是尽量减少注入网络的数据,减轻网络中的路由器和链路的负担。拥塞控制是一个全局性的问题,它涉及网络中的所有路由器和主机,而流量控制只是一个端到端的问题,是两个端点之间通信量的控制

位码,即tcp标志位,有6种标示

  • SYN(synchronous建立联机)

  • ACK(acknowledgement 确认)

  • PSH(push传送)

  • FIN(finish结束)

  • RST(reset重置)

  • URG(urgent紧急)

会产生Sequence number(顺序号码)和Acknowledge number(确认号码)

TCP的三次握手(客户端会稍早于服务器端建立连接)

TCP是全双工通信,任何一方都可以发起建立连接的请求,假设A是客户端,B是服务器。

初始A和B均处于CLOSE状态,B会创建传输进程控制块TCB并进入LISTEND状态,监听端口是否收到了TCP请求以便及时响应。

第一次握手:客户端A发送位码为SYN=1,随机产生seq number=1234567的数据包到服务器B,B由SYN=1知道,客户端A要建立联机。此时客户端进入SYN发送状态,等待服务器确认。

第二次握手:服务器B收到请求后要确认联机信息,向A发送ack number=(客户端A的seq+1),SYN=1,ACK=1,随机产生seq=7654321的包。此时服务器进入SYN接收状态。

第三次握手:主机A收到后检查ack number是否正确,即第一次发送的seq number+1,以及位码ACK是否为1,若正确,客户端A会再发送ack number=(服务器B的seq+1),ACK=1(此时A的连接已经建立,可随即向B发送数据帧),服务器B收到后确认seq值与ACK=1则连接建立成功(B的连接稍晚建立)。客户端和服务器进入ESTABLISHED建立成功状态,完成三次握手。

三次握手的原因主要有两个目的,信息对等和防止超时

从信息对等的角度看,双方只有确定4类信息才能建立连接,即客户端A和服务器B分别确认自己和对方的发送和接受能力正常。在第二次握手后,从服务器B的角度看不能确定自己的发送能力和对方的接受能力,只有在第三次握手后才能确认。

三次握手也是防止失效连接突然到达导致脏连接,网络报文的生存时间往往会超过TCP请求超时时间,客户端A的某个超时连接请求可能会在双方释放连接之后到达服务器B,B会误以为是A创建了新的连接请求,然后发送确认报文创建连接。因为客户端A的状态不是SYN发送状态,所以直接丢弃了B的确认数据。如果是两次握手,连接已经建立了,服务器资源被白白浪费。如果是三次握手,B由于长时间没有收到确认信息,最终超时导致创建连接失败,因此不会出现脏连接。

挥手中重复分组的解释:

TCP分节可能由于路由器异常而“迷途”,在迷途期间,TCP发送端可能因确认超时而重发这个分节,迷途的分节在路由器修复后也会被送到最终目的地,这个迟到的迷途分节到达时可能会引起问题。在关闭“前一个连接”之后,马上又重新建立起一个相同的IP和端口之间的“新连接”,“前一个连接”的迷途重复分组在“前一个连接”终止后到达,而被“新连接”收到了。为了避免这个情况,TCP协议不允许处于TIME_WAIT状态的连接启动一个新的可用连接,因为TIME_WAIT状态持续2MSL,就可以保证当成功建立一个新TCP连接的时候,来自旧连接重复分组已经在网络中消逝。

TCP的四次挥手(服务器端会稍早于客户端释放连接)

TCP断开连接通常是由一方主动,一方被动的,这里我们假设客户端主动,服务器端被动

第一次挥手:当客户端没有数据要发送给服务端了,他会给服务端发送一个FIN报文,其中FIN=1,seq=u(客户端最后发送的一个序号+1),(告诉服务器,我已经没有数据要发送给你了,但是你要是还想给我发数据的话,你就接着发,但是你得告诉我你收到我的关闭信息了),发送完之后进入FIN-WAIT-1状态。

第二次挥手:当服务器收到客户端发来的FIN报文后(告诉客户端“我收到你的FIN消息了,但是你得等我发完的”),此时给客户端返回一个确认报文,ACK=1,ack number=u+1,seq=v,v为服务器之前发送的最后一个序号+1。此时客户端进入FIN-WAIT-2状态,服务器进入CLOSE-WAIT状态,但连接并未完全释放。

第三次挥手:当服务器发完所有数据时,他会给客户端发送一个FIN报文,(告诉客户端“我数据发完了,现在要关闭连接了”),FIN=1,ACK=1,ack number = u+1,seq=w(seq不是v的原因是在关闭状态服务器可能又发送了一些数据),之后服务器进入LAST_ACK状态,等着客户端最后的ACK信息。

第四次挥手:当客户端收到这个FIN报文后,必须发出确认(即给服务器发ACK信息,但是它不相信网络,怕服务器收不到信息,他会进入TIME-WAIT状态,万一服务器没收到ACK消息它可以重传),ACK=1,ack number = w+1,seq = u+1,发送完之后进入TIME-WAIT状态,而当服务器收到这个ACK消息后,就正式关闭了tcp连接,处于CLOSED状态。而客户端在等待2MSL(最长报文段寿命)之后进入CLOSED状态(客户端等待了这么长时间后还没等到消息,它知道服务器已经关闭连接了,于是乎它自己也断开了)。

为什么建立连接协议是三次握手,而关闭连接却是四次握手呢?

建立连接时,ACK和SYN可以放在一个报文中发送,而关闭连接时,被动关闭方可能还需要发送一些数据后,再发送FIN报文表示同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。

四次挥手的原因

第一点原因是为了保证被动关闭方可以进入CLOSED状态。MSL是最大报文段寿命,等待2MSL可以保证A发送的最后一个确认报文能被B接收,如果该报文丢失,B没有收到就会超时重传之前的FIN+ACK报文,而如果A在发送确认报文之后就立即释放连接就无法收到B超时重传的报文,因而也不会再一次发送确认报文段,B就无法正常进入CLOSED状态。

第二点原因是2MSL时间之后,本连接中的所有报文就都会从网络中消失,可以防止已失效连接的请求数据包与正常连接的请求数据包混淆而发生异常。

除此之外,TCP还设有一个保活计时器,用于解决客户端主机故障的问题,服务器每收到一次客户的数据就重新设置保活计时器,时间为2小时。如果2小时内没有收到就间隔75秒发送一次探测报文,连续10次都没有响应后就关闭连接。

为什么TIME-WAIT状态还需要等2MSL后才能返回到CLOSED状态?

1、 无法保证最后发送的ACK报文会一定被对方收到,所以需要重发可能丢失的ACK报文。

2、 关闭连接一段时间后可能会在相同的IP地址和端口建立新的连接,为了防止旧连接的重复分组在新连接已经终止后再现。2MSL足以让分组最多存活msl秒被丢弃。

TCP和UDP的区别

  • TCP是面向连接的协议,提供的是可靠传输,在收发数据前需要通过三次握手建立连接,使用ACK包对收发数据进行正确性检验。而UDP是无连接的协议,不管对方有没有收到或者收到的数据是否正确,减少了开销和发送数据之前的时延,所以速度更快。

  • TCP保证数据的可靠传输,而UDP使用尽最大努力交付,即不保证可靠交付,因此主机不需要维持复杂的连接状态。

  • TCP是面向字节流的,UDP是面向报文的。

  • TCP是点到点的一对一通信,UDP支持一对一,一对多和多对多的交互通信。

  • TCP提供流量控制和拥塞控制,而UDP没有。

在浏览器中输入URL后执行的全部过程

1、 首先是域名解析,客户端使用DNS协议将URL解析为对应的IP地址

2、 然后建立TCP连接,客户端与服务器通过三次握手建立TCP连接

3、 接着是http连接,客户端向服务器发送http连接请求(http无需额外连接,直接通过已经建立的TCP连接发送)

4、 服务器对客户端发来的http请求进行处理,并返回相应

5、 客户端接收到http响应,将结果展示给用户

http常用的状态码有

  • 200-请求成功

  • 301-资源(网页等)被永久转移到其他URL

  • 404-请求的资源(网页等)不存在

  • 500-内部服务器错误

  • 400-请求无效

  • 403-禁止访问

http的请求方法有哪些?

Http的请求方法包括GET,POST,PUT,DELETE四种基本方法。(四种方法中只有POST不是操作幂等性的,每次请求都有不同的URI(统一资源标志符))

get和post的区别:

1、 get方法不会修改服务器上的资源,它的查询是没有副作用的。而post有可能会修改服务器上的资源

2、 get可以保存为书签,可以用缓存来优化,而post不可以

3、 get请求附在url上,而post把参数附在http包的包体中

4、 浏览器和服务器一般对get方法所提交的url长度有限制,一般是1K或者2K,而对post方法所传输的参数大小限制为80k到4M不等

5、 post可以传输二进制编码的信息,get的参数一般只支持ASCII

三、操作系统

定义

操作系统( Operating System,OS)是指控制和管理整个计算机系统的硬件和软件资源,并合理地组织调度计算机的工作和资源的分配,以提供给用户和其他软件方便的接口和环境,它是计算机系统中最基本的系统软件。

OS的功能

  • 进程管理
  • 存储管理
  • 设备管理
  • 文件管理
  • 作业管理

OS的特征

  • 并发:指两个或多个事件在 同一时间间隔 内发生。这些事件宏观上是同时发生的,但微观上是交替发生的。并行:指两个或多个事件在 同一时刻 同时发生。
  • 共享:与并发一起成为最基本的特征,二者互为存在的条件
  • 虚拟:是指把一个物理上的实体变为若干个逻辑上的对应物
  • 异步:在多道程序环境下,允许多个程序并发执行,但由于资源有限,进程的执行不是一贯到底的,而是走走停停,以不可预知的速度向前推进

进程概念(PCB是进程存在的唯一标志)

从不同的角度,进程可以有不同的定义,比较典型的定义有:

  • 进程是程序的一次执行过程。
  • 进程是一个程序及其数据在处理机上顺序执行时所发生的活动。
  • 进程是具有独立功能的程序在一个数据集合上运行的过程,它是系统进行资源分配和调度的一个独立单位。

线程概念

线程是进程中执行运算的最小单位,是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。

  • Linux理论上最多可以创建32768个进程,因为进程的pid是用pid_t来表示的,而pid_t的最大值是32768,所以理论上最多有32768个进程。

  • 而进程最多可以创建的线程数是根据分配给调用栈的大小,以及操作系统(32位和64位不同)共同决定的。Linux32位下是300多个。

进程与线程的区别

  • 调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位
  • 并发性:一个进程可以有多个线程,但是一个线程只能属于一个进程;进程之间可以并发执行,同一个进程的多个线程之间也可并发执行
  • 拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源
  • 系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销
  • 崩溃的影响:进程之间不会相互影响;而一个线程崩溃会导致进程崩溃,从而影响同个进程里面的其他线程

进程与线程的联系

线程是存在进程的内部,一个进程中可以有多个线程,一个线程只能存在一个进程中。同一进程的所有线程共享该进程的所有资源。

进程间的通信方式

进程之间的通信方式主要有六种,包括管道信号量消息队列信号共享内存套接字

管道:管道是半双工的,双方需要通信的时候,需要建立两个管道。管道的实质是一个内核缓冲区,进程以先进先出的方式从缓冲区存取数据:管道一段的进程顺序的将进程数据写入缓冲区,另一端的进程则顺序地读取数据,该缓冲区可以看作一个循环队列,读和写的位置都是自动增加的,一个数据只能被读一次,读出以后在缓冲区都不复存在了。当缓冲区读空或者写满时,有一定的规则控制相应的读进程或写进程是否进入等待队列,当空的缓冲区有新数据写入或满的缓冲区有数据读出时,就唤醒等待队列中的进程继续读写。

信号量:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。信号量只有等待和发送两种操作。等待就是将其值减一或者挂起进程,发送就是将其值加一或者将进程恢复运行。

信号:信号是Linux系统中用于进程之间通信或操作的一种机制,信号可以在任何时候发送给某一进程,而无须知道该进程的状态。如果该进程并未处于执行状态,则该信号就由内核保存起来,直到该进程恢复执行并传递给他为止。

共享内存:共享内存允许两个或多个进程共享一个给定的存储区,这一段存储区可以被两个或两个以上的进程映射至自身的地址空间中。一个进程写入共享内存的信息,可以被其他使用这个共享内存的进程,通过一个简单的内存读取读出,从而实现了进程间的通信。共享内存的效率最高,缺点是没有提供同步机制,需要使用锁等其他机制进行同步。

消息队列:消息队列就是一个消息的链表,是一系列保存在内核中消息的列表。用户进程可以向消息队列添加消息,也可以向消息队列读取消息。消息队列与管道通信相比,其优势是对每个消息指定特定的消息类型,接受的时候不需要按照队列次序,而是可以根据自定义条件接受特定类型的消息。可以把消息看成一个记录,具有特定的格式以及特定的优先级。对消息队列有写权限的进程可以向消息队列按照一定的规则添加新消息,对消息队列有读权限的进程可以从消息队列中读取消息。

套接字:套接字也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同设备及期间的进程通信。

线程之间的通信方式有哪些?进程之间的同步方式又有哪些?

线程之间通信:

  • 使用全局变量

  • 使用信号机制

  • 使用事件

进程之间同步:

  • 信号量

  • 管程

死锁

死锁就是指多个进程因为互相竞争资源而陷入的一种僵局,如果没有外力的作用,这些进程都无法继续向前推进。

死锁的原因包含了:

1、 不可剥夺资源数量的不足,如果是可剥夺资源是不会造成死锁的

2、 进程的推进顺序非法,进程请求和释放资源的顺序不当

3、 信号量的使用不当,彼此等待对方的消息

死锁有四个必要条件:

1、 互斥条件,进程对资源的占用具有排他性控制,如果进程请求的资源已被占用,请求就会被阻塞

2、 不可剥夺条件,当一个资源没有被使用完成前是不能被其他进程强行获取的,只有占用它的进程主动释放才可以

3、 请求和保持条件:一个进程已经占有了某个资源,又要请求其他资源,而该资源被其他进程占用,请求被阻塞,但进程也不会释放自己已经占有的资源

4、 循环等待条件,存在一个进程资源的循环等待链,链中每个进程已经占有的资源同时是其他进程请求的资源

避免死锁:破坏其中的一个条件

1、 破坏互斥条件,系统中的所有资源都允许共享,不太现实

2、 破坏不可剥夺条件,允许剥夺其他进程已经占有的资源,可能会造成前段工作的失效,如果频繁发送就会增加系统开销,严重降低系统的吞吐量

3、 破坏请求和保持条件,采用预先资源分配法,一次性分配进程需要的所有资源,缺点就是会严重浪费系统资源

4、 破坏循环等待条件,采用顺序资源分配法,缺点是会造成编程不便

操作系统的内存管理

操作系统的内存管理包括 物理内存管理 和 虚拟内存管理

物理内存管理包括交换与覆盖,分页管理,分段管理和段页式管理等;

虚拟内存管理包括虚拟内存的概念,页面置换算法,页面分配策略等;

虚拟内存的了解

在运行一个进程的时候,它所需要的内存空间可能大于系统的物理内存容量。通常一个进程会有4G的空间,但是物理内存并没有这么大,所以这些空间都是虚拟内存,它的地址都是逻辑地址,每次在访问的时候都需要映射成物理地址。当进程访问某个逻辑地址的时候,会去查看页表,如果页表中没有相应的物理地址,说明内存中没有这页的数据,发生缺页异常,这时候进程需要把数据从磁盘拷贝到物理内存中。如果物理内存已经满了,就需要覆盖已有的页,如果这个页曾经被修改过,那么还要把它写回磁盘。

虚拟内存的作用:

1、 作为缓存工具,提高内存利用率:将内存视为一个存储在磁盘上的地址空间的告诉缓存,在内存中只保存活动区域,并根据需要在磁盘和内存之间来回传送数据。

2、 作为内存管理工具,简化内存管理:每个进程都有统一的线性地址空间,在内存分配中没有太多限制,每个虚拟页都可以被映射到任何的物理页中。

3、 作为内存保护工具,隔离地址空间:进程之间不会相互影响,用户程序不能访问内核信息和代码。页表中的每个条目的高位部分是表示权限的为,可以通过检查这些位来进行权限控制(读、写、执行)。

服务器高并发的解决方案

1、 应用数据与静态资源分离将静态资源(图片,视频,js,css等)单独保存到专门的静态资源服务器中,在客户端访问的时候从静态资源服务器中返回静态资源,从主服务器中返回应用数据。

2、 客户端缓存:因为效率最高,消耗资源最小的就是纯静态的html页面,所以可以把网站上的页面尽可能用静态的来实现,在页面过期或者有数据更新之后再将页面重新缓存。或者先生成静态页面,然后用ajax异步请求获取动态数据。

3、 集群和分布式(集群是所有的服务器都有相同的功能,请求哪台都可以,主要起分流作用)(分布式是将不同的业务放到不同的服务器中,处理一个请求可能需要使用到多台服务器,起到加快请求处理的速度)

可以使用服务器集群和分布式架构,使得原本属于一个服务器的计算压力分散到多个服务器上。同时加快请求处理的速度。

4、 反向代理:在访问服务器时,服务器通过别的服务器获取资源或结果返回给客户端。

四、数据库

数据库中事务的ACID

1、 atom原子性:事务是一个不可分割的工作单位,这组操作要么全部发生,要么全部不发生。

2、 consistency一致性:在事务开始以前,数据库中的数据有一个一致的状态。在事务完成后,数据库中的事务也应该保持这种一致性。事务应该将数据从一个一致性状态转移到另一个一致性状态。比如在银行转账操作后两个账户的总额应当不变。

3、 isolation隔离性:数据库事务的隔离性要求数据库中事务不会受另一个并发执行的事务的影响,对于数据库中同时执行的每个事务来说,其他事务要么还没开始执行,要么已经执行结束,它都感觉不到还有别的事务正在执行。

4、 durability持久性:数据库事务的持久性要求事务对数据库的改变是永久的,哪怕数据库发生损坏都不会影响到已发生的事务。如果事务没有完成,数据库因故断电了,那么重启后也应该是没有执行事务的状态,如果事务已经完成后数据库断电了,那么重启后就应该是事务执行完成后的状态。

什么是脏读,不可重复读和幻读?

1、脏读:脏读是指一个事务在处理过程中读取了另一个还没提交的事务的数据

比如A向B转账100,A的账户减少了100,而B的账户还没来得及修改,此时一个并发的事务访问到了B的账户,就是脏读。

2、不可重复读:不可重复读是对于数据库中的某一个字段,一个事务多次查询却返回了不同的值,这是由于在查询的间隔中,该字段被另一个事务修改并提交了。

比如A第一次查询自己的账户有1000元,此时另一个事务给A的账户增加了1000元,所以A在此读取他的账户得到了2000的结果,跟第一次读取的不一样。不可重复读与脏读的不同之处在于,脏读是读取了另一个事务没有提交的脏数据,不可重复读是读取了已经提交的数据,实际上并不是一个异常现象。

3、幻读:事务多次读取同一个范围的时候,查询结果的记录数不一样,这是由于在查询的间隔中,另一个事务新增或删除了数据。

比如A公司一共有100个人,第一次查询总人数得到100条记录,此时另一个事务新增了一个人,所以下一次查询得到101条记录。不可重复读和幻读的不同之处在于,幻读是多次读取的结果行数不同,不可重复读是读取结果的值不同。

避免不可重复读需要锁行,避免幻读则需要锁表。

脏读,不可重复读和幻读都是数据库的读一致性问题,是在并行的过程中出现的问题,必须采用一定的隔离级别解决。

数据库高并发的解决方案

1、 在web服务框架中加入缓存。在服务器与数据库层之间加入缓存层,将高频访问的数据存入缓存中,减少数据库的读取负担。

2、 增加数据库索引,提高查询速度。(不过索引太多会导致速度变慢,并且数据库的写入会导致索引的更新,也会导致速度变慢)

3、 主从读写分离,让主服务器负责写,从服务器负责读。

4、 将数据库进行拆分,使得数据库的表尽可能小,提高查询的速度。

5、 使用分布式架构,分散计算压力。

五、设计模式(软件工程的基石,后端选看)

设计模式简介

设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。

设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问,设计模式于己于他人于系统都是多赢的,设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样。项目中合理地运用设计模式可以完美地解决很多问题,每种模式在现实中都有相应的原理来与之对应,每种模式都描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案,这也是设计模式能被广泛应用的原因。

简述设计模式七大原则

  • 开放封闭原则:对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能人为去修改原有的代码,实现一个热插拔的效果。

  • 单一职责原则:一个类、接口或方法只负责一个职责,这样可以降低代码复杂度以及减少代码变更引起的风险。

  • 依赖倒置原则:针对接口编程,编程依赖于抽象类或接口而不依赖于具体实现类。

  • 接口隔离原则:将不同功能定义在不同接口中来实现接口隔离。

  • 里氏替换原则:任何基类可以出现的地方,子类一定可以出现。

  • 迪米特原则:每个模块对其他模块都要尽可能少地了解和依赖,降低代码耦合度。

  • 合成复用原则:尽量使用组合( has-a )/聚合( contains-a )而不是继承( is-a )达到软件复用的目的。

简述设计模式的分类

  • 创建型模式:在创建对象的同时隐藏创建逻辑,不使用 new 直接实例化对象。该模式包含工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。

  • 结构型模式:通过类和接口间的继承和引用实现创建复杂结构的对象。该模式包含适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。

  • 行为型模式:通过类之间不同通信方式实现不同行为。该模式包含策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

六、《代码随想录》

字符串

23.实现strStr()(KMP算法)

帮你把KMP算法学个通透!(理论篇)_哔哩哔哩_bilibili

给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回 -1

分析:

  • KMP的经典思想就是:当出现字符串不匹配时,可以记录一部分之前已经匹配的文本内容,利用这些信息避免从头再去做匹配

  • next数组就是前缀表,前缀表是用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配

  • 为什么要用前缀表?如aabaaf串。下标5之前这部分的字符串(也就是字符串aabaa)的 最长相等的前缀 和 后缀字符串 是子字符串aa ,因为找到了最长相等的前缀和后缀,匹配失败的位置是后缀子串的后面,那么我们找到与其相同的前缀的后面从新匹配就可以了

  • 找到了不匹配的位置, 那么此时我们要看它的前一个字符的前缀表的数值是多少。为什么要前一个字符的前缀表的数值呢,因为要找前面字符串的最长相同的前缀和后缀。

    所以要看前一位的 前缀表的数值,即next[j - 1]

  • 将O(n * m)变为O(n + m)

实现:

  • 构造next数组

    • 初始化
    • 处理前后缀不相同的情况
    • 处理前后缀相同的情况
  • 用next数组来匹配

    • 字符不相同,从next数组找下一个匹配位置,连续回退
    • 字符相同,i和j同时往后移动
    • 实际上,i一直往后移动,到文本串末尾时程序结束,j一直在模式串的前半段往复、不断重新匹配
    class Solution {
    public:
        void getNext(int* next, const string& s) {
            int j = 0;
            next[0] = 0; //建议初始化为0,这样next就是前缀表,不需-1操作
            for(int i = 1; i < s.size(); i++) {
                while (j > 0 && s[i] != s[j]) {//处理不相同的情况,j要保证大于0,因为下面有取j-1作为数组下标的操作
                    j = next[j - 1];//!!!关键步骤,若不匹配,j回退到前一位的next数组的值指向的位置
                }
                if (s[i] == s[j]) {//处理相同的情况
                    j++;
                }
                next[i] = j;//更新next数组
            }
        }
        int strStr(string haystack, string needle) {
            if (needle.size() == 0) {
                return 0;
            }
            int next[needle.size()];
            getNext(next, needle);//构建前缀表
            int j = 0;
            for (int i = 0; i < haystack.size(); i++) {
                while(j > 0 && haystack[i] != needle[j]) {//不匹配回退,j要保证大于0,因为下面有取j-1作为数组下标的操作
                    j = next[j - 1];
                }
                if (haystack[i] == needle[j]) {//匹配则进一
                    j++;
                }
                if (j == needle.size() ) {
                    return (i - needle.size() + 1);//定位
                }
            }
            return -1;
        }
    };
    

以下部分只是略微了解概念,不足以给出总结体会,还需深入思考,日后完善。

回溯算法

带你学透回溯算法-组合问题(对应力扣题目:77.组合)| 回溯法精讲!_哔哩哔哩_bilibili

动态规划

手把手带你入门动态规划 | 对应力扣(leetcode)题号:509.斐波那契数_哔哩哔哩_bilibili

贪心算法

贪心算法巨简单?没套路?每次做题都不知道自己用了贪心?遇到简单的贪心题目靠直觉,难一点就不会了?来来来,贪心算法你该了解这些!_哔哩哔哩_bilibili

每日小结 day 10-day 11

目前知识储备还很薄弱,应在在熟悉完全部数据结构和常用STL库函数的基础上,对回溯、动规、贪心算法进行深入体会。学习应当深入,一定不能贪多、贪全。同时加强对八股文的理解。

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值