408知识复试前摘要复习

dijkstra证明

使用归纳法进行证明,当k = 1的时候,算法显然成立
当 1 < k < j 的时候,假设算法成立,现在考虑k = j的时候
现在我们来计算1号点到j号点的距离,根据dijkstra算法的方法
1号点到j号点的距离是 1号点到i号点 + i号点到j号点的距离 的最小值,其中i从1 ~ j-1
如果我们存在一个更好距离 pj , 那么就会有一个k号点使得 pk + vkj 更小
但是 pk + vkj 这个距离他 >= min(pi + wij) 所以矛盾,即dijkstra在k = j的时候也成立

Bellman-ford算法 O(nm)

循环k次,每次遍历每个边 a,b,c 然后不断地更新dis[b] = dis[a] + c

SPFA算法 O(nm)

由于Bellman-ford算法他不断地遍历各个边,有些边可能根本不需要遍历,我们就可以做进一步优化,然后我们就创建一个队列,每次只加入被更新过的节点,然后我们再遍历更新过的节点的出边,用这个边来继续更新。

queue<int> q;  //记录被更新过的点
st[N];   //记录这个点是否在队列中
dis[N];   //到i号点的距离
dis[0] = 0;
q.push(0);
st[0] = true;
while(!q.empty())
{
    int t = q.front();
    q.pop();
    st[t] = false;
    然后遍历t的出边,用这个边更新其他点,其他点被更新后再放进队列中去
}

Floyd算法

这个是一个基于动态规划的思想
实际上是一个三维的数组 d[k,i,k] 表示从i这个点出发只经过1~k这个些点后到达j号点的最短的距离

先枚举k,再枚举i,再枚举j
    for(int k=1;k<=n;k++)
        for(int i=1;i<=n;i++)
            for(int j=1;j<=n;j++){
                dis[i][j] = min(dis[i][j],dis[i][k]+dis[k][j]);
            }

Prim算法 O(n^2)

算法流程:
dis[i]表示i号点到这个联通块的最小距离
st[i]表示是否在联通快中

dis[N]一开始初始化成正无穷
然后随机选择一个点i使得dis[i] = 0 , st[i] = true 把这个点加入连通块中
然后计算其余点到连通块的最小距离,再把距离最小的那个点加入联通快中如此不断重复
 

Krusal算法 O(nlogn)

算法主要流程:
先将所有边按照权重从小到大排序,一开始所有点都自己属于一个联通分量
从小边向大边遍历,如果这个边的两端端是属于不同的联通分量,那么就把这个边加进去,然后联通分量数目减一
如此重复直到所有节点被加入一个联通分量中去

Huffman-tree

给定n个节点来构造二叉树,如果这个树的带权路径长度能够达到最小,那么就称这个树为哈夫曼树。

哈夫曼树构造方法:就是先构造一个节点集合,每次从中选择两个权值最小的节点,然后用这个两个节点权值相加,构造一个新的节点,再放入节点集合中,在按这种方法,直到最后集合中只剩下一个节点,那么这个节点就是根节点。

Huffman-tree的证明:n = 1 与 n = 2 时按照我们的方法构造出来的显然是Huffman-tree
然后我们假设n = k 的时候按照我们的方法构造出来的是Huffman-tree下面我们来验证n = k + 1的情况,我们有k + 1 个叶节点,然后我们按权值排序,从中选权值最小的两个节点x1 和 x2 然后,用这两个节点构造出一个新节点,新节点权值是这两个节点权值的和,然后我么去掉这两个x1和x2把新节点加入进叶节点集合,这样我们就有一个k个叶节点的集合了,由我们的假设,k个叶节点可以构造出最优的带权路径最小的二叉树,然后我们再把这两个X1 X2加入到我们的构造的这个树中的那个之前用这两个节点构造出的叶节点上,那么就得到一个k + 1个叶节点的树,并且这个树是最优的。证明是反证法:假设构造出来的k + 1的树有更优的方案,就是这x1 和 x2这两个节点被放在了其他位置的叶节点下,那么这个树的权值减去这两个x1和x2的权值就是一个k个叶节点的树的权值,然后这个树比我们假设的k个叶节点的最优的树的权值还要小,就矛盾了。

KMP O(m+n)

