Skip to main content

Recognizing Stack and Queue Pattern Signals

1. Recognizing Next Greater/Smaller Signal

Signal Description Common Use Cases Pattern Indicators
Next Greater Element Find the first element to the right that is greater than the current element. Stock price analysis, daily temperatures, building heights Array of numbers with "rightward comparison"
Next Smaller Element Find the first element to the right that is smaller than the current element. Financial trend analysis, data visualization Array of numbers with "rightward comparison"

Example: Next Greater Element

function nextGreaterElement(arr) {
  const result = [];
  const stack = [];

  for (let i = arr.length - 1; i >= 0; i--) {
    while (stack.length && stack[stack.length - 1] <= arr[i]) {
      stack.pop();
    }

    if (stack.length === 0) {
      result[i] = -1;
    } else {
      result[i] = stack[stack.length - 1];
    }

    stack.push(arr[i]);
  }

  return result;
}

const input = [4, 5, 2, 10, 8];
console.log(nextGreaterElement(input)); // [5, 10, 10, -1, -1]

Example: Next Smaller Element

function nextSmallerElement(arr) {
  const result = [];
  const stack = [];

  for (let i = arr.length - 1; i >= 0; i--) {
    while (stack.length && stack[stack.length - 1] >= arr[i]) {
      stack.pop();
    }

    if (stack.length === 0) {
      result[i] = -1;
    } else {
      result[i] = stack[stack.length - 1];
    }

    stack.push(arr[i]);
  }

  return result;
}

const input = [4, 5, 2, 10, 8];
console.log(nextSmallerElement(input)); // [2, 2, -1, 8, -1]

Key Indicators for Stack Usage

  • Rightward comparison in arrays
  • Need to find "next" element satisfying a condition
  • Monotonic property (increasing or decreasing) in sequence

2. Recognizing Sliding Window Maximum Signal

Signal Description Indicators
Fixed-size window Problem requires processing elements in a fixed-size subset of the input array or list.
  • Input is an array or list
  • Window size is given or can be derived from input
Maximum in window Need to find the maximum element within each sliding window.
  • Maximum value is required for each window
  • Window slides by one position at a time
Dynamic range query Need to maintain a dynamic range (e.g., max, min) as the window slides.
  • Range queries must be updated efficiently
  • Elements are added and removed from the window

Example: Sliding Window Maximum

function maxSlidingWindow(nums, k) {
    const result = [];
    const deque = [];

    for (let i = 0; i < nums.length; i++) {
        // Remove indices that are out of the current window
        while (deque.length && deque[0] <= i - k) {
            deque.shift();
        }

        // Remove indices whose corresponding values are less than nums[i]
        while (deque.length && nums[deque[deque.length - 1]] < nums[i]) {
            deque.pop();
        }

        deque.push(i);

        // Add the maximum for the current window once we have a full window
        if (i >= k - 1) {
            result.push(nums[deque[0]]);
        }
    }

    return result;
}

// Example usage:
const nums = [1, 3, -1, -3, 5, 3, 6, 7];
const k = 3;
console.log(maxSlidingWindow(nums, k)); // [3, 3, 5, 5, 6, 7]
Note: The deque-based approach ensures O(n) time complexity by avoiding redundant computations for each window.

3. Recognizing Range Sum Queries Signal

Signal Description Use Case Pattern Match
Range Sum Query Need to compute sum of elements in a given range [L, R] Dynamic array with frequent updates and queries Query type: sum(L, R)
Update Element Modify value at a specific index Array with frequent updates and sum queries Update type: update(index, value)

Example: Range Sum Query with Segment Tree

class SegmentTree {
  constructor(arr) {
    this.n = arr.length;
    this.tree = new Array(4 * this.n).fill(0);
    this.build(arr, 0, 0, this.n - 1);
  }

  build(arr, node, start, end) {
    if (start === end) {
      this.tree[node] = arr[start];
    } else {
      const mid = Math.floor((start + end) / 2);
      this.build(arr, 2 * node + 1, start, mid);
      this.build(arr, 2 * node + 2, mid + 1, end);
      this.tree[node] = this.tree[2 * node + 1] + this.tree[2 * node + 2];
    }
  }

  query(node, start, end, l, r) {
    if (r < start || end < l) return 0;
    if (l <= start && end <= r) return this.tree[node];

    const mid = Math.floor((start + end) / 2);
    return this.query(2 * node + 1, start, mid, l, r) +
           this.query(2 * node + 2, mid + 1, end, l, r);
  }

  update(node, start, end, idx, val) {
    if (start === end) {
      this.tree[node] = val;
    } else {
      const mid = Math.floor((start + end) / 2);
      if (idx <= mid) {
        this.update(2 * node + 1, start, mid, idx, val);
      } else {
        this.update(2 * node + 2, mid + 1, end, idx, val);
      }
      this.tree[node] = this.tree[2 * node + 1] + this.tree[2 * node + 2];
    }
  }
}

