文字列が完全に同じ部分文字列で構成されているかどうかを確認するにはどうすればよいですか?


128

文字列を受け取る関数を作成する必要があります。これは、入力が繰り返される文字シーケンスで構成されるかどうかに基づいて返されるか、trueそれにfalse基づいている必要があります。指定された文字列の長さは常により大きく1、文字シーケンスには少なくとも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

これのチェックは、実際の問題の一部です。このような非効率的なソリューションを買う余裕はありません。まず、文字列の半分をループします。

2番目の問題はreplace()、各ループで使用しているために遅くなることです。パフォーマンスに関するより良い解決策はありますか?


19
このリンクはあなたに役立つかもしれません。私は、アルゴリズムの問題のための良い情報源としてgeekforgeeksを常に見つける- geeksforgeeks.org/...
Leron_says_get_back_Monica

9
これを借りて、Programming Golf交換サイトでコーディングの課題にしてもいいですか。
ouflak

7
@ouflakできます。
Maheer Ali


24
@Shiderszこれにニューラルネットワークを使用すると、大砲を使用して蚊を撃つような感じがします。
JAD

回答:


186

このような文字列については、気の利いた小さな定理があります。

文字列は、その文字列が自明なローテーションである場合に限り、同じパターンで複数回繰り返されます。

ここで、回転とは、文字列の前面からいくつかの文字を削除し、それらを背面に移動することを意味します。たとえば、文字列helloを回転させて、次の文字列を作成できます。

hello (the trivial rotation)
elloh 
llohe 
lohel 
ohell 

これが機能する理由を確認するために、最初に、文字列が文字列wのk回繰り返されるコピーで構成されると仮定します。次に、繰り返しパターンの最初のコピー(w)を文字列の前面から削除し、背面に貼り付けると、同じ文字列が返されます。逆の方向を証明するには少しトリッキーですが、アイデアは、文字列を回転して最初の状態に戻す場合、その回転を繰り返し適用して、同じパターンの複数のコピーで文字列を並べることができます(そのパターンは回転を行うために最後まで移動する必要がある文字列)。

今問題はこれが事実であるかどうか確認する方法です。そのために、使用できる別の美しい定理があります。

xとyが同じ長さの文字列である場合、xがyyの部分文字列である場合に限り、xはyのローテーションです。

例として、次のlohelローテーションであることがわかりhelloます。

hellohello
   ^^^^^

私たちの場合、すべての文字列xは常にxxの部分文字列になることがわかっています(xのコピーごとに1回ずつ、2回表示されます)。したがって、基本的には、文字列xが最初の文字または途中の文字で一致することを許可せずに、xxの部分文字列であるかどうかを確認するだけです。以下がその一行です。

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

indexOf高速文字列照合アルゴリズムを使用して実装されていると仮定すると、これは時間O(n)で実行されます。ここで、nは入力文字列の長さです。

お役に立てれば!


13
非常に素晴らしい!これをjsPerfベンチマークページに追加しました。
user42723

10
@ user42723かっこいい!それは本当に、本当に速いようです。
templatetypedef

5
参考までに、「文字列は、同じパターンが複数回繰り返されている場合にのみ、文字列はそれ自体の自明な回転ではない」と言うまで、その文を信じるのに苦労しました。図を行きます。
Axel Podehl

11
それらの定理への言及はありますか?
HRK44

4
最初のステートメントは、doi.org / 10.1016 / j.tcs.2008.04.020の補題2.3:xとxの回転が等しい場合、xは繰り返し」と同じだと思います。次も参照してください: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+、キャプチャされた値の繰り返しをチェックします。

ここに正規表現の説明

RegExpデバッグの場合:https : //regex101.com/r/pqlAuP/1/debugger

パフォーマンス:https : //jsperf.com/reegx-and-loop/13


