3

I have a list of items and I want them to be sorted based on the field enqueuedAt in descending order. Within the items belonging to the same queue (identified by queueName), the position (lowest first) should take precedence over enqueuedAt.

In other words, the overall sort order should be based on enqueuedAt (descending), and inside, the items belonging to the same queue interchange positions among themselves, so that the one with a lower position always comes before the one with a higher position.

I came up with the below code in order to achieve this. Is there a way to improve the time complexity?

const data = [
  {
    id: 1,
    enqueuedAt: 8,
    queueName: 'Queue 1',
    position: 1
  },
  {
    id: 2,
    enqueuedAt: 7,
    queueName: 'Queue 2',
    position: 1
  },
  {
    id: 3,
    enqueuedAt: 6,
    queueName: 'Queue 3',
    position: 3
  },
  {
    id: 4,
    enqueuedAt: 5,
    queueName: 'Queue 4',
    position: 2
  },
  {
    id: 5,
    enqueuedAt: 1,
    queueName: 'Queue 1',
    position: 2
  },
  {
    id: 6,
    enqueuedAt: 2,
    queueName: 'Queue 4',
    position: 1
  },
  {
    id: 7,
    enqueuedAt: 4,
    queueName: 'Queue 1',
    position: 3
  },
  {
    id: 8,
    enqueuedAt: 3,
    queueName: 'Queue 2',
    position: 2
  }
]

function sortThem(array) {
  array.sort((a, b) => b.enqueuedAt - a.enqueuedAt)

  for (let i = 0; i < array.length - 1; i++) {
    for (let j = i + 1; j < array.length; j++) {
      if (array[i].queueName === array[j].queueName) {
        if (array[j].position < array[i].position) {
          const t = array[j]
          array[j] = array[i]
          array[i] = t
        }
      }
    }
  }

  return array
}

console.log(sortThem(data))
karthikaruna
  • 3,042
  • 7
  • 26
  • 37
  • Is `enqueuedAt` guaranteed to be unique? – trincot Dec 07 '20 at 10:57
  • Yes, it's guaranteed to be unique – karthikaruna Dec 07 '20 at 11:00
  • Why the entry with `enqueuedAt` 3 does not come before `enqueuedAt` 2? – trincot Dec 07 '20 at 11:27
  • Because, after sorting based on `enqueuedTime`, the spot (index 3) of the item you're talking about (the one with `enqueuedAt` 2), belongs to Queue 4. Some item that belongs to another queue is not allowed to go there. Only an item that also belongs to Queue 4 can go there, provided that its `position` is lesser than that of the original occupant's. – karthikaruna Dec 07 '20 at 13:36
  • *"Some item that belongs to another queue is not allowed to go there"*: I don't understand this rule, and I don't see this rule in your question. I only see two rules: (1) the order should be by descending `enqueuedTime` when it does not conflict with rule 2: (2) items from the same queue should keep the order by their `position`. It seems like you have an unwritten third rule here, something about spots that belong to certain queues. I have posted an answer that follows the two rules that I found in your question. But it gives a different output, as it does not know about this third rule. – trincot Dec 07 '20 at 13:47
  • *"items belonging to the same queue interchange positions **among themselves**, so that the one with a lower position always comes before the one with a higher position"* - this implies *"Some item that belongs to another queue is not allowed to go there"* – karthikaruna Dec 07 '20 at 13:51
  • Yes, but now you are just describing your algorithm (which first sorts, and then considers the taken positions as belonging to the queue). What I am saying is that maybe your algorithm is not optimising within the rules that you have set out in your question. Please have a look at the output that my answer produces and how it still sticks with the rules of your question (not necessarily your algorithm). – trincot Dec 07 '20 at 13:54
  • Definitely, I'll have a look. Appreciate your highly descriptive answer. – karthikaruna Dec 07 '20 at 13:56

3 Answers3

1

I don't think your code produces a result that is giving precedence to queuedAt when it should. Your algorithm first orders by queuedAt and then swaps elements that appear in the wrong position-order, but this does always not lead to an order that gives priority to queuedAt when possible.

For instance, in your output, the element with queuedAt=3 appears after the element with queuedAt=2, yet there is no reason why they could not have appeared in opposite order: that adapted ordering would still list items in their position-order when they belong to the same queue, but would give precedence to a greater queuedAt value.

Suggested Algorithm

To correct this shortcoming, and to get a better time complexity, you could use a max heap. One entry in this heap would correspond to a queue with all its elements ordered by position. The key to be used by the heap would be the queuedAt property of the queue's first element, i.e. the one with the least position value.

