Skip to main content

Two Pointers on Two Arrays or Strings

Editorial

Merging two sorted lists

Two teachers each hand you a stack of exam papers, sorted by score. You need one pile, sorted. You wouldn't throw both stacks on the floor and sort them from scratch: you'd look at the top paper of each stack, take the lower score, and repeat.

Code has the same choice. Gluing two sorted lists together and sorting the result costs O((m + n) log(m + n)): for two lists of a million numbers each, that's about 42 million comparisons (n log₂ n with n = 2 million). Walking both lists at once, with one pointer in each, needs at most 1,999,999: never more than one per number it places. (Python's built-in sort happens to notice the two already-sorted halves and merge them: the very step you're about to write.)

That's the last kind of two pointers in this section: two inputs, one pointer each. In the previous lessons both pointers walked one array. Here every input gets its own, and each step decides which pointer moves.

In this lesson: the merge and why it's always right; Merge Strings Alternately; merging into the same array from the back, and why the back; Is Subsequence, where one pointer waits; Backspace String Compare, read from the back; Valid Word Abbreviation, where a pointer jumps; and the mistakes that fail on real inputs.

Before this: Same-Direction Two Pointers: Changing an Array in Place. Next: the Sliding Window section.

Merging [1, 4, 7] and [2, 3, 8, 9] with one pointer in each list. 1 and 2 are already in the merged list. Pointer i is on 4 and pointer j is on 3; 3 is the smaller, so it goes into the next seat of the merged list, and only j moves on.

One pointer for each list

Put i at the front of a and j at the front of b. Each step compares a[i] with b[j], takes the smaller one, and moves only that pointer.

Why is the smaller one always right? Everything before i and j is already placed. Every value after i is at least a[i], and every value after j is at least b[j], because both lists are sorted. So the smallest value not placed yet is either a[i] or b[j], and the smaller of the two goes next. Nothing can be skipped.

When one list runs out, the rest of the other is still sorted, and every value in it is at least as big as anything placed so far. It goes on the end as it is.

Here's every step on a = [1, 4, 7] and b = [2, 3, 8, 9]:

i, jCompareTakeMerged so far
0, 01 vs 21 from a[1]
1, 04 vs 22 from b[1, 2]
1, 14 vs 33 from b[1, 2, 3]
1, 24 vs 84 from a[1, 2, 3, 4]
2, 27 vs 87 from a[1, 2, 3, 4, 7]

Now a is used up, so the rest of b, [8, 9], goes on the end: [1, 2, 3, 4, 7, 8, 9].

Each version prints 1 2 3 4 7 8 9.

vector<int> mergeSorted(const vector<int>& a, const vector<int>& b) {
    vector<int> out;
    int i = 0, j = 0;
    while (i < (int)a.size() && j < (int)b.size()) {
        if (a[i] <= b[j]) {               // the smaller front value is the smallest one left
            out.push_back(a[i]);
            i++;
        } else {
            out.push_back(b[j]);
            j++;
        }
    }
    while (i < (int)a.size()) {           // b ran out: the rest of a is already in order
        out.push_back(a[i]);
        i++;
    }
    while (j < (int)b.size()) {           // a ran out: the rest of b is already in order
        out.push_back(b[j]);
        j++;
    }
    return out;
}
def merge_sorted(a, b):
    out = []
    i = j = 0
    while i < len(a) and j < len(b):
        if a[i] <= b[j]:              # the smaller front value is the smallest one left
            out.append(a[i])
            i += 1
        else:
            out.append(b[j])
            j += 1
    out.extend(a[i:])                 # one list ran out: the rest of the other is already in order
    out.extend(b[j:])
    return out
static int[] mergeSorted(int[] a, int[] b) {
    int[] out = new int[a.length + b.length];
    int i = 0, j = 0, k = 0;
    while (i < a.length && j < b.length) {
        if (a[i] <= b[j]) {               // the smaller front value is the smallest one left
            out[k] = a[i];
            i++;
        } else {
            out[k] = b[j];
            j++;
        }
        k++;
    }
    while (i < a.length) {                // b ran out: the rest of a is already in order
        out[k] = a[i];
        i++;
        k++;
    }
    while (j < b.length) {                // a ran out: the rest of b is already in order
        out[k] = b[j];
        j++;
        k++;
    }
    return out;
}
function mergeSorted(a, b) {
    const out = [];
    let i = 0, j = 0;
    while (i < a.length && j < b.length) {
        if (a[i] <= b[j]) {               // the smaller front value is the smallest one left
            out.push(a[i]);
            i++;
        } else {
            out.push(b[j]);
            j++;
        }
    }
    while (i < a.length) {                // b ran out: the rest of a is already in order
        out.push(a[i]);
        i++;
    }
    while (j < b.length) {                // a ran out: the rest of b is already in order
        out.push(b[j]);
        j++;
    }
    return out;
}

