拼多多 c++后端最新面试题
1. c++ list与vector的区别是什么?
-
底层实现的区别:vector基于动态数组实现,元素在内存中连续存储,list基于双向链表实现,元素分散存储在内存中,通过指针连接
-
访问效率的区别:vector支持随机访问,通过索引
[]或at()访问元素的时间复杂度为 O (1),list不支持随机访问,访问元素需要从头或尾遍历,时间复杂度为 O (n) -
内存利用率的区别:vector内存利用率高,只需存储元素本身,内存连续,list需要额外空间存储前后指针,内存开销更大,且可能存在内存碎片
-
插入和删除操作的区别:vector在尾部插入 / 删除效率高(O (1)),在中间或头部插入 / 删除需要移动大量元素,效率低(O (n)),可能需要重新分配内存并复制元素;而 list 在任何位置插入 / 删除元素(已知迭代器位置时)效率都很高(O (1)),不需要移动元素,只需调整指针,不会导致内存重新分配。
-
适用场景的区别:vecto适合r需要频繁随机访问元素、主要在尾部进行插入 / 删除操作、元素数量相对稳定的场景,而 list 适合需要频繁在任意位置插入 / 删除元素,不需要随机访问元素,元素数量动态变化较大的场景
2. c++ sort的优化策略是什么?
td::sort主要是三种算法的结合体:插入排序,快速排序,堆排序。
|
算法 |
时间复杂度 |
优点 |
缺点 |
|---|---|---|---|
|
插入排序 |
O(N*N) |
当数据量很少时,效率比较高。 |
当数据量比较大时,时间复杂度比较高。 |
|
快速排序 |
平均 O(NlogN),最坏 O(NxN) |
大部分时候性能比较好。 |
算法时间复杂度不稳定,数据量大时递归深度很大,影响程序工作效率。 |
|
堆排序 |
O(N*logN) |
算法时间复杂度稳定,比较小,适合数据量比较大的排序。 |
堆排序在建堆和调整堆的过程中会产生比较大的开销,数据量少的时候不适用。 |
std::sort 根据上文提到的几种算法的优缺点,对排序算法进行整合。
-
快速排序,递归排序到一定深度后,数据已经被分为多个子区域,子区域里面的数据可能是无序的,但是子区域之间已经是有序了。
-
在这多个子区域里,如果某个子区域数据个数大于阈值(16),采用堆排序,使得某个子区域内部有序。
-
剩下的没有被堆排序的小区域,数据量都是小于阈值的,最后整个数据区域采用插入排序。

-
std::sort 采用的是分治思维,先采用快速排序,将整个区域分成多个子区域,每个子区域内部根据数据量采用不同算法。
-
分治后,各个子区域局部有序后再通过整个区域进行排序。
3. 不可重复读和可重复读的区别是什么?
在MySQL中,不可重复读和可重复读的区别主要体现在同一事务内多次读取同一数据时的表现上。
-
不可重复读发生在较低的隔离级别(如Read Committed),此时同一事务中多次读取同一数据可能得到不同结果,因为其他事务修改并提交了该数据,导致本事务后续读取到新值;
-
而可重复读是MySQL InnoDB存储引擎的默认隔离级别,它能保证同一事务内多次读取同一数据时结果保持一致,即使其他事务修改并提交了该数据,本事务也不会看到这些变化,这是通过InnoDB的多版本并发控制(MVCC)实现的,事务会读取数据的一个一致性快照。
简单来说,不可重复读允许事务看到其他已提交事务的修改,而可重复读则保证事务在整个过程中看到的数据始终一致,不受其他已提交事务的影响。
4. unorder_map底层的hash表是怎么实现的?
C++ 中的 unordered_map 底层采用哈希表(Hash Table)实现,其核心结构是一个由桶(bucket)组成的数组,每个桶会指向一个链表用于存储哈希冲突的元素。

