文字列を受け取る関数を作成する必要があり、入力が繰り返し文字シーケンスで構成されているかどうかに基づいて、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()
を使用しているため、速度が遅くなることです。パフォーマンスに関してより良い解決策はありますか?
これらの文字列については、気の利いた小さな定理があります。
文字列は、それ自体が自明でない回転である場合にのみ、複数回繰り返される同じパターンで構成されます。
ここで、回転とは、文字列の前からいくつかの文字を削除し、それらを後ろに移動することを意味します。たとえば、文字列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は入力文字列の長さです。
お役に立てれば!
キャプチャグループ および 後方参照 で実行できます。最初にキャプチャされた値の繰り返しであることを確認してください。
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
上記の正規表現では:
^
および$
は、位置を予測するための 開始および終了アンカー の略です。(.+)
はパターンをキャプチャし、値をキャプチャします(\n
)。\1
は最初にキャプチャされた値の後方参照であり、\1+
は、キャプチャされた値の繰り返しをチェックします。RegExpデバッグ用: https://regex101.com/r/pqlAuP/1/debugger
パフォーマンス: https://jsperf.com/reegx-and-loop/1
おそらく、最速のアルゴリズム的アプローチは、線形時間で Z-function を構築することです。
この文字列のZ関数は、長さnの配列です。ここで、i番目の要素は、sの最初の文字と一致する位置iから始まる文字の最大数に等しくなります。
つまり、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"));
次に、nを分割するインデックスi
をチェックする必要があります。そのようなi
が見つかったらi+z[i]=n
その後、文字列s
を長さi
に圧縮でき、true
を返すことができます。
たとえば、
string s= 'abacabacabac' with length n=12`
z-arrayは
(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回繰り返されるとして表される場合があります。
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ページ を更新しました。
文字列Sの長さはNで、部分文字列sの複製で構成され、sの長さでNを分割するとします。たとえば、Sの長さが15の場合、部分文字列の長さは1、3、または5です。
Sをsの(p * q)コピーで作成します。次に、Sは(s、q回繰り返される)のp個のコピーからも作成されます。したがって、2つの場合があります。Nが素数または1の場合、Sは長さ1の部分文字列のコピーのみで構成できます。Nが複合の場合、長さN/pの部分文字列sの素数p分割のみをチェックする必要がありますSの長さ.
したがって、N = Sの長さを決定し、時間Oのすべての素因数(sqrt(N))を見つけます。因子Nが1つだけの場合、SがN回繰り返される同じ文字列かどうかを確認し、そうでない場合は各素因数pについて、Sが最初のN/p文字のp回の繰り返しで構成されるかどうかを確認します。
再帰関数も非常に高速だと思います。最初の観察は、最大繰り返しパターン長が文字列全体の長さの半分であることです。そして、可能なすべての繰り返しパターン長をテストすることができます: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/1
これをPythonで書きました。プラットフォームではないことは知っていますが、実際には30分かかりました。 P.S。=> 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")
私のアプローチは、サブストリングの潜在的な長さを主な焦点として使用するという点で、gnasher729に似ていますが、数学的でなく、プロセス集約的です:
L:元の文字列の長さ
S:有効なサブストリングの潜在的な長さ
L/2(の整数部)からSをループします。L/ Sが整数の場合、元の文字列をL/S回繰り返された元の文字列の最初のS文字と照合します。
L/2から1からではなく逆方向にループする理由は、可能な最大のサブストリングを取得するためです。 1からL/2までの可能な限り小さい部分文字列ループが必要な場合。例:「abababab」には、可能なサブストリングとして「ab」と「abab」の両方があります。 true/falseの結果のみを気にする場合、2つのうちどちらが速いかは、これが適用される文字列/部分文字列のタイプによって異なります。
次のMathematicaコードalmostは、リストが少なくとも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
_の合計であるため、繰り返しも考慮されます。
アイデアは、高速フーリエ変換を使用して、周波数スペクトルを探すことです。他の周波数を見ると、この奇妙なシナリオも検出できるはずです。
ここでの基本的な考え方は、長さ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