Interview Handbook by Sayed Elmahdy English

المحور 28 · Problem Solving

Data Structures و Algorithms

Big-O، و Arrays و Linked lists، و Hash tables، و Trees، و Graphs، و Sorting، و DP.

18 سؤالJunior · 6Mid · 9Senior · 310 اختيارات

المفاهيم

هنفهم إيه؟ هنتعلم نختار طريقة تمثيل البيانات وحل المسألة حسب شكلها، ونقدّر تكلفة الحل قبل ما نكتب كود. ده يساعدك تقارن مثلًا بين البحث في قائمة كل مرة وبين تجهيز lookup مرة واحدة.

قبل ما تبدأ: محتاج تعرف أساسيات المتغيرات والـ arrays والـ loops. الـ input هو الداتا اللي المسألة بتستقبلها؛ algorithm خطوات محددة لتحويلها لنتيجة؛ وBig-O وصف لكيفية نمو الوقت أو الذاكرة مع كبر input، مش عدد ثوانٍ. هنستخدم ، يعني شرط يفضل صحيح أثناء خطوات الخوارزمية، ونشرح كل افتراض قبل الاعتماد عليه.

المصطلحات اللي تحتها خط منقّط تقدر تضغط عليها لمعرفة معناها، وترجع تكمل من نفس مكانك.

#Big-O: النمو مش عدد الثوانيBig-O describes growth, not seconds

مثال خطوة بخطوة
  1. البحث عن اسم داخل قائمة غير مرتبة قد يفحص n عنصرًا في أسوأ حالة؛ مضاعفة عدد العناصر تضاعف تقريبًا عدد الفحوص. ده نمو O(n)، وليس وعدًا بزمن محدد.
  2. على قائمة مرتبة، البحث الثنائي يستبعد نصف النطاق كل خطوة؛ مع 1,024 عنصرًا يحتاج عدد خطوات يقارب عشرة، مع اختلاف بسيط حسب الخوارزمية وحالة الإنهاء.
  3. Big-O تصف نمو التكلفة لما n تكبر، ولا تقارن وحدها زمن حلين على بيانات صغيرة. اذكر هل تحسب وقتًا أم مساحة، وهل الكلام عن المتوسط أم أسوأ حالة.

لما orders تزيد عشر مرات، هل المعالجة هتزيد عشر مرات ولا مية؟ Big-O بيوصف حدّ نمو الوقت أو المساحة مع حجم الـ input، مع تجاهل الثوابت والحدود الأقل. لازم تقول بتتكلم عن worst case ولا expected ولا amortized؛ الرمز لوحده مش معناه worst case تلقائياً.

التعقيدمثالعند مليون عنصر تقريباً
O(1)array indexعدد خطوات ثابت بالنسبة للحجم
O(log n)binary searchحوالي 20 تقسيمة
O(n)مسح arrayمليون خطوة
O(n log n)merge sortحوالي 20 مليون وحدة شغل
O(n²)كل زوج عناصرفي رتبة تريليون
O(2ⁿ)تعداد subsetsغير عملي هنا
O(n!)تعداد permutationsغير عملي هنا
// n orders, m customers; customer IDs are unique.
var byId = customers.ToDictionary(c => c.Id); // (1) expected O(m)
foreach (var order in orders) // (2) expected O(n) overall
{
    if (byId.TryGetValue(order.CustomerId, out var customer))
        Console.WriteLine(customer.Name);
}
🧾 شرح الكود
  1. (1) بنستثمر O(m) memory ووقت متوقع لبناء lookup بدل مسح العملاء لكل order.
  2. (2) كل lookup متوقع O(1) مع keys صغيرة وhash مناسب؛ الإجمالي O(n + m)، مش O(n) إلا لو وضحت العلاقة بينهم.

الخطوات المتتابعة بتتجمع، والمتداخلة بتتحسب حسب عدد تكرارها؛ مش أي loop جوه loop يبقى O(n²). List.Add ممكن O(n) وقت resize، لكنه O(1) amortized على سلسلة إضافات. Amortized تحليل لسلسلة عمليات، مش افتراض input عشوائي. قيس الأداء الفعلي كمان؛ cache locality وتكلفة والثوابت ممكن يفرقوا.

#Arrays و List و LinkedListArrays, List and LinkedList

Array صف كراسي مرقمة؛ تروح لرقم معين مباشرة. Linked list سلسلة أوراق، كل ورقة بتقول مكان التالية؛ لازم تمشي علشان توصل لموضع معين.

Array حجمها ثابت، وList<T> بتستخدم array بتكبر عند الحاجة. العناصر متجاورة، لكن لو T reference type، المتجاور هو references مش بالضرورة الـ objects نفسها.

