0Pricing
Coding Interview Prep · درس

عدّ التكرارات والتجميع

استخدم Counter وdefaultdict لعدّ تكرارات المحارف، واجمع التباديل الخرسانية حسب مفتاح مرتب، واعثر على العناصر الأكثر تكرارًا top-k

عدّ التكرارات والتجميع درس مجاني في Coding Interview Prep على CoddyKit. هذا هو الدرس 3 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في Coding Interview Prep، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة Coding Interview Prep 4 دروس في المجموع.

عدّ التكرارات: النمط الأساسي

يُعدّ عدّ التكرارات من أكثر الأنماط تنوعًا في مقابلات البرمجة. ومن خلال إحصاء عدد مرات ظهور كل عنصر في قائمة أو سلسلة نصية، يمكنكم الإجابة عن أسئلة تتعلق بالتكرارات، وAnagram، والعناصر الأكثر شيوعًا، والترتيبات الصحيحة بزمن قدره O(n) — وهو أفضل بكثير من البديل القائم على الفرز والفحص بزمن قدره O(n log n).

تُعدّ Python's Counter وdefaultdict(int) الأداتين القياسيتين لهذا الغرض. تنشئ كل منهما خريطة تربط العنصر بعدد مرات ظهوره؛ وتدعم Counter أيضًا العمليات الحسابية وmost_common.

from collections import Counter

words = ['apple', 'banana', 'apple', 'cherry', 'banana', 'apple']
freq  = Counter(words)
print(freq)                  # Counter({'apple':3,'banana':2,'cherry':1})
print(freq['apple'])         # 3
print(freq['grape'])         # 0 (not KeyError)
print(freq.most_common(2))   # [('apple',3),('banana',2)]

التحقق من Anagram صالح (LeetCode 242)

LeetCode 242 «Anagram صالح»: حدّدوا ما إذا كانت سلسلتان نصيتان Anagram لبعضهما. تكون سلسلتان Anagram إذا كانتا تحتويان على التكرارات نفسها لكل حرف. قارنوا بين كائنَي Counter الخاصين بهما أو افرزوا السلسلتين كلتيهما. استخدام Counter يستغرق O(n)، بينما يستغرق الفرز O(n log n). يُعدّ أسلوب Counter أمثل، كما يعبّر مباشرةً عن التعريف.

from collections import Counter

def isAnagram(s, t):
    return Counter(s) == Counter(t)

# Alternative: manual frequency array for lowercase letters only
def isAnagram_arr(s, t):
    if len(s) != len(t):
        return False
    freq = [0] * 26
    for c in s: freq[ord(c) - ord('a')] += 1
    for c in t: freq[ord(c) - ord('a')] -= 1
    return all(f == 0 for f in freq)

print(isAnagram('anagram', 'nagaram'))  # True
print(isAnagram('rat', 'car'))          # False
print(isAnagram_arr('listen', 'silent'))  # True

تجميع Anagram (LeetCode 49)

LeetCode 49 «تجميع Anagram»: عند إعطائكم قائمة من السلاسل النصية، اجمعوا كل سلاسل Anagram معًا. الفكرة الأساسية هي أن سلاسل Anagram تمتلك التسلسل المرتب نفسه للأحرف. استخدموا defaultdict(list) بمفتاح هو tuple المرتب للسلسلة (إذ إن tuples قابلة للتجزئة). تتراكم كل مجموعة تحت المفتاح نفسه. التعقيد الزمني: O(n × L log L)، حيث تمثل L أقصى طول لسلسلة.

from collections import defaultdict

def groupAnagrams(strs):
    groups = defaultdict(list)
    for s in strs:
        key = tuple(sorted(s))   # hashable canonical form
        groups[key].append(s)
    return list(groups.values())

print(groupAnagrams(['eat','tea','tan','ate','nat','bat']))
# [['eat','tea','ate'], ['tan','nat'], ['bat']]

# Alternative key: tuple of 26 character counts (O(L) not O(L log L))
def groupAnagrams_v2(strs):
    groups = defaultdict(list)
    for s in strs:
        key = tuple(ord(c) - ord('a') for c in sorted(s))
        groups[tuple(Counter(s)[chr(ord('a')+i)] for i in range(26))].append(s)
    return list(groups.values())

العناصر K الأكثر تكرارًا (LeetCode 347)

LeetCode 347 «العناصر K الأكثر تكرارًا»: أعيدوا العناصر k الأكثر تكرارًا. الأسلوب المباشر يستغرق O(n log n): احسبوا التكرارات، ثم افرزوا العناصر تنازليًا حسب عدد مرات ظهورها، وخذوا أول k عناصر. أما الأسلوب الأمثل، الذي يستغرق O(n)، فيستخدم الفرز بالمجموعات (bucket sort): أنشئوا مجموعات مفهرسة حسب التكرار من 1 إلى n، وضعوا كل عنصر في المجموعة الموافقة لتكراره، ثم افحصوا المجموعات من أعلى تكرار إلى أدناه لجمع k عناصر.

from collections import Counter

