1 请简述 std::stack 在 C++ STL 中的基本功能和使用场景

std::stack在C++ STL(标准模板库)中是一个容器适配器,专门用于实现后进先出(LIFO,Last-In-First-Out)的数据结构。其基本功能和使用场景如下:

基本功能:

  • push(element):向栈顶添加元素。
  • pop():移除栈顶元素。如果栈为空,则此操作可能会导致未定义行为。
  • top():返回栈顶元素的引用,但不移除它。如果栈为空,则此操作可能会导致未定义行为。
  • empty():检查栈是否为空。如果栈为空,则返回true;否则返回false。
  • size():返回栈中元素的数量。

此外,std::stack还提供了其他一些成员函数,例如emplace()(C++11 起可用,用于在栈顶构造并添加元素)和 swap()(用于交换两个栈的内容)。

使用场景:

std::stack适用于那些需要后进先出访问顺序的场景。这种数据结构在许多算法和实际问题中都非常有用。以下是一些具体的使用场景示例:

  • 函数调用栈:在计算机科学中,函数调用栈就是一个典型的后进先出结构。每次函数调用时,相关信息(如局部变量、返回地址等)被推入栈中;函数返回时,这些信息从栈中弹出。
  • 撤销操作:在编辑文本或图形时,撤销功能通常使用栈来实现。每次执行一个操作(如键入一个字符或移动一个对象),该操作都被推入撤销栈中。当用户选择撤销时,栈顶的操作被弹出并应用其逆操作。
  • 括号匹配:在解析编程语言的源代码时,通常需要检查括号是否正确匹配。可以使用栈来跟踪尚未匹配的括号,每次遇到一个开括号就将其推入栈中,遇到一个闭括号则从栈顶弹出一个元素并检查是否匹配。
  • 深度优先搜索(DFS):在图形算法中,DFS经常使用栈来保存待访问的节点。每次访问一个节点时,将其推入栈中;当当前节点没有未访问的邻居时,从栈中弹出一个节点并继续访问其邻居。

这些只是 std::stack 的一些常见使用场景,实际上它可以在任何需要后进先出访问顺序的场合中发挥作用。

2 std::stack 和 std::vector 在功能上有哪些异同?

std::stack和std::vector在C++标准模板库(STL)中都是常用的数据结构,但它们在功能和使用上存在明显的异同。

功能相同点:

  • 存储元素:两者都用于在内存中存储一系列元素。
  • 动态大小:std::vector 和 std::stack 都可以动态地调整其大小以适应更多或更少的元素。

功能不同点:

(1)访问方式:

  • std::vector提供随机访问功能,即可以通过索引直接访问其任意位置的元素。这使得std::vector非常适合需要频繁访问和修改元素的应用场景。
  • std::stack则只允许在栈顶进行元素的插入(push)和删除(pop)操作,且只能访问栈顶元素(通过top()方法)。这种限制使得std::stack只能按照后进先出(LIFO)的顺序处理元素。

(2)用途和设计目标:

  • std::vector 是一个通用且灵活的容器,可以用于各种需要动态数组的场景。它可以用来存储任意类型的对象,并且支持高效的插入、删除、访问和遍历操作。
  • std::stack 则是一个专为栈数据结构设计的容器适配器。它主要用于那些需要后进先出顺序处理元素的场景,如函数调用堆栈模拟、表达式求值的计算过程等。
    底层实现:
  • std::vector 通常使用连续的内存空间来存储元素,这使得它可以高效地进行元素的访问和移动。
  • std::stack 的底层实现则依赖于其他容器,如 std::deque 或 std::vector。默认情况下,std::stack 使用 std::deque 作为底层容器,但也可以选择其他容器来优化特定场景下的性能。

总结来说,std::stack 和 std::vector 在功能上各有侧重。std::vector 提供了灵活的随机访问和动态数组功能,而 std::stack 则专注于实现后进先出的栈数据结构。在选择使用哪个数据结构时,应根据具体的应用场景和需求来决定。

3 如何使用 std::stack 实现一个先进先出(FIFO)的数据结构?

以下是一个使用两个 std::stack 尝试模拟 FIFO 的例子:

#include <iostream>  
#include <stack>  

class MyFIFO {
private:
	std::stack<int> inputStack;
	std::stack<int> outputStack;

public:
	void push(int value) {
		inputStack.push(value);
	}