Cost: every step places one value and moves one pointer forward, so it's m + n steps: O(m + n) time. The output takes O(m + n) space; the pointers are two integers.

Ties: with <=, a tie takes from a first, so equal values keep their original order. That makes the merge stable, and merge sort relies on it.

Merge Strings Alternately: take turns, then the rest

Merge Strings Alternately gives you two words and asks for one string that takes a letter from each in turn, starting with the first word. When one word runs out, the rest of the other goes on the end.

There's nothing to compare here: the pointers just take turns. The part people miss is the same as in the merge, the leftover tail. Each version prints apbqcr for "abc" and "pqr", then apbqrs for "ab" and "pqrs".

string mergeAlternately(const string& word1, const string& word2) {
    string out;
    int i = 0, j = 0;
    while (i < (int)word1.size() && j < (int)word2.size()) {
        out += word1[i];                  // take turns: one letter from each
        out += word2[j];
        i++;
        j++;
    }
    out += word1.substr(i);               // the longer word's rest goes on the end
    out += word2.substr(j);
    return out;
}
def merge_alternately(word1, word2):
    out = []
    i = j = 0
    while i < len(word1) and j < len(word2):
        out.append(word1[i])          # take turns: one letter from each
        out.append(word2[j])
        i += 1
        j += 1
    out.append(word1[i:])             # the longer word's rest goes on the end
    out.append(word2[j:])
    return "".join(out)
static String mergeAlternately(String word1, String word2) {
    StringBuilder out = new StringBuilder();
    int i = 0, j = 0;
    while (i < word1.length() && j < word2.length()) {
        out.append(word1.charAt(i));      // take turns: one letter from each
        out.append(word2.charAt(j));
        i++;
        j++;
    }
    out.append(word1.substring(i));       // the longer word's rest goes on the end
    out.append(word2.substring(j));
    return out.toString();
}
function mergeAlternately(word1, word2) {
    const out = [];
    let i = 0, j = 0;
    while (i < word1.length && j < word2.length) {
        out.push(word1[i]);               // take turns: one letter from each
        out.push(word2[j]);
        i++;
        j++;
    }
    out.push(word1.slice(i));             // the longer word's rest goes on the end
    out.push(word2.slice(j));
    return out.join("");
}

Both pointers move together in this one, so a single index would do. Keeping two makes it the same shape as the merge, tail and all.

Merging into the same array: start from the back

Merge Sorted Array is the merge with a twist: there's no new array. nums1 has room for both lists. Its first m seats hold its own values, and the last n seats are placeholders (zeros). You merge nums2 into it, in place.

The merge above fills the result from the front. Try that here and the writes land on seats that still hold values of nums1 that haven't been merged yet. With nums1 = [2, 5, 8, 0, 0, 0] and nums2 = [1, 6, 7], the 1 goes first, straight on top of the 2. The 2 is gone.

The free seats are at the back, so fill from the back, taking the larger value each time:

  • i is the last value of nums1 not placed yet (it starts at m − 1).
  • j is the last value of nums2 not placed yet (it starts at n − 1).
  • w is the seat to fill (it starts at m + n − 1).

Why can't w run over a value that i still needs? Count the free seats from i + 1 to w: there are w − i of them. At the start that's n, and nums2 has n values left. Each step fills one seat:

  • Taking from nums1 moves i and w back together. The free seats stay the same, and so does what's left of nums2.
  • Taking from nums2 moves w and j back. Both counts drop by one.

So the free seats always equal the values of nums2 still to place: w − i = j + 1. The writer can only reach i once nums2 is used up.

And once nums2 is used up, you're done. Whatever is left of nums1 already sits in its final seats, so there's nothing to copy.

Merging nums2 = [1, 6, 7] into nums1 = [2, 5, 8, _, _, _]. Panel 1, from the front: the first write puts 1 into seat 0, on top of the 2 that hasn't been merged yet, and the 2 is lost. Panel 2, from the back: the writer starts at the last seat, and the three free seats between the reader and the writer match the three values of nums2 still to place, so the writer never reaches a value that hasn't been placed.