العمليةArray أو ListLinkedList
indexO(1)O(n)
بحث بقيمةO(n)؛ أو O(log n) لو مرتبة وbinary searchO(n)
إضافة آخر ListO(1) amortizedO(1) مع tail
إدخال أو حذف في النصO(n) shiftingO(1) لو معاك node مناسبة
التكلفة العمليةlocality جيدة وقليل node allocations وreferences وcache misses
var order = new LinkedList<int>(); // .NET LinkedList is doubly linked.
var node = order.AddLast(42); // (1)
order.AddBefore(node, 17); // (2)
order.Remove(node); // (3)
🧾 شرح الكود
  1. (1) احتفظنا بالـ node، مش القيمة بس.
  2. (2) الإضافة قبل node معروفة O(1).
  3. (3) حذف نفس node كمان O(1)؛ البحث عنها بالقيمة كان هيبقى O(n).

ابدأ غالباً بـ List. LinkedList مفيدة لو بتوصل للـ nodes مباشرة وبتحركها كتير، زي Dictionary مع doubly linked list في .

#Stack و Queue و PriorityQueueStack, Queue and PriorityQueue

اختار ترتيب الخدمة قبل الأداة. Stack هي زي كومة أطباق: آخر داخل أول خارج. Queue هي زي طابور طلبات. PriorityQueue بتختار حسب priority مش وقت الوصول.

var undo = new Stack<string>();
undo.Push("edit");
string action = undo.Pop(); // (1)

var waiting = new Queue<int>();
waiting.Enqueue(42);
int next = waiting.Dequeue(); // (2)

var jobs = new PriorityQueue<string, int>(); // .NET 6+
jobs.Enqueue("routine", 5);
jobs.Enqueue("urgent", 1);
string first = jobs.Dequeue(); // (3) urgent
🧾 شرح الكود
  1. (1) Pop بيشيل آخر عنصر؛ Peek بيشوفه بس. push O(1) amortized وpop O(1). مفيد للـ undo والأقواس و.
  2. (2) Dequeue بيشيل أول عنصر؛ enqueue O(1) amortized وdequeue O(1). مناسب لـ والـ buffers.
  3. (3) أصغر priority الأول، والإضافة والحذف O(log n)، وPeek O(1). الترتيب بين priorities المتساوية مش مضمون.

PriorityQueue في مبنية على array-backed quaternary min-heap؛ فكرة heap العامة مش شرط binary. Collections دي مش durable message ولا thread-safe queues تلقائياً. للـ async producer-consumer قيّم Channel.

#Hash tables: الـ hash مش إثبات مساواةHash tables: a hash is not equality

علشان تدور على product بسرعة، hash بيحدد bucket وبعده equality بتحدد الـ key الحقيقي. Collision طبيعي: keys مختلفة ممكن تقع في نفس bucket أو يكون ليها نفس hash. Lookup متوقع O(1) لما hashing وتوزيع الداتا مناسبين؛ worst case O(n)، وتكلفة hash نفسها ممكن تعتمد على طول key.

  1. احسب hash بالـ comparer.
  2. حوّله لرقم bucket مناسب؛ التنفيذ الحقيقي بيتعامل مع العلامة وحدود الأرقام.
  3. افحص candidates بالـ hash والـ equality.
  4. لما الـ table تحتاج تكبر، اعمل resize وإعادة توزيع.

Chaining بيربط العناصر المتصادمة؛ Dictionary في بيستخدم entries array وروابط indexes. Open addressing بيدوّر على slot تانية باستخدام probing. Load factor هو عدد العناصر بالنسبة للخانات؛ الزيادة ممكن تزود التصادم وتستدعي resize.

❌ hash ثابت لكل key
public override int GetHashCode() => 1;
✅ نفس الحقول المستخدمة في Equals
public override int GetHashCode() => HashCode.Combine(StoreId, Sku);

المثال methods داخل key type عنده Equals متوافق. الأسهل value key زي record ProductKey(int StoreId, string Sku). Equal keys لازم نفس hash، لكن نفس hash مش معناه equal. ماتعدّلش حقول key المؤثرة في equality بعد الإدخال. ماتحفظش GetHashCode كمعرّف دائم أو partition بين processes؛ ممكن يختلف بين التشغيلات. للـ string comparison اختار comparer مناسب للـ contract، مش culture افتراضية بالغلط.

#Trees و BST و Heap و Trie و B-treeTrees, BSTs, heaps, tries and B-trees

Tree مناسبة لhierarchy، لكن نوعها بيحدد العملية السريعة. بتحافظ على الأصغر شمال والأكبر يمين؛ البحث O(h)، يعني O(log n) لو balanced، وO(n) لو بقت سلسلة. AVL وRed-black بيحافظوا على التوازن؛ SortedDictionary وSortedSet يستخدموا red-black trees.

public sealed class TreeNode(int value)
{
    public int Value = value;
    public TreeNode? Left, Right;
}
bool Contains(TreeNode? node, int target)
{
    while (node is not null)
    {
        if (node.Value == target) return true;
        node = target < node.Value ? node.Left : node.Right; // (1)
    }
    return false;
}
int MaxDepth(TreeNode? node) => node is null ? 0
    : 1 + Math.Max(MaxDepth(node.Left), MaxDepth(node.Right)); // (2)
🧾 شرح الكود
  1. (1) بنستبعد فرع بناء على BST ؛ مش مضمون نستبعد نص العناصر إلا لو متوازنة.
  2. (2) أكبر عمق O(n) وقت وO(h) call stack؛ شجرة عميقة ممكن تعمل stack overflow.
