การเรียงสับเปลี่ยนและการจัดหมู่
แจกแจงการเรียงสับเปลี่ยนทั้งหมดของรายการทั้งกรณีที่มีและไม่มีสมาชิกซ้ำ พร้อมสร้างการจัดหมู่ขนาด k และรูปแบบผลรวมของการจัดหมู่ทั้งหมด
การเรียงสับเปลี่ยนและการจัดหมู่ เป็นบทเรียน DSA Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน DSA Interview Prep และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส DSA Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
การเรียงสับเปลี่ยนกับการจัดหมู่
การเรียงสับเปลี่ยน คือการจัดวางที่ ลำดับมีความสำคัญ: [1,2,3] และ [3,2,1] ถือว่าแตกต่างกัน จำนวนการเรียงสับเปลี่ยนของสมาชิก n รายการคือ n! การจัดหมู่ คือการเลือกที่ ลำดับไม่มีความสำคัญ: การเลือก {1,2} เหมือนกับ {2,1} จำนวนการจัดหมู่ขนาด k จากสมาชิก n รายการคือ C(n,k) = n! / (k! × (n-k)!) ทั้งสองรูปแบบเป็นพื้นฐานสำคัญของโจทย์สัมภาษณ์ที่เกี่ยวกับการนับ การแจกแจง และการเลือก
import math
# Permutations
n = 4
print(f'Permutations of {n} items: {math.factorial(n)}')
# 4! = 24
# Combinations
for k in range(n+1):
print(f'C({n},{k}) = {math.comb(n,k)}')
# C(4,0)=1, C(4,1)=4, C(4,2)=6, C(4,3)=4, C(4,4)=1
# Sum = 2^4 = 16 (total subsets)การสร้างการเรียงสับเปลี่ยนทั้งหมด
ใช้อาร์เรย์บูลีน used เพื่อติดตามว่าสมาชิกใดอยู่ในเส้นทางปัจจุบัน ในแต่ละขั้น ให้ลองสมาชิกทุกตัวที่ยังไม่ได้ใช้ หลังสำรวจเสร็จแล้ว ให้ทำเครื่องหมายสมาชิกนั้นว่าไม่ได้ใช้อีกครั้ง ต่างจาก subsets ตรงที่ไม่มีดัชนี start เพราะการเรียงสับเปลี่ยนสามารถใช้สมาชิกในลำดับใดก็ได้ การเรียกซ้ำจะสิ้นสุดเมื่อ len(path) == n
def permutations(nums):
result = []
used = [False] * len(nums)
def backtrack(path):
if len(path) == len(nums):
result.append(list(path))
return
for i, num in enumerate(nums):
if not used[i]:
used[i] = True # CHOOSE
path.append(num)
backtrack(path) # EXPLORE
path.pop() # UNCHOOSE
used[i] = False
backtrack([])
return result
print(permutations([1, 2, 3]))
# [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]การเรียงสับเปลี่ยนแบบสลับค่า
อีกทางเลือกหนึ่งคือ สลับสมาชิกที่ตำแหน่ง start กับสมาชิกแต่ละตัวตั้งแต่ start ถึง n-1 เรียกซ้ำ แล้วสลับกลับ วิธีนี้แก้ไขอาร์เรย์ภายในที่เดิมโดยไม่ต้องใช้อาร์เรย์ used แนวคิดสำคัญคือ ในแต่ละระดับ สมาชิกทุกตัวทางซ้ายของ start จะถูกกำหนดตำแหน่งไว้แล้ว และเราจะเลือกว่าสมาชิกใดไปอยู่ที่ตำแหน่ง start วิธีนี้ใช้หน่วยความจำมีประสิทธิภาพกว่าเล็กน้อย และเป็นพื้นฐานของอัลกอริทึมของฮีป
def permutations_swap(nums):
result = []
def backtrack(start):
if start == len(nums):
result.append(list(nums))
return
for i in range(start, len(nums)):
nums[start], nums[i] = nums[i], nums[start] # CHOOSE (swap)
backtrack(start + 1) # EXPLORE
nums[start], nums[i] = nums[i], nums[start] # UNCHOOSE (swap back)
backtrack(0)
return result
print(permutations_swap([1, 2, 3]))
# Same 6 permutations, different orderการเรียงสับเปลี่ยน II: การจัดการค่าซ้ำ
เมื่ออินพุตมีค่าซ้ำ (เช่น [1, 1, 2]) แนวทางใช้อาร์เรย์ติดตามสมาชิกที่ใช้แล้วจะสร้างการเรียงสับเปลี่ยนซ้ำ วิธีแก้คือเรียงลำดับอาร์เรย์ก่อน จากนั้นข้ามค่าซ้ำหากสมาชิกที่เหมือนกันก่อนหน้าไม่ได้ถูกใช้ในการเรียกซ้ำครั้งนี้ เงื่อนไขคือ if i > 0 and nums[i] == nums[i-1] and not used[i-1]: continue วิธีนี้บังคับให้เลือกค่าซ้ำจากซ้ายไปขวาเสมอ
def permutations_unique(nums):
nums.sort()
result = []
used = [False] * len(nums)
def backtrack(path):
if len(path) == len(nums):
result.append(list(path))
return
for i in range(len(nums)):
if used[i]: continue
# Skip if this num is a duplicate and the previous dup was not used
if i > 0 and nums[i] == nums[i-1] and not used[i-1]:
continue
used[i] = True
path.append(nums[i])
backtrack(path)
path.pop()
used[i] = False
backtrack([])
return result
print(permutations_unique([1, 1, 2]))
# [[1,1,2],[1,2,1],[2,1,1]] — 3, not 6การเรียงสับเปลี่ยนถัดไป (ตามลำดับพจนานุกรม)
การเรียงสับเปลี่ยนถัดไป (LeetCode 31) จะแปลงอาร์เรย์ให้เป็นการเรียงสับเปลี่ยนถัดไปที่มีค่ามากขึ้นตามลำดับพจนานุกรม โดยแก้ไขภายในที่เดิม อัลกอริทึมมีขั้นตอนดังนี้: (1) หาดัชนีขวาสุด i ที่ nums[i] < nums[i+1] (2) หาดัชนีขวาสุด j ที่ nums[j] > nums[i] (3) สลับ nums[i] กับ nums[j] (4) กลับลำดับส่วนท้ายหลังดัชนี i หากไม่มี i ดังกล่าว ให้กลับลำดับอาร์เรย์ทั้งหมด (วนกลับไปเป็นการเรียงสับเปลี่ยนที่เล็กที่สุด)
def next_permutation(nums):
n = len(nums)
# Step 1: find rightmost i where nums[i] < nums[i+1]
i = n - 2
while i >= 0 and nums[i] >= nums[i+1]:
i -= 1
if i >= 0:
# Step 2: find rightmost j where nums[j] > nums[i]
j = n - 1
while nums[j] <= nums[i]:
j -= 1
# Step 3: swap
nums[i], nums[j] = nums[j], nums[i]
# Step 4: reverse suffix after i
nums[i+1:] = nums[i+1:][::-1]
return nums
print(next_permutation([1, 2, 3])) # [1,3,2]
print(next_permutation([3, 2, 1])) # [1,2,3] (wraps)
print(next_permutation([1, 1, 5])) # [1,5,1]การย้อนกลับสำหรับการจัดหมู่ k รายการ
สร้างการจัดหมู่ของสมาชิก k รายการจากสมาชิก n รายการทั้งหมด (LeetCode 77) ใช้ดัชนีเริ่มต้นเหมือนกับ subsets เพื่อหลีกเลี่ยงการทำ revisiting สมาชิกและรักษาลำดับที่เรียงแล้ว ตัดกิ่งเมื่อมีสมาชิกเหลือน้อยกว่า k - len(path) รายการ: if len(nums) - i + 1 < k - len(path): break วิธีนี้เทียบเท่ากับ combine(n, k) ก่อนหน้านี้ แต่ทำงานกับอาร์เรย์จริง
def combinations(nums, k):
result = []
def backtrack(start, path):
if len(path) == k:
result.append(list(path))
return
for i in range(start, len(nums)):
# Pruning: not enough elements left
if len(nums) - i < k - len(path):
break
path.append(nums[i])
backtrack(i + 1, path)
path.pop()
backtrack(0, [])
return result
print(combinations([1,2,3,4,5], 3))
# 10 combinations: C(5,3)
import math
print(math.comb(5,3)) # 10ผลรวมของการจัดหมู่: ใช้ซ้ำได้ไม่จำกัด
ผลรวมของการจัดหมู่ (LeetCode 39) อนุญาตให้ใช้ตัวเลขแต่ละตัวซ้ำได้ไม่จำกัด ความแตกต่างจากการจัดหมู่ทั่วไปคือ แทนที่จะเลื่อน start ไปที่ i+1 ให้ส่งค่า i (ดัชนีเดิม) เพื่อให้ใช้สมาชิกปัจจุบันซ้ำได้ การตัดกิ่งทำดังนี้: หากเป้าหมายที่เหลือเป็น 0 ให้บันทึกเส้นทาง หากมีค่าติดลบ ให้หยุด การเรียงลำดับช่วยให้ยุติการค้นหาได้ก่อน เมื่อสมาชิกตัวเลือกที่เหลือทั้งหมดมีค่ามากกว่าเป้าหมายที่เหลือ
def combination_sum(candidates, target):
candidates.sort()
result = []
def backtrack(start, path, remaining):
if remaining == 0:
result.append(list(path))
return
for i in range(start, len(candidates)):
c = candidates[i]
if c > remaining: break # all remaining are too big
path.append(c)
backtrack(i, path, remaining - c) # reuse allowed: pass i, not i+1
path.pop()
backtrack(0, [], target)
return result
print(combination_sum([2, 3, 6, 7], 7))
# [[2,2,3],[7]]ผลรวมของการจัดหมู่ II: ไม่ใช้ซ้ำและมีค่าซ้ำในอินพุต
ผลรวมของการจัดหมู่ II (LeetCode 40) ใช้ตัวเลขแต่ละตัวได้ไม่เกินหนึ่งครั้ง แต่อินพุตอาจมีค่าซ้ำ เทคนิคที่ใช้ร่วมกันมีสองอย่าง: เลื่อน start ไปที่ i+1 (ไม่ใช้ซ้ำ) และข้ามค่าซ้ำในระดับเดียวกัน (if i > start and nums[i] == nums[i-1]: continue) หลังจากเรียงลำดับแล้ว วิธีนี้รวมการจัดการค่าซ้ำจาก subsets II เข้ากับข้อจำกัดไม่ใช้ซ้ำของการจัดหมู่
def combination_sum_ii(candidates, target):
candidates.sort()
result = []
def backtrack(start, path, remaining):
if remaining == 0:
result.append(list(path))
return
for i in range(start, len(candidates)):
if candidates[i] > remaining: break
# Skip duplicates at same level
if i > start and candidates[i] == candidates[i-1]:
continue
path.append(candidates[i])
backtrack(i + 1, path, remaining - candidates[i]) # no reuse: i+1
path.pop()
backtrack(0, [], target)
return result
print(combination_sum_ii([10,1,2,7,6,1,5], 8))
# [[1,1,6],[1,2,5],[1,7],[2,6]]การจัดหมู่ตัวอักษรจากหมายเลขโทรศัพท์
การจัดหมู่ตัวอักษร (LeetCode 17) จับคู่ตัวเลขแต่ละหลักกับตัวอักษรบนแป้นกดโทรศัพท์ และสร้างการจัดหมู่ตัวอักษรที่เป็นไปได้ทั้งหมดสำหรับสตริงตัวเลขที่กำหนด นี่เป็นโจทย์การย้อนกลับ โดยในแต่ละตำแหน่ง เราจะเลือกตัวอักษรหนึ่งตัวจากการจับคู่ของตัวเลขหลักนั้นแล้วเรียกซ้ำต่อ สำหรับสตริงความยาว n ที่แต่ละหลักมีตัวอักษรเฉลี่ย k ตัว ความซับซ้อนด้าน time คือ O(kⁿ)
def letter_combinations(digits):
if not digits: return []
phone = {
'2': 'abc', '3': 'def', '4': 'ghi', '5': 'jkl',
'6': 'mno', '7': 'pqrs', '8': 'tuv', '9': 'wxyz'
}
result = []
def backtrack(index, path):
if index == len(digits):
result.append(''.join(path))
return
for letter in phone[digits[index]]:
path.append(letter)
backtrack(index + 1, path)
path.pop()
backtrack(0, [])
return result
print(letter_combinations('23'))
# ['ad','ae','af','bd','be','bf','cd','ce','cf']การเปรียบเทียบการเรียงสับเปลี่ยนและการจัดหมู่
ความแตกต่างเชิงโครงสร้างที่สำคัญมีดังนี้: การเรียงสับเปลี่ยน — ไม่มีดัชนีเริ่มต้น ใช้อาร์เรย์ used หรือการสลับค่าเพื่อป้องกันการใช้ซ้ำ ต้นไม้มีตัวเลือก n รายการในแต่ละระดับ และมีใบไม้ทั้งหมด n! ใบ การจัดหมู่ — ใช้ดัชนีเริ่มต้นเพื่อบังคับลำดับ มีใบไม้ C(n,k) ใบ ผลรวมของการจัดหมู่ — ไม่เลื่อนดัชนีเริ่มต้นเพื่อให้ใช้ซ้ำได้ และตัดกิ่งตามเป้าหมาย การจับคู่โจทย์ใหม่กับหนึ่งในสามรูปแบบนี้ช่วยให้เลือกแม่แบบที่ถูกต้องได้ทันที
# Pattern summary:
# Permutations: for i in range(n); if not used[i]; no start advancement
# Combinations: for i in range(start, n); advance start → i+1
# Combo Sum (reuse): for i in range(start, n); advance start → i (same)
# Quick reference:
import math
n = 5
print(f'Perm({n}) = n! = {math.factorial(n)}')
print(f'Comb({n},2) = C(n,k) = {math.comb(n,2)}')
print(f'Comb({n},3) = {math.comb(n,3)}')
# Also: subsets = sum(C(n,k) for k=0..n) = 2^n
print(f'Subsets({n}) = 2^n = {2**n}')ความซับซ้อนและเคล็ดลับการสัมภาษณ์
ความซับซ้อนด้านเวลาในการแจกแจง: การเรียงสับเปลี่ยน O(n × n!), การจัดหมู่ O(k × C(n,k)), ผลรวมของการจัดหมู่ O(n^(T/min_val)) การใช้หน่วยความจำคือ O(n) สำหรับความลึกของการเรียกซ้ำ บวก O(ผลลัพธ์) สำหรับคำตอบ เคล็ดลับสำคัญ: (1) ต้องทำให้ชัดเจนเสมอว่าลำดับมีความสำคัญหรือไม่ (การเรียงสับเปลี่ยนหรือการจัดหมู่) (2) กล่าวถึงการจัดการค่าซ้ำก่อนที่จะถูกถาม (3) ระบุเงื่อนไขการตัดกิ่งอย่างชัดเจนเสมอ (4) สำหรับ n ที่มีขนาดใหญ่ ให้ชี้ว่าผลลัพธ์เองมีขนาดแบบเอ็กซ์โพเนนเชียล — อัลกอริทึมนี้จึงเหมาะสมที่สุดสำหรับโจทย์ดังกล่าว
import math
# Complexity for n=10
n = 10
print(f'Permutations(10): {math.factorial(n):,} results')
print(f'Combinations(10,5): {math.comb(n,5):,} results')
print(f'Subsets(10): {2**n:,} results')
# For interview: state which pattern
# 'This is a combinations problem because order doesnt matter'
# 'I will use a start index to avoid revisiting elements'
# 'Pruning: when sum exceeds target, break (after sorting)'ตรวจสอบความเข้าใจ
ทดสอบความเข้าใจแนวคิดเรื่องโครงสร้างข้อมูล & อัลกอริทึม — การเตรียมตัวสัมภาษณ์การเขียนโปรแกรมจากบทเรียนนี้
สรุปบทเรียน
ในบทเรียนนี้ คุณได้เรียนรู้ว่า: permutations ใช้อาร์เรย์ used และไม่มีดัชนีเริ่มต้น เพื่อสร้างการจัดวาง n! รูปแบบ combinations ใช้ดัชนีเริ่มต้นที่เลื่อนไปข้างหน้าเพื่อหลีกเลี่ยงการใช้ซ้ำ และสร้างการเลือก C(n,k) รูปแบบ และ ค่าซ้ำในปัญหาทั้งสองประเภทจัดการได้ด้วยการเรียงลำดับและข้ามค่าที่ซ้ำกันในระดับการเรียกซ้ำเดียวกัน บทถัดไปเราจะประยุกต์ใช้การย้อนกลับกับปัญหา N-ควีนส์ และสำรวจการเผยแพร่ข้อจำกัด
คำถามที่พบบ่อย
บทเรียน “การเรียงสับเปลี่ยนและการจัดหมู่” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “การเรียงสับเปลี่ยนและการจัดหมู่” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส DSA Interview Prep ให้อัปเกรดเป็น CoddyKit PRO คอร์ส DSA Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “การเรียงสับเปลี่ยนและการจัดหมู่”
แจกแจงการเรียงสับเปลี่ยนทั้งหมดของรายการทั้งกรณีที่มีและไม่มีสมาชิกซ้ำ พร้อมสร้างการจัดหมู่ขนาด k และรูปแบบผลรวมของการจัดหมู่ทั้งหมด คุณปฏิบัติ DSA Interview Prep ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน DSA Interview Prep หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน DSA Interview Prep บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน
บทเรียน “การเรียงสับเปลี่ยนและการจัดหมู่” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน DSA Interview Prep นี้ได้ไหม
ได้ บทเรียน DSA Interview Prep ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- แม่แบบการย้อนรอย: เลือก สำรวจ ยกเลิกการเลือก
- เซตย่อยและเพาเวอร์เซต
- การเรียงสับเปลี่ยนและการจัดหมู่
- N-ควีนและการเผยแพร่ข้อจำกัด