首页 / 资讯中心 / 文章详情

大厂算法工程师笔试题拆解:C++对象模型与概率统计自测

大厂算法工程师笔试题拆解:C++对象模型与概率统计自测 ★ FEATURED ARTICLE
简介一份面向阿里巴巴算法工程师岗位求职者的笔试题PDF适合准备技术面试、想检验C与概率统计基础的中高级候选人。内容包含11道选择与简答题涉及继承多态下的构造与虚函数调用顺序、猜拳奖金分配、四维向量内积符号概率、淘宝收藏与购买的条件概率、C语言文件I/O函数归类、新股抽签盈利期望、结对编程缺陷归因、SATA与SSD读写特性、初始化列表约束以及循环链表步长追赶等基本覆盖笔试常见考点。题目附有逐题解释与知识点归因按程序输出、概率统计、线性代数、计算机系统等模块梳理便于对照复习。压缩包仅含1个PDF文件大小约198KB轻量便于离线查阅和打印练习。已有975人浏览学习可作为面试前限时自测与知识点查漏补缺的题目清单并借助题面快速定位薄弱环节。1. 一份 PDF 笔试卷暴露的是「默认你该会」的那部分刷过几百道 LeetCode 的人第一次翻这份大厂算法工程师笔试题 PDF大概率会在第一屏卡住。整份卷子 11 道题真正需要手写算法的只有最后一道循环链表追及其余全是 C 对象模型、条件概率、存储层次和 I/O 函数分类的选择题。它的价值不在押题而在于把算法岗面试里那些「默认你应该知道」的常识压缩成 11 个能在 90 分钟内暴露短板的探针。适合三类人正在准备校招社招算法岗的、写 C 但说不清构造期虚函数行为的、以及概率题一看答案就懂、一合上就忘的。下面按「拆题、跑代码、验算、避坑」四步把这份 PDF 用成一套可复现的自测流程。2. 把 11 道题拆成一张知识点地图先做哪几道哪些可以放拿到一份笔试卷按题号顺序从头做是最低效的方式。我自己的习惯是先做一次「考点聚类」把题干里的关键词抄出来两分钟就能看出这份卷子其实只有五个知识簇而不是十一个孤立知识点。聚类之后你会发现同一簇里的题共用同一套判断逻辑做对一道等于做对一簇做错一道也说明整簇都没过关。2.1 逐题映射11 道题背后的五个知识簇下表是按题干关键词、真实考点、闭卷建议用时和参考答案整理出的映射表。参考答案一并列出方便做完之后立刻对账不必再去翻解析。题号题干关键词真实考点建议用时参考答案1基类构造调用虚函数C 构造期虚函数表状态4 分钟C0 1 22三人猜拳、先赢满 7 局多项竞赛的状态递推6 分钟D19/27、6/27、2/273四维向量夹角 60 度几何概率降维5 分钟D2/34购买 5%、收藏 20%贝叶斯公式3 分钟E80%5fread、gets、preadstdio 缓冲与系统调用分层3 分钟Dpread6新股申购、10 个签号二项分布与期望的线性6 分钟D3757代码比例 3:5、缺陷率加权归因4 分钟A28.6%8扇区、文件系统块、SSD存储层次单位4 分钟C9const、引用、static 成员初始化列表强制条件4 分钟Fb、c10read、pread、fseek同上反向问法3 分钟Efseek1199 节点循环链表、步长 1 和 3模运算与追及问题4 分钟A99五个簇分别是C 对象模型第 1、9 题、概率与统计第 2、3、4、6、7 题、I/O 接口分层第 5、10 题、存储层次第 8 题、基础算法与模运算第 11 题。其中概率簇占了将近一半分值是整份卷子的重心C 两题虽然只有两分却是区分「写过 C」和「读过 C 标准」的分水岭。值得单独说的是第 5 题和第 10 题。它们看似问的是「哪个函数不属于同一类」实际考的是同一件事你能否分清「带用户态缓冲的标准库 I/O」和「直接进内核的系统调用」。第 5 题里fread、gets、getchar、getline、scanf 全部属于前者只有 pread 是后者第 10 题反过来read、pread、write、pwrite、lseek 都是系统调用只有 fseek 属于标准库。两题的答案方向相反判断依据却是同一套分层模型。把这两题放在一起做比分开做效率高一倍。2.2 五个知识簇的复习成本排序如果时间有限复习顺序不该按题号来而该按「单位时间能补回多少分」排序。我一般的排序是这样第一优先是 C 对象模型两题。成本最低、收益最直接把代码复制到本地编译一次加上几行打印十分钟内就能把「构造期虚函数不生效」这件事看得清清楚楚。这种题一旦跑通下次遇到「析构期调用虚函数」「构造函数里调用纯虚函数」的变种也不会慌。第二优先是概率五题。公式本身不难难在建模谁是先验、谁是似然、谁是全概率分母。这部分建议手算一遍再用脚本验算一遍两边对上了才算真的会。成本中等但分值最高值得投入。第三优先是 I/O 分层两题。这部分属于「知道了就永远知道」的知识不需要推导只需要建立一张对照表哪些函数带缓冲、哪些带偏移、哪些能作用于文件描述符。花二十分钟整理一张表比反复做题更稳。第四优先是存储层次题。这道题是纯记忆题但要小心「常识干扰」很多人记得「硬盘最小读写单位是扇区」于是顺手把「文件系统最小单位也是扇区」也判成对的。实际上文件系统的最小分配单位是块通常是 4KB是扇区的整数倍两者层级不同。最后是链表追及题。这是整份卷子里唯一一道需要动笔算的算法题但套路固定环形结构上的追及本质是解同余方程相对速度乘以步数必须等于环长的整数倍。记住这个模型三十秒就能出结果。2.3 三轮使用法闭卷、写解析、只刷错题这份 PDF 最好的用法不是「看一遍解析」而是当成一套三轮使用的模拟卷。第一轮闭卷掐 90 分钟把答案写在一张纸上不许查资料第二轮不限制时间逐题写解析要求每道题都能写出「为什么其他选项不对」写不出来的就是知识盲区第三轮只做第一轮做错的题隔三天再做一次检验是否真的记住。轮次时长目标判断标准第一轮90 分钟暴露真实水平记录每题耗时超时的题单独标记第二轮不限补齐解析每题能说出三个干扰项错在哪第三轮40 分钟验证记忆留存错题正确率回到 90% 以上第一轮最容易犯的错是「做完不记时」。笔试的时间压力本身就是考点之一第 2 题和第 6 题都是计算量偏大的题如果在它们身上耗掉十五分钟后面的题就会慌。所以第一轮一定要记录每题耗时超时的题即使做对了也要标出来因为考场上它仍然会拖累你。2.4 错题记录的字段设计错题本不要只抄题目和答案那样复盘时没有信息量。我用的字段是五个题号、我选的、正确答案、错误类型、一句话复盘。错误类型只分四类知识盲区、建模错误、看错题、计算失误。这四类的补救方式完全不同知识盲区要回去补概念建模错误要重做同类题看错题要在题干上划线计算失误只能靠放慢速度。把 11 道题按这五个字段填一遍你会发现错误往往集中在某一两类而不是均匀分布。有人是建模错误占多数说明公式背得熟但不会用有人是看错题占多数说明做题太快、题干没读完就下手。这两种人的复习策略应该完全相反。3. C 对象模型两题构造函数里的虚函数与初始化列表C 这两道题是整份卷子里最值得动手跑一遍的。纸面上推演「构造期虚函数表指向谁」很容易绕晕实际编译运行一次把每个阶段的输出打印出来理解会牢固得多。3.1 第 1 题的输出为什么是 0 1 2先把原题代码补全成可编译的版本加上必要的注释。这段代码和原题逻辑一致只是把省略号补成了完整定义并去掉了原题里未使用的参数。#include iostream class A { public: A() : m_iVal(0) { test(); } // 第一步基类构造体内调用 test() virtual void func() { std::cout m_iVal ; } void test() { func(); } // 非虚函数但内部调用虚函数 func() public: int m_iVal; }; class B : public A { public: B() { test(); } // 第二步派生类构造体内再次调用 test() void func() override { // 覆盖基类虚函数 m_iVal; std::cout m_iVal ; } }; int main() { A* p new B; // 构造顺序先 A 子对象再 B 子对象 p-test(); // 第三步对象已完整虚调用落到 B::func return 0; // 原题未定义虚析构此处不 delete避免制造未定义行为 }逻辑说明要分三段看。第一段发生在new B的基类构造阶段A的构造函数把m_iVal初始化为 0然后调用test()test()里调用虚函数func()。此时派生类部分还没构造对象的虚函数表指针仍然指向A的虚表所以调用的是A::func()输出0。这是 C 标准明确规定的行为构造函数和析构函数期间虚调用不会下沉到派生类。第二段发生在B的构造函数体内。此时A子对象已经构造完成虚表指针被改写为指向B的虚表test()里的func()调用落到B::func()先自增m_iVal变成 1再输出1。第三段是main里的p-test()。test()是非虚函数按静态类型A*找到A::test但它内部的func()是虚调用此时对象已经完全构造虚表指针指向B的虚表所以再次进入B::func()m_iVal从 1 变成 2输出2。三段合起来就是0 1 2对应选项 C。参数和边界说明m_iVal是公有成员方便在派生类里直接自增如果把func声明为纯虚函数A就变成抽象类A的构造函数里调用纯虚函数属于未定义行为这也是一个高频变种考点。另外原题没有虚析构函数通过基类指针删除派生类对象是未定义行为所以示例代码里刻意没有delete这一点在真实项目里必须补上虚析构。3.2 把三段输出拆开验证加打印跑一遍光看代码容易「以为自己懂了」。更稳的做法是在每个关键节点加一行带标记的打印把执行顺序可视化出来。下面这段是改造后的验证版本输出会带上阶段标签。#include iostream class A { public: A() : m_iVal(0) { std::cout [A ctor] vptr - A\n; test(); } virtual ~A() default; // 补上虚析构便于安全释放 virtual void func() { std::cout A::func m_iVal \n; } void test() { func(); } public: int m_iVal; }; class B : public A { public: B() { std::cout [B ctor] vptr - B\n; test(); } void func() override { m_iVal; std::cout B::func m_iVal \n; } }; int main() { A* p new B; std::cout [main] object complete\n; p-test(); delete p; return 0; }预期输出依次是[A ctor] vptr - A、A::func 0、[B ctor] vptr - B、B::func 1、[main] object complete、B::func 2。这六行把「虚表指针在构造过程中被改写」这件事完整呈现出来了比任何文字描述都直观。编译命令建议带上警告选项很多对象模型的问题在编译期就有提示g -stdc17 -Wall -Wextra -Wreorder -o ctor_demo ctor_demo.cpp ./ctor_demo-Wall -Wextra打开常用警告-Wreorder专门提示初始化列表顺序与成员声明顺序不一致的问题这个警告在写第 9 题那类代码时特别有用。如果用的是 clang可以换成-Wall -Wextra -Wreorder-ctor。提示不同编译器对「构造函数中调用虚函数」的实现细节可能有差异但标准规定的语义是一致的构造期虚调用不会进入尚未构造完成的派生类。如果在本机跑出的结果和预期不符先检查是不是把func写成了非虚函数。3.3 第 9 题必须走初始化列表的只有 const 和引用第 9 题问的是「构造函数中成员变量一定要通过初始化列表来初始化的是哪些」。原题给的成员包括普通int、const int、引用、static const char*和自类型指针。正确答案是const成员和引用成员两个即选项 F。很多人会多选一个指针或者把static成员也算进去这两处都是典型误判。判断依据只有一条这个成员有没有「必须在进入构造函数体之前完成初始化」的约束。const对象一旦构造就必须有值进入函数体后再赋值是不允许的引用必须在绑定时就确定目标之后不能改绑。普通int和指针可以先默认初始化再在函数体里赋值static成员属于类而不属于对象根本不出现在构造函数的初始化列表里它必须在类外定义。成员类型能否在函数体内赋值是否必须初始化列表原因int a能否有默认初始化可后赋值const int b不能是构造后不可修改A c不能是引用必须绑定后不可改绑static const char* d不适用否属于类不属于对象B* e能否指针可默认初始化后赋值下面这段代码把五类成员放进同一个类里可以直接编译验证。#include iostream class A { public: A() : a(0) {} private: int a; }; A g_a; // 全局对象供引用成员绑定 class B : public A { public: // bconst和 c引用只能走初始化列表 // e 是指针列表或函数体赋值都可以 // d 是 static 成员不属于对象不能在列表里初始化 B() : b(1), c(g_a), e(nullptr) { a 2; } static const char* d; private: int a; const int b; A c; B* e; }; const char* B::d static-member; // static 成员必须在类外定义 int main() { B obj; std::cout sizeof(B) sizeof(B) \n; std::cout d B::d \n; return 0; }这段代码有两个观察点。第一sizeof(B)的结果不包含static成员d因为静态成员存放在静态存储区不在对象布局里第二初始化列表里b和c的顺序必须和声明顺序一致否则-Wreorder会报警告。实际工程里我一般会强制自己按声明顺序写初始化列表这样即使有依赖关系也不会踩到「未初始化就被使用」的坑。参数说明g_a是全局对象生命周期贯穿整个程序用它来绑定引用成员可以避免悬垂引用如果改成绑定构造函数的局部变量对象构造完成后引用就悬空了这是另一个高频错误。3.4 编译参数与三个高频报错这三类报错在做这类题时几乎必然遇到提前知道怎么读能省不少时间。第一类是error: uninitialized const member意思是 const 成员没有在初始化列表里给出值。解决方式是把它挪进初始化列表而不是在函数体里赋值。第二类是error: uninitialized reference member引用成员没绑定同理。第三类是warning: B::b will be initialized after B::a初始化列表顺序和声明顺序不一致虽然大多数情况下不影响结果但涉及相互依赖的成员时会导致读到未初始化的值必须按声明顺序改写。注意-Wreorder只是警告不是错误很多人会直接忽略。我自己的习惯是把它当成错误处理加-Werrorreorder让它直接编译失败强迫自己改对顺序。4. 概率统计四题贝叶斯、期望、归因与三人分奖金概率这五道题是整份卷子里最容易「看答案觉得简单、自己做就错」的部分。原因不在公式而在建模先验和似然放错位置、权重和概率混为一谈、期望的线性用错对象。下面按题目顺序拆每道题都配一段可运行的验算脚本。4.1 第 4 题贝叶斯的分母是全概率不是先验题目给的是看到商品后购买的概率 5%收藏的概率 20%收藏之后购买的概率 20%求已知购买的情况下收藏过的概率。很多人第一反应是 20% 乘以 20% 得 4%然后就卡住了因为他把「购买」当成了条件而不是结果。正确的写法是贝叶斯公式P(收藏|购买) P(购买|收藏) × P(收藏) / P(购买)。分子是「收藏且购买」的联合概率等于 0.2 乘 0.2 得 0.04分母是「购买」的边缘概率题目直接给了 0.05。两者相除得 0.8也就是 80%。# 第 4 题已知购买求收藏过的条件概率 p_buy 0.05 # P(购买)看到商品后购买 p_fav 0.20 # P(收藏)看到商品后收藏 p_buy_given_fav 0.20 # P(购买|收藏)收藏之后购买 # 贝叶斯分母用 P(购买) 这个边缘概率 p_fav_given_buy p_buy_given_fav * p_fav / p_buy print(fP(收藏|购买) {p_fav_given_buy:.2%}) # 80.00%脚本里三个变量对应题目给的三个数没有多余假设。参数说明p_buy必须是边缘概率而不是联合概率这是这道题唯一容易错的地方。如果题目改成「购买且收藏的概率是 4%」那分母就要换成 0.04答案会完全不同。做题时先确认每个数字是边缘概率还是联合概率比记住公式更重要。4.2 第 6 题先算单位收益再乘期望中签数新股申购这道题的信息量比较大最多申购 1 万股每 1 千股一个签号共 10 个号每个号中签概率 0.5% 且相互独立发行价 10 元涨到 15 元和 20 元的概率各 50%。求最大申购下的盈利期望。拆成两层就清楚了。第一层是每中一签的期望收益一签对应 1000 股成本 10 元每股涨到 15 元赚 5000 元涨到 20 元赚 10000 元两种情形各占一半所以每签期望收益是 7500 元。第二层是期望中签数10 个签号每个中签概率 0.005期望中签数是 0.05 个。两层相乘7500 乘 0.05 等于 375 元。from math import comb n_signs 10 # 1 万股 / 每 1 千股一个号 10 个号 p_hit 0.005 # 每个号中签概率 0.5% # 每中一签1000 股涨到 15 赚 5000涨到 20 赚 10000各 50% profit_per_hit 0.5 * 5000 0.5 * 10000 # 期望的线性E[总收益] E[中签数] * 每签收益 expected_hits n_signs * p_hit print(expected_hits * profit_per_hit) # 375.0这段代码里comb其实没用到如果想把完整分布也算出来可以补一段二项分布的概率质量函数验证「中 0 签的概率约 95.1%」这个结论顺便确认期望的线性不依赖于独立性假设。参数说明profit_per_hit之所以可以直接乘期望中签数是因为期望的线性不要求随机变量独立这是这道题最值得记住的一点。如果题目问的是「盈利超过某个阈值的概率」那就必须用完整分布不能只算期望。4.3 第 7 题代码比例是权重不是概率两个人结对编程代码比例 3:5千行缺陷率分别是 10‰ 和 15‰发现一个缺陷求由前者代码引起的概率。这道题的陷阱在于「代码比例」和「缺陷概率」是两个不同的量必须相乘之后才能比较。正确做法是加权第一人的「缺陷贡献」是 3 乘 10‰ 等于 0.03第二人是 5 乘 15‰ 等于 0.075总贡献 0.105前者占比 0.03 除以 0.105约等于 28.57%对应选项 A。w_a, w_b 3, 5 # 代码量比例作为权重 d_a, d_b 10e-3, 15e-3 # 千行缺陷率 # 先算各自的缺陷贡献再做归一化 p_a (w_a * d_a) / (w_a * d_a w_b * d_b) print(fP(A 引起 | 发现缺陷) {p_a:.2%}) # 28.57%参数说明10e-3就是千分之十写成 0.01 也可以但保留千分号的形式更贴近题干复盘时不容易看错数量级。这道题和第 4 题是同一个模型的两个变种都是「先算联合再除以总贡献」区别只在于第 4 题的分母是题目直接给的这道题的分母要自己加出来。4.4 第 2 题三人分奖金用状态递推代替直觉三人猜拳先赢满 7 局者获胜当前比分是 6、5、4问奖金怎么分最公平。直觉上很多人会按「还差几局」反比例分配或者按当前胜局数比例分配这两种都不对。公平分配的依据是每个人最终夺冠的概率而夺冠概率要用状态递推算。设三个人还差的局数分别是 1、2、3每一局三人获胜的概率各为三分之一平局重来不影响相对概率。定义P(i, j, k)为三人还差 i、j、k 局时第一位选手最终夺冠的概率。边界条件是谁的差局数归零谁就赢所以P(0, j, k) 1P(i, 0, k) 0P(i, j, 0) 0。递推式是三个方向各取三分之一。from functools import lru_cache lru_cache(maxsizeNone) def win(need, who): need: 三人各自还差的局数who: 0/1/2 表示要求谁夺冠的概率。 if need[who] 0: return 1.0 if any(need[x] 0 for x in range(3) if x ! who): return 0.0 total 0.0 for x in range(3): nxt list(need) nxt[x] - 1 total win(tuple(nxt), who) / 3.0 return total need (1, 2, p a hrefhttps://download.csdn.net/download/custertian/12705568 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p
阅读完成 · 觉得有帮助?
咨询建站