النوع أو المرورالفكرة والاستخدام
In-order: left, node, rightبيطلع BST مترتبة
Pre-order: node, left, rightنسخ أو مع null markers لاسترجاع الشكل
Post-order: left, right, nodeتقييم تعبير أو معالجة الأطفال قبل الأب
Level-order بالـ queue
Heapmin أو max في root؛ binary heap كاملة وتتخزن array؛ مش كل العناصر مرتبة
Trieمسار للحروف؛ prefix lookup بطول الكلمة O(L) مع child lookup ثابت
B-tree وB+ treebalanced وfan-out كبير؛ مناسبة pages وتقليل disk ، وكثير من indexes مبني عليها

Heap ممتازة لاستخراج الأصغر باستمرار، لكن البحث عن قيمة عشوائية O(n). Trie ممكن تستهلك memory كبيرة؛ sorted array مع prefix range أحياناً أبسط. B-tree مش binary tree، ومش كل database index من النوع ده.

#Graphs و BFS و DFSGraphs, BFS and DFS

مثال خطوة بخطوة
  1. عندك روابط A إلى B وC، وB إلى D. من A، يزور الجيران B وC قبل مستوى D، باستخدام Queue تحفظ من ينتظر الزيارة.
  2. قد يمشي A ثم B ثم D قبل الرجوع إلى C، حسب ترتيب الجيران. مجموعة visited تمنع الدوران بلا نهاية لو فيه مسار يرجع لعقدة سابقة.
  3. BFS يعطي أقل عدد حواف من البداية في graph غير موزون. لو الطرق لها أوزان مختلفة، عدد الحواف الأقل لا يضمن أقل تكلفة، فتحتاج خوارزمية تناسب الأوزان.

Graph بتمثل vertices وروابط edges: مستخدمين وعلاقات، أو dependencies، أو شبكة طرق. حدد directed ولا undirected، وweighted ولا unweighted. Adjacency list بتاخد O(V + E) space؛ matrix بتاخد O(V²) لكنها بتفحص وجود edge في O(1).

var graph = new Dictionary<int, List<int>>
{
    [1] = [2, 3], [2] = [4], [3] = [4], [4] = []
};
List<int> Bfs(int start)
{
    var seen = new HashSet<int> { start }; // (1)
    var queue = new Queue<int>([start]);
    var order = new List<int>();
    while (queue.Count > 0)
    {
        int node = queue.Dequeue();
        order.Add(node);
        foreach (int next in graph[node])
            if (seen.Add(next)) queue.Enqueue(next); // (2)
    }
    return order;
}
void Dfs(int node, HashSet<int> seen, List<int> order)
{
    if (!seen.Add(node)) return; // (3)
    order.Add(node);
    foreach (int next in graph[node]) Dfs(next, seen, order);
}
🧾 شرح الكود
  1. (1) بنعلّم start وقت إدخاله. المثال بيفترض كل vertex موجودة كـ key حتى لو مالهاش neighbors.
  2. (2) Mark-on-enqueue يمنع تكرار node في الـ queue. BFS يزور 1 و2 و3 و4 ويلاقي أقل عدد edges في unweighted graph.
  3. (3) DFS يعلّم قبل النزول، فيزور 1 و2 و4 و3 حسب ترتيب neighbors. لو عايز كل graph، ابدأ traversal لكل vertex لسه ماتزارتش.
BFSDFS
الأداةQueueRecursion أو stack
مناسب لـأقرب nodes وunweighted shortest pathsComponents وbacktracking وcycle analysis
المساحةO(V) للـ frontier والـ visitedO(V) للـ visited وstack؛ العمق ممكن يكبر
الوقتO(V + E)O(V + E)

visited لوحده مش كفاية لكشف cycle في directed graph: محتاج recursion-stack state أو topological sort. في undirected graph تجاهل edge الرجوع للأب، مع مراعاة multi-edges لو موجودة.

#Dijkstra و topological sortDijkstra and topological ordering

في الرسم البياني، vertex عقدة مثل مدينة وedge طريق بينها وبين مدينة أخرى. (بحث بعرض الطبقات) يناسب الطرق المتساوية؛ (بحث يتعمق في مسار قبل الرجوع) يحل أسئلة وصول وترابط. هنا نحتاج وزنًا لكل edge مثل تكلفة الطريق. Dijkstra يستخدم relaxation: نحسب تكلفة الوصول عبر الطريق الجديد، ونحدّث أفضل تكلفة محفوظة لو كانت أقل. topological sort يستخدم indegree، أي عدد المتطلبات الداخلة للعقدة، ويبدأ بالعقد التي لا تملك متطلبات.

مثال: لو عايز أقل تكلفة توصيل بين مدن، BFS يفترض إن كل طريق تكلفته واحدة. Dijkstra يحفظ أفضل مسافة معروفة لكل مدينة، ويختار الأقل من طابور أولوية؛ لو لقى طريق أرخص يحدّث المسافة ويضيفها للطابور. ده صحيح مع أوزان غير سالبة فقط، لأن طريقًا جديدًا ماينفعش يخلي مدينة اتثبتت سابقًا أرخص. الـ min-heap طابور يخرج أصغر أولوية أولًا.

