Повторюйте рядок - Javascript


271

Який найкращий чи найкоротший спосіб повернення рядка, повтореного в довільній кількості разів?

Наступний мій найкращий знімок досі:

function repeat(s, n){
    var a = [];
    while(a.length < n){
        a.push(s);
    }
    return a.join('');
}

5
Понад 10 років тому було відоме моє рішення цієї проблеми, яке я використав як приклад у статті про оптимізацію JavaScript за пару місяців до того, як ви поставили це питання: webreference.com/programming/javascript/jkm3/3 .html Мабуть, більшість людей забули про цей код, і я не вважаю, що рішення нижче настільки добре, як моє. Найкращий алгоритм виглядає так, що його зняли з мого коду; за винятком нерозуміння того, як працює мій код, він робить один додатковий крок експоненціальної конкатенації, який усувається в моєму оригіналі спеціальним циклом.
Джозеф Майєрс

10
Ніхто не піднімав рішення Йосифа. Алгоритму 3700 років. Вартість додаткового кроку незначна. І ця стаття містить помилки та хибні уявлення про об'єднання рядків у Javascript. Для всіх, хто цікавиться, як Javascript справді обробляє струни внутрішньо, див. Мотузку .
artistoex

4
Здається, ніхто не помітив, що повторення прототипу String визначено та реалізовано, принаймні, у firefox.
kennebec

3
@kennebec: Так, це функція EcmaScript 6, якої не було, коли було задано це питання. Зараз це досить добре підтримується.
rvighne

3
@rvighne - я щойно перевірив kangax.github.io/compat-table/es6/#String.prototype.repeat, я б не вважав підтримку виключно з firefox та chrome як "досить добре підтримувану"
aaaaaa

Відповіді:


405

Зауважте новим читачам: ця відповідь стара і не дуже практична - вона просто "розумна", оскільки вона використовує масив масиву, щоб виконати String речі. Коли я писав "менше процесу", я напевно мав на увазі "менше коду", тому що, як зазначали інші у наступних відповідях, він виконує функцію "свиня". Тому не використовуйте його, якщо швидкість має значення для вас.

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

String.prototype.repeat = function( num )
{
    return new Array( num + 1 ).join( this );
}

alert( "string to repeat\n".repeat( 4 ) );

36
Я намагаюся не поширювати рідні предмети, але інакше це прекрасне рішення. Дякую!
brad

34
@ brad - чому б і ні? Ви б краще забруднити глобальний простір імен функцією, яка має досить чітко визначений будинок (об'єкт String)?
Пітер Бейлі

16
Насправді обидва ваші аргументи стосуються і глобального простору імен. Якщо я збираюсь розширити простір імен і матимуть потенційні зіткнення, я краще зроблю це 1) не в глобальному 2) у тому, що є релевантним, і 3) легко рефакторировать. Це означає розмістити його на прототипі String, а не в глобальному масштабі.
Пітер Бейлі

11
я б змінив цю функцію - поставити parseInt () навколо "num", оскільки якщо у вас є числовий рядок, ви можете отримати дивну поведінку через жонглювання типу JS. наприклад: "my string" .repeat ("6") == "61"
nickf

19
Якщо ви не хочете , щоб розширити власні об'єкти, ви могли б поставити функцію на строковому об'єкті , а не, як це: String.repeat = function(string, num){ return new Array(parseInt(num) + 1).join(string); };. Назвіть це так:String.repeat('/\', 20)
Znarkus

203

Я перевірив ефективність усіх запропонованих підходів.

Ось найшвидший у мене варіант .

String.prototype.repeat = function(count) {
    if (count < 1) return '';
    var result = '', pattern = this.valueOf();
    while (count > 1) {
        if (count & 1) result += pattern;
        count >>= 1, pattern += pattern;
    }
    return result + pattern;
};

Або як окрема функція:

function repeat(pattern, count) {
    if (count < 1) return '';
    var result = '';
    while (count > 1) {
        if (count & 1) result += pattern;
        count >>= 1, pattern += pattern;
    }
    return result + pattern;
}

Він заснований на алгоритмі artistoex . Це дійсно швидко. І чим більше count, тим швидше це проходить у порівнянні з традиційним new Array(count + 1).join(string)підходом.

Я змінив лише 2 речі:

  1. замінено pattern = thisна pattern = this.valueOf()(очищує одне очевидне перетворення типу);
  2. додано if (count < 1)перевірку від прототипу до верхньої частини функції, щоб виключити непотрібні дії в цьому випадку.
  3. застосовано оптимізацію з відповіді Денніса (5-7% прискорення)

UPD

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

змінна count~ 0 .. 100:

Діаграма виконання

константа count= 1024:

Діаграма виконання

Скористайтеся ним і зробіть це ще швидше, якщо зможете :)


4
Хороша робота! Я думаю, що count < 1справа справді непотрібна оптимізація.
JayVee

Відмінний алгоритм O (log N). Дякуємо за велику оптимізацію з valueOf ()
vp_arth

2
Посилання на зображення мертві.
Бенджамін Груенбаум

Посилання прекрасні. Може бути тимчасовою недоступністю
омріяно

Тестовий JSFiddle вже не працює правильно; здається, що просто продовжуйте виконувати першу функцію знову і знову (залишайте її працювати на півгодини, щоб бути впевненим)
RevanProdigalKnight

47

Ця проблема є відомою / "класичною" проблемою оптимізації JavaScript, викликаною тим, що рядки JavaScript є "незмінними", а додавання шляхом об'єднання навіть одного символу в рядок вимагає створення, включаючи розподіл пам'яті та копіювання в , цілий новий рядок.

