我有一个已排序的JavaScript数组,并且想在该数组中再插入一个项目,以使结果数组保持排序状态。我当然可以实现一个简单的quicksort样式的插入函数:
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中的sort方法是一个本机函数,因此潜在地实现了巨大的好处-对于合理大小的数据集,具有巨大系数的O(log(n))仍然比O(n)差很多。
就像一个数据点一样,我通过使用Windows 7上的Chrome的两种方法,对踢出1000个随机元素到100,000个预排序数字数组中进行了测试:
First Method:
~54 milliseconds
Second Method:
~57 seconds
因此,至少在此设置上,本机方法无法弥补这一不足。即使对于小型数据集,也是如此(将100个元素插入1000个数组中):
First Method:
1 milliseconds
Second Method:
34 milliseconds
简单(演示):
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;
}
一个非常有趣的讨论,这是一个很好的,非凡的问题!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 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);
}
}
没有此修复程序,代码将永远无法在数组的开头插入元素。
我知道这是一个古老的问题,已经有了答案,还有许多其他不错的答案。我看到一些答案,建议您可以通过在O(log n)中查找正确的插入索引来解决此问题-可以,但是您不能在那个时候插入,因为需要部分复制数组以使空间。
底线:如果您确实需要将O(log n)插入和删除到已排序的数组中,则需要一个不同的数据结构-而不是数组。您应该使用B树。通过对大型数据集使用B树可获得的性能提升,将使此处提供的任何改进都相形见war。
如果必须使用数组。我基于插入排序提供以下代码,当且仅当数组已经排序时,该代码才有效。这在每次插入后都需要求助的情况下非常有用:
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;
}
您的插入函数假定给定的数组已排序,通常直接查看数组中的一些元素,即可直接搜索可插入新元素的位置。
数组的常规排序功能不能使用这些快捷方式。显然,它至少必须检查数组中的所有元素,以查看它们是否已正确排序。仅这个事实就使一般排序比插入函数慢。
通用排序算法通常平均为O(n⋅log(n)),根据实现的不同,如果已经对数组进行了排序,则实际上可能是最坏的情况,从而导致复杂性O(n 2)的。直接搜索插入位置仅具有O(log(n))的复杂度,因此它将始终快得多。
这里有一些想法:首先,如果您真正关心代码的运行时,请确保知道调用内置函数时会发生什么!我不知道从Java语言开始,而是从splice函数的快速谷歌返回this,这似乎表明您在每次调用时都在创建一个全新的数组!我不知道这是否真的很重要,但这肯定与效率有关。我看到Breton在评论中已经指出了这一点,但是对于您选择的任何数组操作函数都肯定适用。
无论如何,要真正解决问题。
当我看到您要排序时,我的第一个想法就是使用插入排序!。它很方便,因为它以线性时间运行在已排序或几乎已排序的列表上。由于您的阵列中只有1个元素乱序,因此算得几乎是排序的(好吧,大小为2或3或其他大小的数组除外,但请注意)。现在,实现排序还不是太糟糕,但这是您可能不想处理的麻烦,而且我也不了解javascript,以及它是否简单,困难或其他问题。这样就无需使用查找功能,只需按一下即可(如Breton建议的那样)。
其次,您的“快速排序”查找函数似乎是一种二进制搜索算法!这是一个非常不错的算法,直观且快速,但是有一个要点:众所周知,正确实现它非常困难。我不敢说您的设置是否正确(当然,我希望是这样!:)),但是如果您要使用它,请当心。
总之,总结:将“ push”与插入排序一起使用将在线性时间内工作(假设数组的其余部分已排序),并且避免了任何混乱的二进制搜索算法要求。我不知道这是否是最好的方法(底层实现数组,也许一个疯狂的内置函数会做得更好,谁知道),但是对我来说似乎很合理。:)-Agor。
对于少量物品,差异很小。但是,如果要插入很多项或使用非常大的数组,则每次插入后调用.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)它不接受自定义比较器函数,仅接受用于比较值的方法(我假设使用默认的比较器)。
这是完成此操作的四种不同算法的比较:https :
//jsperf.com/sorted-array-insert-comparison/1
演算法
- 天真:稍后再推并排序()
- 线性:遍历数组并在适当的位置插入
- 二进制搜索:取自https://stackoverflow.com/a/20352387/154329
- “快速排序”:Syntheticzero的改进解决方案(https://stackoverflow.com/a/18341744/154329)
天真总是可怕的。似乎对于较小的阵列,其他三个之间的差异不大,但是对于较大的阵列,最后两个优于简单的线性方法。
这是使用lodash的版本。
const _ = require('lodash');
sortedArr.splice(_.sortedIndex(sortedArr,valueToInsert) ,0,valueToInsert);
注意:sortedIndex进行二进制搜索。
我能想到的最好的数据结构是索引的跳过列表,该列表通过启用日志时间操作的层次结构来维护链接列表的插入属性。平均而言,搜索,插入和随机访问查找可以在O(log n)时间内完成。
一个顺序统计树启用日志的时间索引用RANK函数。
如果您不需要随机访问,但需要O(log n)插入并搜索键,则可以放弃数组结构并使用任何类型的二叉搜索树。
array.splice()
由于平均时间为O(n),因此使用的答案均无效。Google Chrome中array.splice()的时间复杂度是多少?
这是我的功能,使用二进制搜索来找到项目,然后适当地插入:
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
]));
不要在每个项目之后都重新排序,因为它过于夸张。
如果仅要插入一项,则可以使用二进制搜索找到要插入的位置。然后使用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个项目随时到达,则必须执行search + move。但是,如果提前给您列出了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)
确切说明您获得的时间安排。
具有自定义比较方法的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"]
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;
}
文章标签:algorithm , javascript , sorting
版权声明:本文为原创文章,版权归 javascript 所有,欢迎分享本文,转载请保留出处!
评论已关闭!