其实kmp算法的思想就是,也是相当于使用了两个指针,一个指针i指向了字符串s中的字符,一个指针j指向了字符串p中的字符,朴素的做法是当si 和 pj 匹配失败的时候,i指针回退到主串匹配开始的位置+1那个地方,模式串回退到模式串开始的地方,然后再进行配对。但是,由于之前我们主串和模式串已经匹配了一部分了,我们可以利用这部分的信息来减少配对的次数。
我们给模式串一个next数组,next[i] 的含义表示从0~i-1 号的字串中,前k个字符恰好等于后k个字符的最大的k的值。然后下次匹配的时候,我们的主串的i指针就不移动,依然指向了上次失败的位置,然后我们的模式串的指针这次不移动到开始了,这次移动到号码为next[j]的那个位置上,从这个位置再进行匹配,这样就极大的减少了i指针的回退次数,提高了效率。
next[i]表示模式串i号位置匹配失败时,下一次模式串的指针指向哪里。
求那个next数组可以就是用两个指针一个从i-1向前搜,一个从0向后搜。

堆排序

堆是一个完全二叉树,完全二叉树就是除了最后一层节点外,其余节点都是非空的,并且最后一层的节点从左到右依次排开。
对堆的操作可以通过up 和 down这两种操作来维护
每次up 或者 down的操作都是logn的复杂度
然后我们对n个数进行排序就需要n次,所以堆排序的时间复杂度是O(nlogn)

堆排序就是每次把一堆顶的元素放到末尾,然后堆size–,然后再重新建立堆,再得到堆顶元素,如此重复。建堆重建堆

#include <iostream>
using namespace std;
const int N = 1e5+10;
int heap[N];
int n,m;
int heap_size;

void down(int num)
{
    int t = num;
    if(2*num<=heap_size&&heap[t]>heap[2*num]) t=2*num;
    if(2*num+1<=heap_size&&heap[t]>heap[2*num+1]) t=2*num+1;
    if(t!=num)
    {
        swap(heap[num],heap[t]);
        down(t);
    }
}
void up(int num)
{
    while(num/2 && heap[num/2] >heap[num])
    {
        swap(heap[num],heap[num/2]);
        num /= 2;
    }
}

int main()
{
    cin >> n >> m;
    for(int i=1;i<=n;i++) cin >> heap[i];
    heap_size = n;
    
    for(int i=n/2;i>0;i--) down(i);
    
    while(m--)
    {
        cout << heap[1] << " ";
        swap(heap[1],heap[heap_size]);
        heap_size--;
        down(1);
    }
    return 0;
}

快速排序

在这里插入图片描述

快排的思想主要就是基于分治的
首先我们确立一个分界点然后把,然后调整分界点左右两边,使得左边的数都小于等于这个值,右边的数都大于等于这个值,然后如此,同样的方法处理左边那部分和右边那部分。

#include <algorithm>
#include <iostream>
using namespace std;
const int N = 100010;
int q[N];

void quick_sort(int q[],int l,int r)
{
    if(l >= r) return;
    int x = q[l + r >> 1];
    int i = l - 1, j = r + 1;
    while(i < j)
    {
        do i++;while(q[i] < x);
        do j--;while(q[j] > x);
        if(i < j) swap(q[i],q[j]);
    }
    quick_sort(q,l,j);
    quick_sort(q,j+1,r);
}

int main()
{
    int n;
    cin >> n;
    for(int i = 0;i<n;i++) cin >> q[i];
    quick_sort(q,0,n-1);
    for(int i=0;i<n;i++) cout << q[i] << " ";
    return 0;
}

快排是先划分成两部分,一部分<=x 另一部分>=x然后再递归处理
归并是先递归,然后再处理

归并排序

也是基于分治的思想,用中间点作为分界线,分成左边和右边
先递归排序左边和右边
再将两部分有序的数组合成一个数组(合并的时候就是每个部分用个指针指着,然后这么比,把较小的那个放到一个新的数组中,最后再把这个数组复制回原数组)

#include <algorithm>
#include <iostream>
using namespace std;
const int N = 100010;
int q[N];
int n;

void merge_sort(int q[],int l,int r)
{
    if(l >= r) return;
    int mid = (l + r)/2;
    merge_sort(q,l,mid);
    merge_sort(q,mid+1,r);
    int temp[N],k = 0; //k记录temp中已经有几个元素了,temp是临时数组,用于保存两部分合并的结果
    int i = l , j = mid + 1;
    while(i<=mid && j <= r){
        if(q[i]<=q[j]) temp[k++] = q[i++];
        else temp[k++] = q[j++];
    }
    while(i<=mid) temp[k++] = q[i++];
    while(j<=r)   temp[k++] = q[j++];
    for(i = l,j = 0;j<k;i++,j++) q[i] = temp[j];  //再把暂存的结果写会原数组
    
}

