分享

某大厂C 图形面试题汇总!

 秋刀录 2020-07-07

面试分为项目经历介绍以及基础知识。基础知识考察分为三部分:最多的为考察C/C 、STL相关;第二部分是图形学基础渲染算法,考察频率也较高;第三部分简略考察计算机基础知识、图形的数学、数据结构与算法。

第一部分:C/C 语法,STL

STL

  • new、malloc的区别

  • C 分配内存方式

  • vector 与数组区别是什么,vector如何动态分配空间

  • vector 的 size 和 capacity 的区别

  • vector 和 list 的区别?

  • vector clear 函数具体做了什么操作?什么情况下会调用类的析构函数?

  • vector 的 push_back 做了什么操作?分几种情况?

  • 有没有分析过push_back n次平均复杂度?

  • 链表与数组之间的比较

  • list 实际上是一个什么样的链表?单链表?

  • map 和 unordered_map 的区别?插入和删除的复杂度?

  • 哈希表冲突一般怎么做?

  • 哈希函数数学原理?

  • Key 是 int 类型,32 位 Hash Key 添加多少元素时候冲突概率变高?

  • STL迭代器失效情况?

  • STL线程安全?

指针

  • 什么是虚函数?基类指针调用虚函数时会出现哪些情况?

  • C 如何实现虚函数的机制?虚表保存在哪?虚表的大小?虚表大小和虚函数数量是否有关?

  • 类的内存布局是什么样的?考虑有虚函数、多继承、虚继承几种情况。

  • 构造函数调用虚函数会出现什么情况

  • 介绍C 模版?

  • 实例化过程是可以任意实例化的么?为什么?

  • shared_ptr与weak_ptr的区别

  • 析构函数为什么加virtual?

  • 多态除了用virtual这种动态多态,有没有了解过静态多态?有哪几种多态?

  • 模版实例化是什么意思?发生在什么阶段?为什么发生在这个阶段?

  • 有没有办法把模版类实现放在cpp文件里?

  • override和overload的区别

  • C 为什么要增加override关键字?为了解决什么问题?

第二部分:计算机基础

计算机基础

  • TCP/UDP

  • 线程/进程

  • 设计模式

数据结构与算法

  • TopK

  • 二叉树翻转

  • 最长递增子序列

第三部分:图形学基础算法

  • Forward Shading 和 Deferred Shading 的优劣

  • PBR算法简述

基本出自这个网站:Learn OpenGL

其他:

  • 在圆上任意选取三个点,三个点构成锐角三角形的概率?

  • 设计随机函数,在圆内任意点概率相同

  • 怎样判断一个点在三角形内部?

  • 有N个三角形,怎样找出所有相交的三角形

[问题列表待更新]


[以下为粗略解答,后续校订、润色]

STL

  • new、malloc的区别

new和malloc内部的实现方式有什么区别?

  • C 分配内存方式

C 内存划分类型?

  • vector 与数组区别是什么,vector如何动态分配空间

vector 数组差别在于空间灵活性

vector 的 size 和 capacity 的区别

size是当前vector容器真实占用的大小,也就是容器当前拥有多少个容器。

capacity是指在发生realloc前能允许的最大元素数,即预分配的内存空间。

vector 扩充 capacity?

配置新空间/数据移动/释放旧空间。

  • vector 和 list 的区别?

list,非连续存储结构,具有双链表结构,因此支持前向/后向遍历。支持高效的随机插入/删除操作,但随机访问效率低下。

  • vector clear 函数具体做了什么操作?什么情况下会调用类的析构函数?

clear删除储存在vector中的所有元素. 调用clear之后, vector的尺寸(size)将变成zero. 但它的容量(capacity)却并不发生变化, vector本身并不释放任何内存。

如果vector的元素是一些object, 则它将为当前储存的每个元素调用它们各自的析构函数(destructor). 然而, 如果vector储存的是指向对象的指针, 此函数并不会调用到对应的析构函数

