一、数据结构和算法
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值全都映射在同一个地址上,这样哈希表就会退化成链表。
实现原理:
- 把 Key 通过哈希函数转换成一个整型数字,然后将这份数字对数组长度进行取余,取余结果就当作数组的下标,将value 存储在以该数字为下标的数组空间里。
- 当使用哈希表进行查询的时候,就是再次使用哈希函数将 key 转换为对应的数组下标,并定位到该空间获取 value;
常见的哈希算法
哈希表的组成取决于哈希算法,也就是哈希函数的构成,下面列举几种常见的哈希算法。
- 直接定址法
取关键字或关键字的某个线性函数值为散列地址。
即 f(key) = key 或 f(key) = a*key + b,其中a和b为常数。 - 除留余数法
取关键字被某个不大于散列表长度 m 的数 p 求余,得到的作为散列地址。
即 f(key) = key % p, p < m。这是最为常见的一种哈希算法。 - 数字分析法
当关键字的位数大于地址的位数,对关键字的各位分布进行分析,选出分布均匀的任意几位作为散列地址。
仅适用于所有关键字都已知的情况下,根据实际应用确定要选取的部分,尽量避免发生冲突。 - 平方取中法
先计算出关键字值的平方,然后取平方值中间几位作为散列地址。
随机分布的关键字,得到的散列地址也是随机分布的。 - 随机数法
选择一个随机函数,把关键字的随机函数值作为它的哈希值。
通常当关键字的长度不等时用这种方法。
哈希hash冲突
哈希冲突是指哈希函数算出来的地址被别的元素占用了
key1 ≠ key2 , f(key1) = f(key2)
一般来说,哈希冲突是无法避免的,如果要完全避免的话,也就是一个值就有一个索引,这样一来,空间就会增大,甚至内存溢出。
解决办法:
- 线性探测
使用hash函数计算出的位置如果已经有元素占用了,则向后依次寻找,找到表尾则回到表头,直到找到一个空位 - 开链
每个表格维护一个链表list,如果hash函数计算出的格子相同,则按顺序存在这个list中 - 再散列
发生冲突时使用另一种hash函数再计算一个地址,直到不冲突 - 二次探测
使用hash函数计算出的位置如果已经有元素占用了,按照 1 2 1^2 12、 2 2 2^2 22、 3 2 3^2 32…的步长依次寻找,如果步长是随机数序列,则称之为伪随机探测 - 公共溢出区
一旦hash函数计算的结果相同,就放入公共溢出区
3.常用数据结构
有链表、栈、队列、树
- 链表
是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列节点组成,这些节点不必在内存中相连。每个节点由 数据部分 Data 和链部分 Next,Next 指向下一个节点,这样当添加或者删除时,只需要改变相关 节点的 Next 的指向,效率很高
4.二叉查找树、红黑树
二叉树是每个结点最多有两个子树的树结构。通常子树被称作“左子树”(left subtree)和“右子树”;
平衡二叉树(AVL树)在符合二叉查找树(左子树的键值小于根的键值,右子树的键值大于根的键值)的条件下,还满足任何节点的两个子树的高度最大差为1;
二叉查找树(中序遍历,时间O(n))
在树中的任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都要大于这个节点的值。
- 查找
首先取根节点,如果它等于要查找的数据,则直接返回,如果小于要查找的数据,则在右子树中继续查找,如果大于要查找的数据,则在左子树中继续查找,也就是二分查找的思想,这样一直递归。 - 插入
首先还是从根节点开始,然后依次它与节点的关系。如果要插入的数据比节点的数据大,并且节点的右子树为空,就将新数据直接插到右子节点的位置;如果不为空,就再递归遍历右子树,查找插入位置。同理,如果要插入的数据比节点的数据小,也是类似的操作。 - 删除
如果要删除的节点没有子节点,只需要将父节点中,指向要删除节点的指针置为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 可以很好地支持任意地方的插入和删除,只需移动相应的指针即可
底层:双向链表
关联容器:
- map && multimap
是一种关联性容器,该容器用唯一的关键字来映射相应的值,即具有 key-value 功能。map 内部自建一棵红黑树(一种自平衡二叉树),这棵树具有数据自动排序的功能,内部数据都是有序的。
map与multimap的区别在于,multimap允许关键字重复,而map不允许重复。
底层:
根据红黑树的原理,map与multimap可以实现O(lgn)的查找,插入和删除 - unordered_map 与unordered_multimap
无序排序,低层是哈希表,因此其查找时间复杂度理论上达到了O(n) - set & multiset
是一种关联性容器,set系与map系的区别在于map中存储的是,而set可以理解为关键字即值,即只保存关键字的容器。
低层:
底层使用红黑树实现,插入删除操作时仅仅移动指针即可,不涉及内存的移动和拷贝,所以效率比较高。set 中的元素都是唯一的,而且默认情 况下会对元素进行升序排列。所以在 set 中,要改变元素值必须先删除旧元素,再插入新元素。不提供直接存取元素的任何操作函数, 只能通过迭代器进行间接存取 - queue
是一个队列,实现先进先出功能,queue 不是标准的 STL 容器,却以标准的 STL 容器为基础。(stack和queue其实是适配器,而不叫容器,因为是对容器的再封装)
底层:
queue 是在 deque 的基础上封装的。 - stack
实现先进后出的功能,和 queue 一样,也是内部封装了 deque。 - priority_queue:
底层数据结构一般为vector为底层容器,堆heap为处理规则来管理底层容器实现。
5. 迭代器失效
- vector迭代器失效
(1)当执行erase方法时,指向删除节点的迭代器全部失效,指向删除节点之后的全部迭代器也失效
(2)当进行push_back()方法时,end操作返回的迭代器肯定失效。
(3)当插入(push_back)一个元素后,capacity返回值与没有插入元素之前相比有改变,则需要重新加载整个容器,此时first和end操作返回的迭代器都会失效。
(4)当插入(push_back)一个元素后,如果空间未重新分配,指向插入位置之前的元素的迭代器仍然有效,但指向插入位置之后元素的迭代器全部失效。 - deque迭代器
(1)对于deque,插入到除首尾位置之外的任何位置都会导致迭代器、指针和引用都会失效,但是如果在首尾位置添加元素,迭代器会失效,但是指针和引用不会失效
(2)如果在首尾之外的任何位置删除元素,那么指向被删除元素外其他元素的迭代器全部失效
(3)在其首部或尾部删除元素则只会使指向被删除元素的迭代器失效。 - map
对于map,当进行erase操作后,只会使当前迭代器失效,不会造成其他迭代器失效,这是因为map底层实现是由红黑树实现的,所以当删除一个元素时,会进行二叉树的调整,但每个节点在内存中的地址是没有改变的,改变的只是他们之间的指针指向。
6.为什么要有迭代器,不是有指针吗?
Iterator(迭代器)模式是运用于聚合对象的一种模式,通过运用该模式,使得我们可以在不知道对象内部表示的情况下,按照一定顺序(由iterator提供的 方法)访问聚合对象中的各个元素。迭代器不是指针,是类模板,表现的像指针,提供了比指针更高级的行为,相当于一种智能指针,他可以根据不同类型的数据结构来实现不同的++,–等操作。
7.bfs(广度优先搜索)和dfs(深度优先搜索)
广度优先遍历:指的是从图的一个未遍历的节点出发,先遍历这个节点的相邻节点,再依次遍历每个相邻节点的相邻节点。常用于搜索路径的最优解;
深度优先遍历:主要思路是从图中一个未访问的顶点 V 开始,沿着一条路一直走到底,然后从这条路尽头的节点回退到上一个节点,再从另一条路开始走到底…,不断递归重复此过程,直到所有的顶点都遍历完成,它的特点是不撞南墙不回头,先走完一条路,再换一条路继续走。dfs就是搜索全部的解。
在DFS中关键点是递归以及回溯,在BFS中,关键点则是状态的选取和标记。bfs先进先出(队列),dfs先进后出(栈)
bfs 适用于求源点与目标节点距离近的情况,例如:求最短路径。dfs 更适合于求解一个任意符合方案中的一个或者遍历所有情况,例如:全排列、拓扑排序、求到达某一点的任意一条路径。
8.找到比K大的数,使用快排实现和堆数据结构实现
-
最小堆思路:
维护一个K大小的最小堆,堆中元素个数小于K时,新元素直接进入堆;否则
当堆顶小于新元素时,弹出堆顶,将新元素加入堆。
解释:
由于堆是最小堆,堆顶是堆中最小元素,新元素都会保证比堆顶小,否则新元素替换堆顶,故堆中K个元素是已扫描的元素里最大的K个;堆顶即为第K大的数。 -
快排思路:快速排序一趟之后,主元pivot到了他最终应该在的位置,如果是从大到小排序,那么一趟排序之后,他左边的数都是比它大的,他右边的数都是比它小的数,所以,如果我们要找出第K大的数,我们可以在一趟排序后,检查主元的位置和K是否对应
if(pos+1 == k),因为下标是从0开始的,所以要加1。如果相等的话,就正好是要找的第K大的数,否则,if(pos+1>k),说明要找的数在主元的左边,我们就只需要对左边继续进行快排,这样可以节约时间。if(pos+1<k),就说明要找的数是在主元的右半部分。
二、C++基础
1.C++ 程序编译过程
编译过程分为四个过程:编译(编译预处理、编译、优化),汇编,链接。
- 预处理:将所有的#include头文件以及宏定义替换成其真正的内容,gcc的预处理是预处理器cpp来完成的,得到的是文本文件;
- 编译\优化:将源码 .cpp 文件翻译成 .s 汇编代码;
- 汇编:将汇编