Tư duy Tính toán

Vòng lặp (Loops)

Trường ĐH Công nghệ – Đại học Quốc gia Hà Nội

Nội dung

  1. Vòng lặp for
  2. Mẫu tích luỹ (Accumulator Pattern)
  3. Dải số (range)
  4. Lặp theo chỉ số (vs. theo phần tử)
  5. Danh sách & vòng lặp lồng nhau
  6. Thao tác trên danh sách lồng nhau
  7. Dữ liệu dạng bảng

1.
Vòng lặp for

Bạn còn nhớ trò chơi này?

Đưa chú chim tới chú heo và tránh thuốc nổ TNT:

Vòng lặp for trong Python:


for i in range(3):
    move_forward()
        

Ví dụ

In từng phần tử của danh sách lst


print("The list contains:")
print(lst[0])   # in phần tử ở chỉ số 0
print(lst[1])   # in phần tử ở chỉ số 1
# ...
    

‼️ Vấn đề: Ta không biết phải dừng lại ở đâu!

Chúng ta cần lặp lại

Ta thường cần lặp lại hành động:

  • In mọi phần tử của danh sách (kích thước chưa biết)
  • Gửi email cho toàn bộ khách hàng
  • Kiểm tra chính tả cho mọi từ trong tài liệu
  • Di chuyển tất cả quân cờ trên bàn chơi

→ Cần một cách kiểm soát việc lặp dựa trên dữ liệu vào…

Vòng lặp for

Cú pháp

for <biến> in <dãy>:
    <câu-lệnh-1>
    ...
    <câu-lệnh-n>
        
Ví dụ

for item in lst:
    print(item)
        
Mô tả:
  • Gán phần tử thứ nhất trong danh sách lst cho biến item. Thực thi print(item)
  • Lặp lại với phần tử thứ hai, thứ ba, … cho tới phần tử cuối cùng trong lst

Thuật ngữ

Cú pháp

for <biến> in <dãy>:
    <câu-lệnh-1>
    ...
    <câu-lệnh-n>
        
Ví dụ

for item in lst:
    print(item)
        
  • Biến vòng lặp: <biến> / item
  • Dãy để lặp: <dãy> / lst
  • Thân vòng lặp: các câu lệnh thụt lề sau dấu hai chấm

Cách thực thi chi tiết hơn


for x in seq:
    body
        
seq còn phần tử? Đúng gán phần tử tiếp theo cho x thực thi body Sai
Phép kiểm tra “còn phần tử?”:
  • Diễn ra n+1 lần nếu có n phần tử.

Chuỗi cũng là dãy (sequence)


print("The string contains:")

# in mỗi ký tự của chuỗi s trên một dòng
for char in s:
    print(char)
    

💡 Các ký tự trong chuỗi được xem như là các phần tử của một dãy.

Câu hỏi

Hàng số 4 sẽ được thực thi bao nhiêu lần?


def mystery_function(s):              
    """Điều kiện: s là chuỗi."""
    for v in 'aeiou':
        if v in s:
            return True
    return False
    
  • (A) Chính xác 5 lần
  • (B) Từ 1 đến 5 lần
  • (C) Chính xác len(s) lần
  • (D) Từ 1 đến len(s) lần
  • (E) Không bao giờ
Câu hỏi (tiếp)

Hàng số 4 sẽ được thực thi bao nhiêu lần?


def mystery_function(s):              
    """Điều kiện: s là chuỗi."""
    for v in 'aeiou':
        if v in s:
            return True
        return False
    

2.
Mẫu tích luỹ

Tính trung bình của danh sách số

Diễn tả thuật toán bằng giả mã (pseudocode)


def avg(lst):
    """Trả về: trung bình của tất cả phần tử trong lst.
    Điều kiện: lst là danh sách không rỗng
    và mỗi phần tử là float hoặc int."""

    # 1. Gán num bằng tổng của tất cả phần tử trong lst

    # 2. Gán den bằng số lượng phần tử của lst

    # 3. Trả về num/den
    return 0
        