int main()
{
    cin >> n;
    for(int i=0;i<n;i++) cin >> q[i];
    merge_sort(q,0,n-1);
    for(int i = 0; i < n; i++) cout << q[i] << " ";
    return 0;
}

B树

在这里插入图片描述
总的来说解释B树的时候可以这么说:

  1. B树又称多路平衡查找树,一个m阶的B树,他的节点至少有m/2上取整个子节点,同时最多有m个子节点
  2. B树中每个节点内部的数据都是有序的
  3. 每个B树的子树都有相同的高度,同时所有叶节点都在同一层上

在这里插入图片描述

B+树

在这里插入图片描述

B+树是一个m阶的B+树满足每个节点中最多有m个数据,以及m个指针,同时最少有m/2个数据以及m/2个指针,B+树的节点可以分为分值节点和叶子结点两种,分支节点的作用主要是充当索引,加快查找速度,叶子结点中存放的是关键字以及指向记录的指针,并且叶节点中的关键字也是顺序排列的,同时所有叶节点都通过指针连接起来,然后就可以实现顺序查找。
在这里插入图片描述

红黑树

  1. 红黑树首先是个二叉排序树
  2. 每个节点要么是红色的要么是黑色的
  3. 根节点是黑色的
  4. 叶节点(也叫外部节点,Null节点)均是黑色的
  5. 不存在两个相邻的红节点(即红节点的父节点和子节点都是黑色)
  6. 对于每一个节点,从这个节点开始到任意一个叶节点的路径上,所含有的黑节点的数目是相同的

红黑树的插入:先找到插入的位置,然后每个新节点我们都把他看做是红色的,如果这个节点插入到了根的位置,那么染成黑色结束。如果插入到其他位置,然后我们看看插入后是否还满足红黑树的定义,如果满足,那么我们就结束,如果不满足,我们就进行调整,调整的方式主要是看他的叔叔节点。

而且我们插入一个节点的时候,只有这个节点破坏了连续两个不能是红色的性质我们才会调整。
当叔叔节点是黑色时:

  1. 如果这个节点插入到了爷节点左儿子的左子树(LL型),我们进行右单旋,使得父节点换到爷节点的位置,然后父节点和爷节点的颜色都染成之前相反地颜色
  2. 如果这个节点插入到了爷节点的右儿子的右子树上(RR型),我们进行左单旋,使得父节点换到爷节点的位置上,然后父节点和爷节点的颜色都染成之前相反地颜色。
  3. 如果这个节点插入了叶节点左儿子的右子树上(LR型),先把儿节点换到父节点的位置上,再换到爷节点位置上。然后而儿子和爷节点染色。
  4. 如果这个节点插入了爷节点右子树的左儿子上(RL型),先把儿节点旋转到父节点位置,再换到爷节点位置,然后儿节点和爷节点染色。

当叔叔节点是红色时:
我们把叔叔节点,爷节点,父节点染色,变成之前相反地颜色。然后我们要把他的爷节点,看做是新插入的节点,然后按照我们的规则进行调整。

进程和线程的区别

参考

  1. 进程是资源分配的基本单位,线程是处理机分配的基本单位。
  2. 一个进程可以创建若干个线程,这些线程共享进程的资源。
  3. 一个进程会有独立的地址空间,每创建一个进程就会给这个进程分配一个独立的地址空间,然后建立许多的额外开销来维护他的代码段数据段堆栈段。
    线程没有独立的地址空间,同一个进程的线程共享一个进程的地址空间,同时这些线程之间共享大部分数据。
  4. 进程之间的切换花费的开销很大,而线程之间的切换就开销比较小只需要涉及少量寄存器的内容更改,可以提高效率
  5. 同一个进程创建的线程共享进程的代码段,数据段,堆段,但是每个线程都有自己的栈段。
    在这里插入图片描述

数据库部分内容

参考

关系数据库和非关系数据库的区别

关系数据库是采用了关系模型来组织的数据库,关系模型就是二维表格模型。
关系数据库使用简便,易于理解,容易维护。

非关系数据库:非关系的,分布式的且一般不遵循ACID原则的数据存储系统。非关系数据库里不限定与固定的结构,然后提供了很少的约束,所以很灵活。
数据库常见面试题
数据库面试

cache

