Ефективний спосіб вставити число в відсортований масив чисел?


143

У мене є відсортований масив JavaScript, і хочу вставити ще один елемент у масив, таким чином отриманий масив залишається відсортованим. Я, безумовно, міг реалізувати просту функцію вставки в стилі quicksort:

var array = [1,2,3,4,5,6,7,8,9];
var element = 3.5;
function insert(element, array) {
  array.splice(locationOf(element, array) + 1, 0, element);
  return array;
}

function locationOf(element, array, start, end) {
  start = start || 0;
  end = end || array.length;
  var pivot = parseInt(start + (end - start) / 2, 10);
  if (end-start <= 1 || array[pivot] === element) return pivot;
  if (array[pivot] < element) {
    return locationOf(element, array, pivot, end);
  } else {
    return locationOf(element, array, start, pivot);
  }
}

console.log(insert(element, array));

[ПОПЕРЕДЖЕННЯ] цей код має помилку при спробі вставити на початок масиву, наприклад insert(2, [3, 7 ,9]) видає неправильні [3, 2, 7, 9].

Однак я помітив, що реалізація функції Array.sort потенційно може зробити це для мене, і в основному:

var array = [1,2,3,4,5,6,7,8,9];
var element = 3.5;
function insert(element, array) {
  array.push(element);
  array.sort(function(a, b) {
    return a - b;
  });
  return array;
}

console.log(insert(element, array));

Чи є вагомі підстави вибрати першу реалізацію над другою?

Редагувати : Зауважте, що для загального випадку вставка O (log (n)) (як реалізовано в першому прикладі) буде швидшою, ніж алгоритм загального сортування; однак це не обов'язково стосується JavaScript, зокрема. Зауважте, що:

  • Найкращим випадком для декількох алгоритмів вставки є O (n), який все ще суттєво відрізняється від O (log (n)), але не настільки поганий, як O (n log (n)), як зазначено нижче. Це зводиться до конкретного алгоритму сортування (див. Реалізацію Javascript Array.sort?) )
  • Метод сортування в JavaScript - це основна функція, тому потенційно реалізуючи величезні переваги - O (log (n)) з величезним коефіцієнтом, все ще може бути набагато гіршим, ніж O (n) для наборів даних досить розміру.

використання сплайса у другій реалізації трохи марно. Чому б не використовувати push?
Бретон

Добре, я просто скопіював це з першого.
Елліот Круо

4
Все, що містить splice()(наприклад, ваш перший приклад), вже є O (n). Навіть якщо він не створює внутрішньо нову копію всього масиву, він, можливо, повинен перемістити всі n елементів назад 1 позицію, якщо елемент потрібно вставити в позицію 0. Можливо, це швидко, тому що це нативна функція, а константа - низький, але все-таки O (n).
j_random_hacker

6
Крім того, для подальшої довідки для людей, які використовують цей код, код має помилку при спробі вставити на початок масиву. Далі подивіться на виправлений код.
Буратіно

3
Не parseIntвикористовуйте Math.floorнатомість використання . Math.floorнабагато швидше, ніж parseInt: jsperf.com/test-parseint-and-math-floor
Hubert Schölnast

Відповіді:


58

Як єдину точку даних, для ударів я перевірив це, вставляючи 1000 випадкових елементів у масив із 100000 заздалегідь відсортованих чисел за допомогою двох методів за допомогою Chrome у Windows 7:

First Method:
~54 milliseconds
Second Method:
~57 seconds

Тож, принаймні, у цій установці нативний метод цього не компенсує. Це справедливо навіть для невеликих наборів даних, вставляючи 100 елементів у масив 1000:

First Method:
1 milliseconds
Second Method:
34 milliseconds

1
arrays.sort звучить досить жахливо
njzk2

2
Здається, що array.splice повинен робити щось дійсно розумно, щоб вставити один елемент протягом 54 мікросекунд.
gnasher729

@ gnasher729 - Я не думаю, що масиви Javascript насправді такі ж, як фізично безперервні масиви, як у нас в C. Я думаю, що двигуни JS можуть реалізувати їх як хеш-карту / словник, що дозволяє швидко вставляти.
Ян

