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

Posted

技术标签:

【中文标题】将数字插入排序的数字数组的有效方法?【英文标题】:Efficient way to insert a number into a sorted array of numbers? 【发布时间】:2021-12-08 08:35:16 【问题描述】:

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

[WARNING] 此代码在尝试插入数组开头时存在错误,例如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) 差得多。

【问题讨论】:

在第二个实现中使用 splice 有点浪费。为什么不使用推送? 好点,我只是从第一个复制过来的。 任何包含 splice() 的内容(例如您的第一个示例)已经是 O(n)。即使它没有在内部创建整个数组的新副本,如果要将元素插入位置 0,它也可能必须将所有 n 项分流回 1 位置。也许它很快,因为它是一个本机函数并且常量是低,但它仍然是 O(n)。 另外,为了供使用此代码的人将来参考,该代码在尝试插入数组开头时存在错误。进一步向下查找更正后的代码。 不要使用parseInt,而是使用Math.floorMath.floorparseInt 快得多:jsperf.com/test-parseint-and-math-floor 【参考方案1】:

简单(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;

【讨论】:

触感不错。我从未听说过使用按位运算符来查找两个数字的中间值。通常我会乘以 0.5。这样做有没有显着的性能提升? @Jackson x &gt;&gt;&gt; 1 是二进制右移 1 个位置,实际上只是除以 2。例如对于 11:1011 -> 101 结果为 5。 @Qwerty @Web_Designer 已经在这条赛道上,你能解释一下&gt;&gt;&gt; 1和(seenhereandthere)&gt;&gt; 1之间的区别吗? &gt;&gt;&gt; 是无符号右移,而 &gt;&gt; 是符号扩展 - 这一切都归结为负数的内存表示,如果为负,则设置高位。因此,如果您将 0b1000 右移 1 个位置,使用 &gt;&gt;,您将获得 0b1100,如果您改为使用 &gt;&gt;&gt;,您将获得 0b0100。虽然在答案中给出的情况下它并不重要(被移位的数字既不大于有符号 32 位正整数的最大值也不负),但在这两种情况下使用正确的数字很重要(你需要选择您需要处理的情况)。 @asherkin - 这是不对的:“如果你将0b1000 右移一位&gt;&gt;,你会得到0b1100”。不,你会得到0b0100。对于除负数和大于 2^31 的数字(即第一位为 1 的数字)以外的所有值,不同的右移运算符的结果都是相同的。【参考方案2】:

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

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

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

First Method:
1 milliseconds
Second Method:
34 milliseconds

【讨论】:

arrays.sort 听起来很糟糕 看来 array.splice 必须做一些非常聪明的事情,在 54 微秒内插入单个元素。 @gnasher729 - 我认为 Javascript 数组与我们在 C 中的物理连续数组并不完全相同。我认为 JS 引擎可以将它们实现为哈希映射/字典,从而实现快速插入. 当你在Array.prototype.sort中使用比较器函数时,你会失去C++的好处,因为JS函数被调用的次数太多了。 第一种方法现在与Chrome uses TimSort 相比如何?来自TimSort Wikipedia:“在最好的情况下,当输入已经排序时,[TimSort] 在线性时间内运行”。【参考方案3】:

非常好的和非凡的问题,讨论非常有趣!在将单个元素推送到包含数千个对象的数组中后,我还使用了 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;
;

【讨论】:

作为记录,似乎值得注意的是,这个版本在尝试插入到数组的开头时确实可以正常工作。 (值得一提,因为原始问题中的版本存在错误,并且在这种情况下无法正常工作。) 我不确定我的实现是否不同,但我需要将三进制更改为 return c == -1 ? pivot : pivot + 1; 以返回正确的索引。否则对于长度为 1 的数组,函数将返回 -1 或 0。 @James:参数 start 和 end 仅用于递归调用,不会用于初始调用。因为这些是数组的索引值,所以它们必须是整数类型,并且在递归调用时会隐式给出。 @TheRedPea: 不,我的意思是&gt;&gt; 1 应该比/ 2 快(或不慢) 我可以看到comparer 函数的结果存在潜在问题。在此算法中,它与+-1 进行比较,但它可以是任意值&lt;0 / &gt;0。见compare function。有问题的部分不仅是 switch 语句,还有以下行:if (end - start &lt;= 1) return c == -1 ? pivot - 1 : pivot; 其中c 也与-1 进行了比较。【参考方案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);
  

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

【讨论】:

你为什么用 0 对 int 进行或运算?即什么开始|| 0 做吗? @Pinocchio: 开始 || 0 相当于: if(!start) start = 0; - 但是,“更长”的版本更有效,因为它不会为自己分配变量。【参考方案5】:

我知道这是一个已经有答案的老问题,而且还有许多其他不错的答案。我看到一些答案建议您可以通过在 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;