def topKFrequent(nums, k):
    freq  = Counter(nums)
    # Bucket sort by frequency
    buckets = [[] for _ in range(len(nums) + 1)]
    for num, count in freq.items():
        buckets[count].append(num)
    result = []
    for i in range(len(buckets) - 1, -1, -1):
        result.extend(buckets[i])
        if len(result) >= k:
            return result[:k]
    return result

print(topKFrequent([1,1,1,2,2,3], 2))  # [1, 2]
print(topKFrequent([1], 1))             # [1]

فرز الأحرف حسب التكرار (LeetCode 451)

LeetCode 451 «فرز الأحرف حسب التكرار»: أعيدوا ترتيب سلسلة نصية بحيث تظهر الأحرف بترتيب تنازلي حسب تكرارها. احسبوا التكرارات، ثم افرزوا الأحرف حسب التكرار تنازليًا، وادمجوها. يُعدّ استخدام most_common أوضح أسلوب في Python. التعقيد الزمني: O(n log n) لفرز الأحرف الفريدة حسب تكرارها.

from collections import Counter

def frequencySort(s):
    freq = Counter(s)
    return ''.join(ch * count for ch, count in freq.most_common())

print(frequencySort('tree'))    # 'eetr' or 'eert'
print(frequencySort('cccaaa'))  # 'cccaaa' or 'aaaccc'
print(frequencySort('Aabb'))    # 'bbAa' or 'bbaA'

جدولة المهام (LeetCode 621)

LeetCode 621 «جدولة المهام»: عند إعطائكم مهام وفترة تبريد n، أوجدوا أقل وقت لازم لإنهاء جميع المهام. الفكرة المحورية هي أن المهمة الأكثر تكرارًا تحدد البنية. رتّبوا max_count نسخة من المهمة الأكثر تكرارًا مع وجود (n) فجوات بينها. أقل وقت إجمالي = max((max_count - 1) * (n + 1) + num_tasks_with_max_count, total_tasks). وإذا كان عدد المهام المختلفة كافيًا لملء الفجوات، فسيكون وقت الخمول 0.

from collections import Counter

def leastInterval(tasks, n):
    freq      = Counter(tasks)
    max_count = max(freq.values())
    # How many tasks share the max frequency
    num_max   = sum(1 for v in freq.values() if v == max_count)
    # Minimum slots needed based on most frequent task
    min_slots = (max_count - 1) * (n + 1) + num_max
    return max(min_slots, len(tasks))

print(leastInterval(['A','A','A','B','B','B'], 2))  # 8
print(leastInterval(['A','A','A','B','B','B'], 0))  # 6
print(leastInterval(['A','A','A','A','B','B','B','C','C','D'], 2))  # 10

التصويت بالأغلبية باستخدام Counter

LeetCode 169 «العنصر الغالب»: أوجدوا العنصر الذي يظهر أكثر من n/2 مرة. مع أن التصويت باستخدام Boyer-Moore هو الحل الأمثل بمساحة O(1)، فإن استخدام Counter.most_common(1) يحل المسألة مباشرةً بزمن O(n) ومساحة O(n). في المقابلات التي تشترط مساحة O(1)، قدّموا Boyer-Moore كحل لاحق؛ أما إذا كانت المساحة الإضافية مسموحة، فإن Counter أوضح.

from collections import Counter

def majorityElement_counter(nums):
    freq = Counter(nums)
    return freq.most_common(1)[0][0]

# Boyer-Moore O(1) space
def majorityElement_moore(nums):
    candidate, count = None, 0
    for num in nums:
        if count == 0:
            candidate = num
        count += (1 if num == candidate else -1)
    return candidate

nums = [2, 2, 1, 1, 2, 2, 2]
print(majorityElement_counter(nums))  # 2
print(majorityElement_moore(nums))    # 2

أول حرف غير متكرر

LeetCode 387 «أول حرف فريد في سلسلة نصية»: أوجدوا فهرس أول حرف يظهر مرة واحدة بالضبط. يتكون الحل من مرورين: يبني المرور الأول عدًّا للتكرارات، ثم يبحث المرور الثاني عن أول حرف يكون عدّه 1. التعقيد الزمني: O(n)، والمساحة: O(1)، لأن الأبجدية ثابتة وتتكون من 26 حرفًا.

from collections import Counter

def firstUniqChar(s):
    freq = Counter(s)
    for i, ch in enumerate(s):
        if freq[ch] == 1:
            return i
    return -1

print(firstUniqChar('leetcode'))   # 0 (l)
print(firstUniqChar('loveleetcode'))  # 2 (v)
print(firstUniqChar('aabb'))       # -1

مجموع المصفوفة الجزئية يساوي K (LeetCode 560)

LeetCode 560 «مجموع المصفوفة الجزئية يساوي K»: احسبوا عدد المصفوفات الجزئية التي يساوي مجموعها k. يستغرق الحل بالقوة الغاشمة O(n²). أما الحل بزمن O(n)، فيحافظ على مجموع بادئة متراكم وخريطة تكرارات لمجاميع البادئات التي ظهرت حتى الآن. بالنسبة إلى كل موضع i، يساوي عدد المصفوفات الجزئية المنتهية عند i والتي مجموعها k عدد مجاميع البادئات السابقة التي تساوي (current_prefix_sum - k). هيّئوا الخريطة بالقيمة {0: 1} لمعالجة المصفوفات الجزئية التي تبدأ من الفهرس 0.

