2020年精选面试题及答案(一)

1. 在ACM竞赛中,一支队伍由三名队员组成,现在有N+M名学生,其中有N名学生擅长算法,剩下M名学生擅长编程,这些学生要参加ACM竞赛,他们的教练要求每支队伍至少有一名擅长算法和一名擅长编程的学生,那么这些学生最多可以组成多少支队伍?

输入: 输入两个整数M,N,其中1<N,M<10000000
输出: 最多可以组成的队伍数

#include <iostream>
using namespace std;
 
int main()
{
    
    
    int cnt = 0,n,m;
    cout << "输入N个擅长算法的,M个擅长编程的:" << endl;
    cin >> n >> m;
    while(n!=0&&m!=0&&m+n!=2){
    
    
        if(n>=m){
    
    
            n = n-2;
            m = m-1;
            cnt++;
        }
        else if(n<m){
    
    
            m = m-2;
            n = n-1;
            cnt++;
        }
    }
    cout << "最大组对数量" << cnt << endl;
    return 0;
}

2. 什么是幂等性

幂等性概念:幂等通俗来说是指不管进行多少次重复操作,都是实现相同的结果。

3. REST请求中哪些是幂等操作

GET,PUT,DELETE都是幂等操作,而POST不是
分析
首先GET请求很好理解,对资源做查询多次,此实现的结果都是一样的。
PUT请求的幂等性可以这样理解,将A修改为B,它第一次请求值变为了B,再进行多次此操作,最终的结果还是B,与一次执行的结果是一样的,所以PUT是幂等操作。
同理可以理解DELETE操作,第一次将资源删除后,后面多次进行此删除请求,最终结果是一样的,将资源删除掉了。
POST不是幂等操作,因为一次请求添加一份新资源,二次请求则添加了两份新资源,多次请求会产生不同的结果,因此POST不是幂等操作。

4. 根据幂等性区分POST与PUT的使用

可根据idempotent(幂等性)做区分。
举一个简单的例子,假如有一个博客系统提供一个Web API,模式是这样http://superblogging/blogs/{blog-name},很简单,将{blog-name}替换为我们的blog名字,往这个URL发送一个HTTP PUT或者POST请求,HTTP的body部分就是博文,这是一个很简单的REST API例子。
我们应该用PUT方法还是POST方法?
取决于这个REST服务的行为是否是idempotent的,假如我们发送两个http://superblogging/blogs/post/Sample请求,服务器端是什么样的行为?如果产生了两个博客帖子,那就说明这个服务不是idempotent的,因为多次使用产生了副作用了嘛;如果后一个请求把第一个请求覆盖掉了,那这个服务就是idempotent的。前一种情况,应该使用POST方法,后一种情况,应该使用PUT方法。

5. CAS的缺点及解决.

CAS的缺点有如ABA问题,自旋锁消耗问题、多变量共享一致性问题.
1.ABA:
问题描述:线程t1将它的值从A变为B,再从B变为A。同时有线程t2要将值从A变为C。但CAS检查的时候会发现没有改变,但是实质上它已经发生了改变 。可能会造成数据的缺失。
解决方法:CAS还是类似于乐观锁,同数据乐观锁的方式给它加一个版本号或者时间戳,如AtomicStampedReference
2.自旋消耗资源:
问题描述:多个线程争夺同一个资源时,如果自旋一直不成功,将会一直占用CPU。
解决方法:破坏掉for死循环,当超过一定时间或者一定次数时,return退出。JDK8新增的LongAddr,和ConcurrentHashMap类似的方法。当多个线程竞争时,将粒度变小,将一个变量拆分为多个变量,达到多个线程访问多个资源的效果,最后再调用sum把它合起来。
虽然base和cells都是volatile修饰的,但感觉这个sum操作没有加锁,可能sum的结果不是那么精确。
2.多变量共享一致性问题:
解决方法: CAS操作是针对一个变量的,如果对多个变量操作,

  1. 可以加锁来解决。
  2. 封装成对象类解决。
    在这里插入图片描述
    (更多免费 C/C++,Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等等多个知识点干货学习资料加群 960994558)

6. B+树特点

(1)每个结点的关键字个数与孩子个数相等,所有非最下层的内层结点的关键字是对应子树上的最大关键字,最下层内部结点包含了全部关键字。
(2)除根结点以外,每个内部结点有 到m个孩子。 [3]
(3)所有叶结点在树结构的同一层,并且不含任何信息(可看成是外部结点或查找失败的结点),因此,树结构总是树高平衡的。