Tính trung bình của danh sách số

Mở rộng thành code: làm dần (iteratively)


def avg(lst):
    """Trả về: trung bình của tất cả phần tử trong lst.
    Điều kiện: lst là danh sách không rỗng
    và mỗi phần tử là float hoặc int."""

    # 1. Gán num bằng tổng của tất cả phần tử trong lst
    num = 0   # TODO
    # 2. Gán den bằng số lượng phần tử của lst
    den = len(lst)
    # 3. Trả về num/den
    return 0
    

💡 Làm phần nào chắc chắn trước. Phần chưa rõ: đặt biến tạm thời.

Tính trung bình của danh sách số

Mở rộng code: tập trung vào bước tính tổng


# ...
# 1. Gán num bằng tổng của tất cả phần tử trong lst
num = 0
for x in lst:
    num = num + x
# 2. ...
    

Thực thi:

  • Gán phần tử thứ nhất của lst cho x. Cộng x vào num.
  • Gán phần tử thứ hai của lst cho x. Cộng x vào num.
  • Gán phần tử cuối cùng của lst cho x. Cộng x vào num.

Tính trung bình của danh sách số

Hoàn thiện


def avg(lst):
    """Trả về: trung bình của tất cả phần tử trong lst.
    Điều kiện: lst là danh sách không rỗng
    và mỗi phần tử là float hoặc int."""

    # 1. Gán num bằng tổng của tất cả phần tử trong lst
    num = 0
    for x in lst:
        num = num + x

    # 2. Gán den bằng số lượng phần tử của lst
    den = len(lst)

    # 3. Trả về num/den
    return num/den
    

Tính trung bình của danh sách số

Tích luỹ (Accumulator)


def avg(lst):
    """Trả về: trung bình của tất cả phần tử trong lst.
    Điều kiện: lst là danh sách không rỗng
    và mỗi phần tử là float hoặc int."""

    num = 0          # biến tích luỹ (accumulator)
    for x in lst:
        num = num + x
    den = len(lst)
    return num/den
    
num là một biến tích luỹ (accumulator)

Mẫu tích luỹ (Accumulator Pattern)

Biến tích luỹ (accumulator) là biến mà vòng lặp dùng để tính dồn giá trị, mỗi lần một phần tử.

biến_tích_luỹ = <giá-trị-ban-đầu>
for item in seq:
    ...
    biến_tích_luỹ = biến_tích_luỹ <toán-tử> item
    ...
# biến_tích_luỹ bây giờ chứa giá trị tích luỹ
    

num = 0
for x in lst:
    num = num + x
# num bây giờ chứa tổng các phần tử trong lst
    

Thêm ví dụ về accumulator

Xem file accumulators.py

  • Tính tổng của một danh sách
  • Tính tích của một danh sách
  • Loại bỏ khoảng trắng khỏi chuỗi

3.
Dải số (range)

Làm sao in các số từ 1 đến n?

Diễn tả thuật toán bằng giả mã (pseudocode)


def count(n):
    """In các số 1, 2, ..., n; mỗi số trên một dòng.
    Điều kiện: n là số nguyên >= 1."""

    # Tạo danh sách 1..n

    # Lặp qua danh sách và in từng phần tử

    pass
  

Làm sao in các số từ 1 đến n?

Mở rộng thành code: làm dần (iteratively)


def count(n):
    """In các số 1, 2, ..., n; mỗi số trên một dòng.
    Điều kiện: n là số nguyên >= 1."""

    # Tạo danh sách [1, 2, ..., n]
    nums = [1]   # TODO

    # Lặp qua danh sách và in từng phần tử
    for num in nums:
        print(num)
    

💡 Làm phần nào chắc chắn trước. Phần chưa rõ: đặt biến tạm thời.

Làm sao in các số từ 1 đến n?

Mở rộng code: tập trung biến nums


