大家好,我是阿Q。
今天给大家分享一位北邮同学的秋招面经, 可能也许有人还没听过这家公司,但无妨,我们主要是学习知识。
我给大家分享的原因主要是因为他在面试过程中,遇到的这些问题都是比较经典的,所以及时拿出来给大家做一个分享。同时,也预祝这位同学秋招顺利,早日拿到自己心仪的offer。
更多基础知识和面经可点击公众号主页查询
来源:
https://www.nowcoder.com/discuss/536269911090327552
一面技术面
降低线程创建销毁的开销:创建和销毁线程是开销较大的操作,线程池通过重用已创建的线程,减少了这些开销,提高了系统的效率。 限制并发线程数量:线程池可以控制系统中同时运行的线程数量,防止无限制地创建线程导致系统资源枯竭和性能下降。 提高响应速度:通过预先创建线程并将它们保持在就绪状态,线程池可以更快地响应任务请求,而不需要等待线程创建。 实现任务队列:线程池通常与任务队列结合使用。任务可以被提交到队列中,线程池按顺序从队列中获取任务并执行。这有助于控制任务的执行顺序。 统一管理:线程池提供了一个集中管理线程的机制,可以监视线程的状态、重启失败的线程,以及进行统一的配置和调整。 避免资源竞争:在多线程环境中,线程池通常采用一些同步机制来避免资源竞争,从而提高了多线程程序的稳定性。 控制任务执行:线程池可以对任务执行进行控制,例如设置任务的最大执行时间、取消正在执行的任务等。 提高系统稳定性:通过合理管理线程池中的线程数量,可以避免线程过多导致系统资源不足,从而提高系统的稳定性。
哈希表(Hash Table):
定义:哈希表是一种用于存储键值对的数据结构,其中每个键都映射到唯一的值。它通过哈希函数将键映射到特定的索引位置,以实现快速的插入、查找和删除操作。 工作原理:哈希表使用哈希函数将键转换为数组的索引,这样可以快速定位值。冲突可能发生,即不同的键映射到相同的索引,通常通过解决冲突的方法,如链地址法或开放寻址法来处理。 优点:哈希表提供了快速的查找性能,平均情况下插入、查找和删除操作的时间复杂度为O(1)。它适用于需要高效查找的情况,如字典、集合、缓存等。 示例:
#include <iostream>#include <unordered_map> C++标准库中的哈希表int main() {// 创建一个哈希表std::unordered_map<std::string, int> hashMap;// 插入键值对hashMap["apple"] = 5;hashMap["banana"] = 2;hashMap["cherry"] = 7;// 查找键的值std::cout << hashMap["apple"] << std::endl; // 输出 5// 删除键值对hashMap.erase("banana");return 0;}
动态数组(Dynamic Array):
定义:动态数组是一个可以动态增长或缩小的数组,它在内存中连续存储元素,并提供了高效的随机访问和动态大小管理。 工作原理:当数组已满时,动态数组会分配更大的内存块,将元素复制到新的内存中,并释放旧的内存块。这使得数组能够根据需要动态扩展,而不需要预先分配固定大小的空间。 优点:动态数组支持高效的随机访问(O(1)时间复杂度),并且可以动态调整大小以适应不同的数据集合大小。这使得它适用于需要灵活管理数据大小的情况。 示例:
#include <iostream>#include <vector> C++标准库中的动态数组int main() {// 创建一个动态数组std::vector<int> dynamicArray;// 添加元素dynamicArray.push_back(5);dynamicArray.push_back(10);dynamicArray.push_back(20);// 随机访问元素std::cout << dynamicArray[1] << std::endl; // 输出 10// 修改元素dynamicArray[0] = 15;// 获取数组大小std::cout << dynamicArray.size() << std::endl; // 输出 3return 0;}
定义结构体:首先,定义一个结构体,以表示数据库模型中的每一行记录。结构体的字段应该与数据库表的列相对应。
struct UserInfo {int userId;std::string username;std::string email;};
查询数据库:使用数据库连接工具(如ODBC、JDBC、SQLAlchemy等)从数据库中查询记录。你需要编写SQL查询语句来检索所需的数据。
// 假设使用SQL查询从数据库中检索用户信息std::string sqlQuery = "SELECT user_id, username, email FROM users WHERE user_id = ?";// 执行查询并获取结果// 这里的代码可以根据你使用的数据库库或框架来编写
将结果映射到结构体:遍历查询结果并将每一行的字段值分配给相应的结构体字段。
// 假设查询结果已经保存在resultSet中UserInfo user;if (resultSet.next()) { // 假设resultSet有一个next()方法用于迭代结果user.userId = resultSet.getInt("user_id");user.username = resultSet.getString("username");user.email = resultSet.getString("email");}
使用结构体:一旦将数据库记录映射到结构体中,你可以在业务逻辑中使用该结构体。
std::cout << "User ID: " << user.userId << std::endl;std::cout << "Username: " << user.username << std::endl;std::cout << "Email: " << user.email << std::endl;
提高查询性能:索引允许数据库系统更快地定位和检索数据,尤其在大型数据集上能够显著提高查询性能。 加速排序:索引的有序性使得数据库可以快速执行排序操作,而不必扫描整个表。 支持唯一性约束:索引可以用于确保某一列或一组列的唯一性,从而实现数据的一致性。
单列索引和复合索引:单列索引仅包含单个列的值,而复合索引包含多个列的值。复合索引可以用于同时查询多个列,但查询时必须遵循复合索引的顺序。 唯一索引:唯一索引要求索引列的值在整个表中是唯一的,用于实现唯一性约束。 主键索引:主键索引是一种特殊的唯一索引,它定义了表的主键,用于唯一标识每一行数据。 全文索引:全文索引用于全文搜索,允许数据库系统高效地搜索文本数据。 空间索引:空间索引用于地理空间数据,支持地理位置的查询和分析。
多路平衡树结构:B+ 树是一种多路平衡树结构,每个节点可以包含多个子节点,而不仅仅是两个子节点。这意味着在 B+ 树中,每个节点可以存储更多的键值对,从而减少了树的深度。 有序存储:B+ 树的所有叶子节点都按照键值的顺序进行存储,这使得范围查询非常高效。而在二叉搜索树中,要实现有序存储需要进行额外的工作。 叶子节点存储数据:B+ 树的叶子节点存储了所有的数据记录,而非叶子节点仅存储键值和子节点的信息。这意味着在 B+ 树中,数据记录存储在叶子节点上,而在二叉搜索树中,数据可以存储在任何节点上。 自平衡:B+ 树通过进行插入和删除操作时的自平衡操作来保持树的平衡性,从而确保了树的高度相对较小,提高了查询性能。 范围查询高效:由于有序存储和多路平衡树的特性,B+ 树非常适合范围查询,因为范围查询可以通过遍历叶子节点实现。
二叉结构:传统的二叉搜索树每个节点最多有两个子节点,左子节点存储小于当前节点值的键,右子节点存储大于当前节点值的键。 不一定平衡:二叉搜索树的平衡性不如 B+ 树,如果插入或删除操作不当,可能会导致树的高度迅速增加,从而影响查询性能。 无序存储:二叉搜索树中的节点并不一定按键值的顺序存储,因此要实现有序存储需要额外的工作。 适合查找:二叉搜索树非常适合查找操作,因为在查找时可以利用树的有序性进行快速定位。
单一职责原则(Single Responsibility Principle,SRP):一个类应该只有一个引起它变化的原因。换句话说,一个类应该只有一个职责。这有助于使类更加可维护,因为当一个类只负责一个特定功能时,修改该功能不会影响其他功能。 开闭原则(Open/Closed Principle,OCP):软件实体(类、模块、函数等)应该对扩展开放,但对修改关闭。这意味着当需要添加新功能时,不应修改现有代码,而是通过扩展现有代码来实现新功能。 里氏替换原则(Liskov Substitution Principle,LSP):子类应该能够替代其父类而不引起错误。这意味着子类应该继承父类的行为,但可以扩展或修改该行为。 依赖倒置原则(Dependency Inversion Principle,DIP):高层模块不应该依赖于低层模块,两者都应该依赖于抽象。抽象不应该依赖于具体,具体应该依赖于抽象。这促使使用接口或抽象类来定义依赖关系,从而实现松耦合。 接口隔离原则(Interface Segregation Principle,ISP):客户端不应该强制依赖于它们不使用的接口。如果一个接口过于庞大,其中包含了许多不相关的方法,那么实现该接口的类可能会被迫实现不需要的方法,违反了ISP。 合成/聚合复用原则(Composite/Aggregate Reuse Principle,CARP):首选使用合成/聚合,而不是继承来实现代码复用。这意味着应尽量通过将现有对象组合到新对象中来实现复用,而不是通过继承已有对象的方式。 迪米特法则(Law of Demeter,LoD):一个对象应该对其他对象保持最少的了解。换句话说,一个对象不应该直接访问其它对象的内部结构,而应该通过接口来进行通信。
开放(Open):开放意味着系统的行为可以被扩展,可以在不修改现有代码的情况下引入新的功能或改进现有功能。这可以通过添加新的类、接口、模块或方法来实现。 关闭(Closed):关闭意味着已存在的代码不能随意修改。修改可能导致不必要的风险和不稳定性。因此,已存在的代码应该被视为稳定的,不应该被频繁修改。 示例:假设有一个形状(Shape)类,它有一个计算面积的方法,现在需要添加新的形状(例如圆形、三角形),可以通过创建新的形状子类来扩展该类而不修改现有代码。
class Shape {public:virtual double area() const = 0; // 抽象方法};class Circle : public Shape {private:double radius;public:Circle(double r) : radius(r) {}double area() const override {return 3.14159265358979323846 * radius * radius;}};class Triangle : public Shape {private:double base;double height;public:Triangle(double b, double h) : base(b), height(h) {}double area() const override {return 0.5 * base * height;}};
单例模式(Singleton Pattern):用于确保一个类只有一个实例,并提供全局访问点。在项目中,单例模式常用于管理共享资源,如配置信息、日志记录器等。 例如,一个日志记录器可以使用单例模式来确保在应用程序中只有一个日志记录器实例,以便在多个地方记录日志。
class Logger {private:Logger() {} // 私有构造函数,防止外部实例化static Logger* instance;public:static Logger* getInstance() {if (!instance) {instance = new Logger();}return instance;}void log(const std::string& message) {// 实现日志记录逻辑std::cout << "Log: " << message << std::endl;}};// 在应用程序中获取日志记录器实例并使用int main() {Logger* logger = Logger::getInstance();logger->log("This is a log message.");return 0;}
工厂模式(Factory Pattern):用于创建对象的模式,通过将对象的创建逻辑封装在工厂类中,客户端代码可以通过工厂类来创建对象,而不必直接实例化对象。工厂模式在项目中常用于解耦对象的创建和使用。 例如,一个图形库可以使用工厂模式来创建不同类型的图形对象。
// 基类class Shape {public:virtual void draw() = 0;};// 具体的图形类class Circle : public Shape {public:void draw() override {std::cout << "Draw a circle." << std::endl;}};class Rectangle : public Shape {public:void draw() override {std::cout << "Draw a rectangle." << std::endl;}};// 图形工厂class ShapeFactory {public:static Shape* createShape(const std::string& type) {if (type == "Circle") {return new Circle();}else if (type == "Rectangle") {return new Rectangle();}return nullptr;}};// 在应用程序中使用工厂创建图形对象int main() {Shape* shape1 = ShapeFactory::createShape("Circle");shape1->draw();Shape* shape2 = ShapeFactory::createShape("Rectangle");shape2->draw();delete shape1;delete shape2;return 0;}
观察者模式(Observer Pattern):用于实现对象之间的一对多依赖关系,当一个对象的状态发生变化时,所有依赖于它的对象都会得到通知并自动更新。在项目中,观察者模式用于实现事件处理、消息通知等机制。 例如,一个新闻发布系统可以使用观察者模式,让多个订阅者(观察者)订阅新闻发布者(主题)的新闻。
#include <iostream>#include <vector>// 主题接口class Subject {public:virtual void addObserver(Observer* observer) = 0;virtual void removeObserver(Observer* observer) = 0;virtual void notifyObservers() = 0;};// 观察者接口class Observer {public:virtual void update(const std::string& news) = 0;};// 具体主题class NewsPublisher : public Subject {private:std::vector<Observer*> observers;std::string latestNews;public:void addObserver(Observer* observer) override {observers.push_back(observer);}void removeObserver(Observer* observer) override {// 实现移除观察者的逻辑}void setNews(const std::string& news) {latestNews = news;notifyObservers();}void notifyObservers() override {for (Observer* observer : observers) {observer->update(latestNews);}}};// 具体观察者class NewsSubscriber : public Observer {private:std::string name;public:NewsSubscriber(const std::string& n) : name(n) {}void update(const std::string& news) override {std::cout << name << " received news: " << news << std::endl;}};int main() {NewsPublisher publisher;NewsSubscriber subscriber1("Subscriber 1");NewsSubscriber subscriber2("Subscriber 2");publisher.addObserver(&subscriber1);publisher.addObserver(&subscriber2);publisher.setNews("Breaking news: Important event!");return 0;}
策略模式(Strategy Pattern):用于定义一组算法,将它们封装成独立的策略类,并使它们可以互相替换。在项目中,策略模式用于实现动态选择算法或行为的场景,如排序算法、支付方式选择等。 装饰器模式(Decorator Pattern):用于动态地给对象添加新的功能,通过创建装饰器类来包装原始对象,从而扩展其功能。在项目中,装饰器模式常用于添加额外的功能或行为,而无需修改现有代码。 适配器模式(Adapter Pattern):用于将一个类的接口转换成客户端所期望的接口,以解决接口不兼容的问题。在项目中,适配器模式用于集成第三方库、服务或接口,使其能够与现有系统协作。 命令模式(Command Pattern):用于将请求或操作封装成对象,以支持请求的排队、记录请求、撤销请求等功能。在项目中,命令模式用于实现可撤销的操作、日志记录等。 模板方法模式(Template Method Pattern):用于定义一个算法的骨架,将一些步骤延迟到子类实现。在项目中,模板方法模式用于确保算法的基本结构不变,但具体实现可以在子类中定制。 状态模式(State Pattern):用于根据对象的状态改变其行为,将状态封装成对象,并使对象的行为可以动态地切换。在项目中,状态模式用于管理对象的状态机、工作流程等。 组合模式(Composite Pattern):用于将对象组织成树形结构以表示部分-整体的层次结构,使客户端可以一致地处理单个对象和组合对象。在项目中,组合模式用于处理树形数据结构、菜单系统等。 代理模式:代理模式是一种结构型设计模式,它允许一个对象(代理)充当另一个对象的接口,以控制对这个对象的访问。 远程代理(Remote Proxy):在分布式系统中,代理对象可以代表远程对象,使客户端能够访问远程对象,就像访问本地对象一样。远程代理用于隐藏网络通信的复杂性。 虚拟代理(Virtual Proxy):虚拟代理用于延迟创建昂贵对象的实例,直到需要时才进行实际的创建。例如,在加载大型图片或文档时,可以使用虚拟代理来避免初始加载时间过长。 保护代理(Protection Proxy):保护代理控制对真实对象的访问权限,允许或拒绝对对象的某些操作。这用于实施访问控制或安全策略。 缓存代理(Cache Proxy):缓存代理保存对真实对象的引用,以避免频繁的访问和计算。当下次请求相同数据时,代理可以返回缓存的结果,提高性能。
#include <iostream>#include <string>// 图片接口class Image {public:virtual void display() = 0;};// 具体图片类class RealImage : public Image {private:std::string filename;public:RealImage(const std::string& file) : filename(file) {loadFromDisk();}void display() override {std::cout << "Displaying " << filename << std::endl;}void loadFromDisk() {std::cout << "Loading " << filename << " from disk" << std::endl;}};// 虚拟代理class ProxyImage : public Image {private:RealImage* realImage;std::string filename;public:ProxyImage(const std::string& file) : filename(file), realImage(nullptr) {}void display() override {if (!realImage) {realImage = new RealImage(filename);}realImage->display();}};int main() {Image* image1 = new ProxyImage("image1.jpg");Image* image2 = new ProxyImage("image2.jpg");// 图片并未加载,直到调用display方法image1->display();image2->display();// 第二次调用时,图片已经加载,不再从磁盘加载image1->display();image2->display();delete image1;delete image2;return 0;}
使用代理模式来将部分接口的实现委托给其他类。这可以帮助将接口的实现分散到多个代理类中,减轻主要类的负担。 将具有不同职责或接口的部分功能拆分成多个类。每个类负责实现一个特定的接口或职责,从而降低了单个类的复杂性。这符合单一职责原则(SRP)。 如果多个接口之间存在一定的共性,可以考虑使用接口继承。创建一个包含多个接口的接口,然后让类实现这个继承自多个接口的接口。 使用组合而不是继承。创建包含其他类实例的成员变量,并在类中调用这些实例的方法,从而避免实现过多的接口。这符合组合优于继承的原则。 如果多个接口之间存在一定的相关性,可以考虑将它们合并成一个更大的接口,以减少类需要实现的接口数量。 如果一些接口的实现在多个类中是相同的,可以考虑将这些接口定义为抽象类,并让多个类继承这个抽象类,从而共享相同的实现。
层次关系:HTTP通常运行在TCP协议之上。这意味着在使用HTTP进行通信时,HTTP消息(例如Web页面请求和响应)会被封装为TCP数据包并通过TCP连接进行传输。 可靠性:TCP是一种面向连接的协议,它提供了可靠的数据传输机制,包括数据的分段、传输顺序的保证、数据重传等。HTTP在TCP的基础上构建,因此继承了TCP的可靠性。 端口:HTTP默认使用的端口是80,而TCP没有默认端口,它可以用于多种不同的应用。
用途: TCP是一种传输层协议,主要负责数据的可靠传输。 HTTP是一种应用层协议,用于在Web浏览器和Web服务器之间传递超文本文档。 协议层级: TCP位于OSI模型的传输层,处理数据传输的细节。 HTTP位于应用层,负责定义如何请求和响应超文本资源。 连接性: TCP是面向连接的,每次通信都需要建立和维护一个TCP连接,然后在连接上发送数据。 HTTP是无状态的,每个HTTP请求和响应之间是独立的,不会维护连接状态。但可以使用HTTP的持久连接(Keep-Alive)来在单个连接上发送多个请求和响应,以提高性能。 数据格式: TCP传输的是原始的字节流,没有特定的数据格式要求。 HTTP传输的是文本或二进制数据,具有特定的数据格式,例如HTTP头部和消息体。 应用领域: TCP用于支持各种应用层协议,不仅仅限于HTTP。它是通用的传输层协议。 HTTP主要用于Web浏览器和Web服务器之间的通信,用于获取和呈现Web内容。
流量控制:TCP滑动窗口用于控制发送方发送数据的速率,确保接收方可以处理发送方发送的数据而不会溢出接收缓冲区。它通过动态调整滑动窗口的大小来实现流量控制。 滑动窗口的概念:滑动窗口实际上是一个范围,由一个左边界和一个右边界定义。左边界表示接收方已经成功接收的数据,右边界表示接收方期望接收的数据。滑动窗口内的数据可以被发送方发送,而窗口外的数据需要等待。 窗口大小:滑动窗口的大小是动态变化的。接收方会告诉发送方它的接收窗口大小,发送方根据接收方的窗口大小来控制发送数据的速率。如果接收方的窗口大小较小,发送方会减小发送速率,以避免数据的堆积。 确认和应答:接收方收到数据后,会发送确认(ACK)给发送方,表示已成功接收数据。发送方根据接收到的确认信息来决定是否继续发送数据。如果某个数据包未被确认,发送方会认为它可能丢失,会进行重传。 拥塞控制:滑动窗口也用于拥塞控制。如果发送方检测到网络拥塞,它可以减小发送窗口的大小,以减轻网络负担。拥塞控制是TCP协议的另一个重要特性,用于维护网络的稳定性和公平性。 超时和重传:如果发送方发送的数据在一定时间内没有收到确认,它会认为数据包丢失,并触发重传机制,重新发送未确认的数据。
100 Continue(继续):表示服务器已收到客户端的请求头部,并且正在等待客户端发送请求体(通常用于大文件上传)。
200 OK(成功):表示服务器成功处理了客户端的请求,并返回了请求的内容。这是最常见的成功状态码。 201 Created(已创建):表示请求已经成功,并且服务器创建了一个新的资源,通常在POST请求后返回。 204 No Content(无内容):表示服务器成功处理了请求,但没有返回任何内容。通常用于DELETE请求。
300 Multiple Choices(多种选择):表示服务器有多种响应可以返回,需要客户端进一步选择。 301 Moved Permanently(永久重定向):表示请求的资源已经被永久性地移动到了新的URL地址。 302 Found(临时重定向):表示请求的资源暂时性地被移动到了新的URL地址。 304 Not Modified(未修改):表示客户端请求的资源未修改,可以使用缓存的版本。
400 Bad Request(错误请求):表示服务器无法理解客户端的请求,通常是因为请求中包含语法错误或不合法的参数。 401 Unauthorized(未授权):表示客户端请求需要身份验证,但未提供有效的身份验证信息。 403 Forbidden(禁止访问):表示服务器理解客户端的请求,但拒绝执行该请求,通常是因为权限问题。 404 Not Found(未找到):表示服务器无法找到请求的资源,通常是因为该资源不存在。 405 Method Not Allowed(不允许的方法):表示客户端使用了服务器不支持的HTTP方法,例如尝试对只支持GET请求的资源执行POST请求。 408 Request Timeout(请求超时):表示客户端发送的请求在服务器等待超时之前未完成。
500 Internal Server Error(服务器内部错误):表示服务器在处理请求时发生了意外的错误,通常是由于服务器端的代码或配置问题引起的。 501 Not Implemented(未实现):表示服务器不支持客户端请求的功能或方法。 502 Bad Gateway(坏的网关):表示服务器作为网关或代理时从上游服务器接收到无效的响应。 503 Service Unavailable(服务不可用):表示服务器当前无法处理请求,通常是由于服务器过载或维护引起的。 504 Gateway Timeout(网关超时):表示服务器作为网关或代理时,无法在规定时间内从上游服务器获取响应。 505 HTTP Version Not Supported(HTTP版本不支持):表示服务器不支持客户端请求的HTTP协议版本。
一致性(Consistency):一致性要求在分布式系统中的所有节点都看到相同的数据状态。也就是说,无论客户端向哪个节点发出请求,都应该获得相同的响应,系统的数据状态应该保持一致。一致性是强一致性和弱一致性的区别。强一致性要求每个读操作都返回最新写入的数据,而弱一致性则允许在一定条件下返回旧的数据。 可用性(Availability):可用性要求分布式系统在任何时候都能够响应客户端的请求,即使系统中的部分节点出现故障或不可用。可用性强调了系统的稳定性和连续性。 分区容忍性(Partition Tolerance):分区容忍性意味着分布式系统能够在网络分区或节点之间通信失败的情况下仍然能够正常运行。分区容忍性是分布式系统必备的属性,因为在现实世界中,网络分区和通信故障是不可避免的。
二面HR面
为啥学计算机(坚定的兴趣) 为啥去北方读书,父母有担心吗(能去北邮为啥不去,其实大部分人都能够很快适应生活) 什么时候开始决定要学计算机的(小学六年级,笑) 为啥学后端不学前端(后端挑战性大,前端主要是迭代快) 遇到过什么挑战(说了竞赛的东西) 为啥要打竞赛(最开始害怕,但是后来意识到如果不去做足够有挑战性的事,大学生涯就缺少意义) 团队如何应对困难,有没有产生过矛盾(默契的配合,矛盾还是有的,但实际上大家都有错误,最终能够意识到争吵和怀疑不能解决问题) 意向城市怎么考虑的(都投的广州深圳成都这些,主要还是想回南方) 为啥不喜欢北方(不适应,而且美食荒漠,笑) 之前投的有啥进展(大厂面试比较难,竞争力不够) 你在北京,线下面试确定能来吗(那必然要能啊)

文章转载自阿Q正砖,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。




