index.js 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. var _ = require('lodash');
  2. /** Merge two lists, removing duplicates, and doing everything possible to
  3. * maintain the order of the two lists.
  4. *
  5. * This function guarantees that the order of list1 is preserved (that is, if x
  6. * comes before y in list1, x comes before y in the returned list) and tries
  7. * not to undo the order of list2, though sometimes it is unavoidable.
  8. *
  9. * For example, if we have list1 = [1, 2, 4] and list2 = [2, 1, 3, 4], then the
  10. * merged list would be [1, 2, 3, 4], since that preserves the order of list1
  11. * while doing the best job possible of preserving the order of list2.
  12. *
  13. * A case like list1 = [1, 3], list2 = [3, 2, 1] is more complicated. It's not
  14. * clear what the best merged list is, but it's probably either [2, 1, 3] or
  15. * [1, 3, 2].
  16. *
  17. * In general, it's not totally clear what the "best" merged list is, but there
  18. * are some basic properties that anyone would expect:
  19. * - Since the order of list1 is preserved, the merged list will look like
  20. * list1 with the elements exclusive to list2 inserted in betweeen
  21. * - If list2[i] is not in list1, and it is possible to insert list2[i] into
  22. * list1 without contradicting the order of list2, then it should be
  23. * inserted in such a way
  24. *
  25. * This is very slow, crossing the 100ms mark with lists around 150 in length,
  26. * and growing at a rate of
  27. * O(list2.length*list2.length*(list1.length + list2.length)) from there.
  28. *
  29. * @param {Array<*>} list1
  30. * @param {Array<*>} list2
  31. * @return {Array<*>} A list containing all the elements of list1 and list2,
  32. * with duplicates removed, the order of list1 preserved, and the order of
  33. * list2 partially preserved
  34. */
  35. module.exports = function(list1, list2) {
  36. /* This is going to get mathematical. As noted above, the merged list will be
  37. * a copy of list1 with the items exclusive to list2 inserted in between. But
  38. * additionally, we want to preserve the order of list2 as much as possible.
  39. * In more formal terms, for all x and y from list2, we want to minimize the
  40. * number of times that x is before y in the merged list but after y in list2.
  41. * We call each such time an "inversion", after the term in discrete math.
  42. *
  43. * We are going to take a greedy approach to this:
  44. *
  45. * merged_list = a copy of list1
  46. * for(i = 0; i < list2.length; i++)
  47. * if(list2[i] is not in list1)
  48. * insert list2[i] into merged_list in the earliest place that creates the
  49. * minimum number of inversions
  50. * return merged_list
  51. *
  52. * It can be proven that this gives you the two properties mentioned in the
  53. * header comment above
  54. */
  55. var merged = list1.slice(); // The merged list to return
  56. var mergedIndexes = _.invert(merged);
  57. for (var i = 0; i < list2.length; i++) {
  58. var elem = list2[i];
  59. if (mergedIndexes[elem] === undefined) {
  60. // Count the inversions for every possible insertion position
  61. var inversionCnts = typeof Int32Array !== 'undefined' ?
  62. new Int32Array(merged.length + 1) :
  63. _.fill(new Array(merged.length + 1), 0);
  64. for (var j = 0; j < list2.length; j++) {
  65. var jMergedIndex = mergedIndexes[list2[j]];
  66. if (j < i) {
  67. for (var k = 0; k <= jMergedIndex; k++) {
  68. inversionCnts[k]++;
  69. }
  70. } else if (jMergedIndex !== undefined) { // j > i
  71. for (var k = jMergedIndex + 1; k < inversionCnts.length; k++) {
  72. inversionCnts[k]++;
  73. }
  74. }
  75. }
  76. // Pick the earliest place that creates the minimum number of inversions
  77. var minInversionIndex = 0;
  78. for (var j = 1; j < inversionCnts.length; j++) {
  79. if (inversionCnts[j] < inversionCnts[minInversionIndex]) {
  80. minInversionIndex = j;
  81. }
  82. }
  83. merged.splice(minInversionIndex, 0, elem);
  84. mergedIndexes = _.invert(merged);
  85. }
  86. }
  87. return merged;
  88. };