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

Efficient way to insert a number into a sorted array of numbers?

提问人:Elliot Kroo 提问时间:8/28/2009 最后编辑:林果皞Elliot Kroo 更新时间:10/24/2021 访问量:175152

问:

我有一个排序的 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) 差得多。
JavaScript 算法 排序

评论

0赞 Breton 8/28/2009
在第二个实现中使用拼接有点浪费。为什么不使用推送?
0赞 Elliot Kroo 8/28/2009
好点子,我只是从第一个复制了它。
5赞 j_random_hacker 1/2/2011
任何包含(例如您的第一个示例)都已经是 O(n)。即使它没有在内部创建整个数组的新副本,如果要将元素插入到位置 0,它可能必须将所有 n 个项目分流回 1 个位置。也许它很快,因为它是一个原生函数并且常数很低,但它仍然是 O(n)。splice()
6赞 Charlie Parker 4/6/2014
此外,为了将来使用此代码的人参考,该代码在尝试插入数组的开头时有一个错误。进一步向下查看更正的代码。
3赞 Hubert Schölnast 9/1/2017
不要改用 use。 比 : jsperf.com/test-parseint-and-math-floor 快得多parseIntMath.floorMath.floorparseInt

答:

11赞 sth 8/28/2009 #1

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

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

通用排序算法通常平均为 O(n ⋅ log(n)),并且根据实现的不同,如果数组已经排序,它实际上可能是最坏的情况,从而导致 O(n2 的复杂性。相反,直接搜索插入位置的复杂度仅为 O(log(n)),因此它总是要快得多。

评论

0赞 NemPlayer 3/16/2020
值得注意的是,将元素插入数组的复杂度为 O(n),因此最终结果应该大致相同。
5赞 agorenst 8/28/2009 #2

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

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

当我读到你想排序时,我的第一个想法是使用插入排序!。它很方便,因为它在排序或接近排序的列表上以线性时间运行。由于您的数组将只有 1 个元素不按顺序排列,因此这算作近似排序的元素(除了大小为 2 或 3 或其他大小的数组,但此时,c'mon)。现在,实现排序还不错,但这是一个你可能不想处理的麻烦,而且,我对 javascript 一无所知,也不知道它是否容易或困难或其他什么。这消除了对查找函数的需求,您只需推送(正如 Breton 建议的那样)。

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

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

评论

1赞 j_random_hacker 1/2/2011
+1,因为任何包含的东西都已经是 O(n)。即使它没有在内部创建整个数组的新副本,如果要将元素插入到位置 0,它可能必须将所有 n 个项目分流回 1 个位置。splice()
0赞 domoarigato 5/18/2017
我相信插入排序也是 O(n) 最佳情况和 O(n^2) 最坏情况(尽管 OP 的用例可能是最好的情况)。
2赞 Matt Zera 11/14/2019
减去一个用于与 OP 交谈。第一段感觉像是不知道拼接在引擎盖下是如何工作的不合时宜的告诫
69赞 Sam Phillips 11/20/2010 #3

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

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

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

First Method:
1 milliseconds
Second Method:
34 milliseconds

评论

2赞 njzk2 10/11/2012
arrays.sort 听起来很可怕
2赞 gnasher729 7/21/2015
似乎array.splice一定在做一些非常聪明的事情,在54微秒内插入一个元素。
0赞 Ian 9/27/2017
@gnasher729 - 我不认为 Javascript 数组真的与我们在 C 语言中拥有的物理连续数组相同。我认为 JS 引擎可以将它们实现为哈希映射/字典,从而实现快速插入。
1赞 aleclarson 6/14/2018
当您将比较器函数与 一起使用时,您将失去 C++ 的好处,因为 JS 函数被调用得太多了。Array.prototype.sort
0赞 poshest 2/5/2020
现在 Chrome 使用 TimSort,第一种方法如何比较?来自 TimSort 维基百科:“在最好的情况下,当输入已经排序时,[TimSort] 以线性时间运行”。
0赞 Rama Hoetzlein 7/25/2012 #4

不要在每件物品之后重新排序,这太过分了。

如果只有一个项目要插入,则可以使用二进制搜索找到要插入的位置。然后使用 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 ops

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

评论

0赞 njzk2 10/11/2012
是的,但不是,这取决于您的排序算法。使用相反顺序的冒泡排序,如果最后一个元素未排序,则排序始终在 o(n) 中
22赞 syntheticzero 8/21/2013 #5

您的代码中存在一个错误。它应该写成:

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

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

评论

1赞 Charlie Parker 4/6/2014
为什么你要用 0 来 OR-ing int?即什么开始 ||0 做?
3赞 SuperNova 4/9/2014
@Pinocchio:开始 ||0 是以下的短等价物:if(!start) start = 0;- 但是,“更长”的版本更有效率,因为它不会为自己分配变量。
32赞 kwrl 11/28/2013 #6

非常好和非凡的问题,有一个非常有趣的讨论!在推送包含数千个对象的数组中的单个元素后,我也使用了该函数。Array.sort()

为了我的目的,我不得不扩展您的函数,因为有复杂的对象,因此需要一个比较函数,如:locationOfArray.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;
};

评论

7赞 garyrob 5/1/2014
值得一提的是,作为记录,此版本在尝试插入数组开头时确实可以正常工作。(值得一提的是,因为原始问题中的版本存在错误,并且无法在这种情况下正常工作。
3赞 Niel 7/14/2015
我不确定我的实现是否不同,但我需要将三元更改为以返回正确的索引。否则,对于长度为 1 的数组,该函数将返回 -1 或 0。return c == -1 ? pivot : pivot + 1;
3赞 kwrl 4/5/2016
@James:start 和 end 参数仅在递归调用时使用,不会在初次调用时使用。由于这些是数组的索引值,因此它们必须是整数类型,并且在递归调用时,这是隐式给出的。
1赞 kwrl 4/17/2017
@TheRedPea:不,我的意思是应该比>> 1/ 2
1赞 eXavier 4/3/2018
我可以看到函数结果存在潜在问题。在此算法中,它被比较,但它可以是任意值 / 。请参阅比较函数。有问题的部分不仅是语句,还有行:where 也被比较了。comparer+-1<0>0switchif (end - start <= 1) return c == -1 ? pivot - 1 : pivot;c-1
91赞 Web_Designer 2/17/2014 #7

简单(演示):

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

评论

7赞 Jackson 1/25/2016
触感不错。我从未听说过使用按位运算符来查找两个数字的中间值。通常我只会乘以 0.5。这样做是否有显着的性能提升?
7赞 Qwerty 2/1/2016
@Jackson是二进制右移 1 位,实际上只是除以 2。例如,对于 11:->结果到 5。x >>> 11011101
4赞 yckart 2/26/2016
@Qwerty @Web_Designer 既然已经走上了这条轨道,您能解释一下 和 之间的区别吗? >>> 1>> 1
6赞 asherkin 2/27/2016
>>>是无符号右移,而是符号扩展 - 这一切都归结为负数的内存表示,如果为负,则设置高位。因此,如果你向右移动 1 位,你会得到 ,如果你改用,你会得到 .虽然在答案中给出的情况中,这并不重要(数字被移位,既不大于有符号的 32 位正整数的最大值也不大于负数),但在这两种情况下使用正确的数字很重要(您需要选择需要处理的情况)。>>0b1000>>0b1100>>>0b0100
3赞 gilly3 8/3/2016
@asherkin - 这是不对的:“如果你向右移动 1 位,你会得到”。不,你得到.除负数和大于 2^31 的数字(即第一位为 1 的数字)外,所有值的不同右移运算符的结果都是相同的。0b1000>>0b11000b0100
8赞 Sean the Bean 11/14/2015 #8

对于少数项目,差异是微不足道的。但是,如果您要插入大量项目,或者使用非常大的数组,则在每次插入后调用 .sort() 将导致大量开销。

我最终为此目的编写了一个非常巧妙的二进制搜索/插入函数,所以我想我会分享它。由于它使用循环而不是递归,因此不会对额外的函数调用进行偷听,因此我认为性能将比最初发布的任何一种方法都要好。默认情况下,它模拟默认比较器,但如果需要,可以接受自定义比较器功能。whileArray.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 函数,它们可以用来代替循环。两个潜在的缺点是 1) 性能不如我的方法好(以为我不确定它有多差)和 2) 它不接受自定义比较器函数,只接受获取要比较的值的方法(使用默认比较器,我假设)。while

