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

我有一个已排序的 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));

这段代码在尝试插入到数组的开头时有一个 bug,例如 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 中的 sort 方法是一个本机函数,因此对于合理大小的数据集,具有巨大系数的 O (log (n))仍然比 O (n)糟糕得多。
156858 次浏览

插入函数假设给定的数组已经排序,它会直接搜索可以插入新元素的位置,通常只需查看数组中的几个元素。

数组的一般排序函数不能采用这些快捷方式。显然,它至少必须检查数组中的所有元素,以确定它们是否已经正确排序。仅这一点就使得一般排序比插入函数慢。

一个通用的排序算法通常是平均 O (n & sdot; log (n)),如果数组已经排序,那么根据实现的不同,它可能是最糟糕的情况,从而导致 O (n < sup > 2 )的复杂性。直接搜索插入位置只有 O (log (n))的复杂性,因此它总是快得多。

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

不管怎样,我们要真正解决这个问题。

当我读到您想要排序时,我的第一个想法是使用 插入排序!。这很方便,因为 它以线性时间运行在已排序或接近排序的列表上。由于您的数组只有1个元素顺序错误,因此这个数组的计数几乎是排序的(除了大小为2或3或其他的数组,但在这一点上是 c’mon)。现在,实现 sort 并不是太糟糕,但是你可能不想处理这种麻烦,再说一次,我对 javascript 一无所知,也不知道它是简单的还是困难的还是别的什么。这就消除了对查找函数的需要,您只需按下(就像 Bretton 建议的那样)。

其次,您的“快速排序式”查找函数似乎是一个 二进制搜索算法!这是一个非常好的算法,直观、快速,但有一个问题: 正确实现它是出了名的困难。我不敢说你的答案是正确的还是错误的(当然,我希望是正确的!)!:) ,但如果你想使用它要小心。

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

作为一个单独的数据点,我在 Windows 7上使用 Chrome 的两种方法将1000个随机元素插入一个包含100,000个预先排序的数字的数组中进行了测试:

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

因此,至少在这种设置中,本机方法不能弥补这一缺陷。即使对于小型数据集也是如此,在1000个数组中插入100个元素:

First Method:
1 milliseconds
Second Method:
34 milliseconds

不要重新排序后,每一个项目,它的过度杀伤力. 。

如果只有一个要插入的项,则可以使用二进制搜索找到要插入的位置。然后使用 memcpy 或类似的方法批量复制剩余的项,以便为插入的项腾出空间。二进制搜索是 O (logn) ,副本是 O (n) ,给出 O (n + logn) total。使用上面的方法,在每次插入之后进行重新排序,即 O (n logn)。

这重要吗?假设随机插入 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 ps

更新: 关于你的问题。
First method = binary search+move = O(n + log n)Second method = re-sort = O(n log n)完全解释了你得到的时间。

你的代码中有一个 bug,应该是这样的:

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

如果没有这个修复,代码将永远不能在数组的开头插入元素。

非常好的问题和非常有趣的讨论!我还使用了 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;
};

简单(演示) :

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

对于一小部分项目来说,差异是非常微不足道的。但是,如果你要插入很多条目,或者使用一个甚大天线阵,调用。在每次插入之后 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);
};

如果您愿意使用其他库,loash 提供了 SortedIndex 排序索引SortedLastIndex函数,可以用它们来代替 while循环。两个潜在的缺点是: 1)性能不如我的方法(虽然我不确定它有多差) ; 2)它不接受自定义的比较器函数,只接受用于获取值进行比较的方法(我假设使用默认的比较器)。

我知道这是一个已经有了答案的老问题,而且还有许多其他不错的答案。我看到了一些答案,它们认为可以通过在 O (log n)中查找正确的插入索引来解决这个问题——可以,但是不能在这段时间内插入,因为数组需要部分复制出来才能腾出空间。

底线: 如果您确实需要在排序的数组中插入和删除 O (logn) ,那么您需要一个不同的数据结构——而不是一个数组。你应该使用 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;
}

更新2

@ terrymorse 在评论中指出,javascript Array.splice 方法的速度非常快,而且它不仅仅是在时间复杂性方面的不断改进。似乎有人在使用某种链表魔法。这意味着您仍然需要一个不同于普通数组的数据结构——只是 javascript 数组本身可能提供不同的数据结构。

更新的 JS Bin 链接

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

下面是四种不同算法的比较: Https://jsperf.com/sorted-array-insert-comparison/1

算法

天真总是可怕的。似乎对于小的阵列大小,其他三个没有太大的不同,但对于较大的阵列,最后两个优于简单的线性方法。

下面是一个使用 loash 的版本。

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

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

我能想到的最好的数据结构是 有索引的跳过清单,它使用支持日志时间操作的层次结构来维护链表的插入属性。平均而言,搜索、插入和随机访问查找可以在 O (logn)时间内完成。

有序统计树使用秩函数启用日志时间索引。

如果不需要随机访问,但需要 O (logn)插入和搜索键,则可以抛弃数组结构并使用任何类型的 二叉查找树

没有一个使用 array.splice()的答案是有效的,因为这是平均 O (n)时间

下面是我的函数,使用二进制搜索来查找条目,然后适当地插入:

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

自定义比较方法的 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"]

如果您的第一个代码没有 bug,我最好的猜测是,这将是您如何在 JS 中完成这项工作。我是说

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

这几乎总是比一个自上而下或自下而上的线性搜索和插入快2倍,如在 多么感谢你的回答中提到的,我非常喜欢,并把它作为我的基准,最后 pushsort的基础。

当然,在许多情况下,您可能正在对现实生活中的某些对象和 在这里,我已经为这三种情况生成了一个基准测试进行这项工作,在这里,我已经为这三种情况生成了一个基准测试的数组大小为100000,其中包含一些对象。你可以随便玩。

function insertElementToSorted(arr, ele, start=0,end=null) {
var n , mid
    

if (end == null) {
end = arr.length-1;
}
n = end - start
     

if (n%2 == 0) {
mid = start + n/2;
} else {
mid = start + (n-1)/2
}
if (start == end) {
return start
}
 



if (arr[0] > ele ) return 0;
if (arr[end] < ele) return end+2;
if (arr[mid] >= ele  &&   arr[mid-1] <= ele) {
return mid
}


if (arr[mid] > ele  &&   arr[mid-1] > ele) {
return insertElementToSorted(arr,ele,start,mid-1)
}


if (arr[mid] <= ele  &&   arr[mid+1] >= ele) {
return  mid + 1
}


if (arr[mid] < ele  &&   arr[mid-1] < ele) {
return insertElementToSorted(arr,ele,mid,end)
}


if(arr[mid] < ele  &&   arr[mid+1] < ele) {
console.log("mid+1", mid+1, end)
return insertElementToSorted(arr,ele,mid+1,end)
    

}
}


// Example


var test = [1,2,5,9, 10, 14, 17,21, 35, 38,54, 78, 89,102];
insertElementToSorted(test,6)

作为对未来自己的一个备忘录,这里还有另一个版本,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);