from collections import defaultdict

def subarraySum(nums, k):
    freq         = defaultdict(int)
    freq[0]      = 1   # prefix sum of 0 seen once (empty prefix)
    prefix_sum   = 0
    count        = 0
    for num in nums:
        prefix_sum += num
        # How many earlier prefix sums allow a k-sum subarray ending here
        count      += freq[prefix_sum - k]
        freq[prefix_sum] += 1
    return count

print(subarraySum([1, 1, 1], 2))            # 2
print(subarraySum([1, 2, 3], 3))            # 2
print(subarraySum([1, -1, 1, -1, 1], 0))   # 4

العمليات الحسابية والتقاطع في Counter

تدعم Counter العمليات الحسابية: تدمج + القيم (بجمع التكرارات)، وتطرح - القيم (مع جعل القيم السالبة تساوي 0)، وتأخذ & الحد الأدنى (التقاطع)، وتأخذ | الحد الأقصى (الاتحاد). وتبسّط هذه العمليات مسائل مثل «العثور على الأحرف المشتركة في سلاسل نصية متعددة» أو «إزالة أقل عدد من الأحرف لجعل إحدى السلسلتين Anagram للأخرى».

from collections import Counter

A = Counter('abccdd')
B = Counter('ccdde')

print('Add:      ', dict(A + B))  # sum of counts
print('Subtract: ', dict(A - B))  # A - B, clipped at 0
print('Intersect:', dict(A & B))  # min of shared counts
print('Union:    ', dict(A | B))  # max counts

# Min steps to make s anagram of t (LeetCode 1347)
s, t = 'leetcode', 'practice'
diff = Counter(t) - Counter(s)
print('Chars to add:', sum(diff.values()))  # 5

ملخص: متى يُستخدم عدّ التكرارات

لجؤوا إلى عدّ التكرارات عندما تتضمن المسألة: التحقق مما إذا كانت سلسلتان نصيتان متكافئتين بعد إعادة الترتيب (Anagram)، أو العثور على العناصر الأكثر أو الأقل شيوعًا، أو التحقق من أن مجموعةً ما تحتوي على «المكوّنات» الصحيحة، أو تحويل مسألة تتعلق بمصفوفة جزئية أو سلسلة جزئية إلى مسألة مجموع بادئة مع خريطة. الفكرة الأساسية هي أن الترتيب داخل المجموعة لا يهم — المهم هو التكرارات فقط.

استخدموا دائمًا Counter للوضوح؛ وانتقلوا إلى dict عادي أو مصفوفة فقط عندما تحتاجون إلى تحكم أدق أو إلى مساحة صارمة قدرها O(1) مع أبجدية محدودة.

تحقق سريع

اختبروا مدى فهمكم لمفاهيم Data Structures & Algorithms — Coding Interview Prep التي تناولها هذا الدرس.

ملخص الدرس

تعلمتم في هذا الدرس أن: Counter يوفّر عدًّا للتكرارات بزمن O(n)، مع دعم most_common والعوامل الحسابية والوصول بقيمة افتراضية تساوي صفرًا، والتجميع حسب الشكل المعياري (tuple مرتب) يحل مسألة تجميع Anagram بتعقيد O(nL log L)، كما أن مجموع البادئة مع خريطة التكرارات يحوّل مسألة مجموع المصفوفة الجزئية الذي يساوي k من O(n²) إلى O(n). بعد ذلك، سنتناول مسألة أطول متتالية متصلة وتصميم ذاكرة LRU المؤقتة.

الأسئلة الشائعة

هل درس «عدّ التكرارات والتجميع» مجاني؟

نعم — نص درس «عدّ التكرارات والتجميع» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة Coding Interview Prep، انتقل إلى CoddyKit PRO. تتضمن دورة Coding Interview Prep 4 دروس في المجموع.

ماذا ستتعلم في «عدّ التكرارات والتجميع»؟

استخدم Counter وdefaultdict لعدّ تكرارات المحارف، واجمع التباديل الخرسانية حسب مفتاح مرتب، واعثر على العناصر الأكثر تكرارًا top-k تتمرن على Coding Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

هل أحتاج إلى خبرة سابقة لأبدأ Coding Interview Prep؟

لا تُشترط خبرة سابقة. Coding Interview Prep على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 3 من أصل 4.

كم من الوقت يستغرق درس «عدّ التكرارات والتجميع»؟

معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.

هل يمكنني كتابة وتشغيل أكواد في درس Coding Interview Prep هذا؟

نعم. كل درس في Coding Interview Prep يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.

جميع الدروس في هذه الدورة

  1. آليات دوال التجزئة ومعالجة التصادمات
  2. Two-Sum ومتغيراته العديدة
  3. عدّ التكرارات والتجميع
  4. أطول تسلسل متتالٍ وذاكرة LRU المؤقتة
← العودة إلى Coding Interview Prep