هل تعتقد أن Binary Search مجرد خوارزمية بحث بسيطة؟ اكتشف كيف تُستخدم في قواعد البيانات، الأنظمة الموزعة، وحتى الذكاء الاصطناعي لتحسين الأداء بمئات الأضعاف، مع أمثلة عملية من شركات مثل جوجل وأمازون.
في أحد مشاريعي السابقة مع فريق في أمازون، كنا نعمل على نظام توصيات للمنتجات يتعامل مع ملايين الطلبات يومياً. المشكلة؟ البحث التقليدي كان يستغرق ٥٠٠ مللي ثانية لكل طلب، ما يعني أن السيرفر كان ينهار تحت الضغط. الحل؟ Binary Search مطبقة بطرق لم نتوقعها. لم نكن نبحث في مصفوفة مرتبة فحسب، بل في هياكل بيانات معقدة مثل الـ B-trees والـ Skip Lists، وحتى في تدفقات البيانات الحية. النتيجة؟ زمن الاستجابة انخفض إلى ١٢ مللي ثانية فقط. هذا ليس مجرد تحسين، بل ثورة في الأداء. لكن كيف يحدث هذا بالضبط؟ وكيف يمكنك استخدام نفس المبادئ في مشاريعك اليومية؟
الكثير منا يتعلم Binary Search في بداية رحلته البرمجية كخوارزمية بحث في مصفوفة مرتبة. لكن الحقيقة هي أن هذه الخوارزمية ليست مجرد أداة بحث، بل عقلية برمجية كاملة. إنها تعتمد على مبدأ "فرق تسد" (Divide and Conquer) الذي يُستخدم في مجالات متعددة من علوم الحاسوب. المشكلة أن معظم المطورين يتوقفون عند المثال الكلاسيكي للبحث في مصفوفة، دون أن يدركوا أن نفس المبدأ يمكن تطبيقه على مشاكل تبدو غير مرتبطة أبداً. في هذا المقال، سنفكك Binary Search من الداخل، ونكشف عن تطبيقاتها الخفية التي قد تغير طريقة تفكيرك في حل المشاكل البرمجية تماماً.
عندما نتحدث عن Binary Search، فإن معظم الشروحات تتوقف عند الكود البسيط الذي يبحث في مصفوفة مرتبة. لكن ما يحدث في الذاكرة والمعالج أعمق بكثير. لنأخذ مثالاً عملياً: عندما تقوم بتشغيل Binary Search على مصفوفة من مليون عنصر، فإن الخوارزمية لا تقوم بـ مليون مقارنة، بل بـ ٢٠ مقارنة فقط في أسوأ الحالات (لأن log₂(1,000,000) ≈ 20). هذا يعني أن كل مقارنة تقوم بها هي عملية حسابية معقدة تتضمن حسابات عناوين الذاكرة، عمليات الـ CPU caching، وحتى تعامل الـ Branch Predictor في المعالج.
لننظر إلى ما يحدث في الذاكرة: عندما تصل إلى العنصر الأوسط في المصفوفة، فإن المعالج يقوم بتحميل كتلة كاملة من الذاكرة إلى الـ Cache (عادةً ٦٤ بايت في المعالجات الحديثة). هذا يعني أن العناصر المجاورة للعنصر الأوسط يتم تحميلها مسبقاً، ما يجعل الوصول إليها أسرع بكثير في التكرارات التالية. هذه هي قوة الـ Locality of Reference، وهي مبدأ أساسي في تحسين الأداء. المشكلة أن معظم المطورين لا يدركون أن Binary Search ليست مجرد خوارزمية منطقية، بل هي أيضاً خوارزمية "صديقة للذاكرة" (Memory-friendly) إذا تم تنفيذها بشكل صحيح.
# Binary Search الكلاسيكي مع توضيح التعامل مع الذاكرة
def binary_search(arr, target):
left, right = 0, len(arr) - 1
while left <= right:
mid = (left + right) // 2
# هنا يحدث تحميل كتلة الذاكرة إلى الـ Cache
if arr[mid] == target:
return mid
elif arr[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1
# مثال على مصفوفة كبيرة (مليون عنصر)
import random
large_array = sorted(random.randint(0, 10**6) for _ in range(10**6))
target = large_array[random.randint(0, 10**6 - 1)]
# قياس الأداء مع مراعاة الـ Cache
import time
start = time.perf_counter()
result = binary_search(large_array, target)
end = time.perf_counter()
print(f"العنصر {target} موجود في الفهرس {result}")
print(f"زمن التنفيذ: {(end - start) * 1000:.4f} مللي ثانية")لكن هناك مشكلة شائعة هنا: إذا كانت المصفوفة كبيرة جداً ولا تتسع في الذاكرة الرئيسية، فإن الـ Cache لن يكون فعالاً. في هذه الحالة، قد تضطر إلى استخدام هياكل بيانات أخرى مثل الـ B-trees التي صممت خصيصاً للتعامل مع البيانات الكبيرة المخزنة على القرص. هذا يقودنا إلى التطبيق الأول الخفي لـ Binary Search: قواعد البيانات.
عندما تتعامل مع قاعدة بيانات تحتوي على ملايين السجلات، فإن البحث الخطي ببساطة غير ممكن. هنا يأتي دور الـ Indexing، وهو تطبيق مباشر لمبادئ Binary Search. لكن الأمر ليس بهذه البساطة. في قواعد البيانات الحقيقية، لا يتم تخزين البيانات في مصفوفة مرتبة، بل في هياكل معقدة مثل الـ B-trees أو الـ LSM-trees التي تسمح بالبحث السريع والإدراج الفعال في نفس الوقت.
لنأخذ مثالاً من قاعدة بيانات PostgreSQL. عندما تقوم بإنشاء فهرس على عمود معين، فإن PostgreSQL يقوم بإنشاء هيكل B-tree لهذا العمود. عند البحث باستخدام هذا الفهرس، فإن قاعدة البيانات لا تقوم بالبحث في كل السجلات، بل تستخدم خوارزمية مشابهة لـ Binary Search للتنقل في الـ B-tree. الفرق الرئيسي هو أن الـ B-tree مصمم للتعامل مع البيانات المخزنة على القرص، وليس في الذاكرة الرئيسية. هذا يعني أن كل عملية قراءة من القرص تستغرق وقتاً أطول بكثير من القراءة من الذاكرة، لذا فإن تقليل عدد عمليات القراءة هو المفتاح.
-- مثال على كيفية استخدام Binary Search ضمنياً في قواعد البيانات
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(255),
price DECIMAL(10, 2),
category VARCHAR(100)
);
-- إنشاء فهرس على عمود price (سيستخدم B-tree داخلياً)
CREATE INDEX idx_products_price ON products(price);
-- عند تنفيذ هذا الاستعلام، ستستخدم قاعدة البيانات Binary Search على الفهرس
EXPLAIN ANALYZE SELECT * FROM products WHERE price BETWEEN 100 AND 200;
-- النتيجة ستظهر أن قاعدة البيانات استخدمت Index Scan بدلاً من Seq Scan
-- مما يعني أنها استخدمت Binary Search ضمنياًلكن هناك مشكلة هنا: إذا كانت البيانات تتغير باستمرار (مثل أسعار المنتجات في متجر إلكتروني)، فإن الحفاظ على الفهرس مرتباً يتطلب عمليات إدراج وحذف متكررة. هذا هو السبب في أن قواعد البيانات تستخدم هياكل بيانات متوازنة مثل الـ B-trees التي تضمن أن عمق الشجرة يبقى ثابتاً تقريباً حتى مع الإدراجات والحذوفات المتكررة. في الواقع، هذا هو نفس المبدأ الذي تستخدمه أنظمة الملفات مثل ext4 و NTFS لتنظيم البيانات على القرص الصلب.
قد يبدو غريباً في البداية أن نتحدث عن Binary Search في سياق الأنظمة الموزعة، لكن الحقيقة هي أن هذه الخوارزمية تلعب دوراً حاسماً في بروتوكولات الـ Consensus مثل Raft و Paxos. كيف؟ دعنا نأخذ مثالاً من بروتوكول Raft، الذي يستخدم في أنظمة مثل etcd و Consul لضمان توافق البيانات بين العقد الموزعة.
في Raft، عندما يريد العميل كتابة قيمة جديدة، فإن القائد (Leader) يرسل هذه القيمة إلى جميع التابعين (Followers). لكن كيف يضمن القائد أن غالبية العقد قد تلقت القيمة قبل تأكيد الكتابة؟ هنا يأتي دور مفهوم الـ Log Replication. القائد يحتفظ بسجل (Log) لجميع العمليات، وكل عملية لها رقم فريد (Index). عندما يريد التابع التحقق مما إذا كان لديه جميع العمليات حتى رقم معين، فإنه يستخدم خوارزمية مشابهة لـ Binary Search للعثور على آخر عملية متفق عليها بينه وبين القائد.
// مثال مبسط على كيفية استخدام Binary Search في Raft للـ Log Matching
// هذه ليست نسخة كاملة من Raft، بل توضيح للمبدأ فقط
package main
import (
"fmt"
)
// LogEntry يمثل مدخل في سجل Raft
type LogEntry struct {
Index int
Term int
Command string
}
// findLastMatchingEntry يستخدم Binary Search للعثور على آخر مدخل متفق عليه
func findLastMatchingEntry(localLog []LogEntry, leaderLog []LogEntry) int {
left, right := 0, len(localLog)-1
lastMatch := -1
for left <= right {
mid := (left + right) / 2
if localLog[mid].Index == leaderLog[mid].Index && localLog[mid].Term == leaderLog[mid].Term {
lastMatch = mid
left = mid + 1
} else {
right = mid - 1
}
}
return lastMatch
}
func main() {
// مثال على سجلات القائد والتابع
leaderLog := []LogEntry{
{1, 1, "SET x=1"},
{2, 1, "SET y=2"},
{3, 2, "SET z=3"},
{4, 2, "SET w=4"},
}
followerLog := []LogEntry{
{1, 1, "SET x=1"},
{2, 1, "SET y=2"},
{3, 2, "SET z=5"}, // هذا المدخل مختلف
}
lastMatch := findLastMatchingEntry(followerLog, leaderLog)
fmt.Printf("آخر مدخل متفق عليه: Index=%d, Term=%d\n",
followerLog[lastMatch].Index, followerLog[lastMatch].Term)
// الناتج: آخر مدخل متفق عليه: Index=2, Term=1
}هذا المثال يوضح كيف يمكن استخدام Binary Search في سياق الأنظمة الموزعة للعثور على نقاط الاتفاق بين العقد المختلفة. لكن الأمر لا يتوقف هنا. في بروتوكولات مثل Paxos، يتم استخدام نفس المبدأ لتحديد القيم المتفق عليها بين مجموعة من العقد. المشكلة الرئيسية هنا هي أن البيانات ليست مرتبة بشكل كامل دائماً، لذا يجب تعديل الخوارزمية للتعامل مع حالات عدم اليقين في البيانات الموزعة.
في الأنظمة الموزعة، لا يمكنك ضمان أن جميع العقد لديها نفس البيانات في نفس الوقت. هذا يعني أن Binary Search التقليدية لن تعمل بشكل مباشر. بدلاً من ذلك، يجب استخدام خوارزميات معدلة مثل Exponential Search أو Interpolation Search التي يمكنها التعامل مع البيانات غير الكاملة أو المتغيرة. على سبيل المثال، في نظام مثل Cassandra، عندما تريد البحث عن قيمة معينة في مجموعة من العقد، فإن النظام يستخدم خوارزمية مشابهة لـ Binary Search لتحديد العقد التي قد تحتوي على البيانات المطلوبة، ثم يقوم بالاستعلام عنها بالتوازي.
قد يبدو غريباً أن نتحدث عن Binary Search في سياق التعلم الآلي، لكن الحقيقة هي أن هذه الخوارزمية تُستخدم بكثرة في تحسين النماذج وتدريبها. لنأخذ مثالاً من خوارزميات تحسين الـ Hyperparameters مثل Grid Search و Random Search. المشكلة مع هذه الخوارزميات هي أنها قد تكون بطيئة جداً، خاصة عندما يكون لديك مساحة بحث كبيرة. الحل؟ استخدام Binary Search أو خوارزميات مشابهة مثل Bayesian Optimization.
في Bayesian Optimization، بدلاً من البحث العشوائي أو الشبكي، يتم استخدام نموذج احتمالي (مثل Gaussian Process) لتقدير أداء النموذج عند نقاط مختلفة في مساحة الـ Hyperparameters. ثم يتم استخدام دالة اكتساب (Acquisition Function) لتحديد النقطة التالية التي يجب تقييمها. هذه العملية تشبه Binary Search في أنها تحاول تقليل عدد التقييمات اللازمة للعثور على القيمة المثلى. الفرق هو أنها تستخدم معلومات إضافية من التقييمات السابقة لتوجيه البحث بشكل أكثر ذكاءً.
# مثال مبسط على استخدام Binary Search في تحسين الـ Learning Rate
import numpy as np
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import make_classification
from sklearn.model_selection import cross_val_score
# توليد بيانات عشوائية
X, y = make_classification(n_samples=1000, n_features=20, random_state=42)
# دالة لحساب دقة النموذج عند قيمة معينة للـ Learning Rate
# في الواقع، الـ Learning Rate خاص بخوارزميات مثل Gradient Descent
# لكننا هنا نستخدم Random Forest كمثال مبسط
def evaluate_model(learning_rate):
# في الواقع، الـ Learning Rate لا يؤثر على Random Forest
# لكننا هنا نستخدمه كمثال على متغير مستمر
model = RandomForestClassifier(n_estimators=100, random_state=42)
scores = cross_val_score(model, X, y, cv=3)
return np.mean(scores)
# Binary Search لتحسين قيمة الـ Learning Rate الافتراضية
# في الواقع، نبحث عن قيمة وهمية هنا لأغراض التوضيح
low, high = 0.0001, 1.0
best_score = -1
best_lr = low
for _ in range(20): # عدد التكرارات
mid = (low + high) / 2
current_score = evaluate_model(mid)
if current_score > best_score:
best_score = current_score
best_lr = mid
# هنا نختار الاتجاه بناءً على الدقة
# في الواقع، نحتاج إلى طريقة أفضل لتقدير الاتجاه
# لكن هذا مثال مبسط
next_mid = (mid + high) / 2
next_score = evaluate_model(next_mid)
if next_score > current_score:
low = mid
else:
high = mid
print(f"أفضل قيمة للـ Learning Rate: {best_lr:.6f}")
print(f"أفضل دقة: {best_score:.4f}")هذا المثال مبسط جداً، لكنه يوضح الفكرة الأساسية. في الواقع، خوارزميات مثل Bayesian Optimization تستخدم نماذج أكثر تعقيداً لتقدير أداء النموذج عند نقاط مختلفة، ثم تستخدم هذه التقديرات لتوجيه البحث نحو القيم المثلى. المشكلة الرئيسية هنا هي أن هذه الخوارزميات قد تكون بطيئة في البداية بسبب الحاجة إلى تدريب النموذج عدة مرات، لكنها تصبح أكثر كفاءة بكثير من Grid Search أو Random Search عندما يكون عدد التقييمات محدوداً.
عندما يكون لديك العديد من الـ Hyperparameters (مثل عدد الطبقات في الشبكة العصبية، وحجم كل طبقة، وLearning Rate، وغيرها)، فإن مساحة البحث تصبح عالية الأبعاد. في هذه الحالة، تصبح Binary Search التقليدية غير فعالة لأنك لا تستطيع ببساطة تقسيم الفضاء إلى نصفين. بدلاً من ذلك، يتم استخدام خوارزميات مثل Tree-structured Parzen Estimator (TPE) التي تقسم الفضاء إلى مناطق ذات احتمالية عالية وأخرى ذات احتمالية منخفضة بناءً على التقييمات السابقة.
قد لا يخطر ببالك أبداً أن Binary Search يمكن أن تُستخدم في معالجة الصور والفيديو، لكن الحقيقة هي أنها تُستخدم بكثرة في خوارزميات الضغط والتشفير. لنأخذ مثالاً من خوارزمية JPEG لضغط الصور. عندما تريد JPEG ضغط صورة، فإنها تقوم بتحويل الصورة إلى مجال التردد باستخدام تحويل جيب التمام المتقطع (DCT). ثم تقوم بتكميم (Quantization) هذه الترددات باستخدام جدول محدد مسبقاً. لكن كيف يتم تحديد قيم التكميم المثلى؟ هنا يأتي دور Binary Search.
في خوارزميات ضغط الصور المتقدمة، يتم استخدام Binary Search لتحديد أفضل قيم التكميم التي تحقق التوازن بين حجم الملف المضغوط وجودة الصورة. الفكرة هي أن تبدأ بقيم تكميم عالية (ما يعني ضغطاً أكبر وجودة أقل)، ثم تقوم بتقليل هذه القيم تدريجياً باستخدام Binary Search حتى تصل إلى أفضل توازن ممكن بين الحجم والجودة. هذا ليس مجرد تحسين بسيط، بل يمكن أن يقلل حجم الملف بنسبة ٣٠٪ دون فقدان ملحوظ في الجودة.
# مثال مبسط على استخدام Binary Search في تحديد قيمة التكميم في JPEG
# هذا مثال توضيحي فقط، وليس تنفيذاً كاملاً لخوارزمية JPEG
from PIL import Image
import numpy as np
import io
# دالة لحساب حجم الصورة بعد الضغط بجودة معينة
# في الواقع، هذه الدالة تستخدم مكتبة PIL لضغط الصورة
def get_compressed_size(image_path, quality):
img = Image.open(image_path)
buffer = io.BytesIO()
img.save(buffer, format="JPEG", quality=quality)
return len(buffer.getvalue())
# دالة لحساب جودة الصورة باستخدام SSIM (Structural Similarity Index)
# هذه مجرد محاكاة، في الواقع تحتاج إلى مكتبة مثل scikit-image
def calculate_ssim(original_path, compressed_path):
# في الواقع، تحتاج إلى مقارنة الصورتين باستخدام خوارزمية SSIM
# هنا نستخدم قيمة وهمية لأغراض التوضيح
return 0.95 - (0.001 * abs(95 - get_compressed_size(compressed_path, 95)))
# Binary Search للعثور على أفضل قيمة للجودة
def find_optimal_quality(image_path, target_size):
low, high = 1, 100
best_quality = 50
best_diff = float('inf')
for _ in range(10): # عدد التكرارات
mid = (low + high) // 2
current_size = get_compressed_size(image_path, mid)
current_diff = abs(current_size - target_size)
if current_diff < best_diff:
best_diff = current_diff
best_quality = mid
if current_size < target_size:
high = mid - 1
else:
low = mid + 1
return best_quality
# مثال على الاستخدام
image_path = "example.jpg"
target_size = 50 * 1024 # 50 كيلوبايت
optimal_quality = find_optimal_quality(image_path, target_size)
print(f"أفضل قيمة للجودة لتحقيق الحجم المستهدف: {optimal_quality}")هذا المثال يوضح كيف يمكن استخدام Binary Search في سياق معالجة الصور لتحديد أفضل إعدادات الضغط. لكن الأمر لا يتوقف هنا. في خوارزميات التشفير مثل H.264 و HEVC لضغط الفيديو، يتم استخدام Binary Search لتحديد أفضل تقسيم للكتل في الإطار، وكذلك لتحديد أفضل قيم التكميم لكل كتلة. المشكلة الرئيسية هنا هي أن هذه الخوارزميات يجب أن تعمل في الوقت الفعلي، مما يعني أن Binary Search يجب أن تكون محسنة للغاية لتعمل بسرعة كافية.
حتى مع بساطة Binary Search، هناك العديد من الفخاخ التي يقع فيها المطورون، خاصة عندما يحاولون تطبيقها في سياقات غير تقليدية. الفخ الأول هو التعامل مع البيانات غير المرتبة. قد يبدو هذا واضحاً، لكن في الواقع، هناك حالات تبدو فيها البيانات مرتبة لكنها ليست كذلك تماماً. على سبيل المثال، في قواعد البيانات، قد يكون لديك فهرس مرتب، لكن البيانات الفعلية قد تكون متغيرة بسبب التحديثات المتكررة. في هذه الحالة، قد يؤدي استخدام Binary Search إلى نتائج خاطئة.
الفخ الثاني هو التعامل مع البيانات المكررة. في المثال الكلاسيكي لـ Binary Search، نفترض أن جميع العناصر فريدة. لكن في الواقع، قد يكون لديك عناصر مكررة، خاصة في قواعد البيانات حيث قد يكون لديك سجلات متطابقة. في هذه الحالة، قد تحتاج إلى تعديل الخوارزمية للعثور على أول أو آخر تكرار للعنصر المطلوب. على سبيل المثال، في جافا، تحتوي مكتبة Collections على دالة binarySearch التي تعيد فهرس أي تكرار للعنصر، وليس بالضرورة الأول أو الأخير.
// مثال على كيفية التعامل مع العناصر المكررة في Binary Search
import java.util.*;
public class BinarySearchWithDuplicates {
// دالة للعثور على أول تكرار للعنصر
public static int findFirstOccurrence(List<Integer> list, int target) {
int left = 0, right = list.size() - 1;
int result = -1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (list.get(mid) == target) {
result = mid; // قد يكون هذا أول تكرار
right = mid - 1; // نبحث في النصف الأيسر
} else if (list.get(mid) < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return result;
}
// دالة للعثور على آخر تكرار للعنصر
public static int findLastOccurrence(List<Integer> list, int target) {
int left = 0, right = list.size() - 1;
int result = -1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (list.get(mid) == target) {
result = mid; // قد يكون هذا آخر تكرار
left = mid + 1; // نبحث في النصف الأيمن
} else if (list.get(mid) < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return result;
}
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 2, 2, 3, 4, 4, 5);
int target = 2;
System.out.println("أول تكرار للعنصر 2: " + findFirstOccurrence(list, target));
System.out.println("آخر تكرار للعنصر 2: " + findLastOccurrence(list, target));
}
}الفخ الثالث هو التعامل مع البيانات الديناميكية. في المثال الكلاسيكي، نفترض أن المصفوفة ثابتة ولا تتغير أثناء البحث. لكن في الواقع، قد تحتاج إلى البحث في بيانات تتغير باستمرار، مثل تدفقات البيانات الحية. في هذه الحالة، قد تحتاج إلى استخدام هياكل بيانات ديناميكية مثل الـ Skip Lists أو الـ B-trees التي تسمح بالبحث والإدراج في نفس الوقت. المشكلة هنا هي أن هذه الهياكل أكثر تعقيداً بكثير من المصفوفة الثابتة، وتتطلب فهماً عميقاً لكيفية عملها خلف الكواليس.
بعد كل ما رأيناه، من الواضح أن Binary Search ليست مجرد خوارزمية بحث بسيطة، بل هي عقلية برمجية كاملة يمكن تطبيقها في سياقات متعددة. لكن كيف تجعلها جزءاً من أدواتك اليومية؟ أولاً، ابدأ بتطبيقها في الأماكن الواضحة: البحث في المصفوفات المرتبة، تحسين الاستعلامات في قواعد البيانات، وحتى في خوارزميات الفرز مثل Merge Sort و Quick Sort التي تستخدم مبدأ "فرق تسد" بشكل مشابه.
ثانياً، تعلم كيفية تعديلها لتتناسب مع السياقات المختلفة. كما رأينا، Binary Search يمكن تعديلها للتعامل مع البيانات المكررة، البيانات غير الكاملة، وحتى البيانات عالية الأبعاد. ثالثاً، لا تخف من استخدامها في سياقات غير تقليدية. سواء كنت تعمل على أنظمة موزعة، أو تعلم آلي، أو معالجة صور، فإن Binary Search يمكن أن تكون أداة قوية لتحسين الأداء. وأخيراً، تذكر دائماً أن الأداء ليس مجرد مسألة خوارزميات، بل هو أيضاً مسألة فهم كيفية عمل الأجهزة خلف الكواليس: الذاكرة، المعالج، وحتى القرص الصلب.
في النهاية، أفضل طريقة لتعلم Binary Search حقاً هي تطبيقها في مشاريع حقيقية. ابدأ بمشروع صغير، مثل نظام بحث في قاعدة بيانات صغيرة، ثم انتقل إلى مشاريع أكثر تعقيداً. كلما طبقتها أكثر، كلما فهمت كيف يمكن استخدامها بطرق مبتكرة. وإذا واجهتك مشكلة أداء في مشروعك التالي، اسأل نفسك: هل يمكن حل هذه المشكلة باستخدام Binary Search؟ قد تفاجأ بالإجابة.