在这里插入图片描述
RAM主要是掉电后信息容易丢失,ROM掉电后信息不会丢失
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
死锁就是多个进程竞争资源所形成的一种僵局:
在这里插入图片描述
函数名就是函数的地址,调用函数名来实现实际上就是调用了函数的地址

指针函数是返回值是个指针的函数
函数指针是指向函数位置的指针变量,其本质是一个指针。
比如:
int (*p) (参数列表)
p = fun //p就是一个函数指针
fun(参数列表)
{}

多态性

多态性就是用一个名字定义不同的函数,然后调用一个名字,可以实现不同的函数功能。
比如说虚函数:
虚函数用virtual实现,在父类中定义一个虚函数,然后可以在子类中实现重现,
可以把一个子类的指针强制转换为父类指针,然后父类指针就可以调用子类中对虚函数的重写后的功能。

Class A{}
A a;
cout << sizeof(a) << endl;
结果是1也就是说,只要是个空类的对象,他的值就是1而不是0
Class A{
void func1()
void func2()
}
A a;
cout << sizeof(a) << endl;
结果还是1  也就是说类A的普通成员函数他并不占用对象的内存空间

然后再向A里面加入一个虚函数

Class A{
void func1()
void func2()
public:
    virtual void func();
}
A a;
cout << sizeof(a) << endl;
结果是4  
这是因为只要引入了虚函数,那么编译器就会向实例内存位置插入一个成员变量
就是 void* ptr;  称之为虚函数表指针(virtual table pointer简称vptr)
然后这个虚函数表指针正好是4字节,也就是说虚函数表指针占用类对象的内存空间

就是只要类A里面有至少一个虚函数,那么编译器在编译期间就会为类A形成一个虚函数表
然后编译器在编译期间会在类A的构造函数中添加一个为A中的虚函数表指针赋值的语句,使得A的虚函数表指针能够指向虚函数表。
当实例化一个类A的对象的时候,由于构造函数中被插入了一个给虚函数表指针赋值的语句,然后这个实例中的虚函数表指针就可以指向类A的虚函数表。

下面我们再来看一下类的对象在内存中的布局:

class A{
    void func1()
    void func2()
    public:
        virtual vfunc1()
        virtual vfunc2()
        virtual ~A()
    private:
        int m_a;
        int m_b;
}

虚函数表,类成员函数,类虚函数都是占用的是类的内存空间,不占用对象的内存空间。
对象的成员变量,以及每个对象都有个虚函数表指针占用的是对象自己的内存空间。
在这里插入图片描述
比如说现在类A中有三个虚函数,然后虚函数表中就有三个指针,指向了这三个虚函数。

多态性简单来说就是:父类中有一个虚函数,子类中有一个同名的虚函数,当通过父类指针来new一个子类对象的时候(或者通过父类引用来绑定一个子类对象的时候),如果用父类指针来调用这个虚函数,那么调用的其实是子类的虚函数。

下面是一个利用虚函数实现多态性的例子:
在这里插入图片描述
假设父类Base中有三个虚函数f g h
然后子类Derive继承了这个父类Base但是重写了虚函数g
然后这两个类都会有各自有一个虚函数表,虚函数表中记录了自己拥有的各个虚函数的地址是多少,由于子类中重写了虚函数g那么子类中的虚函数表中函数g的指针指向自己新实现的位置,其余没有重写的虚函数,指向父类的虚函数的位置。

静态绑定:编译的时候就给每个函数确定好他运行时的位置,就是说编译的时候就已经确定好要调用哪个函数了,然后把调用的那个函数的地址直接写死了比如call 0x86868686 就是编译时期已经能够确定下来你调用的是那个函数了

当程序运行时,所有类的虚函数表都会加载到内存中的.rodata区域,也就是常量数据区(只能读不能改)
当我们创建一个有虚函数的类的对象的时候,会给这个对象分配一个虚函数表指针,用于指向这个类的虚函数表。

如果派生类中的某个方法和基类中继承来的某个方法,返回值,函数名,参数列表都相同,而且基类中的这个方法是virtual虚函数,那么基类中国的这个方法自动处理成虚函数。

当一个派生类继承一个基类的时候,自己这个类也会有一个自己的虚函数表,首先自己会从基类那里继承所有的虚函数,如果不重写,自己的虚函数表就是基类中那些虚函数的地址,如果编译器发现子类重写过父类的某个虚函数,那么就把继承与父类的那一个虚函数表中的函数地址,替换成子类重写后的函数地址。(其实就是一个覆盖关系,虚函数表中虚地址的覆盖)

