المفاهيم
هنفهم إيه؟ هنتعلم نختار طريقة تمثيل البيانات وحل المسألة حسب شكلها، ونقدّر تكلفة الحل قبل ما نكتب كود. ده يساعدك تقارن مثلًا بين البحث في قائمة كل مرة وبين تجهيز lookup مرة واحدة.
قبل ما تبدأ: محتاج تعرف أساسيات المتغيرات والـ arrays والـ loops. الـ input هو الداتا اللي المسألة بتستقبلها؛ algorithm خطوات محددة لتحويلها لنتيجة؛ وBig-O وصف لكيفية نمو الوقت أو الذاكرة مع كبر input، مش عدد ثوانٍ. هنستخدم ، يعني شرط يفضل صحيح أثناء خطوات الخوارزمية، ونشرح كل افتراض قبل الاعتماد عليه.
المصطلحات اللي تحتها خط منقّط تقدر تضغط عليها لمعرفة معناها، وترجع تكمل من نفس مكانك.
#Big-O: النمو مش عدد الثوانيBig-O describes growth, not seconds
- البحث عن اسم داخل قائمة غير مرتبة قد يفحص n عنصرًا في أسوأ حالة؛ مضاعفة عدد العناصر تضاعف تقريبًا عدد الفحوص. ده نمو O(n)، وليس وعدًا بزمن محدد.
- على قائمة مرتبة، البحث الثنائي يستبعد نصف النطاق كل خطوة؛ مع 1,024 عنصرًا يحتاج عدد خطوات يقارب عشرة، مع اختلاف بسيط حسب الخوارزمية وحالة الإنهاء.
- 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) بنستثمر O(m) memory ووقت متوقع لبناء lookup بدل مسح العملاء لكل order.
- (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 حجمها ثابت، وList<T> بتستخدم array بتكبر عند الحاجة. العناصر متجاورة، لكن لو T reference type، المتجاور هو references مش بالضرورة الـ objects نفسها.
| العملية | Array أو List | LinkedList |
|---|---|---|
| index | O(1) | O(n) |
| بحث بقيمة | O(n)؛ أو O(log n) لو مرتبة وbinary search | O(n) |
| إضافة آخر List | O(1) amortized | O(1) مع tail |
| إدخال أو حذف في النص | O(n) shifting | O(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) احتفظنا بالـ node، مش القيمة بس.
- (2) الإضافة قبل node معروفة O(1).
- (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) Pop بيشيل آخر عنصر؛ Peek بيشوفه بس. push O(1) amortized وpop O(1). مفيد للـ undo والأقواس و.
- (2) Dequeue بيشيل أول عنصر؛ enqueue O(1) amortized وdequeue O(1). مناسب لـ والـ buffers.
- (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.
- احسب hash بالـ comparer.
- حوّله لرقم bucket مناسب؛ التنفيذ الحقيقي بيتعامل مع العلامة وحدود الأرقام.
- افحص candidates بالـ hash والـ equality.
- لما الـ table تحتاج تكبر، اعمل resize وإعادة توزيع.
Chaining بيربط العناصر المتصادمة؛ Dictionary في بيستخدم entries array وروابط indexes. Open addressing بيدوّر على slot تانية باستخدام probing. Load factor هو عدد العناصر بالنسبة للخانات؛ الزيادة ممكن تزود التصادم وتستدعي resize.
public override int GetHashCode() => 1;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) بنستبعد فرع بناء على BST ؛ مش مضمون نستبعد نص العناصر إلا لو متوازنة.
- (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 |
| Heap | min أو max في root؛ binary heap كاملة وتتخزن array؛ مش كل العناصر مرتبة |
| Trie | مسار للحروف؛ prefix lookup بطول الكلمة O(L) مع child lookup ثابت |
| B-tree وB+ tree | balanced و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
- عندك روابط A إلى B وC، وB إلى D. من A، يزور الجيران B وC قبل مستوى D، باستخدام Queue تحفظ من ينتظر الزيارة.
- قد يمشي A ثم B ثم D قبل الرجوع إلى C، حسب ترتيب الجيران. مجموعة visited تمنع الدوران بلا نهاية لو فيه مسار يرجع لعقدة سابقة.
- 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) بنعلّم start وقت إدخاله. المثال بيفترض كل vertex موجودة كـ key حتى لو مالهاش neighbors.
- (2) Mark-on-enqueue يمنع تكرار node في الـ queue. BFS يزور 1 و2 و3 و4 ويلاقي أقل عدد edges في unweighted graph.
- (3) DFS يعلّم قبل النزول، فيزور 1 و2 و4 و3 حسب ترتيب neighbors. لو عايز كل graph، ابدأ traversal لكل vertex لسه ماتزارتش.
| BFS | DFS | |
|---|---|---|
| الأداة | Queue | Recursion أو stack |
| مناسب لـ | أقرب nodes وunweighted shortest paths | Components وbacktracking وcycle analysis |
| المساحة | O(V) للـ frontier والـ visited | O(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) بنضيف heap entries جديدة بدل decrease-key؛ القديمة بتتجاهل لما تطلع. unreachable تفضل long.MaxValue.
- (2) relaxation بتسجل المسار الأرخص. مع lazy duplicates الوقت O((V + E) log(V + E)) كحد عام، والـ heap ممكن O(E)، مش O(V) بالضرورة.
- (3) Kahn بيبدأ بالعناصر اللي مالهاش prerequisites، ويقلل indegree بعد إزالة كل عنصر.
- (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) حساب mid بالشكل ده يتجنب overflow بتاع جمع indexين موجبين.
- (2) بنعدي mid علشان المساحة تصغر ومندخلش loop بلا نهاية.
- (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) Lomuto partition بيحط pivot في مكانها النهائي. استدعِ QuickSort(a, 0, a.Length - 1). المتوسط O(n log n)، لكن last pivot مع sorted أو equal input ممكن O(n²) وO(n) stack.
- (2) بنعمل copies للنصين للتوضيح. Mergesort O(n log n) وقت وO(n) peak auxiliary space مع O(log n) عمق؛ إجمالي في النسخة دي O(n log n).
- (3) اختيار الشمال عند التعادل يحافظ على stability. عدد المقارنات في merge خطي، وبننسخ باقي العناصر.
| Algorithm | Best | Average | Worst | Auxiliary space | Stable |
|---|---|---|---|---|---|
| Bubble مع early exit وInsertion | O(n) | O(n²) | O(n²) | O(1) | أيوه بالتنفيذ المعتاد |
| Selection | O(n²) | O(n²) | O(n²) | O(1) | لأ |
| Quick | O(n log n) | O(n log n) | O(n²) | O(log n) متوسط stack؛ O(n) أسوأ | لأ |
| Merge على array | O(n log n) | O(n log n) | O(n log n) | O(n) | أيوه |
| Heap | O(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 passes | O(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) memoization بتمنع إعادة نفس Fibonacci state: من exponential recursion لوقت O(n) وmemory O(n) شاملة stack.
- (2) tabulation تحتفظ بآخر حالتين بس: O(n) وقت وO(1) مساحة. bounded numeric type جزء من contract.
- (3) best[value] أقل عدد عملات، وبنبنيها من قيمة أصغر. الوقت O(amount × coins.Length)، والمساحة O(amount). ده pseudo-polynomial بالنسبة لعدد bits اللي بيمثل amount.
| الأسلوب | اختاره لما | أمثلة |
|---|---|---|
| Divide and conquer | الأجزاء مستقلة أو بنختار جزء واحد | Merge sort، وquick sort، وbinary search |
| DP | نفس states بتتكرر وتقدر تصيغ recurrence | Coin 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 | اتشاف قبل كده؟ وعدّ أو lookup | Two Sum، وanagrams، وduplicates |
| Two pointers | ترتيب أو مقارنة من الطرفين | Sorted Two Sum، وpalindrome، وcontainer |
| Sliding window | جزء متصل وشرط نقدر نصلحه بتحريك الحدود | Longest unique substring، وfixed-window sum |
| Prefix sum | Range sums أو subarray sums مع negatives | Subarray Sum Equals K |
| Stack | تداخل أو أقرب أكبر عنصر | Parentheses، وdaily temperatures |
| Binary search | Sorted data أو monotonic feasibility | Rotated array، وأقل capacity مناسبة |
| و | Grid أو graph أو tree | Islands، وlevel order، وcomponents |
| Heap | Top-K أو minimum متكرر | Kth largest، وmerge K sorted streams |
| Choices وstates متكررة للتحسين أو العد | House Robber، وstairs، وcoin change | |
| Backtracking | كل الحلول أو الاحتمالات | Subsets، وpermutations، وN-Queens |
| Sorting | تجميع أو intervals | Merge intervals، وmeeting rooms |
أسئلة الانترفيو
مفيش نتايج
راجع فهمك
ليه الاختيار ده مناسب؟ إمتى ممكن ماينفعش؟ وإيه اللي هيتغير لو الحمل زاد أو ظهر فشل؟
28.1What does Big-O notation tell you?Junior
بيوصف حد نمو الوقت أو المساحة، مش زمن ثابت. قول n بيمثل إيه، وحدد worst أو expected أو amortized، واحسب كل inputs مستقلة.
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).
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 تلقائياً.
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.
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 لما يكبر ينسخ كتير، لكن النمو الهندسي يخلي مجموع النسخ خطي.
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.
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.
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 لازم تتتجاهل.
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.
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 ممكن تخلي العادية سلسلة.
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 أفضل في كل حاجة.
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).
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.
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 لوحده.
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.
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.
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 خطوة أولى مش تقييم كامل.
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 يحسب كل البدايات الممكنة.
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.
اختبر نفسك
إجابات الاختبار وعلامات إكمال المحاور بتتحفظ على المتصفح ده، ومش بتتنقل لأجهزة تانية.
Build a dictionary of m customers, then look up n orders. Expected total time?
When is linked-list removal O(1)?
Which finds shortest paths by edge count in an unweighted graph?
Which condition is required by Dijkstra?
A Kahn topological sort processes fewer than V vertices. Why?
What does a hash collision prove?
Which sorting statement is correct?
A sorted array uses the shown last-pivot quicksort. Worst-case stack space?
Coins are 1, 3, 4 and amount is 6. Which is optimal?
What does a lower-bound search return?