Levenshtein Distance که معمولاً با عنوان Edit Distance نیز شناخته میشود، معیاری برای اندازهگیری میزان تفاوت میان دو رشته است.
ایده اصلی بسیار ساده است:
حداقل چند عملیات لازم است تا یک رشته را به رشته دیگری تبدیل کنیم؟
در نسخه استاندارد Levenshtein سه عملیات مجاز داریم:
- Insert — اضافه کردن یک کاراکتر
- Delete — حذف یک کاراکتر
- Replace — جایگزینی یک کاراکتر
هر عملیات معمولاً هزینه 1 دارد.
برای مثال:
cat
↓
cutفقط کافی است a را با u جایگزین کنیم.
بنابراین:
Levenshtein Distance = 1یک مثال معروف
دو رشته زیر را در نظر بگیرید:
kitten
sittingمیتوانیم تبدیل را اینطور انجام دهیم:
kitten
↓ Replace k → s
sitten
↓ Replace e → i
sittin
↓ Insert g
sittingدر مجموع سه عملیات انجام شده است:
Levenshtein Distance = 3این مقدار حداقل تعداد عملیات لازم است.
چرا مقایسه ساده کافی نیست؟
در Hamming Distance فقط موقعیتهای متناظر را مقایسه میکردیم و دو رشته باید طول برابر میداشتند.
اما در Levenshtein Distance میتوانیم رشتههایی با طول متفاوت داشته باشیم.
مثلاً:
cat
catsفقط یک Insert لازم داریم:
cat → catsپس:
Distance = 1این ویژگی باعث میشود Levenshtein برای Spell Checking، Fuzzy Search و مقایسه متن بسیار کاربردیتر باشد.
مسئله را چگونه حل کنیم؟
فرض کنید دو رشته داریم:
word1 = "horse"
word2 = "ros"میخواهیم کمترین تعداد عملیات را پیدا کنیم.
برای این کار معمولاً از Dynamic Programming استفاده میکنیم.
یک ماتریس تعریف میکنیم که:
dp[i][j]نشان میدهد حداقل هزینه تبدیل:
word1[0 ... i-1]به:
word2[0 ... j-1]چقدر است.
Base Caseها
اگر رشته اول خالی باشد:
"" → "abc"باید سه کاراکتر Insert کنیم.
بنابراین:
dp[0][j] = jاگر رشته دوم خالی باشد:
"abc" → ""باید تمام کاراکترها حذف شوند:
dp[i][0] = iاین مقادیر سطر و ستون اول جدول DP را تشکیل میدهند.
رابطه اصلی Dynamic Programming
اگر دو کاراکتر فعلی برابر باشند:
word1[i - 1] === word2[j - 1]هیچ عملیات جدیدی لازم نیست:
dp[i][j] = dp[i - 1][j - 1]اما اگر متفاوت باشند، سه انتخاب داریم.
Insert
dp[i][j - 1] + 1Delete
dp[i - 1][j] + 1Replace
dp[i - 1][j - 1] + 1پس:
dp[i][j] = 1 + min(
dp[i - 1][j],
dp[i][j - 1],
dp[i - 1][j - 1]
)مثال ساده با جدول DP
فرض کنید:
word1 = cat
word2 = cutجدول اولیه:
"" c u t
"" 0 1 2 3
c 1
a 2
t 3پس از تکمیل جدول:
"" c u t
"" 0 1 2 3
c 1 0 1 2
a 2 1 1 2
t 3 2 2 1آخرین خانه:
dp[3][3] = 1پس فاصله میان cat و cut برابر 1 است.
پیادهسازی TypeScript
function levenshteinDistance(a: string, b: string): number {
const rows = a.length + 1;
const cols = b.length + 1;
const dp: number[][] = Array.from(
{ length: rows },
() => new Array(cols).fill(0)
);
for (let i = 0; i < rows; i++) {
dp[i][0] = i;
}
for (let j = 0; j < cols; j++) {
dp[0][j] = j;
}
for (let i = 1; i < rows; i++) {
for (let j = 1; j < cols; j++) {
if (a[i - 1] === b[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = 1 + Math.min(
dp[i - 1][j],
dp[i][j - 1],
dp[i - 1][j - 1]
);
}
}
}
return dp[a.length][b.length];
}
console.log(
levenshteinDistance("kitten", "sitting")
); // 3پیچیدگی زمانی و فضایی
اگر طول رشته اول n و رشته دوم m باشد:
Time Complexity: O(n × m)
Space Complexity: O(n × m)زیرا تقریباً برای هر ترکیب از موقعیتهای دو رشته یک Cell محاسبه میکنیم.
آیا میتوان Space Complexity را کاهش داد؟
بله.
برای محاسبه هر Row فقط به Row قبلی نیاز داریم.
بنابراین لازم نیست کل ماتریس را نگه داریم.
میتوانیم Space Complexity را به:
O(min(n, m))کاهش دهیم.
نمونه TypeScript:
function levenshteinOptimized(a: string, b: string): number {
if (a.length < b.length) {
[a, b] = [b, a];
}
let previous = Array.from(
{ length: b.length + 1 },
(_, i) => i
);
for (let i = 1; i <= a.length; i++) {
const current = new Array(b.length + 1);
current[0] = i;
for (let j = 1; j <= b.length; j++) {
if (a[i - 1] === b[j - 1]) {
current[j] = previous[j - 1];
} else {
current[j] = 1 + Math.min(
previous[j],
current[j - 1],
previous[j - 1]
);
}
}
previous = current;
}
return previous[b.length];
}Levenshtein Distance در Spell Checker
فرض کنید کاربر کلمه زیر را تایپ کرده است:
programingولی در Dictionary این کلمات وجود دارند:
programming
programmer
processingمیتوانیم فاصله کاربر با هر کلمه را محاسبه کنیم.
کلمهای که Levenshtein Distance کمتری دارد احتمالاً پیشنهاد مناسبتری است.
مثلاً:
programing → programming = 1زیرا فقط یک m کم است.
کاربرد در Fuzzy Search
گاهی کاربر دقیقاً عبارت موجود در Database را وارد نمیکند.
برای مثال نام مشتری:
Alexanderولی جستجو میکند:
AlexnderExact Matching ممکن است هیچ نتیجهای برنگرداند.
اما با Edit Distance متوجه میشویم که دو عبارت بسیار نزدیکاند.
این ایده در سیستمهایی مانند:
- Search
- CRM
- Contact Matching
- Product Search
- Deduplication
کاربرد دارد.
کاربرد در Data Cleaning و Deduplication
فرض کنید در CRM دو رکورد داریم:
Arian Soleimanzadeh
Arian Soleimanzadeممکن است این دو رکورد متعلق به یک نفر باشند ولی به دلیل Typo دو بار ثبت شده باشند.
Levenshtein Distance میتواند یکی از Signalهای تشخیص Duplicate باشد.
البته در سیستم واقعی معمولاً نباید فقط بر اساس نام تصمیم بگیریم؛ دادههایی مانند Email، Phone و سایر ویژگیها نیز باید بررسی شوند.
کاربرد در NLP
Levenshtein Distance در بعضی مسائل پردازش زبان طبیعی برای مقایسه Tokenها یا Sequenceها استفاده میشود.
برای مثال:
- Typo Detection
- Text Normalization
- OCR Correction
- Transliteration Matching
- Approximate String Matching
کاربرد در DNA و Bioinformatics
رشتههای DNA را میتوان مانند Sequenceهای متنی تصور کرد.
برای مثال:
ACGTAC
ACGTCEdit Distance میتواند میزان اختلاف میان دو Sequence را اندازهگیری کند.
البته مسائل واقعی Bioinformatics معمولاً الگوریتمها و Cost Modelهای تخصصیتری دارند.
Levenshtein Distance و Hamming Distance
Hamming Distance فقط اختلاف موقعیتهای متناظر را میشمارد و در تعریف استاندارد ورودیها باید همطول باشند.
Hamming:
cat
cut
Distance = 1اما:
cat
catsبرای Hamming استاندارد مناسب نیست.
Levenshtein میگوید:
Insert s
Distance = 1بنابراین Levenshtein انعطاف بیشتری دارد ولی محاسبه آن گرانتر است.
Levenshtein و Longest Common Subsequence
این دو مسئله ارتباط مفهومی نزدیکی دارند، زیرا هر دو معمولاً با Dynamic Programming حل میشوند.
اما هدف متفاوت است.
Levenshtein میپرسد:
حداقل چند Edit لازم است؟
در حالی که LCS میپرسد:
طول بلندترین زیرتوالی مشترک چقدر است؟
درک این تفاوت در مسائل مصاحبه بسیار مهم است.
Weighted Edit Distance چیست؟
همیشه لازم نیست هزینه Insert، Delete و Replace برابر 1 باشد.
مثلاً میتوانیم تعریف کنیم:
Insert = 1
Delete = 1
Replace = 2یا در یک Keyboard Correction System، جایگزینی کلیدهای نزدیک هزینه کمتری داشته باشد.
مثلاً اشتباه:
hello → heloممکن است Cost متفاوتی نسبت به یک تغییر کاملاً نامرتبط داشته باشد.
این نسخهها به Weighted Edit Distance معروفاند.
یک سؤال رایج مصاحبه
صورت مسئله:
دو رشته
word1وword2داده شدهاند. حداقل تعداد عملیات Insert، Delete و Replace را برای تبدیلword1بهword2پیدا کنید.
مثلاً:
word1 = horse
word2 = rosپاسخ:
3این مسئله نمونه کلاسیک Dynamic Programming است.
چرا Greedy بهسادگی جواب نمیدهد؟
ممکن است تصور کنیم در هر mismatch بهترین عملیات فعلی را انتخاب کنیم.
اما یک انتخاب محلی میتواند روی ادامه رشته اثر بگذارد.
به همین دلیل باید حالتهای مختلف Insert، Delete و Replace را مقایسه کنیم.
Dynamic Programming نتیجه مسائل کوچکتر را ذخیره میکند تا بهترین مسیر کلی پیدا شود.
اشتباهات رایج در پیادهسازی
اشتباه در Indexها
در جدول DP، موقعیت i معمولاً مربوط به:
a[i - 1]است، زیرا Row صفر برای رشته خالی استفاده شده است.
فراموش کردن Base Caseها
سطر و ستون اول باید به شکل صحیح مقداردهی شوند.
اضافه کردن هزینه هنگام Match
اگر دو کاراکتر برابر باشند:
dp[i][j] = dp[i - 1][j - 1]و نباید 1 اضافه کنیم.
اشتباه گرفتن Insert و Delete
درک معنای Cellهای مجاور بسیار مهم است:
up → Delete
left → Insert
diagonal → Replaceچه زمانی Levenshtein انتخاب خوبی نیست؟
اگر میلیونها رشته را بخواهیم با یک Query مقایسه کنیم، اجرای مستقیم Edit Distance برای همه آنها ممکن است بسیار گران باشد.
در سیستمهای Search واقعی معمولاً ابتدا Candidateها با روشهایی مانند:
- Indexing
- Prefix Filtering
- N-grams
- Trigrams
- Search Engines
محدود میشوند و سپس Similarity دقیقتر محاسبه میشود.
بنابراین Levenshtein یک ابزار مهم است، اما همیشه نباید آن را روی کل Dataset به صورت Brute Force اجرا کرد.
جمعبندی
Levenshtein Distance حداقل تعداد عملیات Insert، Delete و Replace برای تبدیل یک رشته به رشته دیگر را محاسبه میکند.
نسخه استاندارد آن یکی از مثالهای کلاسیک Dynamic Programming است.
Operations:
Insert
Delete
Replace
Time: O(n × m)
Space: O(n × m)و با بهینهسازی حافظه میتوان Space را به:
O(min(n, m))کاهش داد.
مهمترین نکته این است که Levenshtein فقط یک الگوریتم دانشگاهی نیست؛ مفاهیم آن در Spell Checking، Fuzzy Search، CRM Deduplication، Data Cleaning، NLP، OCR و بسیاری از سیستمهای واقعی کاربرد دارند.