Here's every step on nums1 = [2, 5, 8, 0, 0, 0] and nums2 = [1, 6, 7]. Before the first step it's [2, 5, 8, _, _, _]: three free seats, three values of nums2 left.

i, j, wCompare and writenums1 after (_ is free)Free seats = nums2 left
2, 2, 58 vs 7: 8 from nums1 into seat 5[2, 5, _, _, _, 8]3
1, 2, 45 vs 7: 7 from nums2 into seat 4[2, 5, _, _, 7, 8]2
1, 1, 35 vs 6: 6 from nums2 into seat 3[2, 5, _, 6, 7, 8]1
1, 0, 25 vs 1: 5 from nums1 into seat 2[2, _, 5, 6, 7, 8]1
0, 0, 12 vs 1: 2 from nums1 into seat 1[_, 2, 5, 6, 7, 8]1
-1, 0, 0nums1 used up: 1 from nums2 into seat 0[1, 2, 5, 6, 7, 8]0

Watch the underscores: the free seats move left, and there are always as many as the values of nums2 still to place. The last row is the case the i >= 0 check is for: nums1 is used up, so the rest of nums2 goes in.

Each version prints the merge above, then two more: [1, 2, 9] with [5, 6], where 1 and 2 never move, and an empty nums1 (m = 0) with [1].

void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
    int i = m - 1;                        // the last value of nums1 not placed yet
    int j = n - 1;                        // the last value of nums2 not placed yet
    int w = m + n - 1;                    // the seat to fill, from the back
    while (j >= 0) {                      // once nums2 is used up, the rest of nums1 is already in place
        if (i >= 0 && nums1[i] > nums2[j]) {
            nums1[w] = nums1[i];
            i--;
        } else {
            nums1[w] = nums2[j];
            j--;
        }
        w--;
    }
}
def merge(nums1, m, nums2, n):
    i = m - 1                         # the last value of nums1 not placed yet
    j = n - 1                         # the last value of nums2 not placed yet
    w = m + n - 1                     # the seat to fill, from the back
    while j >= 0:                     # once nums2 is used up, the rest of nums1 is already in place
        if i >= 0 and nums1[i] > nums2[j]:
            nums1[w] = nums1[i]
            i -= 1
        else:
            nums1[w] = nums2[j]
            j -= 1
        w -= 1
static void merge(int[] nums1, int m, int[] nums2, int n) {
    int i = m - 1;                        // the last value of nums1 not placed yet
    int j = n - 1;                        // the last value of nums2 not placed yet
    int w = m + n - 1;                    // the seat to fill, from the back
    while (j >= 0) {                      // once nums2 is used up, the rest of nums1 is already in place
        if (i >= 0 && nums1[i] > nums2[j]) {
            nums1[w] = nums1[i];
            i--;
        } else {
            nums1[w] = nums2[j];
            j--;
        }
        w--;
    }
}
function merge(nums1, m, nums2, n) {
    let i = m - 1;                        // the last value of nums1 not placed yet
    let j = n - 1;                        // the last value of nums2 not placed yet
    let w = m + n - 1;                    // the seat to fill, from the back
    while (j >= 0) {                      // once nums2 is used up, the rest of nums1 is already in place
        if (i >= 0 && nums1[i] > nums2[j]) {
            nums1[w] = nums1[i];
            i--;
        } else {
            nums1[w] = nums2[j];
            j--;
        }
        w--;
    }
}

Cost: each step fills one seat, and there are at most m + n of them: O(m + n) time, O(1) extra space.

Try it yourself

Below is Merge Sorted Array, and you choose every write. The counter shows the free seats and what's left of nums2: watch them stay equal. Then switch to filling from the front and see which values get overwritten before they're merged.

Merge Sorted Array by hand: choose every write, from the back or from the front

Is Subsequence: one pointer waits

Is Subsequence asks whether you can get s by deleting some letters of t, without changing the order of the rest. "ace" is a subsequence of "abcde"; "aec" isn't.

Put one pointer in each string. j walks through t one letter at a time. i points at the next letter of s you're looking for, and it moves only when t[j] matches it. If i reaches the end of s, every letter was found, in order.

Why is it safe to grab the first match? Matching a letter of s as early as possible leaves the most of t for the letters after it. Waiting for a later copy of the same letter leaves less room, never more. So taking the earliest match never loses an answer.