Topological sort يحل مسألة مختلفة: ترتيب مهام بحيث كل prerequisite (مهمة لازم تسبق غيرها) تيجي قبل اللي تعتمد عليها. ده ممكن فقط في ، أي directed graph مفيهوش cycle. الكود التالي يفترض معرفات vertices صحيحة، وأوزان صحيحة غير سالبة، ويمثل الرسم بقوائم جيران.

// Vertices: 0..n-1. Weights are non-negative ints.
long[] Dijkstra(List<(int To, int Weight)>[] graph, int start)
{
    var distance = new long[graph.Length];
    Array.Fill(distance, long.MaxValue);
    distance[start] = 0;
    var heap = new PriorityQueue<int, long>();
    heap.Enqueue(start, 0);
    while (heap.TryDequeue(out int node, out long cost))
    {
        if (cost != distance[node]) continue; // (1)
        foreach (var (to, weight) in graph[node])
        {
            if (weight < 0) throw new ArgumentException("Negative weight");
            long candidate = checked(cost + weight);
            if (candidate >= distance[to]) continue;
            distance[to] = candidate; // (2)
            heap.Enqueue(to, candidate);
        }
    }
    return distance;
}

int[] TopologicalSort(List<int>[] graph)
{
    var degree = new int[graph.Length];
    foreach (var edges in graph)
        foreach (int to in edges) degree[to]++;
    var ready = new Queue<int>();
    for (int i = 0; i < degree.Length; i++)
        if (degree[i] == 0) ready.Enqueue(i); // (3)
    var result = new List<int>();
    while (ready.TryDequeue(out int node))
    {
        result.Add(node);
        foreach (int to in graph[node])
            if (--degree[to] == 0) ready.Enqueue(to);
    }
    if (result.Count != graph.Length) // (4)
        throw new InvalidOperationException("Directed cycle");
    return result.ToArray();
}
🧾 شرح الكود
  1. (1) بنضيف heap entries جديدة بدل decrease-key؛ القديمة بتتجاهل لما تطلع. unreachable تفضل long.MaxValue.
  2. (2) relaxation بتسجل المسار الأرخص. مع lazy duplicates الوقت O((V + E) log(V + E)) كحد عام، والـ heap ممكن O(E)، مش O(V) بالضرورة.
  3. (3) Kahn بيبدأ بالعناصر اللي مالهاش prerequisites، ويقلل indegree بعد إزالة كل عنصر.
  4. (4) عدم معالجة الكل معناه cycle؛ الوقت O(V + E)، والمساحة الإضافية O(V) غير graph.

افترض valid vertex IDs واختبر self-loops وdisconnected graphs. للأوزان السالبة قيّم Bellman-Ford بدل Dijkstra. ترتيب topological مش لازم يبقى وحيد.

#Binary search و lower boundBinary search and lower bound

لما الداتا مرتبة أو شرطك monotonic، تقدر تستبعد نص search space كل مرة. زي البحث في قاموس. مش لازم تكون بتدور على قيمة موجودة؛ ممكن تدور على أول قيمة تحقق شرط، زي أقل capacity تخلص orders قبل موعد معين.

int BinarySearch(int[] sorted, int target)
{
    int lo = 0, hi = sorted.Length - 1;
    while (lo <= hi)
    {
        int mid = lo + (hi - lo) / 2; // (1)
        if (sorted[mid] == target) return mid;
        if (sorted[mid] < target) lo = mid + 1; // (2)
        else hi = mid - 1;
    }
    return -1;
}
int LowerBound(int[] sorted, int target)
{
    int lo = 0, hi = sorted.Length; // (3) half-open interval
    while (lo < hi)
    {
        int mid = lo + (hi - lo) / 2;
        if (sorted[mid] < target) lo = mid + 1;
        else hi = mid;
    }
    return lo;
}
🧾 شرح الكود
  1. (1) حساب mid بالشكل ده يتجنب overflow بتاع جمع indexين موجبين.
  2. (2) بنعدي mid علشان المساحة تصغر ومندخلش loop بلا نهاية.
  3. (3) LowerBound بيرجع أول index قيمته أكبر من أو تساوي target، أو Length لو مفيش؛ ينفع كمان مع array فاضية.

O(log n) وقت وO(1) مساحة على array. Array.BinarySearch وList.BinarySearch موجودين، والنتيجة السالبة فيها bitwise complement لموضع الإدخال. لو بتعمل sort علشان search واحدة بس، تكلفة O(n log n) ممكن تكون أسوأ من scan O(n).

#Sorting: quick و merge والمقارنةSorting: quicksort, mergesort and trade-offs

Sorting بيخلي البحث والتجميع وinterval processing أسهل، لكن اختار حسب stability والـ memory والـ worst case. Quicksort بيقسم حوالين pivot؛ mergesort بيرتب نصين ويدمجهم.