Then you would pick the top entry (queue) from the heap, extract the first element from that queue, and push it to the final result. If the queue is not yet empty, then leave it on the heap, but adapt its key, so it corresponds again to the queuedAt property of the queue element that now comes first (least position). If the queue is depleted, then remove it from the heap. If you continue like that until the heap is emptied, you'll have visited the entries in the desired order.

Unfortunately, JavaScript has no native heap implementation. So you'll have to throw your own, or include a library (there are several). I will include here the minified version of the implementation I posted in answer to Efficient way to implement Priority Queue in Javascript?. See there for an explanation of that code.

In the implementation below I have chosen to order the queues in reverse order, so I can pop the elements in the desired order.

Here we go:

/* MinHeap minimised - taken from https://stackoverflow.com/a/66511107/5459839 */
const MaxHeap={siftDown(h,i=0,v=h[i]){if(i<h.length){let k=v[0];while(1){let j=i*2+1;if(j+1<h.length&&h[j][0]<h[j+1][0])j++;if(j>=h.length||k>=h[j][0])break;h[i]=h[j];i=j;}h[i]=v}},heapify(h){for(let i=h.length>>1;i--;)this.siftDown(h,i);return h},pop(h){return this.exchange(h,h.pop())},exchange(h,v){if(!h.length)return v;let w=h[0];this.siftDown(h,0,v);return w},push(h,v){let k=v[0],i=h.length,j;while((j=(i-1)>>1)>=0&&k>h[j][0]){h[i]=h[j];i=j}h[i]=v;return h}};

// Main function
function customSort(data) {
  // Create a map with a key for each queue
  let map = new Map(data.map(o => [o.queueName, []]));
  // Populate the map entries
  for (let o of data) map.get(o.queueName).push(o);
  // We have no more need of the Map:
  let queues = [...map.values()];
  // Sort each queue by descending position (so we can pop from those queues in ascending order)
  for (let queue of queues) queue.sort((a, b) => b.position - a.position);

  // Build a heap of pairs, where first value in pair is the heap key, and the second is the payload (the queue)
  let heap = MaxHeap.heapify(queues.map(queue => [queue[queue.length - 1].enqueuedAt, queue]));

  let result = [];
  while (heap.length) {
    // Peek in the heap, to get the "top" queue
    let queue = heap[0][1];
    // Extract the element from that queue with the lowest position
    result.push(queue.pop());
    if (queue.length) {
      // Adapt the key of this queue on the heap
      MaxHeap.exchange(heap, [queue[queue.length - 1].enqueuedAt, queue]);
    } else {
      // Remove the now empty queue from the heap
      MaxHeap.pop(heap);
    }
  }
  return result;
}

// Example data from the question:
const data = [{id: 1,enqueuedAt: 8,queueName: 'Queue 1',position: 1},{id: 2,enqueuedAt: 7,queueName: 'Queue 2',position: 1},{id: 3,enqueuedAt: 6,queueName: 'Queue 3',position: 3},{id: 4,enqueuedAt: 5,queueName: 'Queue 4',position: 2},{id: 5,enqueuedAt: 1,queueName: 'Queue 1',position: 2},{id: 6,enqueuedAt: 2,queueName: 'Queue 4',position: 1},{id: 7,enqueuedAt: 4,queueName: 'Queue 1',position: 3},{id: 8,enqueuedAt: 3,queueName: 'Queue 2',position: 2}]

console.log(customSort(data));

The time complexity is O(nlogn), like a normal comparison-based sort

A solution without heap