1
коли ви використовуєте функцію порівняння з Array.prototype.sort, ви втрачаєте переваги C ++, оскільки функція JS так називається.
aleclarson

Як порівняється Перший метод тепер, коли Chrome використовує TimSort ? З Вікіпедії TimSort : "У кращому випадку, який виникає, коли вхід вже відсортований, [TimSort] працює в лінійний час".
найкраще

47

Простий ( демонстраційний ):

function sortedIndex(array, value) {
    var low = 0,
        high = array.length;

    while (low < high) {
        var mid = (low + high) >>> 1;
        if (array[mid] < value) low = mid + 1;
        else high = mid;
    }
    return low;
}

4
Приємний дотик. Я ніколи не чув, щоб використовувати побітові оператори, щоб знайти середнє значення двох чисел. Зазвичай я б просто помножив на 0,5. Чи є значне підвищення продуктивності, роблячи це таким чином?
Джексон

2
@Jackson x >>> 1- це бінарний правий зсув на 1 позицію, що фактично є лише діленням на 2. Наприклад, для 11: 1011-> 101результатів до 5.
Qwerty

3
@Qwerty @Web_Designer Будучи вже на цій доріжці, ви могли б пояснити різницю між >>> 1та ( бачимо тут і там ) >> 1?
yckart

4
>>>являє собою неподписаний правий зсув, тоді >>як розширення знаків - це все зводиться до представлення в пам'яті негативних чисел, де високий біт встановлюється, якщо негативний. Тож якщо ви зміните 0b1000правильне 1 місце, >>ви отримаєте 0b1100, якщо замість цього >>>ви отримаєте, ви отримаєте 0b0100. Хоча у випадку, наведеному у відповіді, це насправді не має значення (число, яке зміщується, не має бути більшим за підписане максимальне значення 32-бітного додатного цілого чи негативного), важливо використовувати правильне в цих двох випадках (ви потрібно вибрати, який випадок потрібно обробити).
ашеркін

2
@asherkin - Це неправильно: "якщо ти змістиш 0b1000праворуч на 1 місце, >>отримаєш 0b1100". Ні, ви отримуєте 0b0100. Результат різних операторів правого зсуву буде однаковим для всіх значень, за винятком від’ємних чисел та чисел, більших від 2 ^ 31 (тобто чисел, що мають 1 у першому біті).
gilly3

29

Дуже хороше і чудове запитання з дуже цікавою дискусією! Я також використовував цю Array.sort()функцію після натискання одного елемента в масиві з деякими тисячами об'єктів.

Мені довелося розширити вашу locationOfфункцію з моїх цілей через наявність складних об'єктів, а отже і необхідності функції порівняння, як у Array.sort():

function locationOf(element, array, comparer, start, end) {
    if (array.length === 0)
        return -1;

    start = start || 0;
    end = end || array.length;
    var pivot = (start + end) >> 1;  // should be faster than dividing by 2

    var c = comparer(element, array[pivot]);
    if (end - start <= 1) return c == -1 ? pivot - 1 : pivot;

    switch (c) {
        case -1: return locationOf(element, array, comparer, start, pivot);
        case 0: return pivot;
        case 1: return locationOf(element, array, comparer, pivot, end);
    };
};

// sample for objects like {lastName: 'Miller', ...}
var patientCompare = function (a, b) {
    if (a.lastName < b.lastName) return -1;
    if (a.lastName > b.lastName) return 1;
    return 0;
};

7
Для запису варто відзначити, що ця версія працює правильно, коли намагається вставити на початок масиву. (Варто згадати це, оскільки версія в оригінальному запитанні має помилку і не працює належним чином для цього випадку.)
garyrob

3
Я не впевнений, чи відрізнялася моя реалізація, але мені потрібно було змінити термінал return c == -1 ? pivot : pivot + 1;, щоб повернути правильний індекс. В іншому випадку для масиву довжиною 1 функція повернеться -1 або 0.
Niel

