Эффективный способ вставить число в отсортированный массив чисел? - PullRequest
113 голосов
/ 28 августа 2009

У меня есть отсортированный массив 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) для наборов данных разумного размера.

Ответы [ 12 ]

53 голосов
/ 19 ноября 2010

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

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

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

First Method:
1 milliseconds
Second Method:
34 milliseconds
33 голосов
/ 17 февраля 2014

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

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;
}
27 голосов
/ 28 ноября 2013

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

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

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

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

9 голосов
/ 28 августа 2009

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

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

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

6 голосов
/ 20 мая 2016

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

5 голосов
/ 13 ноября 2015

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

5 голосов
/ 28 августа 2009

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

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

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

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

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

1 голос
/ 17 апреля 2018

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

Алгоритмы

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

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

0 голосов
/ 20 марта 2019

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

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

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

...