На жаль, прийнята відповідь на цій сторінці помилкова, де "неправильна" означає коефіцієнт продуктивності 3x для простих односимвольних рядків, а 8x-97x для коротких рядків, повторених більше разів, до 300x для повторення речень, і нескінченно помилковий, коли прийняття межі співвідношень складності алгоритмів, як nйде до нескінченності. Крім того, на цій сторінці є ще одна відповідь, яка майже правильна (заснована на одному з багатьох поколінь та варіаціях правильного рішення, що розповсюджується в Інтернеті за останні 13 років). Однак це «майже правильне» рішення пропускає ключовий момент правильного алгоритму, що спричиняє 50% зниження продуктивності.

Результати ефективності JS для прийнятої відповіді, іншої найкращої відповіді (заснованої на деградованій версії оригінального алгоритму в цій відповіді), і ця відповідь за допомогою мого алгоритму, створеного 13 років тому

~ Жовтня 2000 року я опублікував алгоритм цієї точної проблеми, який був широко адаптований, модифікований, а згодом погано зрозумілий і забутий. Щоб виправити цю проблему, у серпні 2008 року я опублікував статтю http://www.webreference.com/programming/javascript/jkm3/3.html, що пояснює алгоритм та використовує його як приклад простих оптимізацій JavaScript загального призначення. На даний момент веб-довідник очистив мою контактну інформацію і навіть моє ім’я з цієї статті. І ще раз, алгоритм був широко адаптований, модифікований, потім погано зрозумілий і значною мірою забутий.

Оригінальний алгоритм повторення / множення рядків JavaScript Джозефа Майєрса, близько Y2K як функція множення тексту в Text.js; опубліковано у серпні 2008 р. у цій формі за веб-посиланням: http://www.webreference.com/programming/javascript/jkm3/3.html (У статті використана функція як приклад оптимізації JavaScript, яка є єдиною для дивних назва "stringFill3.")

/*
 * Usage: stringFill3("abc", 2) == "abcabc"
 */

function stringFill3(x, n) {
    var s = '';
    for (;;) {
        if (n & 1) s += x;
        n >>= 1;
        if (n) x += x;
        else break;
    }
    return s;
}

Протягом двох місяців після опублікування цієї статті це те саме питання було розміщено в Stack Overflow і пролетіло під моїм радаром дотепер, коли, мабуть, оригінальний алгоритм цієї проблеми знову був забутий. Найкраще рішення на цій сторінці переповнення стека - це модифікована версія мого рішення, можливо, розділена на кілька поколінь. На жаль, модифікації зруйнували оптимальність рішення. Насправді, змінюючи структуру циклу з мого оригіналу, модифіковане рішення виконує абсолютно непотрібний додатковий крок експоненціального дублювання (таким чином, приєднуючи найбільший рядок, використовуваний у правильній відповіді, із собою додатковий час, а потім відкидаючи його).

Нижче йде обговорення деяких оптимізацій JavaScript, пов'язаних з усіма відповідями на цю проблему та на користь усім.

Техніка: уникайте посилань на об'єкти або властивості об'єкта

Щоб проілюструвати, як працює ця методика, ми використовуємо функцію JavaScript у реальному житті, яка створює рядки будь-якої довжини. І як ми побачимо, більше оптимізацій можна додати!

Функція, подібна до використовуваної тут, - створити набивання для вирівнювання стовпців тексту, форматування грошей або для заповнення даних блоку до межі. Функція генерації тексту також дозволяє вводити змінну довжину для тестування будь-якої іншої функції, яка працює над текстом. Ця функція є одним з важливих компонентів модуля обробки тексту JavaScript.

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

Оригінальний код для створення рядків stringFill1()

function stringFill1(x, n) { 
    var s = ''; 
    while (s.length < n) s += x; 
    return s; 
} 
/* Example of output: stringFill1('x', 3) == 'xxx' */ 

Синтаксис тут зрозумілий. Як бачите, ми вже використовували локальні змінні функції, перш ніж перейти до додаткових оптимізацій.

Майте на увазі, що s.lengthв коді є одна невинна посилання на властивість об'єкта, що шкодить його продуктивності. Ще гірше, що використання цього властивості об’єкта зменшує простоту програми, роблячи припущення, що читач знає про властивості об’єктів рядка JavaScript.

Використання цього властивості об'єкта руйнує загальність комп'ютерної програми. Програма передбачає, що це xповинен бути рядок довжиною один. Це обмежує застосування stringFill1()функції будь-чим, крім повторення окремих символів. Навіть окремі символи не можна використовувати, якщо вони містять кілька байтів, як HTML-сутність &nbsp;.

Найгірша проблема, викликана цим непотрібним використанням властивості об'єкта, полягає в тому, що функція створює нескінченний цикл, якщо тестується на порожньому вхідному рядку x. Щоб перевірити загальність, застосуйте програму до найменшого можливого обсягу вводу. Програма, яка виходить з ладу, коли її запропонують перевищити об'єм доступної пам'яті, є приводом. Програма на кшталт цієї, яка виходить з ладу, коли її просять нічого не виробляти, неприйнятна. Іноді гарний код - отруйний код.

Простота може бути неоднозначною метою комп’ютерного програмування, але, як правило, це не так. Коли програмі не вистачає будь-якого розумного рівня загальності, не вірно говорити: "Програма досить хороша, наскільки це йде". Як бачите, використання string.lengthвластивості не дозволяє цій програмі працювати в загальних налаштуваннях, а насправді неправильна програма готова спричинити збій браузера або системи.

