Python Algorithms & Data Structures track

Lesson 04 · Part 2: Core Data Structures

Arrays & Strings

Slicing, string methods and ASCII handling

Part 2: Core Data Structures4 min readFree lesson

Introduction

An array is the most basic linear data structure that forms the foundation of all data structures. In Python, list acts as a dynamic array, allowing O(1) random access using indexes.

A string in Python is an immutable object. Once created, a string cannot be changed, and every time a += operation is repeated, a new string object is created. This characteristic can easily become a performance trap in coding tests, so it must be understood.

Index:  0    1    2    3    4
        +----+----+----+----+----+
Value:     | 10 | 20 | 30 | 40 | 50 |
        +----+----+----+----+----+
          ^                   ^
        arr[0]             arr[-1]

Learning Objectives

  1. Correctly understand the slicing syntax and complexity of Python list (dynamic array).
  2. Use major string methods and be able to avoid performance issues due to immutability.
  3. Using the two-pointer technique, solve Array/String problems such as palindrome checks in O(n) time.

Key Concepts

1. Slicing

arr = [0, 1, 2, 3, 4, 5]

arr[1:4]    #[1, 2, 3]  -- index 1 to less than 4
arr[:3]     #[0, 1, 2]  -- from the beginning to less than 3
arr[3:]     #[3, 4, 5]  -- from 3 to the end
arr[::-1]   #[5, 4, 3, 2, 1, 0]  -- reverse order
arr[::2]    #[0, 2, 4]  -- every 2 steps
arr[1:5:2]  #[1, 3]  -- from 1 to less than 5, every 2 steps
Operation Time Complexity Description
arr[i] O(1) Index Access
arr[i:j] O(j-i) Slicing (copy occurs)
arr.append(x) O(1) amortized Append at the end
arr.insert(i, x) O(n) Insert in the middle (movement needed)
arr.pop() O(1) Remove from the end
arr.pop(i) O(n) Remove from the middle (movement needed)
x in arr O(n) Linear Navigation
len(arr) O(1) Check length

Note: arr[i:j] slicing creates a new list copy with O(j-i) complexity.


2. String Methods

Method Description Example Return Value
split(sep) Split by delimiter "a,b".split(",") ["a","b"]
join(iter) Join iterable ",".join(["a","b"]) "a,b"
strip() Trim whitespace on both sides " ab ".strip() "ab"
lstrip() Trim left whitespace " ab ".lstrip() "ab "
rstrip() Trim right whitespace " ab ".rstrip() " ab"
replace(old,new) Replace string "abab".replace("a","x") "xbxb"
find(sub) Find substring position "hello".find("ll") 2
upper() Convert to uppercase "hello".upper() "HELLO"
lower() Convert to lowercase "HELLO".lower() "hello"
count(sub) Count occurrences "abab".count("ab") 2
startswith(s) Check prefix "hello".startswith("he") True
endswith(s) Check suffix "hello".endswith("lo") True

3. ASCII Code -- ord / chr

ord('A')   # 65
ord('a')   # 97
ord('0')   # 48

chr(65)    # 'A'
chr(97)    # 'a'

#Alphabet index (0-based)
idx = ord('c') - ord('a')   # 2
Character Range ASCII Start
'0' ~ '9' 48 ~ 57
'A' ~ 'Z' 65 ~ 90
'a' ~ 'z' 97 ~ 122

4. Two Pointer (Two Pointer) -- Palindrome Check

String: r a c e c a r
Index: 0 1 2 3 4 5 6
        ^               ^
       left            right

Compare r==r -> move
       ^           ^
Compare a==a -> move
         ^       ^
Compare c==c -> move
           ^   ^
Compare e==e -> move
left > right -> palindrome!

Using two pointers, you can check for a palindrome in O(n) time and O(1) space.


Examples

File Content Key Concept
src/01_slicing.py Slicing Basics + 2D Example Indexing, Copy, Reverse
src/02_string_methods.py String Method Practice split, join, replace
src/03_palindrome.py Palindrome Check (Naive + Two Pointer) two-pointer, O(n)
src/04_anagram.py Anagram Check Counter, sorted

Common Mistakes

Mistake 1: Repeated Use of string += -- O(n^2) Pitfall

