将数字插入已排序的数字数组的有效方法?

时间:2009-08-28 00:55:32

标签: javascript algorithm sorting

我有一个已排序的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 implementation?
  • JavaScript中的排序方法是一个本机函数,因此潜在地实现了巨大的好处 - 对于合理大小的数据集,具有巨大系数的O(log(n))仍然比O(n)差得多。

17 个答案:

答案 0 :(得分:52)

就像单个数据点一样,对于踢,我测试了这一点,在Windows 7上使用Chrome使用两种方法将1000个随机元素插入到100,000个预先排序的数字的数组中:

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

因此,至少在此设置上,本机方法无法弥补它。即使对于小数据集也是如此,将100个元素插入到1000的数组中

First Method:
1 milliseconds
Second Method:
34 milliseconds

答案 1 :(得分:32)

简单(Demo):

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

答案 2 :(得分:27)

一个非常有趣的讨论非常好,非常了不起的问题!在使用数千个对象推送数组中的单个元素之后,我也使用了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 :(得分:17)

您的代码中存在错误。它应该是:

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

如果没有此修复,代码将永远无法在数组的开头插入元素。

答案 4 :(得分:9)

您的插入函数假定给定数组已排序,它直接搜索可插入新元素的位置,通常只需查看数组中的一些元素。

数组的常规排序功能无法使用这些快捷方式。显然,它至少必须检查数组中的所有元素,看它们是否已经正确排序。仅这一事实使得一般排序比插入函数慢。

通用排序算法通常平均 O(n·log(n))并且根据实现情况,如果数组已经排序,它可能实际上是最坏的情况,导致复杂性为O(n 2 。直接搜索插入位置只有 O(log(n))的复杂性,所以它总是会快得多。

答案 5 :(得分:6)

我知道这是一个已经有答案的老问题,而且还有其他一些不错的答案。我看到一些答案提出你可以通过在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支持多个赋值会更好。 here's an example to play with:

更新

这可能会更快:

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

Updated JS Bin link

答案 6 :(得分:5)

以下是一些想法: 首先,如果您真的关心代码的运行时,请务必了解调用内置函数时会发生什么!我不知道从javascript中下来,但是快速google的splice函数返回this,这似乎表明你正在为每个调用创建一个全新的数组!我不知道它是否真的重要,但它肯定与效率有关。我看到Breton在评论中已经指出了这一点,但它肯定适用于您选择的任何数组操作函数。

无论如何,要真正解决问题。

当我读到您想要排序时,我的第一个想法是使用insertion sort!。它很方便,因为它在排序或接近排序的列表上以线性时间运行。因为你的数组只有1个元素乱序,所以它几乎排序(除了,大小为2或3的数组或其他什么,但在那时,来吧)。现在,实现排序并不是太糟糕,但这可能是你不想处理的麻烦,而且,我不知道关于javascript的事情,以及它是否容易或困难或诸如此类。这消除了对查找功能的需求,你只需推送(正如布列塔尼建议的那样)。

其次,您的“quicksort-esque”查找功能似乎是binary search算法!这是一个非常好的算法,直观而快速,但有一个问题:很难正确实现。我不敢说你的是否正确(我希望它是正确的!:)),但如果你想使用它,要小心。

无论如何,总结:使用带插入排序的“push”将在线性时间内工作(假设数组的其余部分已排序),并避免任何杂乱的二进制搜索算法要求。我不知道这是不是最好的方法(数组的底层实现,也许是一个疯狂的内置函数做得更好,谁知道),但这对我来说似乎是合理的。 :)     - Agor。

答案 7 :(得分: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会提供sortedIndexsortedLastIndex个函数,这些函数可用于代替while循环。两个潜在的缺点是1)性能不如我的方法(认为我不确定它有多糟糕)和2)它不接受自定义比较器功能,只有一个获取值比较的方法(我假设使用默认比较器。)

答案 8 :(得分:2)

我能想到的最好的数据结构是indexed skip list,它使用可进行日志时间操作的层次结构来维护链接列表的插入属性。平均而言,搜索,插入和随机访问查找可以在O(log n)时间内完成。

order statistic tree启用具有排名功能的日志时间索引。

如果不需要随机访问,但需要O(log n)插入并搜索键,则可以放弃数组结构并使用任何类型的binary search tree

