1. Home
C++ Tutorial

Explore C++ Tutorials: Exploring the World of C++ Programming

Discover comprehensive C++ tutorials designed for beginners and advanced programmers alike. Enhance your coding skills with step-by-step guides and practical examples.

  • 77 Lessons
  • 15 Hours
right-top-arrow
35

Stack in C++: Understanding the LIFO Data Structure

Updated on 30/09/2024414 Views

The stack in C++ is a fundamental data structure with a Last-In, First-Out (LIFO) access pattern. This characteristic makes it well-suited for a wide range of applications, including function call management, expression evaluation, and backtracking algorithms.

The Standard Template Library (STL) in C++ provides a convenient implementation of the stack data structure in the form of the std::stack container adaptor. I will delve into the inner workings of stacks in this tutorial and we will explore their use in various scenarios. We will also discuss how to optimize their performance.

Let’s dive in.

Foundations of Stack in C++: The LIFO Principle

A stack is a linear data structure that adheres to this LIFO rule. We can only access the top element, adding new elements (pushing) or removing existing ones (popping) exclusively from this top position. We can think of it like a spring-loaded tray dispenser where we can only take from the top, and when we add a tray, it goes on top of the others.

I would like to mention that the LIFO principle is a powerful concept that enables a wide range of algorithms and programming techniques.

The Power of Stack in C++

Stacks are behind many computational tasks in C++. They might seem simple, but their LIFO behavior makes them indispensable in various domains:

  • Function call management: Every time you call a function, its information (arguments, return address) gets pushed onto the call stack. When the function completes, its data is popped off, allowing execution to resume seamlessly.
  • Expression evaluation: Stacks are used to parse and evaluate mathematical expressions, especially those using prefix or postfix notation.
  • Backtracking algorithms: Imagine a maze-solving algorithm. A stack in C++ keeps track of the paths taken, allowing the algorithm to "undo" decisions and explore different routes when it hits a dead end.
  • Parsing: Stacks help compilers and interpreters analyze the structure of code and validate syntax.
  • Undo/redo functionality: In text editors or design software, stacks are the backbone of undo/redo features, allowing us to reverse or repeat actions effectively.

std::stack: Ready-Made Stack in C++ with STL

The Standard Template Library (STL) offers a pre-built solution, the std::stack container adaptor when it comes to using stacks. This is a lifesaver because it means that we do not have to write our own stack implementation from scratch. We get all the benefits of a well-designed stack without the extra work.

What is a Container Adaptor?

We can think of container adaptors like a costume change for existing data structures. std::stack takes a regular container (like a vector, deque, or list) and dresses it up to act like a stack. This gives us the core stack functionality (push, pop, top) while letting us choose the underlying storage that best fits our needs. It's like having a wardrobe full of stack outfits for different occasions!

Why std::stack?

The brilliance of std::stack is its adaptability. By default, it uses a std::deque as its underlying container, which offers a nice balance of efficient insertion and removal at both ends. But we are not locked into that choice. We can easily swap it out for a std::vector (if you prefer contiguous storage) or a std::list (if we need frequent insertions/deletions throughout the stack).

This flexibility means we can fine-tune the performance of our stack depending on how we plan to use it. It's like having a Swiss Army knife for stack implementation.

Container Choices: Picking the Right Foundation

The beauty of std::stack is its flexibility. We can choose one of three common container types as the backbone of our stack:

std::vector

Pros:

  • Elements are stored contiguously in memory, leading to fast access.
  • Good for stacks with relatively stable sizes.

Cons:

  • Potentially inefficient for frequent pushes/pops if the stack needs to resize often.
  • Resizing can involve copying all elements to a new memory location.

std::deque (Default)

Pros:

  • Efficient insertion/removal at both the front (top) and back.
  • Good balance of performance for most stack operations.

Cons:

  • Slightly more memory overhead than a vector due to its segmented storage.

std::list (Doubly Linked List)

Pros:

  • Extremely fast insertion/removal at any position, including the top.
  • No need for resizing as the stack grows.

Cons:

  • Slower random access compared to vectors and deques due to non-contiguous storage.
  • More memory overhead per element than vectors or deques.

Choosing the Right Container

Here is how we can choose the right container:

  • Frequent Push/Pop at Top: If we are primarily needing to add and remove from the top of the stack, std::deque (the default) is often a safe choice.
  • Frequent Insertions/Deletions in the Middle: If we need to insert or remove elements anywhere in the stack, std::list is superior.
  • Limited Memory: If memory is very tight, and we are working with simple data types, a std::vector might be slightly more efficient.