#Incorrect method -- O(n^2): creating a new string object each time
result = ""
for c in chars:
    result += c  #O(n) copy each time!

#Correct method -- O(n): collect in a list and join
parts = []
for c in chars:
    parts.append(c)
result = "".join(parts)  #Copy only once

Mistake 2: Misunderstanding in Operator Complexity

arr = [1, 2, 3, ..., 1000000]
if x in arr:   #O(n) linear search!
    ...

#If searched frequently, use set or dict -> O(1)
lookup = set(arr)
if x in lookup:   # O(1)
    ...

Mistake 3: Forgetting that Slicing Creates a Copy

arr = [1, 2, 3, 4, 5]
sub = arr[1:4]   #Copy a new list, O(k) time + O(k) space
sub[0] = 99
print(arr)   #[1, 2, 3, 4, 5] -- original unchanged

Mistake 4: split() vs split(' ')

"a  b".split()    #['a', 'b']  -- ignore consecutive spaces
"a  b".split(' ') #['a', '', 'b']  -- includes empty string!

Mistake 5: Confusion with ord() when Comparing String and Integer

#Incorrect example
if 'a' > 'Z':   #True -- ASCII value 97 > 90
    ...

#Use lower()/upper() for intended behavior
if 'a'.lower() == 'A'.lower():   # True
    ...

Summary

Concept Key Point
Dynamic Array Python list, Random Access O(1), Insert/Delete O(n)
Slicing O(k) Copy Occurs, Original Immutable
String Immutable Object, Repeated += is O(n^2)
join() String Combination Always Use join() O(n)
Two Pointer Technique Narrowing from Both Ends, O(n) Time O(1) Space
ord/chr Character<->ASCII Conversion, Alphabet Index Calculation

Try it yourself

  1. Run src/01_slicing.py and try to predict the results of each slicing.
  2. Draw the two-pointer process by hand for src/03_palindrome.py.
  3. Implement src/04_anagram.py directly using a dictionary without Counter.
  4. Solve the exercises in the assignment below.

Example files

The runnable files used in this lesson. Copy them into your own project and experiment.

src/01_slicing.pyPython
#Topic : Basics of Slicing and 2D Array Slicing
#Time  : O(k) -- proportional to the slicing length k
#Space : O(k) -- size of the slicing result copy


def demonstrate_1d_slicing() -> None:
    """1-dimensional list slicing example."""
    arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    print("=" * 45)
    print("Original Array:", arr)
    print("=" * 45)

    #Basic Slicing
    print("arr[2:6]   :", arr[2:6])     # [2, 3, 4, 5]
    print("arr[:4]    :", arr[:4])      # [0, 1, 2, 3]
    print("arr[6:]    :", arr[6:])      # [6, 7, 8, 9]
    print("arr[:]     :", arr[:])       #Full Copy

    #Negative Index
    print()
    print("arr[-1]    :", arr[-1])      #9 (last)
    print("arr[-3:]   :", arr[-3:])     # [7, 8, 9]
    print("arr[:-3]   :", arr[:-3])     # [0..6]

    #Step Slicing
    print()
    print("arr[::2]   :", arr[::2])     #Even Index
    print("arr[1::2]  :", arr[1::2])    #Odd Index
    print("arr[::-1]  :", arr[::-1])    #Reverse Copy
    print("arr[8:1:-2]:", arr[8:1:-2])  #Reverse Step


def demonstrate_copy_semantics() -> None:
    """Slicing creates a shallow copy."""
    print()
    print("=" * 45)
    print("Check Copy Semantics (Shallow Copy)")
    print("=" * 45)

    original = [10, 20, 30, 40, 50]
    sliced = original[1:4]     #[20, 30, 40] -- new list
    sliced[0] = 999

    print("original:", original)   #No change to original
    print("sliced  :", sliced)     # [999, 30, 40]
    print("-> The slicing result is an independent new list.")

    #Beware of shallow copy of nested lists
    nested = [[1, 2], [3, 4], [5, 6]]
    copied = nested[:]         #Outer list is copied
    copied[0][0] = 999         #Inner list is shared!
    print()
    print("nested (check inner sharing):", nested)   # [[999, 2], ...]
    print("-> Use copy.deepcopy() for nested lists.")