评论

0赞 domoarigato 5/18/2017
对的调用肯定是 O(n) 时间复杂度。arr.splice()
18赞 domoarigato 5/20/2016 #9

我知道这是一个已经有答案的老问题,还有很多其他不错的答案。我看到一些答案表明,您可以通过在 O(log n) 中查找正确的插入索引来解决这个问题 - 您可以,但您不能在那个时候插入,因为数组需要部分复制以腾出空间。

底线:如果你真的需要 O(log n) 在排序数组中插入和删除,你需要一个不同的数据结构 - 而不是一个数组。您应该使用 B 树。将 B 树用于大型数据集将获得的性能提升将使此处提供的任何改进相形见绌。

如果必须使用数组。我提供了以下基于插入排序的代码,当且仅当数组已经排序时,该代码才有效。这对于每次插入后都需要求助的情况很有用:

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在评论中指出,javascripts Array.splice 方法的速度非常快,而且它不仅仅是在时间复杂度上的不断改进。似乎正在使用一些链表魔术。这意味着您仍然需要与普通数组不同的数据结构 - 只是 javascript 数组可能会在本机提供不同的数据结构。

更新了 JS Bin 链接

评论

0赞 trincot 5/8/2019
在 JavaScript 中,你建议的插入排序将比二进制搜索和拼接方法慢,因为拼接具有快速实现。
0赞 domoarigato 5/8/2019
除非 JavaScript 能以某种方式打破时间复杂度定律,否则我持怀疑态度。您是否有一个可运行的示例来说明二进制搜索和拼接方法如何更快?
0赞 trincot 5/9/2019
我收回我的第二条评论;-)事实上,将有一个数组大小,超过该大小,B 树解决方案的性能将优于拼接解决方案。
1赞 terrymorse 3/12/2021
@domoarigato性能测试显著表明,使用 Array.splice 插入的次数远小于 O(N)。在 100 到 100,000 之间,每增加一次,时间/N 就会减少。N
1赞 domoarigato 3/12/2021
嗯,看来 JavaScript 可能在后台实现了一些链表类型的魔术:stackoverflow.com/questions/5175925/......这真的很酷 - 但这也意味着 javascript 数组不仅仅是“一个数组”,因此强化了我答案的基本观点,即您需要另一种数据结构。感谢您指出这一点@terrymorse
-1赞 Marina 7/23/2017 #10
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;
}
3赞 gabtub 4/18/2018 #11

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

