2296. Design a Text Editor
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:
-
TextEditor()
: Initialize an empty text editor -
addText(string text)
: Insert the giventext
at the current cursor position. After insertion, the cursor moves to the right of the newly added text. -
deleteText(int k)
: Delete up tok
characters to the left of the cursor. Returns the actual number of characters deleted (which may be less thank
if there aren't enough characters). -
cursorLeft(int k)
: Move the cursor left by up tok
positions. Returns the lastmin(10, len)
characters to the left of the cursor after moving, wherelen
is the number of characters to the left of the cursor. -
cursorRight(int k)
: Move the cursor right by up tok
positions. Returns the lastmin(10, len)
characters to the left of the cursor after moving, wherelen
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 cursorright
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.
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 toright
-O(k)
for k moves - Moving cursor right: Pop from
right
and push toleft
-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 cursorself.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)
:
- After
addText("hello")
:left = ['h','e','l','l','o']
,right = []
- After
cursorLeft(3)
:left = ['h','e']
,right = ['o','l','l']
(note the reverse order in right) - 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 EvaluatorExample 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)
wheren
is the length of the input text. Theextend()
operation withlist(text)
iterates through all characters.deleteText(k)
:O(k)
- Pops at mostk
characters from theleft
stack. Each pop operation isO(1)
, so total isO(k)
.cursorLeft(k)
:O(k)
- Transfers at mostk
characters fromleft
toright
stack. Each pop and append operation isO(1)
, plus thejoin
operation on the last 10 characters isO(1)
since it's bounded by a constant.cursorRight(k)
:O(k)
- As confirmed by the reference answer, we pop characters from theright
stack up tok
times and push them onto theleft
stack. Each pop and append operation isO(1)
, and returning the last 10 characters withjoin
isO(1)
.
Space Complexity:
- Overall space complexity:
O(m)
wherem
is the total number of characters stored in the text editor. - The
left
andright
stacks together store all the characters in the editor. - Each method uses
O(1)
additional space exceptaddText
which temporarily creates a list from the input string, usingO(n)
extra space wheren
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
Which of the two traversal algorithms (BFS and DFS) can be used to find whether two nodes are connected?
Recommended Readings
Stack Intro Imagine you have a pile of books on your desk If you want to add a new book you place it on top If you want to read a book you take it from the top And if you simply want to see which book is on the top you
Linked List Cycle Given a linked list with potentially a loop determine whether the linked list from the first node contains a cycle in it For bonus points do this with constant space Parameters nodes The first node of a linked list with potentially a loop Result Whether there is a loop contained
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!