def demonstrate_2d_slicing() -> None:
    """Example of slicing a 2-dimensional array (a list of lists)."""
    print()
    print("=" * 45)
    print("2D Array Slicing")
    print("=" * 45)

    matrix = [
        [1,  2,  3,  4],
        [5,  6,  7,  8],
        [9, 10, 11, 12],
    ]

    print("Original Matrix:")
    for row in matrix:
        print(" ", row)

    #Row Slicing
    sub_rows = matrix[0:2]          #Rows 0~1
    print("\nmatrix[0:2] -- top 2 rows:")
    for row in sub_rows:
        print(" ", row)

    #Column Slicing (using list comprehension)
    col_2 = [row[2] for row in matrix]   #Column 2
    print("\nColumn 2:", col_2)

    #Submatrix
    sub_matrix = [row[1:3] for row in matrix[0:2]]
    print("\nmatrix[0:2][column 1:3] -- 2x2 submatrix:")
    for row in sub_matrix:
        print(" ", row)


def demonstrate_string_slicing() -> None:
    """String slicing -- strings are also sequences."""
    print()
    print("=" * 45)
    print("String Slicing")
    print("=" * 45)

    s = "Hello, World!"
    print("Original:", s)
    print("s[7:12] :", s[7:12])    # World
    print("s[:5]   :", s[:5])      # Hello
    print("s[::-1] :", s[::-1])    #Reverse
    print("s[::2]  :", s[::2])     #Step of 2


if __name__ == "__main__":
    demonstrate_1d_slicing()
    demonstrate_copy_semantics()
    demonstrate_2d_slicing()
    demonstrate_string_slicing()
src/02_string_methods.pyPython
#Topic : Practice of Major String Methods
#Time  : Refer to comments for each method
#Space : O(n) -- Most return a new string


def demo_split_join() -> None:
    """split / join methods -- O(n)."""
    print("=" * 50)
    print("split / join")
    print("=" * 50)

    sentence = "apple, banana, cherry, strawberry"
    fruits = sentence.split(",")        #Split by delimiter
    print("split(','):", fruits)

    #Without delimiter -- Split by whitespace, ignore consecutive spaces
    words = "  hello   world  ".split()
    print("split()   :", words)         # ['hello', 'world']

    #Difference between split(' ') vs split()
    spaced = "a  b".split(' ')          #Includes empty strings!
    print("split(' ') :", spaced)       # ['a', '', 'b']

    #join -- Convert list to string
    joined = " | ".join(fruits)
    print("join(' | '):", joined)

    #String accumulation -- join is O(n), repeated += is O(n^2)
    chars = list("Hello")
    fast = "".join(chars)               # O(n)
    print("''.join()  :", fast)


def demo_strip() -> None:
    """strip / lstrip / rstrip -- O(n)."""
    print()
    print("=" * 50)
    print("strip / lstrip / rstrip")
    print("=" * 50)

    s = "   Hello, Python!   "
    print("Original         :", repr(s))
    print("strip()      :", repr(s.strip()))
    print("lstrip()     :", repr(s.lstrip()))
    print("rstrip()     :", repr(s.rstrip()))

    #Remove specific character
    s2 = "###Important###"
    print("strip('#')   :", s2.strip("#"))


def demo_replace_find() -> None:
    """replace / find / count -- O(n)."""
    print()
    print("=" * 50)
    print("replace / find / count")
    print("=" * 50)

    text = "banana"
    print("Original :", text)
    print("replace('a','o')  :", text.replace("a", "o"))       # bonono
    print("replace('a','o',2):", text.replace("a", "o", 2))    #Up to 2 times

    #find -- Returns -1 if not found
    s = "Hello, World!"
    print("find('World') :", s.find("World"))   # 7
    print("find('xyz')   :", s.find("xyz"))     # -1
    print("index('World'):", s.index("World"))  #7 (Raises ValueError if not found)

    #rfind -- Search from right
    t = "abcabcabc"
    print("rfind('abc')  :", t.rfind("abc"))    # 6
    print("count('abc')  :", t.count("abc"))    # 3


