Python
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
447: Solving Array and String Problems
A few years ago, I was reviewing a PR for a colleague who was building a feature to detect "bursts" of activity in a server log. He had written a nested loop that scanned a list of timestamps to find the longest sequence where events happened within 5 seconds of each other. On his local machine with a few hundred test logs, it was instantaneous. But once we deployed it to a production environment with millions of entries, the process hung, the CPU spiked, and the monitoring alerts started screaming. He had accidentally built an $O(n^2)$ algorithm for a problem that only required $O(n)$. That's the danger of array and string problems; the "obvious" solution is often the one that crashes your server.
The Power of Two Pointers
When you're staring at a linear data structure and your instinct is to write a loop inside a loop, stop. Most of the time, you can replace that inner loop by using two pointers moving at different speeds or from different directions. I use this constantly when dealing with sorted arrays. Instead of checking every possible pair, you place one pointer at the start and one at the end, moving them inward based on a condition.
# Example: Finding if two numbers in a sorted list sum to a target
def has_target_sum(nums, target):
left, right = 0, len(nums) - 1
while left < right:
current_sum = nums[left] + nums[right]
if current_sum == target:
return True
elif current_sum < target:
left += 1 # Need a larger sum, move the left pointer up
else:
right -= 1 # Need a smaller sum, move the right pointer down
return False
It's a simple shift in thinking, but it turns a potentially sluggish operation into a lightning-fast one. You aren't blindly searching; you're strategically narrowing the search space.
Sliding Windows for Contiguous Sequences
The "two-pointer" concept evolves into a sliding window when you're dealing with sub-segments of a string or array. Think of it like a camera lens that expands and contracts as it moves across the data. This is exactly how you'd solve that server log problem my colleague struggled with. Instead of re-scanning the list for every single element, you maintain a window that represents your current valid sequence.
If the window no longer meets your criteria (say, the time gap becomes too large), you don't restart the whole process. You simply slide the left boundary of the window forward until the criteria are met again. You're effectively processing each element exactly twice—once when the right edge hits it, and once when the left edge passes it.
Mapping Frequencies to Avoid Re-scanning
Strings are just arrays of characters, but they bring their own set of headaches, especially when you need to track occurrences. I see developers spend way too much time using `.count()` inside a loop, which is a silent performance killer because `.count()` is itself a loop. Instead, use a dictionary or collections.Counter to create a frequency map in a single pass.
from collections import Counter
def find_first_non_repeating(s):
# One pass to build the map: O(n)
counts = Counter(s)
# Second pass to find the first char with count 1: O(n)
for char in s:
if counts[char] == 1:
return char
return None
By trading a little bit of memory (the dictionary) for speed, you avoid the $O(n^2)$ trap. In the professional world, we almost always trade memory for time, because RAM is cheap but user patience is not.
📋 Practical Task
The Unique Substring Length Validator
You are building a tool to analyze DNA sequences (represented as strings). You need to find the length of the longest substring that contains no repeating characters. For example, in the sequence "abcabcbb", the longest unique substring is "abc" with a length of 3. In "bbbbb", it is "b" with a length of 1.
Implement a function longest_unique_sequence(sequence) that uses a sliding window approach to solve this in $O(n)$ time. Avoid using nested loops that re-scan the string from the beginning.
def longest_unique_sequence(sequence):
# Your code here
pass
# Test cases to verify your logic:
print(longest_unique_sequence("abcabcbb")) # Expected: 3
print(longest_unique_sequence("pwwkew")) # Expected: 3
print(longest_unique_sequence("abcdef")) # Expected: 6
print(longest_unique_sequence("")) # Expected: 0
There are no comments for now.