It is possible to get this working in JavaScript without the use of the heap, but it requires that requiredAt is limited in range (but it's still a large range: 232): We can make use of the key iteration order that JavaScript guarantees on plain objects since the 2017-2020 updates of the language specification.

Where requiredAt was the key in the heap, it can become the key in a plain object. As you need the order to be descending we must apply a mapping of requiredAt to positive integers that can be iterated in ascending order. The mapping can be something like: 32000 - requiredAt.

Here is how that can be coded:

function customSort(data) {
  // Create a map with a key for each queue
  let map = new Map(data.map(o => [o.queueName, []]));
  // Populate the map entries
  for (let o of data) map.get(o.queueName).push(o);
  // We have no more need of the Map:
  let queues = [...map.values()];
  // Sort each queue by descending position (so we can pop from those queues in ascending order)
  for (let queue of queues) queue.sort((a, b) => b.position - a.position);

  // Build an object keyed by "negative" `queuedAt`
  const MAX = 2 ** 31 - 1;
  let sorted = Object.fromEntries(queues.map(queue => [MAX - queue[queue.length - 1].enqueuedAt, queue]));

  let result = [];
  for (let i = 0; i < data.length; i++) {
    // Use JS behaviour where index keys are iterated in numerical order
    let queuedAt;
    for (queuedAt in sorted) break;
    // Get the queue at this (first) key and remove the entry
    let queue = sorted[queuedAt];
    delete sorted[queuedAt];

    // Extract the element from that queue with the lowest position
    result.push(queue.pop());
    if (queue.length) {
      // Store the shortened queue at its new key
      sorted[MAX - queue[queue.length - 1].enqueuedAt] = queue;
    }
  }
  return result;
}

// Example data from the question:
const data = [{id: 1,enqueuedAt: 8,queueName: 'Queue 1',position: 1},{id: 2,enqueuedAt: 7,queueName: 'Queue 2',position: 1},{id: 3,enqueuedAt: 6,queueName: 'Queue 3',position: 3},{id: 4,enqueuedAt: 5,queueName: 'Queue 4',position: 2},{id: 5,enqueuedAt: 1,queueName: 'Queue 1',position: 2},{id: 6,enqueuedAt: 2,queueName: 'Queue 4',position: 1},{id: 7,enqueuedAt: 4,queueName: 'Queue 1',position: 3},{id: 8,enqueuedAt: 3,queueName: 'Queue 2',position: 2}]

console.log(customSort(data));

Final Remarks

Notice how the order is different from your output, but still is in line with the rules that you set out. The order in this solution really maximizes the priority of enqueuedAt.

trincot
  • 317,000
  • 35
  • 244
  • 286
1

A short approach could

  • sort the data by enqueuedAt,

  • group by queueName,

  • reduce the array by

    • sort any group by position,
    • taking all items at the same index at the same index in the temporary result array and finally
  • take a flat array.

const
    data = [{ id: 1, enqueuedAt: 8, queueName: 'Queue 1', position: 1 }, { id: 2, enqueuedAt: 7, queueName: 'Queue 2', position: 1 }, { id: 3, enqueuedAt: 6, queueName: 'Queue 3', position: 3 }, { id: 4, enqueuedAt: 5, queueName: 'Queue 4', position: 2 }, { id: 5, enqueuedAt: 1, queueName: 'Queue 1', position: 2 }, { id: 6, enqueuedAt: 2, queueName: 'Queue 4', position: 1 }, { id: 7, enqueuedAt: 4, queueName: 'Queue 1', position: 3 }, { id: 8, enqueuedAt: 3, queueName: 'Queue 2', position: 2 }],
    result = Object
        .values(data
            .sort((a, b) => b.enqueuedAt - a.enqueuedAt)
            .reduce((r, o) => ((r[o.queueName] ??= []).push(o), r), {})
        )
        .reduce((r, array) => (array
            .sort((a, b) => a.position - b.position)
            .forEach((o, i) => (r[i] ??= []).push(o)),
            r
        ), [])
        .flat();

console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
-1

const data = [
  {
    id: 1,
    enqueuedAt: 8,
    queueName: 'Queue 1',
    position: 1
  },
  {
    id: 2,
    enqueuedAt: 7,
    queueName: 'Queue 2',
    position: 1
  },
  {
    id: 3,
    enqueuedAt: 6,
    queueName: 'Queue 3',
    position: 3
  },
  {
    id: 4,
    enqueuedAt: 5,
    queueName: 'Queue 4',
    position: 2
  },
  {
    id: 5,
    enqueuedAt: 1,
    queueName: 'Queue 1',
    position: 2
  },
  {
    id: 6,
    enqueuedAt: 2,
    queueName: 'Queue 4',
    position: 1
  },
  {
    id: 7,
    enqueuedAt: 4,
    queueName: 'Queue 1',
    position: 3
  },
  {
    id: 8,
    enqueuedAt: 3,
    queueName: 'Queue 2',
    position: 2
  }
]

function sortThem(array) {
  const grouped = {}
  const sorted = []
  
  array.forEach(n => {
    if (!grouped[n.queueName]) {
      grouped[n.queueName] = [n];
      return;
    }
    grouped[n.queueName] = [...grouped[n.queueName], n]
  })
  
  Object.keys(grouped).forEach(n => {
    const sort = grouped[n].sort((a, b) => b.enqueuedAt - a.enqueuedAt)
    sorted.push(...sort);
  })
  
  return sorted
}

console.log(sortThem(data))

Something like this will work, grouping the items by the queueName first then sorting each group and pushing them back into the array. You might also need to sort by the queueName as well, as I think it works in this example because the queueName's are already in the correct order.

Ginger Wizard
  • 717
  • 4
  • 13