void QuickSort(int[] values, int lo, int hi)
{
    if (lo >= hi) return;
    int pivot = Partition(values, lo, hi); // (1)
    QuickSort(values, lo, pivot - 1);
    QuickSort(values, pivot + 1, hi);
}
int Partition(int[] values, int lo, int hi)
{
    int pivot = values[hi], write = lo;
    for (int read = lo; read < hi; read++)
        if (values[read] < pivot)
        {
            (values[write], values[read]) = (values[read], values[write]);
            write++;
        }
    (values[write], values[hi]) = (values[hi], values[write]);
    return write;
}

int[] MergeSort(int[] values)
{
    if (values.Length <= 1) return values;
    int mid = values.Length / 2;
    return Merge(MergeSort(values[..mid]), MergeSort(values[mid..])); // (2)
}
int[] Merge(int[] left, int[] right)
{
    var result = new int[left.Length + right.Length];
    int i = 0, j = 0, k = 0;
    while (i < left.Length && j < right.Length)
        result[k++] = left[i] <= right[j] ? left[i++] : right[j++]; // (3)
    while (i < left.Length) result[k++] = left[i++];
    while (j < right.Length) result[k++] = right[j++];
    return result;
}
🧾 شرح الكود
  1. (1) Lomuto partition بيحط pivot في مكانها النهائي. استدعِ QuickSort(a, 0, a.Length - 1). المتوسط O(n log n)، لكن last pivot مع sorted أو equal input ممكن O(n²) وO(n) stack.
  2. (2) بنعمل copies للنصين للتوضيح. Mergesort O(n log n) وقت وO(n) peak auxiliary space مع O(log n) عمق؛ إجمالي في النسخة دي O(n log n).
  3. (3) اختيار الشمال عند التعادل يحافظ على stability. عدد المقارنات في merge خطي، وبننسخ باقي العناصر.
AlgorithmBestAverageWorstAuxiliary spaceStable
Bubble مع early exit وInsertionO(n)O(n²)O(n²)O(1)أيوه بالتنفيذ المعتاد
SelectionO(n²)O(n²)O(n²)O(1)لأ
QuickO(n log n)O(n log n)O(n²)O(log n) متوسط stack؛ O(n) أسوألأ
Merge على arrayO(n log n)O(n log n)O(n log n)O(n)أيوه
HeapO(n log n)O(n log n)O(n log n)O(1)لأ
Counting مستقرةO(n + k)O(n + k)O(n + k)O(n + k)أيوه
Radix مع d passesO(d(n + k))O(d(n + k))O(d(n + k))O(n + k)لو pass مستقرة

k حجم key range في counting أو radix base في radix، وd عدد digits. Stability معناها الحفاظ على ترتيب العناصر المتساوية في key. Random pivot يقلل احتمال worst case، وthree-way partition يفيد duplicates؛ ولا واحد ضمان worst-case O(n log n). Array.Sort يستخدم introsort ومش stable؛ OrderBy stable. في التطبيق استخدم library sort غالباً. للبيانات الأكبر من استخدم external merge sort على runs.

#Recursion و divide and conquer و DP و greedyRecursion, divide and conquer, DP and greedy

Recursion وسيلة للتعبير عن مشكلة أصغر، ولازم base case وتقدم نحوه. Divide and conquer بيقسّم لأجزاء مستقلة ويجمعها. بيستفيد من subproblems متكررة، وoptimal substructure في مسائل التحسين؛ counting problems كمان تستخدم DP. Greedy بيختار محلياً من غير رجوع، ومحتاج proof يناسب المشكلة.

// Inputs for these Fibonacci examples: 0..92, fitting in long.
long FibNaive(int n) => n < 2 ? n : FibNaive(n - 1) + FibNaive(n - 2);
long FibMemo(int n, Dictionary<int, long> memo)
{
    if (n < 2) return n;
    if (memo.TryGetValue(n, out long value)) return value; // (1)
    return memo[n] = FibMemo(n - 1, memo) + FibMemo(n - 2, memo);
}
long Fib(int n)
{
    if (n < 2) return n;
    long previous = 0, current = 1;
    for (int i = 2; i <= n; i++)
        (previous, current) = (current, previous + current); // (2)
    return current;
}
// Positive coin values, non-negative amount, feasible array size.
int CoinChange(int[] coins, int amount)
{
    var best = new int[amount + 1];
    Array.Fill(best, int.MaxValue);
    best[0] = 0;
    for (int value = 1; value <= amount; value++)
        foreach (int coin in coins)
            if (coin <= value && best[value - coin] != int.MaxValue)
                best[value] = Math.Min(best[value], best[value - coin] + 1); // (3)
    return best[amount] == int.MaxValue ? -1 : best[amount];
}
🧾 شرح الكود
  1. (1) memoization بتمنع إعادة نفس Fibonacci state: من exponential recursion لوقت O(n) وmemory O(n) شاملة stack.
  2. (2) tabulation تحتفظ بآخر حالتين بس: O(n) وقت وO(1) مساحة. bounded numeric type جزء من contract.
  3. (3) best[value] أقل عدد عملات، وبنبنيها من قيمة أصغر. الوقت O(amount × coins.Length)، والمساحة O(amount). ده pseudo-polynomial بالنسبة لعدد bits اللي بيمثل amount.