Each version prints true, false and true for ("ace", "abcde"), ("aec", "abcde") and ("", "abc"), as (s, t): an empty s is a subsequence of anything.

bool isSubsequence(const string& s, const string& t) {
    int i = 0, j = 0;
    while (i < (int)s.size() && j < (int)t.size()) {
        if (s[i] == t[j]) {               // found: now look for the next letter of s
            i++;
        }
        j++;                              // t's pointer moves every step
    }
    return i == (int)s.size();            // every letter of s was found, in order
}
def is_subsequence(s, t):
    i = j = 0
    while i < len(s) and j < len(t):
        if s[i] == t[j]:              # found: now look for the next letter of s
            i += 1
        j += 1                        # t's pointer moves every step
    return i == len(s)                # every letter of s was found, in order
static boolean isSubsequence(String s, String t) {
    int i = 0, j = 0;
    while (i < s.length() && j < t.length()) {
        if (s.charAt(i) == t.charAt(j)) { // found: now look for the next letter of s
            i++;
        }
        j++;                              // t's pointer moves every step
    }
    return i == s.length();               // every letter of s was found, in order
}
function isSubsequence(s, t) {
    let i = 0, j = 0;
    while (i < s.length && j < t.length) {
        if (s[i] === t[j]) {              // found: now look for the next letter of s
            i++;
        }
        j++;                              // t's pointer moves every step
    }
    return i === s.length;                // every letter of s was found, in order
}

Cost: j visits each letter of t once: O(|t|) time, O(1) space.

Follow-up: "What if a billion different strings s are checked against the same t?" Scanning t again for every one costs O(|t|) each time. Prepare t once instead: for each letter, the sorted list of positions where it appears. Then each letter of s is a binary search for its first position after the previous match, so a query costs O(|s| log |t|). A table of the next position of every letter after every seat makes each step O(1), for 26 × |t| entries of memory.

Backspace String Compare: read from the back

Backspace String Compare gives you two strings where # is a backspace, and asks whether they type the same text. "ab#c" and "ad#c" both type "ac". A backspace on empty text does nothing.

The easy way builds both typed strings with a stack and compares them, which costs O(n) extra space. The follow-up asks for O(1).

Reading from the front can't do that: when you reach a letter, you don't know yet whether a # further on will delete it. Reading from the back, you meet every # before the letters it deletes. Keep a count of backspaces still to apply. A # adds one. A letter that arrives while the count is above zero is deleted, and the count goes down. A letter that arrives at zero survives.

So each string gets a pointer that moves back to its next surviving letter. Compare the two survivors: if they differ, so do the texts. If one string runs out of survivors before the other, the texts differ too.

On "abc##d" and "ad": both pointers start on a d that survives, and they match. Then the pointer in "abc##d" meets two #s, which delete the c and the b, and stops on the a. The other pointer is already on its a. They match, and both strings run out together: same text.

Each version prints true, true, false and true for ("ab#c", "ad#c"), ("ab##", "c#d#"), ("a#c", "b") and ("abc##d", "ad").

// the index of the next letter at or before k that survives the backspaces, or -1
int nextKept(const string& text, int k) {
    int skip = 0;                         // backspaces seen that still have a letter to delete
    while (k >= 0) {
        if (text[k] == '#') {
            skip++;
        } else if (skip > 0) {
            skip--;                       // a later # deletes this letter
        } else {
            return k;
        }
        k--;
    }
    return -1;
}

bool backspaceCompare(const string& s, const string& t) {
    int i = (int)s.size() - 1, j = (int)t.size() - 1;
    while (true) {
        i = nextKept(s, i);
        j = nextKept(t, j);
        if (i < 0 || j < 0) {
            return i < 0 && j < 0;        // the same text only if both run out together
        }
        if (s[i] != t[j]) {
            return false;
        }
        i--;
        j--;
    }
}
def next_kept(text, k):
    """The index of the next letter at or before k that survives the backspaces, or -1."""
    skip = 0                          # backspaces seen that still have a letter to delete
    while k >= 0:
        if text[k] == "#":
            skip += 1
        elif skip > 0:
            skip -= 1                 # a later # deletes this letter
        else:
            return k
        k -= 1
    return -1


def backspace_compare(s, t):
    i, j = len(s) - 1, len(t) - 1
    while True:
        i, j = next_kept(s, i), next_kept(t, j)
        if i < 0 or j < 0:
            return i < 0 and j < 0    # the same text only if both run out together
        if s[i] != t[j]:
            return False
        i -= 1
        j -= 1
