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

IT技术 javascript algorithm sorting
2021-01-28 05:25:44

我有一个已排序的 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 中的 sort 方法是一个本机函数,因此可能会实现巨大的好处——对于合理大小的数据集,具有巨大系数的 O(log(n)) 仍然可能比 O(n) 差得多。
6个回答

简单(演示):

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;
}
@Jacksonx >>> 1是二进制右移 1 个位置,实际上只是除以 2。例如,对于 11:1011->101结果为 5。
2021-03-29 05:25:44
@asherkin - 这是不对的:“如果你0b1000向右移动1 个位置,>>你就会得到0b1100”。不,你明白了0b0100除了负数和大于 2^31 的数字(即第一位为 1 的数字)外,不同右移运算符的结果对于所有值都是相同的。
2021-04-01 05:25:44
@Qwerty @Web_Designer 已经在这条轨道上,你能解释一下>>> 1和( 这里 那里看到的之间的区别>> 1吗?
2021-04-08 05:25:44
手感不错。我从未听说过使用按位运算符来查找两个数字的中间值。通常我只会乘以 0.5。这样做是否有显着的性能提升?
2021-04-13 05:25:44
>>>是无符号右移,而>>符号扩展 - 这一切都归结为负数的内存表示,如果为负,则设置高位。所以,如果你转向0b1000右1与地方>>,你会得到0b1100,如果你改用>>>你会得到0b0100虽然在答案中给出的情况下并不重要(被移位的数字既不大于有符号的 32 位正整数的最大值也不大于负数),但在这两种情况下使用正确的一个很重要(你需要选择您需要处理的情况)。
2021-04-13 05:25:44

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

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

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

First Method:
1 milliseconds
Second Method:
34 milliseconds
似乎 array.splice 必须做一些非常聪明的事情,在 54 微秒内插入单个元素。
2021-03-31 05:25:44
@gnasher729 - 我不认为 Javascript 数组与我们在 C 中的物理连续数组真的相同。我认为 JS 引擎可以将它们实现为哈希映射/字典,从而实现快速插入。
2021-04-01 05:25:44
既然Chrome 使用了 TimSort,第一种方法如何比较来自TimSort 维基百科:“在最好的情况下,当输入已经排序时,[TimSort] 在线性时间内运行”。
2021-04-01 05:25:44
arrays.sort 听起来很糟糕
2021-04-06 05:25:44
当你使用带有 的比较器函数时Array.prototype.sort,你失去了 C++ 的好处,因为 JS 函数被调用了这么多。
2021-04-06 05:25:44

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

locationOf由于具有复杂的对象,因此我必须为我的目的扩展您的函数,因此需要像 in 的比较函数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。
2021-03-25 05:25:44
我可以看到comparer函数结果的潜在问题在这个算法中,它被比较,+-1但它可以是任意值<0/ >0请参阅比较功能有问题的部分不仅是switch语句,而且是行: if (end - start <= 1) return c == -1 ? pivot - 1 : pivot;wherec也是比较的-1
2021-03-25 05:25:44
作为记录,似乎值得注意的是,当尝试插入到数组的开头时,此版本确实可以正常工作。(值得一提,因为原始问题中的版本有一个错误,在这种情况下无法正常工作。)
2021-03-27 05:25:44
@James:参数 start 和 end 仅用于递归调用,不会用于初始调用。因为这些是数组的索引值,所以它们必须是整数类型,并且在递归调用时这是隐式给出的。
2021-03-30 05:25:44
@TheRedPea:不,我的意思是>> 1应该比/ 2
2021-04-13 05:25:44

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

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 or-ing 一个 int ?即什么开始 || 0 吗?
2021-03-15 05:25:44
@匹诺曹:开始|| 0 相当于: if(!start) start = 0; - 然而,“更长”的版本更有效,因为它不会为自己分配一个变量。
2021-03-16 05:25:44

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

底线:如果你真的需要 O(log n) 插入和删除到一个排序的数组中,你需要一个不同的数据结构 - 而不是数组。您应该使用B-Tree将 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 链接

我收回我的第二条评论 ;-) 事实上,将会有一个数组大小,超过这个大小,B 树解决方案将优于拼接解决方案。
2021-03-17 05:25:44
嗯,好像是JavaScript的可能已经实施了一些链表类型魔法罩下:stackoverflow.com/questions/5175925/...那真的很酷 - 但这也只是意味着 javascript 数组不仅仅是“数组”,因此加强了我的答案的基本观点,即您需要另一种数据结构。感谢您指出这一点@terrymorse
2021-03-17 05:25:44
@domoarigato 性能测试显着地表明使用 Array.splice 插入比 O(N) 少得多N在 100 到 100,000 之间每增加一次,时间/N 就会减少
2021-03-21 05:25:44
除非 javascript 以某种方式可以打破时间复杂性定律,否则我持怀疑态度。您是否有一个可运行的示例来说明二分搜索和拼接方法如何更快?
2021-04-05 05:25:44
在 JavaScript 中,您建议的插入排序将比二进制搜索和拼接方法慢,因为拼接具有快速实现。
2021-04-06 05:25:44