830. Positions of Large Groups

EasyString
Leetcode Link

Problem Description

The given problem revolves around identifying the 'large' groups of consecutive identical characters in a string s. A 'large' group is defined as a sequence of the same character that appears at least three times consecutively. The goal is to find the starting index and the ending index for each of these large groups.

Consider this example: In the string s = "abbxxxxzyy", the following groups are formed: "a", "bb", "xxxx", "z", and "yy". Out of these, only "xxxx" qualifies as a large group because it contains the same character ('x') four times in a row. The interval for this group is [3, 6], where 3 is the starting index, and 6 is the ending index of the group within the string.

The expected output is a list of intervals with each interval representing a large group and the list is sorted by the starting index of each interval.

Intuition

To solve this problem, the approach is to iterate over the string while keeping track of the start of a potential large group. Whenever we encounter a different character or reach the end of the string, we check if the current character sequence qualifies as a large group by ensuring the sequence length is at least three characters long. If it does, we record the start and the end indices of this group. Next, we update our tracker to the current character's index and continue scanning until we have examined all characters in the string.

Iterating only once through the string results in an efficient solution with a linear time complexity (O(n)), where n is the length of the string. We can assert that the solution is efficient because we're only scanning the string once without any nested loops, making it suitable for large input strings as well.

The key takeaway is that we maintain the current character sequence length and start index while traversing the string and only record intervals that satisfy the 'large group' criteria.

Not Sure What to Study? Take the 2-min Quiz to Find Your Missing Piece:

Given a sorted array of integers and an integer called target, find the element that equals to the target and return its index. Select the correct code that fills the ___ in the given code snippet.

1def binary_search(arr, target):
2    left, right = 0, len(arr) - 1
3    while left ___ right:
4        mid = (left + right) // 2
5        if arr[mid] == target:
6            return mid
7        if arr[mid] < target:
8            ___ = mid + 1
9        else:
10            ___ = mid - 1
11    return -1
12
1public static int binarySearch(int[] arr, int target) {
2    int left = 0;
3    int right = arr.length - 1;
4
5    while (left ___ right) {
6        int mid = left + (right - left) / 2;
7        if (arr[mid] == target) return mid;
8        if (arr[mid] < target) {
9            ___ = mid + 1;
10        } else {
11            ___ = mid - 1;
12        }
13    }
14    return -1;
15}
16
1function binarySearch(arr, target) {
2    let left = 0;
3    let right = arr.length - 1;
4
5    while (left ___ right) {
6        let mid = left + Math.trunc((right - left) / 2);
7        if (arr[mid] == target) return mid;
8        if (arr[mid] < target) {
9            ___ = mid + 1;
10        } else {
11            ___ = mid - 1;
12        }
13    }
14    return -1;
15}
16

Solution Approach

The solution's implementation utilizes a two-pointer pattern, which is often used in problems involving arrays or strings where we need to track subarrays or substrings based on a certain condition. Here, the two pointers are denoted as i (the start pointer) and j (the end pointer).

The approach begins with initializing the starting pointer i to 0 and an empty list ans to collect the intervals representing the large groups.

The main algorithm can be described as follows:

  1. Begin iterating over the string with the starting pointer i, aiming to pinpoint the start of a character sequence.
  2. Create an inner loop that increments the end pointer j for as long as the character at position j is the same as the character at the start pointer i.
  3. Upon exit from the inner loop, check if the difference between j and i is greater than or equal to 3, indicating a large group.
  4. If it is indeed a large group, append the interval [i, j - 1] to the list ans, since j - 1 is the end index of the large group.
  5. Finally, update the start pointer i to j to commence scanning for the next potential group.

The code maintains a while loop that will run until i reaches the end of the string (i < n). Each time a large group is found, the interval is appended to the list before resetting i to continue searching from the end of the last found group.

The use of the continuous while loop paired with the two-pointer technique ensures all large groups are identified without repetitively checking characters, which efficaciously reduces the time complexity.

Once the while loop is completed, the function returns ans, which now contains all intervals of the identified large groups, arranged by their start indices as per the requirement.

Here is an excerpt showcasing the two-pointer technique in the given code:

1while i < n:            # Main loop iterating through the string
2    j = i               # Initializing end pointer
3    while j < n and s[j] == s[i]:   # Inner loop to increment 'j' as long as characters match
4        j += 1
5    if j - i >= 3:      # If the length of the group is at least 3, it is a large group
6        ans.append([i, j - 1])  # Append interval to answer list as a large group is found
7    i = j               # Move start pointer 'i' to the end of the last found group

This solution is efficient as it involves a single pass of the input string, making the time complexity O(n), where n is the size of the string.

Discover Your Strengths and Weaknesses: Take Our 2-Minute Quiz to Tailor Your Study Plan:

In a binary min heap, the maximum element can be found in:

Example Walkthrough

Let's walk through an example to illustrate the solution approach:

Consider the string s = "aabbbccdeee". We need to find the large groups of consecutive identical characters, where a large group is defined as a sequence with at least three occurrences of the same character.

  1. Initialize the start pointer i to 0 and create an empty list ans for storing our intervals.
  2. Initiate the main while loop, where i starts at index 0.
  3. The end pointer j is also initialized to the same value as i.
  4. Start the inner while loop that compares characters at positions j and i and increments j as long as they are identical. For the first loop, we compare characters 'a' at index 0 and 1, and since they are not identical, j stops at 1.
  5. No interval is added to ans after the first loop since j - i < 3.
  6. Now, i is set to j, and our pointers are i = 1, j = 1. The comparison starts with 'a' and 'b'. Since they are different characters, we immediately look for the next sequence.
  7. Next iteration, i is still at 1, and 'b' at position 1 matches 'b' at positions 2 and 3. The inner loop increments j to 4.
  8. Now j - i >= 3 – it's a large group, so we add the interval [1, 3] to ans.
  9. Update i to the current value of j, which is 4.
  10. Continue this process for the rest of the string.
    • i = 4, j = 4: Match 'c' with subsequent 'c' until j = 6. j - i = 2, not a large group.
    • i = 6, j = 6: Skip 'd' as it's a single character.
    • i = 7, j = 7: Characters 'e' at index 7, 8, and 9 match. j moves to 10. j - i >= 3, we add the interval [7, 9] to ans.
  11. Finally, our list ans contains [[1, 3], [7, 9]], representing the large groups 'bbb' and 'eee' with their respective starting and ending indices.

The implementation efficiently identifies the large groups in a single traversal of the string s. The output list is sorted by starting index because the string is traversed from left to right, naturally maintaining the order.

Solution Implementation

1class Solution:
2    def largeGroupPositions(self, s: str) -> List[List[int]]:
3        # Initialize start index `start` and length of the string `length`
4        start, length = 0, len(s)
5        # Initialize an empty list to store the answer
6        answer = []
7
8        # Iterate over the string characters by index
9        while start < length:
10            # Initialize the end index `end` at the same position as start
11            end = start
12            # Move `end` forward as long as the subsequent characters are the same as s[start]
13            while end < length and s[end] == s[start]:
14                end += 1
15            # If the length of the group is 3 or more, add it to the answer list
16            if end - start >= 3:
17                answer.append([start, end - 1])
18            # Update start to the next character group
19            start = end
20      
21        # Return the final list of positions of large groups
22        return answer
23
1import java.util.ArrayList;
2import java.util.Arrays;
3import java.util.List;
4
5class Solution {
6
7    // Method to find the starting and ending indices of all large groups.
8    public List<List<Integer>> largeGroupPositions(String s) {
9        int strLength = s.length(); // Length of the input string.
10        int startIndex = 0; // Initialize the starting index of a group.
11        List<List<Integer>> largeGroups = new ArrayList<>(); // Initialize the list to store the result.
12      
13        // Iterate over the string to find large groups.
14        while (startIndex < strLength) {
15            int endIndex = startIndex; // Initialize the end index of the current group.
16            // Increase the endIndex as long as the current character is the same as the start character.
17            while (endIndex < strLength && s.charAt(endIndex) == s.charAt(startIndex)) {
18                ++endIndex;
19            }
20            // Check if the current group is a large group (i.e., has a length of 3 or more).
21            if (endIndex - startIndex >= 3) {
22                // Add the start and end indices (end index is exclusive so subtract 1) of the large group to the result.
23                largeGroups.add(Arrays.asList(startIndex, endIndex - 1));
24            }
25            startIndex = endIndex; // Move the startIndex to the end of the current group to start checking the next group.
26        }
27        return largeGroups; // Return the list of all large groups found.
28    }
29  
30}
31
1#include <vector>
2#include <string>
3
4class Solution {
5public:
6    std::vector<std::vector<int>> largeGroupPositions(std::string s) {
7        int length = s.size();          // Get the size of the input string
8        int startIndex = 0;             // Initialize the starting index of a group
9        std::vector<std::vector<int>> largeGroups; // Result vector for storing large group positions
10      
11        // Iterate over the string to identify groups
12        while (startIndex < length) {
13            int endIndex = startIndex;  // Initialize the end index of the group to the current start index
14          
15            // Advance the end index as long as characters match the character at the start index
16            while (endIndex < length && s[endIndex] == s[startIndex]) {
17                ++endIndex;
18            }
19          
20            // Check if the identified group is a large group (3 or more characters)
21            if (endIndex - startIndex >= 3) {
22                // Add the start and end indexes (exclusive of the end) of the large group to the result
23                largeGroups.push_back({startIndex, endIndex - 1});
24            }
25          
26            startIndex = endIndex; // Move the start index to the end of the current group for the next iteration
27        }
28      
29        return largeGroups; // Return the vector of large groups
30    }
31};
32
1function largeGroupPositions(s: string): number[][] {
2    let length: number = s.length; // Get the length of the input string
3    let startIndex: number = 0; // Initialize the starting index of a group
4    let largeGroups: number[][] = []; // Result array for storing large group positions
5
6    // Iterate over the string to identify groups
7    while (startIndex < length) {
8        let endIndex: number = startIndex; // Initialize the end index of the group to the current start index
9
10        // Advance the end index as long as characters match the character at the start index
11        while (endIndex < length && s.charAt(endIndex) === s.charAt(startIndex)) {
12            endIndex++;
13        }
14
15        // Check if the identified group is a large group (3 or more characters)
16        if (endIndex - startIndex >= 3) {
17            // Add the start and end indices (end is exclusive) of the large group to the result
18            largeGroups.push([startIndex, endIndex - 1]);
19        }
20
21        startIndex = endIndex; // Move the start index to the end of the current group for the next iteration
22    }
23
24    return largeGroups; // Return the array of large groups
25}
26
Not Sure What to Study? Take the 2-min Quiz:

In a binary min heap, the maximum element can be found in:

Time and Space Complexity

Time Complexity

The given Python function scans through the string s once. The inner while-loop advances the index j as long as consecutive characters are equal to s[i], and the outer while-loop is responsible for iterating over each character, but thanks to the inner while-loop skipping groups of the same character, every character is visited at most once.

The time complexity for scanning the string is O(n), where n is the length of the string, since in the worst case, we have to look at each character.

Therefore, the overall time complexity is O(n).

Space Complexity

The space complexity is determined by the space needed to store the output, which is the list ans.

In the worst case, if we have a string where every three characters form a group (for example, "aaabbbccc"), the number of groups (and thus the number of sublists in ans) will be approximately n/3. Each group is represented by a pair of indices (which takes constant space), so the space required grows linearly with the number of groups.

Thus, the space complexity is O(n) where n is the length of the string, as the size of the ans list is proportional to the number of large groups in the string.

Learn more about how to find time and space complexity quickly using problem constraints.

Fast Track Your Learning with Our Quick Skills Quiz:

What's the output of running the following function using input 56?

1KEYBOARD = {
2    '2': 'abc',
3    '3': 'def',
4    '4': 'ghi',
5    '5': 'jkl',
6    '6': 'mno',
7    '7': 'pqrs',
8    '8': 'tuv',
9    '9': 'wxyz',
10}
11
12def letter_combinations_of_phone_number(digits):
13    def dfs(path, res):
14        if len(path) == len(digits):
15            res.append(''.join(path))
16            return
17
18        next_number = digits[len(path)]
19        for letter in KEYBOARD[next_number]:
20            path.append(letter)
21            dfs(path, res)
22            path.pop()
23
24    res = []
25    dfs([], res)
26    return res
27
1private static final Map<Character, char[]> KEYBOARD = Map.of(
2    '2', "abc".toCharArray(),
3    '3', "def".toCharArray(),
4    '4', "ghi".toCharArray(),
5    '5', "jkl".toCharArray(),
6    '6', "mno".toCharArray(),
7    '7', "pqrs".toCharArray(),
8    '8', "tuv".toCharArray(),
9    '9', "wxyz".toCharArray()
10);
11
12public static List<String> letterCombinationsOfPhoneNumber(String digits) {
13    List<String> res = new ArrayList<>();
14    dfs(new StringBuilder(), res, digits.toCharArray());
15    return res;
16}
17
18private static void dfs(StringBuilder path, List<String> res, char[] digits) {
19    if (path.length() == digits.length) {
20        res.add(path.toString());
21        return;
22    }
23    char next_digit = digits[path.length()];
24    for (char letter : KEYBOARD.get(next_digit)) {
25        path.append(letter);
26        dfs(path, res, digits);
27        path.deleteCharAt(path.length() - 1);
28    }
29}
30
1const KEYBOARD = {
2    '2': 'abc',
3    '3': 'def',
4    '4': 'ghi',
5    '5': 'jkl',
6    '6': 'mno',
7    '7': 'pqrs',
8    '8': 'tuv',
9    '9': 'wxyz',
10}
11
12function letter_combinations_of_phone_number(digits) {
13    let res = [];
14    dfs(digits, [], res);
15    return res;
16}
17
18function dfs(digits, path, res) {
19    if (path.length === digits.length) {
20        res.push(path.join(''));
21        return;
22    }
23    let next_number = digits.charAt(path.length);
24    for (let letter of KEYBOARD[next_number]) {
25        path.push(letter);
26        dfs(digits, path, res);
27        path.pop();
28    }
29}
30

Recommended Readings


Got a question? Ask the Teaching Assistant anything you don't understand.

Still not clear? Ask in the Forum,  Discord or Submit the part you don't understand to our editors.


TA 👨‍🏫