Data Structure: list, tuple, dict, set

কোনটা কখন ব্যবহার করবে, কোনটা কত দ্রুত

রোডম্যাপের মাস ১পড়তে প্রায় ১৯ মিনিট

Data structure হলো data সাজিয়ে রাখার পাত্র। ভুল পাত্র বেছে নিলে code চলবে ঠিকই, কিন্তু ধীরে চলবে, বা অদ্ভুত bug দেবে। Python এ চারটা মূল পাত্র আছে: list, tuple, dict, set। এই চারটা ভালো করে বুঝলে Python এর অর্ধেক কাজ সহজ হয়ে যায়, কারণ FastAPI এর request, database এর row, JSON সবই শেষে এগুলোর মধ্যেই আসে।

#এক নজরে চারটা

list tuple dict set
লেখা [1, 2, 3] (1, 2, 3) {"a": 1} {1, 2, 3}
ক্রম থাকে? হ্যাঁ হ্যাঁ হ্যাঁ (যোগ করার ক্রম) না
বদলানো যায়? হ্যাঁ না হ্যাঁ হ্যাঁ
একই জিনিস দুইবার? চলে চলে key একবারই না, সব unique
খোঁজার গতি ধীর (O(n)) ধীর (O(n)) খুব দ্রুত (O(1)) খুব দ্রুত (O(1))
Dart এ List (record) Map Set
কখন ক্রমানুসারে জিনিস স্থির জোড়া/দল key দিয়ে খোঁজা unique জিনিস, সদস্য কিনা দেখা

#list: ক্রমানুসারে জিনিসের তালিকা

Python
cart = ["চাল", "ডাল", "তেল"]

cart.append("লবণ")          # শেষে যোগ
cart.insert(0, "চিনি")      # নির্দিষ্ট জায়গায় যোগ
cart.extend(["আটা", "ডিম"])  # অনেকগুলো একসাথে
removed = cart.pop()         # শেষেরটা সরাও আর ফেরত দাও
cart.remove("তেল")           # value দিয়ে সরাও (প্রথম যেটা মেলে)

print(len(cart))             # কতগুলো
print("ডাল" in cart)         # আছে কিনা: True
print(cart[0], cart[-1])     # প্রথম আর শেষ
print(cart[1:3])             # slicing

# sort
prices = [550, 120, 999, 75]
prices.sort()                # নিজেই বদলে যায়: [75, 120, 550, 999]
top = sorted(prices, reverse=True)  # নতুন list দেয়, পুরনোটা ঠিক থাকে

#key দিয়ে sort: বাস্তব কাজে সবচেয়ে বেশি লাগে

Python
products = [
    {"name": "Headphone", "price": 2500, "rating": 4.2},
    {"name": "Mouse", "price": 650, "rating": 4.7},
    {"name": "Keyboard", "price": 1800, "rating": 4.5},
]

cheap_first = sorted(products, key=lambda p: p["price"])
best_first = sorted(products, key=lambda p: p["rating"], reverse=True)

for p in best_first:
    print(p["name"], p["rating"])

key= এ একটা function দাও যেটা বলে "কোন জিনিস দেখে sort করবে"। lambda হলো ছোট এক লাইনের function, Function অধ্যায়ে বিস্তারিত।

#List comprehension: Python এর প্রিয় কৌশল

Dart এ যেমন list.map(...).where(...).toList(), Python এ সেটা এক লাইনে:

Python
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

squares = [n * n for n in numbers]               # map
evens = [n for n in numbers if n % 2 == 0]       # where/filter
even_squares = [n * n for n in numbers if n % 2 == 0]

names = ["  rahim ", "KARIM", "Sara  "]
clean = [n.strip().title() for n in names]       # ['Rahim', 'Karim', 'Sara']

পড়ার নিয়ম: [কী বানাবো for প্রতিটা in কোথা থেকে if শর্ত]

#tuple: বদলানো যায় না এমন দল

Python
location = (23.8103, 90.4125)     # ঢাকার latitude, longitude
lat, lng = location               # unpacking
print(lat)

# function থেকে একাধিক value ফেরত দেওয়া আসলে tuple ফেরত দেওয়া
def min_max(values: list[int]) -> tuple[int, int]:
    return min(values), max(values)

low, high = min_max([5, 2, 9, 1])

কখন tuple: যখন জিনিসগুলো একসাথে একটা অর্থ বহন করে আর বদলানোর কথা না, যেমন coordinate, RGB রঙ, database এর একটা row। Tuple বদলানো যায় না বলে dict এর key হিসেবেও ব্যবহার করা যায়, list যায় না।

#Unpacking এর আরও কৌশল