每次声明一个类的指针的时候,当用这个类的指针去调用类里的函数的时候,编译器就会检查调用的这个函数的类型,如果这就是个普通的成员函数,就进行静态绑定(直接生成call 函数地址 这种汇编指令)直接定死执行的函数的所在地址。
如果发现调用的函数是个虚函数,那么就执行动态绑定。
动态绑定就是在编译阶段无法最终确定调用的函数地址是什么,只有在程序动态运行时才能确定调用的是那个函数,这种方法叫做动态绑定。
比如说:程序需要调用一个方法,但是不知道这个方法具体的实现是怎么样的,可能汇编的时候就是一个 call ecx 调用寄存器中的地址的函数,然后这个寄存器中的函数的地址得在具体运行的时候确定下来这个寄存器中的值是什么。

你用个父类型的指针去指向一个子类的对象时,实际上在访问其中的虚函数表指针时指向的就是子类中的虚函数表,然后就可以调用子类中重写的虚函数。

静态库和动态库

静态库主要是用于对一个文件连接时与静态库一起连接直接形成可执行文件然后装入。
liunx下生成静态库用ar命令
使用静态库用 -L + 路径名来进行使用
静态库的缺点就是会导致空间浪费,如果有多个文件需要用到静态库,那么这些文件就会和静态库一起编译然后转入内存,就会产生好几个重复的备份

动态库就是运行时连接,在运行时需要用到对应的库再进行连接

python和C的区别

  1. python是一种解释型语言,边执行,边解释。C语言是一种编译型语言,需要先统一翻译成机器代码再统一执行
  2. C语言执行速度相对更快,python执行速度相对更慢
  3. python使用自动的垃圾回收器回收分配的内存;C中需要程序员手动回收分配的内存
  4. Python不需要声明变量类型,C必须声明变量类型

编辑距离思考

f[i][j] 表示将 word1[1 ~ i] 变成 word2[1 ~ j] 的所有操作方案中操作次数的最小值
然后根据word1最后一次操作的种类可以分成三种情况:

  1. 如果最后一步删除了word1[i]后,word1[1 ~ i]变成了word2[1 ~ j] 那么首先word1[1 ~ i-1]这个字串是已经被变成了word2[1 ~ j]这个字串,此时 f[i][j] = f[i-1][j] + 1
  2. 如果最后一步是在word1[i]的后面添加了一个字母使得 word1[1 ~ i]这个子串变成了 word2[1 ~ j]这个子串,那么添加的这个字符一定是word2[j] 此时 f[i][j] = f[i][j-1] + 1
  3. 如果最后一步是通过替换word1[i]使得 word1[1 ~ i] 这个子串调整为word2[1 ~ j]这个子串,那么word1[i]==word2[j]的时候不需要替换,此时f[i][j] = f[i-1][j-1]
    如果word1[i] != word2[j] 的时候,才需要替换,此时f[i][j] = f[i - 1][j - 1]

软件测试

软件缺陷fault: 软件中的静态缺陷(比如有个for循环i应该从0开始,但是写成了从1开始)
软件错误error: 不正确的内部状态,是缺陷的表现形式,程序中某些状态不正确。(比如那个写成i从1开始的缺陷就会从结果不正确表现出来)
软件失效failure: 软件表现出与预期不一样的结果。

动态测试

黑盒测试:不关注程序运行的过程只关注程序运行的结果

  1. 随机测试:测试用例随机产生
  2. 等价类划分:把一堆测试样例划分成等价类(等价类就是那些具有相同特征的用例的集合),然后从每个等价类中挑选一个进行测试
  3. 边界测试:在等价类的基础上挑选边界值进行测试

白盒测试:
1… 语句覆盖:设计若干测试用例,使得每个语句至少被执行1次
2… 判定覆盖:设计测试用例,使得每个判定分支的真分支和假分支至少被取一次
3… 条件覆盖:设计测试用例,使得每个分支的判断条件中的每个条件的可能取值都至少取一次。
4… MCDC测试:设计一组测试用例,使得每个条件的更改都会单独影响到测试结果。

变异测试:通过向软件中注入缺陷,然后使用测试用例进行测试,看这些测试用例能否发现植入的缺陷。
模糊测试:将自动或者半自动生成的测试数据作为输入,输入软件中检查可能出现的缺陷。
模糊测试一般的流程是,给定一些种子输入,然后对这些种子输入进行变异,产生新的一些输入。

静态测试

静态测试指不运行程序本身,通过分析或检查程序的语法,结构来检查程序的正确性。
规则检查
符号执行
数据流分析

