【嵌入式面试】

一、数据结构和算法

1.数组、链表、二叉排序增删改查的时间复杂度

数据结构 插入 优点 缺点
数组 O(1) O(n) O(n) O(n) 插入效率高,查找速度快 空间利用率不高、数组空间大小固定、内存空间要求高
有序数组 O(n) O(n) O(logn) O(logn)
无序链表 O(1) O(n) O(n) O(n) 插入元素速度快、内存利用率高、可以动态拓展 随机访问效率低
有序序链表 O(n) O(n) O(n) O(n)
二叉树 O(logn)-O(n) O(logn)-O(n) O(logn)-O(n) O(logn)- O(n) 查找、插入、删除都快、树保持平衡 算法复杂

2.哈希表及其原理

Hash 表即散列表,是通过关键字(key)根据哈希算法计算出应该存储地址的位置。其最突出的优点是查找和插入删除是O(1),最坏的就是hash值全都映射在同一个地址上,这样哈希表就会退化成链表。
实现原理

  1. 把 Key 通过哈希函数转换成一个整型数字,然后将这份数字对数组长度进行取余,取余结果就当作数组的下标,将value 存储在以该数字为下标的数组空间里。
  2. 当使用哈希表进行查询的时候,就是再次使用哈希函数将 key 转换为对应的数组下标,并定位到该空间获取 value

常见的哈希算法
哈希表的组成取决于哈希算法,也就是哈希函数的构成,下面列举几种常见的哈希算法。

  1. 直接定址法
    取关键字或关键字的某个线性函数值为散列地址。
    即 f(key) = key 或 f(key) = a*key + b,其中a和b为常数。
  2. 除留余数法
    取关键字被某个不大于散列表长度 m 的数 p 求余,得到的作为散列地址。
    即 f(key) = key % p, p < m。这是最为常见的一种哈希算法。
  3. 数字分析法
    当关键字的位数大于地址的位数,对关键字的各位分布进行分析,选出分布均匀的任意几位作为散列地址。
    仅适用于所有关键字都已知的情况下,根据实际应用确定要选取的部分,尽量避免发生冲突。
  4. 平方取中法
    先计算出关键字值的平方,然后取平方值中间几位作为散列地址。
    随机分布的关键字,得到的散列地址也是随机分布的。
  5. 随机数法
    选择一个随机函数,把关键字的随机函数值作为它的哈希值。
    通常当关键字的长度不等时用这种方法。

哈希hash冲突

哈希冲突是指哈希函数算出来的地址被别的元素占用了
key1 ≠ key2 , f(key1) = f(key2)
一般来说,哈希冲突是无法避免的,如果要完全避免的话,也就是一个值就有一个索引,这样一来,空间就会增大,甚至内存溢出。
解决办法:

  1. 线性探测
    使用hash函数计算出的位置如果已经有元素占用了,则向后依次寻找,找到表尾则回到表头,直到找到一个空位
  2. 开链
    每个表格维护一个链表list,如果hash函数计算出的格子相同,则按顺序存在这个list中
  3. 再散列
    发生冲突时使用另一种hash函数再计算一个地址,直到不冲突
  4. 二次探测
    使用hash函数计算出的位置如果已经有元素占用了,按照 1 2 1^2 12 2 2 2^2 22 3 2 3^2 32…的步长依次寻找,如果步长是随机数序列,则称之为伪随机探测
  5. 公共溢出区
    一旦hash函数计算的结果相同,就放入公共溢出区

3.常用数据结构

链表、栈、队列、树

  1. 链表
    是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列节点组成,这些节点不必在内存中相连。每个节点由 数据部分 Data 和链部分 Next,Next 指向下一个节点,这样当添加或者删除时,只需要改变相关 节点的 Next 的指向,效率很高

4.二叉查找树、红黑树

二叉树是每个结点最多有两个子树的树结构。通常子树被称作“左子树”(left subtree)和“右子树”;

平衡二叉树(AVL树)在符合二叉查找树左子树的键值小于根的键值,右子树的键值大于根的键值)的条件下,还满足任何节点的两个子树的高度最大差为1;

