ภายในฟังก์ชันแฮชและการจัดการการชนกัน
ทำความเข้าใจวิธีที่ Python แฮชออบเจ็กต์ วิธีที่การกำหนดตำแหน่งแบบเปิดและการต่อโซ่แก้การชนกัน และเหตุใด O(1) ในกรณีเฉลี่ยจึงอาจแย่ลงเป็น O(n)
ภายในฟังก์ชันแฮชและการจัดการการชนกัน เป็นบทเรียน Coding Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 1 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Coding Interview Prep และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Coding Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
แฮชแมปคืออะไร
แฮชแมป (พจนานุกรมในไพธอน) จับคู่คีย์กับค่าโดยใช้ ฟังก์ชันแฮช ซึ่งแปลงคีย์ใด ๆ เป็นดัชนีจำนวนเต็มในอาร์เรย์พื้นฐาน ฟังก์ชันแฮชในอุดมคติกระจายคีย์ไปทั่วอาร์เรย์อย่างสม่ำเสมอ ทำให้การค้นหา การเพิ่ม และการลบมีความซับซ้อนเฉลี่ยเป็น O(1) อาร์เรย์พื้นฐานนี้เรียกว่า ตารางแฮช หรืออาร์เรย์ช่องจัดเก็บ
ในไพธอน dict เป็นแฮชแมปที่ได้รับการปรับให้เหมาะสมอย่างมาก การเข้าใจโครงสร้างภายในช่วยให้คุณวิเคราะห์พฤติกรรมในกรณีแย่ที่สุดและเลือกคีย์ที่เหมาะสมได้
# Python dict is a hash map
hm = {}
hm['alice'] = 95
hm['bob'] = 87
hm['carol'] = 91
print(hm['alice']) # O(1) lookup: 95
print('bob' in hm) # O(1) membership: True
del hm['bob'] # O(1) deletion
print(hm) # {'alice': 95, 'carol': 91}ฟังก์ชันแฮชและเมธอด __hash__
ไพธอนเรียกใช้ __hash__(key) เพื่อคำนวณค่าจำนวนเต็มจากคีย์ จากนั้นนำจำนวนเต็มนั้นมาหารเอาเศษด้วยขนาดตารางเพื่อหาดัชนีช่องจัดเก็บ ชนิดข้อมูลในตัวอย่างเช่น int str และ tuple มีการคำนวณแฮชในตัวที่รวดเร็ว ส่วน list และ dict ไม่สามารถแฮชได้ (เนื่องจากเปลี่ยนแปลงได้ และการเปลี่ยนแปลงจะทำให้ค่าแฮชที่จัดเก็บไว้ใช้ไม่ได้)
ฟังก์ชันแฮชที่ดีควรกระจายคีย์อย่างสม่ำเสมอ ให้ผลลัพธ์แน่นอน และคำนวณได้รวดเร็ว แฮชของสตริงในไพธอนจะสุ่มค่าแตกต่างกันในแต่ละครั้งที่ทำงาน (เป็นคุณลักษณะด้านความปลอดภัย) — ใช้ PYTHONHASHSEED=0 เพื่อปิดการสุ่มและให้ผลทำซ้ำได้ในการทดสอบ
# Built-in hash in Python
print(hash(42)) # integer hashes to itself (CPython)
print(hash('hello')) # string hash (randomised per run)
print(hash((1, 2, 3))) # tuple hash: depends on contents
# Unhashable types
try:
hash([1, 2, 3]) # lists are mutable -> not hashable
except TypeError as e:
print('Error:', e)
# Custom class: define __hash__ and __eq__
class Point:
def __init__(self, x, y): self.x = x; self.y = y
def __hash__(self): return hash((self.x, self.y))
def __eq__(self, other): return self.x == other.x and self.y == other.y
points = {Point(1, 2): 'A', Point(3, 4): 'B'}
print(points[Point(1, 2)]) # 'A'การชนกัน: เมื่อคีย์สองรายการแฮชไปยังช่องเดียวกัน
การชนกัน เกิดขึ้นเมื่อคีย์ที่แตกต่างกันสองคีย์ให้ดัชนีช่องจัดเก็บเดียวกัน การชนกันเป็นสิ่งที่หลีกเลี่ยงไม่ได้ (ตามหลักรังนกพิราบ: มีคีย์จำนวนไม่สิ้นสุด แต่มีช่องจัดเก็บจำนวนจำกัด) กลยุทธ์มาตรฐานในการแก้ปัญหามีสองแบบคือ การเชื่อมโยง และ การระบุตำแหน่งแบบเปิด ไพธอนใช้รูปแบบหนึ่งของการระบุตำแหน่งแบบเปิดร่วมกับการไล่ตรวจแบบสุ่มเทียม
การเชื่อมโยงจะจัดเก็บรายการเชื่อมโยง (หรืออาร์เรย์แบบไดนามิก) ไว้ในแต่ละช่องจัดเก็บ คีย์ทั้งหมดที่ชนกันในช่องนั้นจะรวมกันเป็นสายโซ่ ส่วนการระบุตำแหน่งแบบเปิดจะค้นหาช่องจัดเก็บถัดไปที่ว่างตามลำดับการไล่ตรวจ
# Simplified chaining hash map
class ChainingHashMap:
def __init__(self, capacity=8):
self.capacity = capacity
self.buckets = [[] for _ in range(capacity)]
def _idx(self, key):
return hash(key) % self.capacity
def put(self, key, val):
bucket = self.buckets[self._idx(key)]
for i, (k, v) in enumerate(bucket):
if k == key:
bucket[i] = (key, val)
return
bucket.append((key, val))
def get(self, key):
for k, v in self.buckets[self._idx(key)]:
if k == key:
return v
return None
hm = ChainingHashMap()
hm.put('a', 1); hm.put('b', 2)
print(hm.get('a')) # 1
print(hm.get('c')) # Noneการระบุตำแหน่งแบบเปิด: การไล่ตรวจเชิงเส้น
ในการ ไล่ตรวจเชิงเส้น เมื่อเกิดการชนกันที่ดัชนี i แฮชแมปจะตรวจสอบ i+1, i+2, ... (วนกลับไปต้นอาร์เรย์เมื่อถึงปลาย) จนกว่าจะพบช่องว่าง การค้นหาต้องไล่ตรวจตามลำดับเดียวกันเพื่อค้นหาคีย์ การลบต้องใช้เครื่องหมายแทนที่การลบแทนการล้างช่อง เพื่อไม่ให้สายโซ่การไล่ตรวจขาดตอน
การกระจุกตัวเป็นข้อเสียหลัก เมื่อเกิดกลุ่มช่องที่มีข้อมูลแล้ว การเพิ่มข้อมูลในบริเวณนั้นในอนาคตจะทำให้กลุ่มขยายใหญ่ขึ้น ส่งผลให้ประสิทธิภาพลดลงจนเข้าใกล้ O(n)
class LinearProbingHashMap:
DELETED = object() # tombstone sentinel
def __init__(self, capacity=8):
self.capacity = capacity
self.keys = [None] * capacity
self.vals = [None] * capacity
self.size = 0
def _probe(self, key):
idx = hash(key) % self.capacity
while self.keys[idx] is not None and self.keys[idx] != key:
idx = (idx + 1) % self.capacity
return idx
def put(self, key, val):
idx = self._probe(key)
if self.keys[idx] is None:
self.size += 1
self.keys[idx] = key
self.vals[idx] = val
def get(self, key):
idx = self._probe(key)
if self.keys[idx] == key:
return self.vals[idx]
return None
hm = LinearProbingHashMap()
hm.put('x', 10); hm.put('y', 20)
print(hm.get('x')) # 10อัตราการโหลดและการปรับขนาด
อัตราการโหลดคืออัตราส่วนระหว่างรายการที่จัดเก็บกับความจุทั้งหมด: α = n/m เมื่อ α เพิ่มขึ้น ความน่าจะเป็นที่จะเกิดการชนกันจะสูงขึ้นและประสิทธิภาพจะลดลง พจนานุกรมของไพธอนจะปรับขนาด (เพิ่มความจุเป็นสองเท่า) เมื่ออัตราการโหลดสูงเกินประมาณ 2/3 การปรับขนาดจะคำนวณแฮชของรายการเดิมทั้งหมดใหม่เพื่อจัดเก็บลงในตารางที่ใหญ่ขึ้น — เป็นการดำเนินการ O(n) ที่เกิดขึ้นไม่บ่อย จึงทำให้ต้นทุนการเพิ่มข้อมูลเฉลี่ยตลอดการใช้งานเป็น O(1)
import sys
d = {}
prev_size = sys.getsizeof(d)
for i in range(30):
d[i] = i
new_size = sys.getsizeof(d)
if new_size != prev_size:
print(f'Resized at n={i+1}: {prev_size} -> {new_size} bytes')
prev_size = new_sizeค่าเฉลี่ย O(1) เทียบกับกรณีแย่ที่สุด O(n)
ภายใต้ฟังก์ชันแฮชที่ดี การชนกันจะเกิดขึ้นไม่บ่อย และความยาวสายโซ่ที่คาดหมายจะคงที่โดยไม่ขึ้นกับ n ดังนั้นการค้นหา การเพิ่ม และการลบในกรณีเฉลี่ยจึงมีความซับซ้อนเป็น O(1) อย่างไรก็ตาม กรณีแย่ที่สุด — เช่น ข้อมูลนำเข้าที่จงใจสร้างขึ้นเพื่อโจมตีและทำให้คีย์ทั้งหมดไปอยู่ในช่องเดียวกัน — จะทำให้การดำเนินการทั้งหมดมีความซับซ้อนเป็น O(n) เมล็ดแฮชแบบสุ่มของไพธอนช่วยลดผลกระทบจากการโจมตีนี้ แต่ในทางทฤษฎียังไม่สามารถกำจัดกรณีแย่ที่สุดได้
ในการวิเคราะห์สำหรับการสัมภาษณ์ ให้กล่าวว่า “O(1) โดยเฉลี่ย และ O(n) ในกรณีแย่ที่สุดเนื่องจากการชนกัน”
# Python randomised hash seed prevents worst-case hash-flooding
import os
print('PYTHONHASHSEED:', os.environ.get('PYTHONHASHSEED', 'random'))
# By default Python randomises the hash of strings each run
# This prevents an attacker from crafting keys that all collide
# To reproduce results in testing: PYTHONHASHSEED=0 python script.pyพจนานุกรมไพธอนเทียบกับพจนานุกรมค่าเริ่มต้นและตัวนับ
ไพธอนมีแฮชแมปสามรูปแบบที่ควรรู้จัก dict คือแฮชแมปสำหรับใช้งานทั่วไป การเข้าถึงคีย์ที่ไม่มีอยู่จะทำให้เกิด KeyError ส่วน defaultdict(factory) จะคืนค่าเริ่มต้นเมื่อเข้าถึงคีย์ที่ไม่มีอยู่ (มีประโยชน์สำหรับการรวบรวมรายการหรือการนับ) และ Counter เป็นคลาสย่อยเฉพาะทางสำหรับนับวัตถุที่แฮชได้ ทั้งยังรองรับการดำเนินการทางคณิตศาสตร์ระหว่างตัวนับด้วย
from collections import defaultdict, Counter
# defaultdict for grouping
groups = defaultdict(list)
for word in ['apple', 'ant', 'banana', 'bee', 'avocado']:
groups[word[0]].append(word)
print(dict(groups))
# {'a': ['apple','ant','avocado'], 'b': ['banana','bee']}
# Counter for frequency
c = Counter('abracadabra')
print(c.most_common(3)) # [('a',5),('b',2),('r',2)]
print(c['a'] - Counter('aa')['a']) # counter subtractionแฮชแมปเทียบกับเซตแฮช
เซตแฮชจัดเก็บเฉพาะคีย์ (ไม่มีค่าที่เชื่อมโยง) และรองรับการตรวจสอบสมาชิก การเพิ่ม และการลบด้วยความซับซ้อน O(1) set ของไพธอนคือเซตแฮช ควรใช้เซตเมื่อคุณต้องการเพียงตอบว่า “องค์ประกอบนี้มีอยู่หรือไม่” โดยไม่ต้องจัดเก็บข้อมูลที่เกี่ยวข้อง และควรใช้พจนานุกรมเมื่อจำเป็นต้องเชื่อมโยงค่าต่าง ๆ (จำนวน ผลลัพธ์ และอื่น ๆ) เข้ากับคีย์
# set for membership testing
visited = set()
for node in [1, 3, 5, 3, 7, 1]:
if node not in visited:
print('New node:', node)
visited.add(node)
# Set operations: union, intersection, difference
A = {1, 2, 3, 4}
B = {3, 4, 5, 6}
print('Union:', A | B) # {1,2,3,4,5,6}
print('Intersection:', A & B) # {3,4}
print('Difference:', A - B) # {1,2}การสร้างแฮชแมปตั้งแต่ต้น (ฉบับสัมภาษณ์)
ผู้สัมภาษณ์อาจขอให้คุณสร้างแฮชแมปพื้นฐาน องค์ประกอบสำคัญมีดังนี้: อาร์เรย์ช่องจัดเก็บขนาดคงที่ (ใช้ 16 หรือ 1024) แต่ละช่องจัดเก็บเป็นรายการคู่ (คีย์, ค่า) สำหรับการเชื่อมโยง ฟังก์ชันแฮช (ใช้แฮชในตัวของไพธอน % ความจุ) และการปรับขนาดเมื่ออัตราการโหลดสูงเกิน 0.7 การกล่าวถึงการปรับขนาดและอัตราการโหลดไว้ก่อนจะแสดงให้เห็นถึงความรู้ความเข้าใจในระดับลึก
class HashMap:
def __init__(self, capacity=16):
self.capacity = capacity
self.size = 0
self.buckets = [[] for _ in range(capacity)]
def _hash(self, key):
return hash(key) % self.capacity
def put(self, key, val):
b = self.buckets[self._hash(key)]
for i, (k, v) in enumerate(b):
if k == key:
b[i] = (key, val)
return
b.append((key, val))
self.size += 1
if self.size / self.capacity > 0.7:
self._resize()
def get(self, key, default=None):
for k, v in self.buckets[self._hash(key)]:
if k == key:
return v
return default
def _resize(self):
old = self.buckets
self.capacity *= 2
self.buckets = [[] for _ in range(self.capacity)]
self.size = 0
for bucket in old:
for k, v in bucket:
self.put(k, v)
hm = HashMap()
for i in range(20):
hm.put(i, i * 2)
print(hm.get(10)) # 20
print(hm.capacity) # should have resizedเมื่อแฮชแมปใช้ไม่ได้: คีย์ที่แฮชไม่ได้
เฉพาะวัตถุที่ แฮชได้เท่านั้นที่ใช้เป็นคีย์ของพจนานุกรมได้ ในไพธอน วัตถุจะแฮชได้หากมีเมธอด __hash__ และเมธอด __eq__ และค่าแฮชของวัตถุนั้นไม่เปลี่ยนแปลงตลอดช่วงอายุการใช้งาน ลิสต์ เซต และพจนานุกรมเปลี่ยนแปลงได้ จึงไม่สามารถแฮชได้ ทูเพิลและฟรอเซนเซตเป็นทางเลือกที่แฮชได้แทนลิสต์และเซตเมื่อต้องใช้เป็นคีย์
กับดักในการสัมภาษณ์ที่พบบ่อยคือ การจัดกลุ่มแอนนาแกรมต้องใช้ทูเพิลที่เรียงลำดับ (ไม่ใช่ลิสต์ที่เรียงลำดับ) เป็นคีย์ของพจนานุกรม
from collections import defaultdict
def groupAnagrams(strs):
groups = defaultdict(list)
for s in strs:
key = tuple(sorted(s)) # tuple is hashable; list is not
groups[key].append(s)
return list(groups.values())
print(groupAnagrams(['eat','tea','tan','ate','nat','bat']))
# [['eat','tea','ate'], ['tan','nat'], ['bat']]สรุป: ความซับซ้อนของแฮชแมป
แฮชแมปให้ความซับซ้อนเฉลี่ยเป็น O(1) สำหรับการเพิ่ม การลบ และการค้นหา ซึ่งเป็นพื้นฐานของวิธีแก้โจทย์สัมภาษณ์ที่มีประสิทธิภาพสูงหลายรูปแบบ สมมติฐานสำคัญคือ ฟังก์ชันแฮชกระจายคีย์อย่างสม่ำเสมอ อัตราการโหลดมีขอบเขตที่เหมาะสม (การปรับขนาดช่วยรักษาเงื่อนไขนี้) และวัตถุที่ใช้เป็นคีย์ไม่เปลี่ยนแปลงและแฮชได้ เมื่อสมมติฐานเหล่านี้เป็นจริง แฮชแมปจะเปลี่ยนการสแกนเชิงเส้น O(n) ให้เป็นการค้นหา O(1) ทำให้สามารถแก้โจทย์อย่างผลรวมสองค่าได้ใน O(n) แทน O(n²)
ตรวจสอบความเข้าใจ
ทดสอบความเข้าใจแนวคิดโครงสร้างข้อมูลและอัลกอริทึม — การเตรียมตัวสัมภาษณ์การเขียนโปรแกรมจากบทเรียนนี้
สรุปบทเรียน
ในบทเรียนนี้ คุณได้เรียนรู้ว่า แฮชแมปจับคู่คีย์กับดัชนีช่องจัดเก็บด้วยฟังก์ชันแฮช และทำให้การดำเนินการมีความซับซ้อนเฉลี่ยเป็น O(1) การชนกันแก้ไขได้ด้วยการเชื่อมโยง (รายการเชื่อมโยงหนึ่งรายการต่อช่องจัดเก็บ) หรือการระบุตำแหน่งแบบเปิด (การไล่ตรวจหาช่องว่างถัดไป) และ เฉพาะวัตถุที่ไม่เปลี่ยนแปลงและแฮชได้เท่านั้นที่ใช้เป็นคีย์ของพจนานุกรมได้ — ใช้ทูเพิลแทนลิสต์เมื่อต้องการคีย์ที่เป็นลำดับ บทถัดไปเราจะฝึกแก้โจทย์ผลรวมสองค่าและรูปแบบต่าง ๆ ที่พบบ่อยในการสัมภาษณ์
คำถามที่พบบ่อย
บทเรียน “ภายในฟังก์ชันแฮชและการจัดการการชนกัน” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “ภายในฟังก์ชันแฮชและการจัดการการชนกัน” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Coding Interview Prep ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Coding Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “ภายในฟังก์ชันแฮชและการจัดการการชนกัน”
ทำความเข้าใจวิธีที่ Python แฮชออบเจ็กต์ วิธีที่การกำหนดตำแหน่งแบบเปิดและการต่อโซ่แก้การชนกัน และเหตุใด O(1) ในกรณีเฉลี่ยจึงอาจแย่ลงเป็น O(n) คุณปฏิบัติ Coding Interview Prep ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Coding Interview Prep หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน Coding Interview Prep บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 1 จากทั้งหมด 4 บทเรียน
บทเรียน “ภายในฟังก์ชันแฮชและการจัดการการชนกัน” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน Coding Interview Prep นี้ได้ไหม
ได้ บทเรียน Coding Interview Prep ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- ภายในฟังก์ชันแฮชและการจัดการการชนกัน
- ผลรวมสองค่าและรูปแบบหลากหลาย
- การนับความถี่และการจัดกลุ่ม
- ลำดับต่อเนื่องที่ยาวที่สุดและแคช LRU