# Tạo danh sách [1, 2, ..., n]
nums = [1]  # TODO
    
Câu hỏi
Vì sao cách thử sau chạy không đúng?

# Tạo danh sách [1, 2, ..., n]
nums = []
for i in n:
    nums.append(i)
      
  • (A) Vì ta cần chuyển biến n sang chuỗi.
  • (B) Vì số nguyên không phải là một dãy (sequence).
  • (C)append() không nhận biến vòng lặp làm tham số.
  • (D)nums là biến tích luỹ, nhưng ta không gán giá trị cho nó trong thân vòng lặp.
  • (E) Thực ra đoạn code trên chạy đúng.

range là gì?

Một rangedãy số (sequence). Vì là dãy số, nó hỗ trợ:
  • lặp (iteration)
  • chuyển sang danh sách (list)
  • độ dài (len())
  • chỉ mục (indexing)
  • cắt lát (slicing)

>>> r = range(3)
>>> r
range(0, 3)
>>> list(r)
[0, 1, 2]
>>> list(range(1, 3))
[1, 2]
>>> len(r)
3
      

>>> r.start 
0
>>> r.stop
3
>>> r[0]
0
>>> r[1]
1
      

Làm sao in các số từ 1 đến n?

Mở rộng code: hoàn thiện biến nums


# Tạo danh sách [1, 2, ..., n]
nums = list(range(1, n+1))
    

Làm sao in các số từ 1 đến n?

Hoàn thiện code


def count(n):
    """In các số 1, 2, ..., n; mỗi số trên một dòng.
    Điều kiện: n là số nguyên >= 1."""

    # Tạo danh sách [1, 2, ..., n]
    nums = list(range(1, n+1))

    # Lặp qua danh sách và in từng phần tử
    for num in nums:
        print(num)
    

Làm sao in các số từ 1 đến n?

Code rút gọn (cách 1): không cần biến nums


def count(n):
    """In các số 1, 2, ..., n; mỗi số trên một dòng.
    Điều kiện: n là số nguyên >= 1."""

    # Tạo dãy số từ 1 đến n và in từng số
    for num in range(1, n+1):
        print(num)
    

Làm sao in các số từ 1 đến n?

Code rút gọn (cách 2): dùng hàm phụ để in


def print_list(lst):
    """In từng phần tử trong lst, mỗi phần tử trên một dòng."""

    for item in lst:
        print(item)

def count(n):
    """In các số 1, 2, ..., n; mỗi số trên một dòng.
    Điều kiện: n là số nguyên >= 1."""

    # Tạo danh sách [1, 2, ..., n]
    nums = list(range(1, n+1))

    # In danh sách
    print_list(nums)  # gọi hàm phụ
    

Làm sao in các số từ 1 đến n?

Code rút gọn (cách 3): kết hợp hai cách trên


def count(n):
    """In các số 1, 2, ..., n; mỗi số trên một dòng.
    Điều kiện: n là số nguyên >= 1."""

    print_list(list(range(1, n+1)))
    

⚠️ Lưu ý: Rút gọn số dòng không phải mục tiêu. Hãy ưu tiên cách viết dễ đọc cho người mới học.

4.
Lặp theo chỉ số (vs. phần tử)

Tăng mỗi phần tử của danh sách

Thử lặp theo các phần tử


def incr_list_wrong(nums):
    """Tăng thêm 1 cho mỗi phần tử trong nums.
    Ví dụ: Biến đổi danh sách đầu vào [6,0,7] thành [7,1,8].
    Điều kiện: Danh sách nums chỉ chứa số."""

    # Lặp qua các phần tử trong nums, tăng chúng lên 1
    # ⚠️ Cách này sai: chỉ thay đổi biến vòng lặp
    for n in nums:
        n = n + 1
    

>>> lst = [6, 0, 7]
>>> incr_list_wrong(lst)
>>> lst
[6, 0, 7]
    

Ôn lại: vòng lặp for

