فرز بلا مقارنة ودالة sort() في Python
استكشف فرز العد وفرز الجذر للمصفوفات الصحيحة، وافهم كيفية عمل Timsort في Python من الداخل عند استدعاء sort المضمّن
فرز بلا مقارنة ودالة sort() في Python درس مجاني في DSA Interview Prep على CoddyKit. هذا هو الدرس 4 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في DSA Interview Prep، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة DSA Interview Prep 4 دروس في المجموع.
الحد الأدنى O(n log n) للمقارنات
تتطلب أي خوارزمية فرز تحدد الترتيب من خلال مقارنات العناصر فقط ما لا يقل عن Ω(n log n) من المقارنات في أسوأ الحالات. ويُثبت ذلك باستخدام حجة شجرة القرار: إذ يتطلب فرز n من العناصر التمييز بين n! من الترتيبات الممكنة. وتحتاج شجرة قرار ثنائية (كل عقدة فيها تمثل مقارنة) إلى log₂(n!) ≈ n log₂(n) من المستويات على الأقل. ولكسر هذا الحد، نحتاج إلى معلومات إضافية عن العناصر، مثل كونها أعدادًا صحيحة محدودة النطاق.
import math
for n in [5, 10, 100, 1000]:
lower_bound = n * math.log2(n)
factorial_log = sum(math.log2(i) for i in range(1, n+1))
print(f'n={n}: n*log2(n)={lower_bound:.1f}, log2(n!)={factorial_log:.1f}')
# n log n is a tight bound on comparison-based sortingفرز العد: الفرز حسب التكرار
يعمل فرز العد من خلال عدّ تكرار كل قيمة، ثم إعادة بناء المصفوفة المرتبة من هذه التعدادات. ويتطلب معرفة النطاق [0, k) للقيم مسبقًا. تعقيد الزمن: O(n + k)؛ وتعقيد المساحة: O(k). وعندما تكون k صغيرة مقارنةً بـ n (مثل فرز الأعمار من 0 إلى 120 أو الأرقام المفردة)، يتفوق فرز العد على جميع خوارزميات الفرز المعتمدة على المقارنة. أما عندما تكون k كبيرة، فتجعل تكلفة المساحة O(k) استخدامه غير عملي.
def counting_sort(arr, k=None):
if not arr: return []
if k is None: k = max(arr) + 1
count = [0] * k
for n in arr:
count[n] += 1
result = []
for val, freq in enumerate(count):
result.extend([val] * freq)
return result
arr = [4, 2, 2, 8, 3, 3, 1]
print(counting_sort(arr)) # [1, 2, 2, 3, 3, 4, 8]
# O(n + k) where k = 9 (max value + 1)فرز العد المستقر باستخدام التعدادات التراكمية
بالنسبة إلى فرز العد المستقر (وهو مهم عند فرز الكائنات وفق مفتاح)، احسب التعدادات التراكمية بحيث يعطي cum[v] موضع البدء للقيمة v في الناتج. امسح مصفوفة الإدخال من اليمين إلى اليسار، وضع كل عنصر في الموضع cum[key] - 1 ثم أنقص قيمة ذلك الموضع. ينتج عن ذلك فرز مستقر — إذ تظهر العناصر ذات المفتاح نفسه بترتيبها النسبي الأصلي.
def counting_sort_stable(arr, k):
count = [0] * k
for n in arr: count[n] += 1
# Cumulative counts: count[v] = first position for value v
for i in range(1, k): count[i] += count[i-1]
output = [0] * len(arr)
# Fill from right to maintain stability
for n in reversed(arr):
count[n] -= 1
output[count[n]] = n
return output
print(counting_sort_stable([4,2,2,8,3,3,1], 9))
# [1, 2, 2, 3, 3, 4, 8]الفرز الجذري: فرز رقمًا رقمًا
الفرز الجذري يرتب الأعداد الصحيحة رقمًا رقمًا، بدءًا من الرقم الأقل أهمية (LSD) وصولًا إلى الرقم الأكثر أهمية (MSD)، باستخدام فرز مستقر (مثل فرز العد) في كل موضع رقمي. بعد d تمريرات، بمعدل تمريرة واحدة لكل رقم، تصبح المصفوفة مرتبة بالكامل. التعقيد الزمني: O(d × (n + k))، حيث d = عدد الأرقام وk = الأساس (عادةً 10). بالنسبة إلى n من الأعداد الصحيحة المحصورة ضمن W، يكون d = log_k(W)، ما يعطي إجماليًا قدره O(n log_k(W)).
def radix_sort(arr):
if not arr: return []
max_val = max(arr)
exp = 1 # current digit position (1, 10, 100, ...)
while max_val // exp > 0:
arr = counting_sort_by_digit(arr, exp)
exp *= 10
return arr
def counting_sort_by_digit(arr, exp):
n = len(arr)
output = [0] * n
count = [0] * 10
for n_ in arr: count[(n_ // exp) % 10] += 1
for i in range(1, 10): count[i] += count[i-1]
for n_ in reversed(arr):
d = (n_ // exp) % 10
count[d] -= 1
output[count[d]] = n_
return output
print(radix_sort([170, 45, 75, 90, 802, 24, 2, 66]))
# [2, 24, 45, 66, 75, 90, 170, 802]فرز الدلاء: توزيع العناصر على الدلاء
فرز الدلاء يوزّع العناصر على عدد ثابت من الدلاء استنادًا إلى نطاق القيمة، ويفرز كل دلو (باستخدام فرز الإدراج للدلاء الصغيرة)، ثم يضم النتائج. بالنسبة إلى البيانات الموزعة بانتظام في [0, 1)، يحقق استخدام n من الدلاء زمنًا متوسطًا قدره O(n). الزمن: O(n + k) في المتوسط، وO(n²) في أسوأ الحالات (عندما تقع جميع العناصر في دلو واحد). يكون هذا الفرز مفيدًا بدرجة أكبر عندما يكون توزيع البيانات معروفًا ومتقاربًا من التوزيع المنتظم.
def bucket_sort(arr):
if not arr: return []
n = len(arr)
min_v, max_v = min(arr), max(arr)
if min_v == max_v: return arr[:]
buckets = [[] for _ in range(n)]
# Map each value to a bucket index
for v in arr:
idx = int((v - min_v) / (max_v - min_v + 1e-9) * n)
idx = min(idx, n - 1)
buckets[idx].append(v)
result = []
for bucket in buckets:
bucket.sort() # insertion sort for small buckets
result.extend(bucket)
return result
print(bucket_sort([0.78, 0.17, 0.39, 0.26, 0.72, 0.94, 0.21]))
# sorted listآلية عمل Timsort في Python
تستخدم sorted() وlist.sort() في Python خوارزمية Timsort، التي صممها Tim Peters عام 2002. تجمع Timsort بين فرز الدمج وفرز الإدراج. فهي تبحث عن «تتابعات طبيعية» (تتابعات فرعية مرتبة مسبقًا)، وتستخدم فرز الإدراج لبناء تتابعات يصل طولها إلى 64 عنصرًا. ثم تدمج التتابعات باستخدام فرز الدمج مع عدة تحسينات: القفز (تجاوز العناصر على دفعات عندما يكون أحد التتابعين مهيمنًا) وتكديس أطوال التتابعات.
# Timsort properties:
# - Stable
# - O(n log n) worst case
# - O(n) best case (data already sorted)
# - O(n) auxiliary space
# - Highly optimised for real-world data with runs
import time
# Nearly sorted data: Timsort is extremely fast
nearly_sorted = list(range(10000))
nearly_sorted[-1] = 0 # one mis-placed element
t = time.perf_counter()
not_used = sorted(nearly_sorted)
elapsed = time.perf_counter() - t
print(f'Timsort on nearly-sorted n=10000: {elapsed*1000:.3f} ms')الفرق الأساسي بين sort() وsorted() في Python
يفرز list.sort() القائمة في موضعها، ويعيد None، ولا يعمل إلا على القوائم. أما sorted(iterable) فيعمل مع أي كائن قابل للتكرار (مثل الصفوف، والمولدات، والقواميس)، ويعيد قائمة جديدة. يقبل كلاهما المعاملين key وreverse. من الأخطاء الشائعة إسناد القيمة المعادة من lst.sort() إلى متغير ثم الاستغراب من كونها None. استخدموا sorted() دائمًا عندما تحتاجون إلى النسخة المرتبة مع الاحتفاظ بالأصل.
nums = [3, 1, 4, 1, 5, 9]
# in-place: returns None
result = nums.sort()
print(result) # None (common bug!)
print(nums) # [1, 1, 3, 4, 5, 9] (modified)
nums2 = [3, 1, 4, 1, 5, 9]
# out-of-place: returns new list
result2 = sorted(nums2)
print(result2) # [1, 1, 3, 4, 5, 9]
print(nums2) # [3, 1, 4, 1, 5, 9] (unchanged)مفاتيح الفرز المخصصة في مقابلات البرمجة
يقبل فرز Python دالة key تُقيَّم مرة واحدة لكل عنصر (بخلاف المقارن في C الذي يُستدعى لكل زوج). من مفاتيح الفرز الشائعة في المقابلات: len لطول السلسلة النصية، وlambda x: -x للترتيب التنازلي، وlambda x: (x[1], x[0]) للفرز باستخدام مفاتيح متعددة، وstr.lower للفرز غير الحساس لحالة الأحرف. فرز Python مستقر مضمونًا، ولذلك تعمل عمليات الفرز باستخدام مفاتيح متعددة بشكل صحيح.
# Sort by length, then alphabetically
words = ['banana', 'fig', 'apple', 'date', 'kiwi']
print(sorted(words, key=lambda w: (len(w), w)))
# ['fig', 'date', 'kiwi', 'apple', 'banana']
# Sort integers as strings (largest concatenation first)
nums = [3, 30, 34, 5, 9]
print(sorted(map(str, nums), key=lambda a: a*10, reverse=True))
# ['9', '5', '34', '3', '30'] => '9534330'
# Descending sort
print(sorted([3,1,4,1,5], reverse=True)) # [5,4,3,1,1]متى تستخدمون كل خوارزمية فرز في المقابلات
اختاروا خوارزمية الفرز المناسبة للسياق:
- استخدموا sorted()/list.sort() في Python: الخيار الافتراضي لجميع مسائل المقابلات — إذ إن Timsort مثالي
- فرز العد: عندما تكون القيم أعدادًا صحيحة صغيرة ومحدودة (من 0 إلى k، مع كون k صغيرًا)
- الفرز الجذري: عند فرز أعداد صحيحة كثيرة مع معرفة عرض البتات أو عدد الأرقام
- فرز الدلاء: عندما تكون البيانات أعدادًا عشرية موزعة بانتظام ضمن نطاق معروف
- طبّقوا فرز الدمج: عندما يُطلب منكم كتابة خوارزمية فرز مستقرة بتعقيد O(n log n) من الصفر
# Problem: sort array of 0s, 1s, 2s efficiently
# Counting sort: O(n), O(1) space (k=3 is tiny)
def sort_012(arr):
count = [0, 0, 0]
for n in arr:
count[n] += 1
i = 0
for val in range(3):
for _ in range(count[val]):
arr[i] = val; i += 1
arr = [2, 0, 2, 1, 1, 0]
sort_012(arr)
print(arr) # [0, 0, 1, 1, 2, 2]الفرز دون فرز: أكبر k عنصرًا باستخدام كومة
تطلب كثير من مسائل المقابلات نتائج شبيهة بالفرز دون الحاجة إلى إجراء فرز كامل. العثور على أكبر k عنصرًا باستخدام كومة دنيا حجمها k يستغرق O(n log k)، وهو أسرع من O(n log n) عندما يكون k << n. أما العثور على العنصر الأكبر k، فتستغرق خوارزمية quickselect وقتًا متوسطًا قدره O(n). ويستغرق العثور على الوسيط باستخدام نهج الكومتين O(log n) لكل عملية إدراج. تجدر معرفة أساليب الفرز الجزئي هذه لأنها بدائل أسرع من الفرز الكامل.
import heapq
# Top-k with heap: O(n log k)
def top_k(nums, k):
return heapq.nlargest(k, nums) # uses heap of size k internally
print(top_k([3,2,1,5,6,4], 2)) # [6, 5]
# kth largest: quickselect O(n) average
import random
def kth_largest(nums, k):
def _select(lo, hi, target):
if lo >= hi: return nums[lo]
rand_i = random.randint(lo, hi)
nums[rand_i], nums[hi] = nums[hi], nums[rand_i]
pivot = nums[hi]; i = lo - 1
for j in range(lo, hi):
if nums[j] >= pivot: i+=1; nums[i],nums[j]=nums[j],nums[i]
nums[i+1],nums[hi]=nums[hi],nums[i+1]
p = i + 1
if p == target: return nums[p]
return _select(lo, p-1, target) if target < p else _select(p+1, hi, target)
return _select(0, len(nums)-1, k-1)
print(kth_largest([3,2,1,5,6,4], 2)) # 5استقرار الفرز عند الفرز باستخدام مفاتيح متعددة
يتيح الاستقرار إجراء فرز صحيح باستخدام مفاتيح متعددة: افرزوا بالمفتاح الثانوي أولًا (باستقرار)، ثم بالمفتاح الأساسي (باستقرار). يُحافَظ على ترتيب المفتاح الثانوي عند تساوي قيم المفتاح الأساسي. تُستخدم هذه التقنية في قواعد البيانات (ORDER BY col1, col2) وفي الفرز الجذري (إذ يجب أن تكون كل تمريرة على رقم مستقرة حتى تكون الخوارزمية ككل صحيحة). فرز Python مستقر دائمًا، لذا يعمل هذا النمط بشكل موثوق.
data = [
('Alice', 'Math', 90),
('Bob', 'Science', 85),
('Carol', 'Math', 90),
('Dave', 'Science', 90),
]
# Sort by score DESC, then by subject ASC (for ties)
# Step 1: sort by subject (secondary)
data.sort(key=lambda x: x[1])
# Step 2: sort by score DESC (primary, stable)
data.sort(key=lambda x: x[2], reverse=True)
for row in data:
print(row)
# All score=90 rows: Math before Science (preserved from step 1)اختبار سريع
اختبروا مدى فهمكم لمفاهيم Data Structures & Algorithms — Coding Interview Prep الواردة في هذا الدرس.
مراجعة الدرس
في هذا الدرس تعلمتم أن: خوارزميات الفرز القائمة على المقارنة لا يمكن أن يقل تعقيدها عن O(n log n) — وكسر هذا الحد يتطلب معلومات غير قائمة على المقارنة، مثل الأعداد الصحيحة ذات الحدود الصغيرة، وأن فرز العد يحقق O(n + k) عبر إحصاء التكرارات، والفرز الجذري يعالج الأرقام بإجمالي O(d × (n + k))، بينما يستفيد فرز الدلاء من التوزيع المنتظم ليحقق O(n) في المتوسط، وأن Timsort في Python هو الخيار العملي الافتراضي — فهو مستقر، وتعقيده O(n log n) في أسوأ الحالات، وO(n) في أفضلها، وأسرع من أي بديل مكتوب يدويًا عند التعامل مع البيانات الواقعية. في الدرس التالي ستتقنون البحث الثنائي الكلاسيكي.
الأسئلة الشائعة
هل درس «فرز بلا مقارنة ودالة sort() في Python» مجاني؟
نعم — نص درس «فرز بلا مقارنة ودالة sort() في Python» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة DSA Interview Prep، انتقل إلى CoddyKit PRO. تتضمن دورة DSA Interview Prep 4 دروس في المجموع.
ماذا ستتعلم في «فرز بلا مقارنة ودالة sort() في Python»؟
استكشف فرز العد وفرز الجذر للمصفوفات الصحيحة، وافهم كيفية عمل Timsort في Python من الداخل عند استدعاء sort المضمّن تتمرن على DSA Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.
هل أحتاج إلى خبرة سابقة لأبدأ DSA Interview Prep؟
لا تُشترط خبرة سابقة. DSA Interview Prep على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 4 من أصل 4.
كم من الوقت يستغرق درس «فرز بلا مقارنة ودالة sort() في Python»؟
معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.
هل يمكنني كتابة وتشغيل أكواد في درس DSA Interview Prep هذا؟
نعم. كل درس في DSA Interview Prep يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.
جميع الدروس في هذه الدورة
- فرز الفقاعات وفرز الإدراج
- فرز الدمج: قسّم وفرز وادمج
- الفرز السريع واختيار المحور
- فرز بلا مقارنة ودالة sort() في Python