二叉查找树(中序遍历,时间O(n))
在树中的任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都要大于这个节点的值。

  1. 查找
    首先取根节点,如果它等于要查找的数据,则直接返回,如果小于要查找的数据,则在右子树中继续查找,如果大于要查找的数据,则在左子树中继续查找,也就是二分查找的思想,这样一直递归。
  2. 插入
    首先还是从根节点开始,然后依次它与节点的关系。如果要插入的数据比节点的数据大,并且节点的右子树为空,就将新数据直接插到右子节点的位置;如果不为空,就再递归遍历右子树,查找插入位置。同理,如果要插入的数据比节点的数据小,也是类似的操作。
  3. 删除
    如果要删除的节点没有子节点,只需要将父节点中,指向要删除节点的指针置为NULL,
    如果要删除的节点只有一个子节点(只有左子节点或者右子节点),只需要删除父节点中,指向要删除的指针,让它指向要删除的节点的子节点就可以了。
    如果要删除的节点上有两个子节点,要稍微复杂一点。首先找到这个节点的右子树中最小的节点,把它替换到要删除的节点,然后再删除这个最小节点。因为最小节点肯定没有左子节点。

红黑树
红黑树是一个近似平衡的二叉树,
7. 定义:
具有二叉查找树的特点;节点是黑色
每个叶子节点都是黑色的空节点(NIL),也就是说,叶子节点不存数据
任何相邻的节点都不能同时为红色,也就是说,红色节点是被黑色节点隔开的
每个节点,从该节点到达其可达的叶子节点是所有路径,都包含相同数目的黑色节点

5.STL常用容器

