Python Indexing and Slicing

· Seokhyeon Byun

Note: This post is based on my old programming study notes when I taught myself.

Sequence Operations

  • len(): Returns the number of items in a sequence. When the length of a sequence is $n$, the index set is 0~$n$-1

Indexing

# Indexing Example
a = "Life is too short"
a[0] = L, a[5] = i  # 0th item of string a
a[-1] = t, a[-2] = r  # reverse indexing
  • The first index is 0
  • When processing indexes in reverse from the end, you can specify indexes like -1, -2, -3, etc.

Slicing

  • Function that cuts from start to just before end based on index
  • A[x:y:z] - x or more, less than y, z interval
  • B[n:] - from n index to the end
  • C[:m] - from beginning to (m-1)th index value
  • D[:] - from beginning to end

Examples

# Example 1)
b = "20010331Rainy"  # string including numbers and character

b[:8] = 20010331  # Returns a value starting from 0th to before 8th index

# Warning: it looks like integer but type of returned value of b[:8] is still string!!!

b[8:] = Rainy  # from 8th index to the end

# Example 2)
c = "12345678"
c[::1] = 12345678   # from beginning to end with 1 space interval
c[::2] = 1357       # every 2nd character
c[::-1] = 87654321  # from beginning to end in reverse order
c[::-2] = 7531      # every 2nd character in reverse

Key Points

  • Index Range: For a sequence of length n, valid indexes are 0 to n-1
  • Negative Indexing: -1 refers to last element, -2 to second-to-last, etc.
  • Slicing Syntax: [start:stop:step]
    • start: inclusive starting position
    • stop: exclusive ending position
    • step: interval between elements
  • Default Values: Omitted values use defaults (0 for start, length for stop, 1 for step)
  • Reverse Slicing: Use negative step values to reverse direction

Advanced Slicing Techniques

Multi-dimensional Slicing

# Working with lists of lists
matrix = [
    [1, 2, 3, 4],
    [5, 6, 7, 8],
    [9, 10, 11, 12]
]

# Access specific row
first_row = matrix[0]       # [1, 2, 3, 4]
last_row = matrix[-1]       # [9, 10, 11, 12]

# Access specific element
element = matrix[1][2]      # 7 (row 1, column 2)

# Access column (requires list comprehension)
first_column = [row[0] for row in matrix]  # [1, 5, 9]

# Slice rows
first_two_rows = matrix[:2]  # [[1, 2, 3, 4], [5, 6, 7, 8]]

# Slice within rows
partial_rows = [row[1:3] for row in matrix]  # [[2, 3], [6, 7], [10, 11]]

String Slicing Patterns

text = "Programming Python"

# Extract words
words = text.split()        # ['Programming', 'Python']
first_word = text.split()[0]  # 'Programming'

# Extract substring between positions
middle = text[5:10]         # 'ammin'

# Extract every nth character
every_second = text[::2]    # 'Pormig yhn'
every_third = text[::3]     # 'Prgmn to'

# Extract first/last n characters
first_5 = text[:5]          # 'Progr'
last_5 = text[-5:]          # 'ython'

# Remove first/last n characters  
without_first_last = text[1:-1]  # 'rogramming Pytho'

Technical Interview Essentials

Time Complexity of Indexing & Slicing

Time Complexities (Critical for Interviews):

# List operations
my_list = [1, 2, 3, 4, 5]

# Indexing: O(1) - direct memory access
item = my_list[2]           # O(1)
item = my_list[-1]          # O(1)

# Slicing: O(k) where k = number of elements returned
sublist = my_list[1:4]      # O(3) = O(k)
reversed_list = my_list[::-1]  # O(n)

# String operations
my_string = "hello world"

# String indexing: O(1)
char = my_string[0]         # O(1)

# String slicing: O(k) - creates new string
substring = my_string[1:5]  # O(4) = O(k)
reversed_str = my_string[::-1]  # O(n)

Common Interview Patterns

1. Two Pointers Pattern

def reverse_string(s):
    """
    Reverse string in-place using two pointers.
    Time: O(n), Space: O(1)
    """
    s = list(s)  # Convert to list since strings are immutable
    left, right = 0, len(s) - 1
    
    while left < right:
        s[left], s[right] = s[right], s[left]
        left += 1
        right -= 1
    
    return ''.join(s)

def is_palindrome(s):
    """
    Check if string is palindrome using two pointers.
    Time: O(n), Space: O(1)
    """
    left, right = 0, len(s) - 1
    
    while left < right:
        if s[left] != s[right]:
            return False
        left += 1
        right -= 1
    
    return True

# Test
print(reverse_string("hello"))    # "olleh"
print(is_palindrome("racecar"))   # True
print(is_palindrome("hello"))     # False

2. Sliding Window Pattern

def max_sum_subarray(arr, k):
    """
    Find maximum sum of subarray of size k.
    Time: O(n), Space: O(1)
    """
    if len(arr) < k:
        return None
    
    # Calculate sum of first window
    window_sum = sum(arr[:k])
    max_sum = window_sum
    
    # Slide window and update sum
    for i in range(k, len(arr)):
        window_sum = window_sum - arr[i-k] + arr[i]
        max_sum = max(max_sum, window_sum)
    
    return max_sum