الأسلوباختاره لماأمثلة
Divide and conquerالأجزاء مستقلة أو بنختار جزء واحدMerge sort، وquick sort، وbinary search
DPنفس states بتتكرر وتقدر تصيغ recurrenceCoin change، وknapsack، و، وclimbing stairs
Greedyاختيار محلي آمن يمكن إثباتهActivity selection بترتيب النهاية

عملات 1 و3 و4 ومبلغ 6: greedy يختار 4 و1 و1، لكن الأفضل 3 و3. ماتستنتجش greedy من كام مثال. Deep recursion ممكن تعمل stack overflow؛ explicit stack أو bottom-up أفضل لما العمق كبير.

#خريطة patterns: ابدأ من شكل السؤالA problem-solving pattern map

Pattern بيديك hypothesis مش إجابة جاهزة. اكتب أو recurrence، واختبر constraints قبل اختيار الحل.

Patternعلامات السؤالمثال
Hash mapاتشاف قبل كده؟ وعدّ أو lookupTwo Sum، وanagrams، وduplicates
Two pointersترتيب أو مقارنة من الطرفينSorted Two Sum، وpalindrome، وcontainer
Sliding windowجزء متصل وشرط نقدر نصلحه بتحريك الحدودLongest unique substring، وfixed-window sum
Prefix sumRange sums أو subarray sums مع negativesSubarray Sum Equals K
Stackتداخل أو أقرب أكبر عنصرParentheses، وdaily temperatures
Binary searchSorted data أو monotonic feasibilityRotated array، وأقل capacity مناسبة
وGrid أو graph أو treeIslands، وlevel order، وcomponents
HeapTop-K أو minimum متكررKth largest، وmerge K sorted streams
Choices وstates متكررة للتحسين أو العدHouse Robber، وstairs، وcoin change
Backtrackingكل الحلول أو الاحتمالاتSubsets، وpermutations، وN-Queens
Sortingتجميع أو intervalsMerge intervals، وmeeting rooms
Sliding window للمجاميع مش حل تلقائي مع أرقام سالبة؛ الشرط ممكن يفقد monotonicity. وbacktracking لعرض كل النتائج لازم يدفع تكلفة حجم الـ output.

أسئلة الانترفيو

راجع فهمك

ليه الاختيار ده مناسب؟ إمتى ممكن ماينفعش؟ وإيه اللي هيتغير لو الحمل زاد أو ظهر فشل؟

28.1What does Big-O notation tell you?Junior

بيوصف حد نمو الوقت أو المساحة، مش زمن ثابت. قول n بيمثل إيه، وحدد worst أو expected أو amortized، واحسب كل inputs مستقلة.

الإجابة النموذجية (English)

I use Big-O to describe how time or space grows with input size, ignoring constant factors and smaller terms. I define what each input size represents and state whether I mean worst-case, expected or amortized cost. A linear scan is O(n), binary search is O(log n), and comparing every pair is O(n squared). It does not predict seconds, so I still consider , cache behavior and and measure performance when practical differences matter.

28.2What is the difference between an array and a linked list?Junior

Array index مباشر وlocality أحسن؛ LinkedList محتاجة مشي للوصول لكن تعديل pointers سريع لو node معاك. البحث عن node مش O(1).

الإجابة النموذجية (English)

I choose arrays or List for indexed access and locality. Their elements are contiguous, giving O(1) indexing, while middle insertion or removal generally shifts O(n) elements. A linked list stores separate nodes, so positional access and search are O(n), but insertion or removal is O(1) when I already have the relevant node. Nodes add memory and overhead. I usually start with List and use a linked list when direct node moves matter, such as an cache.

28.3How do stacks, queues and priority queues differ?Junior

و وpriority. Stack للـ undo و، وQueue للـ ، وheap للـ top-K والـ scheduling؛ priority ties مش FIFO تلقائياً.

الإجابة النموذجية (English)

I use a stack for last-in-first-out behavior, such as undo, matching brackets or depth-first traversal. A queue provides first-in-first-out behavior, useful for buffering and breadth-first traversal. A priority queue removes the item with the best priority instead of the earliest arrival. In , lower priorities come out first, with logarithmic insertion and removal. I do not assume equal-priority items retain arrival order, and none of these ordinary collections provides durable distributed delivery.

28.4How does a hash table handle collisions?Mid

hash يختار bucket وEquals يفرق keys. Chaining أو probing، وresize عند الحاجة. Expected O(1) مش ضمان worst case.

الإجابة النموذجية (English)

I hash the key to choose a bucket, then use equality to find the actual entry. Different keys can collide, so chaining links entries within a bucket, while open addressing probes alternative slots. Dictionary uses array-based chaining. Growth can trigger resizing and redistribution. With suitable hashing, operations are expected O(1), but severe collisions can produce O(n) behavior. I keep equality and hashing consistent and avoid mutating fields that determine a stored key's identity.

