it-swarm.com.ru

Эффективный способ вставить число в отсортированный массив чисел?

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

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));

Есть ли веская причина для выбора первой реализации над второй?

Edit: обратите внимание, что для общего случая вставка O(log(n)) (как реализовано в первом примере) будет быстрее, чем общий алгоритм сортировки; однако это не обязательно относится к JavaScript в частности. Обратите внимание, что:

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

Просто как единая точка данных, для пинка я тестировал это, вставляя 1000 случайных элементов в массив из 100 000 предварительно отсортированных чисел, используя два метода с использованием Chrome в Windows 7:

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

Так что, по крайней мере, на этой установке нативный метод не восполняет это. Это верно даже для небольших наборов данных, вставляя 100 элементов в массив из 1000:

First Method:
1 milliseconds
Second Method:
34 milliseconds
51
Sam Phillips

Простой ( Демо ):

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;
}
31
Web_Designer

Очень хороший и замечательный вопрос с очень интересной дискуссией! Я также использовал функцию 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;
};
26
kwrl

В вашем коде есть ошибка. Следует читать:

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);
  }
}

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

15
syntheticzero

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

Общая функция сортировки массива не может использовать эти ярлыки. Очевидно, что, по крайней мере, необходимо проверить все элементы в массиве, чтобы убедиться, что они уже правильно упорядочены. Уже один этот факт делает общую сортировку медленнее, чем функция вставки.

Общий алгоритм сортировки обычно находится в среднем O (n ⋅ log (n)) и в зависимости от реализации это может быть на самом деле худший случай, если массив уже отсортирован, что приводит к сложностям O (n2) . Прямой поиск позиции вставки имеет сложность O(log(n)) , поэтому он всегда будет намного быстрее.

9
sth

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

Итог: если вам действительно нужно O (log n) вставлять и удалять в отсортированный массив, вам нужна другая структура данных - не массив. Вы должны использовать B-Tree . Повышение производительности, которое вы получите от использования 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

6
domoarigato

Вот несколько соображений: Во-первых, если вы действительно обеспокоены временем выполнения своего кода, обязательно узнайте, что происходит при вызове встроенных функций! Я не знаю сверху вниз в javascript, но быстрый гугл функции сплайсинга вернул это , что, кажется, указывает на то, что вы создаете новый массив при каждом вызове! Я не знаю, имеет ли это значение, но это, безусловно, связано с эффективностью. Я вижу, что Бретон в комментариях уже указывал на это, но это, безусловно, справедливо для любой функции манипулирования массивами, которую вы выберете.

В любом случае, на самом деле на решение проблемы.

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

Во-вторых, ваша функция поиска типа «быстрая сортировка» выглядит как бинарный поиск алгоритм! Это очень хороший алгоритм, интуитивно понятный и быстрый, но с одним уловом: его сложно реализовать правильно. Я не осмелюсь сказать, верно ли это у вас (надеюсь, конечно! :)), но будьте осторожны, если хотите его использовать.

В любом случае, сводка: использование «Push» с сортировкой вставок будет работать за линейное время (при условии, что остальная часть массива отсортирована), и избежать каких-либо грязных требований алгоритма двоичного поиска. Я не знаю, является ли это лучшим способом (базовая реализация массивов, может быть, сумасшедшая встроенная функция делает это лучше, кто знает), но мне это кажется разумным. :) - Агор.

5
agorenst

Для небольшого количества предметов разница довольно незначительная. Однако, если вы вставляете много элементов или работаете с очень большим массивом, вызов .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) она не принимает пользовательскую функцию сравнения, только метод получения значения для сравнения (используя компаратор по умолчанию, я полагаю).

4
Sean the Bean

Вот сравнение четырех различных алгоритмов для достижения этой цели: https://jsperf.com/sorted-array-insert-comparison/1

Алгоритмы

  • Наивный: просто нажмите и сортировать () потом
  • Линейный: итерация по массиву и вставка в случае необходимости
  • Двоичный поиск: взят из https://stackoverflow.com/a/20352387/154329
  • «Быстрая сортировка лайка»: изысканный раствор от syntzerzero ( https://stackoverflow.com/a/18341744/154329 )

Наивный всегда ужасен. Кажется, для небольших размеров массивов остальные три не отличаются слишком сильно, но для больших массивов последние 2 превосходят простой линейный подход.

0
gabtub
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;
}
0
Marina

Вот версия, которая использует lodash.

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

примечание: sortedIndex выполняет бинарный поиск.

0
I. Cantrell

Не пересортируйте после каждого предмета, его перебор.

Если есть только один элемент для вставки, вы можете найти место для вставки с помощью бинарного поиска. Затем используйте 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 операций

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

0
Rama Hoetzlein