Чи є спосіб покращити роботу цього JavaScript, а також подбати про ці дві серйозні проблеми?

Звичайно. Просто використовуйте цілі числа.

Оптимізований код для створення рядків stringFill2()

function stringFill2(x, n) { 
    var s = ''; 
    while (n-- > 0) s += x; 
    return s; 
} 

Код часу для порівняння stringFill1()таstringFill2()

function testFill(functionToBeTested, outputSize) { 
    var i = 0, t0 = new Date(); 
    do { 
        functionToBeTested('x', outputSize); 
        t = new Date() - t0; 
        i++; 
    } while (t < 2000); 
    return t/i/1000; 
} 
seconds1 = testFill(stringFill1, 100); 
seconds2 = testFill(stringFill2, 100); 

Успіх поки що stringFill2()

stringFill1()Для заповнення 100-байтового рядка потрібно 47,297 мікросекунд (мільйонних секунд), і stringFill2()те ж саме потрібно 27,68 мікросекунд . Це майже подвоєння продуктивності, уникаючи посилання на властивість об'єкта.

Техніка: уникайте додавання коротких рядків до довгих ниток

Наш попередній результат виглядав непогано - насправді дуже добре. Удосконалена функція stringFill2()набагато швидша за рахунок використання наших перших двох оптимізацій. Чи повірите ви, якби я сказав вам, що це можна вдосконалити в багато разів швидше, ніж зараз?

Так, ми можемо досягти цієї мети. Зараз нам потрібно пояснити, як ми уникаємо додавання коротких рядків до довгих рядків.

Короткочасна поведінка виявляється досить хорошою, порівняно з нашою початковою функцією. Комп'ютерні фахівці люблять аналізувати "асимптотичну поведінку" функції або алгоритму комп'ютерної програми, що означає вивчити її довгострокову поведінку, перевіряючи її з більшими вхідними даними. Іноді, не роблячи подальших тестів, ніколи не стає відомо про шляхи вдосконалення комп'ютерної програми. Щоб побачити, що станеться, ми збираємося створити 200-байтовий рядок.

Проблема, яка виявляється в stringFill2()

Використовуючи нашу функцію синхронізації, ми виявляємо, що час збільшується до 62,54 мікросекунди для 200-байтового рядка порівняно з 27,68 для 100-байтового рядка. Здається, час повинен бути подвоєний для виконання вдвічі більше роботи, але замість цього він втричі або вчетверо. З досвіду програмування цей результат видається дивним, оскільки, якщо що, функція повинна бути трохи швидшою, оскільки робота виконується більш ефективно (200 байт на виклик функції, а не 100 байт на виклик функції). Ця проблема пов'язана з підступною властивістю рядків JavaScript: рядки JavaScript є "незмінними".

Незмінний означає, що ви не можете змінити рядок, коли він створений. Додаючи по одному байті за один раз, ми не використовуємо ще один байт зусиль. Ми фактично відтворюємо весь рядок плюс ще один байт.

Насправді, щоб додати ще один байт до 100-байтового рядка, потрібно працювати 101 байт. Давайте коротко проаналізуємо обчислювальну вартість для створення рядка Nбайтів. Вартість додавання першого байта становить 1 одиницю обчислювальних зусиль. Вартість додавання другого байта - це не одна одиниця, а 2 одиниці (копіювання першого байта до нового рядкового об’єкта, а також додавання другого байта). Третій байт вимагає витрат у 3 одиниці тощо.

C(N) = 1 + 2 + 3 + ... + N = N(N+1)/2 = O(N^2). Символ O(N^2)вимовляється Big O з N квадрата, і це означає, що обчислювальна вартість в перспективі пропорційна квадрату довжини струни. Для створення 100 символів потрібно 10000 одиниць роботи, а для створення 200 символів потрібно 40 000 одиниць роботи.

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

(Історична примітка: Цей аналіз не обов'язково застосовується до рядків у вихідному коді, наприклад html = 'abcd\n' + 'efgh\n' + ... + 'xyz.\n', оскільки компілятор вихідного коду JavaScript може об'єднати рядки разом, перш ніж перетворити їх у об'єкт JavaScript-рядка. Ще кілька років тому реалізація KJS JavaScript зависає або збивається під час завантаження довгих рядків вихідного коду, з'єднаних знаками плюс. Оскільки для обчислювального часу O(N^2)не важко було зробити веб-сторінки, які перевантажували веб-браузер Konqueror або Safari, які використовували ядро ​​двигуна KJS JavaScript. Перший я зіткнувся з цим питанням, коли я розробляв мову розмітки та аналізатор мови розмітки JavaScript, і тоді я виявив, що спричиняє проблему, коли я писав свій сценарій для JavaScript включає.)

Очевидно, що ця швидка деградація продуктивності є величезною проблемою. Як ми можемо з цим впоратися, враховуючи, що ми не можемо змінити спосіб обробки JavaScript рядками як незмінними об'єктами? Рішення полягає у використанні алгоритму, який відтворює рядок якомога менше разів.

Для уточнення наша мета - уникнути додавання коротких рядків до довгих рядків, оскільки для того, щоб додати короткий рядок, також слід дублювати весь довгий рядок.

Як алгоритм працює, щоб уникнути додавання коротких рядків до довгих рядків

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

Наприклад, зробити рядок довжиною N = 9:

x = 'x'; 
s = ''; 
s += x; /* Now s = 'x' */ 
x += x; /* Now x = 'xx' */ 
x += x; /* Now x = 'xxxx' */ 
x += x; /* Now x = 'xxxxxxxx' */ 
s += x; /* Now s = 'xxxxxxxxx' as desired */

Для цього потрібно створити рядок довжиною 1, створити рядок довжиною 2, створити рядок довжиною 4, створити рядок довжиною 8 і, нарешті, створити рядок довжиною 9. Скільки ми заощадили?

Стара вартість C(9) = 1 + 2 + 3 + 4 + 5 + 6 + 7 + 9 = 45.

Нова вартість C(9) = 1 + 2 + 4 + 8 + 9 = 24.

Зауважимо, що нам довелося додати рядок довжиною 1 до рядка довжиною 0, потім рядок довжиною 1 до рядка довжиною 1, потім рядок довжиною 2 до рядку довжиною 2, потім рядок довжиною 4 до рядка довжиною 4, потім до рядка довжиною 8 до рядка довжиною 1, щоб отримати рядок довжиною 9. Те, що ми робимо, може бути узагальнено як уникнення додавання коротких рядків до довгих рядків або в інші слова, намагаючись з'єднати рядки разом, що мають однакову або майже рівну довжину.

Для старої обчислювальної вартості ми знайшли формулу N(N+1)/2. Чи є формула нової вартості? Так, але це складно. Важливим є те, що воно є O(N), і тому подвоєння довжини струни приблизно вдвічі збільшить кількість роботи, а не вчетверо.

Код, який реалізує цю нову ідею, майже такий же складний, як і формула обчислювальної вартості. Коли ви читаєте його, пам’ятайте, що це >>= 1означає, щоб зрушити право на 1 байт. Отже, якщо n = 10011це двійкове число, то це n >>= 1призводить до значення n = 1001.

Інша частина коду, яку ви можете не розпізнати, - це побітове та операторне записування &. Вираз n & 1оцінює істинне, якщо остання двійкова цифра nдорівнює 1, а помилкове, якщо остання двійкова цифра nдорівнює 0.

Нова високоефективна stringFill3()функція

function stringFill3(x, n) { 
    var s = ''; 
    for (;;) { 
        if (n & 1) s += x; 
        n >>= 1; 
        if (n) x += x; 
        else break; 
    } 
    return s; 
} 

Це некрасиво виглядає на непідготовленому оці, але його виконання є не менш ніж чудовим.

Подивимося, наскільки добре ця функція виконує. Побачивши результати, цілком ймовірно, що ви ніколи не забудете різницю між O(N^2)алгоритмом і O(N)алгоритмом.

stringFill1()для створення 200-байтового рядка потрібно 88,7 мікросекунд (мільйони секунди), stringFill2()займає 62,54 і stringFill3()займає лише 4,608. Що зробило цей алгоритм набагато кращим? Усі функції скористалися використанням локальних змінних функцій, але скориставшись другою та третьою технологіями оптимізації, було додано двадцятикратне поліпшення продуктивності stringFill3().

Поглиблений аналіз

Що змушує цю особливу функцію видувати конкуренцію з води?

Як я вже говорив, причиною того, що обидві ці функції, stringFill1()і stringFill2(), працювати так повільно, що рядки JavaScript незмінні. Пам'ять не може бути перерозподілена, щоб дозволити додавати ще один байт одночасно до рядкових даних, що зберігаються в JavaScript. Кожного разу, коли до кінця рядка додається ще один байт, вся рядок відновлюється від початку до кінця.

Таким чином, для поліпшення продуктивності сценарію потрібно попередньо обчислити рядки довшої довжини шляхом об'єднання двох рядків достроково, а потім рекурсивно нарощувати потрібну довжину рядка.

Наприклад, для створення 16-літерного байтового рядка спочатку буде обчислено двобайтовий рядок. Тоді два байтові рядки будуть повторно використані для попереднього обчислення чотирибайтового рядка. Тоді чотирибайтовий рядок буде повторно використаний для попереднього обчислення восьмибайтового рядка. Нарешті, два восьмибайтові рядки будуть повторно використані для створення потрібного нового рядка з 16 байт. Всього потрібно було створити чотири нові рядки: одна довжиною 2, одна довжиною 4, одна довжиною 8 та одна довжиною 16. Загальна вартість - 2 + 4 + 8 + 16 = 30.

У перспективі цю ефективність можна обчислити шляхом додавання у зворотному порядку та використання геометричного ряду, що починається з першого члена a1 = N і має спільне співвідношення r = 1/2. Сума геометричного ряду задається числом a_1 / (1-r) = 2N.

Це більш ефективно, ніж додавання одного символу для створення нового рядка довжиною 2, створення нового рядка довжиною 3, 4, 5 тощо, до 16. Попередній алгоритм використовував цей процес додавання одного байта за раз , і загальна вартість цього була б n (n + 1) / 2 = 16 (17) / 2 = 8 (17) = 136.

Очевидно, що 136 - це набагато більша кількість, ніж 30, і тому попередній алгоритм потребує набагато, набагато більше часу для створення рядка.

Для порівняння двох методів ви можете бачити, наскільки швидше рекурсивний алгоритм (його також називають "ділити і перемагати") на рядку довжиною 123,457. На моєму комп'ютері FreeBSD цей алгоритм, реалізований у stringFill3()функції, створює рядок за 0,001058 секунд, тоді як початкова stringFill1()функція створює рядок за 0,0808 секунд. Нова функція в 76 разів швидша.

