2020年京东Linux c/c++后台开发精选面试题及答案

1、求1~N的最小公倍数。把每个数字分解质因数,算他们每个质因数的贡献,然后乘起来。我的代码没写好(算质因数不用这么慢的)
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define maxn 100009
int fact[maxn];
bool prime[maxn];
ll mod = 987654321;
int cal(int t, int p) {
int cnt = 0;
while(t % p == 0) {
cnt++;
t /= p;
}
return cnt;
}
void first() {
memset(prime, true, sizeof(prime));
prime[1] = false;
for(int i = 2; i <= 100000; i++) {
int top = sqrt(i);
for(int j = 2; j <= top; j++) {
if(i % j == 0) {
prime[i] = false;
break;
}
}
}
}
void solve(int Limit) {
first();
for (int i = 2; i <= Limit; i++) {
int top = sqrt(i);
for (int j = 2; j <= top; j++) {
if(prime[j] && i % j == 0) {
fact[j] = max(fact[j], cal(i, j));
}
}
if(prime[i])
fact[i] = max(fact[i], 1);
}
}
int main() {
ll n;
cin>>n;
solve(n);
ll ans = 1;
for(ll i = 1; i <= n; i++) {
for(ll j = 1; j <= fact[i]; j++) {
ans = ans * i % mod;
}
}
cout<<ans<<endl;
return 0;
}
2、去掉字符串构成回文。其实是经典的求回文子序列个数。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll f[59][59];
string str;
ll dfs(int i, int j) {
if(i > j) {
return 0;
}
if(i == j) {
f[i][j] = 1;
return f[i][j];
}
if(f[i][j] != 0) {
return f[i][j];
}
f[i][j] = dfs(i, j - 1) + dfs(i + 1, j) - dfs(i + 1, j - 1);
if(str[i] == str[j])
f[i][j] += dfs(i + 1, j - 1) + 1;
return f[i][j];
}
int main() {
cin>>str;
int len = str.length();
cout<<dfs(0, len - 1)<<endl;
return 0;
}
3、去掉字符串git pull和git merge的区别?
你修改好了代码,先要提交

git commit -am “commit message"

然后有两种方法来把你的代码和远程仓库中的代码合并:

a. git pull这样就直接把你本地仓库中的代码进行更新但问题是可能会有冲突(conflicts),个人不推荐。

b. 先git fetch origin(把远程仓库中origin最新代码取回),再git merge origin/master(把本地代码和已取得的远程仓库最新代码合并),如果你的改动和远程仓库中最新代码有冲突,会提示,再去一个一个解决冲突,最后再从1开始。

c. 如果没有冲突,git push origin master,把你的改动推送到远程仓库中。

4、内存分配方式有几种?
内存分配方式:

(1)符号起始的区块(.bss段):通常指的是存放程序中未初始化或者初始化为0的变量的和静态数据的区域。bss属于静态内存分配,程序结束后静态资源变量由系统自动释放。

(2)数据段:通常指存放程序中已初始化的全局变量的一块内存区域。也属于静态内存分配。

(3)代码段:有时也叫文本段,通常指的是用来存放程序执行代码(包含类成员函数和全局函数及其他函数代码),这部分区域的大小在程序运行前就已经确定,也有可能包含一些只读的常数变量,例如字符串变量。

(4)堆(heap):用于存放进程运行中被动态分配的内存段,大小不固定。当进程调用malloc或者new等函数时,新分配的内存就被动态添加到堆上(堆被扩张),当使用free或者delete等函数释放内存时,被释放的内存从堆中被删除。需要注意的是,它与数据结构中的堆是两回事,它的分配方式类似于链表。

(5)栈(stack):存放程序临时创建的局部变量,不包括static声明的变量,static意味着在数据段中存放。除此之外,当函数被调用时,其参数也会被压到栈中,并在调用结束后,函数的返回值也会被放到栈中。栈由编译器自动释放。其操作方式类似于数据结构中的栈。栈内存分配运算内置于处理器的指令集中,一般使用寄存器来存取,效率很高,但是分配的内存容量有限。

5、分布式服务接口请求的顺序性如何保证?
①首先,一般来说,从业务逻辑上最好设计系统不需要这种顺序的保证,因为一旦引入顺序性保障,会导致系统复杂度的上升,效率会降低,对于热点数据会压力过大等问题。

②操作串行化。

