Implement Dynamic Multi Stack (K stacks) using only one Data Structure
Last Updated :
30 Oct, 2023
In this article, we will see how to create a data structure that can handle multiple stacks with growable size. The data structure needs to handle three operations:
- push(x, stackNum) = pushes value x to the stack numbered stackNum
- pop(stackNum) = pop the top element from the stack numbered stackNum
- top(stackNum) = shows the topmost element of the stack stackNum.
Example:
Suppose the given multi stack is [{1, 2}, {4, 6}, {9, 10}]
Input: push(3, 0), top(0)
push(7, 1), top(1)
pop(2), top(2)
Output: 3, 7, 9
Explanation: When 3 is pushed in stack 0, the stack becomes {1, 2, 3}. So the top element is 3.
When 7 is pushed in stack 1, the stack becomes {4, 6, 7}. So the top element is 7.
When topmost element is popped from stack 2, the stack becomes {9}. So the topmost element is 9
Approach: Follow the approach mentioned below to implement the idea.
- Store the size and the top index of every stack in arrays sizes[] and topIndex[].
- The sizes will be stored in a prefix sum array (using a prefix array sum will help us find the start/size of a stack in O(1) time)
- If the size of a stack reaches the maximum reserved capacity, expand the reserved size (multiply by 2)
- If the size of a stack gets down to a quarter of the reserved size shrink the reserved size (divide it by 2)
- Every time we need to expand/shrink a stack in the data structure, the indexes of other stacks might change so we need to take care of that. That is taken care by incrementing/decrementing value of sizes[] and topIndex[] arrays (we can do that in O(number of stacks) time).
Below is the implementation :
C++
#include <bits/stdc++.h>
using namespace std;
template <typename T>
// Class to implement multistack
class MultiStack {
int numberOfStacks;
vector<T> values;
vector<int> sizes, topIndex;
public:
// Constructor to create k stacks
// (by default 1)
MultiStack(int k = 1)
: numberOfStacks(k)
{
// reserve 2 elements for each stack first
values = vector<T>(numberOfStacks << 1);
// For each stack store the index
// of the element on the top
// and the size (starting point)
sizes = vector<int>(numberOfStacks);
topIndex = vector<int>(numberOfStacks);
// Sizes is a prefix sum vector
for (int size = 2, i = 0; i < numberOfStacks;
i++, size += 2)
sizes[i] = size, topIndex[i] = size - 2;
}
// Push a value in a stack
void push(int stackNum, T val)
{
// Check if the stack is full,
// if so Expand it
if (isFull(stackNum))
Expand(stackNum);
// Add the value to the top of the stack
// and increment the top index
values[topIndex[stackNum]++] = val;
}
// Pop the top value and
// return it from a stack
T pop(int stackNum)
{
// If the stack is empty
// throw an error
if (empty(stackNum))
throw("Empty Stack!");
// Save the top value
T val = values[topIndex[stackNum] - 1];
// Set top value to default data type
values[--topIndex[stackNum]] = T();
// Shrink the reserved size if needed
Shrink(stackNum);
// Return the pop-ed value
return val;
}
// Return the top value form a stack
// Same as pop (but without removal)
T top(int stackNum)
{
if (empty(stackNum))
throw("Empty Stack!");
return values[topIndex[stackNum] - 1];
}
// Return the size of a stack
// (the number of elements in the stack,
// not the reserved size)
int size(int stackNum)
{
if (!stackNum)
return topIndex[0];
return topIndex[stackNum] - sizes[stackNum - 1];
}
// Check if a stack is empty or not
// (has no elements)
bool empty(int stackNum)
{
int offset;
if (!stackNum)
offset = 0;
else
offset = sizes[stackNum - 1];
int index = topIndex[stackNum];
return index == offset;
}
// Helper function to check
// if a stack size has reached
// the reserved size of that stack
bool isFull(int stackNum)
{
int offset = sizes[stackNum];
int index = topIndex[stackNum];
return index >= offset;
}
// Function to expand the reserved size
// of a stack (multiply by 2)
void Expand(int stackNum)
{
// Get the reserved_size of the stack()
int reserved_size = size(stackNum);
// Update the prefix sums (sizes)
// and the top index of the next stacks
for (int i = stackNum + 1; i < numberOfStacks; i++)
sizes[i] += reserved_size,
topIndex[i] += reserved_size;
// Update the size of the recent stack
sizes[stackNum] += reserved_size;
// Double the size of the stack by
// inserting 'reserved_size' elements
values.insert(values.begin() + topIndex[stackNum],
reserved_size, T());
}
// Function to shrink the reserved size
// of a stack (divide by2)
void Shrink(int stackNum)
{
// Get the reserved size and the current size
int reserved_size, current_size;
if (!stackNum)
reserved_size = sizes[0],
current_size = topIndex[0];
else
reserved_size
= sizes[stackNum] - sizes[stackNum - 1],
current_size
= topIndex[stackNum] - sizes[stackNum - 1];
// Shrink only if the size is
// lower than a quarter of the
// reserved size and avoid shrinking
// if the reserved size is 2
if (current_size * 4 > reserved_size
|| reserved_size == 2)
return;
// Divide the size by 2 and update
// the prefix sums (sizes) and
// the top index of the next stacks
int dif = reserved_size / 2;
for (int i = stackNum + 1; i < numberOfStacks; i++)
sizes[i] -= dif, topIndex[i] -= dif;
sizes[stackNum] -= dif;
// Erase half of the reserved size
values.erase(values.begin() + topIndex[stackNum],
values.begin() + topIndex[stackNum]
+ dif);
}
};
// Driver code
int main()
{
// create 3 stacks
MultiStack<int> MStack(3);
// push elements in stack 0:
MStack.push(0, 21);
MStack.push(0, 13);
MStack.push(0, 14);
// Push one element in stack 1:
MStack.push(1, 15);
// Push two elements in stack 2:
MStack.push(2, 1);
MStack.push(2, 2);
MStack.push(2, 3);
// Print the top elements of the stacks
cout << MStack.top(0) << '\n';
cout << MStack.top(1) << '\n';
cout << MStack.top(2) << '\n';
return 0;
}
Java
// Java implementation for the above approach
import java.util.*;
class MultiStack<T> {
private int numberOfStacks;
private ArrayList<T> values;
private ArrayList<Integer> sizes, topIndex;
// Constructor for MultiStack
// Takes the number of stacks (k) as input and initializes the MultiStack object
public MultiStack(int k) {
// Set the number of stacks
numberOfStacks = k;
// Initialize the values ArrayList with an initial capacity of k*2
values = new ArrayList<>(numberOfStacks << 1);
// Initialize the sizes ArrayList with a size of k
sizes = new ArrayList<>(numberOfStacks);
// Initialize the topIndex ArrayList with a size of k
topIndex = new ArrayList<>(numberOfStacks);
// Loop through k times
for (int size = 2, i = 0; i < numberOfStacks; i++, size += 2) {
// Add size to the sizes ArrayList
sizes.add(size);
// Add size-2 to the topIndex ArrayList
topIndex.add(size - 2);
}
}
// Push a value onto a specified stack
public void push(int stackNum, T val) {
// If the stack is full, expand it
if (isFull(stackNum))
Expand(stackNum);
// Add the value to the ArrayList at the index
// specified by the topIndex of the specified stack
values.add(topIndex.get(stackNum), val);
// Increment the topIndex of the specified stack
topIndex.set(stackNum, topIndex.get(stackNum) + 1);
}
// Pop a value off a specified stack
public T pop(int stackNum) {
// If the stack is empty, throw an exception
if (empty(stackNum))
throw new RuntimeException("Empty Stack!");
// Get the value at the top of the specified stack
T val = values.get(topIndex.get(stackNum) - 1);
// Set the value at the top of the specified stack to null
values.set(topIndex.get(stackNum) - 1, null);
// Decrement the topIndex of the specified stack
topIndex.set(stackNum, topIndex.get(stackNum) - 1);
// Shrink the specified stack if necessary
shrink(stackNum);
// Return the value at the top of the specified stack
return val;
}
// Returns the element at the top of the specified stack
public T top(int stackNum) {
if (empty(stackNum))
throw new RuntimeException("Empty Stack!");
return values.get(topIndex.get(stackNum) - 1);
}
// Returns the number of elements in the specified stack
public int size(int stackNum) {
if (stackNum == 0)
return topIndex.get(0);
return topIndex.get(stackNum) - sizes.get(stackNum - 1);
}
// Checks if the specified stack is empty
public boolean empty(int stackNum) {
int offset;
if (stackNum == 0)
offset = 0;
else
offset = sizes.get(stackNum - 1);
int index = topIndex.get(stackNum);
return index == offset;
}
// Checks if the specified stack is full
public boolean isFull(int stackNum) {
int offset = sizes.get(stackNum);
int index = topIndex.get(stackNum);
return index >= offset;
}
public void Expand(int stackNum) {
int reserved_size = size(stackNum);
for (int i = stackNum + 1; i < numberOfStacks; i++) {
sizes.set(i, sizes.get(i) + reserved_size);
topIndex.set(i, topIndex.get(i) + reserved_size);
}
sizes.set(stackNum, sizes.get(stackNum) + reserved_size);
for (int i = 0; i < reserved_size; i++)
values.add(topIndex.get(stackNum), null);
}
// Function to shrink the reserved size
// of a stack (divide by2)
void shrink(int stackNum) {
// Get the reserved size and the current size
int reserved_size, current_size;
if (stackNum == 0) {
reserved_size = sizes.get(0);
current_size = topIndex.get(0);
} else {
reserved_size = sizes.get(stackNum) - sizes.get(stackNum - 1);
current_size = topIndex.get(stackNum) - sizes.get(stackNum - 1);
}
// Shrink only if the size is
// lower than a quarter of the
// reserved size and avoid shrinking
// if the reserved size is 2
if (current_size * 4 > reserved_size || reserved_size == 2) {
return;
}
// Divide the size by 2 and update
// the prefix sums (sizes) and
// the top index of the next stacks
int dif = reserved_size / 2;
for (int i = stackNum + 1; i < numberOfStacks; i++) {
sizes.set(i, sizes.get(i) - dif);
topIndex.set(i, topIndex.get(i) - dif);
}
sizes.set(stackNum, sizes.get(stackNum) - dif);
// Erase half of the reserved size
values.subList(topIndex.get(stackNum), topIndex.get(stackNum) + dif).clear();
}
// Driver code
public static void main(String[] args) {
// create 3 stacks
MultiStack<Integer> MStack = new MultiStack<>(3);
// push elements in stack 0:
MStack.push(0, 21);
MStack.push(0, 13);
MStack.push(0, 14);
// Push one element in stack 1:
MStack.push(1, 15);
// Push two elements in stack 2:
MStack.push(2, 1);
MStack.push(2, 2);
MStack.push(2, 3);
// Print the top elements of the stacks
System.out.println(MStack.top(0));
System.out.println(MStack.top(1));
System.out.println(MStack.top(2));
}
};
// This code is contributed by amit_mangal_
Python3
# Python3 implementation for the above approach
class MultiStack:
def __init__(self, k=1):
# Initializes the MultiStack with k number of stacks.
self.number_of_stacks = k
# Initializes an array to hold values of all stacks.
self.values = [None] * (self.number_of_stacks * 2)
# Initializes an array to hold sizes of all stacks.
self.sizes = [2 + i*2 for i in range(self.number_of_stacks)]
# Initializes an array to hold the top index of each stack.
self.top_index = [size-2 for size in self.sizes]
def push(self, stack_num, val):
# Pushes a value onto the given stack_num.
# If the stack is full, expands the stack.
if self.is_full(stack_num):
self.expand(stack_num)
self.values[self.top_index[stack_num]] = val
self.top_index[stack_num] += 1
def pop(self, stack_num):
# Pops the top value off of the given stack_num.
# If the stack is empty, raises an exception.
if self.is_empty(stack_num):
raise Exception("Empty Stack!")
val = self.values[self.top_index[stack_num]-1]
self.values[self.top_index[stack_num]-1] = None
self.top_index[stack_num] -= 1
self.shrink(stack_num)
return val
def top(self, stack_num):
# Check if the stack is empty
if self.is_empty(stack_num):
raise Exception("Empty Stack!")
# Return the top element of the stack
return self.values[self.top_index[stack_num]-1]
def size(self, stack_num):
# If no stack_num specified, return the
# total number of elements in all stacks
if not stack_num:
return self.top_index[0]
# Calculate the number of elements in the specified stack
return self.top_index[stack_num] - self.sizes[stack_num-1]
def is_empty(self, stack_num):
# Calculate the index offset for the specified stack
if not stack_num:
offset = 0
else:
offset = self.sizes[stack_num-1]
# Check if the stack is empty
index = self.top_index[stack_num]
return index == offset
def is_full(self, stack_num):
# Calculate the index offset for the specified stack
offset = self.sizes[stack_num]
# Check if the stack is full
index = self.top_index[stack_num]
return index >= offset
# This method expands a stack by copying its elements
# to the next stack(s) and increasing its size
def expand(self, stack_num):
# Get the reserved size of the stack
reserved_size = self.size(stack_num)
# Increase the size and top index of all the
# stacks after the current stack
for i in range(stack_num+1, self.number_of_stacks):
self.sizes[i] += reserved_size
self.top_index[i] += reserved_size
# Increase the size of the current stack
self.sizes[stack_num] += reserved_size
# Insert reserved_size None values at the
# top of the current stack to expand it
self.values = (self.values[:self.top_index[stack_num]] +
[None] * reserved_size +
self.values[self.top_index[stack_num]:])
# This method shrinks a stack by reducing its size
# and copying its elements to the next stack(s)
def shrink(self, stack_num):
# Get the reserved size and current size of the stack
if not stack_num:
reserved_size, current_size = self.sizes[0], self.top_index[0]
else:
reserved_size = self.sizes[stack_num] - self.sizes[stack_num-1]
current_size = self.top_index[stack_num] - self.sizes[stack_num-1]
# Check if the stack should be shrunk
if current_size * 4 > reserved_size or reserved_size == 2:
return
# Calculate the difference to reduce the stack size
dif = reserved_size // 2
# Reduce the size and top index of all the stacks after the current stack
for i in range(stack_num+1, self.number_of_stacks):
self.sizes[i] -= dif
self.top_index[i] -= dif
# Reduce the size of the current stack
self.sizes[stack_num] -= dif
# Remove dif elements from the top of the current stack to shrink it
self.values = (self.values[:self.top_index[stack_num]-dif] +
self.values[self.top_index[stack_num]:])
# create 3 stacks
MStack = MultiStack(3)
# push elements in stack 0:
MStack.push(0, 21)
MStack.push(0, 13)
MStack.push(0, 14)
# Push one element in stack 1:
MStack.push(1, 15)
# Push two elements in stack 2:
MStack.push(2, 1)
MStack.push(2, 2)
MStack.push(2, 3)
# Print the top elements of the stacks
print(MStack.top(0))
print(MStack.top(1))
print(MStack.top(2))
# This code is contributed by amit_mangal_
C#
using System;
using System.Collections.Generic;
public class MultiStack<T> {
private int numberOfStacks; // Number of stacks in the
// MultiStack
private List<T> values; // List to store all the values
// of the stacks
private List<int>
sizes; // List to store the sizes of each stack
private List<int> topIndexes; // List to store the top
// indexes of each stack
// Constructor to create a MultiStack with 'k' stacks
// (default is 1)
public MultiStack(int k = 1)
{
numberOfStacks = k;
values = new List<T>();
sizes = new List<int>(new int[k]);
topIndexes = new List<int>(new int[k]);
}
// Push a value onto a specific stack
public void Push(int stackNum, T val)
{
if (stackNum < 0 || stackNum >= numberOfStacks) {
throw new ArgumentOutOfRangeException(
"Invalid stack number");
}
// Add the value to the main list
values.Add(val);
// Increase the size of the stack
sizes[stackNum]++;
// Update the top index for this stack
topIndexes[stackNum] = values.Count - 1;
}
// Pop a value from a specific stack
public T Pop(int stackNum)
{
if (stackNum < 0 || stackNum >= numberOfStacks) {
throw new ArgumentOutOfRangeException(
"Invalid stack number");
}
if (IsEmpty(stackNum)) {
throw new InvalidOperationException(
"Stack is empty");
}
// Get the index of the top element of the stack
int index = topIndexes[stackNum];
// Get the value at this index
T val = values[index];
// Remove the value from the main list
values.RemoveAt(index);
// Decrease the size of the stack
sizes[stackNum]--;
// Update top indexes for the remaining stacks
UpdateTopIndexes(stackNum, index);
// Return the popped value
return val;
}
// Get the top value of a specific stack
public T Top(int stackNum)
{
if (stackNum < 0 || stackNum >= numberOfStacks) {
throw new ArgumentOutOfRangeException(
"Invalid stack number");
}
if (IsEmpty(stackNum)) {
throw new InvalidOperationException(
"Stack is empty");
}
// Return the value at the top index of this stack
return values[topIndexes[stackNum]];
}
// Get the size of a specific stack
public int Size(int stackNum)
{
if (stackNum < 0 || stackNum >= numberOfStacks) {
throw new ArgumentOutOfRangeException(
"Invalid stack number");
}
// Return the size of this stack
return sizes[stackNum];
}
// Check if a specific stack is empty
public bool IsEmpty(int stackNum)
{
if (stackNum < 0 || stackNum >= numberOfStacks) {
throw new ArgumentOutOfRangeException(
"Invalid stack number");
}
// Check if the size of this stack is 0
return sizes[stackNum] == 0;
}
// Update the top indexes of stacks after a pop
// operation
private void UpdateTopIndexes(int stackNum,
int removedIndex)
{
for (int i = stackNum; i < numberOfStacks; i++) {
// Decrement the top index if it's greater than
// the removed index
if (topIndexes[i] > removedIndex) {
topIndexes[i]--;
}
}
}
}
class Program {
static void Main()
{
// Create an instance of MultiStack with 3 stacks
MultiStack<int> MStack = new MultiStack<int>(3);
// Push elements into different stacks
MStack.Push(0, 21);
MStack.Push(0, 13);
MStack.Push(0, 14);
MStack.Push(1, 15);
MStack.Push(2, 1);
MStack.Push(2, 2);
MStack.Push(2, 3);
// Print the tops of each stack
Console.WriteLine("Top of Stack 0: "
+ MStack.Top(0));
Console.WriteLine("Top of Stack 1: "
+ MStack.Top(1));
Console.WriteLine("Top of Stack 2: "
+ MStack.Top(2));
}
}
JavaScript
class MultiStack {
constructor(k = 1) {
this.numberOfStacks = k;
this.values = new Array(k * 2).fill(0);
this.sizes = new Array(k).fill(0);
this.topIndex = new Array(k).fill(0);
// Sizes is a prefix sum array
for (let size = 2, i = 0; i < this.numberOfStacks; i++, size += 2) {
this.sizes[i] = size;
this.topIndex[i] = size - 2;
}
}
push(stackNum, val) {
if (this.isFull(stackNum)) {
this.expand(stackNum);
}
this.values[this.topIndex[stackNum]++] = val;
}
pop(stackNum) {
if (this.empty(stackNum)) {
throw new Error("Empty Stack!");
}
const val = this.values[this.topIndex[stackNum] - 1];
this.values[--this.topIndex[stackNum]] = 0;
this.shrink(stackNum);
return val;
}
top(stackNum) {
if (this.empty(stackNum)) {
throw new Error("Empty Stack!");
}
return this.values[this.topIndex[stackNum] - 1];
}
size(stackNum) {
if (!stackNum) {
return this.topIndex[0];
}
return this.topIndex[stackNum] - this.sizes[stackNum - 1];
}
empty(stackNum) {
const offset = !stackNum ? 0 : this.sizes[stackNum - 1];
const index = this.topIndex[stackNum];
return index === offset;
}
isFull(stackNum) {
const offset = this.sizes[stackNum];
const index = this.topIndex[stackNum];
return index >= offset;
}
expand(stackNum) {
const reservedSize = this.size(stackNum);
for (let i = stackNum + 1; i < this.numberOfStacks; i++) {
this.sizes[i] += reservedSize;
this.topIndex[i] += reservedSize;
}
this.sizes[stackNum] += reservedSize;
const reservedArray = new Array(reservedSize).fill(0);
this.values.splice(this.topIndex[stackNum], 0, ...reservedArray);
}
shrink(stackNum) {
let reservedSize, currentSize;
if (!stackNum) {
reservedSize = this.sizes[0];
currentSize = this.topIndex[0];
} else {
reservedSize = this.sizes[stackNum] - this.sizes[stackNum - 1];
currentSize = this.topIndex[stackNum] - this.sizes[stackNum - 1];
}
if (currentSize * 4 > reservedSize || reservedSize === 2) {
return;
}
const difference = reservedSize / 2;
for (let i = stackNum + 1; i < this.numberOfStacks; i++) {
this.sizes[i] -= difference;
this.topIndex[i] -= difference;
}
this.sizes[stackNum] -= difference;
this.values.splice(
this.topIndex[stackNum],
difference,
);
}
}
// Driver code
const MStack = new MultiStack(3);
MStack.push(0, 21);
MStack.push(0, 13);
MStack.push(0, 14);
MStack.push(1, 15);
MStack.push(2, 1);
MStack.push(2, 2);
MStack.push(2, 3);
console.log(MStack.top(0));
console.log(MStack.top(1));
console.log(MStack.top(2));
Time complexities:
- O(1) for top() function.
- Amortized O(1) for push() and pop() functions.
Auxiliary Space: O(N) where N is the number of stacks
Similar Reads
DSA Tutorial - Learn Data Structures and Algorithms DSA (Data Structures and Algorithms) is the study of organizing data efficiently using data structures like arrays, stacks, and trees, paired with step-by-step procedures (or algorithms) to solve problems effectively. Data structures manage how data is stored and accessed, while algorithms focus on
7 min read
Quick Sort QuickSort is a sorting algorithm based on the Divide and Conquer that picks an element as a pivot and partitions the given array around the picked pivot by placing the pivot in its correct position in the sorted array. It works on the principle of divide and conquer, breaking down the problem into s
12 min read
Merge Sort - Data Structure and Algorithms Tutorials Merge sort is a popular sorting algorithm known for its efficiency and stability. It follows the divide-and-conquer approach. It works by recursively dividing the input array into two halves, recursively sorting the two halves and finally merging them back together to obtain the sorted array. Merge
14 min read
Data Structures Tutorial Data structures are the fundamental building blocks of computer programming. They define how data is organized, stored, and manipulated within a program. Understanding data structures is very important for developing efficient and effective algorithms. What is Data Structure?A data structure is a st
2 min read
Bubble Sort Algorithm Bubble Sort is the simplest sorting algorithm that works by repeatedly swapping the adjacent elements if they are in the wrong order. This algorithm is not suitable for large data sets as its average and worst-case time complexity are quite high.We sort the array using multiple passes. After the fir
8 min read
Breadth First Search or BFS for a Graph Given a undirected graph represented by an adjacency list adj, where each adj[i] represents the list of vertices connected to vertex i. Perform a Breadth First Search (BFS) traversal starting from vertex 0, visiting vertices from left to right according to the adjacency list, and return a list conta
15+ min read
Binary Search Algorithm - Iterative and Recursive Implementation Binary Search Algorithm is a searching algorithm used in a sorted array by repeatedly dividing the search interval in half. The idea of binary search is to use the information that the array is sorted and reduce the time complexity to O(log N). Binary Search AlgorithmConditions to apply Binary Searc
15 min read
Insertion Sort Algorithm Insertion sort is a simple sorting algorithm that works by iteratively inserting each element of an unsorted list into its correct position in a sorted portion of the list. It is like sorting playing cards in your hands. You split the cards into two groups: the sorted cards and the unsorted cards. T
9 min read
Array Data Structure Guide In this article, we introduce array, implementation in different popular languages, its basic operations and commonly seen problems / interview questions. An array stores items (in case of C/C++ and Java Primitive Arrays) or their references (in case of Python, JS, Java Non-Primitive) at contiguous
4 min read
Sorting Algorithms A Sorting Algorithm is used to rearrange a given array or list of elements in an order. For example, a given array [10, 20, 5, 2] becomes [2, 5, 10, 20] after sorting in increasing order and becomes [20, 10, 5, 2] after sorting in decreasing order. There exist different sorting algorithms for differ
3 min read