Як перевірити, чи рядок повністю виконаний з тієї ж підрядки?


128

Мені потрібно створити функцію, яка займає рядок, і вона повинна повертатися trueабо falseбазуватися на тому, чи вхід складається з повторюваної послідовності символів. Довжина заданого рядка завжди більша, 1і послідовність символів повинна мати принаймні одне повторення.

"aa" // true(entirely contains two strings "a")
"aaa" //true(entirely contains three string "a")
"abcabcabc" //true(entirely containas three strings "abc")

"aba" //false(At least there should be two same substrings and nothing more)
"ababa" //false("ab" exists twice but "a" is extra so false)

Я створив наступну функцію:

function check(str){
  if(!(str.length && str.length - 1)) return false;
  let temp = '';
  for(let i = 0;i<=str.length/2;i++){
    temp += str[i]
    //console.log(str.replace(new RegExp(temp,"g"),''))
    if(!str.replace(new RegExp(temp,"g"),'')) return true;
  }
  return false;
}

console.log(check('aa')) //true
console.log(check('aaa')) //true
console.log(check('abcabcabc')) //true
console.log(check('aba')) //false
console.log(check('ababa')) //false

Перевірка цього є частиною реальної проблеми. Я не можу дозволити собі таке неефективне рішення, як це. Перш за все, це прокручування половини рядка.

Друга проблема полягає в тому, що він використовує replace()в кожному циклі, що робить його повільним. Чи є краще рішення щодо продуктивності?


19
Це посилання може бути корисним для вас. Я завжди вважаю geekforgeeks гарним джерелом проблем алгоритмів - geeksforgeeks.org/…
Leron_says_get_back_Monica

9
Ви не заперечуєте, якщо я позичу це і зроблю це завданням кодування на сайті біржі програмування Golf?
ouflak

7
@ouflak ти можеш це зробити.
Maheer Ali

12
У випадку, якщо ваш цікавий, codegolf.stackexchange.com/questions/184682/…
ouflak

24
@Shidersz Використання нейронних мереж для цього трохи схоже на використання гармати для стрілянини в комара.
JAD

Відповіді:


186

Існує чудова теорема про такі струни.

Рядок складається з одного і того ж шаблону, повтореного кілька разів, якщо і лише тоді, коли рядок є нетривіальним обертанням.

Тут обертання означає видалення деякої кількості символів з передньої частини рядка та переміщення їх на задню частину. Наприклад, рядок helloможна повернути, щоб утворити будь-який з цих рядків:

hello (the trivial rotation)
elloh 
llohe 
lohel 
ohell 

Щоб зрозуміти, чому це працює, спочатку припустимо, що рядок складається з k повторних копій рядка w. Тоді видалення першої копії повторного шаблону (w) з передньої частини рядка та наклеювання її на задню сторону поверне ту саму нитку. Зворотний напрямок трохи складніше для доказування, але ідея полягає в тому, що якщо ви обертаєте рядок і повертаєте те, з чого ви почали, ви можете застосовувати це обертання кілька разів, щоб нанести плитку на рядок з декількома копіями одного шаблону (цей шаблон є рядок, який потрібно було перемістити до кінця, щоб зробити обертання).

Тепер питання полягає в тому, як перевірити, чи це так. Для цього є ще одна прекрасна теорема, яку ми можемо використовувати:

Якщо x і y - це рядки однакової довжини, то x - це обертання y, якщо і тільки якщо x є підрядком yy.

Як приклад, ми можемо бачити, що lohelце обертання helloнаступним чином:

hellohello
   ^^^^^

У нашому випадку ми знаємо, що кожен рядок x завжди буде підрядком xx (він з’явиться двічі, один раз у кожній копії x). Тому в основному нам просто потрібно перевірити, чи є наш рядок x підрядком xx, не дозволяючи йому збігатися з першим або на півдорозі символом. Ось один вкладиш для цього:

function check(str) {
    return (str + str).indexOf(str, 1) !== str.length;
}

Припускаючи indexOf, що реалізовано за допомогою алгоритму зіставлення швидких рядків, це буде працювати в часі O (n), де n - довжина вхідної рядка.

Сподіваюся, це допомагає!


13
Дуже хороша! Я додав його на сторінку еталону jsPerf .
user42723

10
@ user42723 Класно! Схоже, це справді, дуже швидко.
templatetypedef

5
FYI: Мені важко було повірити в це речення, поки я не змінив формулювання: "Рядок - це нетривіальне обертання саме тоді, і лише тоді, коли воно складається з тієї ж схеми, повтореної кілька разів". Піди розберися.
Аксель Подель