具体实现机制如下:哈希表的基础是数组(桶数组),每个元素通过哈希函数计算得到一个哈希值,再通过哈希值映射到数组的特定索引(桶位置)。
-
当插入键值对时,首先对键应用哈希函数得到哈希值,然后通过取模运算(通常是对桶数组大小取模)确定该元素应存入的桶。如果多个键计算后映射到同一个桶(即哈希冲突),这些元素会被存储在该桶对应的链表中(链地址法解决冲突)。
-
在查找元素时,同样先对键计算哈希值找到对应的桶,再遍历该桶的链表(或其他结构),通过键的相等性比较找到目标元素。
为了保证操作效率,哈希表会在元素数量达到一定阈值(负载因子,通常为 1.0)时进行扩容:创建一个更大的桶数组(通常是原大小的两倍),然后将所有现有元素重新计算哈希并迁移到新的桶中,以减少哈希冲突,维持查找、插入、删除操作的平均 O(1) 时间复杂度。
5. http的get和post区别是什么?
根据 RFC 规范,GET 的语义是从服务器获取指定的资源,这个资源可以是静态的文本、页面、图片视频等。GET 请求的参数位置一般是写在 URL 中,URL 规定只能支持 ASCII,所以 GET 请求的参数只允许 ASCII 字符 ,而且浏览器会对 URL 的长度有限制(HTTP协议本身对 URL长度并没有做任何规定)。
比如,你打开我的文章,浏览器就会发送 GET 请求给服务器,服务器就会返回文章的所有文字及资源。

根据 RFC 规范,POST 的语义是根据请求负荷(报文body)对指定的资源做出处理,具体的处理方式视资源类型而不同。POST 请求携带数据的位置一般是写在报文 body 中,body 中的数据可以是任意格式的数据,只要客户端与服务端协商好即可,而且浏览器不会对 body 大小做限制。
比如,你在我文章底部,敲入了留言后点击「提交」(暗示你们留言),浏览器就会执行一次 POST 请求,把你的留言文字放进了报文 body 里,然后拼接好 POST 请求头,通过 TCP 协议发送给服务器。

如果从 RFC 规范定义的语义来看:
-
GET 方法就是安全且幂等的,因为它是「只读」操作,无论操作多少次,服务器上的数据都是安全的,且每次的结果都是相同的。所以,可以对 GET 请求的数据做缓存,这个缓存可以做到浏览器本身上(彻底避免浏览器发请求),也可以做到代理上(如nginx),而且在浏览器中 GET 请求可以保存为书签。
-
POST 因为是「新增或提交数据」的操作,会修改服务器上的资源,所以是不安全的,且多次提交数据就会创建多个资源,所以不是幂等的。所以,浏览器一般不会缓存 POST 请求,也不能把 POST 请求保存为书签。
但是实际过程中,开发者不一定会按照 RFC 规范定义的语义来实现 GET 和 POST 方法。比如:
-
可以用 GET 方法实现新增或删除数据的请求,这样实现的 GET 方法自然就不是安全和幂等。
-
可以用 POST 方法实现查询数据的请求,这样实现的 POST 方法自然就是安全和幂等。
6. mysql 如何进行多表连接?
数据库有以下几种联表查询类型:

1. 内连接 (INNER JOIN)
内连接返回两个表中有匹配关系的行。示例:
SELECT employees.name, departments.name
FROM employees
INNER JOIN departments
ON employees.department_id = departments.id;
这个查询返回每个员工及其所在的部门名称。
2. 左外连接 (LEFT JOIN)
左外连接返回左表中的所有行,即使在右表中没有匹配的行。未匹配的右表列会包含NULL。示例:
SELECT employees.name, departments.name
FROM employees
LEFT JOIN departments
ON employees.department_id = departments.id;
这个查询返回所有员工及其部门名称,包括那些没有分配部门的员工。
3. 右外连接 (RIGHT JOIN)
右外连接返回右表中的所有行,即使左表中没有匹配的行。未匹配的左表列会包含NULL。示例:
SELECT employees.name, departments.name
FROM employees
RIGHT JOIN departments
ON employees.department_id = departments.id;
这个查询返回所有部门及其员工,包括那些没有分配员工的部门。
4. 全外连接 (FULL JOIN)
全外连接返回两个表中所有行,包括非匹配行,在MySQL中,FULL JOIN 需要使用 UNION 来实现,因为 MySQL 不直接支持 FULL JOIN。示例:
SELECT employees.name, departments.name
FROM employees
LEFT JOIN departments
ON employees.department_id = departments.id
UNION
SELECT employees.name, departments.name
FROM employees
RIGHT JOIN departments
ON employees.department_id = departments.id;
这个查询返回所有员工和所有部门,包括没有匹配行的记录。
7. TCP协议栈安全关闭的话,要怎么关闭?
TCP 协议栈的安全关闭需要通过四次挥手过程来实现,确保双方都能正确处理剩余数据并释放资源,具体步骤如下:

-
当主动关闭方(如客户端)完成数据发送后,会向被动关闭方(如服务器)发送一个 FIN 报文段,表示自己已无数据要发送。
-
被动关闭方收到 FIN 后,会先回复一个 ACK确认报文,告知已收到关闭请求,此时主动关闭方进入 FIN_WAIT_2 状态,被动关闭方则进入 CLOSE_WAIT 状态,继续处理可能剩余的数据发送。
-
当被动关闭方也完成所有数据发送后,会向主动关闭方发送一个 FIN 报文,表示自己也准备关闭连接。
-
主动关闭方收到这个 FIN 后,会回复一个 ACK 确认,此时主动关闭方进入 TIME_WAIT 状态(等待一段时间以确保被动关闭方能收到 ACK,防止报文丢失导致的重传),被动关闭方在收到 ACK 后则直接关闭连接。
-
主动关闭方在 TIME_WAIT 状态等待足够长的时间(通常为 2 倍最大报文段寿命 MSL)后,确认对方已正确关闭,才最终关闭连接,完成整个安全关闭过程。这一过程确保了双方都有充足的时间处理剩余数据,避免了数据丢失或连接异常中断的问题。
8. TCP协议各阶段状态有哪些?标志位有哪些?
TCP 协议在通信过程中会经历多个状态转换,同时通过不同的标志位控制连接的建立、数据传输和关闭过程。

-
连接建立阶段:包括
LISTEN(服务器监听状态,等待客户端连接)、SYN_SENT(客户端发送 SYN 后等待确认)、SYN_RCVD(服务器收到 SYN 后发送 SYN+ACK,等待客户端 ACK)、ESTABLISHED(连接建立完成,可进行数据传输)。 -
数据传输阶段:主要保持
ESTABLISHED状态,直到一方准备关闭连接。 -
连接关闭阶段:有
FIN_WAIT_1(主动关闭方发送 FIN 后等待 ACK)、FIN_WAIT_2(收到对方对 FIN 的 ACK,等待对方发送 FIN)、CLOSE_WAIT(被动关闭方收到 FIN 并回复 ACK 后,等待自身数据发送完成)、LAST_ACK(被动关闭方发送 FIN 后等待 ACK)、TIME_WAIT(主动关闭方收到对方 FIN 并回复 ACK 后,等待一段时间确保对方收到 ACK)、CLOSED(连接完全关闭)。
TCP 报文段头部包含 6 个标志位,分别是:

-
SYN(同步):用于建立连接时同步序列号,在连接请求(SYN 报文)和同意连接(SYN+ACK 报文)中设置。
-
ACK(确认):表示报文中的确认号有效,用于对收到的报文进行确认,除了初始 SYN 报文外,多数报文都会设置该标志。
-
FIN(结束):表示发送方已完成数据传输,请求关闭连接。
-
RST(重置):用于强制关闭异常连接,当检测到错误或非法请求时发送。
-
PSH(推送):指示接收方立即将数据提交给应用层,而非缓存。
-
URG(紧急):表示报文中包含紧急数据,应优先处理,配合紧急指针使用。
9. 算法手撕:字符串最大递增子串
要解决字符串最大递增子串问题,我们需要找到字符串中最长的字符严格递增的连续子串。这里的 "递增" 指的是字符的 ASCII 值严格递增。
下面是一个高效的 C++ 实现,时间复杂度为 O (n),其中 n 是字符串的长度:
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
// 正确定义函数,移除多余的注释符
string longestIncreasingSubstring(const string& s) {
if (s.empty()) return"";
int maxLength = 1; // 最长递增子串的长度
int currentLength = 1; // 当前递增子串的长度
int start = 0; // 最长递增子串的起始索引
int currentStart = 0; // 当前递增子串的起始索引
for (int i = 1; i < s.length(); ++i) {
// 如果当前字符大于前一个字符,说明仍在递增
if (s[i] > s[i-1]) {
currentLength++;
} else {
// 检查当前递增子串是否是最长的
if (currentLength > maxLength) {
maxLength = currentLength;
start = currentStart;
}
// 重置当前递增子串
currentLength = 1;
currentStart = i;
}
}
// 最后一次检查,处理字符串结束时的递增子串
if (currentLength > maxLength) {
maxLength = currentLength;
start = currentStart;
}
// 返回最长递增子串
return s.substr(start, maxLength);
}
int main() {
string testStr1 = "abcabcdabcde";
string testStr2 = "abacdb";
string testStr3 = "zyxwvu";
string testStr4 = "a";
string testStr5 = "";
cout << "测试字符串1: " << testStr1 << endl;
cout << "最长递增子串: " << longestIncreasingSubstring(testStr1) << endl << endl;
cout << "测试字符串2: " << testStr2 << endl;
cout << "最长递增子串: " << longestIncreasingSubstring(testStr2) << endl << endl;
cout << "测试字符串3: " << testStr3 << endl;
cout << "最长递增子串: " << longestIncreasingSubstring(testStr3) << endl << endl;
cout << "测试字符串4: " << testStr4 << endl;
cout << "最长递增子串: " << longestIncreasingSubstring(testStr4) << endl << endl;
cout << "测试字符串5: " << testStr5 << endl;
cout << "最长递增子串: " << longestIncreasingSubstring(testStr5) << endl;
return0;
}
运行的效果:
测试字符串1: abcabcdabcde
最长递增子串: abcde
测试字符串2: abacdb
最长递增子串: acd
测试字符串3: zyxwvu
最长递增子串: z
测试字符串4: a
最长递增子串: a
测试字符串5:
最长递增子串:
这个算法的实现思路如下:
-
我们使用一次遍历的方式,时间复杂度为 O (n),其中 n 是字符串的长度
-
算法维护两个变量对:1、currentLength 和 currentStart:跟踪当前正在检查的递增子串 ;2、maxLength 和 start:记录目前发现的最长递增子串
-
遍历过程中:当遇到字符递增时(当前字符 > 前一个字符),延长当前子串;当递增中断时,比较当前子串与最长子串,更新最长子串(如果需要);遍历结束后,进行最后一次比较,确保没有遗漏最长子串
-
特殊情况处理:空字符串返回空;完全递减的字符串返回第一个字符;单字符字符串返回自身
该算法的时间复杂度为 O (n),其中 n 是字符串的长度,因为算法只对字符串进行一次线性遍历(从第一个字符到最后一个字符),遍历过程中每个字符只被处理一次,没有嵌套循环或递归操作。所有操作(比较字符、更新计数器等)都是常数时间 O (1) 级别的,因此整体时间复杂度为线性级别。
空间复杂度是O (1)(常数级),因为算法仅使用了固定数量的额外变量(maxLength、currentLength、start、currentStart 等)来存储中间结果,这些变量的数量不随输入字符串的长度变化而变化。即使输入字符串非常长,额外占用的内存空间也保持不变,因此空间复杂度是常数级别的。
更多推荐
所有评论(0)