Python
first, *rest = [10, 20, 30, 40]      # first=10, rest=[20, 30, 40]
a, b = 1, 2
a, b = b, a                          # swap, temp variable ছাড়াই

#dict: key দিয়ে খোঁজা

dict হলো Python এর সবচেয়ে গুরুত্বপূর্ণ data structure। JSON মানেই dict, FastAPI এর request body শেষে dict, configuration dict।

Python
user = {
    "id": 101,
    "name": "Nusrat",
    "city": "Chattogram",
    "skills": ["Flutter", "Python"],
}

print(user["name"])                  # Nusrat
print(user.get("phone"))             # None, error দেয় না
print(user.get("phone", "নেই"))      # default value

user["phone"] = "01700000000"        # যোগ বা বদল
del user["city"]                     # মুছে ফেলা

for key, value in user.items():
    print(f"{key} → {value}")

print(list(user.keys()))
print("name" in user)                # key আছে কিনা: True

#dict কেন এত দ্রুত: hash table

list এ কিছু খুঁজতে Python কে প্রথম থেকে একটা একটা করে দেখতে হয়। ১০ লাখ জিনিস থাকলে সবচেয়ে খারাপ ক্ষেত্রে ১০ লাখ বার। dict এ key থেকে একটা hash (একটা সংখ্যা) বের করা হয়, আর সেই সংখ্যা সরাসরি বলে দেয় data কোন ঘরে আছে।

"01711..." "01819..." "01552..." hash() key → ঘরের নম্বর ঘর ০: খালি ঘর ১: Karim ঘর ২: খালি ঘর ৩: Rahim ঘর ৪: Sara
Phone number দিয়ে নাম খোঁজা। hash function সরাসরি ঘরের নম্বর বলে দেয়, তাই ১০ জন থাকুক বা ১ কোটি, খুঁজতে প্রায় একই সময় লাগে।

এই একই ধারণা পরে বারবার আসবে: Redis cache একটা বিশাল dict, database এর hash index, আর sharding এ কোন data কোন server এ যাবে সেটাও hash দিয়ে ঠিক হয়।

এর একটা শর্ত আছে: key অবশ্যই hashable (বদলানো যায় না এমন) হতে হবে। str, int, tuple চলে; list, dict চলে না।

#Dict comprehension

Python
prices = {"আম": 120, "কলা": 60, "আপেল": 280}
with_vat = {item: round(p * 1.15) for item, p in prices.items()}
cheap = {item: p for item, p in prices.items() if p < 150}

#set: unique জিনিস আর দ্রুত "আছে কিনা"

Python
visitors = ["ali", "sara", "ali", "rafi", "sara"]
unique = set(visitors)          # {'ali', 'sara', 'rafi'}
print(len(unique))              # 3 জন আলাদা মানুষ

flutter_devs = {"ali", "sara", "nila"}
python_devs = {"sara", "rafi", "nila"}

print(flutter_devs & python_devs)   # দুটোই জানে: {'sara', 'nila'}
print(flutter_devs | python_devs)   # যেকোনো একটা জানে
print(flutter_devs - python_devs)   # শুধু Flutter: {'ali'}

#গতির পার্থক্য নিজে মেপে দেখো

Python
import time

big_list = list(range(1_000_000))
big_set = set(big_list)

start = time.perf_counter()
for _ in range(1000):
    999_999 in big_list
print(f"list: {time.perf_counter() - start:.3f} সেকেন্ড")

start = time.perf_counter()
for _ in range(1000):
    999_999 in big_set
print(f"set:  {time.perf_counter() - start:.6f} সেকেন্ড")

তোমার Mac এ list এ কয়েক সেকেন্ড আর set এ প্রায় শূন্য সময় লাগবে। এক লাখ বার "এই user কি block করা?" check করতে হলে এই পার্থক্যই server ধীর আর দ্রুতর পার্থক্য।

#Big-O: কত দ্রুত, সহজ ভাষায়

Big-O বলে data বাড়লে কাজের সময় কীভাবে বাড়ে:

Big-O মানে উদাহরণ
O(1) data যতই হোক, সময় প্রায় একই dict/set এ খোঁজা, list এর শেষে append
O(log n) data দ্বিগুণ হলে সময় সামান্য বাড়ে sorted list এ binary search, database index
O(n) data দ্বিগুণ হলে সময়ও দ্বিগুণ list এ in দিয়ে খোঁজা, পুরো list ঘোরা
O(n log n) sort এর সময় sorted()
O(n²) data দ্বিগুণ হলে সময় চার গুণ loop এর ভেতরে loop, প্রতিটার সাথে প্রতিটা তুলনা

#Mutable আর reference: সবচেয়ে বেশি bug এখানে