更新 2

@terrymorse 在 cmets 中指出 javascripts Array.splice 方法非常快,而且不仅仅是时间复杂度的不断改进。似乎正在使用一些链表魔法。这意味着您仍然需要与普通数组不同的数据结构 - 只是 javascript 数组可能本机提供不同的数据结构。

Updated JS Bin link

【讨论】:

在 JavaScript 中,您建议的插入排序将比二分查找和拼接方法慢,因为 splice 的实现速度很快。 除非 javascript 能以某种方式打破时间复杂度定律,否则我持怀疑态度。您是否有一个可运行的示例说明二分查找和拼接方法如何更快? 我收回我的第二条评论 ;-) 事实上,将存在一个数组大小,超出该数组大小,B-tree 解决方案将优于拼接解决方案。 @domoarigato 性能测试显着表明insertion with Array.splice is much less than O(N)。 N 每增加 100 到 100,000,时间/N 就会减少。 hmm,似乎 javascript 可能在后台实现了一些链表类型的魔法:***.com/questions/5175925/…。这真的很酷 - 但这也只是意味着 javascript 数组不仅仅是“一个数组”,因此强化了我的回答的基本点,即您需要另一个数据结构。感谢您指出这一点@terrymorse【参考方案6】:

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

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

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

【讨论】:

值得注意的是,向数组中插入元素的复杂度为O(n),所以最终结果应该差不多。【参考方案7】:

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

无论如何,要实际解决问题。

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

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

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

【讨论】:

+1 因为任何包含 splice() 的东西都已经是 O(n)。即使它没有在内部创建整个数组的新副本,如果要将元素插入位置 0,它也可能必须将所有 n 项分流回 1 位置。 我相信插入排序也是 O(n) 最好的情况,也是 O(n^2) 最坏的情况(尽管 OP 的用例可能是最好的情况)。 减一表示与 OP 交谈。第一段感觉像是一个不必要的告诫,因为不知道拼接是如何在引擎盖下工作的【参考方案8】:

对于少量项目,差异非常微不足道。但是,如果您要插入大量项目,或者处理非常大的数组,则在每次插入后调用 .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)它不接受自定义比较器功能,只是一种获取值进行比较的方法(我假设使用默认比较器)。

【讨论】:

arr.splice() 的调用肯定是 O(n) 时间复杂度。【参考方案9】:

这是一个使用 lodash 的版本。

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

注意:sortedIndex 进行二分查找。

【讨论】:

【参考方案10】:

以下是实现此目的的四种不同算法的比较: https://jsperf.com/sorted-array-insert-comparison/1

算法

天真:之后只需 push 和 sort() 线性:遍历数组并在适当的地方插入 二分查找:取自https://***.com/a/20352387/154329 “Quick Sort Like”:syntheticzero (https://***.com/a/18341744/154329) 的精炼解决方案

天真总是可怕的。似乎对于较小的数组大小,其他三个差异不大,但对于较大的数组,最后两个优于简单的线性方法。

【讨论】:

为什么不测试旨在实现快速插入和搜索的数据结构呢?前任。跳过列表和 BST。 ***.com/a/59870937/3163618 Native 与 Chrome uses TimSort 相比如何?来自TimSort Wikipedia:“在最好的情况下,当输入已经排序时,它会以线性时间运行”。【参考方案11】:

我能想到的最好的数据结构是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?

【讨论】:

这个怎么回答Is there a good reason to choose [splice into location found] over [push &amp; sort]? @greybeard 它回答了标题。愤世嫉俗地,这两种选择都没有效率。 如果涉及复制数组的许多元素,这两个选项都不会有效。【参考方案12】:

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

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

【讨论】:

【参考方案13】:

不要在每个项目之后重新排序,这太过分了..

如果只有一项要插入,您可以使用二分搜索找到要插入的位置。然后使用 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) 准确解释了你得到的时间。

【讨论】:

是,但不是,这取决于您的排序算法。以相反的顺序使用冒泡排序,如果最后一个元素未排序,您的排序总是在 o(n)【参考方案14】:

带有自定义比较方法的 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】:

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

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

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

当然,在许多情况下,您可能在现实生活中的某些对象上执行此工作,而here i have generated a benchmark test for these three cases 用于保存一些对象的大小为 100000 的数组。随意玩吧。

【讨论】:

【参考方案16】:
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)

【讨论】:

【参考方案17】:

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

【讨论】:

【参考方案18】:
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;

【讨论】:

以上是关于将数字插入排序的数字数组的有效方法?的主要内容,如果未能解决你的问题,请参考以下文章

插入排序

经典的算法之插入排序,你会了吗

java 数组排序 插入排序法

经典排序算法——插入排序

Swift之算法:插入排序

用C语言编写程序,实现在数组中指定位置插入一个新的数字?(数组不是排序好的)谢谢