首先使用一致性hash负载均衡策略,将同一个id的请求都分发到同一个机器上面去处理,比如订单可以根据订单id。如果处理的机器上面是多线程处理的,可以引入内存队列去处理,将相同id的请求通过hash到同一个队列当中,一个队列只对应一个处理线程。

③最好能将多个操作合并成一个操作。

6、分编译时多态和运行时多态的区别?
编译时多态

主要是方法的重载,通过参数列表的不同来区分不同的方法。

运行时多态

也叫作动态绑定,一般是指在执行期间(非编译期间)判断引用对象的实际类型,根据实际类型判断并调用相应的属性和方法。主要用于继承父类和实现接口时,父类引用指向子类对象。

7、常用的内存管理方法有哪几种?
段式

页式

段页式

8、给定字符串(ASCII码0-255)数组,请在不开辟额外空间的情况下删除开始和结尾处的空格,并将中间的多个连续的空格合并成一个。例如:“i am a little boy.”,变成“i am a little boy”,C++语言实现,不要用伪代码作答,函数输入输出请参考如下的函数原型:
C++函数原型:
void FormatString(char str[],int len){
}
#include<stdio.h>
#include <string.h>

void FormatString(char str[],int len)
{
if (str == NULL || len <= 0) {
return;
}
int i = 0;
int j = 0;
if (str[i] == ’ ') {
while (str[i] == ’ ') {
++i;
}
}
while (str[i] != ‘\0’) {
if (str[i] == ’ ’ && str[i + 1] == ’ ’ || str[i + 1] == ‘\0’) {
++i;
continue;
}
str[j++] = str[i++];
}
str[j] = ‘\0’;
}