C++ STL从广义来讲包括了三类:算法,容器和迭代器。
算法包括排序,复制等常用算法,以及不同容器特定的算法。
容器就是数据的存放形式,包括序列式容器和关联式容器,序列式容器就是list,vector等,关联式容器就是set,map等。
迭代器就是在不暴露容器内部结构的情况下对容器的遍历。
顺序容器:
3. vector
是一种动态数组,具有连续的存储空间,支持快速随机访问。但在插入和删除操作方面,效率比较
底层
底层结构为数组,由于数组的特点,vector也具有以下特性:
1)、O(1)时间的快速访问;
2)、顺序存储,所以插入到非尾结点位置所需时间复杂度为O(n),删除也一样;
3)、扩容规则
当我们新建一个vector的时候,会首先分配给他一片连续的内存空间,如std::vector vec,当通过push_back向其中增加元素时,如果初始分配空间已满,就会引起vector扩容,其扩容规则在gcc下以2倍方式完成:
首先重新申请一个2倍大的内存空间
然后将原空间的内容拷贝过来;
最后将原空间内容进行释放,将内存交还给操作系统;
4. deque
和 vector 类似,支持快速随机访问。二者最大的区别在于,vector 只能在末端插入 数据,而 deque 支持双端插入数据。deque 空间的重新分配要比 vector 快,重新分配空间后,原有的元素是不需要拷贝的。
底层:
底层数据结构为一个中央控制器(map)和多个缓冲区,支持首位(中间不能)快速增删,也支持也随访问,deque 的内存空间分布是小片的连续小片间用链表相连中控器(map保存着一组指针,每个指针指向一段数据空间的起始位置,通过中控器可以找到所有的数据空间。如果中控器的数据空间满了,会重新申请一块更大的空间,并将中控器的所有指针拷贝到新空间中。
1.start迭代器:绑定到第一个有效的map结点和该结点对应的缓冲区。
2.finish迭代器:绑定到最后一个有效的map结点和该结点对应的缓冲区。
5. list
是一个
双向链表
,它的内存空间可以不连续,通过指针来进行数据的访问,导致其随机存储非常低效;但 list 可以很地支持任意地方的插入和删除,只需移动相应的指针即可
底层:双向链表

关联容器:

  1. map && multimap
    是一种关联性容器,该容器用唯一的关键字来映射相应的值,即具有 key-value 功能。map 内部自建一棵红黑树(一种自平衡二叉树),这棵树具有数据自动排序的功能,内部数据都是有序的。
    map与multimap的区别在于,multimap允许关键字重复,而map不允许重复。
    底层:
    根据红黑树的原理,map与multimap可以实现O(lgn)的查找,插入和删除
  2. unordered_map 与unordered_multimap
    无序排序,低层是哈希表,因此其查找时间复杂度理论上达到了O(n)
  3. set & multiset
    是一种关联性容器,set系与map系的区别在于map中存储的是,而set可以理解为关键字即值,即只保存关键字的容器。
    低层
    底层使用红黑树实现,插入删除操作时仅仅移动指针即可,涉及内存的移动和拷贝,所以效率比较高。set 中的元素都是唯一的,而且默认情 况下会对元素进行升序排列。所以在 set 中,要改变元素值必须先删除旧元素再插入新元素。不提供直接存取元素的任何操作函数, 只能通过迭代器进行间接存取
  4. queue
    是一个队列,实现先进先出功能,queue 不是标准的 STL 容器,却以标准的 STL 容器为基础。(stack和queue其实是适配器,而不叫容器,因为是对容器的再封装)
    底层:
    queue 是在 deque 的基础上封装的。
  5. stack
    实现先进后出的功能,和 queue 一样,也是内部封装了 deque
  6. priority_queue:
    底层数据结构一般为vector为底层容器堆heap为处理规则来管理底层容器实现。

5. 迭代器失效

  1. vector迭代器失效
    (1)当执行erase方法时,指向删除节点的迭代器全部失效,指向删除节点之后的全部迭代器也失效
    (2)当进行push_back()方法时,end操作返回的迭代器肯定失效
    (3)当插入(push_back)一个元素后,capacity返回值与没有插入元素之前相比有改变,则需要重新加载整个容器,此时first和end操作返回的迭代器都会失效。
    (4)当插入(push_back)一个元素后,如果空间未重新分配,指向插入位置之前的元素的迭代器仍然有效,但指向插入位置之后元素的迭代器全部失效。
  2. deque迭代器
    (1)对于deque,插入到除首尾位置之外的任何位置都会导致迭代器、指针和引用都会失效,但是如果在首尾位置添加元素,迭代器会失效,但是指针和引用不会失效
    (2)如果在首尾之外的任何位置删除元素,那么指向被删除元素外其他元素的迭代器全部失效
    (3)在其首部或尾部删除元素则只会使指向被删除元素的迭代器失效。
  3. map
    对于map,当进行erase操作后,只会使当前迭代器失效,不会造成其他迭代器失效,这是因为map底层实现是由红黑树实现的,所以当删除一个元素时,会进行二叉树的调整,但每个节点在内存中的地址是没有改变的,改变的只是他们之间的指针指向。

6.为什么要有迭代器,不是有指针吗?

Iterator(迭代器)模式是运用于聚合对象的一种模式,通过运用该模式,使得我们可以在不知道对象内部表示的情况下,按照一定顺序(由iterator提供的 方法)访问聚合对象中的各个元素。迭代器不是指针,是类模板,表现的像指针,提供了比指针更高级的行为,相当于一种智能指针,他可以根据不同类型的数据结构来实现不同的++,–等操作。

7.bfs(广度优先搜索)和dfs(深度优先搜索)

广度优先遍历:指的是从图的一个未遍历的节点出发,先遍历这个节点的相邻节点,再依次遍历每个相邻节点的相邻节点。常用于搜索路径的最优解;
深度优先遍历:主要思路是从图中一个未访问的顶点 V 开始,沿着一条路一直走到底,然后从这条路尽头的节点回退到上一个节点,再从另一条路开始走到底…,不断递归重复此过程,直到所有的顶点都遍历完成,它的特点是不撞南墙不回头,先走完一条路,再换一条路继续走。dfs就是搜索全部的解。
在DFS中关键点是递归以及回溯,在BFS中,关键点则是状态的选取和标记。bfs先进先出(队列),dfs先进后出(栈)
bfs 适用于求源点与目标节点距离近的情况,例如:求最短路径。dfs 更适合于求解一个任意符合方案中的一个或者遍历所有情况,例如:全排列、拓扑排序、求到达某一点的任意一条路径。

8.找到比K大的数,使用快排实现和堆数据结构实现

  1. 最小堆思路:
    维护一个K大小的最小堆,堆中元素个数小于K时,新元素直接进入堆;否则
    当堆顶小于新元素时,弹出堆顶,将新元素加入堆。
    解释:
    由于堆是最小堆,堆顶是堆中最小元素,新元素都会保证比堆顶小,否则新元素替换堆顶,故堆中K个元素是已扫描的元素里最大的K个;堆顶即为第K大的数。

  2. 快排思路:快速排序一趟之后,主元pivot到了他最终应该在的位置,如果是从大到小排序,那么一趟排序之后,他左边的数都是比它大的,他右边的数都是比它小的数,所以,如果我们要找出第K大的数,我们可以在一趟排序后,检查主元的位置和K是否对应
    if(pos+1 == k),因为下标是从0开始的,所以要加1。如果相等的话,就正好是要找的第K大的数,否则,if(pos+1>k),说明要找的数在主元的左边,我们就只需要对左边继续进行快排,这样可以节约时间。if(pos+1

二、C++基础

1.C++ 程序编译过程

编译过程分为四个过程:编译(编译预处理、编译、优化),汇编,链接。

  1. 预处理:将所有的#include头文件以及宏定义替换成其真正的内容,gcc的预处理是预处理器cpp来完成的,得到的是文本文件;
  2. 编译\优化:将源码 .cpp 文件翻译成 .s 汇编代码;
  3. 汇编:将汇编

你可能感兴趣的:(嵌入式面试,面试,c++,职场和发展)