快手C++开发一面凉经
公众号:阿Q技术站
C++基础
1、unique_ptr的用法和实现?
std::unique_ptr
是 C++11 引入的智能指针,用于管理动态分配的对象。它提供了独占(unique)所有权,确保在其生命周期结束时自动释放所管理的对象。
用法
#include <memory>
int main() {
// 创建一个 unique_ptr,指向一个动态分配的整数
std::unique_ptr<int> ptr(new int(42));
// 使用箭头运算符访问所管理的对象
*ptr = 10;
// unique_ptr 在作用域结束时会自动释放所管理的对象
return 0;
}
实现
std::unique_ptr
的实现通常基于模板类 std::unique_ptr
,其关键特性是通过一个模板参数指定要管理的对象类型,并使用模板的参数推导功能确定其大小。
template <typename T>
class unique_ptr {
public:
// 构造函数
explicit unique_ptr(T* ptr = nullptr) : ptr_(ptr) {}
// 析构函数
~unique_ptr() {
delete ptr_;
}
// 禁用拷贝构造函数和赋值运算符
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
// 移动构造函数
unique_ptr(unique_ptr&& other) noexcept {
ptr_ = other.ptr_;
other.ptr_ = nullptr;
}
// 移动赋值运算符
unique_ptr& operator=(unique_ptr&& other) noexcept {
if (this != &other) {
delete ptr_;
ptr_ = other.ptr_;
other.ptr_ = nullptr;
}
return *this;
}
// 获取指针
T* get() const {
return ptr_;
}
// 重载箭头运算符
T* operator->() const {
return ptr_;
}
// 解引用操作符
T& operator*() const {
return *ptr_;
}
// 显示释放资源
void reset(T* ptr = nullptr) {
delete ptr_;
ptr_ = ptr;
}
// 显示释放资源并放弃所有权
T* release() {
T* temp = ptr_;
ptr_ = nullptr;
return temp;
}
private:
T* ptr_;
};
2、父类指针找到子类虚函数的寻址过程?
当使用父类指针调用子类的虚函数时,会发生动态绑定(dynamic binding),这意味着在运行时确定要调用的函数版本。
-
编译阶段:编译器会根据父类指针的静态类型(即指针声明的类型)来确定可用的成员函数。因此,如果父类中有一个虚函数被子类重写(override),那么编译器会为该虚函数生成一个虚函数表(vtable),并将其添加到父类的类型信息中。
-
运行阶段:当使用父类指针调用虚函数时,实际上会通过虚函数表来确定要调用的函数版本。这是一个两步过程:
- 根据对象的动态类型(即实际指向的对象类型)找到其虚函数表。
- 在虚函数表中查找要调用的函数的地址。
如果找到了相应的虚函数地址,则会跳转到该地址并执行子类中的虚函数。否则,如果未找到匹配的虚函数地址,则会根据虚函数表中的虚函数默认实现(如果有的话)或者引发未定义行为。
3、虚函数表指针存储到哪里?
虚函数表指针(vptr)存储在对象的内存布局中,通常位于对象的开头或结尾,具体取决于编译器和平台的实现。
考虑以下基类 Base
和子类 Derived
的情况:
class Base {
public:
virtual void foo() {}
};
class Derived : public Base {
public:
virtual void foo() override {}
virtual void bar() {}
};
对于一个 Base
类型的指针 Base* ptr = new Derived;
,在内存中的布局可能如下:
[虚函数表指针 | Base 类的数据成员]
在这个布局中,虚函数表指针是一个指向虚函数表的指针,虚函数表中存储着虚函数的地址。虚函数表指针指向的虚函数表包含了 Base
类和 Derived
类的虚函数地址,在这个示例中,虚函数表可能是这样的:
[&Base::foo | &Derived::bar]
当调用 ptr->foo()
时,实际上会通过 ptr
指针找到对象的虚函数表指针,然后根据虚函数表中 foo
函数的地址调用相应的函数。这样,即使 ptr
是 Base*
类型,但由于动态绑定的机制,会调用 Derived
类中的 foo
函数。
4、const修饰一个函数对函数有什么约束?
- 不能修改成员变量:常量成员函数不能修改类的非静态成员变量。如果试图在常量成员函数中修改非静态成员变量,编译器会报错。
- 不能调用非常量成员函数:常量成员函数只能调用其他常量成员函数。这是因为在调用非常量成员函数时,可能会修改对象的状态,而常量成员函数保证不会修改对象的状态,因此不能调用可能会修改状态的函数。
- 可以访问类的所有成员:尽管常量成员函数不能修改非静态成员变量,但它们可以访问类的所有成员,包括非常量成员变量和静态成员变量。
- 对于非常量对象和常量对象有不同行为:对于一个非常量对象,可以调用常量成员函数和非常量成员函数;但对于一个常量对象,只能调用常量成员函数。
常量成员函数的声明通常形式为在函数声明或定义的末尾加上 const
关键字,例如:
class MyClass {
public:
void normalFunction(); // 非常量成员函数
void constFunction() const; // 常量成员函数
};
在类外定义常量成员函数时,也要记得加上 const
关键字:
void MyClass::constFunction() const {
// 实现代码
}
使用 const
修饰成员函数有助于提高代码的可读性和安全性,同时可以明确表明该函数不会修改对象的状态。
5、如何实现对象的函数返回this指针的share_ptr?
要实现一个函数,该函数返回一个指向对象的 shared_ptr
,可以使用 enable_shared_from_this
类。这个类是为了解决在对象的成员函数中返回指向自身的 shared_ptr
时可能导致的资源管理问题。
给个例子:
#include <memory>
class MyClass : public std::enable_shared_from_this<MyClass> {
public:
std::shared_ptr<MyClass> getSharedPtr() {
return shared_from_this();
}
};
int main() {
std::shared_ptr<MyClass> ptr(new MyClass);
std::shared_ptr<MyClass> ptr2 = ptr->getSharedPtr();
// 使用 ptr2 指向的对象...
return 0;
}
MyClass
继承了 enable_shared_from_this
类,并在成员函数 getSharedPtr
中调用了 shared_from_this
函数,该函数返回一个指向对象的 shared_ptr
。
需要注意的是,在使用 shared_from_this
之前,需要确保对象已经被动态分配,并且至少有一个 shared_ptr
对象管理了该对象的生命周期。否则,如果尝试在对象还没有被 shared_ptr
管理时调用 shared_from_this
,将会导致未定义的行为。
6、C++的右值引用的用法?
右值引用是 C++11 引入的特性,用于表示对临时对象(右值)的引用。右值引用可以绑定到临时对象,也可以通过 std::move
转换绑定到右值上,通常用于实现移动语义和完美转发。
基本语法
右值引用的语法使用双 &&,例如 T&&
,其中 T 是类型。右值引用可以绑定到右值,但不能绑定到左值。
int&& rref = 42; // 右值引用绑定到临时整数
移动语义
右值引用最常用于实现移动语义,通过将资源的所有权从一个对象转移到另一个对象,避免不必要的资源拷贝。
#include <iostream>
#include <string>
class MyString {
public:
MyString(std::string str) : data_(std::move(str)) {}
// 移动构造函数
MyString(MyString&& other) noexcept : data_(std::move(other.data_)) {
other.data_.clear();
}
const std::string& getData() const { return data_; }
private:
std::string data_;
};
int main() {
std::string str = "Hello";
MyString obj1(str); // 调用构造函数
MyString obj2(std::move(obj1)); // 调用移动构造函数
std::cout << "obj1: " << obj1.getData() << std::endl; // obj1 的数据被移走
std::cout << "obj2: " << obj2.getData() << std::endl;
return 0;
}
完美转发
右值引用还可以与模板一起使用,实现完美转发,即在函数模板中保留参数的值类别(左值或右值)。
#include <iostream>
#include <utility>
void process(int& i) {
std::cout << "Lvalue reference: " << i << std::endl;
}
void process(int&& i) {
std::cout << "Rvalue reference: " << i << std::endl;
}
template <typename T>
void forward(T&& i) {
process(std::forward<T>(i));
}
int main() {
int x = 42;
forward(x); // 传递左值
forward(42); // 传递右值
return 0;
}
操作系统
1、为什么要用虚拟内存?
它将计算机的物理内存(RAM)抽象成为一个更大的、连续的地址空间,称为虚拟地址空间。
- 内存隔离:虚拟内存可以将每个进程的地址空间隔离开来,使得每个进程都认为自己拥有整个地址空间,从而保护进程不受其他进程的影响。
- 更大的地址空间:虚拟内存使得每个进程可以拥有比实际物理内存更大的地址空间。当物理内存不足时,操作系统可以使用虚拟内存技术将部分数据存储在硬盘上,从而扩展了可用的地址空间。
- 内存映射:虚拟内存可以将磁盘上的文件映射到内存中,使得文件的访问看起来像是在访问内存一样,从而简化了文件的读写操作。
- 内存保护:虚拟内存可以为每个页面设置权限,例如只读、读写、执行等,以保护系统不受恶意程序的破坏。
- 内存共享:虚拟内存可以将同一个物理页面映射到多个进程的虚拟地址空间中,实现内存共享,节省内存资源。
- 内存回收:虚拟内存可以通过页面置换算法将长时间不使用的页面从内存中换出到磁盘,以释放内存空间给其他需要的页面。
2、缺页执行的流程?
缺页(Page Fault)指的是当程序访问的内存页面不在物理内存中时发生的情况。缺页执行的流程如下:
- 缺页异常:当程序访问一个不在物理内存中的页面时,CPU 会触发一个缺页异常,将控制权转交给操作系统的内存管理模块。
- 处理缺页异常:操作系统的内存管理模块首先会检查引起缺页的原因,可能有以下几种情况:
- 页面不在物理内存中:如果所需的页面不在物理内存中,操作系统需要将该页面加载到内存中。
- 页面访问权限错误:如果程序试图访问的页面权限不正确(例如只读页面尝试写入),操作系统会根据情况进行处理。
- 无效的页面引用:如果程序引用了一个无效的页面,操作系统可能会终止该程序或者向程序发送相应的信号。
- 页面调度:如果所需的页面不在物理内存中,操作系统会选择一个物理页面作为牺牲页,将其写入到磁盘上(如果需要),然后将所需的页面从磁盘读取到物理内存中。
- 更新页表:操作系统更新页表,将新加载的页面映射到程序的虚拟地址空间中,并标记该页面已经在物理内存中。
- 恢复程序执行:一旦缺页处理完成,操作系统会将控制权返回给程序,并重新执行引起缺页的指令。由于页面已经在物理内存中,这次内存访问应该会成功。
- 程序继续执行:程序继续执行,并可能会再次发生缺页异常,这取决于程序访问的内存页面是否在物理内存中。
3、缺页中断是软中断还是硬中断?
缺页中断属于硬中断。硬中断是由计算机硬件生成的中断,用于向 CPU 报告发生的异常或需要处理的事件。在缺页中断发生时,CPU 会暂停当前正在执行的程序,将控制权转移到操作系统的内存管理模块,以便处理缺页异常。
具体来说,当程序访问的页面不在物理内存中时,CPU 会检测到这一情况并触发缺页中断。这个过程是由硬件中的内存管理单元(Memory Management Unit,MMU)负责的。MMU 检测到缺页后,会向 CPU 发送一个中断信号,使得 CPU 中断当前正在执行的程序,并跳转到操作系统的缺页处理程序。
操作系统的缺页处理程序会负责从磁盘中加载缺失的页面到物理内存中,并更新页表等数据结构。处理完成后,操作系统会重新启动被中断的程序,使其继续执行。
4、介绍一下硬中断和软中断?
硬中断(Hardware Interrupt):
硬中断是由计算机硬件生成的中断,用于向 CPU 报告发生的异常或需要处理的事件。硬中断通常由外部设备或其他硬件组件触发,例如定时器到期、IO设备就绪、内存访问错误等。当硬件触发了中断时,CPU 会暂停当前正在执行的程序,保存当前的执行现场(程序计数器、寄存器状态等),然后转移到相应的中断处理程序中执行。硬中断的处理是由硬件和操作系统协作完成的,通常包括中断响应、中断嵌套、中断屏蔽等机制。
软中断(Software Interrupt):
软中断是由软件(通常是应用程序或操作系统内核)主动发起的中断。软中断通常用于请求系统服务、执行特权操作或处理异常情况。软中断是通过系统调用(System Call)实现的,应用程序可以通过调用特定的系统调用接口向操作系统发起软中断请求。例如,应用程序可以通过系统调用来请求文件操作、进程创建、内存分配等功能,这些请求会触发操作系统内核中相应的软中断处理程序来处理。
5、进程间通信?
进程间通信(Inter-Process Communication,IPC)是指不同进程之间进行数据交换和信息传递的机制。在操作系统中,进程间通信是实现多任务和并发的重要手段,可以让不同的进程协作完成复杂的任务。
进程间通信的方式:
- 管道(Pipe):管道是一种半双工的通信方式,适用于具有亲缘关系的父子进程或兄弟进程之间的通信。管道可以是匿名管道(只能在父子进程或兄弟进程之间使用)或命名管道(可以在不同进程之间使用)。
- 消息队列(Message Queue):消息队列是一种可以在不同进程之间传递消息的通信方式,每个消息都有一个特定的类型和优先级。
- 信号量(Semaphore):信号量是一种用于进程间同步和互斥的机制,可以控制对共享资源的访问。
- 共享内存(Shared Memory):共享内存是一种允许多个进程访问同一块物理内存的通信方式,可以实现高效的数据共享。
- 套接字(Socket):套接字是一种在网络编程中常用的通信方式,可以在不同主机之间的进程间进行通信。
- 信号(Signal):信号是一种异步通信方式,用于通知进程发生了特定事件,例如程序异常或用户输入。
- 文件(File):进程可以通过读写文件来进行通信,但这种方式效率较低,通常用于简单的数据交换。
6、哪个进程间通信最快,为什么?
共享内存通常被认为是进程间通信中最快的一种方式。这是因为共享内存允许多个进程直接访问同一块物理内存,避免了数据的复制和传输开销,从而提高了通信的效率。
原因:
- 无需数据复制:共享内存中的数据可以直接在不同进程之间共享,而不需要进行数据的复制操作。相比其他通信方式,避免了复制数据的开销,节省了时间和资源。
- 直接访问内存:进程可以直接访问共享内存中的数据,不需要通过内核进行数据传输,减少了上下文切换和系统调用的次数,提高了通信的效率。
- 高效的同步机制:共享内存通常与信号量等同步机制结合使用,保证多个进程对共享数据的访问是安全和有序的。
- 适用于大量数据共享:共享内存适用于需要大量数据共享的场景,可以提供高效的数据传输和访问方式。
尽管共享内存通常被认为是最快的进程间通信方式,但也存在一些缺点。例如,共享内存需要进程之间进行同步操作,以避免数据访问冲突;另外,共享内存对操作系统和硬件的支持要求较高,不同操作系统和硬件平台的实现方式可能有所不同。因此,在选择进程间通信方式时,需要根据具体的应用场景和需求综合考虑。
7、两个进程共享内存怎么同步?
在两个进程共享内存时,为了确保数据的一致性和避免竞态条件(Race Condition),需要使用同步机制进行进程间的同步。
常用的同步机制包括:
- 信号量(Semaphore):信号量是一种计数器,用于控制多个进程对共享资源的访问。可以使用信号量来保护共享内存区域,确保一次只有一个进程可以访问。
- 互斥锁(Mutex):互斥锁是一种二进制信号量,只能被一个进程持有。可以使用互斥锁来保护共享内存中的关键数据结构,确保一次只有一个进程可以修改。
- 条件变量(Condition Variable):条件变量是一种用于线程间通信的同步机制,可以用于进程间的同步。可以使用条件变量来实现进程间的等待和通知机制。
- 读写锁(Read-Write Lock):读写锁允许多个进程同时读取共享数据,但只允许一个进程写入共享数据。可以使用读写锁来提高共享内存的读取效率。
- 文件锁(File Lock):可以使用文件锁来保护共享文件或共享内存区域,确保只有一个进程可以访问。
在使用这些同步机制时,需要注意以下几点:
- 确保同步机制的正确使用:正确地使用同步机制可以避免死锁(Deadlock)和饥饿(Starvation)等问题。
- 合理设计共享内存的访问方式:尽量减少对共享内存的访问,避免频繁的读写操作。
- 使用适当的同步粒度:根据实际情况选择合适的同步粒度,避免过度同步或不足同步。
网络编程
1、read函数返回值对应的情况?
- 大于 0:
read
函数返回大于 0 的值表示成功接收到数据,并返回接收到的字节数。这时,可以通过读取到的数据进行后续处理。 - 等于 0:
read
函数返回等于 0 表示对端已经关闭了连接(End-of-File),即没有更多的数据可以读取。这时,应用程序可以根据需要处理连接关闭的情况。 - 小于 0:
read
函数返回小于 0 的值表示出现了错误,具体的错误代码保存在errno
变量中,可以通过perror
函数或strerror
函数打印出错误信息。常见的错误代码有:EAGAIN
或EWOULDBLOCK
:表示当前没有数据可读,可以稍后再试。EINTR
:表示读操作被信号中断。- 其他的错误代码表示发生了其他类型的错误,例如连接重置、连接超时等。
2、阻塞read和非阻塞read返回值<0的意义一不一样?
- 阻塞 read:
- 如果没有数据可读,阻塞 read 会一直等待,直到有数据可读或者出错才返回。
- 如果返回值小于 0,通常表示出现了错误。可能的错误包括连接被重置(Connection reset)、连接关闭(Connection closed)等。
- 非阻塞 read:
- 如果没有数据可读,非阻塞 read 会立即返回,返回值为 -1,并将
errno
设置为EAGAIN
或EWOULDBLOCK
(表示当前没有数据可读)。 - 如果返回值大于 0,则表示成功读取到了数据,返回值为读取到的字节数。
- 如果返回值等于 0,表示对端已经关闭了连接(End-of-File)。
- 如果没有数据可读,非阻塞 read 会立即返回,返回值为 -1,并将
3、TCP四次挥手?
- 第一次挥手(FIN-1):客户端发送一个 FIN 报文段,表示数据发送完毕,请求关闭连接。客户端进入 FIN_WAIT_1 状态,等待服务端的确认。
- 第二次挥手(ACK):服务端收到 FIN 报文段后,发送一个 ACK 报文段作为应答,表示已经收到了客户端的关闭请求。服务端进入 CLOSE_WAIT 状态,等待自己的数据发送完毕。
- 第三次挥手(FIN-2):服务端数据发送完毕后,发送一个 FIN 报文段给客户端,请求关闭连接。服务端进入 LAST_ACK 状态。
- 第四次挥手(ACK):客户端收到 FIN 报文段后,发送一个 ACK 报文段作为应答,表示已经收到了服务端的关闭请求。客户端进入 TIME_WAIT 状态,等待可能出现的延迟数据。
4、在客户端收到服务端ack后服务端还能发数据吗?
在 TCP 的四次挥手过程中,服务端在发送了最后的 ACK 确认报文后,表示已经接收到了客户端的关闭请求,并进入了 CLOSE_WAIT 状态。在 CLOSE_WAIT 状态中,服务端仍然可以继续向客户端发送数据,直到服务端自己也完成了数据的发送,此时服务端会发送一个 FIN 报文段给客户端,进入最后的 LAST_ACK 状态,等待客户端的最后一个 ACK 确认。
因此,服务端在收到客户端的 ACK 确认报文后,仍然可以继续向客户端发送数据,直到服务端自己也准备好关闭连接为止。TCP 协议保证了在连接关闭的过程中双方可以继续传输数据,直到双方都确认关闭连接为止,确保了数据的可靠传输。
5、客户端发完最后一个ack包能不能立即关闭?
在 TCP 的四次挥手过程中,客户端发送完最后一个 ACK 报文段后,通常不会立即关闭连接,而是进入 TIME_WAIT 状态。TIME_WAIT 状态的作用是等待可能出现的延迟数据报文,以确保对方接收到最后一个 ACK 报文段。TIME_WAIT 状态的持续时间通常为 2 倍的最大报文段生存时间(2MSL,Maximum Segment Lifetime)。
为什么客户端需要等待一段时间后才能关闭连接呢?这是为了处理网络中可能存在的延迟报文。如果客户端立即关闭连接,而对方仍有延迟的数据报文未处理,则可能导致对方无法正确处理这些延迟的数据报文。因此,客户端在发送完最后一个 ACK 后,进入 TIME_WAIT 状态,等待一段时间,以确保对方有足够的时间接收可能的延迟数据报文。
6、time_wait状态为什么要等待60s,这个是怎么确定的?
TIME_WAIT 状态等待时间为 2 倍的最大报文段生存时间(2MSL,Maximum Segment Lifetime),而不是固定的 60 秒。MSL 是指数据报文在网络中能存活的最长时间,一般情况下 MSL 在几分钟到几十分钟之间。
TIME_WAIT 状态的等待时间为 2MSL 的主要目的是确保在网络中存在的所有报文段都被丢弃,从而避免这些报文段对后续建立连接的影响。具体来说,TIME_WAIT 状态等待时间的确定有以下几个方面的考虑:
- 确保连接关闭的正常终止:等待一段时间可以确保对方接收到最后一个 ACK 报文段,并且对方发送的可能延迟的数据报文都能够被丢弃,从而保证连接的正常终止。
- 处理网络中的重复报文:在 TIME_WAIT 状态等待期间,如果对方重传了最后一个 FIN 报文段,客户端可以根据 TCP 头部的序列号来识别这是一个重复的报文段,并进行相应的处理,避免对方错误地认为连接还未关闭。
- 防止新连接的混淆:等待一段时间可以确保旧连接的所有数据报文都在网络中消失,从而避免新连接与旧连接之间的混淆。
数据库
1、mysql的第一范式,第二范式,第三范式的区别?
- 第一范式(1NF):
- 第一范式要求关系数据库中的每个属性都是原子的,即不可再分。换句话说,每个属性的值不能是多个值的集合或列表,必须是单一的值。
- 例如,一个包含学生信息的表,学生姓名字段应该是一个完整的姓名,而不是分成姓和名两个字段。
- 第二范式(2NF):
- 第二范式要求表中的非主键属性完全依赖于候选键(Candidate Key)。换句话说,每个非主键属性必须完全依赖于表中的每个候选键,而不是部分依赖。
- 例如,一个包含订单信息的表,如果订单号和产品号是联合主键,那么订单数量就应该完全依赖于订单号和产品号的组合,而不是只依赖于订单号或产品号。
- 第三范式(3NF):
- 第三范式要求表中的非主键属性不传递依赖于主键。换句话说,非主键属性之间不应该存在传递依赖关系。
- 例如,一个包含学生信息和教师信息的表,如果存在一个字段是教师的办公室号,这个字段依赖于教师名字,而教师名字又依赖于学生名字,那么就存在传递依赖关系,不符合第三范式。
2、mysql两张表的三种join的区别?
- INNER JOIN:
- INNER JOIN 返回两个表中匹配的行,即只返回两个表中连接键相等的行。
- 如果左表中的行在右表中没有匹配的行,或者右表中的行在左表中没有匹配的行,则这些行不会包含在结果集中。
- INNER JOIN 是最常用的 JOIN 类型,用于获取两个表中共有的数据。
- LEFT JOIN:
- LEFT JOIN 返回左表中的所有行,以及右表中匹配的行。如果右表中没有匹配的行,则返回 NULL 值。
- 换句话说,LEFT JOIN 会返回左表中的所有行,即使右表中没有匹配的行。
- LEFT JOIN 通常用于获取左表中的所有数据,同时获取右表中与之匹配的数据(如果有的话)。
- RIGHT JOIN:
- RIGHT JOIN 与 LEFT JOIN 类似,但是它返回右表中的所有行,以及左表中匹配的行。如果左表中没有匹配的行,则返回 NULL 值。
- 换句话说,RIGHT JOIN 会返回右表中的所有行,即使左表中没有匹配的行。
- RIGHT JOIN 在实际使用中比较少见,通常可以使用 LEFT JOIN 来达到相同的效果。
3、Nginx和memchaced区别?
- Nginx:Nginx 是一个高性能的 Web 服务器和反向代理服务器,以其高性能、高并发能力和低内存消耗而闻名。Nginx 通常用于静态资源的服务、负载均衡和反向代理等场景。它的配置简洁灵活,可以通过配置文件实现各种高级功能,如反向代理、负载均衡、HTTPS 支持等。Nginx 也支持动态模块,可以通过安装扩展模块来增加更多功能,例如处理 PHP、Python 等动态内容。
- Memcached:Memcached 是一个高性能的分布式内存对象缓存系统,用于缓存数据和减轻数据库负载。Memcached 的主要优点是快速、简单和可扩展。它通过在内存中缓存数据来加速数据访问,适用于需要频繁读取相同数据的应用场景,如网站的页面缓存、数据库查询结果缓存等。Memcached 采用键值对存储数据,支持多种编程语言,并且可以通过在不同服务器上部署实现数据的分布式存储和负载均衡。
手撕
力扣 32最长有效括号
问题描述
给你一个只包含 '('
和 ')'
的字符串,找出最长有效(格式正确且连续)括号子串的长度。
示例 1:
输入:s = "(()"
输出:2
解释:最长有效括号子串是 "()"
示例 2:
输入:s = ")()())"
输出:4
解释:最长有效括号子串是 "()()"
示例 3:
输入:s = ""
输出:0
思路
- 创建一个栈,用于存储左括号 ‘(’ 的下标。
- 初始化一个变量
maxLen
用于记录最长有效括号子串的长度,初始值为 0。 - 遍历字符串,对于每个字符:
- 如果是左括号 ‘(’,将其下标入栈。
- 如果是右括号 ‘)’,判断栈是否为空:
- 如果栈为空,则将当前右括号的下标入栈,作为一个分界点。
- 如果栈不为空,则出栈一个左括号的下标,计算当前有效括号子串的长度:当前右括号的下标减去栈顶元素的值加一(加一是因为下标从 0 开始)。
- 更新
maxLen
的值为当前有效括号子串的长度和maxLen
中的较大值。
- 遍历结束后,
maxLen
的值即为最长有效括号子串的长度。
参考代码
#include <iostream>
#include <stack>
#include <string>
using namespace std;
int longestValidParentheses(string s) {
stack<int> stk; // 用于存储左括号的下标
stk.push(-1); // 初始化栈,用于处理边界情况
int maxLen = 0; // 最长有效括号子串的长度
for (int i = 0; i < s.length(); ++i) {
if (s[i] == '(') {
stk.push(i); // 左括号入栈
} else { // 右括号
stk.pop(); // 出栈一个左括号的下标
if (stk.empty()) {
// 栈为空,表示当前右括号为分界点
stk.push(i); // 将当前右括号的下标入栈
} else {
// 计算当前有效括号子串的长度
maxLen = max(maxLen, i - stk.top());
}
}
}
return maxLen;
}
int main() {
string s;
cout << "请输入只包含 '(' 和 ')' 的字符串:" << endl;
cin >> s;
int result = longestValidParentheses(s);
cout << "最长有效括号子串的长度为:" << result << endl;
return 0;
}