3
@James: Параметри початку та кінця використовуються лише для рекурсивного виклику і не використовуються при вхідному дзвінку. Оскільки це значення індексу для масиву, вони повинні бути цілими типами, а при рекурсивному виклику це неявно задано.
kwrl

1
@TheRedPea: ні, я мав на увазі, що >> 1повинен бути швидшим (або не повільніше) ніж/ 2
kwrl

1
Я бачу потенційну проблему з результатом comparerфункції. У цьому алгоритмі його порівнюють, +-1але це може бути довільне значення <0/ >0. Див. Функцію порівняння . Проблемною частиною є не лише switchтвердження, а й рядок: if (end - start <= 1) return c == -1 ? pivot - 1 : pivot;де cпорівнюється -1також.
eXavier

19

У вашому коді помилка. Він повинен читати:

function locationOf(element, array, start, end) {
  start = start || 0;
  end = end || array.length;
  var pivot = parseInt(start + (end - start) / 2, 10);
  if (array[pivot] === element) return pivot;
  if (end - start <= 1)
    return array[pivot] > element ? pivot - 1 : pivot;
  if (array[pivot] < element) {
    return locationOf(element, array, pivot, end);
  } else {
    return locationOf(element, array, start, pivot);
  }
}

Без цього виправлення код ніколи не зможе вставити елемент на початку масиву.


чому ти або -и інт з 0? тобто з чого починається || 0 робити?
Буратіно

3
@Pinocchio: початок || 0 - короткий еквівалент: якщо (! Start) start = 0; - Однак "довша" версія є більш ефективною, оскільки вона не присвоює змінну собі.
SuperNova

11

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

Підсумок: Якщо вам дійсно потрібні O (log n) вставлення та видалення в відсортований масив, вам потрібна інша структура даних - не масив. Ви повинні використовувати B-дерево . Підвищення продуктивності, яке ви отримаєте від використання B-Tree для великого набору даних, дозволить уникнути будь-якого з запропонованих тут удосконалень.

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

function addAndSort(arr, val) {
    arr.push(val);
    for (i = arr.length - 1; i > 0 && arr[i] < arr[i-1]; i--) {
        var tmp = arr[i];
        arr[i] = arr[i-1];
        arr[i-1] = tmp;
    }
    return arr;
}

Він повинен працювати в O (n), що, на мою думку, найкраще, що ви можете зробити. Було б приємніше, якби js підтримував декілька завдань. ось приклад для гри:

Оновлення:

це може бути швидше:

function addAndSort2(arr, val) {
    arr.push(val);
    i = arr.length - 1;
    item = arr[i];
    while (i > 0 && item < arr[i-1]) {
        arr[i] = arr[i-1];
        i -= 1;
    }
    arr[i] = item;
    return arr;
}

Оновлено посилання JS Bin


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

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

Я повертаю свій другий коментар ;-) Дійсно, буде розмір масиву, за яким рішення B-дерева перевершить рішення сплайсингу.
трінкот

9

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

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

Загальний алгоритм сортування зазвичай в середньому O (n ⋅ log (n)), і залежно від реалізації він може бути насправді найгіршим випадком, якщо масив уже відсортований, що призводить до складності O (n 2 ) . Безпосередньо пошук позиції вставки натомість має лише складність O (log (n)) , тому це завжди буде набагато швидше.


Варто зазначити, що вставка елемента в масив має складність O (n), тому кінцевий результат повинен бути приблизно однаковим.
NemPlayer

5

Для невеликої кількості предметів різниця досить тривіальна. Однак якщо ви вставляєте багато елементів або працюєте з дуже великим масивом, виклик .sort () після кожної вставки спричинить величезну кількість накладних витрат.

Я закінчив писати досить чітку двійкову функцію пошуку / вставки саме для цієї мети, тому я подумав, що поділюсь цим. Оскільки він використовує whileцикл замість рекурсії, для додаткових викликів функцій немає підслуховування, тому я думаю, що продуктивність буде навіть кращою, ніж будь-який із спочатку розміщених методів. І він за замовчуванням емулює Array.sort()компаратор за замовчуванням, але при бажанні приймає функцію спеціального компаратора.

