Sequential Digits Explained in Python β Sliding Window Over a String (LeetCode 1291)
Learn how to solve the Sequential Digits problem (LeetCode 1291) with a surprisingly simple trick: slide a window over the string "123456789" and cut out every candidate number. We start with the three Python tools you need β strings, slicing, and loops with lists β then break down what the problem really asks and reveal the big insight that makes it easy. You'll build the sliding window solution step by step, trace a full run with low=1000 and high=13000, compare it to a digit-by-digit alternative, analyze why both run in effectively constant time, and stress-test the edge cases. Perfect for beginners learning algorithm patterns and interview prep. #Python #LeetCode #SlidingWindow #CodingInterview #Algorithms
SEO PACKAGE
1. SEO Title
Sequential Digits (LeetCode 1291): The Sliding Window Trick That Makes This Medium Feel Easy in Python
2. SEO Subtitle
Stop searching a billion numbers β generate the only 36 answers that can ever exist by sliding a window over "123456789".
3. Featured Quote
When valid answers are rare and structured, generate them directly instead of searching for them.
4. Meta Description
Solve LeetCode 1291 Sequential Digits in Python using a sliding window over "123456789". Learn the O(1) trick, dry run, edge cases, and interview tips.
5. URL Slug
sequential-digits-leetcode-1291-python-sliding-window
6. Keywords
Sequential Digits, LeetCode 1291, Python sliding window, coding interview, string slicing Python, generate vs search, constant time algorithm, LeetCode medium Python, algorithm patterns, Python loops and lists, interview preparation, data structures and algorithms, brute force vs optimal, sliding window pattern, Daily Python LeetCode, software engineering interview, Python string manipulation, O(1) time complexity, beginner algorithms, number range problem
7. Tags
python, leetcode, sliding-window, coding-interview, algorithms, data-structures, string-slicing, interview-prep, leetcode-medium, python-for-beginners, problem-solving, software-engineering, daily-leetcode, programming-tutorial
Sequential Digits (LeetCode 1291): Generate the Answers Instead of Searching for Them
8. Introduction
Some problems look intimidating until you notice the constraint hiding in plain sight. Sequential Digits is exactly that kind of problem β labeled medium on LeetCode, but secretly one of the easiest mediums you will ever solve once you see the trick.
At its heart, this problem is a test of a single, career-defining instinct: when the set of valid answers is small and highly structured, you should manufacture those answers directly rather than scanning a giant search space looking for them. Interviewers love this problem precisely because the naive approach works but is embarrassingly slow, and the elegant approach turns a range that could span a billion numbers into a fixed, tiny amount of work.
If you are learning Python, preparing for a coding interview, or building fluency with algorithms and data structures, this problem is a perfect teacher. It rewards you for thinking about the shape of valid outputs before you write a single loop β a habit that separates engineers who grind through brute force from those who spot the clean solution in seconds. By the end of this article you will understand not just the code, but the reasoning that produces it.
9. Problem Overview
You are given two integers, low and high. Your job is to return every integer in the inclusive range [low, high] whose digits are sequential β meaning each digit is exactly one more than the digit before it.
A number has sequential digits if, reading left to right, every digit increases by exactly one. So 1234 qualifies because 1 β 2 β 3 β 4 each step goes up by one. The number 1357 does not qualify, because 1 β 3 jumps by two.
The result must be returned as a sorted list in ascending order.
A few consequences fall straight out of that definition:
- Sequential numbers never contain a
0, because a digit must always be one greater than the last, and there is no digit before1. - Sequential numbers never wrap past
9. There is no9 β 10continuation, so the longest possible sequential number is123456789. - Because every sequential number is built from strictly increasing single digits with no repeats, there is a finite, fixed universe of them.
That last point is the whole game.
10. Example
Let's ground the definition in concrete cases.
Example 1
Input: low = 100, high = 300
Output: [123, 234]
Why? The only three-digit sequential numbers are 123, 234, 345, 456, and so on. Of those, only 123 and 234 fall inside [100, 300]. 345 is greater than 300, so it is excluded.
Example 2
Input: low = 1000, high = 13000
Output: [1234, 2345, 3456, 4567, 5678, 6789, 12345]
Here low has 4 digits and high has 5 digits, so valid answers can only be 4 or 5 digits long. Every 4-digit sequential number (1234 through 6789) sits comfortably inside the range. Among 5-digit candidates, only 12345 is small enough β 23456 already exceeds 13000, so it is dropped.
Notice how the output is already sorted. Hold onto that observation; it becomes a free gift later.
11. Intuition
Imagine you have never seen the optimal solution. How would an experienced engineer discover it?
The first instinct is almost always: loop from low to high, and for each number, check whether its digits are sequential. That is a perfectly reasonable starting thought. But then you glance at the constraints and see that high can be as large as one billion. A billion iterations to find β what, exactly? Let's count how many valid answers can even exist.
The longest sequential number is 123456789 (9 digits). Every sequential number is a contiguous run of that master string. There are:
- 8 numbers of length 2 (
12β¦89) - 7 numbers of length 3
- 6 of length 4 β¦ down to 1 of length 9
That sums to 36 sequential numbers in the entire universe of integers. Ever. Full stop.
So the naive approach potentially checks a billion numbers to surface at most 36 answers. That imbalance is the signal. When the valid outputs are this rare and this structured, searching is the wrong verb. Generating is the right one.
Now the key insight crystallizes: every sequential number is a slice of the string "123456789". 234 is characters 1β3. 4567 is characters 3β6. If you can systematically cut every substring out of "123456789", you have generated every sequential number that could ever exist β no searching required.
The tool for systematically cutting fixed-width chunks out of a string and moving along is the sliding window. Picture a cardboard cutout three digits wide sliding across the digit necklace. At each stop, the digits showing through form one candidate. Slide right, get the next one.
That's the whole idea: slide a window over one short string, and every candidate falls out for free.
12. Brute Force Solution
Idea: Walk through every number from low to high and test each one for the sequential property.
Algorithm:
- Loop
numfromlowtohigh. - Convert
numto a string. - Check that every digit is exactly one more than the previous digit.
- If it passes, append it to the result.
def sequentialDigits_brute(low: int, high: int) -> list[int]:
result = []
for num in range(low, high + 1):
s = str(num)
if all(int(s[i]) + 1 == int(s[i + 1]) for i in range(len(s) - 1)):
result.append(num)
return result
Advantages:
- Dead simple to write and reason about.
- Directly mirrors the problem statement, so it is easy to trust.
Disadvantages:
- Scans up to a billion numbers to find at most 36 answers.
- Wastes essentially all of its work on numbers that were never going to qualify.
Time complexity: O(high - low) β up to O(10^9), far too slow.
Space complexity: O(1) excluding the output list.
The brute force is correct, but it searches when it should generate. Let's fix that.
13. Optimal Solution
We flip the strategy from search to generate and filter.
Step 1 β Establish the master string. Every sequential number lives inside "123456789".
Step 2 β Only try useful lengths. A candidate can only land in [low, high] if it has the right number of digits. A 3-digit number can never beat a 5-digit low. So we count the digits of low and high and only consider window widths in that range.
| Length | Candidates produced |
|---|---|
| 2 | 12, 23, 34, 45, 56, 67, 78, 89 |
| 3 | 123, 234, 345, 456, 567, 678, 789 |
| 4 | 1234, 2345, 3456, 4567, 5678, 6789 |
| β¦ | β¦ |
| 9 | 123456789 |
Step 3 β Slide the window for each length. For a window of width length, the left edge start runs from 0 up to 10 - length. Why 10 - length? The master string is 9 characters long. A window of width 4 must not hang off the end, so its last valid start is index 5 β and 10 - 4 = 6, which is exactly the exclusive upper bound range needs.
Step 4 β Convert and filter. Each slice is text, so wrap it in int(). If the resulting number sits within [low, high], keep it.
The free gift: because we try short lengths before long ones, and slide left-to-right within each length, the numbers emerge already sorted. No call to sorted() needed. Generate, filter, done.
14. Python Code
class Solution:
def sequentialDigits(self, low: int, high: int) -> list[int]:
digits = "123456789"
result = []
# Only lengths between the digit-count of low and high can possibly fit.
for length in range(len(str(low)), len(str(high)) + 1):
# Slide a window of this width across the master string.
for start in range(0, 10 - length):
candidate = int(digits[start:start + length])
if low <= candidate <= high:
result.append(candidate)
return result
15. Code Walkthrough
digits = "123456789"β our entire search space, compressed into nine characters. Every possible answer is a substring of this.result = []β an ordered list we append valid candidates to. Order matters, and lists preserve insertion order.for length in range(len(str(low)), len(str(high)) + 1)β we convertlowandhighto strings purely to count their digits. This loop skips every length that couldn't possibly produce an in-range answer, which is where most of the savings come from.for start in range(0, 10 - length)β the sliding window itself.startis the left edge; the upper bound10 - lengthguarantees the window never runs off the end of the 9-character string.candidate = int(digits[start:start + length])β the slicedigits[start:start + length]cuts out one window of characters. Remember the end index is exclusive, which is the classic Python slicing gotcha.int(...)turns the text into a real number.if low <= candidate <= highβ a single inclusive bounds check. Python's chained comparison reads exactly like the math: is the candidate betweenlowandhigh?result.append(candidate)β keep the good ones. Because of the loop order, they arrive pre-sorted.
16. Dry Run
Let's trace Example 2: low = 1000, high = 13000.
len(str(1000))= 4,len(str(13000))= 5 β we trylengthin{4, 5}.
Length 4 β start runs 0 to 5:
| start | slice | candidate | in [1000, 13000]? |
|---|---|---|---|
| 0 | 1234 |
1234 | β keep |
| 1 | 2345 |
2345 | β keep |
| 2 | 3456 |
3456 | β keep |
| 3 | 4567 |
4567 | β keep |
| 4 | 5678 |
5678 | β keep |
| 5 | 6789 |
6789 | β keep |
Length 5 β start runs 0 to 4:
| start | slice | candidate | in [1000, 13000]? |
|---|---|---|---|
| 0 | 12345 |
12345 | β keep |
| 1 | 23456 |
23456 | β too big |
| 2 | 34567 |
34567 | β too big |
| 3 | 45678 |
45678 | β too big |
| 4 | 56789 |
56789 | β too big |
Result: [1234, 2345, 3456, 4567, 5678, 6789, 12345] β sorted, exact match.
17. Complexity Analysis
Here is the part that almost never happens on LeetCode.
Time complexity: O(1). The master string is fixed at 9 characters, so there are only ever 36 possible candidates, regardless of whether high is 300 or a billion. We do a fixed, tiny amount of work every time. The input size does not drive the runtime at all.
Space complexity: O(1). We store one 9-character string and a result list whose size is bounded by 36 entries. Both are constant, independent of low and high.
Contrast that with the brute force's O(high - low) time. The optimal solution isn't just faster by a constant factor β it's in an entirely different complexity class, because it stopped searching and started generating.
18. Alternative Solutions
There is a clean pure-arithmetic approach that avoids strings entirely. Start from each digit 1 through 8, then repeatedly "glue on" the next digit by multiplying by 10 and adding it: 1 β 12 β 123 β 1234 β¦.
class Solution:
def sequentialDigits(self, low: int, high: int) -> list[int]:
result = []
for start in range(1, 9):
num = start
next_digit = start + 1
while next_digit <= 9:
num = num * 10 + next_digit
if low <= num <= high:
result.append(num)
next_digit += 1
return sorted(result)
Comparison:
| Aspect | Sliding Window | Arithmetic |
|---|---|---|
| Uses strings | Yes (slicing) | No, pure math |
| Output order | Already sorted, free | Grouped by start digit |
| Extra sort | Not needed | sorted() required |
| Readability | Very high | High, slightly trickier |
| Complexity | O(1) time / O(1) space | O(1) time / O(1) space |
Both are constant time. Choose the sliding window for the free sorted output and readability. Choose arithmetic if you want to demonstrate that you can solve it without any string operations β a nice flex in an interview.
19. Edge Cases
- Single-point range (
low == high == 123): the inclusive<=check catches it.123is generated and kept. β - No valid answers in range (
low = 200, high = 220): no window ever lands inside, so we correctly return an empty list[]instead of crashing. - Numbers containing zero (like a hypothetical
8910): impossible by construction β the master string has no0, so such a number can never be generated. - Minimum bound (
low = 10): 2-digit candidates like12,23are correctly included. - Maximum bound (
high = 10^9): the full123456789is generated and included; we still do only constant work.
The beauty of the generate-and-filter design is that it makes bad cases impossible instead of handled one by one. You never write special-case code, because the invalid inputs simply never get generated.
20. Common Mistakes
- Brute-forcing the whole range. Looping
lowtohighpasses on small inputs but times out whenhighis near a billion. The structure of the answers is the whole point. - Off-by-one in the window bound. Writing
range(0, 9 - length)orrange(0, 10 - length + 1)either misses the last window or slices past the end of the string. The correct bound is10 - length. - Forgetting slicing is end-exclusive.
digits[0:4]gives four characters (1234), not five. Beginners expect the end index to be included. - Skipping the
int()conversion. A slice is a string. Comparing"1234" <= highagainst an integer either errors or compares incorrectly. Always convert. - Adding an unnecessary
sorted()to the sliding-window version. The loop order already produces sorted output. The redundant sort isn't wrong, but it shows you didn't notice the free ordering β and interviewers notice. - Trying to include lengths outside
[len(low), len(high)]. Testing every length 2β9 still works but does needless comparisons; bounding the lengths is cleaner reasoning.
21. Interview Questions
Expect follow-ups that probe whether you truly understand the structure:
- "Why is this O(1)?" Because there are only 36 sequential numbers total, regardless of input size.
- "How many sequential numbers exist in total?" 36 β the sum
8 + 7 + 6 + β¦ + 1. - "Can you solve it without converting to strings?" Yes β the arithmetic approach.
- "What if digits had to decrease by one instead?" Same idea, but the master string becomes
"9876543210"(and now0is allowed). - "What if the step could be any fixed
k, not just 1?" You'd generate candidates by starting at each digit and stepping byk, or generalize the master pattern. - "How would you return them in descending order?" Reverse the loop order, or reverse the final list.
22. Similar Problems
- LeetCode 1validation-style problems where output is small and structured β the generate-don't-search pattern recurs constantly.
- Next Greater Element / Next Permutation β building the next valid structured number from an existing one.
- Letter Combinations of a Phone Number (17) β generating a bounded set of candidates rather than searching a huge space.
- Combinations (77) β enumerating structured subsets, another "produce every valid shape" problem.
- Fizz Buzz / Count and Say β pattern-driven generation where understanding the shape of output beats brute iteration.
They all share the same lesson: recognize structure, then generate.
23. Key Takeaways
- Sequential Digits is O(1) because only 36 valid answers can ever exist.
- Every sequential number is a slice of
"123456789"β that single observation collapses the whole problem. - The sliding window turns "search a billion numbers" into "cut 36 substrings."
- Looping short lengths first gives you sorted output for free.
- The core interview lesson: when valid answers are rare and structured, generate them directly instead of searching for them.
24. Watch the Video
Reading the reasoning is one thing β seeing the window slide across the string makes it click permanently. Watch the full visual walkthrough here, where we trace every candidate and animate the window step by step: {LINK}
25. About the Series
This is part of Daily Python LeetCode β a series that turns one LeetCode problem a day into a clear, beginner-friendly lesson in Python, algorithms, data structures, and interview technique. Every entry builds intuition first, then code, so you learn how experienced engineers think, not just what to memorize. Follow along and you'll build real pattern recognition, one problem at a time.
26. Call To Action
If this made Sequential Digits finally make sense:
- βΆοΈ Subscribe on YouTube so you never miss a daily breakdown.
- βοΈ Subscribe to the Substack for the written deep-dives like this one.
- π¬ Comment with the problem you want explained next.
- π Share this with a friend who's grinding LeetCode β the generate-don't-search insight will save them hours.
SOCIAL MEDIA
LinkedIn Post
Sequential Digits (LeetCode 1291) is labeled a "medium," but it's secretly one of the most elegant easy problems you'll encounter β and it teaches a lesson that shows up constantly in real engineering.
The task: return every number in a range [low, high] whose digits each increase by exactly one, like 123 or 4567.
The instinct most of us reach for first: loop from low to high and test each number. It works. But high can be a billion β that's up to a billion checks to find, at most, 36 answers.
That imbalance is the whole insight.
There are only ever 36 sequential numbers in existence, because every single one is a slice of the string "123456789". So instead of searching an enormous range for rare answers, you generate the answers directly: slide a window across those nine characters, convert each slice to a number, and keep the ones in range.
The payoff: β’ O(1) time and O(1) space β the input size doesn't affect the work at all β’ The results come out already sorted, because you try short lengths first β’ Edge cases like zeros and empty ranges become impossible by construction, not special-cased
The deeper takeaway applies far beyond this one problem: when the set of valid answers is small and highly structured, generating them directly beats searching for them every time.
That mental shift β from "search the space" to "produce the shape" β is what separates brute force from clean solutions in interviews and in production.
What's your favorite "generate, don't search" problem?
#Python #LeetCode #CodingInterview #Algorithms #SoftwareEngineering
Twitter/X Thread
1/ Sequential Digits (LeetCode 1291) looks like a medium. It's secretly one of the easiest problems on the site β once you see one trick. π§΅
2/
The task: given low and high, return every number whose digits go up by exactly one. Like 123, 4567, 6789. Return them sorted.
3/
First instinct: loop from low to high, test each number. Fine⦠until you see high can be 1,000,000,000. That's a billion checks.
4/ For what? Let's count the valid answers. The longest sequential number is 123456789. Every answer is a chunk of it.
5/ Count them up: 8 of length 2, 7 of length 3, 6 of length 4β¦ down to 1. Total = 36. There are only 36 sequential numbers in all of existence. Ever.
6/ So we're scanning a billion numbers to find 36 answers. That's the signal. Stop searching. Start generating.
7/ Key insight: every sequential number is a slice of "123456789". Cut out every substring and you've generated every possible answer.
8/ The tool for cutting fixed-width chunks and sliding along: a sliding window. Width-3 window β 123, slide β 234, slide β 345β¦
9/
Only try lengths between the digit-count of low and high. Slide the window, int() each slice, keep the in-range ones. Short lengths first β output is sorted for FREE.
10/ Complexity? O(1) time, O(1) space β fixed 36 candidates no matter the input. The real lesson: when answers are rare and structured, generate them, don't search for them. Full breakdown + video π
Facebook Post
Ever hit a coding problem that looks scary but turns out to be genuinely fun? Sequential Digits (LeetCode 1291) is one of those. π
You're asked to find every number in a range whose digits climb by one β like 123 or 4567. The obvious approach is to check every number one by oneβ¦ but the range can go up to a BILLION. Yikes.
Here's the "aha": there are only 36 such numbers in the entire universe, because every one of them is just a piece of "123456789". So instead of searching forever, you slide a little window across those nine digits and grab every answer directly. It runs in constant time and the results even come out sorted automatically.
I broke the whole thing down step by step β the intuition, the Python code, a full dry run, and the edge cases β in a beginner-friendly way. If you're learning Python or prepping for interviews, come check it out. Would love to hear what clicked for you!
Reddit Summary
Sequential Digits (LeetCode 1291): a clean example of "generate, don't search"
Sharing a walkthrough of this problem because it's a nice teaching case for a pattern that shows up a lot.
The problem: return every number in [low, high] whose digits each increase by one (123, 4567, etc.), sorted.
The naive approach loops from low to high and tests each number. It works but high can be 1e9, so worst case ~a billion iterations.
The observation that changes everything: the longest possible sequential number is 123456789, and every sequential number is a contiguous substring of it. Counting them (8 + 7 + β¦ + 1) gives exactly 36 total, ever. So there's no reason to search β you can just generate all candidates by sliding a window across "123456789", converting each slice with int(), and filtering to the range.
Two things fall out for free:
- If you iterate lengths shortest-first, the output is already sorted (no
sorted()needed). - Zeros and out-of-range cases are impossible by construction rather than special-cased.
Both the sliding-window and pure-arithmetic versions run in O(1) time and space, which is unusual and kind of satisfying. The generalizable takeaway is the useful part: when valid outputs are rare and structured, producing them directly beats scanning a large space. Happy to discuss variations (decreasing digits, arbitrary step size) in the comments.
GITHUB README
# Sequential Digits β LeetCode 1291 (Python)
Return every integer in the inclusive range `[low, high]` whose digits are
sequential β each digit exactly one greater than the one before it.
## Problem
Given two integers `low` and `high`, return a **sorted** list of all
integers in `[low, high]` that have sequential digits.
A number has *sequential digits* if each digit is one more than the previous
digit (e.g. `123`, `4567`, `6789`).
**Example 1**
Input: low = 100, high = 300 Output: [123, 234]
**Example 2**
Input: low = 1000, high = 13000 Output: [1234, 2345, 3456, 4567, 5678, 6789, 12345]
## Intuition
`high` can be up to 1e9, so scanning every number in the range is wasteful β
there are only **36 sequential numbers in existence**. Every one of them is a
contiguous slice of the string `"123456789"`. So instead of *searching* a huge
range, we *generate* all valid candidates directly and filter by range.
## Approach
1. All answers live inside the master string `"123456789"`.
2. Only lengths between the digit-count of `low` and `high` can fit.
3. Slide a window of each length across the master string.
4. Convert each slice to an int; keep it if it falls in `[low, high]`.
5. Iterating shortest length first yields already-sorted output β no sort call.
## Python Solution
```python
class Solution:
def sequentialDigits(self, low: int, high: int) -> list[int]:
digits = "123456789"
result = []
for length in range(len(str(low)), len(str(high)) + 1):
for start in range(0, 10 - length):
candidate = int(digits[start:start + length])
if low <= candidate <= high:
result.append(candidate)
return result
Alternative (pure arithmetic, no strings)
class Solution:
def sequentialDigits(self, low: int, high: int) -> list[int]:
result = []
for start in range(1, 9):
num, nxt = start, start + 1
while nxt <= 9:
num = num * 10 + nxt
if low <= num <= high:
result.append(num)
nxt += 1
return sorted(result)
Complexity
| Metric | Value | Why |
|---|---|---|
| Time | O(1) | Only 36 candidates exist, regardless of input |
| Space | O(1) | Fixed 9-char string + bounded result list |
Video
πΊ Watch the visual walkthrough: {LINK}
Article
π Full written explanation with dry run, edge cases, and interview tips: {LINK}
Part of the Daily Python LeetCode series β one problem a day, explained from intuition to code. ```
The solution
def sequentialDigits(low, high):
res = []
for d in range(1, 9):
num, nxt = d, d + 1
while nxt <= 9:
num = num * 10 + nxt
if low <= num <= high:
res.append(num)
nxt += 1
return sorted(res)Ready to try it yourself? Solve Twopointer problems with instant feedback in the practice sandbox.
Practice now β