	int pop() {
		if (outputStack.empty()) {
			while (!inputStack.empty()) {
				outputStack.push(inputStack.top());
				inputStack.pop();
			}
		}
		if (outputStack.empty()) {
			throw std::out_of_range("FIFO is empty");
		}
		int value = outputStack.top();
		outputStack.pop();
		return value;
	}

	bool empty() const {
		return inputStack.empty() && outputStack.empty();
	}
};

int main() 
{
	MyFIFO fifo;
	fifo.push(1);
	fifo.push(2);
	fifo.push(3);
	std::cout << fifo.pop() << std::endl;  // 输出 1  
	std::cout << fifo.pop() << std::endl;  // 输出 2  
	return 0;
}

这个 MyFIFO 类使用两个栈:inputStack 用于接收新的元素,而 outputStack 用于在调用 pop() 时提供元素。当 outputStack 为空时,pop() 会将 inputStack 中的所有元素弹出并压入 outputStack,从而反转元素的顺序。这样,最后一个进入 inputStack 的元素会第一个从 outputStack 中弹出,实现了一种类似 FIFO 的行为。但请注意,这种方法在 pop() 操作上的时间复杂度是 O(n),其中 n 是栈中的元素数量,因此效率非常低。

4 如果在 std::stack 为空的情况下调用 pop 或 top,会发生什么?

在 std::stack 为空的情况下调用 pop 或 top 会导致未定义行为(Undefined Behavior)。这意味着程序可能会崩溃、产生错误的结果或表现出其他不可预测的行为。

具体来说:

  • pop() 函数用于移除栈顶元素。如果栈是空的,调用 pop() 将会导致程序错误,因为没有元素可以被移除。
  • top() 函数用于返回栈顶元素的引用。如果栈是空的,调用 top() 也会引发未定义行为,因为没有元素可供引用。

为了避免这种情况,应该在调用 pop 或 top 之前检查栈是否为空,这可以通过调用 empty() 函数来实现。例如:

std::stack<int> myStack;  
  
// ... 在某处可能向栈中添加元素 ...  
  
if (!myStack.empty()) {  
    int topElement = myStack.top(); // 安全地获取栈顶元素  
    myStack.pop(); // 安全地移除栈顶元素  
} else {  
    // 处理栈为空的情况,例如抛出异常或返回错误代码  
    throw std::out_of_range("Stack is empty, cannot pop or top.");  
}

在实践中,为了避免未定义行为,应该始终确保在调用 pop 或 top 之前栈不为空。这是一种良好的编程习惯,可以帮助编写更加健壮和可靠的代码。

5 能否通过迭代的方式遍历 std::stack 中的所有元素?为什么?

不能通过迭代的方式直接遍历 std::stack 中的所有元素。这是因为 std::stack 是一个容器适配器,其设计初衷是提供后进先出(LIFO)的数据结构,而不是像 std::vector 或 std::deque 那样支持随机访问或迭代。

std::stack 的接口只提供了有限的几个操作,如 push、pop、top、empty 和 size,并没有提供迭代器(iterator)或类似机制来遍历栈中的元素。这是为了保持栈的简单性和一致性,并强调其后进先出的特性。

如果需要遍历栈中的元素,一种可能的方法是先将栈中的元素逐个弹出并存储到一个其他支持迭代的容器中(如 std::vector),然后再遍历这个容器。但请注意,这样做会改变栈的状态(即清空栈),因此如果之后还需要使用原始的栈,这种方法就不适用了。

下面是一个示例代码,展示了如何将 std::stack 中的元素转移到一个 std::vector 中,并遍历这个 vector:

#include <iostream>  
#include <stack>  
#include <vector>  

int main() 
{
	std::stack<int> myStack;
	// 假设向栈中添加了一些元素  
	myStack.push(1);
	myStack.push(2);
	myStack.push(3);

	std::vector<int> myVector;
	while (!myStack.empty()) {
		myVector.push_back(myStack.top());
		myStack.pop();
	}

	// 现在可以遍历 vector 了  
	for (const auto& element : myVector) {
		std::cout << element << " ";
	}
	std::cout << std::endl;

	return 0;
}

上面代码的输出为:

3 2 1

这个输出展示了栈中元素的原始顺序(从栈底到栈顶),但由于栈是后进先出的数据结构,因此元素是以相反的顺序弹出的。

03-24 23:32