如果你想同时做到清空vector的元素和释放vector的容量, 你可以使用swap技巧(此技巧并非在所有环境中都管用 e.g. not with Intel Compiler 10.0.69 and LINUX 2.6.9-89 x64)

vector的clear操作的内部过程

  • vector 的 push_back 做了什么操作?分几种情况?

该函数将一个新的元素加到vector的最后面,位置为当前最后一个元素的下一个元素,新的元素的值是val的拷贝。

该方法可以快速有效率地在数组size范围内增长元素,除非当增长的元素个数大小超出了vector的capacity的时候才会发生重分配。

  • 有没有分析过push_back n次平均复杂度?

假定有 n 个元素,倍增因子为 m。那么完成这 n 个元素往一个 vector 中的push_back操作,需要重新分配内存的次数大约为 logm(n),第 i 次重新分配将会导致复制 m^i (也就是当前的vector.size() 大小)个旧空间中元素,因此 n 次 push_back操作所花费的总时间约为 n*m/(m - 1):

时间复杂度计算

很明显这是一个等比数列.那么 n 个元素,n 次操作,每一次操作需要花费时间为 m / (m - 1),这是一个常量.

所以,我们采用均摊分析的方法可知,vector 中 push_back 操作的时间复杂度为常量时间.

  • 链表与数组之间的比较

随机访问,数据增删,内存使用效率

  • list 实际上是一个什么样的链表?单链表?

双链表

  • map 和 unordered_map 的区别?插入和删除的复杂度?

map内部实现了一个红黑树,该结构具有自动排序的功能,因此map内部的所有元素都是有序的,红黑树的每一个节点都代表着map的一个元素,因此,对于map进行的查找,删除,添加等一系列的操作都相当于是对红黑树进行这样的操作,故红黑树的效率决定了map的效率。

有序性,这是map结构最大的优点,其元素的有序性在很多应用中都会简化很多的操作。红黑树,内部实现一个红黑书使得map的很多操作在lgnlgn的时间复杂度下就可以实现,因此效率非常的高

unordered_map内部实现了一个哈希表,因此其元素的排列顺序是杂乱的,无序的。因为内部实现了哈希表,因此其查找速度非常的快,但哈希表的建立比较耗费时间。

  • 哈希表冲突一般怎么做?

开放定址法(再散列法)
在开法定址法中,哈希表中的空闲单元(记为d)不仅允许哈希地址为d的同义词关键字使用,而且也允许发生冲突的其他关键字使用。开法定址法的名字就是来自于此方法的哈希表空闲单元既向同义词开放,也向发生冲突的非同义词关键字开放。谁先找到这个单元谁先占用,这和哈希表的元素排列次序有关。开放定址法以发生冲突的地址d作为自变量来得到一个新的空闲单元,下面介绍常用的几种。(d加下标i记为d[i],小i打不出来==)

  1. 线性探查法
    发生冲突时,线性遍历后续单元直到找到空闲单元。即d[i] = (d[i-1] 1) mod m线性探查容易产生堆积的问题。因为若是出现了若干个同意词会堆积在第一个同义词的地址单元附近。

  2. 平方探查法
    发生冲突时,用平方探查法的探查序列为d[i] 1²,d[i] 2², d[i] 3²...直到找到空闲单元。平方探查法是一种比较好的处理冲突的方法,可以避免堆积问题。它的缺点是不能探查到哈希表上的所有单元,不过至少也能探查到一半单元。

链地址法(拉链法)
链地址法的思想是将哈希表的每个单元作为链表的头结点,所有哈希地址为i的元素构成一个同义词链表。即发生冲突时就把该关键字链在以该单元为头结点的链表的尾部。(图得靠自己脑补)链地址法适用于经常插入删除的情况,其中查找、插入和删除操作主要在同义词链中进行。

  1. 再哈希法
    在构造函数时同时构造多个不同的哈希函数。当哈希地址发生冲突用其他的函数计算另一个哈希函数地址,直到冲突不再产生为止。这种方法不易产生聚集,但增加了计算时间。

  2. 建立公共溢出区
    建立公共溢出区的基本思想是将哈希表分为基本表和溢出表2部分,发生冲突的元素都放入溢出表中

  • 哈希函数数学原理?hash相当与把值映射到另外一个空间。