Різниця в продуктивності зростає, коли довжина струни стає більшою. У межах, коли створюються більші та більші рядки, оригінальна функція поводиться приблизно як C1(постійні) часи N^2, а нова функція поводиться як C2(постійні) часи N.

За допомогою нашого експерименту ми можемо визначити значення C1бути C1 = 0.0808 / (123457)2 = .00000000000530126997та значення C2бути C2 = 0.001058 / 123457 = .00000000856978543136. За 10 секунд нова функція могла створити рядок, що містить 1166 890 359 символів. Для того щоб створити цей самий рядок, для старої функції знадобиться 7181884 секунди часу.

Це майже три місяці порівняно з десятьма секундами!

Я відповідаю лише (запізнившись на кілька років), оскільки моє оригінальне рішення цієї проблеми плаває по Інтернету вже більше 10 років, і, мабуть, мало хто розуміє цю проблему. Я думав, що, написавши тут статтю про це, я допоможу:

Оптимізація продуктивності для високошвидкісного JavaScript / Сторінка 3

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

Я хочу взяти час, щоб відтворити частину статті тут як канонічну відповідь на "Переповнення стека".

Зауважте, що алгоритм найкращої ефективності тут явно базується на моєму алгоритмі і, ймовірно, був успадкований від чужої адаптації третього чи четвертого поколінь. На жаль, модифікації призвели до зниження його продуктивності. Варіант мого рішення, представлений тут, можливо, не зрозумів моє заплутане for (;;)вираження, схоже на основний нескінченний цикл сервера, написаний на C, і який був просто розроблений, щоб дозволити ретельно розміщений оператор розриву для управління циклом, найбільш компактний спосіб уникайте експоненціальної реплікації рядка один зайвий непотрібний час.


4
Ця відповідь не повинна була отримати так багато відгуків. Перш за все, претензії на право власності Джозефа є глузливими. Основний алгоритм - 3700 років.
artistoex

2
По-друге, вона містить багато дезінформації. Сучасні реалізації Javascript навіть не торкаються вмісту рядка при виконанні конкатенації (v8 представляє об'єднані рядки як об'єкт типу ConsString). Усі решта удосконалень незначні (з точки зору асимптотичної складності).
artistoex

3
Ваше уявлення про об'єднання рядків є неправильним. Для об'єднання двох рядків Javascript взагалі не читає байти складових рядків. Натомість він просто створює об’єкт, який відноситься до лівої та правої частини. Ось чому остання конкатенація в циклі не є більш дорогою, ніж перша.
artistoex

3
Звичайно, це призводить до більшої, ніж O (1) вартості для індексації рядка, тому конкатенація може бути вирівняна бічною стороною, що дійсно заслуговує на подальшу оцінку.
artistoex

1
Це було чудове прочитання. Вам слід написати книгу про ефективність та все таке!

39

Цей досить ефективний

String.prototype.repeat = function(times){
    var result="";
    var pattern=this;
    while (times > 0) {
        if (times&1)
            result+=pattern;
        times>>=1;
        pattern+=pattern;
    }
    return result;
};

11
@Olegs, я думаю, що ідея голосування менше, ніж голосування за людину або для творчості людини (що насправді є аплодируючим), але ідея полягає в голосуванні за найповніше рішення, щоб його можна було легко знайти на вгорі списку, не потребуючи прочитати всі відповіді в пошуках ідеального. (Тому що, на жаль, у всіх нас обмежений час ...)
Сорін Постельніцу

38

Гарні новини! String.prototype.repeatв даний час є частиною JavaScript .

"yo".repeat(2);
// returns: "yoyo"

Метод підтримується всіма основними браузерами, крім Internet Explorer та Android Webview. Щоб переглянути оновлений список, див. MDN: String.prototype.repeat> Сумісність браузера .

У MDN є поліфіл для браузерів без підтримки.


Завдяки доповіді про поточний стан справ, хоча я думаю, що полізасипка Mozilla є досить складною для більшості потреб (я припускаю, що вони намагаються імітувати їх ефективну поведінку впровадження C) - тому насправді не відповідатиме вимозі ОП щодо стиснення. Будь-який з інших підходів, створених як заповнення, повинен бути більш коротким ;-)
Гасс

2
Безумовно! Але використання вбудованого має бути найбільш стислим варіантом. Оскільки полісистеми в основному є лише резервними портами, вони можуть бути трохи складнішими для забезпечення сумісності з специфікаціями (або запропонованими специфікаціями в цьому випадку). Я додав це для повноти, саме ОП вирішувати, який метод використовувати, я думаю.
Андре Ласло


17

Розширення рішення П.Бейлі :

String.prototype.repeat = function(num) {
    return new Array(isNaN(num)? 1 : ++num).join(this);
    }

Таким чином, ви повинні бути захищені від несподіваних типів аргументів:

var foo = 'bar';
alert(foo.repeat(3));              // Will work, "barbarbar"
alert(foo.repeat('3'));            // Same as above
alert(foo.repeat(true));           // Same as foo.repeat(1)

alert(foo.repeat(0));              // This and all the following return an empty
alert(foo.repeat(false));          // string while not causing an exception
alert(foo.repeat(null));
alert(foo.repeat(undefined));
alert(foo.repeat({}));             // Object
alert(foo.repeat(function () {})); // Function

РЕДАКТУЙТЕ : Кредити для героя за його елегантну ++numідею!


2
Трохи змінили свої:String.prototype.repeat = function(n){return new Array(isNaN(n) ? 1 : ++n).join(this);}
jerone