2
あなたは)この行が戻り/^(.+)\1+$/.test(strをやっている私たちに説明することができます
Thanveerシャー

34
また、このソリューションの複雑さは何ですか?私は完全には定かではありませんが、OPが持っているものよりもはるかに高速ではないようです。
Leron_says_get_back_Monica

8
@PranavCBalan私はアルゴリズムが得意ではないので、コメントセクションに書き込みます。ただし、言及すべきことがいくつかあります。OPには既に有効なソリューションがあるので、彼はより優れたパフォーマンスを提供できるソリューションを求めており、ソリューションがどのように彼のパフォーマンスを上回るかを説明していません。短いからといって速いというわけではありません。また、あなたが与えたリンクから: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ステップvs 20ステップ)
オンライントーマス

29

おそらく最速のアルゴリズムアプローチは、線形関数Z関数を作成することです。

この文字列のZ関数は、長さnの配列です。ここで、i番目の要素は、位置iから始まり、sの最初の文字と一致する最大文字数に等しくなります。

言い換えると、z [i]は、sとiで始まるsのサフィックスの間の最も長い共通のプレフィックスの長さです。

参照用のC ++実装:

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`

したがって、s3回繰り返された長さ4のサブストリングとして表すことができます。


3
return z.some((zi, i) => (i + zi) === n && n % i === 0)
Pranav C Balan

2
Salman AとPranav C BalanにJavaScriptを追加してくれてありがとう
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

@Axel Podehlそれでも、O(n)時間で文字列を処理します(各文字は最大2回使用されます)。いずれの場合でも、すべての文字をチェックする必要があるため、理論的にはより高速なアルゴリズムはありません(最適化された組み込みメソッドのパフォーマンスが向上する可能性があります)。また、最後の編集では、計算を文字列長の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
とても良いですが、部分文字列の呼び出しを行う前に、最初の文字が指定された場所で繰り返し発生することを確認すると思います。
Ben Voigt

function*私のように初めてつまずく人にとって、それはジェネレーターを宣言するためのものであり、通常の関数ではありません。MDNを
JulienRousé19年

17

文字列Sの長さがNであり、部分文字列sの複製で構成されていると仮定すると、sの長さがNを割ります。たとえば、Sの長さが15の場合、部分文字列の長さは1、3、または5です。

Sをsの(p * q)コピーで構成します。次に、Sも(s、q回繰り返される)のpコピーで構成されます。したがって、2つのケースがあります。Nが素数または1の場合、Sは長さ1の部分文字列のコピーでのみ作成できます。Nが複合の場合、素数pの分割について長さN / pの部分文字列sのみをチェックする必要があります。 Sの長さ

したがって、N = Sの長さを決定し、時間O(sqrt(N))におけるその素因数をすべて見つけます。因子Nが1つしかない場合、SがN回繰り返される同じ文字列であるかどうかを確認します。それ以外の場合は、各素因数pについて、Sが最初のN / p文字のp回の繰り返しで構成されるかどうかを確認します。


私は他の解決策をチェックしていませんが、これは非常に速いようです。これは特別なケースではないので、簡単にするために、「因子Nが1つしかない場合は、...をチェックし、それ以外の場合はチェックする」の部分を省略できます。他の実装の隣にあるjsPerfで実行できるJavascript実装を見るとよいでしょう。
user42723

1
私はこれを私の答えに
user42723

10

再帰関数も非常に高速かもしれません。最初の観察は、最大繰り返しパターン長が文字列全体の半分であるということです。そして、すべての可能な繰り返しパターン長をテストすることができます:1、2、3、...、str.length / 2

再帰関数isRepeating(p、str)は、このパターンがstrで繰り返されるかどうかをテストします。

strがパターンよりも長い場合、再帰では、最初の部分(pと同じ長さ)が繰り返しであり、strの残りの部分である必要があります。したがって、strは事実上、長さp.lengthの断片に分割されます。

テストしたパターンとstrのサイズが等しい場合、再帰はここで正常に終了します。

長さが異なる場合( "aba"とパターン "ab"に起こります)、または断片が異なる場合、falseが返され、再帰が伝播します。

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
jsperfテストにconsole.logsを入れないでください。グローバルセクション内で関数を準備し、グローバルセクションでテスト文字列も準備してください(申し訳ありませんが、jsperfを編集できません)
Salman A

@サルマン-良い点。先代(Pranav C)からjsperfを変更したばかりで、初めてjsperfというクールなツールを使用しました。
Axel Podehl

@SalmanA:更新:jsperf.com/regex-and-loop/1 ...情報に感謝...私も知らない(Jsperf)...情報に感謝
Pranav C Balan

こんにちはSalmanさん、jsperf.com / reegx - and - loop / 10に感謝し ます -はい、新しいperfテストはもっと理にかなっています。関数の設定は準備コードに入る必要があります。
Axel Podehl

7

これをPythonで書いた。私はそれがプラットフォームではないことを知っていますが、それは30分の時間がかかりました。PS => PYTHON

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:有効なサブストリングの潜在的な長さ

L / 2から1までの整数Sをループします。L/ Sが整数の場合は、元の文字列の最初のS文字に対して元の文字列をチェックし、L / S回繰り返します。

L / 2からループバックして1からループバックしないのは、可能な限り最大のサブストリングを取得するためです。1からL / 2までの可能な限り小さい部分文字列ループが必要な場合。例:「abababab」には、可能なサブストリングとして「ab」と「abab」の両方があります。true / falseの結果のみを考慮する場合、どちらが高速になるかは、これが適用される文字列/部分文字列のタイプによって異なります。


5

以下のMathematicaコードは、リストが少なくとも1回繰り返されるかどうかをほぼ検出します。文字列が少なくとも1回繰り返される場合はtrueを返しますが、文字列が繰り返し文字列の線形結合である場合もtrueを返す場合があります。

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

このコードは、「完全長」の寄与を探します。これは繰り返し文字列ではゼロでなければなりませんが、文字列accbbdは2つの繰り返し文字列abababとの合計であるため、繰り返しと見なされます012012

アイデアは、高速フーリエ変換を使用して、周波数スペクトルを探すことです。他の周波数を見ると、この奇妙なシナリオも検出できるはずです。


4

ここでの基本的な考え方は、長さ1から始まり、元の文字列の長さの半分で停止する、潜在的な部分文字列を調べることです。元の文字列の長さを均等に分割する部分文字列の長さ(つまり、str.length%substring.length == 0)のみを調べます。

この実装は、2番目の文字に移動する前に、可能な各部分文字列反復の最初の文字を確認します。これにより、部分文字列が長いと予想される場合に時間を節約できます。部分文字列全体を調べても不一致が見つからない場合は、trueを返します。

チェックする潜在的なサブストリングが不足すると、falseを返します。

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の回答のアルゴリズムとほぼ同じ量のコードを使用します。


OPは繰り返しが存在するかどうかを知りたいです。関数(の本体)の2行目は繰り返しの数を数えます-それは説明する必要があるビットです。例えば「ABC」の3回の繰り返しを持っていますが、どのようにあなたのセカンドライン作業を行った「ABCABCABC」かどうか、それはすべての繰り返しを持っていましたか?
ローレンス

@ローレンス私はあなたの質問を理解していません。このアルゴリズムは、文字列がある場合にのみ、その長さのいくつかの除数のためならばその部分文字列の繰り返しであるという考えに基づいているis[0:n-i] == s[i:n]、、または同等s == s[i:n] + s[0:i]。なぜ2行目に繰り返しがあるかどうかを調べる必要があるのですか?
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 )時間。pの値を調べるときにO(L)チェックを行う必要があるため、O(L ^ 2)になります。
ローレンス

-10

単純なアイデアの1つは、文字列を ""の部分文字列に置き換えることです。テキストが存在する場合は偽であり、それ以外の場合は真です。

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


はい、abcまたはunicornの場合、ユーザーは/ abc /または/ unicorn /でチェックしません。コンテキストがない場合は申し訳ありません
Vinod kumar G

3
質問はもっと明確になるかもしれませんが、求められているのは、文字列が他の文字列の2回以上の繰り返しで完全に構成されているかどうかを判断する方法です。特定の部分文字列を検索するのではありません。
HappyDog

2
質問にいくつかの明確化を追加しました。これにより、より明確になるはずです。
HappyDog

@Vinod既に正規表現を使用する場合は、一致をアンカーしてテストを使用する必要があります。何らかの条件を検証するためだけに文字列を変更する理由はありません。
マリー
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.