a是str,s是seed。这个哈希函数的好坏首先取决于这个序列的循环周期,如果周期很短的话,周期之后的字符就会乘以和前面的某个对应字符相同的系数,这样很容易产生碰撞。这个hash函数足够好的条件是:

  1. s是奇数

不是所有的奇数都满足第二条,但是131是满足的。

  • Key 是 int 类型,32 位 Hash Key 添加多少元素时候冲突概率变高?


哈希碰撞与生日攻击

  • STL迭代器失效情况?

迭代器是一种面向对象的广义指针,用于指向容器中或流中的对象。可以看做是一种指向数据的指针。

向容器中添加或者删除元素的操作可能会使指向容器元素的迭代器失效,失效的迭代器将不指向任何元素。

一般有两种情况无法通过迭代器 操作遍历整个stl容器; 无法通过迭代器存取迭代器所指向的内存。

eg. vector 使用erase不当,使用erase删除某一个结点之后,vector迭代器虽然还是指向当前位置,而且也引起了元素前挪,但是由于删除结点的迭代器就已经失效,指向删除点后面的元素的迭代器也全部失效,所以不能对当前迭代器进行任何操作;需要对迭代器重新赋值或者接收erase它的返回值;

  • STL线程安全?

STL 语义上不提供任何强度的线程安全保证。使用 STL 做多线程编程是基于你对实现的了解的。因此你这个问题不可能有一个简单的回答,假如你读的时候(锁定的情况下)获取了引用,而随后的写触发了重新分配,那照样会有问题。读还有一致性的问题,而在 vector 上你大体上只能(通过锁)获得按照索引原子读取一个值这样的能力,索引、多次读之间都可能不一致。
并发编程的核心问题从来就不是并发容器或者同步原语,而是并发模式,你要处理什么问题?你需要什么样的语义?这才是核心问题。

指针

  • 什么是虚函数?基类指针调用虚函数时会出现哪些情况?

虚函数主要是用来实现多态和多重继承的

动态单分派子类型多态,运行时决定,基于一个类型,以子类型-超类关系实现多态。

定义一个函数为虚函数,不代表函数为不被实现的函数。

定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。

定义一个函数为纯虚函数,才代表函数没有被实现。

定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。

  • C 如何实现虚函数的机制?虚表保存在哪?虚表的大小?虚表大小和虚函数数量是否有关?

都会有一个虚函数表vtable,表的第一个位置放的是跟运行时有关的东西(RTTI),然后就是根据你类中虚函数的声明次序来存放。然后,在每一个类对象中,都会含有一个指向虚函数表的指针vtpr,在程序运行时,会根据类的动态类型的,通过vtpr从而找到相应的vtable,从而执行某个虚函数正确的版本。
《深度探索C 对象模型》(Inside The Model C ),虚函数实现机制主要在第四章有详细讲解。

  • 类的内存布局是什么样的?考虑有虚函数、多继承、虚继承几种情况。

简单总结一下就是类只有成员变量占用内存(静态成员不占类内部内存,函数不占内存)。如果有虚函数,每个类对象都会有一个虚函数指针Vptr(占用一个指针大小的内存),vptr指向一个虚函数表,表里面记录了各项标记virtual的函数,子类如果覆盖父类虚函数,对应虚表位置的虚函数会被子类的替换。如果是虚继承,还会有虚基类表记录当前对象相对虚基类的偏移,以及一个虚基类指针指向这个虚基类表。