How Do We Implement Stack in C++?

Here's how we create std::stack objects with different data types and initialize them with values:

Code:

#include <iostream>

#include <stack>

#include <vector>

int main() {

// Declare and initialize a stack of integers

std::stack<int> intStack;

intStack.push(10);

intStack.push(20);

intStack.push(30);

// Declare and initialize a stack of strings

std::stack<std::string> stringStack;

stringStack.push("apple");

stringStack.push("banana");

stringStack.push("cherry");

// Declare a stack of a custom object type

struct Person {

std::string name;

int age;

};

std::stack<Person> personStack;

// Create some Person objects and push them onto the stack

Person person1 = {"Alice", 30};

Person person2 = {"Bob", 25};

personStack.push(person1);

personStack.push(person2);

std::cout << "Integer stack top: " << intStack.top() << std::endl;

std::cout << "String stack top: " << stringStack.top() << std::endl;

std::cout << "Person stack top name: " << personStack.top().name << std::endl; // Access the name of the top Person object

}

If you wish to learn how to code in C++ like the above stack in C++ program, you can check out upGrad’s software engineering courses.

Essential Stack in C++ Operations

Let us explore some important stack operations.

1. push()

The push() operation takes an element as input and adds it to the top of the stack. Internally, this typically involves:

  • Checking if there's enough space in the underlying container (array, deque, or list).
  • Placing the new element at the index/position which represents the top of the stack.
  • Incrementing the internal counter that tracks the stack's size.

Overflow (Capacity Exceeded)

If the underlying container is full, pushing a new element leads to a stack overflow. std::stack built on std::vector or std::deque might handle this by automatically resizing, but this can be costly. std::stack built on std::list won't overflow but might consume excessive memory.

Handling:

  • Check size() before pushing to see if the stack is full.
  • Consider using a std::list if we need a stack with potentially unbounded size.

2. pop()

The pop() operation removes the top element from the stack.

It typically involves:

  • Checking if the stack is empty.
  • Removing the element at the index/position representing the top of the stack.
  • Decrementing the internal size counter.

Underflow (Empty Stack)

Calling pop() on an empty stack is an error (stack underflow). std::stack might throw an exception in this case.

Handling:

  • Always use empty() to check if the stack has elements before calling pop().

3. top()

The top() function returns a reference to the topmost element without removing it. This allows us to inspect or modify the top element without altering the stack's structure.

Underflow (Empty Stack)

Like pop(), calling top() on an empty stack is an error.

Handling:

  • Always check empty() before calling top().

4. empty()

The empty() function is a boolean check. It returns true if the stack contains no elements and false otherwise. We can use it to control loop conditions or avoid underflow errors.

5. size()

The size() function returns the number of elements currently stored in the stack.

Member Types

Here are the member types we need for stack in C++:

  • value_type: This is an alias for the type of data we are storing in the stack (int, string, etc.).
  • container_type: This is the type of the underlying container (e.g., std::deque<int> if our stack stores integers and uses a deque).
  • size_type: This is an unsigned integer type used to represent the size or index of elements in the stack.

Example stack push pop program in C++:

Code:

#include <iostream>

#include <stack>

int main() {

std::stack<int> numbers;

numbers.push(10);

numbers.push(20);

std::cout << "Top element: " << numbers.top() << std::endl; // Output: 20

numbers.pop();

std::cout << "New top element: " << numbers.top() << std::endl; // Output: 10

}

Stack in C++ Performance and Optimizations

I feel that choosing the right stack implementation involves balancing performance, memory usage, and the specific requirements of our application. In most cases, the std::stack with its default std::deque container is a solid choice, but if we have very specific performance needs, exploring specialized libraries might be worthwhile.

Time Complexity Analysis

In general, the core stack operations offered by std::stack exhibit constant time complexity, denoted as O(1). This means that the time it takes to push, pop, or access the top element is independent of the size of the stack.

  • push(item): O(1) – Adding an element simply involves placing it at the top, regardless of how many elements are already in the stack.
  • pop(): O(1) – Removing the top element is equally straightforward.
  • top(): O(1) – Accessing the top element requires no searching or iteration.
  • empty() and size(): O(1) – These operations merely check or return internal counters.

The time complexity assumes that the underlying container (vector, deque, or list) also supports O(1) insertion and removal at the end. While this is generally true, there might be edge cases where the container needs to resize, leading to a slightly higher (but amortized constant) time complexity.

Memory Management Considerations