11
Чи маєте ви посилання на ці теореми?
грн.

4
Я думаю, що перше твердження те саме, що " Лема 2.3 : Якщо x і обертання x рівні, то x - повторення" на doi.org/10.1016/j.tcs.2008.04.020 . Дивіться також: stackoverflow.com/a/2553533/1462295
BurnsBA

67

Ви можете це зробити за допомогою групи захоплення та зворотної референції . Просто перевірте це повторення першого захопленого значення.

function check(str) {
  return /^(.+)\1+$/.test(str)
}

console.log(check('aa')) //true
console.log(check('aaa')) //true
console.log(check('abcabcabc')) //true
console.log(check('aba')) //false
console.log(check('ababa')) //false

У наведеному вище RegExp:

  1. ^і $стоїть на стартових і кінцевих якорях для прогнозу позиції.
  2. (.+)фіксує будь-який візерунок і фіксує значення (крім \n).
  3. \1є зворотним відношенням першого захопленого значення і \1+перевірить на повторність захопленого значення.

Пояснення Regex тут

Для налагодження RegExp використовуйте: https://regex101.com/r/pqlAuP/1/debugger

Виступ: https://jsperf.com/reegx-and-loop/13


2
Чи можете ви пояснити нам, що ця лінія робить поверненням /^(.+)\1+$/.test(str)
Thanveer Shah

34
Також у чому полягає складність цього рішення? Я не зовсім впевнений, але це, здається, не набагато швидше, ніж той, що має ОП.
Leron_says_get_back_Monica

8
@PranavCBalan Я не добре в алгоритмах, тому пишу в розділі коментарів. Однак я маю зазначити декілька речей - в ОП вже є діюче рішення, тому він просить той, який дасть йому кращі показники, і ви не пояснили, як ваше рішення буде перевершувати його. Коротше не означає швидше. Крім того, за посиланням, яке ви дали: If you use normal (TCS:no backreference, concatenation,alternation,Kleene star) regexp and regexp is already compiled then it's O(n).але як ви писали, ви використовуєте зворотній зв'язок, так це все ще O (n)?
Leron_says_get_back_Monica

5
Ви можете використовувати [\s\S]замість того, .якщо вам потрібно відповідати символи нового рядка так само, як і інші символи. Символ крапки не відповідає новому рядку; альтернативний пошук усіх символів пробілу та непробілу, що означає, що у відповідність включаються нові рядки. (Зверніть увагу, що це швидше, ніж інтуїтивніше (.|[\r\n]).) Однак, якщо рядок точно не містить нових рядків, то простий .буде найшвидшим. Зауважте, що це буде набагато простіше, якщо реалізований прапор dotall .
HappyDog

2
Хіба не /^(.+?)\1+$/трохи швидше? (12 кроків проти 20 кроків)
онлайн Томас

29

Мабуть, найшвидший алгоритмічний підхід - це побудова Z-функції за лінійним часом:

Z-функція для цього рядка - це масив довжиною n, де i-й елемент дорівнює найбільшій кількості символів, починаючи з положення i, що збігаються з першими символами s.

Іншими словами, z [i] - це довжина найдовшого загального префікса між s та суфіксом s, що починається з i.

С ++ реалізація для довідки:

vector<int> z_function(string s) {
    int n = (int) s.length();
    vector<int> z(n);
    for (int i = 1, l = 0, r = 0; i < n; ++i) {
        if (i <= r)
            z[i] = min (r - i + 1, z[i - l]);
        while (i + z[i] < n && s[z[i]] == s[i + z[i]])
            ++z[i];
        if (i + z[i] - 1 > r)
            l = i, r = i + z[i] - 1;
    }
    return z;
}

Реалізація JavaScript
Додані оптимізації - створення половини z-масиву та раннього виходу

function z_function(s) {
  var n = s.length;
  var z = Array(n).fill(0);
  var i, l, r;
  //for our task we need only a half of z-array
  for (i = 1, l = 0, r = 0; i <= n/2; ++i) {
    if (i <= r)
      z[i] = Math.min(r - i + 1, z[i - l]);
    while (i + z[i] < n && s[z[i]] == s[i + z[i]])
      ++z[i];

      //we can check condition and return here
     if (z[i] + i === n && n % i === 0) return true;
    
    if (i + z[i] - 1 > r)
      l = i, r = i + z[i] - 1;
  }
  return false; 
  //return z.some((zi, i) => (i + zi) === n && n % i === 0);
}
console.log(z_function("abacabacabac"));
console.log(z_function("abcab"));