Cú pháp

for <var> in <seq>:
    <statement>
    ...
    <statement>
        
Ví dụ

for item in lst:
    print(item)
        

Mỗi vòng lặp gán phần tử kế tiếp của <seq> cho <var> rồi chạy thân vòng lặp.

Vì sao cách trên không tốt?

Ta chỉ gán lại n (biến vòng lặp) sang giá trị mới, không hề gán vào vị trí nào của danh sách nums.

Sơ đồ thực thi


def incr_list_wrong(nums):
    for n in nums:
        n = n + 1

lst = [6, 0, 7]
incr_list_wrong(lst)
Không gian toàn cục lst id2 Không gian Heap id2 list 0 1 2 6 0 7 Khung gọi hàm incr_list_wrong nums n RETURN id2 None 6 7 0 1 7 8
⚠️ Danh sách nums không đổi; chỉ biến vòng lặp n thay đổi.

Ôn lại: range

Dùng range để tạo ra một dãy số.

>>> r = range(3)
>>> r
range(0, 3)
>>> list(r)
[0, 1, 2]
>>> r[0]
0
>>> r[1]
1
    

Lặp theo chỉ số

Thay đổi trực tiếp phần tử trong danh sách


def incr_list(nums):
    """Tăng thêm 1 cho mỗi phần tử trong nums.
    Ví dụ: [6,0,7] -> [7,1,8].
    Điều kiện: nums chỉ chứa số."""

    for i in range(len(nums)):
        nums[i] = nums[i] + 1
    

>>> lst = [6, 0, 7]
>>> incr_list(lst)
>>> lst
[7, 1, 8]
    

Vì sao cách này hiệu quả?

Biến i thay đổi ta gán vào nums[i], nên đối tượng danh sách cũng thay đổi.

Sơ đồ thực thi


def incr_list_wrong(nums):
    for i in range(len(nums)):
        nums[i] = nums[i] + 1

lst = [6, 0, 7]
incr_list_wrong(lst)
Không gian toàn cục lst id2 Không gian Heap id2 list 0 1 2 6 7 0 1 7 8 Khung gọi hàm incr_list_wrong nums i RETURN id2 None 0 1 2
✅ Danh sách nums thay đổi vì ta gán giá trị mới vào nums[i].

Hai mẫu lặp qua danh sách

Lặp theo phần tử

for item in lst:
    # dùng item
    print(item)
        
  • Mã đơn giản
  • Sức mạnh hạn chế
  • Dùng khi không cần thay phần tử tại chỗ
Lặp theo chỉ số

for idx in range(len(lst)):
    # dùng lst[idx]
    lst[idx] = lst[idx] + 1
        
  • Phức tạp hơn một chút
  • Mạnh hơn (cập nhật tại chỗ, truy cập vị trí)
  • Dùng khi cần biết/chỉnh vị trí phần tử

enumerate()

Hàm enumerate(iterable, start=0) trả về một iterator của các bộ (index, item).

  • Mặc định bắt đầu từ start=0.
  • Tiện hơn range(len(lst)).
  • Dùng tốt cho cả list, string, và các iterable khác.

enumerate()

Ví dụ


a = ["I", "am", "UET"]

# Lặp để lấy cả chỉ số và phần tử
for i, name in enumerate(a):
    print(f"Index {i}: {name}")

# Đổi vị trí bắt đầu của chỉ số
for i, name in enumerate(a, start=1):
    print(f"{i}) {name}")

# Chuyển thành danh sách các bộ (index, item)
pairs = list(enumerate(a))
print(pairs)   # [(0, 'I'), (1, 'am'), (2, 'UET')]
      

5.
Danh sách &
vòng lặp
lồng nhau

Đôi khi một danh sách là chưa đủ

Dữ liệu Tổ chức
Dàn ý bài luận Gạch đầu dòng, gạch con, gạch con của gạch con
Nội dung cuốn sách Chương, đoạn, câu, từ
Video Khung hình (frame), điểm ảnh
Bảng tính Hàng và cột

