1410. HTML Entity Parser

MediumHash TableString
Leetcode Link

Problem Description

In this problem, we are asked to implement an HTML entity parser. HTML uses special character sequences known as entities to represent certain characters. Each entity begins with an ampersand (&) and ends with a semicolon (;). In this problem, we are given several predefined entities and their corresponding characters that they represent. For instance, the entity " stands for the double quote character ". Our task is to write a parser that takes an input string containing these entities and replaces them with their associated characters. The entities and their corresponding characters we need to handle are:

  • Quotation Mark: ""
  • Single Quote Mark: ''
  • Ampersand: &&
  • Greater Than Sign: >>
  • Less Than Sign: &lt;<
  • Slash: &frasl;/

The challenge is to correctly parse the input string and make the substitutions wherever an entity is found so that the output string is the equivalent string with special characters.

Intuition

To solve this problem, we can take a string matching approach. We know that entities are fixed patterns, and we can map each of these patterns to their corresponding characters. The solution involves scanning the input string and checking for the presence of an entity. When an entity is detected, it is replaced with the actual character it represents.

Our solution maintains a dictionary (hash map) d where the keys are the entities we want to check for (&quot;, &apos;, etc.) and the values are the characters those entities represent (", ', etc.). As we scan the input string text, we use a loop to extract a substring starting from the current position i and check if it matches any entity. We examine possible substrings up to a length of 7 (as the longest entity &frasl; has a length of 7 characters including & and ;).

Here’s the step-by-step approach we use in the solution code:

  1. Initialize a pointer i to 0 and an empty list ans to store the result characters.
  2. Iterate over the input string text while i is less than the length of text.
  3. For each position i, check substrings of length 1 to 7, to see if any of them match an entity in dictionary d.
  4. If a match is found, append the corresponding character to ans, and update i to skip the matched entity.
  5. If no matching entity is found, append the current character to ans and increment i by 1 to move to the next character.
  6. Continue this process until the end of the input string is reached.
  7. Finally, join all the characters in ans into a single string and return that as the result.

The key part of this solution is the use of a dictionary for efficient entity look-up and the careful iteration over potential entity substrings to perform the necessary replacements.

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

What is an advantages of top-down dynamic programming vs bottom-up dynamic programming?

Solution Approach

The implementation of the solution follows a straightforward pattern matching and substitution approach leveraging a dictionary for fast lookups. Below is an in-depth walk-through of how the algorithm, data structures, and patterns are used in the provided solution code:

  1. Dictionary for Entity Mapping: A dictionary d is initialized to store the mapping of HTML entities to their corresponding characters. The use of a dictionary (hash map) allows for constant-time look-up which is efficient for our substitution needs.

    1d = {
    2    '&quot;': '"',
    3    '&apos;': "'",
    4    '&amp;': "&",
    5    "&gt;": '>',
    6    "&lt;": '<',
    7    "&frasl;": '/',
    8}
  2. Scanning the Input String: Two pointers are initiated, i to iterate through the string, and j to check for entities within a window that slides forward from i.

    • i starts at 0 and moves through the string character by character.
    • n is the length of the input string text.
  3. Checking for Entities: Within the while loop, which continues until i is less than n, the code uses a nested loop to check for entities of varying lengths (from 1 to 7).

    1while i < n:
    2    for l in range(1, 8):
    3        j = i + l
    4        if text[i:j] in d:
  4. Appending Replaced Characters or Original Characters: When a matching entity is found within the current window of the string (signified by the range (i, j)), the corresponding character is appended to the ans list and i is updated to be j to continue scanning after the entity. If no entity is found, the current character at position i is appended instead and i is incremented by one.

    1ans.append(d[text[i:j]])
    2i = j

    or if no match is found:

    1ans.append(text[i])
    2i += 1
  5. Joining and Returning the Result: After the entire string has been scanned and entities have been replaced, the ans list is joined to form a string which is returned as the final output.

    1return ''.join(ans)

The algorithm effectively does the following:

  • It processes the input string character by character, but also examines potential entity matches by looking ahead up to 7 characters.
  • It uses a dictionary for efficient mapping from entities to characters.
  • It maintains the order of characters within the input string, only replacing entities when found.
  • It compiles the output dynamically, making it responsive to any length of the input text without the need for pre-sizing.

This approach ensures that all entities are replaced appropriately, and the string is processed in a single pass, which offers a good balance of time complexity (linear in relation to the length of the text) and space complexity (constant for the dictionary plus linear for the output string).

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

What's the relationship between a tree and a graph?

Example Walkthrough

Let's go through the solution approach with a small example.

Consider the input string text = "Hello &amp; World &gt; Universe &frasl; Spaces". In this example, we have HTML entities that correspond to an ampersand, a greater-than sign, and a forward slash, which need to be replaced with their respective characters &, >, and /.

Using our solution approach:

  1. We initialize our dictionary d that maps entities to their corresponding characters.

  2. We set i = 0, which will be used to iterate over text, and ans, an empty list to store the result characters.

  3. Start with the while loop: while i < len(text).

  4. Now, looking at the first characters starting from index 0, we see "Hello ". Since none of these characters match an entity, they are appended to ans.

  5. When we reach &amp;, our loop checks substring lengths from 1 to 7 characters starting with i. It finds that the substring "&" matches an entity in d.

    • We append & to ans.
    • Set i to the index after the matched entity.
  6. We repeat this process for the rest of the string.

    • " World " is added directly to ans.
    • &gt; translates to >, so > is added to ans.
    • " Universe " is added directly to ans.
    • &frasl; translates to /, so / is added to ans.
    • The rest of the string " Spaces" is added directly to ans.
  7. After the entire string is processed, we join the list ans into a single string:

    1"Hello & World > Universe / Spaces"

And that is the expected output, which we return.

This walkthrough demonstrates how the algorithm scans the input string, checks for entities, appends the corresponding characters or the original ones, and ultimately joins them together for the result.

Solution Implementation

1class Solution:
2    def entityParser(self, text: str) -> str:
3        # Dictionary to map HTML entities to their corresponding characters
4        entity_to_char = {
5            '&quot;': '"',
6            '&apos;': "'",
7            '&amp;': '&',
8            '&gt;': '>',
9            '&lt;': '<',
10            '&frasl;': '/',
11        }
12      
13        i, text_length = 0, len(text)  # Initialize pointer i and get the length of the text
14        parsed_text = []  # List to hold the parsed characters
15      
16        # Loop through each character in the text
17        while i < text_length:
18            # Check for entities with a maximum length of 7 characters
19            for length in range(1, 8):
20                end_index = i + length
21                # If a substring matches a key in the dictionary, append the corresponding character
22                if text[i:end_index] in entity_to_char:
23                    parsed_text.append(entity_to_char[text[i:end_index]])
24                    i = end_index  # Move the pointer past the entity
25                    break
26            else:
27                # If no entity is found, append the current character
28                parsed_text.append(text[i])
29                i += 1  # Move the pointer to the next character
30      
31        # Join all the parsed characters to form the final string
32        return ''.join(parsed_text)
33
1import java.util.Map;
2import java.util.HashMap;
3
4class Solution {
5    public String entityParser(String text) {
6        // Create a map for the HTML entity to its corresponding character.
7        Map<String, String> htmlEntityMap = new HashMap<>();
8        htmlEntityMap.put("&quot;", "\"");
9        htmlEntityMap.put("&apos;", "'");
10        htmlEntityMap.put("&amp;", "&");
11        htmlEntityMap.put("&gt;", ">");
12        htmlEntityMap.put("&lt;", "<");
13        htmlEntityMap.put("&frasl;", "/");
14      
15        // StringBuilder to hold the final parsed string.
16        StringBuilder parsedString = new StringBuilder();
17      
18        // Variable to track the current index in the input string.
19        int currentIndex = 0;
20      
21        // Length of the input text.
22        int textLength = text.length();
23      
24        // Iterate over the input text to find and replace entities.
25        while (currentIndex < textLength) {
26            // Flag to mark if we found an entity match.
27            boolean isEntityFound = false;
28            // Try all possible lengths for an entity (entities are between 1 and 7 characters long).
29            for (int length = 1; length <= 7 && currentIndex + length <= textLength; ++length) {
30                // Extract a substring of the current length from the current index.
31                String currentSubstring = text.substring(currentIndex, currentIndex + length);
32                // Check if the current substring is a known entity.
33                if (htmlEntityMap.containsKey(currentSubstring)) {
34                    // If it's an entity, append the corresponding character to parsedString.
35                    parsedString.append(htmlEntityMap.get(currentSubstring));
36                    // Move the current index forward past the entity.
37                    currentIndex += length;
38                    // Indicate that we found and handled an entity.
39                    isEntityFound = true;
40                    // Break out of the loop as we only want to match the longest possible entity.
41                    break;
42                }
43            }
44            // If no entity was found, append the current character to parsedString and move to the next character.
45            if (!isEntityFound) {
46                parsedString.append(text.charAt(currentIndex++));
47            }
48        }
49        // Return the fully parsed string with all entities replaced.
50        return parsedString.toString();
51    }
52}
53
1#include <string>
2#include <unordered_map>
3using namespace std;
4
5class Solution {
6public:
7    // This function converts HTML entities in a string to their corresponding characters.
8    string entityParser(const string& text) {
9        // Dictionary to map HTML entities to their respective characters.
10        unordered_map<string, string> htmlEntityToChar = {
11            {"&quot;", "\""},
12            {"&apos;", "'"},
13            {"&amp;", "&"},
14            {"&gt;", ">"},
15            {"&lt;", "<"},
16            {"&frasl;", "/"}
17        };
18
19        // The resulting string after parsing HTML entities.
20        string parsedText = "";
21
22        // Loop through each character of the input text.
23        int index = 0;
24        int textLength = text.size();
25        while (index < textLength) {
26            bool entityFound = false; // Flag to indicate if an HTML entity is found.
27
28            // Try to match the longest possible HTML entity by checking substrings of lengths 1 to 7.
29            for (int length = 1; length <= 7; ++length) {
30                int endIndex = index + length; // Calculate the end index of the current substring.
31
32                // Check if the substring is within the text boundaries.
33                if (endIndex <= textLength) {
34                    string currentSubstring = text.substr(index, length);
35
36                    // If the current substring is an HTML entity, append the respective character.
37                    if (htmlEntityToChar.count(currentSubstring)) {
38                        parsedText += htmlEntityToChar[currentSubstring];
39                        index = endIndex; // Move the index past the HTML entity.
40                        entityFound = true; // Set the flag.
41                        break; // Break out of for-loop once an HTML entity is found and handled.
42                    }
43                }
44            }
45          
46            // If no HTML entity is found, append the current character to the result.
47            if (!entityFound) {
48                parsedText += text[index++];
49            }
50        }
51
52        return parsedText;
53    }
54};
55
1// TypeScript type definition for a mapping from HTML entities to characters.
2type EntityToCharMap = { [entity: string]: string };
3
4// A dictionary to map HTML entities to their respective characters.
5const htmlEntityToChar: EntityToCharMap = {
6  "&quot;": '"',
7  "&apos;": "'",
8  "&amp;": "&",
9  "&gt;": ">",
10  "&lt;": "<",
11  "&frasl;": "/"
12};
13
14// This function converts HTML entities in a string to their corresponding characters.
15function entityParser(text: string): string {
16  // The resulting string after parsing HTML entities.
17  let parsedText = "";
18
19  // Loop through each character of the input text.
20  let index = 0;
21  const textLength = text.length;
22
23  while (index < textLength) {
24    let entityFound = false; // Flag to indicate if an HTML entity is found.
25
26    // Try to match the longest possible HTML entity by checking substrings of lengths 1 to 7.
27    for (let length = 1; length <= 7; ++length) {
28      const endIndex = index + length; // Calculate the end index of the current substring.
29
30      // Check if the substring is within the text boundaries.
31      if (endIndex <= textLength) {
32        const currentSubstring = text.substring(index, endIndex);
33
34        // If the current substring is an HTML entity, append the respective character.
35        if (currentSubstring in htmlEntityToChar) {
36          parsedText += htmlEntityToChar[currentSubstring];
37          index = endIndex; // Move the index past the HTML entity.
38          entityFound = true; // Set the flag.
39          break; // Break out of for-loop once an HTML entity is found and handled.
40        }
41      }
42    }
43
44    // If no HTML entity is found, append the current character to the result.
45    if (!entityFound) {
46      parsedText += text[index++];
47    }
48  }
49
50  return parsedText;
51}
52
53// Example usage:
54// const originalText = "Hello &amp; welcome to &lt;coding&gt;!";
55// const convertedText = entityParser(originalText);
56// console.log(convertedText); // Output should be "Hello & welcome to <coding>!"
57
Not Sure What to Study? Take the 2-min Quiz:

What is the space complexity of the following code?

1int sum(int n) {
2  if (n <= 0) {
3    return 0;
4  }
5  return n + sum(n - 1);
6}

Time and Space Complexity

The time complexity of the algorithm is O(n), where n is the length of the input string text. This is because, in the worst case, each character in the string is visited once. The check text[i:j] in d is O(1) because dictionary lookups in Python are constant time, and the loop for range l doesn't significantly affect the time complexity as it's bounded (maximum length of the strings in the dictionary d is 7, which is a constant).

The space complexity of the code is O(n) as well, where n is the length of the text. This is because a list ans is being used to construct the output, and it can grow up to the length of the input string in the case where no entity is replaced.

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 [30, 20, 10, 100, 33, 12]?

1def fun(arr: List[int]) -> List[int]:
2    import heapq
3    heapq.heapify(arr)
4    res = []
5    for i in range(3):
6        res.append(heapq.heappop(arr))
7    return res
8
1public static int[] fun(int[] arr) {
2    int[] res = new int[3];
3    PriorityQueue<Integer> heap = new PriorityQueue<>();
4    for (int i = 0; i < arr.length; i++) {
5        heap.add(arr[i]);
6    }
7    for (int i = 0; i < 3; i++) {
8        res[i] = heap.poll();
9    }
10    return res;
11}
12
1class HeapItem {
2    constructor(item, priority = item) {
3        this.item = item;
4        this.priority = priority;
5    }
6}
7
8class MinHeap {
9    constructor() {
10        this.heap = [];
11    }
12
13    push(node) {
14        // insert the new node at the end of the heap array
15        this.heap.push(node);
16        // find the correct position for the new node
17        this.bubble_up();
18    }
19
20    bubble_up() {
21        let index = this.heap.length - 1;
22
23        while (index > 0) {
24            const element = this.heap[index];
25            const parentIndex = Math.floor((index - 1) / 2);
26            const parent = this.heap[parentIndex];
27
28            if (parent.priority <= element.priority) break;
29            // if the parent is bigger than the child then swap the parent and child
30            this.heap[index] = parent;
31            this.heap[parentIndex] = element;
32            index = parentIndex;
33        }
34    }
35
36    pop() {
37        const min = this.heap[0];
38        this.heap[0] = this.heap[this.size() - 1];
39        this.heap.pop();
40        this.bubble_down();
41        return min;
42    }
43
44    bubble_down() {
45        let index = 0;
46        let min = index;
47        const n = this.heap.length;
48
49        while (index < n) {
50            const left = 2 * index + 1;
51            const right = left + 1;
52
53            if (left < n && this.heap[left].priority < this.heap[min].priority) {
54                min = left;
55            }
56            if (right < n && this.heap[right].priority < this.heap[min].priority) {
57                min = right;
58            }
59            if (min === index) break;
60            [this.heap[min], this.heap[index]] = [this.heap[index], this.heap[min]];
61            index = min;
62        }
63    }
64
65    peek() {
66        return this.heap[0];
67    }
68
69    size() {
70        return this.heap.length;
71    }
72}
73
74function fun(arr) {
75    const heap = new MinHeap();
76    for (const x of arr) {
77        heap.push(new HeapItem(x));
78    }
79    const res = [];
80    for (let i = 0; i < 3; i++) {
81        res.push(heap.pop().item);
82    }
83    return res;
84}
85

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 👨‍🏫