def demo_case_methods() -> None:
    """upper / lower / capitalize / title -- O(n)."""
    print()
    print("=" * 50)
    print("Case conversion")
    print("=" * 50)

    s = "hello, World!"
    print("Original         :", s)
    print("upper()      :", s.upper())
    print("lower()      :", s.lower())
    print("capitalize() :", s.capitalize())   #Capitalize first letter only
    print("title()      :", s.title())        #Capitalize first letter of each word
    print("swapcase()   :", s.swapcase())     #Swap upper<->lower


def demo_check_methods() -> None:
    """isdigit / isalpha / isalnum / startswith / endswith -- O(n)."""
    print()
    print("=" * 50)
    print("Check methods")
    print("=" * 50)

    samples = ["hello", "Hello123", "12345", "  ", ""]
    for s in samples:
        print(
            f"{s!r:12} -> isalpha={str(s.isalpha()):5}"
            f" isdigit={str(s.isdigit()):5}"
            f" isalnum={str(s.isalnum()):5}"
        )

    url = "https://example.com"
    print()
    print("startswith('https'):", url.startswith("https"))
    print("endswith('.com')   :", url.endswith(".com"))


def demo_ord_chr() -> None:
    """ord / chr -- ASCII code conversion."""
    print()
    print("=" * 50)
    print("ord() / chr() -- ASCII code")
    print("=" * 50)

    print("ord('A') =", ord("A"), "/ ord('a') =", ord("a"), "/ ord('0') =", ord("0"))
    print("chr(65)  =", chr(65),  "/ chr(97)  =", chr(97),  "/ chr(48)  =", chr(48))

    #Alphabet index (0-based)
    for ch in "ace":
        idx = ord(ch) - ord("a")
        print(f"Alphabet index of '{ch}': {idx}")

    #Print all alphabets
    alphabet = [chr(ord("a") + i) for i in range(26)]
    print("Alphabets:", "".join(alphabet))


if __name__ == "__main__":
    demo_split_join()
    demo_strip()
    demo_replace_find()
    demo_case_methods()
    demo_check_methods()
    demo_ord_chr()
src/03_palindrome.pyPython
#Topic: Palindrome Check
#Time: O(n) -- proportional to the length of the string
#Space: O(n) naive / O(1) two-pointers


def is_palindrome_naive(s: str) -> bool:
    """Naive method: Compare with reversed string -- Time O(n), Space O(n)."""
    cleaned = s.lower()
    return cleaned == cleaned[::-1]


def is_palindrome_two_pointer(s: str) -> bool:
    """Two-pointer method -- Time O(n), Space O(1).

    Visualization:
        r a c e c a r
        ^           ^   r==r -> move
          ^       ^     a==a -> move
            ^   ^       c==c -> move
              ^ ^        e==e -> left >= right -> palindrome!"""
    left, right = 0, len(s) - 1
    while left < right:
        if s[left] != s[right]:
            return False
        left += 1
        right -= 1
    return True


def is_palindrome_alphanumeric(s: str) -> bool:
    """Palindrome considering only letters/numbers -- LeetCode 125 style.

    Example: 'A man, a plan, a canal: Panama' -> True
    Time O(n), Space O(1) (two pointers)."""
    left, right = 0, len(s) - 1
    while left < right:
        #Skip non-alphanumeric characters
        while left < right and not s[left].isalnum():
            left += 1
        while left < right and not s[right].isalnum():
            right -= 1
        if s[left].lower() != s[right].lower():
            return False
        left += 1
        right -= 1
    return True


def longest_palindrome_expand(s: str) -> str:
    """Center Expansion (Expand Around Center) -- O(n^2) time, O(1) space."""
    def expand(left: int, right: int) -> str:
        while left >= 0 and right < len(s) and s[left] == s[right]:
            left -= 1
            right += 1
        return s[left + 1:right]

    best = ""
    for i in range(len(s)):
        odd = expand(i, i)          #Odd-length palindrome
        even = expand(i, i + 1)     #Even-length palindrome
        if len(odd) > len(best):
            best = odd
        if len(even) > len(best):
            best = even
    return best