7. 详细解释事务的隔离性.

事务的隔离性
为了保证事务的隔离性,自然我们可以把事务设计成单线程的,这样的话效率就会极其低下,为了保证隔离性,又不失效率我们把丧失隔离性的情况分为三种。
脏读:读到另一个未提交事务的数据
幻读:在一个事务过程中已经读取了一次表,此时恰巧另一个事务commit,导致这次事务再一次读取表时前后不一致。(表影响)
不可重复读:在一个事务过程中已经读取了一次a数据,此时恰巧另一个事务commit,导致这次事务再一次读取a数据时前后不一致。
针对这三种情况推出了四大隔离级别
四大隔离级别:
Read uncommitted – 不防止任何隔离性问题,具有脏读/不可重复度/虚读(幻读)问题
Read committed – 可以防止脏读问题,但是不能防止不可重复度/虚读(幻读)问题
Repeatable read – 可以防止脏读/不可重复读问题,但是不能防止虚读(幻读)问题
Serializable – 数据库被设计为单线程数据库,可以防止上述所有问题
这四大隔离级别,安全性递增。效率递减

8. 已知一个函数rand7()能够生成1-7的随机数,请给出一个函数,该函数能够生成1-10的随机数。 该解法基于一种叫做拒绝采样的方法。主要思想是只要产生一个目标范围内的随机数,则直接返回。如果产生的随机数不在目标范围内,则丢弃该值,重新取样。由于目标范围内的数字被选中的概率相等,这样一个均匀的分布生成了。

显然rand7至少需要执行2次,否则产生不了1-10的数字。通过运行rand7两次,可以生成1-49的整数,
在这里插入图片描述

由于49不是10的倍数,所以我们需要丢弃一些值,我们想要的数字范围为1-40,不在此范围则丢弃并重新取样。

代码:

int rand10() {
    
    
  int row, col, idx;
  do {
    
    
    row = rand7();
    col = rand7();
    idx = col + (row-1)*7;
  } while (idx > 40);
  return 1 + (idx-1)%10;
}

由于row范围为1-7,col范围为1-7,这样idx值范围为1-49。大于40的值被丢弃,这样剩下1-40范围内的数字,通过取模返回。下面计算一下得到一个满足1-40范围的数需要进行取样的次数的期望值:
在这里插入图片描述

9. 请说明线程池都有哪些优化措施.

线程等待时间所占比例越高,需要越多线程。线程 CPU 时间所占比例越高,需要越少线程。
如果你是CPU密集型运算,那么线程数量和CPU核心数相同就好,避免了大量无用的切换线程上下文.
如果你是IO密集型的话,需要大量等待,那么线程数可以设置的多一些,比如CPU核心乘以2.

10. C++11创建线程的三种方式

  1. 通过函数
    thread:标准库的类
    join:阻塞主线程并等待
// MultiThread.cpp : Defines the entry point for the console application.
#include "stdafx.h"
#include<iostream>
#include<vector>
#include<map>
#include<string> 
#include<thread>

using namespace std;
void myPrint()
{
    
    
	cout << "线程开始运行" << endl;
	cout << "线程运行结束了" << endl;
}

int main()
{
    
    
	std::thread my2Obj(myPrint);  // 可调用对象
	my2Obj.join();// 主线程阻塞在这,并等待myPrint()执行完
	cout << "wangtao" << endl;
    return 0;
}

detach(): 将主线程和子线程完全分离,子线程会驻留在后台运行,被C++运行时库接管,失去控制

void myPrint()
{
    
    
	cout << "线程开始运行1" << endl;
	cout << "线程开始运行2" << endl;
	cout << "线程开始运行3" << endl;
	cout << "线程开始运行4" << endl;
	cout << "线程开始运行5" << endl;
	cout << "线程开始运行6" << endl;
	cout << "线程开始运行7" << endl;
	cout << "线程开始运行8" << endl;
	cout << "线程开始运行9" << endl;

}

int main()
{
    
    
	std::thread my2Obj(myPrint); // 主线程阻塞在这,并等待myPrint()执行完
	my2Obj.detach();
	cout << "wangtao1" << endl;
	cout << "wangtao2" << endl;
	cout << "wangtao3" << endl;
	cout << "wangtao4" << endl;
	cout << "wangtao5" << endl;
	cout << "wangtao6" << endl;
	cout << "wangtao7" << endl;
	cout << "wangtao8" << endl;
    return 0;
}

joinable():判断是否可以成功使用join()或者detach()

程序说明:detach后不能在实施join