使用array.splice()的答案根本没有效率,因为这平均需要O(n)时间。 What's the time complexity of array.splice() in Google Chrome?

答案 9 :(得分:1)

以下是对完成此操作的四种不同算法的比较: https://jsperf.com/sorted-array-insert-comparison/1

算法

天真永远是可怕的。对于较小的阵列大小,其他三个看起来并没有太大差别,但对于较大的阵列,最后两个阵容的表现优于简单的线性方法。

答案 10 :(得分: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
 ]));

答案 11 :(得分: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)准确地解释了你得到的时间。

答案 12 :(得分:0)

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

答案 13 :(得分:0)

这是使用lodash的版本。

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

注意:sortedIndex进行二进制搜索。

答案 14 :(得分:0)

具有自定义比较方法的TypeScript版本:

const { compare } = new Intl.Collator(undefined, {
  numeric: true,
  sensitivity: "base"
});

const insert = (items: string[], item: string) => {
    let low = 0;
    let high = items.length;

    while (low < high) {
        const mid = (low + high) >> 1;
        compare(items[mid], item) > 0
            ? (high = mid)
            : (low = mid + 1);
    }

    items.splice(low, 0, item);
};

使用:

const items = [];

insert(items, "item 12");
insert(items, "item 1");
insert(items, "item 2");
insert(items, "item 22");

console.log(items);

// ["item 1", "item 2", "item 12", "item 22"]

答案 15 :(得分:0)

如果你的第一个代码没有错误,我最好的猜测是,你会如何在 JS 中完成这项工作。我的意思是;

  1. 进行二分查找以找到插入的索引
  2. 使用 splice 执行插入操作。

这几乎总是比 domoarigato's answer 中提到的自上而下或自下而上的线性搜索和插入快 2 倍,我非常喜欢它并将其作为基准测试的基础,最后 pushsort

当然,在许多情况下,您可能正在对现实生活中的某些对象进行这项工作,here i have generated a benchmark test for these three cases 则是针对包含某些对象的大小为 100000 的数组。随意使用它。

答案 16 :(得分:0)

作为给我未来的自己的备忘录,这是另一个版本,findOrAddSorted 对极端情况和基本测试进行了一些优化。

// returns BigInt(index) if the item has been found
// or BigInt(index) + BigInt(MAX_SAFE_INTEGER) if it has been inserted 
function findOrAddSorted(items, newItem) {
  let from = 0;
  let to = items.length;
  let item;

  // check if the array is empty
  if (to === 0) {
    items.push(newItem);
    return BigInt(Number.MAX_SAFE_INTEGER);
  }

  // compare with the first item
  item = items[0];
  if (newItem === item) {
    return 0;
  }
  if (newItem < item) {
    items.splice(0, 0, newItem);
    return BigInt(Number.MAX_SAFE_INTEGER);
  }

  // compare with the last item
  item = items[to-1];
  if (newItem === item) {
    return BigInt(to-1);
  }
  if (newItem > item) {
    items.push(newItem);
    return BigInt(to) + BigInt(Number.MAX_SAFE_INTEGER);
  }

  // binary search
  let where;
  for (;;) {
    where = (from + to) >> 1;
    if (from >= to) {
      break;
    }

    item = items[where];
    if (item === newItem) {
      return BigInt(where);
    }
    if (item < newItem) {
      from = where + 1;
    }
    else {
      to = where;
    }
  }

  // insert newItem
  items.splice(where, 0, newItem);
  return BigInt(where) + BigInt(Number.MAX_SAFE_INTEGER);
}

// generate a random integer < MAX_SAFE_INTEGER
const generateRandomInt = () => Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);

// fill the array with random numbers
const items = new Array();
const amount = 1000;
let i = 0;
let where = 0;
for (i = 0; i < amount; i++) {
  where = findOrAddSorted(items, generateRandomInt());
  if (where < BigInt(Number.MAX_SAFE_INTEGER)) {
    break;
  }
}

if (where < BigInt(Number.MAX_SAFE_INTEGER)) {
  console.log(`items: ${i}, repeated at ${where}: ${items[Number(where)]}`)
}
else {
  const at = Number(where - BigInt(Number.MAX_SAFE_INTEGER));
  console.log(`items: ${i}, last insert at: ${at}: ${items[at]}`);
}
console.log(items);