if __name__ == "__main__":
    print("=" * 50)
    print("Palindrome check")
    print("=" * 50)

    test_cases = [
        ("racecar", True),
        ("hello",   False),
        ("abcba",   True),
        ("a",       True),
        ("",        True),
        ("abba",    True),
    ]

    print(f"{'String':15} {'Naive':8} {'Two-pointers':10}")
    print("-" * 35)
    for word, expected in test_cases:
        naive = is_palindrome_naive(word)
        tp    = is_palindrome_two_pointer(word)
        status = "OK" if naive == expected else "NG"
        print(f"{word!r:15} {str(naive):8} {str(tp):10} {status}")

    print()
    print("=" * 50)
    print("Alphanumeric palindrome")
    print("=" * 50)
    alphanums = [
        "A man, a plan, a canal: Panama",
        "race a car",
        " ",
    ]
    for s in alphanums:
        short = repr(s)[:35]
        print(f"{short:37} -> {is_palindrome_alphanumeric(s)}")

    print()
    print("=" * 50)
    print("Longest palindromic substring")
    print("=" * 50)
    for s in ["babad", "cbbd", "racecar", "abcba"]:
        result = longest_palindrome_expand(s)
        print(f"{s!r:10} -> {result!r}")
src/04_anagram.pyPython
#Topic: Anagram Check
#Time: O(n) Counter method / O(n log n) sorted method
#Space: O(1) -- 26 fixed alphabets (Counter method)

from collections import Counter


def is_anagram_sorted(s: str, t: str) -> bool:
    """Comparison using sorted -- Time O(n log n), Space O(n).

    If two strings are sorted and are the same, they are anagrams."""
    return sorted(s.lower()) == sorted(t.lower())


def is_anagram_counter(s: str, t: str) -> bool:
    """Counter comparison -- Time O(n), Space O(1) (fixed 26 letters of the alphabet).

    Counter stores the frequency of each character in a dictionary."""
    return Counter(s.lower()) == Counter(t.lower())


def is_anagram_manual(s: str, t: str) -> bool:
    """Direct dictionary implementation -- Time O(n), Space O(1).

    How to implement without Counter."""
    if len(s) != len(t):
        return False

    freq: dict[str, int] = {}
    for ch in s.lower():
        freq[ch] = freq.get(ch, 0) + 1
    for ch in t.lower():
        freq[ch] = freq.get(ch, 0) - 1
        if freq[ch] < 0:
            return False
    return True


def is_anagram_array(s: str, t: str) -> bool:
    """Using an array (size 26) -- Time O(n), Space O(1).

    The fastest implementation when only lowercase letters are allowed."""
    if len(s) != len(t):
        return False

    count = [0] * 26
    for ch in s:
        count[ord(ch) - ord("a")] += 1
    for ch in t:
        count[ord(ch) - ord("a")] -= 1
    return all(c == 0 for c in count)


def group_anagrams(words: list[str]) -> list[list[str]]:
    """Anagram Grouping -- Time O(n * k log k), Space O(n*k).

    k = maximum length of a word.
    Group using the sorted string as a key."""
    groups: dict[str, list[str]] = {}
    for word in words:
        key = "".join(sorted(word.lower()))
        groups.setdefault(key, []).append(word)
    return list(groups.values())


def find_all_anagram_positions(text: str, pattern: str) -> list[int]:
    """Return all anagram positions of a pattern in a string -- O(n) sliding window.

    A list of indices where an anagram of the pattern starts in the text."""
    result = []
    n, k = len(text), len(pattern)
    if k > n:
        return result

    pattern_cnt = Counter(pattern)
    window_cnt  = Counter(text[:k])

    if window_cnt == pattern_cnt:
        result.append(0)

    for i in range(1, n - k + 1):
        #Window sliding: remove left character, add right character
        left_char  = text[i - 1]
        right_char = text[i + k - 1]

        window_cnt[right_char] += 1
        window_cnt[left_char]  -= 1
        if window_cnt[left_char] == 0:
            del window_cnt[left_char]

        if window_cnt == pattern_cnt:
            result.append(i)

    return result