def longest_substring_without_repeating(s):
    """
    Find length of longest substring without repeating characters.
    Time: O(n), Space: O(min(m,n)) where m is character set size
    """
    char_set = set()
    left = 0
    max_length = 0
    
    for right in range(len(s)):
        # Shrink window until no duplicates
        while s[right] in char_set:
            char_set.remove(s[left])
            left += 1
        
        char_set.add(s[right])
        max_length = max(max_length, right - left + 1)
    
    return max_length

# Test
print(max_sum_subarray([1, 2, 3, 4, 5], 3))      # 12 (3+4+5)
print(longest_substring_without_repeating("abcabcbb"))  # 3 ("abc")

3. Array Rotation Using Slicing

def rotate_array_right(arr, k):
    """
    Rotate array to right by k positions using slicing.
    Time: O(n), Space: O(n)
    """
    if not arr or k == 0:
        return arr
    
    n = len(arr)
    k = k % n  # Handle k > n
    
    return arr[-k:] + arr[:-k]

def rotate_array_left(arr, k):
    """
    Rotate array to left by k positions using slicing.
    Time: O(n), Space: O(n)
    """
    if not arr or k == 0:
        return arr
    
    n = len(arr)
    k = k % n  # Handle k > n
    
    return arr[k:] + arr[:k]

# In-place rotation without extra space
def rotate_array_inplace(arr, k):
    """
    Rotate array in-place using reversal method.
    Time: O(n), Space: O(1)
    """
    def reverse(start, end):
        while start < end:
            arr[start], arr[end] = arr[end], arr[start]
            start += 1
            end -= 1
    
    n = len(arr)
    k = k % n
    
    # Reverse entire array
    reverse(0, n - 1)
    # Reverse first k elements
    reverse(0, k - 1)
    # Reverse remaining elements
    reverse(k, n - 1)
    
    return arr

# Test
print(rotate_array_right([1, 2, 3, 4, 5], 2))  # [4, 5, 1, 2, 3]
print(rotate_array_left([1, 2, 3, 4, 5], 2))   # [3, 4, 5, 1, 2]

Common Interview Gotchas

1. Index Out of Bounds

# Safe indexing techniques
def safe_get(lst, index, default=None):
    """Safely get element at index, return default if out of bounds."""
    try:
        return lst[index]
    except IndexError:
        return default

# Alternative using length check
def safe_get_alt(lst, index, default=None):
    """Alternative safe get using bounds checking."""
    if 0 <= index < len(lst):
        return lst[index]
    elif -len(lst) <= index < 0:  # Handle negative indices
        return lst[index]
    else:
        return default

# Test edge cases
test_list = [1, 2, 3]
print(safe_get(test_list, 10))     # None
print(safe_get(test_list, -10))    # None
print(safe_get(test_list, 1))      # 2

2. Slicing Edge Cases

# Slicing never raises IndexError - important for interviews!
test_str = "hello"

print(test_str[10:20])   # "" - empty string, no error!
print(test_str[-10:3])   # "hel" - handles out of bounds gracefully
print(test_str[3:100])   # "lo" - stops at end

# Step must not be zero
try:
    result = test_str[::0]  # ValueError!
except ValueError as e:
    print(f"Error: {e}")

# Empty sequences
empty_list = []
print(empty_list[:5])    # [] - still empty
print(empty_list[-5:])   # [] - still empty

3. Immutable vs Mutable Slicing

# String slicing creates new objects (immutable)
original_str = "hello"
sliced_str = original_str[1:4]  # Creates new string "ell"
modified_str = original_str[:2] + "y"  # Creates new string "hey"

# List slicing creates new objects but elements may be shared
original_list = [[1, 2], [3, 4], [5, 6]]
sliced_list = original_list[1:]  # New list: [[3, 4], [5, 6]]

# But inner lists are shared!
sliced_list[0].append(7)
print(original_list)  # [[1, 2], [3, 4, 7], [5, 6]] - original affected!

# Deep copy for complete independence
import copy
deep_sliced = copy.deepcopy(original_list[1:])

Performance Tips for Interviews

1. Efficient String Operations

# BAD - O(n²) due to string immutability
def build_string_slow(words):
    result = ""
    for word in words:
        result += word + " "
    return result.strip()

# GOOD - O(n) using join
def build_string_fast(words):
    return " ".join(words)

# For character-level operations, use list
def process_chars_fast(s):
    chars = list(s)  # O(n) conversion
    # ... modify chars in-place ...
    return ''.join(chars)  # O(n) conversion back

2. Memory-Efficient Slicing

# For large sequences, consider iterators
def process_large_slice(large_list, start, end):
    # Instead of creating a slice: large_list[start:end]
    # Use iterator to save memory
    for i in range(start, min(end, len(large_list))):
        yield large_list[i]

# Example usage
large_data = list(range(1000000))
for item in process_large_slice(large_data, 1000, 2000):
    # Process without creating intermediate slice
    pass