28.5What is amortized complexity?Mid

تكلفة سلسلة عمليات تتوزع على كل العمليات. List.Add لما يكبر ينسخ كتير، لكن النمو الهندسي يخلي مجموع النسخ خطي.

الإجابة النموذجية (English)

I use amortized analysis when occasional expensive operations are spread across a sequence. Adding to a List is usually constant time, but growing its backing array copies existing elements. With geometric growth, the total copying across many additions is linear, so each addition is O(1) amortized even though an individual one can be O(n). This is different from average-case analysis over random inputs; it describes the cost of the operation sequence.

28.6How does binary search work?Junior

نطاق مرتب أو predicate monotonic، ومقارنة بالنص، وتقليص النطاق. راجع فاضي وعنصر واحد وهدف مش موجود وduplicates.

الإجابة النموذجية (English)

I maintain a search interval over sorted data and compare the target with its midpoint. The ordering lets me discard the half that cannot contain the target, giving O(log n) time and O(1) auxiliary space iteratively. I compute the midpoint from the interval distance to avoid overflow and move bounds past the midpoint so the search terminates. For duplicates, I clarify whether any match or the first match is required and use a bound-search variant when necessary.

28.7How do BFS and DFS differ?Junior

طبقات بـ queue، و فرع عميق بـ stack. الاتنين O(V + E) على adjacency list؛ BFS shortest path بعدد edges من غير weights.

الإجابة النموذجية (English)

I use to explore vertices by distance in edges, maintaining a queue and marking vertices when enqueued. That gives shortest paths in an unweighted graph. follows one branch before backtracking, using recursion or an explicit stack, and fits structural exploration and backtracking. With adjacency lists, both take O(V + E) over the visited graph. I track visited state to avoid repetition and account for disconnected components if the task requires visiting every vertex.

28.8When is Dijkstra appropriate?Mid

أقصر مسارات بأوزان non-negative. لو كل weights متساوية؛ مع negative weights محتاج خوارزمية تانية. stale heap entries لازم تتتجاهل.

الإجابة النموذجية (English)

I use Dijkstra for shortest paths when edge weights are non-negative. A priority queue selects the smallest tentative distance, and relaxation improves distances to neighbors. With 's priority queue, I can insert a new entry for an improved distance and skip stale entries later instead of implementing decrease-key. That increases heap storage, so I describe complexity for that implementation. For unweighted graphs is simpler; negative edges require another algorithm, such as Bellman-Ford.

28.9What is topological sorting?Mid

ترتيب dependencies في . Kahn يحسب indegrees ويطلع zero-indegree؛ لو فضل vertices يبقى directed cycle.

الإجابة النموذجية (English)

I use topological sorting to order a directed graph so every prerequisite appears before its dependents. A valid order exists only when the graph is acyclic. Kahn's algorithm counts indegrees, processes zero-indegree vertices and reduces their neighbors' counts. If fewer than all vertices are processed, there is a directed cycle. It takes O(V + E) time with adjacency lists, and several valid orders may exist. Typical uses include dependency scheduling and checking whether all courses can be completed.

28.10What is a BST, and why does balancing matter?Mid

الـ على كل subtree، مش الأطفال المباشرين بس. الوقت O(height)، وsorted insertion ممكن تخلي العادية سلسلة.

الإجابة النموذجية (English)

I define a through its ordering : all values in the left subtree are smaller and all values in the right subtree are larger, with an explicit duplicate policy. Search, insertion and deletion depend on height. A balanced tree has logarithmic height, but an ordinary BST built from sorted input can become a chain with linear operations. Self-balancing structures maintain logarithmic height. In-order traversal returns values in sorted order, which differs from a heap's weaker parent-child ordering.

28.11How do a heap, trie and B-tree serve different needs?Mid

Heap لاستخراج min أوmax، وtrie للـ prefix، وB-tree لفان آوت كبير وpages. مفيش structure أفضل في كل حاجة.

الإجابة النموذجية (English)

I choose a heap for repeatedly accessing an extreme value, such as the next priority or a top-K boundary. A trie follows character paths and supports prefix lookup based on key length, at potentially high memory cost. A B-tree keeps many keys per node and stays balanced, which reduces page accesses and suits storage indexes. These structures optimize different operations. I would not use a heap for arbitrary fast lookup or assume every database index is a B-tree.

28.12What causes quicksort's worst case?Mid

Partitions غير متوازنة باستمرار. last pivot مع sorted أو equal input في الكود المعروض يعمل O(n²)، وstack O(n).

الإجابة النموذجية (English)

I get quicksort's worst case when partitioning repeatedly leaves almost every element on one side. A first- or last-element pivot on sorted input is a common example, and a simple two-way partition can also handle duplicates badly. Time becomes O(n squared) and recursive depth can become linear. Random pivots reduce the chance of adversarial partitions, while three-way partitioning helps repeated values. A hybrid such as introsort uses a fallback to guarantee better worst-case behavior.

28.13When would you prefer mergesort over quicksort?Mid