const arr = [1, 3, 5, 7, 9, 11];
const segTree = new SegmentTree(arr);
console.log(segTree.query(0, 0, 5, 1, 3)); // Output: 15 (3 + 5 + 7)
Note: Range sum queries often indicate the use of a segment tree or Fenwick tree for efficient updates and queries.

4. Recognizing Range Update Queries Signal

Signal Description Typical Use Case Common Operations
Range Update Updating values in a range of indices Dynamic array modifications updateRange(start, end, value)
Range Query Querying sum, min, max over a range Frequency counting, statistics queryRange(start, end)
Lazy Propagation Delaying updates until necessary Efficient handling of multiple updates propagate()

Example: Range Update with Segment Tree

class SegmentTree {
  constructor(arr) {
    this.n = arr.length;
    this.tree = new Array(4 * this.n).fill(0);
    this.lazy = new Array(4 * this.n).fill(0);
    this.build(arr, 0, 0, this.n - 1);
  }

  build(arr, node, start, end) {
    if (start === end) {
      this.tree[node] = arr[start];
    } else {
      const mid = Math.floor((start + end) / 2);
      this.build(arr, 2 * node + 1, start, mid);
      this.build(arr, 2 * node + 2, mid + 1, end);
      this.tree[node] = this.tree[2 * node + 1] + this.tree[2 * node + 2];
    }
  }

  updateRange(node, start, end, l, r, value) {
    if (start > r || end < l) return;
    if (start >= l && end <= r) {
      this.tree[node] += value * (end - start + 1);
      if (start !== end) {
        this.lazy[2 * node + 1] += value;
        this.lazy[2 * node + 2] += value;
      }
      return;
    }

    const mid = Math.floor((start + end) / 2);
    this.updateRange(2 * node + 1, start, mid, l, r, value);
    this.updateRange(2 * node + 2, mid + 1, end, l, r, value);
    this.tree[node] = this.tree[2 * node + 1] + this.tree[2 * node + 2];
  }

  queryRange(node, start, end, l, r) {
    if (start > r || end < l) return 0;
    if (start >= l && end <= r) return this.tree[node];

    const mid = Math.floor((start + end) / 2);
    return this.queryRange(2 * node + 1, start, mid, l, r) +
           this.queryRange(2 * node + 2, mid + 1, end, l, r);
  }
}

// Usage
const arr = [1, 2, 3, 4, 5];
const segTree = new SegmentTree(arr);
segTree.updateRange(0, 0, arr.length - 1, 1, 3, 5);
console.log(segTree.queryRange(0, 0, arr.length - 1, 0, 2)); // 15
Note: Range update queries often require segment trees or Fenwick trees for efficient handling.

Key Indicators of Range Update Signals

  • Multiple updates over ranges
  • Need to query aggregated values (sum, min, max) in ranges
  • Efficiency is crucial for large datasets

5. Recognizing Frequency/Count/Grouping Signal

Signal Description Common Patterns Examples
Frequency Count Need to count occurrences of elements Hash maps, frequency tables
const freq = {};
for (let num of nums) {
  freq[num] = (freq[num] || 0) + 1;
}
Grouping Elements Group items by a property or criteria Maps, grouping functions
const groups = {};
for (let item of items) {
  const key = item.category;
  if (!groups[key]) groups[key] = [];
  groups[key].push(item);
}
Counting Unique Items Track unique elements or their counts Set, Map, frequency tracking
const unique = new Set();
for (let item of items) {
  unique.add(item);
}

Example: Grouping by Frequency

const items = ['a', 'b', 'c', 'a', 'b', 'a'];
const freq = {};
for (let item of items) {
  freq[item] = (freq[item] || 0) + 1;
}
// Result: { a: 3, b: 2, c: 1 }
Note: Frequency counting often indicates a need for hash maps or frequency tables to efficiently track element occurrences.

6. Recognizing Valid Parentheses/Brackets Signal

Pattern Description Use Case Examples
Stack-based validation Uses a stack to track opening brackets and match them with closing ones Checking syntax correctness in compilers or parsers ()[]{}()
Mismatch detection Stack top does not match the current closing bracket Identifying malformed expressions ([)]
Unbalanced brackets Stack is not empty after processing all characters Ensuring balanced expressions in code or math ((())

Example: Valid Parentheses Check

function isValid(s) {
  const stack = [];
  const map = { ')': '(', ']': '[', '}': '{' };

  for (let i = 0; i < s.length; i++) {
    const char = s[i];
    if (map[char]) {
      if (stack.pop() !== map[char]) return false;
    } else {
      stack.push(char);
    }
  }

  return stack.length === 0;
}

isValid("()[]{}"); // true
isValid("([)]"); // false
Note: The stack ensures last-in-first-out (LIFO) order, which matches the nesting behavior of parentheses.

Was this article helpful?