// the index of the next letter at or before k that survives the backspaces, or -1
static int nextKept(String text, int k) {
    int skip = 0;                         // backspaces seen that still have a letter to delete
    while (k >= 0) {
        if (text.charAt(k) == '#') {
            skip++;
        } else if (skip > 0) {
            skip--;                       // a later # deletes this letter
        } else {
            return k;
        }
        k--;
    }
    return -1;
}

static boolean backspaceCompare(String s, String t) {
    int i = s.length() - 1, j = t.length() - 1;
    while (true) {
        i = nextKept(s, i);
        j = nextKept(t, j);
        if (i < 0 || j < 0) {
            return i < 0 && j < 0;        // the same text only if both run out together
        }
        if (s.charAt(i) != t.charAt(j)) {
            return false;
        }
        i--;
        j--;
    }
}
// the index of the next letter at or before k that survives the backspaces, or -1
function nextKept(text, k) {
    let skip = 0;                         // backspaces seen that still have a letter to delete
    while (k >= 0) {
        if (text[k] === "#") {
            skip++;
        } else if (skip > 0) {
            skip--;                       // a later # deletes this letter
        } else {
            return k;
        }
        k--;
    }
    return -1;
}

function backspaceCompare(s, t) {
    let i = s.length - 1, j = t.length - 1;
    while (true) {
        i = nextKept(s, i);
        j = nextKept(t, j);
        if (i < 0 || j < 0) {
            return i < 0 && j < 0;        // the same text only if both run out together
        }
        if (s[i] !== t[j]) {
            return false;
        }
        i--;
        j--;
    }
}

Cost: each pointer moves back over its string once: O(n + m) time, and a few integers of extra space.

Valid Word Abbreviation: a pointer that jumps

Valid Word Abbreviation replaces stretches of a word with their lengths: "substitution" can be written "s10n" or "sub4u4". Given a word and an abbreviation, say whether they match.

One pointer in each string. A letter in the abbreviation must equal the letter in the word, and both pointers move one step. A number makes the word's pointer jump: read all its digits, then skip that many letters. Two rules settle the edge cases:

  • A number can't start with 0. "s010n" is invalid, and so is "s0ubstitution", where 0 would replace nothing.
  • Both pointers have to reach their ends together. "a3" doesn't match "apple": after the a and three skipped letters, the word still has its e left.

And a letter after a jump still has to match: in "a2e" the jump lands on the l of "apple", where the abbreviation wants an e.

Each version prints true, false, false and true for ("internationalization", "i12iz4n"), ("apple", "a2e"), ("substitution", "s010n") and ("substitution", "s10n"), as (word, abbr).

bool validWordAbbreviation(const string& word, const string& abbr) {
    int i = 0, j = 0;                     // i in word, j in abbr
    while (i < (int)word.size() && j < (int)abbr.size()) {
        if (abbr[j] >= '0' && abbr[j] <= '9') {
            if (abbr[j] == '0') {         // a number can't start with 0 (or be 0)
                return false;
            }
            int num = 0;
            while (j < (int)abbr.size() && abbr[j] >= '0' && abbr[j] <= '9') {
                num = num * 10 + (abbr[j] - '0');
                j++;
            }
            i += num;                     // skip that many letters of word
        } else {
            if (word[i] != abbr[j]) {
                return false;
            }
            i++;
            j++;
        }
    }
    return i == (int)word.size() && j == (int)abbr.size();   // both must end together
}
def valid_word_abbreviation(word, abbr):
    i = j = 0                         # i in word, j in abbr
    while i < len(word) and j < len(abbr):
        if abbr[j].isdigit():
            if abbr[j] == "0":        # a number can't start with 0 (or be 0)
                return False
            num = 0
            while j < len(abbr) and abbr[j].isdigit():
                num = num * 10 + int(abbr[j])
                j += 1
            i += num                  # skip that many letters of word
        else:
            if word[i] != abbr[j]:
                return False
            i += 1
            j += 1
    return i == len(word) and j == len(abbr)   # both must end together