💡 Đây đều là cấu trúc phân cấp: danh sách của danh sách, danh sách của danh sách của danh sách, …

Danh sách lồng nhau

Danh sách (list) trong Python:

  • Phần tử của danh sách có thể là bất kỳ đối tượng nào
  • Bản thân danh sách cũng là đối tượng

→ Danh sách có thể chứa các danh sách khác

Danh sách lồng nhau

Ví dụ


lst = [
    [2, 4],
    [3, 6, 9],
    [4, 8, 12, 16]
]
  • Danh sách ngoài (Outer list): Có 3 phần tử
  • Ba danh sách trong (Inner lists): Mỗi danh sách có số phần tử khác nhau

Một bài haiku của Yosa Buson

The light of a candle
Is transferred to another candle—
spring twilight.

Một bộ kiểm tra chính tả có thể xem bài thơ như danh sách lồng nhau:




[["The", "light", "of", "a", "candle"],
["Is", "transferred", "to", "another", "candle—"],
["spring", "twilight."]] 

minh hoạ Cấu trúc bộ nhớ

Không gian toàn cục Các đối tượng global haiku id1 id2:list 0 "The" 1 "light" 2 "of" 3 "a" 4 "candle" id3:list 0 "Is" 1 "transferred" 2 "to" 3 "another" 4 "candle—" id4:list 0 "spring" 1 "twilight." id1:list 0 id2 1 id3 2 id4 Danh sách ngoài chứa id của các danh sách trong.

In từng từ của bài haiku?

Dùng hàm phụ từ phần trước


def print_list(lst):
    """In danh sách lst, mỗi phần tử trên một dòng."""
    for word in lst:
        print(word)

def print_haiku_with_helper(h):
    """In bài haiku h, một danh sách các danh sách chuỗi."""
    for line in h:
        print_list(line) 

In từng từ của bài haiku?

Dùng vòng lặp for lồng nhau


def print_haiku(h):
    """In bài haiku h, một danh sách các danh sách chuỗi."""
    for line in h:
        for word in line:
            print(word)
    
Vòng lặp Biến vòng lặp Dãy để lặp
Ngoài line h
Trong word line

6.
Thao tác trên danh sách lồng

Độ dài và truy cập phần tử


>>> h = [['The', 'light', 'of', 'a', 'candle'],
...      ['Is', 'transferred', 'to', 'another', 'candle—'],
...      ['spring', 'twilight.']]
>>> len(h)               # độ dài danh sách ngoài
3
>>> len(h[0])            # độ dài danh sách trong đầu tiên
5
>>> h[2][0]              # phần tử hàng 2, cột 0
'spring'
>>> h[2][1] = 'dawning'  # gán phần tử hàng 2, cột 1
    

Cắt lát (slicing)

Chỉ sao chép cấp trên cùng


>>> b = [[9, 6], [4, 5], [7, 7]]
>>> x = b[:2]           # sao chép *cấp trên cùng*
>>> x
[[9, 6], [4, 5]]
>>> x[0][0] = 100       # thay đổi phần tử bên trong
>>> b
[[100, 6], [4, 5], [7, 7]]
        

Cắt lát (slicing)

Chỉ sao chép cấp trên cùng

Không gian toàn cục Không gian Heap b id1 x id5 id1:list 0 1 2 id2 id3 id4 id5:list (b[:2]) 0 1 id2 id3 id2:list 9 6 id3:list 4 5 id4:list 7 7
Slicing tạo danh sách mới ở id5, nhưng các danh sách trong id2, id3, id4 vẫn được dùng chung.
Câu hỏi
Kết quả đoạn code sau là gì?

>>> b = [[9, 6], [4, 5], [7, 7]]
>>> x = b[:2]
>>> x[1].append(10)
>>> x
      
  • (A) [[9, 6, 10]]
  • (B) [[9, 6], [4, 5, 10]]
  • (C) [[9, 6], [4, 5, 10], [7, 7]]
  • (D) [[9, 6], [4, 10], [7, 7]]

