Next Greater Frequency Element

Do not miss this exclusive book on Binary Tree Problems. Get it now for free.

In the world of data manipulation and algorithms, numerous intriguing challenges arise. One such challenge is finding the "Next Greater Frequency Element" in an array. This problem involves combining the concepts of frequency counting and stack-based processing to efficiently determine the next element with a higher frequency for each element in the array.

Table of contents:

  1. Problem Statement
  2. Key takeaways
  3. Approach 1
  4. Approach 2
  5. Comparing both Approaches
  6. Real World Examples

Problem Statement:

Given an array of integers, our task is to find the next element with a higher frequency for each element in the array. In other words, for each element, we need to identify the element that appears more frequently after it.

Example:

Let's take an array [4, 6, 4, 2, 6, 2, 10] to illustrate the problem. We'll find the next greater frequency element for each element in the array.

  • For element 4, the next greater frequency element is 6, as 4 appears 2 times, and 6 appears 3 times after it.
  • For element 6, the next greater frequency element is 6 itself because it appears 3 times, and there are no elements with a higher frequency.
  • For element 4, the next greater frequency element is 6, similar to the first occurrence of 4.
  • For element 2, the next greater frequency element is 6 because 2 occurs 2 times, and 6 appears 3 times after it.
  • For element 6, the next greater frequency element is 10 because 6 occurs 3 times, and 10 appears with a frequency of 1 after it.
  • For element 2, the next greater frequency element is 10 because 2 occurs 2 times, and 10 appears with a frequency of 1 after it.
  • For element 10, there are no elements with a higher frequency, so it's denoted as -1.

The result for this example would be [6, 6, 6, 6, 10, 10, -1].

Key takeaways

Key takeaways for the two approaches to solving the "Next Greater Frequency Element" problem:

Approach 1 - Using Two Stacks:

  • Clear Separation: Utilizes two separate stacks for elements and frequencies, making the code easy to understand.
  • Readability: Offers a simple and readable solution, suitable for various scenarios.
  • Code Modularity: Enhances code modularity and ease of maintenance.
  • Efficiency: Efficient for smaller to moderately sized arrays, with time and space complexity of O(n).

Approach 2 - Using a Single Stack:

  • Memory Efficiency: Employs a single stack to manage element-frequency pairs, potentially reducing memory usage.
  • Code Complexity: Slightly more complex due to pair management, but memory-optimized.
  • Comparable Performance: Similar time and space complexity (O(n)) as the two-stack approach.
  • Use Case Consideration: Ideal when memory optimization or performance is crucial, and handling pairs is acceptable.

Approach 1:Using Two Stacks

In this approach, we'll use two stacks: one to maintain elements and another to maintain their corresponding frequencies. We'll traverse the array once to populate both stacks and then traverse it again to find the next greater frequency element for each element.

Algorithm steps:

  1. Initialize two empty stacks: one for elements and one for frequencies.

  2. Traverse the array:

    1. For each element, while the frequency stack is not empty and the frequency of the current element is greater than the frequency at the top of the frequency stack, pop elements from both stacks.
    2. Push the current element onto the element stack and its frequency onto the frequency stack.
  3. For each element, look up its frequency in the frequency stack to find the next greater frequency element.

IMPLEMENTATION

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

using namespace std;

vector<int> nextGreaterFrequencyElementTwoStack(const vector<int>& arr) {
    stack<int> elementStack, frequencyStack;
    vector<int> result;
    unordered_map<int, int> frequencyCount;

    for (int i = 0; i < arr.size(); i++) {
        int element = arr[i];
        int frequency = ++frequencyCount[element];

        while (!elementStack.empty() && frequency <= frequencyStack.top()) {
            elementStack.pop();
            frequencyStack.pop();
        }

        if (elementStack.empty()) {
            result.push_back(-1);
        } else {
            result.push_back(elementStack.top());
        }

        elementStack.push(element);
        frequencyStack.push(frequency);
    }

    return result;
}

int main() {
    vector<int> arr = {4, 6, 4, 2, 6, 2, 10};
    vector<int> result = nextGreaterFrequencyElementTwoStack(arr);

    for (int i : result) {
        cout << i << " ";
    }

    return 0;
}

Step by step explanation :

