Design a Text Editor
HardPrompt
Design a text editor with a cursor that can do the following:
- Add text to where the cursor is.
- Delete text from where the cursor is (simulating the backspace key).
- Move the cursor either left or right.
When deleting text, only characters to the left of the cursor will be deleted. The cursor will also remain within the actual text and cannot be moved beyond it. More formally, we have that 0 <= cursor.position <= currentText.length always holds.
Implement the TextEditor class:
TextEditor()Initializes the object with empty text.void addText(string text)Appendstextto where the cursor is. The cursor ends to the right oftext.int deleteText(int k)Deleteskcharacters to the left of the cursor. Returns the number of characters actually deleted.string cursorLeft(int k)Moves the cursor to the leftktimes. Returns the lastmin(10, len)characters to the left of the cursor, wherelenis the number of characters to the left of the cursor.string cursorRight(int k)Moves the cursor to the rightktimes. Returns the lastmin(10, len)characters to the left of the cursor, wherelenis the number of characters to the left of the cursor.
Example 1:
Input
["TextEditor", "addText", "deleteText", "addText", "cursorRight", "cursorLeft", "deleteText", "cursorLeft", "cursorRight"]
[[], ["leetcode"], [4], ["practice"], [3], [8], [10], [2], [6]]
Output
[null, null, 4, null, "etpractice", "leet", 4, "", "practi"]
Explanation
TextEditor textEditor = new TextEditor(); // The current text is "|". (The '|' character represents the cursor)
textEditor.addText("leetcode"); // The current text is "leetcode|".
textEditor.deleteText(4); // return 4
// The current text is "leet|".
// 4 characters were deleted.
textEditor.addText("practice"); // The current text is "leetpractice|".
textEditor.cursorRight(3); // return "etpractice"
// The current text is "leetpractice|".
// The cursor cannot be moved beyond the actual text and thus did not move.
// "etpractice" is the last 10 characters to the left of the cursor.
textEditor.cursorLeft(8); // return "leet"
// The current text is "leet|practice".
// "leet" is the last min(10, 4) = 4 characters to the left of the cursor.
textEditor.deleteText(10); // return 4
// The current text is "|practice".
// Only 4 characters were deleted.
textEditor.cursorLeft(2); // return ""
// The current text is "|practice".
// The cursor cannot be moved beyond the actual text and thus did not move.
// "" is the last min(10, 0) = 0 characters to the left of the cursor.
textEditor.cursorRight(6); // return "practi"
// The current text is "practi|ce".
// "practi" is the last min(10, 6) = 6 characters to the left of the cursor.
Constraints:
1 <= text.length, k <= 40textconsists of lowercase English letters.- At most
2 * 104calls in total will be made toaddText,deleteText,cursorLeftandcursorRight.
Follow-up: Could you find a solution with time complexity of O(k) per call?
Approaches
3 approaches with complexity analysis and trade-offs.
This approach uses a single StringBuilder to store the entire text and an integer variable to maintain the cursor's position. While simple to conceptualize, operations like adding or deleting text in the middle are inefficient because they require shifting all subsequent characters.
Algorithm
- Initialize a
StringBuildersbto store the text and an integercursorto track the cursor's position. addText(text): Usesb.insert(cursor, text)to add text. This operation shifts all subsequent characters, takingO(N)time whereNis the total text length.deleteText(k): Usesb.delete(cursor - k, cursor)to remove text. This also takesO(N)time due to character shifting.cursorLeft(k)andcursorRight(k): Update thecursorindex. This is anO(1)operation. The required substring is then extracted, which takes constant time (O(10)).
Walkthrough
In this straightforward approach, we model the text editor's content with a java.lang.StringBuilder and the cursor's position with an integer index.
- Data Structures: A
StringBuilder sbholds the entire text, and anint cursormarks the insertion point (i.e., new text is inserted atsb[cursor]). addText(text): We callsb.insert(cursor, text). This method inserts the giventextat thecursor's position. However, this is costly because all characters from the cursor position to the end of the string must be shifted to the right. The cursor position is then advanced by the length of the added text.deleteText(k): We deletekcharacters to the left of the cursor by callingsb.delete(Math.max(0, cursor - k), cursor). Similar to insertion, this can be slow as it may require shifting all characters that were to the right of the deleted segment.cursorLeft(k)/cursorRight(k): These are simple arithmetic operations on thecursorindex, bounded by0and the current length of the text. They are very fast.- Returning Left Substring: After a cursor move, we extract the last
min(10, cursor)characters to the left of the cursor usingsb.substring(). This is a fast operation as the length is small and fixed.
class TextEditor { StringBuilder sb; int cursor; public TextEditor() { sb = new StringBuilder(); cursor = 0; } public void addText(String text) { sb.insert(cursor, text); cursor += text.length(); } public int deleteText(int k) { int start = Math.max(0, cursor - k); int end = cursor; if (start == end) return 0; int deletedCount = end - start; sb.delete(start, end); cursor = start; return deletedCount; } private String getLeftText() { int start = Math.max(0, cursor - 10); return sb.substring(start, cursor); } public String cursorLeft(int k) { cursor = Math.max(0, cursor - k); return getLeftText(); } public String cursorRight(int k) { cursor = Math.min(sb.length(), cursor + k); return getLeftText(); }}Complexity
Time
O(N) for `addText` and `deleteText`, where N is the current length of the text. `cursorLeft` and `cursorRight` are O(1) (as substring extraction is on at most 10 chars). Due to the O(N) operations, this approach is too slow for the given constraints.
Space
O(N), where N is the total number of characters in the text editor, to store the text in the `StringBuilder`.
Trade-offs
Pros
Simple to understand and implement.
Uses standard, well-known Java library features.
Cons
The
addTextanddeleteTextoperations are very slow (O(N)) for a large text body, as they require shifting a potentially large number of characters. This will lead to a Time Limit Exceeded error given the problem's constraints.
To overcome the inefficiency of shifting elements in an array-based structure, this approach represents the text using a doubly linked list of characters. The cursor is a pointer to a node in the list. This structure allows for efficient O(1) insertions and deletions at any point in the list, once the position is located.
Algorithm
- Define a
Nodeclass for a doubly linked list, containing a character andprev/nextpointers. - Use two sentinel nodes,
headandtail, to handle edge cases gracefully. - The
cursoris a pointer to theNodethat is immediately to the right of the conceptual cursor. addText(text): For each character intext, create a newNodeand insert it before thecursornode by adjusting pointers. This isO(1)per character.deleteText(k): Forktimes, remove the node to the left of thecursor(cursor.prev) by updating its neighbors' pointers. This isO(1)per deletion.cursorLeft(k)/cursorRight(k): Move thecursorpointerksteps by followingprevornextlinks.- To get the text for return, traverse backwards from
cursor.prevfor up to 10 nodes.
Walkthrough
This approach models the text as a chain of character nodes, where each node is connected to its predecessor and successor. This allows for modifications in the middle of the text without affecting distant characters.
- Data Structures: We use a
Nodeclass withchar data,Node prev, andNode next. The editor maintains a pointer,cursor, which points to the node just after the insertion point. Sentinelheadandtailnodes are used to simplify list manipulation, especially for an empty list or when the cursor is at the beginning or end. addText(text): We iterate through the inputtext. For each character, we create a newNodeand insert it betweencursor.prevandcursor. This involves updating four pointers and takes constant time per character.deleteText(k): We performkdeletions. In each step, we identify the node to be deleted (cursor.prev), and then bypass it by linking its predecessor (cursor.prev.prev) to its successor (cursor). This is a constant time operation per character deletion.cursorLeft(k)/cursorRight(k): Moving the cursor is achieved by simply traversing the list. To move left, we updatecursor = cursor.prevktimes. To move right,cursor = cursor.nextktimes.- Returning Left Substring: We traverse backwards from
cursor.prevfor up to 10 nodes, appending each character to aStringBuilder, which is then reversed to get the correct order.
class TextEditor { class Node { char c; Node prev, next; Node(char c) { this.c = c; } } Node cursor; // Node to the right of the conceptual cursor Node head, tail; // Sentinel nodes public TextEditor() { head = new Node(' '); tail = new Node(' '); head.next = tail; tail.prev = head; cursor = tail; } public void addText(String text) { for (char ch : text.toCharArray()) { Node newNode = new Node(ch); Node prevNode = cursor.prev; prevNode.next = newNode; newNode.prev = prevNode; newNode.next = cursor; cursor.prev = newNode; } } public int deleteText(int k) { int count = 0; for (int i = 0; i < k && cursor.prev != head; i++) { Node toDelete = cursor.prev; toDelete.prev.next = cursor; cursor.prev = toDelete.prev; count++; } return count; } private String getLeftText() { StringBuilder sb = new StringBuilder(); Node curr = cursor.prev; for (int i = 0; i < 10 && curr != head; i++) { sb.append(curr.c); curr = curr.prev; } return sb.reverse().toString(); } public String cursorLeft(int k) { for (int i = 0; i < k && cursor.prev != head; i++) { cursor = cursor.prev; } return getLeftText(); } public String cursorRight(int k) { for (int i = 0; i < k && cursor != tail; i++) { cursor = cursor.next; } return getLeftText(); }}Complexity
Time
`addText(text)` is O(L) where L is `text.length()`. `deleteText(k)`, `cursorLeft(k)`, and `cursorRight(k)` are all O(k). This is highly efficient.
Space
O(N), where N is the total number of characters. The constant factor is high due to storing two pointers for each character node.
Trade-offs
Pros
All operations are efficient, with time complexities proportional to
kor the length of the text being added (L), not the total text lengthN.Satisfies the follow-up requirement of
O(k)per call.
Cons
High memory overhead. Each character requires a
Nodeobject, which includes two pointers in addition to the character data. This can consume significantly more memory than a simple character array.Implementation is more complex and error-prone compared to using a
StringBuilder.
This is a highly efficient and practical approach that simulates a gap buffer using two stacks, which can be implemented conveniently with two StringBuilders. The text is conceptually split at the cursor into a "left" part and a "right" part. By storing the right part in reverse, all operations at the cursor become manipulations at the end of the strings, which is very fast.
Algorithm
- Use two
StringBuilders:leftfor text before the cursor, andrightfor text after the cursor. - Crucially, store the
rightpart in reverse order. This makes operations at the cursor point (end ofleft, end ofright) very fast. addText(text): Appendtextto theleftStringBuilder.deleteText(k): Remove the lastkcharacters from theleftStringBuilder.cursorLeft(k): Movekcharacters from the end ofleftto the end ofright.cursorRight(k): Movekcharacters from the end ofrightto the end ofleft.- To get the text for return, take the last
min(10, left.length())characters fromleft.
Walkthrough
This optimal approach, often called the "two-stack" method, provides excellent performance by avoiding costly data shifts. It's a practical implementation of the gap buffer concept.
- Data Structures: We use two
StringBuilders,leftandright.leftstores the text to the left of the cursor in its natural order.rightstores the text to the right of the cursor, but in reverse order. For example, if the text isleet|practice, thenleft="leet"andright="ecitcarp". addText(text): New text is always added at the cursor, so we simply append it to theleftStringBuilder. This is an amortizedO(1)operation per character.deleteText(k): Deleting text to the left of the cursor means removing characters from the end of theleftStringBuilder. This is done efficiently by shortening its length.cursorLeft(k): To move the cursor left bykpositions, we popkcharacters from the end ofleftand append them to the end ofright.cursorRight(k): To move the cursor right bykpositions, we popkcharacters from the end ofright(which are the characters immediately to the right of the cursor, but in reverse) and append them toleft.- Returning Left Substring: The required text is simply the last
min(10, left.length())characters of theleftStringBuilder.
This design ensures all operations manipulate the ends of the StringBuilders, leveraging their amortized constant-time performance for append and delete-at-end operations.
class TextEditor { StringBuilder left; // Text to the left of the cursor StringBuilder right; // Text to the right of the cursor, in reverse order public TextEditor() { left = new StringBuilder(); right = new StringBuilder(); } public void addText(String text) { left.append(text); } public int deleteText(int k) { int actualDeleted = Math.min(k, left.length()); left.setLength(left.length() - actualDeleted); return actualDeleted; } private String getLeftText() { int len = left.length(); return left.substring(Math.max(0, len - 10)); } public String cursorLeft(int k) { int count = Math.min(k, left.length()); for (int i = 0; i < count; i++) { right.append(left.charAt(left.length() - 1)); left.deleteCharAt(left.length() - 1); } return getLeftText(); } public String cursorRight(int k) { int count = Math.min(k, right.length()); for (int i = 0; i < count; i++) { left.append(right.charAt(right.length() - 1)); right.deleteCharAt(right.length() - 1); } return getLeftText(); }}Complexity
Time
`addText(text)` is O(L) where L is `text.length()`. `deleteText(k)`, `cursorLeft(k)`, and `cursorRight(k)` are all O(k). This is because all modifications happen at the end of the `StringBuilder`s, which is an amortized O(1) operation per character.
Space
O(N), where N is the total number of characters in the text. The space overhead is minimal.
Trade-offs
Pros
Optimal time complexity for all operations, meeting the
O(k)follow-up requirement.More space-efficient than a character-based linked list.
Relatively straightforward to implement using standard library data structures.
Cons
The concept of storing the right part of the text in reverse order can be slightly counter-intuitive at first glance.
Solutions
Solution
class TextEditor { private StringBuilder left = new StringBuilder (); private StringBuilder right = new StringBuilder (); public TextEditor () { } public void addText ( String text ) { left . append ( text ); } public int deleteText ( int k ) { k = Math . min ( k , left . length ()); left . setLength ( left . length () - k ); return k ; } public String cursorLeft ( int k ) { k = Math . min ( k , left . length ()); for ( int i = 0 ; i < k ; ++ i ) { right . append ( left . charAt ( left . length () - 1 )); left . deleteCharAt ( left . length () - 1 ); } return left . substring ( Math . max ( left . length () - 10 , 0 )); } public String cursorRight ( int k ) { k = Math . min ( k , right . length ()); for ( int i = 0 ; i < k ; ++ i ) { left . append ( right . charAt ( right . length () - 1 )); right . deleteCharAt ( right . length () - 1 ); } return left . substring ( Math . max ( left . length () - 10 , 0 )); } } /** * Your TextEditor object will be instantiated and called as such: * TextEditor obj = new TextEditor(); * obj.addText(text); * int param_2 = obj.deleteText(k); * String param_3 = obj.cursorLeft(k); * String param_4 = obj.cursorRight(k); */class TextEditor { public: TextEditor () { } void addText ( string text ) { left += text ; } int deleteText ( int k ) { k = min ( k , ( int ) left . size ()); left . resize ( left . size () - k ); return k ; } string cursorLeft ( int k ) { k = min ( k , ( int ) left . size ()); while ( k -- ) { right += left . back (); left . pop_back (); } return left . substr ( max ( 0 , ( int ) left . size () - 10 )); } string cursorRight ( int k ) { k = min ( k , ( int ) right . size ()); while ( k -- ) { left += right . back (); right . pop_back (); } return left . substr ( max ( 0 , ( int ) left . size () - 10 )); } private: string left , right ; }; /** * Your TextEditor object will be instantiated and called as such: * TextEditor* obj = new TextEditor(); * obj->addText(text); * int param_2 = obj->deleteText(k); * string param_3 = obj->cursorLeft(k); * string param_4 = obj->cursorRight(k); */class TextEditor : def __init__ ( self ): self . left = [] self . right = [] def addText ( self , text : str ) -> None : self . left . extend ( list ( text )) def deleteText ( self , k : int ) -> int : k = min ( k , len ( self . left )) for _ in range ( k ): self . left . pop () return 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 :]) 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 :]) # Your TextEditor object will be instantiated and called as such: # obj = TextEditor() # obj.addText(text) # param_2 = obj.deleteText(k) # param_3 = obj.cursorLeft(k) # param_4 = obj.cursorRight(k)Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.