7.
Dữ liệu bảng bằng danh sách lồng nhau

Dữ liệu bảng

Biểu diễn bảng bằng danh sách lồng nhau

NetID A1 A2 A3
abc1 100 95 97
def2 100 80 92
Dạng cột
Mỗi cột là một danh sách con

[
  ['NetID', 'abc1', 'def2'],
  ['A1', 100, 100],
  ['A2', 95, 80],
  ['A3', 97, 92]
]
        
Dạng hàng
Mỗi hàng là một danh sách con

[
  ['NetID', 'A1', 'A2', 'A3'],
  ['abc1',   100,  95,  97],
  ['def2',   100,  80,  92]
]
        
✅ Cả hai cách đều hợp lý (sau này có thể dùng dict để biểu diễn bảng).

Danh sách 2D

Hình chữ nhật vs. Răng cưa

Danh sách 2D: một danh sách của các danh sách

Hình chữ nhật: mọi danh sách con có cùng độ dài

Răng cưa: không phải hình chữ nhật

Răng cưa:

[['The', 'light', 'of', 'a', 'candle'],
 ['Is', 'transferred', 'to', 'another', 'candle—'],
 ['spring', 'twilight.']]
        
Hình chữ nhật:

[['NetID', 'A1',  'A2', 'A3'],
 ['abc1',  100,   95,   97],
 ['def2',  100,   80,   92]]
        

Tính trung bình của một bảng số?

Dùng vòng lặp for lồng và mẫu tích luỹ


def avg_tab(tab):
    """Trả về giá trị trung bình của tab, một danh sách 2D các số."""
    sum = 0
    count = 0
    for inner_list in tab:           # lặp theo hàng
        for number in inner_list:    # lặp theo phần tử trong hàng
            sum = sum + number       # tích luỹ tổng
            count = count + 1        # tích luỹ số lượng
    return sum / count
    

Cộng 1 vào mọi số trong bảng?

Dùng vòng lặp lồng, lặp theo chỉ số và truy cập lồng


def add1_tab(tab):
    """Cộng 1 vào từng số trong tab (thay đổi trực tiếp tab)."""
    for r in range(len(tab)):                 # chỉ số hàng
        for c in range(len(tab[r])):          # chỉ số cột
            old_val = tab[r][c]
            tab[r][c] = old_val + 1
    

Chuyển giữa dạng hàng/cột?

Còn gọi là chuyển vị (transpose) ma trận

1 2 3 4 5 6 3×2 1 3 5 2 4 6 2×3

Chuyển giữa dạng hàng/cột?

Còn gọi là chuyển vị (transpose) ma trận


def transpose(tab):
    """Trả về ma trận chuyển vị của tab.
    Điều kiện: tab hình chữ nhật (mọi hàng cùng độ dài)."""

    rows = len(tab)
    cols = len(tab[0])
    out = []
    for c in range(cols):
        row_new = []
        for r in range(rows):
            row_new.append(tab[r][c])
        out.append(row_new)
    return out
    

Tổng kết

  1. Vòng lặp: for x in seq
  2. Dải số: dùng range cho lặp đếm
  3. Mẫu tích luỹ: Khởi tạo ở ngoài, cập nhật trong vòng lặp

Tổng kết

  1. Phần tử vs. chỉ số:
    • Ưu tiên duyệt phần tử
    • Dùng chỉ số khi cần sửa tại chỗ
  2. Danh sách & vòng lặp lồng:
    • Danh sách lồng: danh sách chứa danh sách khác
    • Vòng lặp lồng: vòng lặp bên trong vòng lặp khác
  3. Dữ liệu bảng:
    • Dạng hàng (row-major) vs. Dạng cột (column-major)
    • Hình chữ nhật vs. răng cưa