面试系列文章:
点击这里直接跳转面试经验贴专栏
[1] C++软件开发工程师概念手册
[2] 从浏览器输入一个URL(www.baidu.com)后执行全过程
[3] const指针和指向常量的指针
[4] C/C++预处理指令#define,#ifdef,#ifndef,#endif…
[5] 堆与栈的区别(经典讲解)
[6] 堆栈、BSS段、代码段、数据段、RO、RW、ZI等概念区分
[7] C语言中关键字auto、static、register、extern、volatile、restrict的作用
昨天面试完字节跳动头条的测试开发,我更是想要规划写一篇应届生校园招聘的面经,做一个总结。不管你面试哪个方向,主要考察内容都是C++软件知识、操作系统、计算机网络、数据库这几类。
因此,我以字节跳动面试的主要内容按这几类分类整理,供大家参考,之后也会在此基础上进行整理完善。
C++考点整理请参看我的另一篇文章,常考题带参考答案。
C++软件开发工程师概念手册
重载和覆盖是面向对象多态性的不同的表现方式。其中,重载是在一个类中多态性的一种表现,是指在一个类中定义了多个同名的方法,他们或有不同的参数个数,或有不同的参数类型,或参数顺序不同。与访问修饰符和返回值类型无关。在使用重载时,需要注意以下几点:
重载是通过不同的方法参数来区分的,例如不同的参数个数,不同的参数类型或者不同的参数顺序。
重载和方法的访问修饰符、返回值类型、抛出的异常类型无关。
对于继承来说,如果父类方法的访问修饰符为private,那么就不能在子类对其重载;如果子类也定义了一个同名的函数,这只是一个新的方法,不会达到重载的效果。
覆盖是指子类函数覆盖父类函数。覆盖一个方法并对其进行重写,以达到不同的作用。在使用覆盖时要注意以下几点:
子类中的覆盖方法必须要和父类中被覆盖的方法有着相同的函数名和参数。
子类中覆盖方法的返回值必须和父类中被覆盖方法的返回值相同。
子类中覆盖方法所抛出的异常必须要和父类中被覆盖方法所抛出的异常一致。
父类中被覆盖的方法不能为private,否则其子类只是定义了一个方法,并没有对其覆盖。
覆盖和重载的区别如下:
覆盖是子类和父类之间的关系,是垂直关系;重载是同一个类中方法之间的关系,是水平关系。
覆盖只能由一对方法产生关系,重载是多个方法之间的关系。
覆盖要求参数列表相同,重载要求参数列表不同。
覆盖关系中,调用方法是根据对象的类型来决定;而重载关系是根据调用时的实参表与形参表来选择方法体的。
拓展:
重载: 函数重载是指在同一作用域内(名字空间),可以有一组具有相同函数名,不同参数列表的函数;
覆盖override(也叫重写):指在派生类中重新对基类中的虚函数(注意是虚函数)重新实现。即函数名和参数都一样,只是函数的实现体不一样;
隐藏:指派生类中的函数把基类中相同名字的函数屏蔽掉了,隐藏与另外两个概念表面上看来很像,很难区分,其实他们的关键区别就是在多态的实现上。
真题示例:
下面那种情形下myfunc函数声明是重载?
A. namespace IBM
{
int myfunc(int a);
}
namespace SUN
{
int myfunc(double b);
}
B. namespace IBM
{
int myfunc(int a);
}
namespace SUN
{
using IBM::myfunc;
int myfunc(double b);
}
C. namespace IBM
{
int myfunc(int a);
namespace SUN
{
int myfunc(double b);
}
}
D. class A
{
public:
int myfunc(int a);
}
class SubA: public A
{
public:
int myfunc(double b);
}
答案是B,A和C都是名字空间不同;D是隐藏,只有B是重载!
存在如下声明:
void f (); //全局函数
class A
{
public:
void f(int);
};
class B: public A
{
public:
void f(int *);
static void f(int **);
void test();
//...
};
那么在如下B::test实现中哪几个函数调用是合法的:
void B::test()
{
int x = 0;
int *p = NULL;
f(); //(1)
f(x); //(2)
f(&x); //(3)
f(&p); //(4)
};
A.(1)(2)(3)(4) B.(1)(3)(4) C.(2)(3)(4) D.(3)(4)
答案是D,类成员函数重载:局部同名函数将隐藏而不是重载全局声明,不引入父类名字空间时子类的同名函数不会和父类的构成重载,静态成员函数可以和非静态成员函数构成重载。
补充:
当模板函数与重载函数同时出现在一个程序体内时,C++编译器的求解次序是:
函数默认值
C89标准的C语言不支持函数默认值,C++支持函数默认值,且需要遵循从右向左赋初始值。
inline内联函数
C89没有,在调用点直接展开,不生成符号,没有栈帧的开辟回退,仅在Release版本下生效。一般写在头文件中。
函数重载
C语言不存在函数重载,C++根据函数名参数个数参数类型判断重载,属于静多态,必须同一作用域下才叫重载。
const
C中的const叫只读变量,只是无法做左值的变量;C++中的const是真正的常量,但也有可能退化成c语言的常量,默认生成local符号。
引用
引用底层就是指针,使用时会直接解引用,可以配合const对一个立即数进行引用。
malloc,free && new,delete
见此部分第4点
作用域
C语言中作用域只有两个:局部,全局。C++中则是有:局部作用域,类作用域,名字空间作用域三种。
扩展:
命名空间就是将多个变量和函数等包含在内,使其不会与命名空间外的任何变量和函数等发生重命名的冲突。
在其中的很多实例中,都有这么一条语句:using namespace std;,即使用命名空间std,其作用就是规定该文件中使用的标准库函数都是在标准命名空间std中定义的。
使用命名空间成员的方法
(1)使用命名空间别名
namespace TV=Television;
TV::func();
(2)使用using命名空间成员名
using TV::func();
以上语句声明:在本作用域(using语句所在的作用域)中会用到命名空间TV中的成员函数func(),在本作用域中如果使用该命名空间成员时,不必再用命名空间限定。
(3)使用using namespace命名空间成员名
在用using namespace声明的作用域中,命名空间的成员就好像在全局域声明的一样。因此可以不必用命名空间限定。显然这样的处理对写程序比较方便。但是如果同时用using namespace声明多个命名空间时,往往容易出错,因为两个命名空间可能会有同名的类和函数。
(4)无名的命名空间
由于命名空间没有名字,在其他文件中显然无法引用,它只在本文件的作用域内有效。在本程序中的其他文件中也无法使用该fun函数,也就是把fun函数的作用域限制在本文件范围中。在C浯言中可以用static声明一个函数,其作用也是使该函数的作用域限于本文件。C++保留了用static声明函数的用法,同时提供了用无名命名空间来实现这一功能。
(5)标准命名空间std
标准C++库的所有的标识符都是在一个名为std的命名空间中定义的,或者说标准头文件(如iostream)中函数、类、对象和类模板是在命名空间std中定义的。在std中定义和声明的所有标识符在本文件中都可以作为全局量来使用。但是应当绝对保证在程序中不出现与命名空间std的成员同名的标识符。
由于在命名空间std中定义的实体实在太多,有时程序设计人员也弄不请哪些标识符已在命名空间std中定义过,为减少出错机会,有的专业人员喜欢用若干个using命名空间成员声明来代替using namespace命名空间声明。但是目前所用的C++库大多是几年前开发的,当时并没有命名空间,库中的有关内容也没有放在std命名空间中,因而在程序中不必对std进行声明。
参考:
【C++】命名空间(namespace)详解
【C++研发面试笔记】3. 命名空间与内存管理
C语言中的结构体只有数据成员,无函数成员;C++语言中的结构可有数据成员和函数成员。
在缺省情况下,结构体中的数据成员和成员函数都是公有的,而在类中是私有的。
一般我们仅在描述数据成员时使用结构,当既有数据成员又有成员函数时使用类。
C语言中有数据和函数。函数部分放在代码区,数据分为两类:局部的和全局的,它们的区别在于放在静态数据区还是堆栈中。而且全局变量和静态变量是在函数执行前就创建好的。
C语言又有一个规定:全局区不能有可执行代码 ,可执行代码必须进入函数中。但是C语言中的函数都是全局的,这就导致函数不能嵌套定义:嵌套定义导致函数内部定义的函数成了局部函数。所以要解决各个函数的执行问题只能通过函数的嵌套调用。这时就需要有一个函数首先被执行,来调用其他一系列的函数,完成程序的功能,而这个第一个调用的函数就是main函数。
(1)指针:指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元;而引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名而已。如:
int a=1;int *p=&a;
int a=1;int &b=a;
上面定义了一个整形变量和一个指针变量p,该指针变量指向a的存储单元,即p的值是a存储单元的地址。
而下面2句定义了一个整形变量a和这个整形a的引用b,事实上a和b是同一个东西,在内存占有同一个存储单元。
(2)可以有const指针,但是没有const引用;
(3)指针可以有多级,但是引用只能是一级(int **p;合法 而 int &&a是不合法的)
(4)指针的值可以为空,但是引用的值不能为NULL,并且引用在定义的时候必须初始化;
(5)指针的值在初始化后可以改变,即指向其它的存储单元,而引用在进行初始化后就不会再改变了。
(6)"sizeof引用"得到的是所指向的变量(对象)的大小,而"sizeof指针"得到的是指针本身的大小;
(7)指针和引用的自增(++)运算意义不一样;指针:地址后移(不一定是1),引用:内容/值加一
属性
new和delete是C++关键字,需要编译器支持;malloc和free是库函数,需要头文件支持。
参数
使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸。
返回类型
new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。
自定义类型
new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。
malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。
new可以调用malloc(),但malloc不能调用new。
重载
C++允许重载new/delete操作符,malloc不允许重载。
内存区域
new做两件事:分配内存和调用类的构造函数,delete是:调用类的析构函数和释放内存。而malloc和free只是分配和释放内存。
new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。自由存储区不等于堆,如上所述,布局new就可以不位于堆中。
分配失败
new内存分配失败时,会抛出bac_alloc异常。malloc分配内存失败时返回NULL。
内存泄漏
内存泄漏对于new和malloc都能检测出来,而new可以指明是哪个文件的哪一行,malloc确不可以。
构造函数是与类同名,没有返回值的特殊成员函数。一般用于初始化类的数据成员,每当创建一个对象时(包括使用new动态创建对象,不包括创建一个指向对象的指针),编译系统就自动调用构造函数,类的构造函数一般是公有的(public)。构造函数可以重载。
析构函数的功能是当对象被撤消时,释放该对象占用的内存空间。
析构函数被自动调用的三种情况
(1) 一个动态分配的对象被删除,即使用delete删除对象时,编译系统会自动调用析构函数;
(2) 程序运行结束时;
(3) 一个编译器生成的临时对象不再需要时。
拷贝构造函数的功能是用一个已有的对象来初始化一个被创建的同类对象。拷贝构造函数的声明形式为:
类名(类名&对象名);
自动调用拷贝构造函数的四种情况:
① 用类的一个对象去初始化另一个对象
cat cat1;
cat cat2(cat1); /*创建cat2时系统自动调用拷贝构造函数,用cat1初始化cat2*/
② 用类的一个对象去初始化另一个对象时的另外一种形式
cat cat2=cat1; // 注意并非cat cat1,cat2; cat2=cat1;
③ 对象作为函数参数传递时,调用拷贝构造函数。
f(cat a){ } // 定义f函数,形参为cat类对象
cat b; // 定义对象b
f(b); // 进行f函数调用时,系统自动调用拷贝构造函数
④ 如果函数的返回值是类的对象,函数调用返回时,调用拷贝构造函数。
cat f() // 定义f函数,函数的返回值为cat类的对象
{ cat a;
…
return a;
}
cat b; // 定义对象b
b=f(); // 调用f函数,系统自动调用拷贝构造函数
(1) 在类定义体中使用保护成员
保护段成员可以被它的派生类访问。
(2) 将派生类声明为基类的友元类,以访问基类的私有成员
(3) 派生类使用基类提供的接口间接使用基类的私有成员。
简单地说,那些被virtual关键字修饰的成员函数,就是虚函数。虚函数的作用,用专业术语来解释就是实现多态性(Polymorphism),多态性是将接口与实现进行分离;用形象的语言来解释就是实现以共同的方法,但因个体差异,而采用不同的策略。
说明: C++知识点这部分详细参考我之前写的一篇文章 C++软件开发工程师概念手册
<空缺>
在main函数执行之前,需要执行什么程序?
从浏览器输入一个URL(www.baidu.com)后执行全过程
总结:
执行过程:
(1) 浏览器获取输入的域名www.baidu.com
(2) 浏览器向DNS请求解析www.baidu.com的IP地址
(3) 域名系统DNS解析出百度服务器的IP地址
(4) 浏览器发出HTTP请求,请求百度首页
(5) 浏览器与该服务器建立TCP连接(默认端口号80)
(6) 服务器通过HTTP响应把首页文件发送给浏览器
(7) TCP连接释放
(8) 浏览器将首页文件进行解析,并将Web页显示给用户。
DNS查找过程
(1)浏览器会检查缓存中有没有这个域名对应的解析过的IP地址,如果缓存中有,这个解析过程就将结束。
(2)如果用户的浏览器缓存中没有,浏览器会查找操作系统缓存(hosts文件)中是否有这个域名对应的DNS解析结果。
(3)若还没有,此时会发送一个数据包给DNS服务器,DNS服务器找到后将解析所得IP地址返回给用户。
TCP和UDP的区别分析与总结
UDP | TCP | |
---|---|---|
是否连接 | 无连接 | 面向连接 |
是否可靠 | 不可靠传输,不使用流量控制和拥塞控制 | 可靠传输,使用流量控制和拥塞控制 |
连接对象个数 | 支持一对一,一对多,多对一和多对多交互通信 | 只能是一对一通信 |
传输方式 | 面向报文 | 面向字节流 |
首部开销 | 首部开销小,仅8字节 | 首部最小20字节,最大60字节 |
适用场景 | 适用于实时应用(IP电话、视频会议、直播等) | 适用于要求可靠传输的应用,例如文件传输 |
TCP三次握手、四次挥手及常见面试题全集
1xx (临时响应)表示临时响应并需要请求者继续执行操作的状态代码。
100 (继续) 请求者应当继续提出请求。 服务器返回此代码表示已收到请求的第一部分,正在等待其余部分。
2xx (成功)表示成功处理了请求的状态代码。
200 (成功) 服务器已成功处理了请求。 通常,这表示服务器提供了请求的网页。
3xx (重定向) 表示要完成请求,需要进一步操作。 通常,这些状态代码用来重定向。
300 (多种选择) 针对请求,服务器可执行多种操作。 服务器可根据请求者 (useragent)选择一项操作,或提供操作列表供请求者选择。
4xx (请求错误) 这些状态代码表示请求可能出错,妨碍了服务器的处理。
400 (错误请求) 服务器不理解请求的语法。
401 (未授权) 请求要求身份验证。 对于需要登录的网页,服务器可能返回此响应。
402 该状态码是为了将来可能的需求而预留的。
403 (禁止) 服务器拒绝请求。
404 (未找到) 服务器找不到请求的网页。
5xx (服务器错误)这些状态代码表示服务器在尝试处理请求时发生内部错误。这些错误可能是服务器本身的错误,而不是请求出错。
500 (服务器内部错误) 服务器遇到错误,无法完成请求。
600 源站没有返回响应头部,只返回实体内容
1、https协议需要到ca申请证书,一般免费证书较少,因而需要一定费用。
2、http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。
3、http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
4、http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。
一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。线程依赖于进程而存在。
进程在执行过程中拥有独立的内存单元,而多个线程共享进程的内存。(资源分配给进程,同一进程的所有线程共享该进程的所有资源。同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量。)
进程是资源分配的最小单位,线程是CPU调度的最小单位;
系统开销: 由于在创建或撤消进程时,系统都要为之分配或回收资源,如内存空间、I/O设备等。因此,操作系统所付出的开销将显著地大于在创建或撤消线程时的开销。类似地,在进行进程切换时,涉及到整个当前进程CPU环境的保存以及新被调度运行的进程的CPU环境的设置。而线程切换只须保存和设置少量寄存器的内容,并不涉及存储器管理方面的操作。可见,进程切换的开销也远大于线程切换的开销。
通信: 由于同一进程中的多个线程具有相同的地址空间,致使它们之间的同步和通信的实现,也变得比较容易。进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。在有的系统中,线程的切换、同步和通信都无须操作系统内核的干预
进程编程调试简单可靠性高,但是创建销毁开销大;线程正相反,开销小,切换速度快,但是编程调试相对复杂。
进程间不会相互影响 ;线程一个线程挂掉将导致整个进程挂掉
进程适应于多核、多机分布;线程适用于多核
进程间通信主要包括管道pipe、有名管道FIFO、消息队列MessageQueue、共享存储、信号量Semaphore、信号Signal、套接字Socket。
(1)管道
管道,通常指无名管道,是 UNIX 系统IPC最古老的形式。
特点:
(2)有名管道FIFO
FIFO,也称为命名管道,它是一种文件类型。
特点:
(3)消息队列
消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识。
特点:
(4)信号量
信号量(semaphore)与已经介绍过的 IPC 结构不同,它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
特点:
(5)共享内存
共享内存(Shared Memory),指两个或多个进程共享一个给定的存储区。
特点:
(5)套接字
socket也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同主机之间的进程通信。
为了防止不同进程同一时刻在物理内存中运行而对物理内存的争夺和践踏,采用了虚拟内存。
虚拟内存技术使得不同进程在运行过程中,它所看到的是自己独自占有了当前系统的4G内存。所有进程共享同一物理内存,每个进程只把自己目前需要的虚拟内存空间映射并存储到物理内存上。 事实上,在每个进程创建加载时,内核只是为进程“创建”了虚拟内存的布局,具体就是初始化进程控制表中内存相关的链表,实际上并不立即就把虚拟内存对应位置的程序数据和代码(比如.text .data段)拷贝到物理内存中,只是建立好虚拟内存和磁盘文件之间的映射就好(叫做存储器映射),等到运行到对应的程序时,才会通过缺页异常,来拷贝数据。还有进程运行过程中,要动态分配内存,比如malloc时,也只是分配了虚拟内存,即为这块虚拟内存对应的页表项做相应设置,当进程真正访问到此数据时,才引发缺页异常。
请求分页系统、请求分段系统和请求段页式系统都是针对虚拟内存的,通过请求实现内存与外存的信息置换。
死锁是指两个或两个以上进程在执行过程中,因争夺资源而造成的下相互等待的现象。死锁发生的四个必要条件如下:
解决死锁的方法即破坏上述四个条件之一,主要方法如下:
1)平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2)性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
1)数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行。
2)结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。
3)结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。
可以通过预编译命令#pragma pack(n),n=1,2,4,8,16来改变这一系数,其中的n就是指定的“对齐系数”。
#pragma pack(2)
struct AA {
int a; //长度4 > 2 按2对齐;偏移量为0;存放位置区间[0,3]
char b; //长度1 < 2 按1对齐;偏移量为4;存放位置区间[4]
short c; //长度2 = 2 按2对齐;偏移量要提升到2的倍数6;存放位置区间[6,7]
char d; //长度1 < 2 按1对齐;偏移量为7;存放位置区间[8];共九个字节
};
#pragma pack()
比较常见的内存替换算法有:FIFO,LRU,LFU,LRU-K,2Q。
思想:最近刚访问的,将来访问的可能性比较大。
实现:使用一个队列,新加入的页面放入队尾,每次淘汰队首的页面,即最先进入的数据,最先被淘汰。
弊端:无法体现页面冷热信息
思想:如果数据过去被访问多次,那么将来被访问的频率也更高。
实现:每个数据块一个引用计数,所有数据块按照引用计数排序,具有相同引用计数的数据块则按照时间排序。每次淘汰队尾数据块。
开销:排序开销。
弊端:缓存颠簸。
思想:如果数据最近被访问过,那么将来被访问的几率也更高。
实现:使用一个栈,新页面或者命中的页面则将该页面移动到栈底,每次替换栈顶的缓存页面。
优点:LRU算法对热点数据命中率是很高的。
缺陷:
思想:最久未使用K次淘汰算法。
LRU-K中的K代表最近使用的次数,因此LRU可以认为是LRU-1。LRU-K的主要目的是为了解决LRU算法“缓存污染”的问题,其核心思想是将“最近使用过1次”的判断标准扩展为“最近使用过K次”。
相比LRU,LRU-K需要多维护一个队列,用于记录所有缓存数据被访问的历史。只有当数据的访问次数达到K次的时候,才将数据放入缓存。当需要淘汰数据时,LRU-K会淘汰第K次访问时间距当前时间最大的数据。
实现:
针对问题: LRU-K的主要目的是为了解决LRU算法“缓存污染”的问题,其核心思想是将“最近使用过1次”的判断标准扩展为“最近使用过K次”。
类似LRU-2。使用一个FIFO队列和一个LRU队列。
实现:
针对问题:LRU的缓存污染
弊端: 当FIFO容量为2时,访问负载是:ABCABCABC会退化为FIFO,用不到LRU。
这部分我面试的时候,问的较多且我掌握的也不多,我就不详细总结写了,我也在参考这几篇学习。
数据库常见面试题(附答案)
索引就一种特殊的查询表,数据库的搜索可以利用它加速对数据的检索。它很类似与现实生活中书的目录,不需要查询整本书内容就可以找到想要的数据。索引可以是唯一的,创建索引允许指定单个列或者是多个列。缺点是它减慢了数据录入的速度,同时也增加了数据库的尺寸大小。
1.原子性:要么都执行,要么都不执行。
2.一致性:合法的数据才可以被写入。
3.隔离性:允许多个用户并发访问。
4.持久性:事务结束后,事务处理的结果必须得到固化。即一旦提交,对数据库改变是永久的。
事务的四大特性是:
推荐力扣和剑指offer,需要电子版PDF的微信公众号后台回复“剑指offer”获取网盘链接。
题目描述:给出两个字符串,求出这样的一 个最长的公共子序列的长度:子序列 中的每个字符都能在两个原串中找到, 而且每个字符的先后顺序和原串中的 先后顺序一致。
Sample Input
abcfbc abfcab
programming contest
abcd mnp
Sample Output
4
2
0
最长公共子序列(POJ1458)
温馨提示: 如果你点开这个链接,本题以下的内容你可以不看了,直接跳到扩展部分看“最长公共子序列(LCS问题)”。
算法思路:
把两个字符串分别以行和列组成一个二维矩阵。
比较二维矩阵中每个点对应行列字符中否相等,相等的话值设置为1,否则设置为0。
通过查找出值为1的最长对角线就能找到最长公共子串。
针对于上面的两个字符串我们可以得到的二维矩阵如下:
从上图可以看到,str1和str2共有5个公共子串,但最长的公共子串长度为5。
为了进一步优化算法的效率,我们可以再计算某个二维矩阵的值的时候顺便计算出来当前最长的公共子串的长度,即某个二维矩阵元素的值由record[i][j]=1演变为record[i][j]=1 +record[i-1][j-1],这样就避免了后续查找对角线长度的操作了。修改后的二维矩阵如下:
递推公式为:
当A[i] != B[j],dp[i][j] = 0
当A[i] == B[j],
若i = 0 || j == 0,dp[i][j] = 1
否则 dp[i][j] = dp[i - 1][j - 1] + 1
实现源代码:
暴力法:
string getLCS(string str1, string str2) {
vector<vector<int> > record(str1.length(), vector<int>(str2.length()));
int maxLen = 0, maxEnd = 0;
for(int i=0; i<static_cast<int>(str1.length()); ++i)
for (int j = 0; j < static_cast<int>(str2.length()); ++j) {
if (str1[i] == str2[j]) {
if (i == 0 || j == 0) {
record[i][j] = 1;
}
else {
record[i][j] = record[i - 1][j - 1] + 1;
}
}
else {
record[i][j] = 0;
}
if (record[i][j] > maxLen) {
maxLen = record[i][j];
maxEnd = i; //若记录i,则最后获取LCS时是取str1的子串
}
}
return str1.substr(maxEnd - maxLen + 1, maxLen);
}
动态规划法:
public int getLCS(String s, String t) {
if (s == null || t == null) {
return 0;
}
int result = 0;
int sLength = s.length();
int tLength = t.length();
int[][] dp = new int[sLength][tLength];
for (int i = 0; i < sLength; i++) {
for (int k = 0; k < tLength; k++) {
if (s.charAt(i) == t.charAt(k)) {
if (i == 0 || k == 0) {
dp[i][k] = 1;
} else {
dp[i][k] = dp[i - 1][k - 1] + 1;
}
result = Math.max(dp[i][k], result);
} else {
dp[i][k] = 0;
}
}
}
return result;
}
简化一下递推公式:
当A[i] != B[j],dp[i][j] = 0
否则 dp[i][j] = dp[i - 1][j - 1] + 1
全部都归结为一个公式即可,二维数组默认值为0
行、列都多一行,更适应公式。
参考:最长公共子串(动态规划)
扩充:动态规划经典例题——最长公共子序列和最长公共子串(python)
最长公共子序列(LCS问题)
题目描述:
给定两个字符串A和B,长度分别为m和n,要求找出他们最长的公共子序列,并返回其长度。例如:
A = “HelloWorld”;
B = “loop”
则A与B的最长公共子序列为“loo”,返回的长度为5.
动态规划算法是面试时常考的内容,更多编程练习可参考如下几个题目(附解析和答案)。
考虑到篇幅和冗余,需要OneNote上的笔记的也可以私信我,我发给你。
var nums = new int[100];
var random = new Random();
for (int i = 0; i < 100; i++)
{
nums[i] = i;
}
for (int i = 0; i < 100; i++)
{
var r = random.Next(i, 99);
Swap(ref nums[i], ref nums[r]);
}
此题解析参考:
算法:如何高效产生m个n范围内的不重复随机数(m<=n)
如何高效产生m个n范围内的不重复随机数(m<=n)
分析: 要查询出每门课程都大于80分的学生姓名,因为一个学生有多门课程,可能所有课程都大于80分,可能有些课程大于80分,另外一些课程少于80分,也可能所有课程都小于80分,那么我们要查找出所有大于80分的课程的学生姓名,我们可以反向思考,找出课程小于80分(可以找出有一些课程小于80分,所有课程小于80分的学生)的学生姓名再排除这些学生剩余的就是所有课程都大于80分的学生姓名了,
-- 查询各科成绩都大于90的学生姓名
id name course score
1 小白 语文 91
2 小白 数学 88
3 小黑 语文 79
4 小黑 数学 92
5 小花 语文 99
6 小花 数学 95
7 小花 英语 96
实现源代码:
--创建表aa
create table aa(
name varchar(10),
kecheng varchar(10),
fengshu int
)
--插入数据到表aa中
insert into aa values('张三','语文',81)
insert into aa values('张三','数学',75)
insert into aa values('李四','语文',76)
insert into aa values('李四','数学',90)
insert into aa values('王五','语文',81)
insert into aa values('王五','数学',100)
insert into aa values('王五','英语',90)
--用一条SQL语句查询出每门课都大于80分的学生姓名
select distinct name from aa where name not in (select distinct name from aa where fengshu<=80)
此题解析参考:
补充几道j经典常考题目:
试题1: 8个试剂,其中一个有毒,最少多少只小白鼠能检测出有毒试剂
方法1:(二进制编码)
用3只小鼠,能组合成8种状态。
第一只喂食【1、3、5、7】四只试剂
第二只喂食【2、3、6、7】四只试剂
第三只喂食【4、5、6、7】四只试剂
[3 2 1]
1 0 0 1 = 1 # 2、3没死,1死了,说明第1支试剂有毒
2 0 1 0 = 2 # 1、3没死,2死了,说明第2支试剂有毒
3 0 1 1 = 3 # 3没死,1、2死了,说明第3支试剂有毒
4 1 0 0 = 4 # 1、2没死,3死了,说明第4支试剂有毒
5 1 0 1 = 5 # 2没死,1、3死了,说明第5值试剂有毒
6 1 1 0 = 6 # 1没死,2、3死了,说明第6值试剂有毒
7 1 1 1 = 7 # 三只都死了,说明第7值试剂有毒
8 0 0 0 = 0 # 三只都没死,说明第8值试剂有毒
方法2:(二分查找法)
二分法,每次把试剂分成两堆,然后用两只小鼠测试,如果一只死掉了,那么就能确定哪一堆有毒。然后继续分。因此,小鼠的数量就是试剂能被二分的次数。8只试剂能被二分3次,所以就需要3只小鼠。
同理,1000种药剂要检验出有效的那一种,也至少需要10只。
这是一道典型的二分法查找的算法题,一般情况下,我们使用的都是串行的二分法,如果这道题没有时间限制,我们就可以使用串行的二分法找到毒药,步骤如下:
(1)首先,给试剂编号,1~1000
(2)给第一只小白鼠喂1~500号混合的试剂,等待24小时,
(3)如果小白鼠死亡,则给第二只喂1~250号混合的试剂,否则,喂501~750号试剂
(4)依次进行二分,可以看出,这样最多需要10只小白鼠就能找到毒药。
但是,这道题有时间限制,所以我们要同时给一定的小白鼠喂药,然后从小白鼠的死亡情况找出毒药。步骤如下:
(1)第一只小白鼠:1~500
(2)第二只小白鼠:1~250 + 501~750
(3)第三只小白鼠:1~125 + 251~500 + 501~625 + 751+875
……….
依次下去,由于2^9 < 1000 < 2^10,所以需要10只小白鼠才能找到毒药。
试题2: 跳台阶问题
有n阶台阶,你可以一次跳一阶,也可以跳2阶,请问有多少种跳法。
1 思路
首先我们考虑最简单的情况。如果只有1级台阶,那么显然只一种跳法。如果有2级台阶,那就有两种跳法:一种是分两次跳,每次跳1级;另一种是一次跳2级。
接着,我们来讨论一般情况。我们把n级台阶时的跳法看成是n的函数,记为f(n)。当n>2时,第一次跳的时候就有两种不同的选择:一是第一次只跳1级,此时跳法数目等于后面剩下的n-1级台阶的跳法数目,即为f(n-1);另外一种选择是跳一次跳2级,此时跳法数目等于后面剩下的n-2级台阶的跳法数目,即为f(n-2)。因此n级台阶的不同跳法的总数f(n)=f(n-1)+f(n-2)。分析到这里,我们不难看出这实际上就是斐波那契数列了。
2 程序实现:
C++
class Solution {
public:
int jumpFloor(int number) {
if(number <= 0){
return 0;
}
else if(number < 3){
return number;
}
int first = 1, second = 2, third = 0;
for(int i = 3; i <= number; i++){
third = first + second;
first = second;
second = third;
}
return third;
}
};
Python
# -*- coding:utf-8 -*-
class Solution:
def jumpFloor(self, number):
# write code here
if number < 3:
return number
first, second, third = 1, 2, 0
for i in range(3, number+1):
third = first + second
first = second
second = third
return third
关注微信公众号:迈微电子研发社,获取更多精彩内容,首发于个人公众号。
知识星球:社群旨在秋招/春招准备攻略(含刷题)、面经和内推机会、学习路线、知识题库等。