SPFA的简单证明

假设我们的起始点和原点之间存在着
SPFA算法就是不断的枚举那些被更新过的点,然后用这些被更新过的点的路径去更新新的点,然后最后求得最短路径。

我们有一个性质:最短路径的子路径也一定是最短路径。
假设我们从0 ~ k 号点有一个最短路径v0 v1 … vk
然后这条路径上的一个子路径vi vi+1 … vj 也一定是i到j的最短的
采用反证法,假设从i到j有一个更短的路径,那么从0号点到k号点的距离可以分成0到i号点i到j号点j到k号点这三段加起来,如果i到j有个更短的路径,那么0到k也有个更短的路径,与我们假设的这个路径是0到k的最短路径矛盾
所以,任意一个最短路径他的子路径也是最短路径

对我们的算法,当节点数为1的时候,显然成立,当节点数为k的时候,假设根据我们的算法能够给每个节点找到最短路径,那么我们再往图里加入一个节点,使得节点数目成为k+1时,假设我们到k+1号点的最短路径是 V0,V1,… , Vi , Vk+1 那么根据最短路径的子路径也是最短路径,那么到k+1号点的路径就是到i号点的最短路径 + i到k+1号点的这条边的权值 而我们的算法会不断地枚举被更新过的节点的边,当我们更新到i节点的路径时,就会枚举i节点到k+1号节点的边,从而就会更新到k+1号点的距离也成为最短距离,所以算法是正确的。

面向对象程序设计

面向对象就是把数据和对数据的操作封装成一起,作为一个相互依存的整体。是一种自下而上的设计思想,先设计组件,再完成拼装。
面向过程设计是一种自上而下的设计思想,先定好整体框架,再向其中添砖加瓦。

面向对象的三大特征:继承,封装,多态。
继承就是子类从父类中继承到父类具有的特征和行为,用于提高代码的可复用性。
封装就是一是把对象的属性和行为封装起来形成一个密不可分的整体,封装在一个不可分割的单元之中。二是信息隐藏,把不需要被外界所知道的信息隐藏起来。
多态:就是一个行为具有不同的表现特征。用一个名字去定义不同的函数,然后调用这个名字的函数就可以表现出不同的功能出来。主要是通过虚函数的形式表现出来的
每个类都可以实例化一些对象,但是这些对象的内存空间中只有自己的成员变量,而成员函数和虚函数是属于类的地址空间的。
当一个类有虚函数的时候,编译器就会给这个类分配一个虚函数表,虚函数表中存放的就是各个虚函数的起始地址。然后会在类的构造函数中隐含的添加上对虚函数表指针的赋值操作。当用这个类声明一个个对象的时候,对象内存空间中不仅包含了自己的成员变量,还包含了一个虚地址表指针,指向了这个类的虚地址表。
一个子类继承了父类,他也继承了父类的虚函数,然后子类自己有一个新的虚地址表,但是如果子类中不重写继承于父类的虚函数,那么这个虚函数表里的指针就指向父类中的虚函数的起始地址。如果子类重写了虚函数,那么就会指向自己重写 的虚函数的起始地址。
我们可以用一个父类型的指针指向子类型,然后可以调用子类型中实现的虚函数,实际上是因为实例化子类对象时,先调用父类的构造函数,这时虚函数表指针指向的是父类的虚函数表,然后再调用子类的构造函数,这时候这个对象的虚函数表指针就指向了子类的虚函数表,然后就可以通过子类的虚函数表来调用子类的虚函数实现方法。
这种思想其实就是父类的虚函数是一个接口,然后调用这个接口可以实现不同的功能不必在意实现的细节这样一种思想。
面向对象的5个设计原则:
单一职责原则:一个类不可以有过多的功能,最好只有一个功能。
开闭原则:对增加开放,对修改封闭,也就是说当我们要调整一个功能的时候,应该是通过增加代码来加以调整。
Liskov原则:子类必须能够替代父类的功能。
依赖倒转原则:应该依赖于抽象,不应依赖于具体
接口隔离原则:应该依赖于多个小接口,不应该依赖于大接口

召回率

评价最终推荐结果的好坏,主要是根据
用户实际喜欢,推荐结果喜欢
用户实际不喜欢,推荐结果喜欢
用户实际喜欢,推荐结果不喜欢
用户实际不喜欢,推荐结果不喜欢
然后计算 推荐结果喜欢,用户也喜欢的人数 / (推荐结果喜欢用户喜欢 + 推荐结果不喜欢用户喜欢) 这个是召回率
准确率 = (用户实际喜欢推荐喜欢)/(所有样本数量)