У будь-якому випадку, згідно з цим тестом ( jsperf.com/string-repeat/2 ), простий цикл із конкатенацією рядків здається набагато швидшим на Chrome, порівняно з використанням Array.join. Хіба це не смішно ?!
Marco Demaio


5
/**  
@desc: repeat string  
@param: n - times  
@param: d - delimiter  
*/

String.prototype.repeat = function (n, d) {
    return --n ? this + (d || '') + this.repeat(n, d) : '' + this
};

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


4

Ось на 5-7% покращення щодо відповіді недоброзичливця.

Відкрутіть цикл, зупинившись count > 1і виконайте додатковий result += pattnernконмат після циклу. Це дозволить уникнути попереднього невикористання кінцевих циклів pattern += patternбез використання дорогої if-check. Кінцевий результат виглядатиме так:

String.prototype.repeat = function(count) {
    if (count < 1) return '';
    var result = '', pattern = this.valueOf();
    while (count > 1) {
        if (count & 1) result += pattern;
        count >>= 1, pattern += pattern;
    }
    result += pattern;
    return result;
};

А ось загадка недоброзичливця роздвоєна на розкручену версію: http://jsfiddle.net/wsdfg/


2
function repeat(s, n) { var r=""; for (var a=0;a<n;a++) r+=s; return r;}

2
Чи не стручне об'єднання струн? Принаймні, так у Java.
Vijay Dev

Чому так вони є. Однак це не може бути оптимізовано в javarscript. :(
McTrafik

А як щодо цієї продуктивності: var r=s; for (var a=1;...:)))) У будь-якому випадку згідно з цим тестом ( jsperf.com/string-repeat/2 ) робити простий цикл із заглибленням рядка, як, наприклад, те, що ви запропонували, здається, на Chrome швидше порівняно з використанням Array . приєднатися.
Марко Демайо

@VijayDev - не відповідно до цього тесту: jsperf.com/ultimate-concat-vs-join
jbyrd

2

Тести різних методів:

var repeatMethods = {
    control: function (n,s) {
        /* all of these lines are common to all methods */
        if (n==0) return '';
        if (n==1 || isNaN(n)) return s;
        return '';
    },
    divideAndConquer:   function (n, s) {
        if (n==0) return '';
        if (n==1 || isNaN(n)) return s;
        with(Math) { return arguments.callee(floor(n/2), s)+arguments.callee(ceil(n/2), s); }
    },
    linearRecurse: function (n,s) {
        if (n==0) return '';
        if (n==1 || isNaN(n)) return s;
        return s+arguments.callee(--n, s);
    },
    newArray: function (n, s) {
        if (n==0) return '';
        if (n==1 || isNaN(n)) return s;
        return (new Array(isNaN(n) ? 1 : ++n)).join(s);
    },
    fillAndJoin: function (n, s) {
        if (n==0) return '';
        if (n==1 || isNaN(n)) return s;
        var ret = [];
        for (var i=0; i<n; i++)
            ret.push(s);
        return ret.join('');
    },
    concat: function (n,s) {
        if (n==0) return '';
        if (n==1 || isNaN(n)) return s;
        var ret = '';
        for (var i=0; i<n; i++)
            ret+=s;
        return ret;
    },
    artistoex: function (n,s) {
        var result = '';
        while (n>0) {
            if (n&1) result+=s;
            n>>=1, s+=s;
        };
        return result;
    }
};
function testNum(len, dev) {
    with(Math) { return round(len+1+dev*(random()-0.5)); }
}
function testString(len, dev) {
    return (new Array(testNum(len, dev))).join(' ');
}
var testTime = 1000,
    tests = {
        biggie: { str: { len: 25, dev: 12 }, rep: {len: 200, dev: 50 } },
        smalls: { str: { len: 5, dev: 5}, rep: { len: 5, dev: 5 } }
    };
var testCount = 0;
var winnar = null;
var inflight = 0;
for (var methodName in repeatMethods) {
    var method = repeatMethods[methodName];
    for (var testName in tests) {
        testCount++;
        var test = tests[testName];
        var testId = methodName+':'+testName;
        var result = {
            id: testId,
            testParams: test
        }
        result.count=0;

        (function (result) {
            inflight++;
            setTimeout(function () {
                result.start = +new Date();
                while ((new Date() - result.start) < testTime) {
                    method(testNum(test.rep.len, test.rep.dev), testString(test.str.len, test.str.dev));
                    result.count++;
                }
                result.end = +new Date();
                result.rate = 1000*result.count/(result.end-result.start)
                console.log(result);
                if (winnar === null || winnar.rate < result.rate) winnar = result;
                inflight--;
                if (inflight==0) {
                    console.log('The winner: ');
                    console.log(winnar);
                }
            }, (100+testTime)*testCount);
        }(result));
    }
}

2

Ось безпечна версія JSLint

String.prototype.repeat = function (num) {
  var a = [];
  a.length = num << 0 + 1;
  return a.join(this);
};

2

Для всіх браузерів

Це приблизно настільки стисло, як це виходить:

function repeat(s, n) { return new Array(n+1).join(s); }

Якщо ви також дбаєте про продуктивність, це набагато кращий підхід:

function repeat(s, n) { var a=[],i=0;for(;i<n;)a[i++]=s;return a.join(''); }

Якщо ви хочете порівняти ефективність обох варіантів, дивіться цю скрипку та цю скрипку для еталонів тестів. Під час моїх власних тестів другий варіант був приблизно в 2 рази швидший у Firefox і приблизно в 4 рази швидший у Chrome!

Тільки для сучасних браузерів:

У сучасних браузерах тепер ви також можете це зробити:

function repeat(s,n) { return s.repeat(n) };

Цей варіант не тільки коротший, ніж обидва інші варіанти, але навіть швидший, ніж другий варіант.

На жаль, він не працює в будь-якій версії Internet Explorer. Цифри в таблиці вказують першу версію браузера, яка повністю підтримує метод:

введіть тут опис зображення



2

Ще одна повторна функція:

function repeat(s, n) {
  var str = '';
  for (var i = 0; i < n; i++) {
    str += s;
  }
  return str;
}

2

ES2015був реалізований цей repeat()метод!

http://www.ecma-international.org/ecma-262/6.0/#sec-string.prototype.repeat
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ Рядок / повтор
http://www.w3schools.com/jsref/jsref_repeat.asp

/** 
 * str: String
 * count: Number
 */
const str = `hello repeat!\n`, count = 3;

let resultString = str.repeat(count);

console.log(`resultString = \n${resultString}`);
/*
resultString = 
hello repeat!
hello repeat!
hello repeat!
*/

({ toString: () => 'abc', repeat: String.prototype.repeat }).repeat(2);
// 'abcabc' (repeat() is a generic method)

// Examples

'abc'.repeat(0);    // ''
'abc'.repeat(1);    // 'abc'
'abc'.repeat(2);    // 'abcabc'
'abc'.repeat(3.5);  // 'abcabcabc' (count will be converted to integer)
// 'abc'.repeat(1/0);  // RangeError
// 'abc'.repeat(-1);   // RangeError


1

Це може бути найменший рекурсивний:

String.prototype.repeat = function(n,s) {
s = s || ""
if(n>0) {
   s += this
   s = this.repeat(--n,s)
}
return s}


1

Проста рекурсивна конкатенація

Я просто хотіла дати йому це бажання і зробила це:

function ditto( s, r, c ) {
    return c-- ? ditto( s, r += s, c ) : r;
}

ditto( "foo", "", 128 );

Не можу сказати, що я багато думав, і це, мабуть, показує :-)

Це , мабуть, краще

String.prototype.ditto = function( c ) {
    return --c ? this + this.ditto( c ) : this;
};

"foo".ditto( 128 );

І це дуже схоже на вже опубліковану відповідь - я це знаю.

Але чому взагалі бути рекурсивним?

А як щодо маленької поведінки за замовчуванням?

String.prototype.ditto = function() {
    var c = Number( arguments[ 0 ] ) || 2,
        r = this.valueOf();
    while ( --c ) {
        r += this;
    }
    return r;
}

"foo".ditto();

Тому що , хоча нерекурсивний метод буде обробляти довільно великі повтори, не впливаючи на обмеження стека викликів, він набагато повільніше.

Чому я турбувався, додаючи більше методів, які не наполовину розумніші ніж ті, що вже розміщені?

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

Порівняно швидкий і складний метод може ефективно руйнуватись і спалювати за певних обставин, тоді як повільніший і простіший метод може виконати роботу - врешті-решт.

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

"Що робити, якщо я не знаю, як це працює ?!"

Серйозно?

JavaScript страждає від однієї з його найбільших сильних сторін; він дуже терпимий до поганої поведінки, і настільки гнучкий, він буде нахилятися назад, щоб повернути результати, коли це, можливо, було б краще для всіх, якби воно зірвало!

"З великою силою приходить велика відповідальність" ;-)

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

Ці "ідеальні" алгоритми - це весело і все, але "один розмір підходить усім" рідко, якщо коли-небудь буде кращим, ніж зроблений на замовлення.

Ця проповідь була принесена вам люб’язно через недосипання та прохожі інтереси. Ідіть і кодуйте!


1

По-перше, питання ОП, здається, стосуються стислість - що, наскільки я розумію, означає "простий і легкий для читання", тоді як більшість відповідей, мабуть, стосується ефективності - що, очевидно, не те саме, а також я думаю, що якщо ви не реалізуєте дуже конкретні алгоритми маніпулювання великими даними, не повинні вас турбувати, коли ви прийдете реалізовувати основні функції Javascript для обробки даних. Лаконічність набагато важливіша.

По-друге, як зазначив Андре Ласло, String.repeat є частиною ECMAScript 6 і вже доступний у декількох популярних реалізаціях - тому найкоротша реалізація String.repeatне полягає в її реалізації ;-)

Нарешті, якщо вам потрібно підтримати хости, які не пропонують реалізацію ECMAScript 6, поліфін MDN, згаданий Андре Ласло, є не що іншим, як стислим.

Тож без зайвих прихисток - ось мій стислий поліфайл:

String.prototype.repeat = String.prototype.repeat || function(n){
    return n<=1 ? this : this.concat(this.repeat(n-1));
}

Так, це рекурсія. Мені подобаються рекурсії - вони прості, і якщо виконано правильно, їх легко зрозуміти. Щодо ефективності, якщо мова підтримує її, вони можуть бути дуже ефективними, якщо правильно написані.

З моїх тестів, цей метод на 60% швидший, ніж Array.joinпідхід. Хоча це, очевидно, ніде не наближається до реалізації спокою, але це набагато простіше, ніж обидва.

Моя тестова установка - вузол v0.10, використовуючи "Суворий режим" (я думаю, що це дає змогу отримати якийсь TCO ), викликаючи repeat(1000)рядок 10 символів мільйон разів.


1

Якщо ви вважаєте, що всі ці визначення прототипу, створення масивів та операції приєднання є надмірними, просто використовуйте єдиний код рядка там, де вам це потрібно. Рядок S, що повторюється N разів:

for (var i = 0, result = ''; i < N; i++) result += S;

3
Код повинен бути читабельним. Якщо ви буквально кожен раз збираєтесь використовувати його один раз, то відформатуйте його належним чином (або використовуйте Array(N + 1).join(str)метод, якщо це не вузьке місце). Якщо є найменший шанс, що ви збираєтесь його використовувати двічі, перемістіть його на відповідну функцію.
cloudfeet

1

Використовуйте функцію утиліти Lodash для утиліти Javascript, наприклад повторення рядків.

Lodash забезпечує приємну продуктивність та сумісність із ECMAScript.

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

Ось як повторити рядок "yo" 2 рази, використовуючи Lodash:

> _.repeat('yo', 2)
"yoyo"

0

Рекурсивне рішення з використанням ділення та перемоги:

function repeat(n, s) {
    if (n==0) return '';
    if (n==1 || isNaN(n)) return s;
    with(Math) { return repeat(floor(n/2), s)+repeat(ceil(n/2), s); }
}

0

Я прийшов сюди випадковим чином і ніколи раніше не мав причин повторювати знак у JavaScript.

Мене вразив спосіб це зробити artistoex та збитки від результатів. Я помітив, що останній струнний конмат був непотрібним, як і вказував Денніс.

Я помітив ще декілька речей, коли грав із вибірковим розбором, складеним разом.

Результати варіювали неабияку кількість, що часто сприяло останньому пробігу, і подібні алгоритми часто жокеювали позицію. Одним із речей, які я змінив, було замість використання рахунку, створеного JSLitmus, як джерело для дзвінків; так як підрахунок генерувався різним для різних методів, я ставлю в індекс. Це зробило річ набагато надійнішою. Потім я переглянув, як забезпечити передачу рядків різного розміру у функції. Це завадило деяким варіантам, які я бачив, де деякі алгоритми були кращими для окремих символів або менших рядків. Однак у топ-3 методах все було добре, незалежно від розміру рядка.

Випробуваний набір тестів

http://jsfiddle.net/schmide/fCqp3/134/

// repeated string
var string = '0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789';
// count paremeter is changed on every test iteration, limit it's maximum value here
var maxCount = 200;

var n = 0;
$.each(tests, function (name) {
    var fn = tests[name];
    JSLitmus.test(++n + '. ' + name, function (count) {
        var index = 0;
        while (count--) {
            fn.call(string.slice(0, index % string.length), index % maxCount);
            index++;
        }
    });
    if (fn.call('>', 10).length !== 10) $('body').prepend('<h1>Error in "' + name + '"</h1>');
});

JSLitmus.runAll();

Тоді я включив виправлення Денніса і вирішив подивитися, чи зможу я знайти спосіб вивірити трохи більше.

Оскільки JavaScript не може реально оптимізувати речі, найкращий спосіб покращити продуктивність - це уникати речей вручну. Якби я взяв перші 4 тривіальних результату з циклу, я міг би уникнути 2-4 рядкових сховищ і записати остаточне сховище безпосередньо до результату.

// final: growing pattern + prototypejs check (count < 1)
'final avoid': function (count) {
    if (!count) return '';
    if (count == 1) return this.valueOf();
    var pattern = this.valueOf();
    if (count == 2) return pattern + pattern;
    if (count == 3) return pattern + pattern + pattern;
    var result;
    if (count & 1) result = pattern;
    else result = '';
    count >>= 1;
    do {
        pattern += pattern;
        if (count & 1) result += pattern;
        count >>= 1;
    } while (count > 1);
    return result + pattern + pattern;
}

Це призвело до поліпшення на 1-2% в середньому за виправлення Денніса. Однак різні запуски та різні браузери показали б досить справедливу дисперсію, що цей додатковий код, ймовірно, не вартий зусиль над двома попередніми алгоритмами.

Діаграма

Редагувати: Я робив це переважно під хромом. Firefox та IE часто віддають перевагу Деннісу на пару%.



0

Люди надмірно ускладнюють це до смішної міри або марно працюють. Масиви? Рекурсія? Ти мусиш жартувати мене.

function repeat (string, times) {
  var result = ''
  while (times-- > 0) result += string
  return result
}

Редагувати. Я провів кілька простих тестів, щоб порівняти з побітною версією, розміщеною artistoex / disfated, і купою інших людей. Останнє було лише незначно швидше, але на порядок ефективніше пам'яті. За 1000000 повторів слова "blah" процес Node збільшився до 46 мегабайт за допомогою простого алгоритму конкатенації (вище), але лише 5,5 мегабайт з логарифмічним алгоритмом. Останнє, безумовно, шлях. Повторно відклавши це для наочності:

function repeat (string, times) {
  var result = ''
  while (times > 0) {
    if (times & 1) result += string
    times >>= 1
    string += string
  }
  return result
}

У вас зайва string += stringполовина часу.
nikolay

0

Об'єднання рядків на основі числа.

function concatStr(str, num) {
   var arr = [];

   //Construct an array
   for (var i = 0; i < num; i++)
      arr[i] = str;

   //Join all elements
   str = arr.join('');

   return str;
}

console.log(concatStr("abc", 3));

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


0

З ES8 ви також можете використовувати padStartабо padEndдля цього. напр.

var str = 'cat';
var num = 23;
var size = str.length * num;
"".padStart(size, str) // outputs: 'catcatcatcatcatcatcatcatcatcatcatcatcatcatcatcatcatcatcatcatcatcatcat'
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.