华为OD—C++面经
一面(60min)
1、自我介绍
2、做题——旋转数组
思路:
- 首先,处理右移距离
m大于数组长度n的情况,这是通过将m取模n来实现的,以确保右移的距离在合理范围内。 - 接下来,创建一个新的整数数组
result,它的大小与输入数组a相同,用于存放操作后的结果。 - 接下来,使用两个循环来将输入数组
a中的元素按照右移的规则复制到result数组中:- 第一个循环负责将数组
a的后面m个元素复制到result数组的末尾。这是通过将a数组的后面m个元素复制到result数组的前面来实现的。 - 第二个循环负责将数组
a的前面(n-m)个元素复制到result数组的前面。
- 第一个循环负责将数组
- 最后,函数返回
result数组作为结果。
#include <iostream>
#include <vector>
class Solution {
public:
std::vector<int> solve(int n, int m, std::vector<int>& a) {
// 首先,处理 m 大于数组长度的情况
m = m % n;
// 创建一个新数组用于存放结果
std::vector<int> result(n);
// 将前 M 个元素复制到结果数组的末尾
for (int i = 0; i < m; i++) {
result[i] = a[n - m + i];
}
// 将后面的 (N-M) 个元素复制到结果数组的前面
for (int i = 0; i < n - m; i++) {
result[m + i] = a[i];
}
return result;
}
};
int main() {
int n, m;
std::cin >> n >> m; // 输入数组长度和右移距离
std::vector<int> a(n);
for (int i = 0; i < n; i++) {
std::cin >> a[i]; // 输入数组元素
}
Solution solution;
std::vector<int> result = solution.solve(n, m, a);
// 输出结果数组
for (int i = 0; i < n; i++) {
std::cout << result[i];
if (i < n - 1) {
std::cout << " ";
}
}
std::cout << std::endl;
return 0;
}输入示例:
6 2 // 数组长度为5,右移距离为2
1 2 3 4 5 6 // 数组元素结果输出:
3、C++的面相对象主要体现在哪些方面?
- 封装:封装是面向对象编程的核心概念之一。它允许将数据和方法组合成一个单一的单元,即类。通过类,数据(成员变量)可以被隐藏在类的内部,只有类的方法(成员函数)可以访问和操作这些数据。这提供了数据的安全性和保护,防止了未经授权的直接访问。
- 继承:继承允许创建一个新的类,可以继承现有类的属性和方法。子类(派生类)可以继承父类(基类)的特性,然后根据需要添加新的属性和方法,或者覆盖父类的方法。继承促进了代码的重用和层次化设计。
- 多态:多态性允许不同类型的对象对同一方法做出不同的响应。这可以通过函数重载和虚函数来实现。多态性使得代码更加灵活,能够以一种通用的方式处理不同类型的对象。
- 类和对象:C++ 中,面向对象编程的基本单元是类(Class)和对象(Object)。类是一个抽象的模板,描述了数据和方法的结构,而对象是类的实例,代表了具体的数据和方法的调用。
- 构造函数和析构函数:C++ 中的构造函数和析构函数用于对象的初始化和清理工作。构造函数在对象创建时自动调用,而析构函数在对象销毁时自动调用。这使得资源管理更加方便,例如内存分配和释放。
- 访问控制:C++ 中,类可以定义不同的访问控制级别,包括公共(public)、私有(private)、受保护(protected)。这些级别决定了哪些部分的类可以被外部访问,哪些部分是私有的,只能在类内部访问。
- 运算符重载:C++ 允许用户重载运算符,使得用户自定义的类可以像内置类型一样使用运算符。这增强了自定义类的可用性和可读性。
- 模板(Templates):C++ 支持泛型编程,通过模板可以创建通用的数据结构和算法,而不必为每种类型都编写特定的代码。这提高了代码的复用性和灵活性。
4、对多态的理解,怎么体现出多态的?
- 多态是指同一个方法名或操作符可以有多个不同的实现方式,具体取决于对象的类型或类的结构。这意味着相同的函数名可以在不同的类中具有不同的实现,或者相同的运算符可以根据操作数的类型执行不同的操作。
- 在面向对象编程中,多态性通常通过继承和虚函数(或抽象类)来实现。子类可以继承父类的虚函数,并根据需要提供自己的实现。当通过基类指针或引用调用虚函数时,实际执行的是对象的子类版本,而不是基类版本。
- 多态的体现:
- 函数重载:在同一个类中,可以定义多个同名但参数列表不同的函数。编译器根据函数参数的类型和数量来选择调用哪个函数。这被称为编译时多态(静态多态)。
- 虚函数:通过在基类中声明虚函数,并在派生类中覆盖(重写)虚函数,可以实现运行时多态。当通过基类指针或引用调用虚函数时,将根据实际对象的类型来决定调用哪个子类的实现,这被称为运行时多态。
- 抽象类和接口:抽象类可以包含纯虚函数,子类必须提供这些函数的具体实现。接口是一种特殊的抽象类,只包含纯虚函数,用于定义类之间的契约。这使得不同类可以实现相同的接口,从而实现多态性。
- 多态的优点:
- 代码复用性:多态允许不同类共享相同的接口和行为,从而提高了代码的复用性。
- 可扩展性:通过添加新的子类或实现新的接口,可以轻松扩展程序的功能,而无需修改现有代码。
- 可维护性:多态使代码更容易理解和维护,因为它隐藏了对象的具体类型,减少了条件分支语句。
5、析构函数、构造函数还有复制构造函数什么时候会被调用?
析构函数:析构函数在对象的生命周期结束时被调用,用于清理对象占用的资源(如释放内存、关闭文件等)。当对象超出作用域、被
delete运算符销毁或程序结束时,析构函数会被调用。Cclass MyClass { public: ~MyClass() { // 析构函数被调用 } }; int main() { { MyClass obj; // 对象超出作用域,析构函数被调用 } MyClass* ptr = new MyClass(); delete ptr; // delete 运算符销毁对象,析构函数被调用 return 0; // 程序结束,析构函数被调用 }构造函数:
构造函数在创建对象时被调用,用于初始化对象的成员变量。当通过
new运算符创建对象或在栈上声明对象时,构造函数都会被调用。如果有继承关系,派生类的构造函数会首先调用基类的构造函数,然后再执行自己的构造逻辑。
- C
class MyClass { public: MyClass() { // 构造函数被调用 } }; int main() { MyClass obj; // 构造函数被调用 MyClass* ptr = new MyClass(); // 构造函数被调用 return 0; }
复制构造函数:
复制构造函数在创建一个对象并将其初始化为另一个对象的副本时被调用。这包括对象的传递、返回和初始化。
当需要将一个对象从一种类型转换为另一种类型时,复制构造函数可能会被调用。
- C
class MyClass { public: MyClass(const MyClass& other) { // 复制构造函数被调用 } }; int main() { MyClass obj1; MyClass obj2 = obj1; // 复制构造函数被调用 return 0; }
6、指针和引用的区别?
定义和声明:
- 指针:指针是一个变量,存储的是另一个变量的内存地址。它必须经过定义和初始化才能使用,可以指向不同的变量。
- 引用:引用是一个别名,它没有自己的内存地址。引用必须在定义时初始化,并且一旦初始化,不能再引用其他变量。
语法:
- 指针:通过使用
*运算符来声明指针和访问指针指向的值,同时使用&运算符来获取变量的地址。 - 引用:引用的声明和使用与普通变量类似,不需要
*运算符来访问值,因为引用本身就是变量的别名。
可空性:
- 指针:指针可以具有空值(nullptr),表示它不指向任何有效的内存地址。
- 引用:引用在定义时必须初始化,并且一旦初始化后,不能改变其引用的对象,因此不具备可空性。
使用场景:
- 指针:通常用于需要动态分配内存、数组操作、迭代等情况,或者当需要多态性(通过基类指针访问派生类对象)时。
- 引用:用于函数参数传递、函数返回值、避免对象拷贝的情况,以及需要直观的别名来简化代码。
效率:
- 指针:指针的操作可能需要更多的代码,因为需要解引用操作(
*ptr)来访问值。 - 引用:引用在访问值时更加直接,因此通常更高效。
7、静态成员和静态函数跟普通成员和普通函数有什么区别?
- 静态成员vs普通成员:
静态成员:
- 静态成员属于类而不是类的实例,它在类的所有实例之间共享相同的值。
- 可以通过类名来访问静态成员,不需要创建类的实例。
- 通常用于存储类的共享数据,例如计数器、配置信息等。
- 静态成员的内存分配在编译时进行,不需要实例化类。
普通成员:
- 普通成员属于类的实例,每个实例都有自己的副本。
- 必须创建类的实例才能访问和使用普通成员。
- 用于存储特定于实例的数据,通常是对象的属性。
- 静态函数vs普通函数:
静态函数:
- 静态函数属于类而不是类的实例,它不能访问非静态成员(包括普通成员和非静态函数)。
- 可以通过类名来调用静态函数,不需要创建类的实例。
- 通常用于执行与类相关的操作,而不需要实例的状态。
普通函数:
- 普通函数不属于类,它可以访问类的实例成员和状态。
- 必须通过类的实例来调用普通函数。
- 用于执行与对象状态相关的操作。
8、c++ 为什么会发生内存泄漏,怎么避免?
内存泄漏是指在程序运行过程中,动态分配的内存没有被正确释放,导致程序持续占用内存,最终可能导致系统的内存耗尽。
- 如果使用
new或malloc分配了内存,但没有使用delete或free进行释放,就会导致内存泄漏。 - 当一个指向动态分配内存的指针被重新赋值或者超出了其作用域,就无法再释放内存,导致泄漏。
- 在使用智能指针时,如果存在循环引用(两个或多个对象相互持有对方的智能指针),就会导致对象无法被销毁,从而导致内存泄漏。
避免内存泄漏的方法:
- 使用智能指针: C++11引入的
std::shared_ptr和std::unique_ptr可以自动管理内存的释放,确保在不再需要时正确释放内存。 - 在使用
new或malloc分配内存时,确保在不再需要内存时调用delete或free释放内存。 - 避免丢失指针: 确保在不再需要的时候将指针置为
nullptr,或者使用智能指针来自动管理。 - 在使用智能指针时,避免形成循环引用,或者使用
std::weak_ptr来打破循环引用。 - 借助C++的对象生命周期管理,确保在对象的析构函数中释放资源(如内存),从而避免手动管理资源时出现错误。
9、什么样的内存在堆上,什么样的内存在栈上?
堆上的内存:
- 堆上的内存需要手动分配和释放,程序员负责管理其生命周期。通常使用
new(C++)或malloc(C)来分配堆内存,并使用delete(C++)或free(C)来释放堆内存。 - 堆上的内存可以根据需要动态分配,其大小通常比栈大得多,受系统总内存的限制。
- 堆上的内存访问速度较慢,因为它是散列的,访问数据需要通过指针进行间接访问。
- 堆上的数据的生命周期可以跨越函数调用和程序的不同部分,因此可以在程序的整个执行过程中保持有效。
栈上的内存:
- 栈上的内存是自动分配和释放的,其生命周期由程序的函数调用栈控制。当一个函数被调用时,其局部变量被分配到栈上,当函数返回时,这些局部变量会被自动释放。
- 栈的大小通常是有限的,它受到操作系统或编译器的限制。这意味着栈上的内存用于存储相对较小的数据,如基本数据类型和小型对象。
- 栈上的内存访问速度很快,因为它是线性的,数据的存储和检索效率高。
- 栈上的数据的生命周期通常与函数调用的生命周期相匹配,因此它们是短暂的。一旦函数返回,栈上的数据将被销毁。
10、常见的数据结构有哪些,有哪些特点?
数组(Array):
- 特点:连续的内存存储,通过索引快速访问元素。
- 适用场景:适用于元素数量已知且不经常插入或删除的情况。
链表(Linked List):
- 特点:非连续的内存存储,通过指针链接节点。
- 适用场景:适用于频繁插入或删除元素的情况,或者在内存不连续的情况下。
栈(Stack):
- 特点:后进先出(LIFO)的数据结构,只能在栈顶进行插入和删除操作。
- 适用场景:适用于需要遵循后进先出规则的情况,如函数调用栈。
队列(Queue):
- 特点:先进先出(FIFO)的数据结构,只能在队首和队尾进行插入和删除操作。
- 适用场景:适用于需要遵循先进先出规则的情况,如任务调度。
树(Tree):
- 特点:层次结构,包括二叉树、二叉搜索树、平衡二叉树等。
- 适用场景:用于表示层次关系的数据,如文件系统、组织结构等。
图(Graph):
- 特点:由节点和边构成的数据结构,有向图和无向图。
- 适用场景:用于表示复杂的关系和网络,如社交网络、路由算法等。
哈希表(Hash Table):
- 特点:使用哈希函数将键映射到数组索引,实现快速的查找、插入和删除。
- 适用场景:适用于需要高效查找的情况,如字典、缓存等。
堆(Heap):
- 特点:用于实现优先级队列,包括最小堆和最大堆。
- 适用场景:适用于需要快速查找最小或最大元素的情况,如堆排序、调度算法等。
集合(Set)和映射(Map):
- 特点:集合用于存储唯一元素,映射用于键值对的存储。
- 适用场景:用于去重、快速查找键值对的情况。
栈和队列的变种(如双端队列、优先级队列等):
特点:根据需要实现不同的操作,如双端队列支持队首和队尾的插入和删除操作,优先级队列支持按优先级排序。
11、散列表的解决冲突的方案有哪些?
- 链地址法(Separate Chaining):
- 在每个散列桶(哈希表中的每个槽位)上维护一个链表或其他数据结构,用于存储冲突的元素。
- 当发生冲突时,将新元素插入到对应桶的链表中。
- 优点:简单,容易实现;适用于元素分布不均匀的情况。
- 缺点:链表过长时可能会影响查找性能,需要额外的内存空间。
- 开放寻址法(Open Addressing):
- 在散列表中的每个槽位中存储元素,当发生冲突时,根据某种规则来寻找下一个可用的槽位。
- 常见的开放寻址法包括线性探测、二次探测、双重散列等。
- 优点:不需要额外的内存空间,适用于小规模散列表。
- 缺点:容易出现聚集现象,影响性能;不适用于动态散列表。
- 线性探测(Linear Probing):
- 当发生冲突时,线性探测依次查找下一个可用的槽位,直到找到一个空槽位或达到散列表的末尾。
- 插入操作:尝试将元素插入到哈希桶,如果槽位已被占用,就线性查找下一个空槽位。
- 查找操作:根据哈希值查找槽位,如果不是目标元素,就线性查找下一个槽位,直到找到目标元素或遇到空槽位。
- 删除操作:标记被删除元素,而不是直接删除,以防止查找时出错。
- 二次探测(Quadratic Probing):
- 当发生冲突时,二次探测以二次方的步长查找下一个可用的槽位。
- 插入、查找和删除操作与线性探测类似,但步长不同。
- 双重散列(Double Hashing):
- 当发生冲突时,使用第二个哈希函数来计算下一个槽位。
- 插入操作:根据第一个哈希函数找到初始位置,然后使用第二个哈希函数来计算下一个槽位,直到找到空槽位。
- 查找和删除操作也使用第二个哈希函数计算槽位。
12、讲一下常用的排序算法和他们的时间复杂度?
这个表再次分享给大家,记得保存。
| 排序算法 | 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 稳定性 |
|---|---|---|---|---|---|
| 冒泡排序 | O(n^2) | O(n) | O(n^2) | O(1) | 稳定 |
| 快速排序 | O(nlogn) | O(nlogn) | O(n^2) | O(logn) | 不稳定 |
| 选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 |
| 插入排序 | O(n^2) | O(n) | O(n^2) | O(1) | 稳定 |
| 希尔排序 | O(nlogn) | O(log^2n) | O(log^2n) | O(1) | 不稳定 |
| 归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 稳定 |
| 堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不稳定 |
| 计数排序 | O(n+k) | O(n+k) | O(n+k) | O(k) | 稳定 |
| 桶排序 | O(n+k) | O(n+k) | O(n^2) | O(n+k) | 稳定 |
| 基数排序 | O(nxk) | O(nxk) | O(nxk) | O(n+k) | 稳定 |
13、bfs 一般会用到什么数据结构?
广度优先搜索(BFS)通常需要使用队列(Queue)这种数据结构来辅助实现。队列是一种先进先出(FIFO)的数据结构,非常适合用于层次遍历,广度优先搜索正是一种层次遍历算法。
下边我给大家放一个例子自己可以再理解理解它的应用。
假设我们要在一个图中查找从起始节点到目标节点的最短路径。
#include <iostream>
#include <queue>
#include <vector>
using namespace std;
// 定义图的数据结构,这里使用邻接矩阵表示
class Graph {
public:
Graph(int numNodes) : numNodes(numNodes), adjMatrix(numNodes, vector<int>(numNodes, 0)) {}
void addEdge(int from, int to) {
adjMatrix[from][to] = 1;
adjMatrix[to][from] = 1; // 无向图,需要双向连接
}
vector<int> shortestPath(int start, int end) {
vector<bool> visited(numNodes, false);
vector<int> parent(numNodes, -1);
queue<int> q;
q.push(start);
visited[start] = true;
while (!q.empty()) {
int current = q.front();
q.pop();
if (current == end) {
// 构造最短路径
vector<int> path;
while (current != -1) {
path.insert(path.begin(), current);
current = parent[current];
}
return path;
}
for (int neighbor = 0; neighbor < numNodes; ++neighbor) {
if (adjMatrix[current][neighbor] == 1 && !visited[neighbor]) {
q.push(neighbor);
visited[neighbor] = true;
parent[neighbor] = current;
}
}
}
// 若未找到路径,返回空路径
return vector<int>();
}
private:
int numNodes;
vector<vector<int>> adjMatrix;
};
int main() {
Graph g(6); // 创建一个有6个节点的图
g.addEdge(0, 1);
g.addEdge(0, 2);
g.addEdge(1, 3);
g.addEdge(2, 4);
g.addEdge(3, 5);
int startNode = 0;
int endNode = 5;
vector<int> shortestPath = g.shortestPath(startNode, endNode);
if (!shortestPath.empty()) {
cout << "Shortest path from " << startNode << " to " << endNode << ": ";
for (int node : shortestPath) {
cout << node << " ";
}
cout << endl;
} else {
cout << "No path exists from " << startNode << " to " << endNode << endl;
}
return 0;
}14、了解过哪些设计模式,讲讲比较了解的?
这个最起码要答出工厂模式、单例模式、观察者模式吧
15、进程和线程的区别?
- 定义:
- 进程(Process): 进程是计算机中的一个独立的执行环境,它包含了一个程序的代码、数据、资源、以及一个执行线程。每个进程都有独立的内存空间,相互之间不会直接共享数据,需要通过进程间通信(Inter-Process Communication,IPC)来进行数据交换。
- 线程(Thread): 线程是进程内的执行单元,一个进程可以包含多个线程。线程共享进程的内存空间和资源,它们可以直接访问相同的数据,因此线程之间的通信相对容易。
- 资源占用:
- 进程: 每个进程都有独立的内存空间和系统资源(如文件描述符、网络连接等),因此进程的资源占用相对较高。
- 线程: 线程共享进程的内存空间和资源,因此线程的资源占用相对较低。
- 创建和销毁:
- 进程: 创建和销毁进程相对较重,需要分配和释放独立的资源,通常比线程开销大。
- 线程: 创建和销毁线程相对较轻量,因为它们共享进程的资源,通常比进程开销小。
- 切换代价:
- 进程: 进程切换的代价相对较高,因为需要保存和恢复进程的完整上下文信息,包括内存空间、寄存器状态等。
- 线程: 线程切换的代价相对较低,因为线程共享同一进程的地址空间和资源,切换时只需切换线程的上下文。
- 并发性:
- 进程: 进程是独立的执行单元,不同进程之间的并发性相对较低,它们通常需要通过进程间通信来协调和共享数据。
- 线程: 线程是进程内的执行单元,不同线程之间的并发性相对较高,它们可以直接共享数据,但需要注意线程同步问题,以避免竞争条件和死锁。
- 安全性:
- 进程: 进程之间的数据隔离较好,一个进程崩溃通常不会影响其他进程。
- 线程: 线程之间共享相同的内存空间,因此一个线程的错误可能会影响整个进程的稳定性。
- 平台支持:
- 进程: 进程是操作系统级别的概念,可以跨平台使用。
- 线程: 线程通常依赖于特定的线程库和操作系统支持,跨平台性可能有限。
16、多线程的情况下为什么会发生死锁,列举导致死锁的几种情况?
发生在多个线程相互等待对方释放资源的情况下,导致所有线程都无法继续执行。
- 互斥资源的竞争:
死锁最常见的情况之一是多个线程竞争互斥资源。当多个线程需要访问同一互斥资源(如文件、数据库记录、锁等)时,如果它们同时持有部分资源并等待其他线程释放资源,就可能发生死锁。这种情况通常涉及以下几个要素:
- 互斥性(Mutual Exclusion): 资源一次只能被一个线程占用。
- 持有和等待(Hold and Wait): 线程持有一些资源并等待其他线程的资源。
- 不可剥夺(No Preemption): 资源只能由占用它的线程主动释放,不能被强制剥夺。
- 循环等待(Circular Wait): 多个线程之间形成一个等待环路,每个线程都在等待另一个线程的资源。
- 资源分配不当:
死锁还可以由于资源的分配不当而发生。例如,线程A获得了一些资源,但由于某种原因无法继续执行,而线程B也需要这些资源,但无法获得。这种情况下,线程A和线程B可能都会陷入死锁状态。
- 线程调度问题:
死锁也可能由于线程调度问题而发生。当操作系统不当地调度线程,导致它们永远无法继续执行时,可能会发生死锁。
- 资源不足:
如果系统的资源(如内存、处理器时间等)不足以满足所有线程的需求,也可能导致死锁。例如,如果多个线程都在等待获得更多内存,但系统没有足够的内存可供分配,那么可能会发生死锁。
17、跨进程通信有哪些方式?
- 管道(Pipe):
管道是一种半双工的通信方式,通常用于父子进程之间或兄弟进程之间的通信。管道分为匿名管道(无名管道)和命名管道(FIFO)两种类型。
- 匿名管道: 创建后只能用于具有共同祖先的进程之间的通信。通常通过
pipe系统调用来创建。 - 命名管道(FIFO): 可以用于不相关的进程之间的通信,通过文件系统中的一个特殊文件实现,通常通过
mkfifo系统调用创建。
- 消息队列(Message Queue):
消息队列是一种进程间通信的方式,允许多个进程通过发送和接收消息来进行通信。消息队列通常是系统级的,由操作系统维护。
- 消息队列提供了一种异步通信方式,允许发送者和接收者以独立的速度进行通信。
- 可以通过消息队列发送结构化数据,而不仅仅是字节流。
- 共享内存(Shared Memory):
共享内存是一种高效的IPC方式,允许多个进程共享同一块内存区域。多个进程可以将相同的内存区域映射到它们的地址空间,从而实现快速的数据交换。
- 共享内存通常用于需要高速数据传输的场景,如图像处理或数据库管理系统。
- 需要谨慎管理共享内存,以避免竞争条件和数据一致性问题。
- 信号(Signal):
信号是一种轻量级的IPC方式,用于通知进程发生了某个事件。信号通常用于处理异步事件,如进程终止、中断等。
- 信号可以通过
kill、raise和signal等函数发送和处理。 - 信号可以用于进程间通信,但通常不用于大量数据的传输。
- 套接字(Socket):
套接字是一种网络编程中常见的IPC方式,它允许不同主机上的进程进行通信。套接字通常用于网络通信,但也可以用于本地进程间通信(Unix域套接字)。
- 套接字提供了一种通用的跨网络或跨进程通信方式。
- 可以通过TCP或UDP协议在套接字上传输数据。
- 信号量(Semaphore)和互斥锁(Mutex):
信号量和互斥锁是用于进程间同步的IPC方式。它们可以用于控制多个进程对共享资源的访问。
- 信号量: 用于控制多个进程对有限资源的访问。可以通过
semget、semop等系统调用来操作。 - 互斥锁: 用于确保在任何给定时刻只有一个进程可以访问共享资源。可以通过
pthread_mutex_init、pthread_mutex_lock等函数来操作。
二面(40min)
这个当时面完忘记记录了,大部分问题都忘记了,主要问了我算法与数据结构以及c++基础(30min)+ 做题(10min)
1、自我介绍
2、常见的排序算法有哪些,你觉得哪个算法最快
3、链表和数组的区别
4、c++ 的一些基础知识
5、算法题——最长不重复子序列的长度
使用滑动窗口算法来解决:
#include <iostream>
#include <string>
#include <unordered_map>
using namespace std;
int lengthOfLongestSubstring(string s) {
int n = s.length(); // 输入字符串的长度
int maxLength = 0; // 最长不重复子序列的长度
unordered_map<char, int> charIndex; // 用于记录字符最近出现的位置
int left = 0; // 滑动窗口的左边界
for (int right = 0; right < n; right++) {
char c = s[right];
// 如果字符已经出现过,并且出现的位置在当前窗口内
if (charIndex.find(c) != charIndex.end() && charIndex[c] >= left) {
left = charIndex[c] + 1; // 更新窗口的左边界,使窗口不包含重复字符
}
charIndex[c] = right; // 记录字符最近出现的位置
int currentLength = right - left + 1; // 当前窗口的长度
maxLength = max(maxLength, currentLength); // 更新最大长度
}
return maxLength;
}
int main() {
string s = " "; // 示例输入字符串
cin >> s;
int result = lengthOfLongestSubstring(s); // 调用函数计算最长不重复子序列的长度
cout << "最长不重复子序列的长度为: " << result << endl; // 输出结果
return 0;
}