参数传递

传地址

把实际参数的地址传给形式单元,形式单元也有自己的地址,传地址就是形式单元里存的是地址,然后根据形势单元里地址的内容来找到实际参数的地址,用这个实际参数地址里的东西进行运算
c++中的传引用就是传的地址

得结果

给每个形式参数准备两个形式单元,第一个形势单元存放实际参数的地址,第二个形势单元存放实际参数的值。
然后对每个形参的访问,实际上就是对那个形参分配的第二个单元访问,然后返回时再把结果写回形参的第一个单元对应的地址。

传值

调用时先把实际参数的值计算出来,然后存放到形式参数所在的单元中,然后直接对这个单元进行访问。

传名

相当于把对应过程的过程体,复制到调用的地方,然后直接调用实际参数。

数据库隔离等级

ACID
A(Atomic原子性):事务的操作是一个完整的过程,要么都做,要么都不做
C(Consistency一致性):当数据库事务完成时,数据要保持一致
I(Isolation隔离性):每个事务不会被其他事务干扰
D(Duration持久性):一个事务一旦被提交了,那么对数据库的操作更改是永久的

数据库的三大范式:
第一范式:强调的是列的原子性,即每个列不可以再分成其他的列
第二范式:首先满足第一范式的特性,然后每个表中必须有一个主键,然后其他所有的列必须完全依赖于主键,不能只依赖于主键的一部分。
第三范式:首先满足第二范式的定义,并且非主键列之间不能存在传递依赖(即非主键A依赖于非主键B,非主键B依赖于主键)

数据库事务并发的问题:

  1. 脏读:事务A读取了事务B的数据,然后B回滚了操作,事务A就是脏读
  2. 不可重复读: 事务A多次读取某些数据,然后事务B在A读取过程中进行了修改,使得A前后读出数据不同
  3. 幻读:事务再前后两次查询时,第二次看到了第一次没有的行

事务隔离的级别:
读未提交
不可重复读
可重复度
序列化

自然连接就是从两个关系构成的笛卡尔积中,把两个关系中相同列属性中值相同的行选出来拼接而成的结果
内连接与自然连接基本相同,不同在于内连接可以自己选择比较的几个属性
左外连接就是接受左表所有行,然后根据左右两个表中相同属性列的相同值来拼接,右表未拼接成功的填NULL
右外连接就是保留右表的所有列,然后用右表与左表中的相同属性列来进行拼接,然后左表未拼成功的取NULL值

关系数据库和非关系数据库的区别:
关系数据库都是采用的关系模型,数据组织成一张张的表,易于维护,使用方便采用SQL查询语句
但是读写性能差,当数据量庞大时读写速度慢,并且固定表的模式,不够灵活。存放地方一般是硬盘,所以读取速度慢。

非关系数据库其实是一种数据结构化的存储集合,有文档型,键值对型等存储方式。
他的格式灵活,存取速度快,可扩展性强,数据存放方式一般在缓存,读取速度快。

列举出3~5个计算机领域的热门词汇

区块链
人工智能:使计算机获得类人思考类人行为
虚拟现实:虚拟现实是一种可以创建和体验虚拟世界的计算机仿真系统,使用计算机实现一种虚拟环境。

TCP的拥塞控制

拥塞控制是让网络能够承载现有的负荷,是一个全局性的问题。
流量控制是点对点的通信量的控制,是一个端到端的问题。

拥塞控制的主要算法有:慢开始,拥塞避免,快重传,快恢复
慢开始:当一个TCP刚刚建立的时候,先令拥塞窗口cwnd=1(这个值代表有几个最大报文段长度),然后A发送第一个报文段给B,B给A发送一个回复,如果A收到了B发来的回复,那么就把cwnd的值乘2,每次收到回复都把cwnd的值乘2,直到达到慢开始的阈值.
当达到慢开始的阈值后,就开始拥塞避免算法,每次收到回复,使得cwnd的值加1
当未按时收到确认时(即重传计时器的时间到了),就认为网络出现了拥塞,此时把慢开始的阈值设置为此时拥塞窗口的一半,然后,然后拥塞窗口从1开始继续执行满开始算法。

而快重传和快恢复是对拥塞避免算法的改进。
快重传就是当收到三个冗余ACK的时候,直接重传报文,而不需要等待重传计时器结束。
快恢复就是当收到三个冗余ACK的时候,把慢启动阈值设置为当前拥塞窗口的一半,但是拥塞窗口的值从这个新的阈值开始执行拥塞避免算法,而不是重新变成1来执行慢开始算法了。