Input Array: [4, 6, 4, 2, 6, 2, 10]

  1. Initialize two empty stacks: elementstack and frequencystack.

  2. For each element in the array:

    • Element: 4, Frequency: 2

      • elementstack: [4]
      • frequencystack: [2]
    • Element: 6, Frequency: 2

      • Since the frequency of 6 is not greater than the frequency on top of the frequencystack, continue to the next step.
    • Element: 4, Frequency: 2

      • elementstack: [4, 4]
      • frequencystack: [2, 2]
      • Record next greater frequency elements:
        • For 4: 6
        • For 6: 4
    • Element: 2, Frequency: 2

      • elementstack: [4, 4, 2]
      • frequencystack: [2, 2, 2]
      • Record next greater frequency elements:
        • For 2: 6
    • Element: 6, Frequency: 2

      • elementstack: [4, 4, 2, 6]
      • frequencystack: [2, 2, 2, 2]
      • Record next greater frequency elements:
        • For 6: 4, 4
        • For 2: 6
    • Element: 2, Frequency: 2

      • elementstack: [4, 4, 2, 6, 2]
      • frequencystack: [2, 2, 2, 2, 2]
      • Record next greater frequency elements:
        • For 2: 6, 6
    • Element: 10, Frequency: 1

      • elementstack: [4, 4, 2, 6, 2, 10]
      • frequencystack: [2, 2, 2, 2, 2, 1]
      • Record next greater frequency elements:
        • For 10: None (as there's no greater frequency element)

Now, you have recorded the next greater frequency elements for each element in the input array as per the given algorithm:

  • For 4: 6
  • For 6: 4
  • For 2: 6
  • For 10: -1

OUTPUT:
[6, 4, 4, 6, 6, 6, -1]

Time Complexity: O(n)
  • The algorithm involves two traversals through the array: one to populate the stacks and another to find the next greater frequency elements.
  • Each element is pushed and popped from the stacks at most once during both traversals.
Space Complexity: O(n)

The two stacks grow proportionally to the input array size, resulting in a linear space complexity.

Approach 2 : Using a Single Stack

we'll use a single stack to maintain pairs of elements and their frequencies. We'll traverse the array once to populate the stack and then traverse it again to find the next greater frequency element for each element.

Algorithmic Steps:

1 . Initialize an empty stack (elementStack), an empty dictionary (frequencyCount), and an empty result list (result).

2 . Start iterating through the input array from left to right.

3 . For each element in the array : Increment its frequency count in the frequencyCount dictionary.
4 . While the stack is not empty and the frequency of the element on the top of the stack is less than or equal to the frequency of the current element:

  • Pop elements from the stack. These elements do not have a greater frequency element to their right, so for each popped element:
  • Set its result in the result list to the current element.

5 . If the stack is empty after the above loop or the frequency of the element on top of the stack is greater than the frequency of the current element, push the current element onto the stack.

6 . After processing all elements in the array, if there are any elements left in the stack, it means there is no greater frequency element to their right. For each remaining element in the stack:

  • Set its result in the result list to -1.
    7 . The result list now contains the next greater frequency element for each element in the input array.

8 . Return the result list as the output.

IMPLEMENTATION.

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

using namespace std;

vector<int> nextGreaterFrequencyElementSingleStack(const vector<int>& arr) {
    stack<int> elementStack;
    unordered_map<int, int> frequencyCount;
    vector<int> result;

    for (int element : arr) {
        frequencyCount[element]++;

        while (!elementStack.empty() && frequencyCount[elementStack.top()] < frequencyCount[element]) {
            elementStack.pop();
        }

        if (elementStack.empty()) {
            result.push_back(-1);
        } else {
            result.push_back(elementStack.top());
        }

        elementStack.push(element);
    }

    return result;
}

int main() {
    vector<int> arr = {4, 6, 4, 2, 6, 2, 10};
    vector<int> result = nextGreaterFrequencyElementSingleStack(arr);

    for (int i : result) {
        cout << i << " ";
    }

    return 0;
}


STEP BY STEP EXPLANATION:

Input:{4, 6, 4, 2, 6, 2, 10};

  1. Initialize an empty stack: elementSstack.

  2. Initialize an empty dictionary: frequencyCount.

  3. Initialize an empty result list: result.

  4. For each element in the array:

    • Element: 4

      • Increment frequency for 4: frequencyCount = {4: 1}
      • element_stack: [4]
    • Element: 6

      • Increment frequency for 6: frequencyCount = {4: 1, 6: 1}
      • Since the frequency of 6 is greater than the frequency of 4 on top of the stack, pop 4 and set its next greater frequency element as 6.
      • elementStack: [6]
      • Record 4's next greater frequency element as 6.
      • result: [-1] (as there's no element greater in frequency yet)
    • Element: 4

      • Increment frequency for 4: frequency_count = {4: 2, 6: 1}
      • Since the frequency of 4 is equal to the frequency of 6 on top of the stack, pop 6 and set its next greater frequency element as 4.
      • element_stack: [4]
      • Record 6's next greater frequency element as 4.
      • result: [-1, 4]
    • Element: 2

      • Increment frequency for 2: frequencyCount = {4: 2, 6: 1, 2: 1}
      • Since the frequency of 2 is less than the frequency of 4 on top of the stack, push 2 onto the stack.
      • elementStack: [4, 2]
    • Element: 6

      • Increment frequency for 6: frequencyCount = {4: 2, 6: 2, 2: 1}
      • Since the frequency of 6 is greater than the frequency of 2 on top of the stack, pop 2 and set its next greater frequency element as 6.
      • elementStack: [4, 6]
      • Record 2's next greater frequency element as 6.
      • result: [-1, 4, -1]
    • Element: 2

      • Increment frequency for 2: frequencyCount = {4: 2, 6: 2, 2: 2}
      • Since the frequency of 2 is equal to the frequency of 6 on top of the stack, pop 6 and set its next greater frequency element as 2.
      • elementStack: [4, 2]
      • Record 6's next greater frequency element as 2.
      • result: [-1, 4, -1, 2]
    • Element: 10

      • Increment frequency for 10: frequencyCount = {4: 2, 6: 2, 2: 2, 10: 1}
      • Since the stack is empty, push 10 onto the stack.
      • elementStack: [10]
  5. For the remaining elements in the stack (if any), set their next greater frequency element to -1:

    • Element: 10
      • result: [-1, 4, -1, 2, -1, -1, -1]
  6. The result list is [6, 4, 6, 6, 4, 4, -1].

Time Complexity: O(n)

  • Similar to the first approach, this algorithm requires two traversals through the array: one to populate the stack and another to find the next greater frequency elements.
  • Each element is pushed and popped from the stack at most once during both traversals.

Space Complexity: O(n)

  • The single stack and the frequency map both grow proportionally to the input array size, resulting in a linear space complexity.

Comparision :

  • Readability vs. Efficiency: Approach 1 (Two Stacks) is generally easier to understand due to its clear separation of elements and frequencies. Approach 2 (Single Stack) might be slightly more efficient in terms of memory usage due to using a single stack for pairs.
  • Memory Usage: Approach 2 may have a slight advantage in terms of memory usage due to using a single stack for both elements and frequencies.
  • Code Complexity: Approach 1 tends to have simpler and more modular code due to the clear distinction between stacks. Approach 2's code involves managing pairs, which can make it slightly more complex.
  • Performance: Both approaches have the same time complexity and similar performance. The difference in efficiency is likely to be minimal and might not be significant for most use cases.
  • Use Case Consideration: Choose Approach 1 when readability and separation of concerns are important. Choose Approach 2 when optimizing memory usage or performance is a priority, and you're comfortable working with pairs.

REAL LIFE EXAMPLES:

1.Social Media Engagement:

Imagine you're building a social media platform. Users can like posts, and you want to display a user's next post with higher engagement (likes, comments, shares). Here, each post is analogous to an element, and its engagement metrics are similar to its frequency. By finding the next post with higher engagement for each post, you can enhance user experience.

2.Text Prediction in Natural Language Processing:

When building text prediction systems, you might want to predict the next word in a sentence based on its frequency in a large corpus. Similar to the problem, we're essentially looking for the next word with a higher frequency for each word in the sentence.

3. Optimizing Resource Allocation:

In resource management scenarios, you could be allocating resources (such as servers) based on their load or utilization. You might want to distribute workloads to servers with higher capacity or lower utilization, which resembles finding the next server with higher capacity for each server.

4.Playlist Creation in Music Apps:

Music apps often create playlists based on users' listening history. You could determine the next song with higher play count for each song in a playlist, ensuring that users are presented with songs they enjoy more frequently.

5. Sorting and Scheduling:

In scheduling tasks, you might need to prioritize tasks based on their importance or urgency. This can be done by finding the next task with higher priority or urgency for each task in your list.

Sign up for FREE 3 months of Amazon Music. YOU MUST NOT MISS.