虚表在编译完成时大小与布局就被决定了,加载时其内存位置也就被确定了。

  1. 单继承一个或多个类只有一个虚表一个虚指针

  2. 普通多继承会有基类的个数个虚表,基类的个数个虚指针。派生类自己独有的虚函数可能会放在第一个虚表的最后面

  3. 单个虚继承会有两个虚表(看情况)以及一个虚基类表,两个虚指针(这个可能与我们想象中的不一样,一个指向自己独有的虚函数的虚表,一个指向覆盖基类虚函数的虚表)以及一个虚基类指针与虚基类表 注意:如果派生类自己的虚函数与基类完全相同,可能只有一个虚表,一个虚指针

  4. 菱形多虚继承会有基类的个数个虚指针以及虚表(看情况,第3条有提到),有几个虚继承就有几个虚基类指针以及虚基类表

  • 构造函数调用虚函数会出现什么情况

《Effective C 》条款9:永远不要在构造函数或析构函数中调用虚函数。

  • 介绍C 模版?

模板是泛型编程的基础,泛型编程即以一种独立于任何特定类型的方式编写代码。

模板是创建泛型类或函数的蓝图或公式。库容器,比如迭代器和算法,都是泛型编程的例子,它们都使用了模板的概念。

每个容器都有一个单一的定义,比如 向量,我们可以定义许多不同类型的向量,比如 vector <int> 或 vector <string>

  • 实例化过程是可以任意实例化的么?为什么?

  • shared_ptr与weak_ptr的区别

shared_ptr在原始指针周围包装引用计数机制。所以对于shared_ptr的每个实例,引用计数增加1。如果两个share_ptr对象引用他们,他们永远不会被删除,因为他们永远不会结束一个引用计数为零。

weak_ptr指向一个shared_ptr,但不增加它的引用计数。这意味着即使有一个weak_ptr引用,仍然可以删除underying对象。

这种方式的工作原理是,weak_ptr可以用来创建一个shared_ptr,每当有人想要使用基础对象。然而,如果对象已经被删除,则返回shared_ptr的空实例。由于底层对象上的引用计数不会随weak_ptr引用而增加,因此循环引用不会导致底层对象未被删除。

  • shared_ptr是不是线程安全的,如果要线程安全,最重要的是那一点?在什么地方实现线程安全?

如何修改shared_ptr智能指针,让他支持多线程?

  • 析构函数为什么加virtual

Effiective C 3rd edition.

Item 7: Declare destructors virtual inpolymorphicbase classes.

就是要避免base class的无效释放以造成memory leak。

什么时候用呢,可以说只要涉及到继承的多样性,基本上基类就逃不过虚析构函数了。

  • 多态除了用virtual这种动态多态,有没有了解过静态多态?有哪几种多态?

静态动态(编译时多态):主要通过函数和运算符重载来实现;

动态动态(运行时多态):主要通过继承和虚函数来实现.

  • 模版实例化是什么意思?发生在什么阶段?为什么发生在这个阶段?

当首次为每个类型调用函数模板时,编译器会创建一个实例化。每个实例化是专用于该类型的模板化函数版本。每次将该函数用于该类型时,此实例化都将调用。如果有几个相同的实例化,即使在不同的模块中,也只有该实例化的一个副本将在可执行文件中结束。

函数模板实例化

  • 有没有办法把模版类实现放在cpp文件里?

在分离式编译的环境下,编译器编译某一个.cpp文件时并不知道另一个.cpp文件的存在,也不会去查找(当遇到未决符号时它会寄希望于连接器)。这种模式在没有模板的情况下运行良好,但遇到模板时就傻眼了,因为模板仅在需要的时候才会实例化出来,所以,当编译器只看到模板的声明时,它不能实例化该模板,只能创建一个具有外部连接的符号并期待连接器能够将符号的地址决议出来。然而当实现该模板的.cpp文件中没有用到模板的实例时,编译器懒得去实例化,所以,整个工程的.obj中就找不到一行模板实例的二进制代码,于是连接器也黔驴技穷了。