TCP流量控制

TCP提供一种基于滑动窗口的协议来进行流量控制,发送方的
发送窗口的上限值 = min(接受窗口的大小rwnd, 拥塞窗口的大小cwnd)
接受窗口的大小是根据接收方发送过来的窗口字段的值来决定的。
然后A会向B发送信息,总发送量是发送窗口的大小,每收到一个对发送窗口中已发送的段的ACK确认,就会把发送窗口向后移动,然后继续发送消息至发送窗口的大小。

访问www.baidu.com的过程

一个主机接入网络中,首先向本网络广播一个DHCP请求报文,当DHCP服务器收到这个DHCP请求报文的时候,DHCP服务器把分配给他的ip地址,本网络的网关服务器ip地址一并封装起来,然后发送回这个新接入的主机,然后这个主机就有了自己的ip地址。
然后输入www.baidu.com就会调用DNS服务,生成一个DNS查询报文,然后层层封装,封装成一个以太网帧,被发送到网关路由器上,由于之前知道了网关路由器的ip地址,但是并不知道他的mac地址,需要运行ARP地址解析协议,首先检查本地ARP缓存,如果有就填上,如果没有,就广播一个ARP查询报文,网关路由器收到这个消息后就给他一个回答。
至此,就可以向网关路由器发自己那个DNS查询了,网关路由器收到DNS查询报文后,首先使用NAT协议,把私有地址转化成公有地址,然后根据报文中的目的地址,选择合适的路由进行发送。当报文到达DNS服务器后,DNS服务器返回一个DNS相应报文,把对应的url与其ip地址返回过来。当主机收到DNS返回的报文后,从中抽取url对应的ip地址,然后与这个ip地址进行TCP连接,然后经过三次握手,两个人建立连接,这时就可以把HTTP请求报文封装起来,发送给url对应的网址,url对应的网址收到这个报文后从中抽取HTTP请求,然后把请求的资源封装起来,返回给他,至此一个请求的流程就结束了。

中断

中断分为 外中断:CP

中断向量表:所有的中断类型码以及对应的中断向量构成的表
中断向量:中断处理程序的入口地址

中断的类型主要分为内中断和外中断
内中断(也称之为异常,陷入),主要是CPU内部产生,与当前执行的指令有关,然后内中断又分为自愿中断(也叫指令中断) 还有 强迫中断(包含硬件故障如缺页,软件中断:如除0错误)
外中断是信号来源是CPU外部的中断,比如外设请求和人工干预。

中断处理的过程:

  1. 关中断
  2. 保存断点
  3. 引出中断服务程序
  4. 保存现场和屏蔽字
  5. 开中断
  6. 执行中断服务程序
  7. 关中断
  8. 恢复现场和屏蔽字
  9. 开中断
    10.中断返回

系统调用就是用户在程序中调用操作系统所提供的一些子功能,比如存储分配,I/O操作等等。
用户可以使用trap指令,也叫做访管指令来进行操作。

SPFA算法

假设从原点到终点的距离是V0,V1, … , Vk
当k = 0 的时候显然成立
假设经过了k - 1 轮松弛操作,那么所有长度为 k - 1 的最短路都会被搜索到,此时dis[Vk-1]取到了最短路,那么第k轮操作,dis[Vk]一定会被Vk-1到Vk的边所松弛,而根据松弛定理如果存在一个从V0到Vk的路径,如果按照(V0,V1) (V1,V2) … (Vk-1 , Vk)的操作进行松弛,那么得到的就是到k点的最短路。

Bellman-ford算法证明主要是根据一个路径松弛性质来的,如果存在一个最短路V0 V1 … Vk
那么对其进行松弛操作 (V0,V1) (V1,V2) … (Vk-1, Vk)最后得到的到k点的距离就是我们到k点的最短路径。并且这些松弛操作不受向其中添加其他松弛操作的影响。
而按照我们的算法,可以满足路径松弛性质定理,所以我们一定会找到一个最短路。

路径松弛定理又是根据收敛定理来得到的。
收敛定理是:就是如果有一条最短路径 S -> i -> j 那么在对(i,j)这条边进行松弛的时候,已经有到S -> i 的路径是最短路了,那么经过松弛i j 这条边后,就得到了从s到j的最短路径。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

新城里的旧少年^_^

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值