Есть две строки как проверить, что из одной можно сделать другую при помощи вставки, удаления или замены не более чем одного символа?
3 Answers
Минимальное количество операций вставки, удаления и замены символа, необходимое для преобразования одной строки в другую, называется расстоянием Левенштейна. В общем случае оно вычисляется за O(n*m), где n и m - длины строк.
Однако, в данном случае требуется не вычислить количество действий, а проверить, возможно ли сделать преобразование за 0 (строки изначально одинаковые) или 1 действие.
Рассмотрим две строки. Выделим наидлиннейший одинаковый префикс и наидлиннейший одинаковый суффикс (допускается, что один или оба найденных куска могут быть пустыми). Очевидно, что в этих кусках нам ничего менять не требуется. Центральной частью строки назовём участок между префиксом и суффиксом.
Рассмотрим несколько возможных ситуаций:
- (1) Префикс и суффикс совпадают со всей строкой. Строки были изначально одинаковы.
- Конкатенация префикс и суффикса даёт одну из строк. В таком случае для того, чтобы преобразование было возможно, необходимо и достаточно, чтобы центральная часть другой строки состояла из одного символа. Понадобится операция вставки или удаления.
- Центральная часть обеих строк состоит из одного символа. Нужна операция замены.
- Центральная часть хотя бы одной из строк состоит из нескольких символов. Преобразование невозможно.
- (2) Т. о. для различных строк необходимо и достаточно, чтобы центральная часть каждой из строк была не длиннее одного символа.
Несколько преобразуем этот алгоритм:
- Найдём индекс
S, указывающий на следующий символ за одинаковым префиксом (возможно, длина строки плюс 1). - Найдём индексы
E1иE2, указывающие на символ, предшествующий одинаковому суффиксу в первой и второй строках соответственно (возможно, -1). Они могут различаться, т. к. длина строк может быть разной. - В случае 1 получим
S=Length+1, E1=-1, E2=-1. - Центральная часть нулевой длины означает, что указатель конца будет стоять раньше указателя начала на 1 символ.
Центральная часть из одного символа будет представлена равными указателями конца и начала.
Во всех остальных случаях указатель конца будет больше. - Т. о. для (2) условие превратится в такое:
Для обеих строк указатель конца не превосходит указателя начала. - Заметим, что для одинаковых строк это условие так же выполняется.
- Если язык позволяет, то можно (в качестве дополнительной оптимизации) сначала проверить, что длины строк отличаются не более чем на 1.
А теперь в коде:
Public Function AreSimilar(ByVal Str1 As String, ByVal Str2 As String) As Boolean
If Math.Abs(Str1.Length - Str2.Length) > 1 Then Return False
Dim S As Integer = 0
Dim E1 As Integer = Str1.Length - 1, E2 As Integer = Str2.Length - 1
Dim MinLength As Integer = Math.Min(E1 + 1, E2 + 1)
Do While S < MinLength AndAlso Str1(S) = Str2(S)
S += 1
Loop
Do While Not E1 AndAlso Not E2 AndAlso Str1(E1) = Str2(E2)
E1 -= 1
E2 -= 1
Loop
Return E1 <= S And E2 <= S
End Function
Асимптотика линейная. В худшем случае (при одинаковых строках) делается по два полных прохода по каждой из строк.
- 123,725
-
В языках, наследниках C длина строки определяется только по наличию 0 в конце, поэтому получение длины строки = полному проходу по ней – Mike Jan 01 '16 at 14:40
-
@Mike 0. Только с Си и Си++. В других сиподобных - нет.
- Даже если для длины нужен проход по строке, это не отменяет линейности.
- Плюс, мы можем проверить в первом цикле
str1[s], если конец не достигнут, то узнать длину какs+strlen(str+s), получив за один проход и длину префикса, и длину строки. Таким образом сохраним не более двух проходов.
-
@Mike, в таких языках длина вполне может передаваться параметром в функцию – Grundy Jan 01 '16 at 14:51
-
@Grundy, ещё раз: в таких языках мне не нужна длина строки до второго цикла. Соответственно первый цикл и определение длины дадут один проход. – Qwertiy Jan 01 '16 at 14:53
-
2
-
-
@YuriNegometyanov, а что не так с ним? Нормально работает - один юникодный символ же. – Qwertiy Jan 03 '16 at 04:04
-
-
1
Алгоритм для случая, когда длины строк заранее не известны и в общем случае вообще не доступны, т.е. это потоки байт или файлы целиком не умещающиеся в памяти.
int teststr(char *s1,char *s2)
{
int k0=1,k1=1,k2=1;
while(*s1==*s2 && *s1) {s1++; s2++;} // Ищем первое несовпадение
char o1,o2;
if(!*s1 && !*s2) return 1; // Строки равны
do {
o1=*s1++; o2=*s2++;
k0=k0 && c1==c2;
k1=k1 && c1==o2;
k2=k2 && c2==o1;
if(!(k0 | k1 | k2)) return 0; // Обнаружена ошибка (строки не преобразуемы)
} while(o1 && o2);
if( (!o1 && *s2) || (!o2 && *s1) ) return 0; // Расхождение длин строк более чем на 1
return 2; // Строки преобразуемы за одно действие
}
Работает за один прямой проход строки до конца (если строки равны) или до первой обнаруженной ошибки. Т.е. максимальная сложность O(n).
Сначала идет сканирование строк до первого несовпадения. После этого начинается цикл идущий от точки несовпадения. Он сравнивает текущие символы двух строк, они равны в случае если предыдущее расхождение было заменой символа. А так же текущий символ одной строки с предыдущим символом другой строки, они равны в случаях если один символ был вставлен или удален и следовательно произошел сдвиг одной из строк. Одно из этих равенств должно сохранятся от точки когда было встречено первое расхождение и до конца строк.
Для любителей UTF-8 привожу полный код, нормально разбирающий данную кодировку с символами разной длины. Замечу, что он поймет любые символы, включая китайские иероглифы и т.п. Если нужна кодировка с символами фиксированной длины, большей чем байт, т.е. например UTF-16, то это решается проще, заменой char на short в предыдущем коде.
#include <stdio.h>
unsigned int utf32(char **s)
{
unsigned char b=(unsigned char)**s;
unsigned int code=0;
int n=-1;
if(b) (*s)++;
if(! (b & 0x80) ) return b;
b<<=1;
while(b & 0x80) {b<<=1; n+=5; code=code<<6|**s & 0x3F; (*s)++;}
code|= b << n;
return code;
}
int teststr(char *s1,char *s2)
{
int k0=1,k1=1,k2=1;
unsigned int c1,c2,o1,o2;
do {c1=utf32(&s1); c2=utf32(&s2);} while(c1==c2 && c1);
if(!c1 && !c2) return 1;
do {
o1=c1; o2=c2;
c1=utf32(&s1); c2=utf32(&s2);
k0=k0 && c1==c2;
k1=k1 && c1==o2;
k2=k2 && c2==o1;
if(!(k0 | k1 | k2)) return 0;
} while(o1 && o2);
if( (!o1 && c2) || (!o2 && c1) ) return 0;
return 1;
}
int main()
{
char *s1="Тестюabc";
char *s2="ТестЦюabc";
if(teststr(s1,s2)) printf("True\n");
else printf("False\n");
}
- 44,087
-
-
Почему именно 16, а не 8 ? C 16 юникодом то все как раз просто, вместо char * делается short * и весь код работает. C utf-8 конечно отдельная песня, потому как надо учитывать длину символов. но в общем то это то же поправимо. Например предварительной конвертацией в utf-32, разумеется то же на лету. Основной смысл показать сам алгоритм достижения цели простыми средствами, без использования специализированных функций. – Mike Jan 02 '16 at 22:35
-
Я говорю об универсальности - чтобы автору не приходилось допиливать код при полной ясности с алгоритмом., – Yuri Negometyanov Jan 03 '16 at 10:18
-
Код - не цель. Код - что бы показать алгоритм. Учитывая задачу, она скорее академическая, а не практическая. Если я сюда докручу поддержку utf8, то это усложнит алгоритм и будет сложнее увидеть в нем то, что он демонстрирует. Цел же - не написать его на конкретном языке, а показать принцип решения – Mike Jan 03 '16 at 10:23
-
Речь не о языке программирования, а о локализации. И это не мелочь. – Yuri Negometyanov Jan 03 '16 at 10:26
-
Ладно, чуть позже добавлю код для utf8. Но ваш способ для php тем более по локализации не выдерживает никаких испытаний. Поддерживает только русский. А я тут сталкивался с проблемой при подобных конвертациях. В интернете часто многоточие пишут в виде специального UTF символа и в cp1251 он не представлен, так что у вас та конвертация не пройдет и мне сложно сказать как вообще ваш код отработает – Mike Jan 03 '16 at 10:37
-
-
+=- теоретически, int может переполняться. Там вроде&&на самом деле нужно? – Qwertiy Jan 10 '16 at 20:20 -
А любителей локализации могу порадовать тем, что не любой набор символов может быть корректно преобразован из одной кодировки в другую, даже если обе полностью поддерживают юникод. – Qwertiy Jan 10 '16 at 20:22
-
@Qwertiy Я надеялся никто не заметит переполнения :) а то еще условие добавлять, что когда
chподходит к 2 млрд синхронно из всех счетчиков эти лярды вычитать. А код и так сложней, чем хотелось бы. А&&там это где ? вроде где стоят - везде необходимы. Да, с кодировками конечно всегда беда, только вчера про составноййтут на ruSO прочитал ... – Mike Jan 10 '16 at 20:31 -
Я имел в виду сделать так:
k0=true;...k0 = k0 && c1==c2;. Ну и с остальными аналогично. – Qwertiy Jan 10 '16 at 20:34 -
@Qwertiy А знаете да, уже не нужны. У меня с ними проблемы были в начале разработки, когда первого цикла поиска первого несовпадения не было и я отсчет начинал в нужный момент в пределах единого цикла. Та версия сюда не попадала потому как была более громоздкой. сейчас поправлю второй вариант под это – Mike Jan 10 '16 at 20:51
-
-
@Qwertiy Я пока не понял зачем. но когда я написал
k0 = k0 && c1!=c2;появились ложные срабатывания на тесте, а с if нет. Пока так внес для скорости. Сам не понимаю, почему оно могло не сработать – Mike Jan 10 '16 at 21:00 -
-
@Qwertiy Глюки какие то, может просто неаккуратно что то переносил. Все ok в общем. – Mike Jan 10 '16 at 21:05
-
Скобки зачем? Что все так любят лишние скобки ставить :( А использование
+вместо||- это такая оптимизация? – Qwertiy Jan 10 '16 at 21:08 -
@Qwertiy Скобки - привычка, появившаяся лет 20 назад с одним кривым компилятором :) Да, сложение для оптимизации, незачем напрягать предсказатель переходов. Да и как то оно лаконичнее. Сделаю ка я битовый OR :) Надо второй код поправить, раз пошла такая пьянка ... я более лаконичный utf32 сделал :))) – Mike Jan 10 '16 at 21:15
-
В PHP есть стандартная процедура levenshtein(), которая работает только с однобайтовыми кодировками. Но это поправимо.
Программа:
function near_cyr($s1,$s2){
$str1 = $s1;
$str2 = $s2;
$str1 = mb_convert_encoding($str1, "CP1251");
$str2 = mb_convert_encoding($str2, "CP1251");
return ((levenshtein($s1,$s2) == 1)
|| (levenshtein($str1,$str2) == 1)) ? "рядом" : "не рядом";
};
printf("<br>%s ? %s - %s", $s1 = "карма", $s2 = "жара",
near_cyr ($s1, $s2));
printf("<br>%s ? %s - %s", $s1 = "карма", $s2 = "карман",
near_cyr ($s1, $s2));
printf("<br>%s ? %s - %s", $s1 = "карма", $s2 = "кара",
near_cyr ($s1, $s2));
printf("<br>%s ? %s - %s", $s1 = "карма", $s2 = "карта",
near_cyr ($s1, $s2));
Результаты:
карма ? жара - не рядом карма ? карман - рядом карма ? кара - рядом карма ? карта - рядом
- 5,198
-
Если уж вы заговорили о юникоде 16 - то как ваш код разберет не русские символы, которые в принципе не кодируются в cp1251 ? И суть вопроса с тегом "алгоритм" показать как вообще в программировании решают такие задачи, т.е. как устроены внутри во эти самые функции типа
levenshtein. – Mike Jan 02 '16 at 22:40 -
Я только что посмотрел исходники PHP, функция Левенштейна там реализована классическим общим алгоритмом. Сложность O(n*m), что явно дорого для решения задачи поставленной в вопросе, которая решается за O(n), или max O(2n) для примера с заходом с двух сторон. Плюс ко всему, функция в php не работает со строками >255 символов. – Mike Jan 02 '16 at 23:00
-
@Mike Сложность O(n*m) при m=1 меня не пугает. Ограничение по количеству символов (<256) действительно следовало оговорить. В то же время стандартная реализация оставляет возможность со временем перейти на m>1. и это тоже цель. – Yuri Negometyanov Jan 03 '16 at 10:13
-
2Я сделал полностью универсальный код для UTF-8, как говорилось выше, он работает со строками любой длины и понимает китайские иероглифы. Раз локализация - это важно, то важно что бы код понимал любые кодировки. Доделайте ваш алгоритм для работы со строками произвольной длины и любыми кодировками – Mike Jan 03 '16 at 11:41
-
@Mike Благодарю за интересное обсуждение. Я хотел осветить вопрос локализации стандартной функции, чтобы не заниматься изобретением велосипеда. Думаю, что наше обсуждение помогло увидеть ограничения обоих подходов и улучшить Ваш вариант программы. Важно то, что теперь появился выбор между "ленивым" (моим) и оптимизированным (Вашим) подходами. Считаю, что возможности стандартной функции практически исчерпаны. – Yuri Negometyanov Jan 03 '16 at 12:25