function insertSorted(arr, item, comparator) {
    if (comparator == null) {
        // emulate the default Array.sort() comparator
        comparator = function(a, b) {
            if (typeof a !== 'string') a = String(a);
            if (typeof b !== 'string') b = String(b);
            return (a > b ? 1 : (a < b ? -1 : 0));
        };
    }

    // get the index we need to insert the item at
    var min = 0;
    var max = arr.length;
    var index = Math.floor((min + max) / 2);
    while (max > min) {
        if (comparator(item, arr[index]) < 0) {
            max = index;
        } else {
            min = index + 1;
        }
        index = Math.floor((min + max) / 2);
    }

    // insert the item
    arr.splice(index, 0, item);
};

Якщо ви відкриті для використання інших бібліотек, lodash надає sortedIndex та sortedLastIndex функції, які можна використовувати замість whileциклу. Два потенційних недоліки: 1) продуктивність не настільки хороша, як мій метод (я думав, не впевнений, наскільки це гірше) і 2) він не приймає спеціальну функцію порівняння, лише метод отримання значення для порівняння (я використовую компаратор за замовчуванням, я вважаю).


заклик до arr.splice(), безумовно, O (n) часової складності.
domoarigato

4

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

У будь-якому разі, насправді вирішити проблему.

Коли я прочитав, що ти хотів сортувати, моя перша думка - використовувати сортування вставки! . Це зручно, оскільки він працює в лінійний час за відсортованими або майже відсортованими списками . Оскільки у ваших масивах буде лише 1 елемент, який не впорядкований, він вважається майже відсортованим (за винятком, ну, масивів розміром 2 або 3 або будь-якого іншого, але в цьому випадку, c'mon). Тепер реалізація сорту не надто погана, але це клопоти, з якими ви, можливо, не захочете мати справу, і, знову ж таки, я не знаю нічого про JavaScript, і чи буде це легко чи важко чи що. Це знімає потребу у вашій функції пошуку, і ви просто натискаєте (як запропонував Бретон).

По-друге, ваша функція пошуку "quicksort-esque", здається, є бінарним алгоритмом пошуку ! Це дуже приємний алгоритм, інтуїтивно зрозумілий і швидкий, але з одним уловом: його, як відомо, важко реалізувати правильно. Я не наважусь сказати, чи правильно це чи ні (сподіваюся, звичайно! :)), але будьте обережні, якщо ви хочете його використовувати.

У будь-якому випадку, підсумок: використання "push" з сортуванням вставки буде працювати в лінійний час (якщо припустити, що решта масиву відсортовано) та уникнути будь-яких брудних вимог до алгоритму двійкового пошуку. Я не знаю, чи це найкращий спосіб (основна реалізація масивів, можливо, божевільна вбудована функція робить це краще, хто знає), але мені це здається розумним. :) - Агор.


1
+1, оскільки все, що містить splice(), вже є O (n). Навіть якщо він не створить внутрішньо нову копію всього масиву, він, можливо, повинен перемістити всі n елементів назад 1 позицію, якщо елемент потрібно вставити в позицію 0.
j_random_hacker

Я вважаю, що сортування вставки - це також найкращий випадок O (n), а O (n ^ 2) - найгірший (хоча, мабуть, найкращий випадок використання ОП).
domoarigato

Мінус один для розмови в ОП. Перший абзац відчував себе нецікавим застереженням за те, що він не знає, як працює
Метт Зера

2

Ось порівняння чотирьох різних алгоритмів для цього: https://jsperf.com/sorted-array-insert-comporder/1

Алгоритми

Наївність завжди жахлива. Здається, для невеликих розмірів масиву, інші три не дуже відрізняються, але для великих масивів останні 2 перевершують простий лінійний підхід.


Чому б не перевірити структури даних, розроблені для швидкого введення та пошуку? колишній пропуск списків і BSTs. stackoverflow.com/a/59870937/3163618
qwr