int main()
{
    
    
	std::thread my2Obj(myPrint); // 主线程阻塞在这,并等待myPrint()执行完
	if (my2Obj.joinable()){
    
    
		cout << "1:joinable() == true" << endl;
	}
	else {
    
    
		cout << "1:joinable() == false" << endl;
	}
	my2Obj.detach();

	if (my2Obj.joinable()) {
    
    
		cout << "2:joinable() == true" << endl;
	}
	else {
    
    
		cout << "2:joinable() == false" << endl;
	}
	cout << "wangtao1" << endl;
	cout << "wangtao2" << endl;
	cout << "wangtao3" << endl;
	cout << "wangtao4" << endl;
	cout << "wangtao5" << endl;
	cout << "wangtao6" << endl;
	cout << "wangtao7" << endl;
	cout << "wangtao8" << endl;
    return 0;
}

int main()
{
    
    
	std::thread my2Obj(myPrint); // 主线程阻塞在这,并等待myPrint()执行完
	if (my2Obj.joinable()){
    
    
		my2Obj.join();
	}
	cout << "wangtao1" << endl;
	cout << "wangtao2" << endl;
	cout << "wangtao3" << endl;
	cout << "wangtao4" << endl;
	cout << "wangtao5" << endl;
	cout << "wangtao6" << endl;
	cout << "wangtao7" << endl;
	cout << "wangtao8" << endl;
    return 0;
}

2.通过类对象创建线程

class CObject
{
    
    
public:
	void operator ()() {
    
    
		cout << "线程开始运行" << endl;
		cout << "线程结束运行" << endl;
	}
};


int main()
{
    
    
	CObject obj;
	std::thread my2Obj(obj); // 主线程阻塞在这,并等待myPrint()执行完
	if (my2Obj.joinable()){
    
    
		my2Obj.join();
	}
	cout << "see you " << endl;
	
    return 0;
}


class CObject
{
    
    
	int& m_obj;
public:
	CObject(int& i) :m_obj(i) {
    
    }
	void operator ()() {
    
     // 不带参数
		cout << "线程开始运行1" << endl;
		cout << "线程开始运行2" << endl;
		cout << "线程开始运行3" << endl;
		cout << "线程开始运行4" << endl;
		cout << "线程开始运行5" << endl;
	}
};
int main()
{
    
    
	int i = 6;
	CObject obj(i);
	std::thread my2Obj(obj); // 主线程阻塞在这,并等待myPrint()执行完
	if (my2Obj.joinable()){
    
    
		my2Obj.detach();
	}
	cout << "see you " << endl;
	
    return 0;
}

用detach() 主线程结束对象即被销毁,那么子线程的成员函数还能调用吗?
这里的的对象会被复制到子线程中,当主线程结束,复制的子线程对象并不会被销毁
只要是没有引用、指针就不会出现问题

通过复制构造函数和析构函数来验证对象是否复制到了子线程中

// MultiThread.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include<iostream>
#include<vector>
#include<map>
#include<string> 
#include<thread>
using namespace std;
class CObject
{
    
    
	int& m_obj;
public:
	CObject(int& i) :m_obj(i) {
    
    
		cout << "ctor" << endl;
	}
	CObject(const CObject& m) :m_obj(m.m_obj) {
    
    
		cout << "copy ctor" << endl;
	}
	~CObject(){
    
    
		cout << "dtor" << endl;
	}
	void operator ()() {
    
     // 不带参数
		cout << "线程开始运行1" << endl;
		cout << "线程开始运行2" << endl;
		cout << "线程开始运行3" << endl;
		cout << "线程开始运行4" << endl;
		cout << "线程开始运行5" << endl;
	}
};
int main()
{
    
    
	int i = 6;
	CObject obj(i);
	std::thread my2Obj(obj); // 主线程阻塞在这,并等待myPrint()执行完
	if (my2Obj.joinable()){
    
    
		my2Obj.detach();
	}
	cout << "see you " << endl;
	
    return 0;
}

子线程的析构函数在后台执行,所以输出的dtor是主线程的。用join() 结果为:

3.通过lambda表达式创建线程

int main()
{
    
    
	auto myLamThread = [] {
    
    
		cout << "线程开始运行" << endl;
		cout << "线程结束运行" << endl;
	};
	thread cthread(myLamThread);
	cthread.join();
	std::cout << "see you " << endl;
	
    return 0;
}

学以致用才是好,以上可能存在有不足的地方欢迎指出讨论。

猜你喜欢

转载自blog.csdn.net/weixin_52622200/article/details/110440419