C 中的模板类声明头文件和实现文件分离后,如何能实现正常编译?Export Template,即外名模板,它的作用在于使得模板代码可依照C/C 语言习惯,将模板声明和实现分开分别放到.h和.cpp文件中,并且可以减少冗长的模板编译时间(否则同一模板实例需要在不同编译单元中分别实例化)。

  • override和overload的区别

Overload(重载):在C 程序中,可以将语义、功能相似的几个函数用同一个名字表示,但参数或返回值不同(包括类型、顺序不同),即函数重载。

1)相同的范围(在同一个类中);

2)函数名字相同;

3)参数不同;

4virtual关键字可有可无。

Override(覆盖):是指派生类函数覆盖基类函数,特征是:

1)不同的范围(分别位于派生类与基类);

2)函数名字相同;

3)参数相同;

4)基类函数必须有virtual关键字。

Overwrite(重写):是指派生类的函数屏蔽了与其同名的基类函数,规则如下:

(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。

(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。

  • C 为什么要增加override关键字?为了解决什么问题?

如果派生类在虚函数声明时使用了override描述符,那么该函数必须覆盖其基类中的同名函数,否则代码将无法通过编译。

如果派生类里面是像覆盖虚函数 就加上关键字override 这样编译器可以辅助检查是不是正确覆盖,如果没加这个关键字 也没什么严重的error 只是少了编译器检查的安全性

计算机基础

  • TCP/UDP

小结TCP与UDP的区别:

  1. 基于连接与无连接;

  2. 对系统资源的要求(TCP较多,UDP少);

  3. UDP程序结构较简单;

  4. 流模式与数据报模式 ;

  5. TCP保证数据正确性,UDP可能丢包,TCP保证数据顺序,UDP不保证。

  • 线程/进程

线程进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.

  • 红黑树

红黑树(英语:Red–black tree)是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组。它是在1972年由鲁道夫·贝尔发明的,他称之为'对称二叉B树',它现代的名字是在Leo J. Guibas和Robert Sedgewick于1978年写的一篇论文中获得的。它是复杂的,但它的操作有着良好的最坏情况运行时间,并且在实践中是高效的:它可以在Ologn时间内做查找,插入和删除,这里的n是树中元素的数目。

  • 设计模式

设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。

游戏编程模式

程序设计

  • TopK

堆排序

快速排序,不断调用分治函数,直到它输出的position = K-1

  • 设计随机函数,在圆内任意点概率相同

蒙特卡洛,随机模拟。产生2个范围[-r,r]之间的随机数dx,dy,理论上(x dx,y dy)在{{x-r,y-r},{x r,y r}}矩形(左下角,右上角)范围内。添加判断(x dx,y dy)距离圆心的距离小于r即可。

CDF

  • 二叉树翻转

Loading...

/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
public class Solution {
public TreeNode invertTree(TreeNode root) {
if (root == null) {
return null;
}
root.left = invertTree(root.left);
root.right = invertTree(root.right);
TreeNode tmp = root.left;
root.left = root.right;
root.right = tmp;
return root;
}
}
  • 最长递增子序列

动态规划

图形学

  • Forward Shading 和 Deferred Shading 的优劣

  • 在圆上任意选取三个点,三个点构成锐角三角形的概率?

圆上任选三点组成三角形,这个三角形是锐角、钝角和直角三角形的概率分别是多少?

  • 怎样判断一个点在三角形内部?

判断点是否在三角形内

  • 有N个三角形,怎样找出所有相交的三角形

两个三角形香蕉

首先判相交,从两个三角形上分别枚举一条线段判断是否相交,

如果不相交,枚举一个三角形的顶点判断是否在另一个三角形内,

如果也互不包含,那么剩下就是相离这种情况了,

上述涉及到的所有判定都只需要利用叉积判断方向。

N个?

来源:葡式蛋挞 - 计算机图形学

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多