int main() {
char a[] = " i am a little boy. “;
int len = strlen(a);
printf(”%d\n",len);
FormatString(a,len);
printf("%d\n",strlen(a));
printf("%s\n",a);
return 0;
}
9、B-树和B+树的区别是什么?
B-树是一种多路搜索树(并不是二叉的。),一颗m阶的B-树,或为空树,或者定义任意非叶子结点最多只有M个儿子。

且M>2;根结点的儿子数为[2, M]。

除根结点以外的非叶子结点的儿子数为[M/2]。

每个结点存放至少M/2-1(取上整)和至多M-1个关键字;(至少2个关键字)非叶子结点的关键字个数=指向儿子的指针个数-1;

B+树, B+树是B-树的变体,也是一种多路搜索树:其定义基本与B-树同。

B-树是一种 多路搜索 树(并不是二叉的。),一颗 m 阶 的B-树,或为空树,或 者定 义任意非叶子结点最 多只 有M 个儿子。

且M>2;根 结 点的儿 子 数 为 [2, M]。

除根结 点以 外的非叶子结点的儿子数为[M/2]。

每个结 点存放至 少M/2-1 (取上整) 和至 多 M- 1 个 关键 字;(至少2个关键字)非叶子结点的关 键 字个数 =指 向儿子 指针个数-1;

B+树, B+树是B-树的变体, 也是一种多路搜索树:其定义基本与B-树同。

10、分布式服务接口的幂等性如何设计(比如不能重复扣款)?
所谓幂等性,就是说一个接口,多次发起同一个请求,你这个接口得保证结果是准确的,比如不能多扣款,不能多插入一条数据,不能将统计值多加了1。这就是幂等性,不给大家来学术性词语了。

其实保证幂等性主要是三点:

(1)对于每个请求必须有一个唯一的标识,举个例子:订单支付请求,肯定得包含订单id,一个订单id最多支付一次,对吧

(2)每次处理完请求之后,必须有一个记录标识这个请求处理过了,比如说常见的方案是在mysql中记录个状态啥的,比如支付之前记录一条这个订单的支付流水,而且支付流水采

(3)每次接收请求需要进行判断之前是否处理过的逻辑处理,比如说,如果有一个订单已经支付了,就已经有了一条支付流水,那么如果重复发送这个请求,则此时先插入支付流水,orderId已经存在了,唯一键约束生效,报错插入不进去的。然后你就不用再扣款了。

(4)上面只是给大家举个例子,实际运作过程中,你要结合自己的业务来,比如说用redis用orderId作为唯一键。只有成功插入这个支付流水,才可以执行实际的支付扣款。

要求是支付一个订单,必须插入一条支付流水,order_id建一个唯一键,unique key

所以你在支付一个订单之前,先插入一条支付流水,order_id就已经进去了

你就可以写一个标识到redis里面去,set order_id payed,下一次重复请求过来了,先查redis的order_id对应的value,如果是payed就说明已经支付过了,你就别重复支付了

然后呢,你再重复支付这个订单的时候,你写尝试插入一条支付流水,数据库给你报错了,说unique key冲突了,整个事务回滚就可以了

来保存一个是否处理过的标识也可以,服务的不同实例可以一起操作redis。

11、分C和C++分配释放内存区别?
0.属性

new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件支持。

1.参数

使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸。

2.返回类型

new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。

3.分配失败

new内存分配失败时,会抛出bac_alloc异常。malloc分配内存失败时返回NULL。

4.自定义类型

new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。

malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。

5.重载

C++允许重载new/delete操作符,特别的,布局new的就不需要为对象分配内存,而是指定了一个地址作为内存起始区域,new在这段内存上为对象调用构造函数完成初始化工作,并返回此地址。而malloc不允许重载。

6.内存区域

new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。自由存储区不等于堆,如上所述,布局new就可以不位于堆中。

12、进程和纯种的区别介绍?
1、首先是定义

进程:是执行中一段程序,即一旦程序被载入到内存中并准备执行,它就是一个进程。进程是表示资源分配的的基本概念,又是调度运行的基本单位,是系统中的并发执行的单位。

线程:单个进程中执行中每个任务就是一个线程。线程是进程中执行运算的最小单位。

2、一个线程只能属于一个进程,但是一个进程可以拥有多个线程。多线程处理就是允许一个进程中在同一时刻执行多个任务

3、线程是一种轻量级的进程,与进程相比,线程给操作系统带来侧创建、维护、和管理的负担要轻,意味着线程的代价或开销比较小。

4、线程没有地址空间,线程包含在进程的地址空间中。线程上下文只包含一个堆栈、一个寄存器、一个优先权,线程文本包含在他的进程 的文本片段中,进程拥有的所有资源都属于线程。所有的线程共享进程的内存和资源。 同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段, 寄存器的内容,栈段又叫运行时段,用来存放所有局部变量和临时变量。

5、父和子进程使用进程间通信机制,同一进程的线程通过读取和写入数据到进程变量来通信。

6、进程内的任何线程都被看做是同位体,且处于相同的级别。不管是哪个线程创建了哪一个线程,进程内的任何线程都可以销毁、挂起、恢复和更改其它线程的优先权。线程也要对进程施加控制,进程中任何线程都可以通过销毁主线程来销毁进程,销毁主线程将导致该进程的销毁,对主线程的修改可能影响所有的线程。

7、子进程不对任何其他子进程施加控制,进程的线程可以对同一进程的其它线程施加控制。子进程不能对父进程施加控制,进程中所有线程都可以对主线程施加控制。

相同点:

进程和线程都有ID/寄存器组、状态和优先权、信息块,创建后都可更改自己的属性,都可与父进程共享资源、都不鞥直接访问其他无关进程或线程的资源。

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

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

分布式的好处:

系统之间的耦合度大大降低,可以独立开发、独立部署、独立测试,系统与系统之间的边界非常明确,排错也变得相当容易,开发效率大大提升。

系统之间的耦合度降低,从而系统更易于扩展。我们可以针对性地扩展某些服务。假设这个商城要搞一次大促,下单量可能会大大提升,因此我们可以针对性地提升订单系统、产品系统的节点数量,而对于后台管理系统、数据分析系统而言,节点数量维持原有水平即可。

服务的复用性更高。比如,当我们将用户系统作为单独的服务后,该公司所有的产品都可以使用该系统作为用户系统,无需重复开发。

14、智能指针是线程安全的吗?哪些地方需要考虑线程安全?
1.智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或者–,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2,这样引用计数就乱了,有可能造成资源未释放或者程序崩溃的风险。所以说智能指针中++或–的操作是需要加锁的,也就是说引用计数的操作是线程安全的

2.智能指针的对象存放在堆上,两个线程同时去访问,就会造成线程安全问题.

std::shared_ptr循环引用

struct ListNode
{
int _data;
shared_ptr _prev;
shared_ptr _next;
~ListNode(){ cout << “~ListNode()” << endl; }
};
int main()
{
shared_ptr node1(new ListNode);
shared_ptr node2(new ListNode);
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
node1->_next = node2;
node2->_prev = node1;
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
return 0;
}
node1和node2两个智能指针对象指向两个节点,引用计数变为1,我们不需要手动delete

node1的_next指向node2,node2的_prev指向node1,引用计数变成2

node1和node2析构,引用计数减到1,但是_next还指向下一个节点,_prev指向上一个节点

也就是说_next析构了,node2释放了

也就是说_prev析构了,node1释放了

但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2成员,所以这就叫循环引用,谁都不会释放在这里插入图片描述
解决方案

在引用计数的场景下,把shared_ptr换成weak_ptr就可以了

原理就是,node1->_next = node2; 和 node2->_prev = node1; 时weak_ptr的_next和_prev不会增加node1和node2的引用计数

struct ListNode
{
int _data;
weak_ptr _prev;
weak_ptr _next;
~ListNode(){
cout << “~ListNode()” << endl;
}
};
int main()
{
shared_ptr node1(new ListNode);
shared_ptr node2(new ListNode);
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
node1->_next = node2;
node2->_prev = node1;
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
return 0;
}
如果不是new出来的空间如何用智能指针管理呢?

其实shared_ptr设计了一个删除器来解决这个问题

// 仿函数的删除器
template
struct FreeFunc {
void operator()(T* ptr)
{
cout << “free:” << ptr << endl;
free(ptr);
}
};
template
struct DeleteArrayFunc {
void operator()(T* ptr)
{
cout << “delete[]” << ptr << endl;
delete[] ptr;
}
};
int main()
{
FreeFunc freeFunc;
shared_ptr sp1((int*)malloc(4), freeFunc);
DeleteArrayFunc deleteArrayFunc;
shared_ptr sp2((int*)malloc(4), deleteArrayFunc);
return 0;
}
15、决策树是如何解决过拟合问题的?
一.产生过度拟合数据问题的原因

原因1:样本问题

(1)样本里的噪音数据干扰过大,大到模型过分记住了噪音特征,反而忽略了真实的输入输出间的关系;(什么是噪音数据?)

(2)样本抽取错误,包括(但不限于)样本数量太少,抽样方法错误,抽样时没有足够正确考虑业务场景或业务特点,等等导致抽出的样本数据不能有效足够代表业务逻辑或业务场景;

(3)建模时使用了样本中太多无关的输入变量。

原因2:构建决策树的方法问题

在决策树模型搭建中,我们使用的算法对于决策树的生长没有合理的限制和修剪的话,决策树的自由生长有可能每片叶子里只包含单纯的事件数据或非事件数据,可以想象,这种决策树当然可以完美匹配(拟合)训练数据,但是一旦应用到新的业务真实数据时,效果是一塌糊涂。

二.如何解决过度拟合数据问题

针对原因1的解决方法:

合理、有效地抽样,用相对能够反映业务逻辑的训练集去产生决策树;

针对原因2的解决方法(主要):

剪枝:提前停止树的增长或者对已经生成的树按照一定的规则进行后剪枝。

剪枝的方法

剪枝是一个简化过拟合决策树的过程。有两种常用的剪枝方法:

(1)先剪枝(prepruning):通过提前停止树的构建而对树“剪枝”,一旦停止,节点就成为树叶。该树叶可以持有子集元组中最频繁的类;

先剪枝的方法

有多种不同的方式可以让决策树停止生长,下面介绍几种停止决策树生长的方法:

限制决策树的高度和叶子结点处样本的数目

1.定义一个高度,当决策树达到该高度时就可以停止决策树的生长,这是一种最为简单的方法;

2.达到某个结点的实例具有相同的特征向量,即使这些实例不属于同一类,也可以停止决策树的生长。这种方法对于处理数据中的数据冲突问题非常有效;

3.定义一个阈值,当达到某个结点的实例个数小于该阈值时就可以停止决策树的生长;

4.定义一个阈值,通过计算每次扩张对系统性能的增益,并比较增益值与该阈值的大小来决定是否停止决策树的生长。

(2)后剪枝(postpruning):它首先构造完整的决策树,允许树过度拟合训练数据,然后对那些置信度不够的结点子树用叶子结点来代替,该叶子的类标号用该结点子树中最频繁的类标记。后剪枝的剪枝过程是删除一些子树,然后用其叶子节点代替,这个叶子节点所标识的类别通过大多数原则(majority class criterion)确定。所谓大多数原则,是指剪枝过程中, 将一些子树删除而用叶节点代替,这个叶节点所标识的类别用这棵子树中大多数训练样本所属的类别来标识,所标识的类称为majority class .相比于先剪枝,这种方法更常用,正是因为在先剪枝方法中精确地估计何时停止树增长很困难。

后剪枝的方法

1)REP方法是一种比较简单的后剪枝的方法,在该方法中,可用的数据被分成两个样例集合:一个训练集用来形成学习到的决策树,一个分离的验证集用来评估这个决策树在后续数据上的精度,确切地说是用来评估修剪这个决策树的影响。这个方法的动机是:即使学习器可能会被训练集中的随机错误和巧合规律所误导,但验证集合不大可能表现出同样的随机波动。所以验证集可以用来对过度拟合训练集中的虚假特征提供防护检验。