Як Native порівнює зараз, коли Chrome використовує TimSort ? З Вікіпедії TimSort : "У кращому випадку, який відбувається, коли вхід вже відсортований, він працює в лінійний час".
найкраще

2

Ось версія, яка використовує lodash.

const _ = require('lodash');
sortedArr.splice(_.sortedIndex(sortedArr,valueToInsert) ,0,valueToInsert);

Примітка: sortedIndex здійснює двійковий пошук.


1

Найкраща структура даних, яку я можу придумати, - це індексований пропускний список який підтримує властивості вставки зв'язаних списків із ієрархічною структурою, яка дозволяє здійснювати операції в часі журналу. У середньому пошук, вставка та пошук у довільному доступі можна здійснити за O (log n) час.

Порядок статистика дерево дозволяє час індексації журналу з функцією рангу.

Якщо вам не потрібен випадковий доступ, але вам потрібна вставка O (log n) та пошук ключів, ви можете скинути структуру масиву і використовувати будь-яке дерево двійкового пошуку .

Жоден із відповідей, які використовують, array.splice()взагалі не ефективний, оскільки це в середньому O (n) час. Яка складність у часі array.splice () у Google Chrome?


Як ця відповідьIs there a good reason to choose [splice into location found] over [push & sort]?
сіра борода

1
@greybeard Відповідає заголовку. цинічно жоден вибір не є ефективним.
qwr

Жоден варіант не може бути ефективним, якщо він передбачає копіювання багатьох елементів масиву.
qwr

1

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

function binaryInsert(val, arr){
    let mid, 
    len=arr.length,
    start=0,
    end=len-1;
    while(start <= end){
        mid = Math.floor((end + start)/2);
        if(val <= arr[mid]){
            if(val >= arr[mid-1]){
                arr.splice(mid,0,val);
                break;
            }
            end = mid-1;
        }else{
            if(val <= arr[mid+1]){
                arr.splice(mid+1,0,val);
                break;
            }
            start = mid+1;
        }
    }
    return arr;
}

console.log(binaryInsert(16, [
    5,   6,  14,  19, 23, 44,
   35,  51,  86,  68, 63, 71,
   87, 117
 ]));


0

Не переробляйте після кожного елемента, його надмірність ..

Якщо є лише один елемент для вставки, ви можете знайти місце для вставки, використовуючи двійковий пошук. Потім скористайтеся memcpy або подібним способом для копіювання решти елементів, щоб звільнити місце для вставленого. Двійковий пошук - O (log n), а копія - O (n), даючи O (n + log n). Використовуючи вищезазначені методи, ви робите повторну сортування після кожного вставки, що є O (n log n).

Це важливо? Скажімо, ви випадковим чином вставляєте k елементів, де k = 1000. Відсортований список становить 5000 елементів.

  • Binary search + Move = k*(n + log n) = 1000*(5000 + 12) = 5,000,012 = ~5 million ops
  • Re-sort on each = k*(n log n) = ~60 million ops

Якщо k елементи, які потрібно вставити, надходять будь-коли, тоді потрібно виконати пошук + переміщення. Однак якщо вам надано список k елементів, які потрібно вставити в відсортований масив - достроково - тоді ви можете зробити ще краще. Сортуйте k елементів окремо від уже відсортованого n масиву. Потім зробіть сортування сканування, при якому ви переміщуєте обидва відсортовані масиви одночасно, об’єднуючи один в інший. - Сортування в один крок = k log k + n = 9965 + 5000 = ~ 15 000 ops

Оновлення: Що стосується вашого питання.
First method = binary search+move = O(n + log n). Second method = re-sort = O(n log n)Точно пояснює терміни, які ви отримуєте.


так, але ні, це залежить від вашого алгоритму сортування. Використовуючи сортування бульбашок у зворотному порядку, ваш сортування, якщо останній елемент не відсортовано, завжди знаходиться у o (n)
njzk2

-1
function insertOrdered(array, elem) {
    let _array = array;
    let i = 0;
    while ( i < array.length && array[i] < elem ) {i ++};
    _array.splice(i, 0, elem);
    return _array;
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.