Python এ variable হলো একটা লেবেল, যেটা কোনো object এর দিকে নির্দেশ করে। b = a লিখলে নতুন list তৈরি হয় না, একই list এর গায়ে দ্বিতীয় লেবেল লাগে।

a b একটাই list [1, 2, 3, 4] b.append(4) করলে a তেও 4 দেখা যাবে
b = a নতুন copy বানায় না। দুইটা নাম, একটা list।
Python
a = [1, 2, 3]
b = a
b.append(4)
print(a)        # [1, 2, 3, 4]  😮

# আলাদা copy চাইলে
c = a.copy()    # বা list(a) বা a[:]
c.append(5)
print(a)        # [1, 2, 3, 4]  ঠিক আছে

# ভেতরে list থাকলে copy() যথেষ্ট না (shallow copy)
import copy
matrix = [[1, 2], [3, 4]]
shallow = matrix.copy()
shallow[0].append(99)
print(matrix)   # [[1, 2, 99], [3, 4]]  ভেতরেরটা তখনও share হচ্ছে
deep = copy.deepcopy(matrix)  # পুরোপুরি আলাদা

Flutter এ state management এর সময় তুমি এই সমস্যা দেখেছো: list এ কিছু যোগ করলে UI update হয় না, কারণ reference একই আছে, তাই নতুন list বানাতে হয় ([...old, item])। Python এও একই নীতি।

#collections: আরও কিছু দরকারি পাত্র

Python
from collections import Counter, defaultdict, deque

# Counter: গোনা
words = "আম জাম আম কাঁঠাল আম জাম".split()
count = Counter(words)
print(count.most_common(2))          # [('আম', 3), ('জাম', 2)]

# defaultdict: key না থাকলে নিজে default বানায়
orders_by_city = defaultdict(list)
orders = [("Dhaka", 101), ("Sylhet", 102), ("Dhaka", 103)]
for city, order_id in orders:
    orders_by_city[city].append(order_id)   # আগে key আছে কিনা check লাগে না
print(dict(orders_by_city))          # {'Dhaka': [101, 103], 'Sylhet': [102]}

# deque: দুই দিক থেকেই দ্রুত যোগ/বাদ (queue এর জন্য)
queue = deque()
queue.append("order-1")
queue.append("order-2")
first = queue.popleft()               # list.pop(0) এর চেয়ে অনেক দ্রুত

#কোনটা কখন: সিদ্ধান্তের নিয়ম

  • ক্রম দরকার, জিনিস বদলাবে → list
  • কয়েকটা জিনিস মিলে একটা অর্থ, বদলাবে না → tuple
  • নাম/id দিয়ে খুঁজবে → dict
  • শুধু "আছে কিনা" বা unique জিনিস দরকার → set
  • গুনতে হবে → Counter
  • group করতে হবে → defaultdict(list)
  • সামনে থেকে বের করবে, পেছনে যোগ করবে → deque

#অনুশীলন

#নিজেকে যাচাই করো

১০ লাখ user এর মধ্যে কেউ banned কিনা বারবার check করতে হবে। কোন data structure, আর কেন?

set। কারণ set এ "আছে কিনা" check করা O(1), মানে প্রায় সঙ্গে সঙ্গে। list এ সেটা O(n), প্রতিবার পুরো list ঘুরতে হতে পারে।

`a = [1, 2]; b = a; b.append(3); print(a)` কী print করবে, কেন?

[1, 2, 3]। কারণ b = a নতুন list বানায় না, দুইটা নাম একই list কে নির্দেশ করে। আলাদা copy চাইলে b = a.copy()।

list কেন dict এর key হতে পারে না, কিন্তু tuple পারে?

dict এর key এর hash বের করতে হয়, আর key বদলে গেলে hash বদলে যাবে, তখন data আর খুঁজে পাওয়া যাবে না। list বদলানো যায়, তাই hashable না। tuple বদলানো যায় না, তাই key হতে পারে।

`sorted(items)` আর `items.sort()` এর পার্থক্য কী?

sorted() নতুন একটা sorted list ফেরত দেয়, মূলটা অপরিবর্তিত থাকে। .sort() মূল list টাকেই বদলে দেয় আর None ফেরত দেয়।

Loop এর ভেতরে loop দিয়ে দুইটা ১০,০০০ জিনিসের list তুলনা করলে মোটামুটি কতগুলো তুলনা হবে? কীভাবে কমাবে?

প্রায় ১০ কোটি (O(n²))। একটা list কে set বা dict এ বদলে নিলে প্রতিটার জন্য O(1) খোঁজা হবে, মোট প্রায় ১০,০০০ বার (O(n))।

চিহ্ন দিলে sidebar আর রোডম্যাপে এই অধ্যায়ের station ভরে যাবে।