The choice of the underlying container can significantly impact memory usage:

std::vector

Potentially less memory overhead initially, as elements are stored contiguously. However, resizing can lead to reallocation and copying of elements, increasing memory usage temporarily.

std::deque

Offers a good balance, as it avoids the frequent reallocation issues of vectors but still allows for relatively efficient memory usage.

std::list

Typically has the highest memory overhead per element due to storing pointers for the linked list structure. It is ideal if we anticipate a very large stack size or frequent insertions/deletions throughout the stack.

Specialized Stack Libraries

While std::stack covers most general use cases, specialized libraries exist for scenarios where performance is absolutely critical:

  • Lock-free stacks: These are designed for highly concurrent environments where multiple threads need to access the stack simultaneously without locks, potentially improving performance. Examples include boost::lockfree::stack and the folly::ProducerConsumerQueue.
  • Persistent stacks: These stacks allow us to efficiently create "snapshots" of the stack's state at different points in time. This can be useful for implementing undo/redo functionality or backtracking algorithms.

Stack in C++ vs. Other Data Structures

While stack in cpp is powerful, it are not always the right tool for the job. Let us compare stack functions in C++ to using other common data structures for our functions:

  • Queues: Queues are FIFO (First-In, First-Out), like a line at the grocery store. They are perfect for tasks where order is important, like print jobs or task scheduling.
  • Linked Lists: Linked lists are flexible and allow efficient insertion/deletion at any point. However, accessing elements in the middle can be slower than in a stack.
  • Arrays: Arrays offer fast random access to elements by index, but inserting or removing them in the middle is costly.

Stack Using Linked List C++ and Array

You can trying to implement stack in C++ using array for exploring how the stack function can work with different data structures. You can also similarly implement stack in C++ using linked lists and queues.

Wrapping Up

The std::stack container in C++ is a versatile and efficient tool for managing data in a LIFO manner. By understanding its underlying principles, exploring its various applications, and considering performance optimization techniques, we can effectively leverage stacks to solve a wide array of programming challenges.

While simple in concept, stacks play a crucial role in numerous algorithms and computational tasks, highlighting their significance in the world of software development. If you wish to learn programming languages such as C++, you can check out upGrad’s computer science programs such as the Master’s in Computer Science Program.

Frequently Asked Questions

  1. What is a stack in C++?

A stack in C++ is a LIFO (Last-In, First-Out) data structure where elements are added (pushed) and removed (popped) from the same end, known as the top.

  1. How do I implement a stack in C++?

We can implement a stack using an array or a linked list, or we can simply use the std::stack container adaptor from the Standard Template Library (STL).

  1. What are the basic operations on a stack?

The basic operations are:

  • push(item): Adds an element to the top of the stack.
  • pop(): Removes the element at the top of the stack.
  • top(): Retrieves the element at the top of the stack without removing it.
  • empty(): Checks if the stack is empty.
  • size(): Returns the number of elements in the stack.
  1. How do I use the stack container in C++ STL?

Include the <stack> header and then create a stack object like this: std::stack<data_type> stack_name; (replace data_type with the type of elements we want to store).

  1. Does C++ have a stack library?

Yes, C++ provides the std::stack container in the <stack> header as part of the STL.

  1. Can I use custom objects with stack in C++?

Yes, you can use custom objects with the std::stack as long as your object type supports copy construction.

  1. When should I use a stack?

Use a stack when you need LIFO behavior, like in function call tracking, expression evaluation, backtracking algorithms, or undo/redo functionality.

  1. What are the time complexities of stack operations?

In general, all stack operations (push(), pop(), top(), empty(), size()) have a time complexity of O(1) in C++.

Rohan Vats

Rohan Vats

Software Engineering Manager @ upGrad. Passionate about building large scale web apps with delightful experiences. In pursuit of transforming eng…Read More

Need Guidance? We're Here to Help!
form image
+91
*
By clicking, I accept theT&Cand
Privacy Policy
image
Join 10M+ Learners & Transform Your Career
Learn on a personalised AI-powered platform that offers best-in-class content, live sessions & mentorship from leading industry experts.
right-top-arrowleft-top-arrow

upGrad Learner Support

Talk to our experts. We’re available 24/7.

text

Indian Nationals

1800 210 2020

text

Foreign Nationals

+918045604032

Disclaimer

upGrad does not grant credit; credits are granted, accepted or transferred at the sole discretion of the relevant educational institution offering the diploma or degree. We advise you to enquire further regarding the suitability of this program for your academic, professional requirements and job prospects before enr...