algorithms
string manipulation
complexity optimization
substring removal
computational efficiency

Remove substrings inside a list with better than On2 complexity

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

When working with strings in Python, one may encounter the problem of needing to remove substrings within a list efficiently. The naive approach to solving this task is evaluating if each string in a list occurs in another string in the list, leading to O(n2)O(n^2) time complexity, as each string in the list is compared with every other string. However, this article explores strategies that achieve better than O(n2)O(n^2) complexity.

Understanding the Problem

Assume we have a list of strings, and we aim to remove strings that are substrings of another string within the same list. For example:

python
# Initial list of strings
strings = ["apple", "app", "banana", "ban", "candy", "can"]
# Desired output: ["apple", "banana", "candy"]

In the example above, "app" is a substring of "apple", and "can" is a substring of "candy". These substrings should be removed from the resulting list.

Naive Approach: Nested Loops

The most straightforward approach is to use nested loops:

python
1filtered_list = []
2for i in range(len(strings)):
3    substring_found = False
4    for j in range(len(strings)):
5        if i != j and strings[i] in strings[j]:
6            substring_found = True
7            break
8    if not substring_found:
9        filtered_list.append(strings[i])

The nested loops result in O(n2)O(n^2) complexity, which is inefficient for large lists. Therefore, alternatives with better performance are desirable.

Efficient Solution: Trie-based Approach

A Trie (prefix tree) is a data structure that can efficiently store strings for fast search of substrings. Here’s how to use a Trie to filter substrings efficiently:

  1. Construct a Trie: Insert each string in the list into the Trie.
  2. Search for Non-Substrings: For each string, search in Trie to find if it is not a prefix or an inner node that completely matches another string.
  3. Build the Result: Collect strings that aren’t found as internal node substrings.

Trie Construction

A Trie is built by inserting characters of each string hierarchically:

python
1class TrieNode:
2    def __init__(self):
3        self.children = {}
4        self.is_end_of_word = False
5
6class Trie:
7    def __init__(self):
8        self.root = TrieNode()
9
10    def insert(self, word):
11        current_node = self.root
12        for char in word:
13            if char not in current_node.children:
14                current_node.children[char] = TrieNode()
15            current_node = current_node.children[char]
16        current_node.is_end_of_word = True
17
18    def is_non_substring(self, word):
19        current_node = self.root
20        for char in word:
21            if char not in current_node.children:
22                return True
23            current_node = current_node.children[char]
24        # Ensure it's a complete match and not a prefix
25        return not current_node.is_end_of_word

Complexity and Performance

The Trie-based solution can give a better average-case complexity. Trie insertions and checks are O(m)O(m), where mm is the average length of the strings. Building the Trie for all strings is then O(nm)O(n \cdot m).

Example Implementation

python
1def filter_substrings(strings):
2    trie = Trie()
3    for string in strings:
4        trie.insert(string)
5    
6    filtered_list = [s for s in strings if trie.is_non_substring(s)]
7    return filtered_list
8
9strings = ["apple", "app", "banana", "ban", "candy", "can"]
10result = filter_substrings(strings)
11print(result)  # Output: ["apple", "banana", "candy"]

Alternative Methods

Suffix Array

Suffix arrays are another approach for dealing with substring matching problems. Constructing a suffix array takes O(nlogn)O(n \log n), and searching can be done in O(m)O(m), but it’s more suitable for large texts where preprocessing is amortized over many searches.

Sorting and String Matching

Sort the strings by length (longest first) and use a hash set to track the processed strings. This can optimize some of the operations but is generally less efficient than a Trie.

Summary Table

MethodTime ComplexitySpace ComplexityBest Use Case
Nested LoopsO(n2)O(n^2)O(n)O(n)Small lists, readability over performance
Trie-basedO(nm)O(n \cdot m)O(nm)O(n \cdot m)Moderate-sized lists, fast insert/search
Suffix ArrayO(nlogn)O(n \log n)O(n)O(n)Large strings or texts, less space efficiency
Sorting & HashingDepends (usually faster)O(n)O(n)Small to mid-sized lists, simple implementation

Conclusion

Efficiently removing substrings from a list requires strategic use of data structures. The Trie-based approach allows for scalable handling of medium-sized datasets, significantly improving performance over naive methods. However, the choice of method may depend on specific use case requirements such as dataset size and complexity tolerance.


Related reading
Course
Intermediate
27 lessons
15 hours
DSA Fundamentals

Master algorithmic patterns and data structures through hands-on LeetCode-style problems - from arrays and hashing to dynamic programming and advanced graphs.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

All Rights Reserved.