Facebook Pixel

2296. Design a Text Editor

HardStackDesignLinked ListStringDoubly-Linked ListSimulation
Leetcode Link

Problem Description

You need to design a text editor that simulates a cursor's behavior with the following operations:

Core Functionality:

  • Add text at the cursor position
  • Delete text to the left of the cursor (like pressing backspace)
  • Move cursor left or right within the text

Key Constraints:

  • The cursor position is always valid: 0 <= cursor.position <= currentText.length
  • Deletion only affects characters to the left of the cursor
  • The cursor cannot move beyond the text boundaries

Class Methods to Implement:

  1. TextEditor(): Initialize an empty text editor

  2. addText(string text): Insert the given text at the current cursor position. After insertion, the cursor moves to the right of the newly added text.

  3. deleteText(int k): Delete up to k characters to the left of the cursor. Returns the actual number of characters deleted (which may be less than k if there aren't enough characters).

  4. cursorLeft(int k): Move the cursor left by up to k positions. Returns the last min(10, len) characters to the left of the cursor after moving, where len is the number of characters to the left of the cursor.

  5. cursorRight(int k): Move the cursor right by up to k positions. Returns the last min(10, len) characters to the left of the cursor after moving, where len is the number of characters to the left of the cursor.

Solution Strategy:

The solution uses two stacks to efficiently manage text on both sides of the cursor:

  • left stack: stores characters to the left of the cursor
  • right stack: stores characters to the right of the cursor

This design makes cursor movements and text operations efficient by simply transferring elements between stacks. When moving left, characters pop from the left stack and push to the right stack. When moving right, the opposite occurs. Text addition pushes to the left stack, and deletion pops from the left stack.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

Think about how a cursor works in a real text editor - it divides the text into two parts: everything before the cursor and everything after the cursor. When you type, new characters appear right where the cursor is, pushing everything after the cursor to the right. When you press backspace, you delete what's immediately before the cursor.

This natural division suggests we can treat the text as two separate segments. But how should we store these segments? If we use simple strings or arrays, moving the cursor would require shifting many elements. For example, moving the cursor left means taking characters from the "before" segment and moving them to the "after" segment.

The key insight is recognizing that cursor movements follow a Last-In-First-Out pattern. When you move the cursor left, the last character before the cursor becomes the first character after the cursor. When you move right, the first character after the cursor becomes the last character before the cursor. This LIFO behavior is exactly what stacks provide!

By using two stacks:

  • The left stack naturally maintains characters before the cursor, with the top being the character immediately to the left
  • The right stack maintains characters after the cursor, with the top being the character immediately to the right

This makes all operations elegant:

  • Adding text: Just push to the left stack - O(n) where n is the text length
  • Deleting: Simply pop from the left stack - O(k) for k deletions
  • Moving cursor left: Pop from left and push to right - O(k) for k moves
  • Moving cursor right: Pop from right and push to left - O(k) for k moves

The beauty of this approach is that the cursor position is implicitly defined by the boundary between the two stacks, eliminating the need to track it explicitly or shift elements when it moves.

Learn more about Stack and Linked List patterns.

Solution Approach

The implementation uses two stacks to represent the text editor state:

  • self.left: Stack containing characters to the left of the cursor
  • self.right: Stack containing characters to the right of the cursor

Let's walk through each method implementation:

1. __init__()

def __init__(self):
    self.left = []
    self.right = []

Initialize two empty lists that will serve as our stacks. The cursor starts at position 0, meaning all text (initially none) is to the right of the cursor.

2. addText(text)

def addText(self, text: str) -> None:
    self.left.extend(list(text))

Add text at the cursor position by pushing all characters onto the left stack. Using extend() with list(text) efficiently adds all characters at once. After this operation, the cursor is positioned after the newly added text.

Time Complexity: O(|text|) where |text| is the length of the input string.

3. deleteText(k)

def deleteText(self, k: int) -> int:
    k = min(k, len(self.left))
    for _ in range(k):
        self.left.pop()
    return k

Delete up to k characters to the left of the cursor. First, we calculate the actual number of characters to delete using min(k, len(self.left)) to handle cases where k exceeds available characters. Then pop from the left stack k times and return the actual deletion count.

Time Complexity: O(k) for k deletions.

4. cursorLeft(k)

def cursorLeft(self, k: int) -> str:
    k = min(k, len(self.left))
    for _ in range(k):
        self.right.append(self.left.pop())
    return ''.join(self.left[-10:])

Move the cursor left by transferring characters from the left stack to the right stack. Each character popped from left becomes the new top of right, maintaining the correct order. After moving, return the last 10 characters (or fewer if less than 10 exist) to the left of the cursor using slice notation self.left[-10:].

Time Complexity: O(k) for moving the cursor plus O(min(10, len(self.left))) for building the return string.

5. cursorRight(k)

def cursorRight(self, k: int) -> str:
    k = min(k, len(self.right))
    for _ in range(k):
        self.left.append(self.right.pop())
    return ''.join(self.left[-10:])

Move the cursor right by transferring characters from the right stack to the left stack. This is the reverse of cursorLeft. Characters move from right to left, effectively moving the cursor rightward. Return the last 10 characters to the left of the new cursor position.

Time Complexity: O(k) for moving the cursor plus O(min(10, len(self.left))) for building the return string.

Example Walkthrough

Consider the sequence: addText("hello"), cursorLeft(3), deleteText(2):

  1. After addText("hello"): left = ['h','e','l','l','o'], right = []
  2. After cursorLeft(3): left = ['h','e'], right = ['o','l','l'] (note the reverse order in right)
  3. After deleteText(2): left = [], right = ['o','l','l']

The final text would be "llo" with the cursor at position 0.

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's trace through a sequence of operations to see how the two-stack approach works:

Initial state: Empty editor

  • left = [], right = []
  • Text: "" (cursor at position 0)

Step 1: addText("abc")

  • Push 'a', 'b', 'c' to left stack
  • left = ['a','b','c'], right = []
  • Text: "abc|" (cursor at position 3)

Step 2: cursorLeft(2)

  • Pop 'c' from left, push to right
  • Pop 'b' from left, push to right
  • left = ['a'], right = ['c','b']
  • Text: "a|bc" (cursor at position 1)
  • Returns: "a" (last 10 chars to the left)

Step 3: addText("xy")

  • Push 'x', 'y' to left stack
  • left = ['a','x','y'], right = ['c','b']
  • Text: "axy|bc" (cursor at position 3)

Step 4: deleteText(1)

  • Pop 'y' from left
  • left = ['a','x'], right = ['c','b']
  • Text: "ax|bc" (cursor at position 2)
  • Returns: 1 (deleted 1 character)

Step 5: cursorRight(1)

  • Pop 'c' from right, push to left
  • left = ['a','x','c'], right = ['b']
  • Text: "axc|b" (cursor at position 3)
  • Returns: "axc" (last 10 chars to the left)

Notice how the cursor position is always the boundary between the two stacks. Characters to the left are in order in the left stack, while characters to the right are in reverse order in the right stack (top element is closest to cursor).

Solution Implementation

1class TextEditor:
2    def __init__(self) -> None:
3        """
4        Initialize the text editor with two stacks to represent text on both sides of cursor.
5        The cursor position is between the left and right stacks.
6        """
7        self.left = []   # Stack for characters to the left of cursor
8        self.right = []  # Stack for characters to the right of cursor
9
10    def addText(self, text: str) -> None:
11        """
12        Add text at the current cursor position.
13      
14        Args:
15            text: String to be added at cursor position
16        """
17        # Convert string to list of characters and add to left of cursor
18        self.left.extend(list(text))
19
20    def deleteText(self, k: int) -> int:
21        """
22        Delete at most k characters to the left of cursor.
23      
24        Args:
25            k: Maximum number of characters to delete
26          
27        Returns:
28            The actual number of characters deleted
29        """
30        # Delete min(k, available characters) from left of cursor
31        k = min(k, len(self.left))
32        for _ in range(k):
33            self.left.pop()
34        return k
35
36    def cursorLeft(self, k: int) -> str:
37        """
38        Move cursor k positions to the left.
39      
40        Args:
41            k: Number of positions to move left
42          
43        Returns:
44            String of up to 10 characters to the left of cursor after movement
45        """
46        # Move min(k, available characters) from left stack to right stack
47        k = min(k, len(self.left))
48        for _ in range(k):
49            self.right.append(self.left.pop())
50      
51        # Return last 10 characters to the left of cursor
52        return ''.join(self.left[-10:])
53
54    def cursorRight(self, k: int) -> str:
55        """
56        Move cursor k positions to the right.
57      
58        Args:
59            k: Number of positions to move right
60          
61        Returns:
62            String of up to 10 characters to the left of cursor after movement
63        """
64        # Move min(k, available characters) from right stack to left stack
65        k = min(k, len(self.right))
66        for _ in range(k):
67            self.left.append(self.right.pop())
68      
69        # Return last 10 characters to the left of cursor
70        return ''.join(self.left[-10:])
71
72
73# Your TextEditor object will be instantiated and called as such:
74# obj = TextEditor()
75# obj.addText(text)
76# param_2 = obj.deleteText(k)
77# param_3 = obj.cursorLeft(k)
78# param_4 = obj.cursorRight(k)
79
1class TextEditor {
2    // StringBuilder to store text to the left of the cursor
3    private StringBuilder left = new StringBuilder();
4    // StringBuilder to store text to the right of the cursor (in reverse order)
5    private StringBuilder right = new StringBuilder();
6
7    /**
8     * Constructor to initialize the text editor
9     */
10    public TextEditor() {
11    }
12
13    /**
14     * Adds text at the current cursor position
15     * @param text The text to be added
16     */
17    public void addText(String text) {
18        left.append(text);
19    }
20
21    /**
22     * Deletes at most k characters to the left of the cursor
23     * @param k The maximum number of characters to delete
24     * @return The actual number of characters deleted
25     */
26    public int deleteText(int k) {
27        // Ensure we don't delete more characters than available
28        k = Math.min(k, left.length());
29        // Remove k characters from the end of left StringBuilder
30        left.setLength(left.length() - k);
31        return k;
32    }
33
34    /**
35     * Moves the cursor k positions to the left
36     * @param k The number of positions to move left
37     * @return The last 10 characters (or less) to the left of the cursor after moving
38     */
39    public String cursorLeft(int k) {
40        // Ensure we don't move beyond the available text
41        k = Math.min(k, left.length());
42      
43        // Move k characters from left to right (right stores in reverse)
44        for (int i = 0; i < k; i++) {
45            right.append(left.charAt(left.length() - 1));
46            left.deleteCharAt(left.length() - 1);
47        }
48      
49        // Return at most 10 characters to the left of cursor
50        return left.substring(Math.max(left.length() - 10, 0));
51    }
52
53    /**
54     * Moves the cursor k positions to the right
55     * @param k The number of positions to move right
56     * @return The last 10 characters (or less) to the left of the cursor after moving
57     */
58    public String cursorRight(int k) {
59        // Ensure we don't move beyond the available text
60        k = Math.min(k, right.length());
61      
62        // Move k characters from right to left (right stores in reverse)
63        for (int i = 0; i < k; i++) {
64            left.append(right.charAt(right.length() - 1));
65            right.deleteCharAt(right.length() - 1);
66        }
67      
68        // Return at most 10 characters to the left of cursor
69        return left.substring(Math.max(left.length() - 10, 0));
70    }
71}
72
73/**
74 * Your TextEditor object will be instantiated and called as such:
75 * TextEditor obj = new TextEditor();
76 * obj.addText(text);
77 * int param_2 = obj.deleteText(k);
78 * String param_3 = obj.cursorLeft(k);
79 * String param_4 = obj.cursorRight(k);
80 */
81
1class TextEditor {
2public:
3    // Constructor initializes an empty text editor
4    TextEditor() {
5        // Text is split into two parts at cursor position:
6        // - left: text before cursor
7        // - right: text after cursor (stored in reverse order)
8    }
9
10    // Add text at the current cursor position
11    void addText(string text) {
12        // Append the new text to the left of cursor
13        textBeforeCursor += text;
14    }
15
16    // Delete at most k characters to the left of cursor
17    // Returns the actual number of characters deleted
18    int deleteText(int k) {
19        // Can't delete more characters than available
20        int charactersToDelete = min(k, static_cast<int>(textBeforeCursor.size()));
21      
22        // Remove characters from the end of left string
23        textBeforeCursor.resize(textBeforeCursor.size() - charactersToDelete);
24      
25        return charactersToDelete;
26    }
27
28    // Move cursor k positions to the left
29    // Returns at most 10 characters to the left of the new cursor position
30    string cursorLeft(int k) {
31        // Can't move beyond the beginning of text
32        int positionsToMove = min(k, static_cast<int>(textBeforeCursor.size()));
33      
34        // Move characters from left to right (right stores in reverse)
35        while (positionsToMove--) {
36            textAfterCursor += textBeforeCursor.back();
37            textBeforeCursor.pop_back();
38        }
39      
40        // Return at most 10 characters before cursor
41        int startIndex = max(0, static_cast<int>(textBeforeCursor.size()) - 10);
42        return textBeforeCursor.substr(startIndex);
43    }
44
45    // Move cursor k positions to the right
46    // Returns at most 10 characters to the left of the new cursor position
47    string cursorRight(int k) {
48        // Can't move beyond the end of text
49        int positionsToMove = min(k, static_cast<int>(textAfterCursor.size()));
50      
51        // Move characters from right to left (right is stored in reverse)
52        while (positionsToMove--) {
53            textBeforeCursor += textAfterCursor.back();
54            textAfterCursor.pop_back();
55        }
56      
57        // Return at most 10 characters before cursor
58        int startIndex = max(0, static_cast<int>(textBeforeCursor.size()) - 10);
59        return textBeforeCursor.substr(startIndex);
60    }
61
62private:
63    string textBeforeCursor;  // Stores text before the cursor position
64    string textAfterCursor;   // Stores text after the cursor position (in reverse order for O(1) operations)
65};
66
67/**
68 * Your TextEditor object will be instantiated and called as such:
69 * TextEditor* obj = new TextEditor();
70 * obj->addText(text);
71 * int param_2 = obj->deleteText(k);
72 * string param_3 = obj->cursorLeft(k);
73 * string param_4 = obj->cursorRight(k);
74 */
75
1// Two stacks to maintain text on left and right of cursor
2// left stack: characters before cursor position
3// right stack: characters after cursor position (stored in reverse order)
4let left: string[] = [];
5let right: string[] = [];
6
7/**
8 * Adds text at the current cursor position
9 * @param text - The text string to be added
10 */
11function addText(text: string): void {
12    // Split text into characters and push each to left stack
13    for (const char of text) {
14        left.push(char);
15    }
16}
17
18/**
19 * Deletes k characters to the left of cursor
20 * @param k - Number of characters to delete
21 * @returns The actual number of characters deleted
22 */
23function deleteText(k: number): number {
24    // Calculate actual characters to delete (cannot exceed left stack size)
25    const charactersToDelete = Math.min(k, left.length);
26  
27    // Remove characters from left stack
28    for (let i = 0; i < charactersToDelete; i++) {
29        left.pop();
30    }
31  
32    return charactersToDelete;
33}
34
35/**
36 * Moves cursor k positions to the left
37 * @param k - Number of positions to move left
38 * @returns The last 10 characters to the left of cursor after movement
39 */
40function cursorLeft(k: number): string {
41    // Calculate actual positions to move (cannot exceed left stack size)
42    const positionsToMove = Math.min(k, left.length);
43  
44    // Move characters from left stack to right stack
45    for (let i = 0; i < positionsToMove; i++) {
46        const character = left.pop()!;
47        right.push(character);
48    }
49  
50    // Return last 10 characters from left stack
51    return left.slice(-10).join('');
52}
53
54/**
55 * Moves cursor k positions to the right
56 * @param k - Number of positions to move right
57 * @returns The last 10 characters to the left of cursor after movement
58 */
59function cursorRight(k: number): string {
60    // Calculate actual positions to move (cannot exceed right stack size)
61    const positionsToMove = Math.min(k, right.length);
62  
63    // Move characters from right stack to left stack
64    for (let i = 0; i < positionsToMove; i++) {
65        const character = right.pop()!;
66        left.push(character);
67    }
68  
69    // Return last 10 characters from left stack
70    return left.slice(-10).join('');
71}
72

Time and Space Complexity

Time Complexity:

  • __init__(): O(1) - Simply initializes two empty lists.
  • addText(text): O(n) where n is the length of the input text. The extend() operation with list(text) iterates through all characters.
  • deleteText(k): O(k) - Pops at most k characters from the left stack. Each pop operation is O(1), so total is O(k).
  • cursorLeft(k): O(k) - Transfers at most k characters from left to right stack. Each pop and append operation is O(1), plus the join operation on the last 10 characters is O(1) since it's bounded by a constant.
  • cursorRight(k): O(k) - As confirmed by the reference answer, we pop characters from the right stack up to k times and push them onto the left stack. Each pop and append operation is O(1), and returning the last 10 characters with join is O(1).

Space Complexity:

  • Overall space complexity: O(m) where m is the total number of characters stored in the text editor.
  • The left and right stacks together store all the characters in the editor.
  • Each method uses O(1) additional space except addText which temporarily creates a list from the input string, using O(n) extra space where n is the length of the input text.

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

Common Pitfalls

1. Incorrect Order When Moving Cursor Right

A common mistake is forgetting that the right stack stores characters in reverse order. When moving the cursor right, characters must be popped from right and pushed to left, not the other way around.

Incorrect Implementation:

def cursorRight(self, k: int) -> str:
    k = min(k, len(self.right))
    for _ in range(k):
        # Wrong: This would reverse the text order
        self.left.extend(self.right[:k])  
        del self.right[:k]
    return ''.join(self.left[-10:])

Correct Implementation:

def cursorRight(self, k: int) -> str:
    k = min(k, len(self.right))
    for _ in range(k):
        # Correct: Pop from right and append to left one by one
        self.left.append(self.right.pop())
    return ''.join(self.left[-10:])

2. Inefficient Character-by-Character Addition

When implementing addText, developers might add characters one by one, which is less efficient than bulk operations.

Inefficient Implementation:

def addText(self, text: str) -> None:
    for char in text:
        self.left.append(char)  # O(n) iterations with function call overhead

Efficient Implementation:

def addText(self, text: str) -> None:
    self.left.extend(list(text))  # Single operation, more efficient

3. Forgetting to Cap Movement/Deletion Operations

A critical mistake is not limiting operations to available characters, which can cause index errors or incorrect behavior.

Problematic Implementation:

def deleteText(self, k: int) -> int:
    # This will crash if k > len(self.left)
    for _ in range(k):
        self.left.pop()
    return k

Correct Implementation:

def deleteText(self, k: int) -> int:
    k = min(k, len(self.left))  # Cap k to available characters
    for _ in range(k):
        self.left.pop()
    return k

4. Returning Wrong Slice for Display

The methods cursorLeft and cursorRight should return the last 10 characters to the left of the cursor, not the first 10.

Wrong Implementation:

def cursorLeft(self, k: int) -> str:
    k = min(k, len(self.left))
    for _ in range(k):
        self.right.append(self.left.pop())
    return ''.join(self.left[:10])  # Wrong: Returns first 10, not last 10

Correct Implementation:

def cursorLeft(self, k: int) -> str:
    k = min(k, len(self.left))
    for _ in range(k):
        self.right.append(self.left.pop())
    return ''.join(self.left[-10:])  # Correct: Returns last 10 characters

5. Memory Inefficiency with String Concatenation

Building the return string inefficiently can impact performance, especially if done repeatedly.

Less Efficient:

def cursorLeft(self, k: int) -> str:
    # ... cursor movement code ...
    result = ""
    for char in self.left[-10:]:
        result += char  # String concatenation creates new objects
    return result

More Efficient:

def cursorLeft(self, k: int) -> str:
    # ... cursor movement code ...
    return ''.join(self.left[-10:])  # Single join operation
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which of the two traversal algorithms (BFS and DFS) can be used to find whether two nodes are connected?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More