static boolean validWordAbbreviation(String word, String abbr) {
    int i = 0, j = 0;                     // i in word, j in abbr
    while (i < word.length() && j < abbr.length()) {
        char c = abbr.charAt(j);
        if (c >= '0' && c <= '9') {
            if (c == '0') {               // a number can't start with 0 (or be 0)
                return false;
            }
            int num = 0;
            while (j < abbr.length() && abbr.charAt(j) >= '0' && abbr.charAt(j) <= '9') {
                num = num * 10 + (abbr.charAt(j) - '0');
                j++;
            }
            i += num;                     // skip that many letters of word
        } else {
            if (word.charAt(i) != c) {
                return false;
            }
            i++;
            j++;
        }
    }
    return i == word.length() && j == abbr.length();   // both must end together
}
function validWordAbbreviation(word, abbr) {
    let i = 0, j = 0;                     // i in word, j in abbr
    while (i < word.length && j < abbr.length) {
        if (abbr[j] >= "0" && abbr[j] <= "9") {
            if (abbr[j] === "0") {        // a number can't start with 0 (or be 0)
                return false;
            }
            let num = 0;
            while (j < abbr.length && abbr[j] >= "0" && abbr[j] <= "9") {
                num = num * 10 + (abbr.charCodeAt(j) - 48);
                j++;
            }
            i += num;                     // skip that many letters of word
        } else {
            if (word[i] !== abbr[j]) {
                return false;
            }
            i++;
            j++;
        }
    }
    return i === word.length && j === abbr.length;   // both must end together
}

Cost: every pass of the loop uses up at least one character of the abbreviation, and the skipped letters are never visited: O(length of the abbreviation) time, O(1) space.

When to use it

Reach for one pointer per input when you see:

  • Two sorted arrays or lists to merge, compare or intersect. To intersect, move the pointer on the smaller value; on a match, keep it and move both.
  • "Is one sequence inside another": a subsequence, or matching a pattern letter by letter.
  • Two strings to compare after edits: backspaces, abbreviations, skipped characters.
  • "In place" with the free space at one end: fill from that end.

And watch for look-alikes:

  • More than two sorted lists. Merging k of them two at a time redoes work; a heap of the k front values does it in O(N log k). That's Merge k Sorted Lists, in the Heaps & Priority Queues section.
  • Unsorted inputs. "Take the smaller one and move its pointer" only works because both lists are sorted. Intersecting unsorted arrays is a job for a hash set.
  • A block of neighbouring characters (a substring, not a subsequence). That's a sliding window or string matching, not this.

The same move shows up elsewhere: Assign Cookies, in Greedy Strategies, walks the children and the cookies with one pointer each.

When two pointers doesn't work

  • k lists instead of two. Comparing k front values each step costs O(k) per value; a heap gets that down to O(log k).
  • No free space to write into. Merge Sorted Array works in place only because nums1 has its room at the back. Without that, a merge needs a second array (or a far trickier in-place algorithm).
  • Many queries against one input. Rescanning the long input for every query is the wrong cost. Index it once, as in Is Subsequence's follow-up.
  • No order to follow. If neither input is sorted and the question isn't about positions, there's no rule for which pointer should move.

Common mistakes

Each of these is a real submission bug, shown on the smallest input that breaks it. The outputs come from running the code.

Filling from the front in place. Merging nums1 = [2, 0] (m = 1) with nums2 = [1] from the front writes the 1 over the 2, then copies "the rest of nums1", which is now that 1. The result is [1, 1]. Fill from the back.

Stopping when either array runs out. Loop while i >= 0 and j >= 0 and the leftovers of nums2 are never written. On the problem's own example, nums1 = [0] with m = 0 and nums2 = [1], the loop never runs and nums1 stays [0]. Loop while nums2 has values left.

Popping a backspace from an empty stack. In the stack version, "#a" crashes Python with IndexError: pop from empty list. A backspace on empty text does nothing, so check the stack isn't empty before you pop.

Counting letters instead of keeping their order. Checking that t has enough of each letter says "ab" is a subsequence of "ba". It isn't: the b comes first.

Accepting a number that starts with 0. Without that check, "a" and "01" match. "01" has a leading zero, so it isn't a valid abbreviation.

Check yourself

Quiz

1 / 6

Multiple choice

Merge Sorted Array fills nums1 from the back. Why?

Practice problems

From easier to harder:

  1. Merge Strings Alternately: take turns, then the tail.
  2. Merge Sorted Array: in place, from the back.
  3. Is Subsequence: one pointer waits for a match.
  4. Backspace String Compare: read from the back, counting backspaces.
  5. Valid Word Abbreviation: a number makes one pointer jump.

Next: the Sliding Window section, where both pointers move the same way and mark the stretch between them.