该剪枝方法考虑将书上的每个节点作为修剪的候选对象,决定是否修剪这个结点有如下步骤组成:

1:删除以此结点为根的子树

2:使其成为叶子结点

3:赋予该结点关联的训练数据的最常见分类

4:当修剪后的树对于验证集合的性能不会比原来的树差时,才真正删除该结点

因为训练集合的过拟合,使得验证集合数据能够对其进行修正,反复进行上面的操作,从底向上的处理结点,删除那些能够最大限度的提高验证集合的精度的结点,直到进一步修剪有害为止(有害是指修剪会减低验证集合的精度)。

REP是最简单的后剪枝方法之一,不过由于使用独立的测试集,原始决策树相比,修改后的决策树可能偏向于过度修剪。这是因为一些不会再测试集中出现的很稀少的训练集实例所对应的分枝在剪枝过如果训练集较小,通常不考虑采用REP算法。

尽管REP有这个缺点,不过REP仍然作为一种基准来评价其它剪枝算法的性能。它对于两阶段决策树学习方法的优点和缺点提供了了一个很好的学习思路。由于验证集合没有参与决策树的创建,所以用REP剪枝后的决策树对于测试样例的偏差要好很多,能够解决一定程度的过拟合问题。

2)PEP,悲观错误剪枝,悲观错误剪枝法是根据剪枝前后的错误率来判定子树的修剪。该方法引入了统计学上连续修正的概念弥补REP中的缺陷,在评价子树的训练错误公式中添加了一个常数,假定每个叶子结点都自动对实例的某个部分进行错误的分类。它不需要像REP(错误率降低修剪)样,需要用部分样本作为测试数据,而是完全使用训练数据来生成决策树,又用这些训练数据来完成剪枝。决策树生成和剪枝都使用训练集, 所以会产生错分。

