图解学习网站:https://xiaolincoding.com
大家好,我是小林。
之前我在公众号说过 25 届理想汽车的校招薪资,总包有 40w+,属实是一线大厂梯队的校招薪资了,跟腾讯、字节、阿里巴巴等互联网大厂是一个梯队的薪资了,这么看理想汽车的薪资还是蛮“理想的。”
25 届理想校招年薪是 16 薪,这跟以前不一样,以前都是 14 薪为主,所以 25 届的月薪 base 会比以前之前的少一些,但是整体的总包是差不多的。
比如有 24 届训练营同学拿到了理想 offer,开的是 31k x 14(总包43.4w),base 就是 31k。
当时谈到理想汽车今年校招给了 16 薪,不少读者留言说,16 薪别想了,能拿满 14 薪都偷笑了。
每个公司的年终奖都是没办法说一定能拿满的,除了跟自己本身绩效有关系之外,还跟公司当年的市场环境有很大的关系,甚至也跟所在部门给公司创作的价值也有关系。
简单来说,公司超预期赚钱了,那么之前谈的年终数很大概率能兑现成功,当然公司赚钱不及预期,年终数自然有可能会减少的,甚至可能直接没有了。
理想汽车在 23 年销量暴涨,23 年全年共交付了 37w+ 辆车, 同比增长182.2%,这一年理想成了交付量最高的中国新势力车企。
既然 23 年市场反馈那么猛,年终奖自然只会多不会少呀,去年就有理想汽车的员工爆料拿到了「超高的年终奖」,能打达到 7-8 个月的水平,这比预期的 2 个月(14 薪)年终奖,翻了 3-4 倍啊,还是蛮给力的。
当时理想汽车 CEO 还为这次超额年终奖的事情,说了自己的想法,企业要做到赏罚分明,而且企业不能只学习华为的流程,而不学华为的利益分配。
只可惜新能源车市场还是太残酷了,后来雷总的小米汽车也杀入了新能源汽车的赛道,理想的 MEGA 上市又被疯狂吐槽,MEGA 新车市场表现也远远不及预期。
原本 24 年的销量目标是 80 万,结果最后调整为了 50 万。最后 24 年理想汽车销量是达到了 50 万的目标,同比增长了 33.1%,但是与之前年初定的 80 万目标少了 30万。
因此,今年理想汽车的年终奖肯定是没有去年超额年终奖的了。
根据脉脉网的爆料,今年理想汽车年终奖基本是 14 薪左右,也就是 2 个月年终奖,还真给之前留言的读者说对, 拿不到 16 薪资。
关键是有些员工加入理想汽车的时候,是以 16 薪进来的,现在拿到是 14 薪,有一定的落差感,感觉之前谈薪谈了个寂寞。
如果之前是以 16 薪谈的,那肯定是亏了的,因为相比 14 薪的,base 会低一些的,那最后既然是 14 薪,那还不如按 14 薪谈总包。
所以啊,咱们谈薪的时候,最好争取 base 高点的方案,年终数得等到年底才能知道,而且能不能拿满还另说,不可预知的事情太多,不如每个月拿到手的钱多一些更好。
好了,接下来聊聊理想汽车的面经,之前已经分析过理想汽车的 Java 面经,这次我们看点不一样的。
来看看理想汽车 C++ 岗的面经,是一面的面经,拷打了近 50 分钟,从 C++11 新特性、智能指针、迭代器、STL原理、Linux 内存、HTTPS、排序算法等底层原理进行的盘问。
你们觉得难度如何?
理想汽车(C++一面)
C++11 新特性了解哪些内容?
类型推导与简化语法
特性名称 | 描述 | 示例 |
---|---|---|
auto 关键字 |
自动推导变量类型,简化复杂类型声明(如迭代器)。 | auto x = 42; → int x |
decltype |
推导表达式类型,保留 const 和引用属性,适用于模板编程。 |
int i=1; decltype(i) j = i; → int j |
右值引用与移动语义
右值引用(&& ) |
区分左值/右值,支持资源高效转移(如临时对象)。 | std::vector<int> v2 = std::move(v1); |
---|---|---|
移动构造函数/赋值运算符 | 减少深拷贝开销,通过资源转移提升性能。 | 类中定义 ClassName(ClassName&& other) noexcept; |
完美转发(std::forward ) |
保持参数原始类型,避免多次拷贝。 | 模板中使用 std::forward<T>(arg) |
智能指针
std::unique_ptr |
独占所有权,不可复制但可移动,替代 auto_ptr 。 |
std::unique_ptr<int> ptr = std::make_unique<int>(10); |
---|---|---|
std::shared_ptr |
共享所有权,引用计数管理资源,线程安全。 | std::shared_ptr<int> ptr = std::make_shared<int>(10); |
std::weak_ptr |
解决 shared_ptr 循环引用问题。 |
std::weak_ptr<int> w_ptr = s_ptr; |
函数与模板增强
Lambda 表达式 | 匿名函数,支持捕获外部变量,简化回调和算法。 | std::sort(vec.begin(), vec.end(), [](int a, int b) { return a < b; }); |
---|---|---|
变长参数模板 | 支持任意数量/类型的模板参数,用于元编程和容器设计。 | template<typename... Args> void func(Args... args); |
constexpr 常量表达式 |
编译时求值,优化性能,允许函数在编译期执行。 | constexpr int factorial(int n) { return n <=1 ? 1 : n*factorial(n-1); } |
并发编程支持
std::thread |
原生多线程支持,结合互斥锁和原子操作实现同步。 | std::thread t(func); t.join(); |
---|---|---|
std::async 和 std::future |
简化异步任务管理,获取异步操作结果。 | auto future = std::async(func); int result = future.get(); |
智能指针介绍一下?
std::shared_ptr:是一种引用计数智能指针,允许多个shared_ptr对象共享同一个对象,并通过引用计数来管理内存的释放,当最后一个shared_ptr指向对象析构时,会释放对象所占用的内存。不适合处理循环引用的情况,可能导致内存泄漏。
std::unique_ptr:是一种独占式智能指针,确保只有一个指针可以指向一个对象,不能进行复制,但可以移动它的所有权。
std::weak_ptr:是用于解决std::shared_ptr可能导致的循环引用问题的智能指针。它允许引用shared_ptr所管理的对象,但不会增加引用计数,不会影响对象的生命周期,避免循环引用导致的内存泄漏。
shared_ptr的作用是什么?
在传统的 C++ 编程中,使用 new
操作符分配的内存需要手动使用 delete
操作符释放,若忘记释放或者在异常情况下无法执行 delete
操作,就会造成内存泄漏。
std::shared_ptr
可以自动处理内存的释放,当不再有 std::shared_ptr
指向该对象时,对象的内存会被自动释放。
std::shared_ptr
支持多个 std::shared_ptr
实例共享同一个对象的所有权。它通过引用计数机制来实现这一点,每个 std::shared_ptr
都会维护一个引用计数,记录有多少个 std::shared_ptr
共享同一个对象。当引用计数变为 0 时,对象的内存会被释放。
#include <iostream>
#include <memory>
class MyClass {
public:
MyClass() { std::cout << "MyClass constructor" << std::endl; }
~MyClass() { std::cout << "MyClass destructor" << std::endl; }
};
int main() {
std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>();
std::shared_ptr<MyClass> ptr2 = ptr1; // 共享所有权
std::cout << "Use count: " << ptr1.use_count() << std::endl; // 输出 2
ptr2.reset(); // 释放 ptr2 的所有权
std::cout << "Use count: " << ptr1.use_count() << std::endl; // 输出 1
// 当 ptr1 离开作用域时,MyClass 对象的内存会被释放
return0;
}
在这个例子中,ptr1
和 ptr2
共享同一个 MyClass
对象的所有权,引用计数变为 2。当调用 ptr2.reset()
时,ptr2
释放了对对象的所有权,引用计数减为 1。当 ptr1
离开作用域时,引用计数变为 0,对象的内存被释放。
weak_ptr的作用是什么?如何和shared_ptr结合使用?
std::weak_ptr
主要用于辅助 std::shared_ptr
进行内存管理,解决 std::shared_ptr
可能存在的循环引用问题,同时还可以用于观察 std::shared_ptr
所管理对象的生命周期。
当两个或多个 std::shared_ptr
相互引用形成循环时,会导致引用计数永远不会降为 0,从而造成内存泄漏。std::weak_ptr
不会增加所指向对象的引用计数,因此可以打破这种循环引用。
#include <iostream>
#include <memory>
class B;
class A {
public:
std::shared_ptr<B> b_ptr;
~A() { std::cout << "A destructor" << std::endl; }
};
class B {
public:
std::weak_ptr<A> a_ptr; // 使用 std::weak_ptr 打破循环引用
~B() { std::cout << "B destructor" << std::endl; }
};
int main() {
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->b_ptr = b;
b->a_ptr = a;
return0;
}
在上述代码中,如果 B
类中的 a_ptr
也使用 std::shared_ptr
,就会形成循环引用,导致 A
和 B
对象的内存无法释放。使用 std::weak_ptr
后,b->a_ptr
不会增加 A
对象的引用计数,当 main
函数结束时,a
和 b
的引用计数降为 0,A
和 B
对象的内存会被正确释放。
另外,std::weak_ptr
可以用于观察 std::shared_ptr
所管理对象的生命周期。通过 std::weak_ptr
的 expired()
方法可以检查所指向的对象是否已经被释放。
#include <iostream>
#include <memory>
int main() {
std::shared_ptr<int> shared = std::make_shared<int>(42);
std::weak_ptr<int> weak = shared;
if (!weak.expired()) {
std::cout << "Object is still alive." << std::endl;
}
shared.reset();
if (weak.expired()) {
std::cout << "Object has been destroyed." << std::endl;
}
return0;
}
在这个例子中,weak
观察 shared
所管理的对象。当 shared
释放对象后,weak.expired()
返回 true
,表示对象已经被销毁。
lambda表达式的原理是什么?
从本质上讲,Lambda 表达式是编译器自动生成的一个匿名的函数对象(也称为仿函数)。当我们编写一个 Lambda 表达式时,编译器会创建一个未命名的类,这个类重载了函数调用运算符 operator()
。
#include <iostream>
int main() {
auto lambda = [](int a, int b) { return a + b; };
int result = lambda(3, 4);
std::cout << "Result: " << result << std::endl;
return 0;
}
编译器会将上述 Lambda 表达式转换为类似下面的代码:
#include <iostream>
// 编译器生成的未命名类
class __lambda_4_13 {
public:
__lambda_4_13() = default;
inline int operator()(int a, int b) const {
return a + b;
}
};
int main() {
__lambda_4_13 lambda;
int result = lambda(3, 4);
std::cout << "Result: " << result << std::endl;
return0;
}
stl迭代器的失效情况你知道哪些?
序列式容器(
vector
、deque
),迭代器失效的情况
当在 vector
中插入元素时,如果插入操作导致容器的内存重新分配(即插入后容器的容量不足,需要重新分配更大的内存空间),那么所有指向 vector
的迭代器、指针和引用都会失效。因为重新分配内存后,元素会被移动到新的内存位置,例子代码:
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec = {1, 2, 3};
auto it = vec.begin();
vec.push_back(4); // 可能导致内存重新分配
// 此时 it 可能已经失效
// std::cout << *it << std::endl; // 未定义行为
return 0;
}
当在 vector
中删除元素时,指向被删除元素的迭代器、指针和引用会失效,并且指向删除位置之后的元素的迭代器、指针和引用也会失效。代码如下:
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec = {1, 2, 3};
auto it = vec.begin() + 1;
vec.erase(vec.begin()); // 删除第一个元素
// 此时 it 失效
// std::cout << *it << std::endl; // 未定义行为
return 0;
}
在 deque
的中间插入元素时,所有迭代器、指针和引用都会失效,还有在 deque
的头部或尾部插入元素时,指向元素的迭代器、指针和引用不会失效,但如果插入操作导致内存重新分配,那么迭代器可能会失效。
#include <iostream>
#include <deque>
int main() {
std::deque<int> deq = {1, 2, 3};
auto it = deq.begin() + 1;
deq.insert(deq.begin() + 1, 4); // 在中间插入元素
// 此时 it 失效
// std::cout << *it << std::endl; // 未定义行为
return 0;
}
删除 deque
中间的元素时,所有迭代器、指针和引用都会失效,还有删除 deque
头部或尾部的元素时,指向被删除元素的迭代器、指针和引用会失效。
#include <iostream>
#include <deque>
int main() {
std::deque<int> deq = {1, 2, 3};
auto it = deq.begin() + 1;
deq.erase(deq.begin() + 1); // 删除中间元素
// 此时 it 失效
// std::cout << *it << std::endl; // 未定义行为
return 0;
}
关联式容器(
set
、map
),迭代器失效的情况
set
在插入元素不会使任何迭代器、指针和引用失效,因为关联式容器使用红黑树等平衡二叉搜索树实现,插入操作只是在树中添加新节点,不会影响其他节点的内存位置。
不过指向被删除元素的迭代器、指针和引用会失效,其他迭代器、指针和引用不会失效。
#include <iostream>
#include <set>
int main() {
std::set<int> s = {1, 2, 3};
auto it = s.begin();
auto it_to_delete = s.find(2);
s.erase(it_to_delete);
// 此时 it_to_delete 失效
// std::cout << *it_to_delete << std::endl; // 未定义行为
std::cout << *it << std::endl; // 正常输出
return 0;
}
map
在插入元素不会使任何迭代器、指针和引用失效,原因与 set
。但是指向被删除元素的迭代器、指针和引用会失效,其他迭代器、指针和引用不会失效。
#include <iostream>
#include <map>
int main() {
std::map<int, int> m = {{1, 10}, {2, 20}, {3, 30}};
auto it = m.begin();
auto it_to_delete = m.find(2);
m.erase(it_to_delete);
// 此时 it_to_delete 失效
// std::cout << it_to_delete->second << std::endl; // 未定义行为
std::cout << it->second << std::endl; // 正常输出
return 0;
}
链表式容器(
list
),迭代器失效
在 list
插入元素不会使任何迭代器、指针和引用失效,因为链表的插入操作只是修改节点的指针,不会影响其他节点的内存位置。但是,指向被删除元素的迭代器、指针和引用会失效,其他迭代器、指针和引用不会失效。
#include <iostream>
#include <list>
int main() {
std::list<int> lst = {1, 2, 3};
auto it = lst.begin();
auto it_to_delete = ++lst.begin();
lst.erase(it_to_delete);
// 此时 it_to_delete 失效
// std::cout << *it_to_delete << std::endl; // 未定义行为
std::cout << *it << std::endl; // 正常输出
return 0;
}
unordered_map的底层结构是什么?
底层结构基于哈希表实现。
std::unordered_map
内部维护了一个哈希桶数组,数组中的每个元素称为一个哈希桶。每个哈希桶可以存储一个或多个键值对。当多个键通过哈希函数计算得到相同的索引时,就会发生哈希冲突。
std::unordered_map
通常使用链地址法来解决哈希冲突。在链地址法中,每个哈希桶是一个链表,当发生哈希冲突时,新的键值对会被插入到对应的链表或容器中。
linux程序崩溃怎么定位问题?
如果开启了core dump文件的转存,那么程序崩溃之后,就会生成 core dump 文件,这里面会有程序运行时的堆栈信息,然后我们可以用 gdp 工具去分析 core dump 文件。
gdb myapp core
在 GDB 中,使用 bt
(backtrace)命令可以查看程序崩溃时的调用栈:
(gdb) bt
如果程序崩溃问题可以重现,页可以使用 GDB 直接调试程序。在编译程序时,使用 -g
选项添加调试信息。例如:
gcc -g -o myapp myapp.c
使用 GDB 启动程序并运行,当程序崩溃时,GDB 会停在崩溃的位置。
gdb myapp
(gdb) run
程序崩溃后,使用 bt
命令查看调用栈,找出问题所在的函数和代码行。
有些程序会在崩溃时输出错误信息到标准错误输出(stderr),那么这时候直接去看错误日志就行了。
cat error.log
linux的内存是如何组织的?
操作系统设计了虚拟内存,每个进程都有自己的独立的虚拟内存,我们所写的程序不会直接与物理内打交道。
有了虚拟内存之后,它带来了这些好处:
- 第一,虚拟内存可以使得进程对运行内存超过物理内存大小,因为程序运行符合局部性原理,CPU 访问内存会有很明显的重复访问的倾向性,对于那些没有被经常使用到的内存,我们可以把它换出到物理内存之外,比如硬盘上的 swap 区域。第二,由于每个进程都有自己的页表,所以每个进程的虚拟内存空间就是相互独立的。进程也没有办法访问其他进程的页表,所以这些页表是私有的,这就解决了多进程之间地址冲突的问题。第三,页表里的页表项中除了物理地址之外,还有一些标记属性的比特,比如控制一个页的读写权限,标记该页是否存在等。在内存访问方面,操作系统提供了更好的安全性。
Linux 是通过对内存分页的方式来管理内存,分页是把整个虚拟和物理内存空间切成一段段固定尺寸的大小。这样一个连续并且尺寸固定的内存空间,我们叫页(Page)。在 Linux 下,每一页的大小为 4KB。
虚拟地址与物理地址之间通过页表来映射,如下图:
页表是存储在内存里的,内存管理单元 (MMU)就做将虚拟内存地址转换成物理地址的工作。
而当进程访问的虚拟地址在页表中查不到时,系统会产生一个缺页异常,进入系统内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行。
你说到缺页中断,能介绍一下缺页中断么?
缺页中断是指在虚拟内存管理中,当程序试图访问的页面(Page)不在物理内存中,而在磁盘等外存上时,操作系统会产生一个中断,将该页面从外存调入物理内存,以满足程序的访问需求。这种中断就被称为缺页中断。
对于缺页中断这种情况,linux操作系统是如何减少页面的换入换出的呢,有哪些方式?
按需加载机制:仅在进程实际访问内存时才分配物理页框,而非预先分配所有所需内存。例如,mmap 和 malloc
仅建立虚拟地址映射,物理页的分配延迟到首次访问时触发缺页中断,这样可以减少不必要的物理内存占用,避免过早的页面换入换出。写时复制:在进程 fork
时,父子进程共享只读页面;当任一进程尝试写入时,触发缺页中断并复制新物理页,避免不必要的内存复制。内存预取:文件映射(如 mmap
)时,内核在缺页处理中预加载相邻文件内容到内存,减少后续缺页次数。内存分配策略:优先使用空闲页框,避免频繁触发页面回收。例如,通过伙伴系统和 Slab 分配器高效管理内存碎片。
你了解数字证书么?
数字证书采用公钥加密技术,它将实体的身份信息与公钥绑定在一起,并由权威的证书颁发机构(CA)进行数字签名。
具体来说,当一个实体申请数字证书时,它会生成一对密钥,即公钥和私钥,然后将公钥以及自身的身份信息提交给 CA。CA 会对这些信息进行验证,验证通过后,CA 使用自己的私钥对这些信息进行签名,生成数字证书。在数字证书中,包含了证书持有者的身份信息、公钥、证书颁发机构的信息、有效期等内容。
数字证书的作用是接收方可以通过验证数字证书来确认发送方的身份是否真实可靠。例如,在访问银行网站时,浏览器会验证银行网站的数字证书,以确保用户连接的是真实的银行网站,而不是假冒的钓鱼网站。
https的握手过程说一下?
传统的 TLS 握手基本都是使用 RSA 算法来实现密钥交换的,在将 TLS 证书部署服务端时,证书文件其实就是服务端的公钥,会在 TLS 握手阶段传递给客户端,而服务端的私钥则一直留在服务端,一定要确保私钥不能被窃取。
在 RSA 密钥协商算法中,客户端会生成随机密钥,并使用服务端的公钥加密后再传给服务端。根据非对称加密算法,公钥加密的消息仅能通过私钥解密,这样服务端解密后,双方就得到了相同的密钥,再用它加密应用消息。
我用 Wireshark 工具抓了用 RSA 密钥交换的 TLS 握手过程,你可以从下面看到,一共经历了四次握手:
TLS 第一次握手
首先,由客户端向服务器发起加密通信请求,也就是 ClientHello 请求。在这一步,客户端主要向服务器发送以下信息:
- (1)客户端支持的 TLS 协议版本,如 TLS 1.2 版本。(2)客户端生产的随机数(Client Random),后面用于生成「会话秘钥」条件之一。(3)客户端支持的密码套件列表,如 RSA 加密算法。
TLS 第二次握手
服务器收到客户端请求后,向客户端发出响应,也就是 SeverHello。服务器回应的内容有如下内容:
- (1)确认 TLS 协议版本,如果浏览器不支持,则关闭加密通信。(2)服务器生产的随机数(Server Random),也是后面用于生产「会话秘钥」条件之一。(3)确认的密码套件列表,如 RSA 加密算法。(4)服务器的数字证书。
TLS 第三次握手
客户端收到服务器的回应之后,首先通过浏览器或者操作系统中的 CA 公钥,确认服务器的数字证书的真实性。
如果证书没有问题,客户端会从数字证书中取出服务器的公钥,然后使用它加密报文,向服务器发送如下信息:
- (1)一个随机数(pre-master key)。该随机数会被服务器公钥加密。(2)加密通信算法改变通知,表示随后的信息都将用「会话秘钥」加密通信。(3)客户端握手结束通知,表示客户端的握手阶段已经结束。这一项同时把之前所有内容的发生的数据做个摘要,用来供服务端校验。
上面第一项的随机数是整个握手阶段的第三个随机数,会发给服务端,所以这个随机数客户端和服务端都是一样的。
服务器和客户端有了这三个随机数(Client Random、Server Random、pre-master key),接着就用双方协商的加密算法,各自生成本次通信的「会话秘钥」。
TLS 第四次握手
服务器收到客户端的第三个随机数(pre-master key)之后,通过协商的加密算法,计算出本次通信的「会话秘钥」。
然后,向客户端发送最后的信息:
- (1)加密通信算法改变通知,表示随后的信息都将用「会话秘钥」加密通信。(2)服务器握手结束通知,表示服务器的握手阶段已经结束。这一项同时把之前所有内容的发生的数据做个摘要,用来供客户端校验。
至此,整个 TLS 的握手阶段全部结束。接下来,客户端与服务器进入加密通信,就完全是使用普通的 HTTP 协议,只不过用「会话秘钥」加密内容。
对于https中的加密算法,rsa和ecc椭圆加密,这两者有什么区别么?
- RSA 加密算法:国际标准算法,应用较早的算法之一,普遍性更强,相比 ECC 算法的适用范围更广,兼容性更好,一般采用2048位的加密长度,服务端性能消耗较高。ECC 加密算法:椭圆加密算法,新一代算法趋势主流,一般采用256位加密长度(相当于 RSA 3072 位加密强度)更安全,抗攻击性更强,相比 RSA 算法加密速度快,效率更高,服务器资源消耗更低。
排序算法介绍一下?
- 冒泡排序:通过相邻元素的比较和交换,每次将最大(或最小)的元素逐步“冒泡”到最后(或最前)。时间复杂度:最好情况下O(n),最坏情况下O(n^2),平均情况下O(n^2)。
- 空间复杂度:O(1)。插入排序:将待排序元素逐个插入到已排序序列的合适位置,形成有序序列。时间复杂度:最好情况下O(n),最坏情况下O(n^2),平均情况下O(n^2),空间复杂度:O(1)。选择排序(Selection Sort):通过不断选择未排序部分的最小(或最大)元素,并将其放置在已排序部分的末尾(或开头)。
- 时间复杂度:最好情况下O(n^2),最坏情况下O(n^2),平均情况下O(n^2),空间复杂度:O(1)。快速排序(Quick Sort):通过选择一个基准元素,将数组划分为两个子数组,使得左子数组的元素都小于(或等于)基准元素,右子数组的元素都大于(或等于)基准元素,然后对子数组进行递归排序。时间复杂度:最好情况下O(nlogn),最坏情况下O(n^2),平均情况下O(nlogn),空间复杂度:最好情况下O(logn),最坏情况下O(n)。归并排序(Merge Sort):将数组不断分割为更小的子数组,然后将子数组进行合并,合并过程中进行排序。时间复杂度:最好情况下O(nlogn),最坏情况下O(nlogn),平均情况下O(nlogn)。空间复杂度:O(n)。堆排序(Heap Sort):通过将待排序元素构建成一个最大堆(或最小堆),然后将堆顶元素与末尾元素交换,再重新调整堆,重复该过程直到排序完成。时间复杂度:最好情况下O(nlogn),最坏情况下O(nlogn),平均情况下O(nlogn)。空间复杂度:O(1)。
大文件排序应该怎么办?
可以采用外部排序的方式。外部排序主要分为两个阶段:
1. 分段排序(生成初始归并段):将大文件分割成多个较小的片段,这些片段的大小要能够适应内存的限制,接着把每个片段加载到内存中,使用内部排序算法(如快速排序、归并排序等)对其进行排序,最后将排序好的片段写回到磁盘中,这些片段就被称为初始归并段。2. 归并操作:将多个初始归并段逐步合并成一个有序的大文件。在归并过程中,每次从各个归并段中读取一部分数据到内存中,比较这些数据并将最小(或最大)的元素依次写入输出文件,同时不断从归并段中补充数据,直到所有归并段的数据都被处理完毕。