算法

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

评论

0赞 qwr 1/23/2020
为什么不测试旨在实现快速插入和搜索的数据结构呢?例如,跳过列表和 BST。 stackoverflow.com/a/59870937/3163618
1赞 poshest 2/5/2020
现在 Chrome 使用 TimSort,Native 如何比较?来自 TimSort 维基百科:“在最好的情况下,当输入已经排序时,它会以线性时间运行”。
9赞 I. Cantrell 3/20/2019 #12

这是一个使用 lodash 的版本。

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

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

2赞 qwr 1/23/2020 #13

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

顺序统计树支持使用秩函数对日志时间进行索引。

如果你不需要随机访问,但你需要O(log n)插入和搜索键,你可以放弃数组结构,使用任何类型的二叉搜索树

没有一个答案是有效的,因为这是平均 O(n) 时间。谷歌浏览器中array.splice()的时间复杂度是多少?array.splice()

评论

1赞 greybeard 1/23/2020
这如何回答Is there a good reason to choose [splice into location found] over [push & sort]?
1赞 qwr 1/23/2020
@greybeard 它回答了标题。愤世嫉俗的是,这两种选择都是无效的。
0赞 qwr 1/23/2020
如果这两个选项涉及复制数组的许多元素,则这两个选项都不是有效的。
2赞 Oguz Yilmaz 4/12/2020 #14

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

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

0赞 phamhongphuc 9/24/2020 #15

带有自定义比较方法的 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"]
0赞 Redu 1/29/2021 #16

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

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

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

当然,在许多情况下,您可能正在现实生活中的某些对象上执行这项工作,在这里,我为这三种情况生成了一个基准测试,用于容纳一些对象的大小为 100000 的数组。随意玩它。

0赞 Sai Ram 5/23/2021 #17
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)
0赞 noseratio 6/21/2021 #18

作为对我未来自我的备忘录,这是另一个版本,针对极端情况进行了一些优化,并进行了初步测试。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);