把一棵子树(具有多个叶子节点)的分类用一个叶子节点来替代的话,在训练集上的误判率肯定是上升的,但是在测试数据上不一定,我们需要把子树的误判计算加上一个经验性的惩罚因子,用于估计它在测试数据上的误判率。对于一棵叶子节点,它覆盖了N个样本,其中有E个错误,那么该叶子节点的错误率为(E+0.5)/N。这个0.5就是惩罚因子,那么对于该棵子树,假设它有L个叶子节点,则该子树的误判率估计为:在这里插入图片描述
剪枝后该子树内部节点变成了叶子节点,该叶子结点的误判个数J同样也需要加上一个惩罚因子,变成J+0.5。那么子树是否可以被剪枝就取决于剪枝后的错误J+0.5在在这里插入图片描述
的标准误差内。对于样本的误差率e,我们可以根据经验把它估计成伯努利分布,那么可以估计出该子树的误判次数均值和标准差在这里插入图片描述
使用训练数据,子树总是比替换为一个叶节点后产生的误差小,但是使用校正的误差计算方法却并非如此。剪枝的条件:当子树的误判个数大过对应叶节点的误判个数一个标准差之后,就决定剪枝:在这里插入图片描述

这个条件就是剪枝的标准。当然并不一定非要大一个标准差,可以给定任意的置信区间,我们设定一定的显著性因子,就可以估算出误判次数的上下界。

16、说说代理设计模式,并画一下代理模式结构图。
所谓代理模式是指客户端并不直接调用实际的对象,而是通过调用代理,来间接的调用实际的对象。
在这里插入图片描述
由于篇幅有限,今天就分享到这里,需要更多2020各大厂(含BAT、滴滴、京东等)面试题可以加q裙812855908,免费分享在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_40989769/article/details/104968459