Тоді вам потрібно перевірити індекси, iякі ділять n. Якщо ви виявите, iщо i+z[i]=nструна sможе бути стиснута до довжини iі ви можете повернутися true.

Наприклад, для

string s= 'abacabacabac'  with length n=12`

z-масив є

(0, 0, 1, 0, 8, 0, 1, 0, 4, 0, 1, 0)

і ми можемо знайти це для

i=4
i+z[i] = 4 + 8 = 12 = n
and
n % i = 12 % 4 = 0`

тому sможе бути представлений у вигляді підрядка довжиною 4, повтореного три рази.


3
return z.some((zi, i) => (i + zi) === n && n % i === 0)
Пранів С Балан

2
Дякуємо за те, що додали матеріали JavaScript до Salman A та Pranav C Balan
MBo

1
Альтернативний підхід, уникаючи додаткової ітераціїconst check = (s) => { let n = s.length; let z = Array(n).fill(0); for (let i = 1, l = 0, r = 0; i < n; ++i) { if (i <= r) z[i] = Math.min(r - i + 1, z[i - l]); while (i + z[i] < n && s[z[i]] == s[i + z[i]]) ++z[i]; // check condition here and return if (z[i] + i === n && n % i === 0) return true; if (i + z[i] - 1 > r) l = i, r = i + z[i] - 1; } // or return false return false; }
Pranav C Balan

2
Використання z-функції є хорошою ідеєю, але це "інформація-важка", вона містить багато інформації, яка ніколи не використовується.
Аксель Подель

@Axel Podehl Тим не менш, вона обробляє рядок в O (n) час (кожен знак використовується не більше двох разів). У будь-якому випадку ми повинні перевірити кожну таблицю, щоб не було теоретично більш швидкого алгоритму (хоча оптимізовані вбудовані методи можуть перевершити). Також в останньому редагуванні я обмежив обчислення на 1/2 довжини рядка.
MBo

23

Я прочитав відповідь gnasher729 і здійснив її. Ідея полягає у тому, що якщо є якісь повторення, то має бути (також) проста кількість повторень.

function* primeFactors (n) {
    for (var k = 2; k*k <= n; k++) {
        if (n % k == 0) {
            yield k
            do {n /= k} while (n % k == 0)
        }
    }
    if (n > 1) yield n
}

function check (str) {
    var n = str.length
    primeloop:
    for (var p of primeFactors(n)) {
        var l = n/p
        var s = str.substring(0, l)
        for (var j=1; j<p; j++) {
            if (s != str.substring(l*j, l*(j+1))) continue primeloop
        }
        return true
    }
    return false
}

Трохи інший алгоритм такий:

function check (str) {
    var n = str.length
    for (var p of primeFactors(n)) {
        var l = n/p
        if (str.substring(0, n-l) == str.substring(l)) return true
    }
    return false
}

Я оновив сторінку jsPerf, яка містить алгоритми, які використовуються на цій сторінці.


Це здається дуже швидким, оскільки пропускає непотрібні чеки.
Pranav C Balan

1
Дуже приємно, тільки я думаю, що я перевірив би, що перша буква повторюється у вказаному місці перед тим, як робити виклики підрядків.
Бен

Для людей, які function*вперше натрапляють на таких, як я, це оголошення генератора, а не звичайної функції. Дивіться MDN
Жульєн Русе

17

Припустимо, рядок S має довжину N і складається з дублікатів підрядки s, тоді довжина s ділить N. Наприклад, якщо S має довжину 15, то підрядок має довжину 1, 3 або 5.

Нехай S складається з (p * q) копій s. Тоді S також робиться з p копій (s, повторених q разів). Отже, у нас є два випадки: Якщо N є простим або 1, то S може бути зроблений лише з копій підрядка довжиною 1. Якщо N складений, нам потрібно лише перевірити підрядки s довжиною N / p на числення p поділу довжина С.

Отже, визначте N = довжина S, а потім знайдіть усі основні фактори у часі O (sqrt (N)). Якщо є лише один коефіцієнт N, перевірте, чи S - однаковий рядок, повторений N разів, інакше для кожного основного фактора p перевірте, чи S складається з p повторів перших N / p символів.


Я не перевіряв інші рішення, але це здається дуже швидким. Ви можете залишити частину "Якщо є лише один фактор N, перевірте ..., інакше" для простоти, оскільки це не окремий випадок. Було б добре побачити реалізацію Javascript, яку можна запустити в jsPerf поруч з іншими реалізаціями.
user42723

1
Зараз я це реалізував у своїй відповіді
user42723,

10

Я думаю, що рекурсивна функція також може бути дуже швидкою. Перше спостереження полягає в тому, що максимальна повторювана довжина візерунка наполовину довша загальної рядки. І ми могли просто перевірити всі можливі повторювані довжини шаблону: 1, 2, 3, ..., ст.д. Довжина / 2

Рекурсивна функція є повторення (p, str) тестів, якщо ця схема повторюється у str.

Якщо str довший за малюнок, для повторної рекурсії потрібно, щоб перша частина (такої ж довжини, як p) була повторенням, а також решта str. Таким чином, str ефективно розбивається на шматки довжини p.length.

Якщо випробуваний малюнок і str мають однаковий розмір, рекурсія тут закінчується успішно.

Якщо довжина різна (трапляється для "аба" та шаблону "аб") або якщо шматки різні, то повертається помилка, поширюючи рекурсію.

function check(str)
{
  if( str.length==1 ) return true; // trivial case
  for( var i=1;i<=str.length/2;i++ ) { // biggest possible repeated pattern has length/2 characters

    if( str.length%i!=0 ) continue; // pattern of size i doesn't fit
    
    var p = str.substring(0, i);
    if( isRepeating(p,str) ) return true;
  }
  return false;
}


function isRepeating(p, str)
{
  if( str.length>p.length ) { // maybe more than 2 occurences

    var left = str.substring(0,p.length);
    var right = str.substring(p.length, str.length);
    return left===p && isRepeating(p,right);
  }
  return str===p; 
}

console.log(check('aa')) //true
console.log(check('aaa')) //true 
console.log(check('abcabcabc')) //true
console.log(check('aba')) //false
console.log(check('ababa')) //false

Виступ: https://jsperf.com/reegx-and-loop/13


1
Чи було б швидше перевірити if( str===p.repeat(str.length/i) ) return true;замість використання рекурсивної функції?
Хроноцид

1
Не ставте console.logs в тести на jsperf, підготуйте функції всередині розділу глобалів, а також підготуйте тестові рядки в розділі глобалів (вибачте, не можу редагувати jsperf)
Salman A

@Salman - хороший момент. Я щойно змінив jsperf від свого попередника (Pranav C), вперше застосував jsperf, cool tool.
Аксель Подель

@SalmanA: оновлено: jsperf.com/regex-and-loop/1 ... спасибі за інформацію ... навіть я не знайомий з цим (Jsperf) ... дякую за інформацію
Pranav C Balan

Привіт Салман, велике спасибі за jsperf.com/reegx-and-loop/10 - так, новий тест на парфу має набагато більше сенсу. Налаштування функцій повинно входити в код підготовки.
Аксель Подель

7

Написав це в Python. Я знаю, що це не платформа, але це зайняло 30 хвилин часу. PS => ПІТОН

def checkString(string):
    gap = 1 
    index= 0
    while index < len(string)/2:
        value  = [string[i:i+gap] for i in range(0,len(string),gap) ]

        x = [string[:gap]==eachVal for eachVal in value]

        if all(x):
            print("THEY ARE  EQUAL")
            break 

        gap = gap+1
        index= index+1 

checkString("aaeaaeaaeaae")

6

Мій підхід схожий на gnasher729 тим, що він використовує потенційну довжину підрядки як основний фокус, але він менш математичний і інтенсивний процес:

L: Довжина оригінального рядка

S: Потенційна довжина дійсних підрядів

Цикл S від (ціла частина) L / 2 до 1. Якщо L / S є цілим числом, перевірте свою початкову рядок проти кулаків S символів оригінальної рядки, повторених L / S разів.

Причиною циклу від L / 2 назад, а не від 1 далі, є отримання найбільшої можливої ​​підрядки. Якщо ви хочете найменший цикл підрядки від 1 до L / 2. Приклад: "abababab" містить як "ab", так і "abab" як можливі підрядки. Яке з двох було б швидше, якщо ви дбаєте лише про справжній / хибний результат, залежить від типу рядків / підрядів, до яких буде застосовано.


5

Наступний код Mathematica майже виявляє, чи список повторюється хоча б раз. Якщо рядок повторюється хоча б один раз, вона повертає true, але може також повернути true, якщо рядок є лінійною комбінацією повторюваних рядків.

IsRepeatedQ[list_] := Module[{n = Length@list},
   Round@N@Sum[list[[i]] Exp[2 Pi I i/n], {i, n}] == 0
];

Цей код шукає внесок "повна довжина", який повинен бути нуль у рядку, що повторюється, але рядок accbbdтакож вважається повторним, оскільки це сума двох повторених рядків abababі 012012.

Ідея полягає у використанні швидкого перетворення Фур'є та пошуку частотних спектрів. Дивлячись на інші частоти, слід виявити і цей дивний сценарій.


4

Основна ідея тут - вивчити будь-яку потенційну підрядку, починаючи з довжини 1 і зупиняючись на половині початкової довжини рядка. Ми дивимося лише на довжину підрядок, які поділяють початкову довжину рядка рівномірно (тобто str.length% substring.length == 0).

Ця реалізація розглядає перший символ кожної можливої ​​ітерації підрядки перед переходом до другого символу, що може заощадити час, якщо очікується, що підрядки будуть довгими. Якщо після вивчення всієї підрядки не виявлено невідповідності, ми повертаємо true.

Ми повертаємо помилкові, коли у нас закінчуються потенційні підрядки для перевірки.

function check(str) {
  const len = str.length;
  for (let subl = 1; subl <= len/2; ++subl) {
    if ((len % subl != 0) || str[0] != str[subl])
      continue;
    
    let i = 1;
    for (; i < subl; ++i)
    {
      let j = 0;
      for (; j < len; j += subl)
        if (str[i] != str[j + i])
          break;
      if (j != len)
        break;
    }
    
    if (i == subl)
      return true;
  }
  return false;
}

console.log(check('aa')) //true
console.log(check('aaa')) //true
console.log(check('abcabcabc')) //true
console.log(check('aba')) //false
console.log(check('ababa')) //false


-1

Я не знайомий з JavaScript, тому не знаю, наскільки швидко це буде, але ось лінійне часове рішення (за умови розумної вбудованої реалізації) з використанням лише вбудованих. Я опишу алгоритм у псевдокоді.

function check(str) {
    t = str + str;
    find all overlapping occurrences of str in t;
    for each occurrence at position i
        if (i > 0 && i < str.length && str.length % i == 0)
            return true;  // str is a repetition of its first i characters
    return false;
}

Ідея схожа на відповідь MBo. Для кожного, iякий ділить довжину, strє повторення його перших iсимволів, якщо і лише тоді, коли воно залишається таким же після зміщення для iсимволів.

Мені спадає на думку, що такий вбудований може бути недоступним або неефективним. У цьому випадку завжди можна вручну реалізувати алгоритм KMP , що займає приблизно стільки ж коду, скільки алгоритм у відповіді MBo.


ОП хоче знати, чи існує повторення . Другий рядок (тіло) вашої функції враховує кількість повторень - це той біт, який потрібно пояснити. Наприклад, "abcabcabc" має 3 повторення "abc", але як у вашому другому рядку вийшло, чи не було повторень?
Лоуренс

@Lawrence Я не розумію твого питання. Цей алгоритм заснований на ідеї про те , що рядок є повторенням її підрядка , якщо і тільки якщо для деякого подільника її довжини i, s[0:n-i] == s[i:n]або , що еквівалентно, s == s[i:n] + s[0:i]. Чому другий рядок потребує розробки, чи не було повторень?
infmagic2047

Дозвольте мені побачити, чи я розумію ваш алгоритм. Спочатку ви додаєте strсебе до форми t, а потім скануєте, tщоб спробувати знайти strвсередині t. Гаразд, це може спрацювати (я відмовився від свого голосу). Однак це не лінійно у strlen (str). Скажімо str, довжиною L. Потім у кожному положенні p = 0,1,2, ..., перевіряючи, чи str [0..L-1] == t [p..p + L-1] приймає O (L ) час. Вам потрібно зробити O (L) перевірку під час проходження значень p, значить, це O (L ^ 2).
Лоуренс

-10

Однією з простих ідей є заміна рядка на підрядку "", і якщо якийсь текст існує, то він хибний, інакше це правда.

'ababababa'.replace(/ab/gi,'')
"a" // return false
'abababab'.replace(/ab/gi,'')
 ""// return true


так, для abc або єдиноріг користувач не перевірить / abc / або / єдиноріг /, вибачте, якщо я пропускаю ваш контекст
Vinod kumar G

3
Питання може бути зрозумілішим, але те, що його задають, - це спосіб вирішити, чи строка повністю складається з 2 або більше повторень будь-якої іншої рядки. Це не пошук конкретної підрядки.
HappyDog

2
До питання я додав дещо роз’яснення, яке має зробити це тепер зрозумілішим.
HappyDog

@Vinod, якщо ви вже збираєтесь використовувати регулярний вираз, слід прив’язати відповідність і використовувати тест. Немає причин змінювати рядок просто для перевірки певної умови.
Марі
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.