if __name__ == "__main__":
    print("=" * 50)
    print("Anagram check")
    print("=" * 50)

    pairs = [
        ("listen",  "silent",  True),
        ("hello",   "world",   False),
        ("rat",     "car",     False),
        ("anagram", "nagaram", True),
    ]

    print(f"{'s':12} {'t':12} {'sorted':8} {'counter':9} {'manual':8}")
    print("-" * 51)
    for s, t, expected in pairs:
        r1 = is_anagram_sorted(s, t)
        r2 = is_anagram_counter(s, t)
        r3 = is_anagram_manual(s, t)
        ok = "OK" if r1 == expected else "NG"
        print(f"{s:12} {t:12} {str(r1):8} {str(r2):9} {str(r3):8} {ok}")

    print()
    print("=" * 50)
    print("Anagram grouping")
    print("=" * 50)
    words = ["eat", "tea", "tan", "ate", "nat", "bat"]
    groups = group_anagrams(words)
    print("Input:", words)
    for g in groups:
        print(" ", sorted(g))

    print()
    print("=" * 50)
    print("Sliding window anagram position")
    print("=" * 50)
    text, pat = "cbaebabacd", "abc"
    positions = find_all_anagram_positions(text, pat)
    print(f"text={text!r}, pattern={pat!r}")
    print("Anagram start position:", positions)   # [0, 6]

Assignment

Pre-Submission Checklist

  • Have you manually simulated all test cases?
  • Have you indicated the time complexity in comments?
  • Have you considered edge cases (empty input, single character, etc.)?

Problem 1: Baekjoon 10808 -- Count of Alphabets (Bronze I)

Link: https://www.acmicpc.net/problem/10808

Problem Description

Given a word, print the number of a's, b's, ..., z's included in the word, separated by spaces.

Input

The first line contains a word S consisting only of lowercase letters. (1 <= |S| <= 100)

Output

Print 26 integers separated by spaces.

Example

Input: baekjoon
Output: 1 0 0 0 1 0 0 0 0 1 1 0 0 1 2 0 0 0 0 0 0 0 0 0 0 0

Hint

  • Use ord(c) - ord('a') to get the alphabet index.
  • Either create an array of size 26 to count, or use Counter.
  • Time O(n), Space O(1)

Problem 2: Baekjoon 1316 -- Group Word Checker (Silver V)

Link: https://www.acmicpc.net/problem/1316

Problem Description

A group word is a word in which each letter appears consecutively. For example, ccazzz is a group word because c, a, and z appear consecutively, but aabba is not a group word because a appears separated by b.

Given N words as input, output the number of group words.

Input

3
ccazzz
aabba
aaabab

Output

1

Hint

  • Traverse each word and compare the previous character with the current character.
  • When a character changes, if the character has appeared before, it is not a group word.
  • Use a set of visited characters.
  • Time O(n * k), Space O(26) = O(1) (k = maximum length of a word)

Answer File

  • boj_10808.py
  • boj_1316.py
Show the reference solution

Try the assignment yourself first — comparing your attempt with a working solution is where most of the learning happens.

boj_10808.pyPython
#Baekjoon 10808 -- Number of Alphabets
#Time: O(n) -- Length of the string n
#Space: O(1) -- Size 26 array (number of alphabets fixed)
#
#Solution: Accumulate the occurrence count of each alphabet in an array of size 26.
#Convert to index a=0, b=1, ..., z=25 with ord(c) - ord('a').

import sys

input = sys.stdin.readline


def solve() -> None:
    s = input().strip()
    count = [0] * 26
    for ch in s:
        count[ord(ch) - ord("a")] += 1
    print(*count)


solve()
boj_1316.pyPython
#Baekjoon 1316 -- Group Word Checker
#Time : O(N * K) -- N words, each word up to K letters
#Space: O(1)    -- Size of visited set is less than or equal to the number of alphabets (26)
#
#Solution:
#As you traverse each word, the moment a character changes,
#if a character that has already been visited appears again, it is not a group word.

import sys

input = sys.stdin.readline


def is_group_word(word: str) -> bool:
    """Returns whether it is a group word."""
    seen: set[str] = set()
    prev = ""
    for ch in word:
        if ch != prev:            #Character changed
            if ch in seen:        #Previously seen character appears again
                return False
            seen.add(prev)
            prev = ch
    return True


def solve() -> None:
    n = int(input())
    answer = 0
    for _ in range(n):
        word = input().strip()
        if is_group_word(word):
            answer += 1
    print(answer)


solve()

Learn this lesson with a mentor

Work through the Python Algorithms & Data Structures track on your own, or join a guided cohort at our Pitampura campus and online.

Call NowEnroll Now