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

У меня есть отсортированный массив 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));

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

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

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

Ответ 1

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

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

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

First Method:
1 milliseconds
Second Method:
34 milliseconds

Ответ 2

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

Ответ 3

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

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

В вашем коде есть ошибка. Он должен гласить:

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

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

Ответ 5

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

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

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

Ответ 6

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

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

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

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

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

Ответ 7

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

Ответ 8

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

Ответ 9

Не пересобирайте после каждого элемента, его избыток.

Если вставить только один элемент, вы можете найти место для вставки с помощью двоичного поиска. Затем используйте memcpy или аналогичную для массового копирования оставшиеся элементы, чтобы освободить место для вставленного. Бинарный поиск - O (log n), а копия - O (n), дающая O (n + log n) total. Используя вышеприведенные методы, вы выполняете повторный сортировку после каждой вставки, которая является 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) Точно объясняет тайминги, которые вы получаете.

Ответ 10

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