لو stability أو worst-case guarantee مهمين، أو external sorting. المقابل buffer و؛ وضّح stack حتى لو partition in-place.

الإجابة النموذجية (English)

I prefer mergesort when stable ordering or predictable O(n log n) time is important, or when merging sorted runs from data larger than memory. Its usual array implementation needs O(n) extra storage. Quicksort often performs well with in-place partitioning and good locality, but simple implementations have a quadratic worst case and are unstable. In application code I normally use a library sort and select it by required semantics rather than implement either algorithm myself.

28.14What are recursion's main risks?Junior

مفيش base case أو مفيش تقدم، أو عمق كبير، أو نفس subproblem بيتحسب كتير. memo يعالج التكرار لكن مش عمق الـ stack لوحده.

الإجابة النموذجية (English)

I check that recursion has a correct base case and makes progress toward it. Even correct recursion can overflow the stack on deep trees or long paths, because each call adds a frame. Repeated overlapping calls can also create exponential work, as in naive Fibonacci. Memoization removes repeated computation but does not necessarily remove deep call stacks. When depth is unbounded, I consider iteration with an explicit stack or a bottom-up formulation instead.

28.15How do divide and conquer, DP and greedy differ?Mid

استقلال الأجزاء مقابل states متكررة مقابل local choice محتاج proof. مش معناها recursion بس؛ tabulation ممكن تقلل memory.

الإجابة النموذجية (English)

I use divide and conquer when smaller subproblems can be solved independently and combined, as in mergesort. Dynamic programming stores results of repeated states, using memoization or tabulation; optimization problems also need an appropriate optimal-substructure recurrence. Greedy commits to a local choice without reconsidering it, which needs a correctness argument. Coin change shows the danger: with coins one, three and four, greedy uses three coins for six, while two threes are better.

28.16How would you design an O(1) LRU cache?Senior

Dictionary من key للـ node، وdoubly linked list للـ recency. Expected O(1) للـ hashing، وحدد capacity وthread safety.

الإجابة النموذجية (English)

I combine a dictionary mapping keys to linked-list nodes with a doubly linked list ordered by recency. A successful get moves its node to the front. Put updates or inserts at the front and evicts the tail when capacity is exceeded, removing its dictionary entry too. That gives expected O(1) operations with suitable hashing. I define capacity and miss behavior, and coordinate both structures under one synchronization policy if shared across threads. Exact is a specific requirement, not every production cache's eviction policy.

28.17Why can an O(n) algorithm still be unsuitable for production?Senior

input size نفسه أو memory أو أو constants ممكن تكون كبيرة، ومتوسط hash ممكن يسوء. complexity خطوة أولى مش تقييم كامل.

الإجابة النموذجية (English)

I treat asymptotic complexity as one part of suitability. A linear algorithm over billions of records may exceed memory, require expensive network calls or miss a target. , key length, cache locality, concurrency and data distribution also matter. I check resource limits and workload, then benchmark representative cases and failure paths. Streaming, batching, indexing or changing the system boundary may be more valuable than a small improvement inside the loop.

28.18Why can prefix sums work where a sum-based sliding window fails?Senior

negative values بتكسر افتراض إن توسيع النافذة يزوّد المجموع وتقليصها يقلله. Prefix difference مع frequency map يحسب كل البدايات الممكنة.

الإجابة النموذجية (English)

I first check the monotonicity needed to move a sliding window safely. With negative values, expanding the window can decrease its sum and shrinking it can increase it, so the usual positive-array rule breaks. Prefix sums use an algebraic identity instead: a subarray totals k when the current prefix minus an earlier prefix equals k. A frequency map of earlier prefixes counts valid starts in expected linear time, including repeated prefixes and subarrays starting at index zero.

اختبر نفسك

إجابات الاختبار وعلامات إكمال المحاور بتتحفظ على المتصفح ده، ومش بتتنقل لأجهزة تانية.

النتيجة: 0 / 0

Build a dictionary of m customers, then look up n orders. Expected total time?

البناء O(m) والمرور O(n)، مع افتراض hash وkeys مناسبين.

When is linked-list removal O(1)?

البحث عن node بالقيمة أو الموضع ممكن O(n).

Which finds shortest paths by edge count in an unweighted graph?

بيوسع طبقة طبقة باستخدام queue.

Which condition is required by Dijkstra?

أوزان سالبة ممكن تكسر صحة اختيار أقل tentative distance.

A Kahn topological sort processes fewer than V vertices. Why?

Cycle بتمنع باقي indegrees من الوصول للصفر.

What does a hash collision prove?

hash مش unique identifier؛ keys مختلفة ممكن تشترك فيه.

Which sorting statement is correct?

اختار semantics المطلوبة، واحسب d passes في radix.

A sorted array uses the shown last-pivot quicksort. Worst-case stack space?

partitions غير المتوازنة تعمل سلسلة استدعاءات بطول خطي.

Coins are 1, 3, 4 and amount is 6. Which is optimal?

ده counterexample لاختيار أكبر عملة بشكل greedy.

What does a lower-bound search return?

ينفع مع duplicates ومع target مش موجود، بشرط array مترتبة.