2

Indexing (maintaining indices) in an array makes Array.prototype.shift and Array.prototype.unshift O(N) instead of O(1).

However, if we just want to use pop() / push() / shift() and unshift() and never use indices for lookup, is there a way to implement a JavaScript array that omits indexing?

I can't think of a way to do it. The only way I can think of doing it would be with arrays, and only using pop() / push() (since those are O(1)) ... but even with multiple arrays, not sure if it's possible.

Looking to do this w/o a linked-list if possible. I implemented a solution to this with a doubly linked list, but wondering if it's possible to do this w/o a linked-list.

End goal: trying to create a FIFO queue where all operations are in constant time, without using a linked-list.

Alexander Mills
  • 90,741
  • 139
  • 482
  • 817
  • Just use an object or Map and build those methods around them? – Felix Kling Jun 04 '18 at 21:48
  • You can build a Linked List, it would support all the operations you mentioned in O(1) – Minderov Jun 04 '18 at 21:50
  • Using only Objects/map - it's impossible to retrieve both the first/last inserted item in O(1). You have to use a linked-list with an object/map in order to retrieve first/last item in O(1). – Alexander Mills Jun 04 '18 at 21:50
  • @Minderov looking to do this w/o a linked list, if possible – Alexander Mills Jun 04 '18 at 21:50
  • *"it's impossible to retrieve both the first/last inserted item in O(1)"* Sure it is, you just have to remember the fist and last key. I mean, you would not just use an object or map, you would build a class around it. Or use two arrays. – Felix Kling Jun 04 '18 at 21:52
  • @FelixKling first and last key won't work without a linked list, go through it your head, I had the same line of thought before. I can explain if you want. And doing it with two arrays won't work either. – Alexander Mills Jun 04 '18 at 21:56
  • 1
    @FelixKling but what if you remove the last inserted item? You'd have to find the last last item to save it as the last key? Sounds like you'd need to build another data structure just to support that – Minderov Jun 04 '18 at 21:56
  • @FelixKling yep, what Minderov said. – Alexander Mills Jun 04 '18 at 21:57
  • I added details to the question, I implemented a solution to this using a doubly linked list, but I am wondering if there is a simpler way, if you only need to implement push()/pop()/shift()/unshift() all in O(1). You'd have to omit indexing somehow. – Alexander Mills Jun 04 '18 at 21:59
  • Since push and pop are O(1), you could try to use multiple arrays, and avoid shift/unshift, but I can't figure out if that's possible. – Alexander Mills Jun 04 '18 at 21:59
  • 1
    Not really clear what higher level problem you are trying to solve – charlietfl Jun 04 '18 at 22:06
  • @charlietfl trying to create a FIFO queue where all operations are in constant time. – Alexander Mills Jun 04 '18 at 22:09
  • If you create a FIFO queue with a simple array, shift/unshift are always going to be O(N), and at least one of shift/unshift are imperative to use for a queue. – Alexander Mills Jun 04 '18 at 22:09
  • Relevant question: https://stackoverflow.com/questions/34292087/is-there-anything-that-guarantees-constant-time-for-accessing-a-property-of-an-o?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa – Adam Jenkins Jun 04 '18 at 22:20
  • @Adam sure, what I asking it simple to do the best with the language as possible, looks like since Map, etc are guaranteed to sublinear we can work with Map. – Alexander Mills Jun 04 '18 at 22:24
  • For FIFO do you really need `unshift()`? Been trying to figure that one out using a Map and is the only functionality that complicates it – charlietfl Jun 04 '18 at 22:35
  • @charlietfl for a FIFO using an array, you would need either shift or unshift depending on which direction it was going. But the fact that you need at least one of them is the source of the problem and motivator of the OP. – Alexander Mills Jun 04 '18 at 22:49
  • Right but with Map.set() all entries are in order they were added. So easy to track first and last but goes to crap if you have to unshift but unshift isn't FIFO – charlietfl Jun 04 '18 at 22:53

3 Answers3

1

How about an ES2015 Map that you index with integers?

Let's call the map myFIFOMap.

You keep a first and last integer member as part of your FIFO class. Start them both at zero.

Every time you want to push() into your FIFO queue, you call myFIFOMap.set(++last,item). And pop() looks something like:

const item = myFIFOMap.get(first); 
myFIFOMap.delete(first++); 
return item;

Should be O(1) to push or pop.

Don't forget to check for boundary conditions (e.g., don't let them pop() when first===last).

Given that JavaScript actually uses double precision floating point, you should be able to run ~2^53 objects through your FIFO before you have problems with the integer precision. So if you run 10,000 items through your FIFO per second, that should be good for around 28,000 years of run time.

SomeCallMeTim
  • 4,503
  • 2
  • 28
  • 27
0

If the data you are storing is primitive (string, integers, floats, or combinations of primitives), you can use a JavaScript TypedArray, cast it into an appropriate typed array view, load it with data, and then keep track of the offset(s) yourself.

In your example, pop, shift, and unshift can all implemented by incrementing/decrementing an integer index. push is more difficult, because a TypedArray is a fixed size: if the ArrayBuffer is full, the only two options are to truncate the data, or allocate a new typed array, since JS cannot store pointers.

If you are storing homogeneous objects (they have the same properties), you can save each value into a TypedArray using different views and offsets to mimic a C struct (see the MDN example), and then use a JS function to serialize/unserialize them from the TypedArray, basically converting the data from a binary representation, into a full-fledged JS object.

ouni
  • 3,233
  • 3
  • 15
  • 21
  • Yeah this won't work, because (a) need to be general and not just store primitives and (b) cannot be fixed size since most likely use case is as a FIFO queue. – Alexander Mills Jun 04 '18 at 22:12
0

Going with @SomeCallMeTim 's answer, which I think is on the right track, I have this:

export class Queue {

  lookup = new Map<number, any>();
  first = 0;
  last = 0;
  length = 0;
  elementExists = false; // when first === last, and item exists there

  peek() {
    return this.lookup.get(this.first);
  }

  getByIndex(v: number) {
    return this.lookup.get(v);
  }

  getLength() {
    return this.length;
  }

  pop() {

    const last = this.last;

    if (this.elementExists && this.first === this.last) {
      this.length--;
      this.elementExists = false;
    }
    else if (this.last > this.first) {
      this.length--;
      this.last--;
    }

    const v = this.lookup.get(last);
    this.lookup.delete(last);
    return v;
  }

  shift() {

    const first = this.first;

    if (this.elementExists && this.first === this.last) {
      this.length--;
      this.elementExists = false;
    }
    else if (this.first < this.last) {
      this.length--;
      this.first++;
    }

    const v = this.lookup.get(first);
    this.lookup.delete(first);
    return v;
  }

  push(v: any) {

    this.length++;

    if (this.elementExists && this.first === this.last) {
      this.last++;
    }
    else if (this.first === this.last) {
      this.elementExists = true;
    }
    else {
      this.last++;
    }

    return this.lookup.set(this.last, v);

  }

  enq(v: any) {
    return this.push.apply(this, arguments);
  }

  enqueue(v: any) {
    return this.push.apply(this, arguments);
  }

  deq() {
    return this.shift.apply(this, arguments);
  }

  dequeue() {
    return this.shift.apply(this, arguments);
  }

  unshift(v: any) {

    this.length++;

    if (this.elementExists && this.first === this.last) {
      this.first--;
    }
    else if (this.first === this.last) {
      this.elementExists = true;
    }
    else {
      this.first--;
    }

    return this.lookup.set(this.first, v);
  }

  addToFront(v: any){
    return this.unshift.apply(this,arguments);
  }

  removeAll() {
    return this.clear.apply(this, arguments);
  }

  clear(): void {
    this.length = 0;
    this.elementExists = false;
    this.first = 0;
    this.last = 0;
    this.lookup.clear();
  }

}

takeaways:

  1. it turns out, you can call getByIndex(), as Tim's suggestion points out.

  2. Using Map is surprisingly ~10% faster than POJSO, possibly only because with a POJSO the integers need to get converted to strings for lookup.

  3. The Map implementation is about 20% faster than doubly-linked list, so a doubly-linked list is not that much slower. It's probably slower mostly because we must create a container object with next/prev pointers for each item in the queue, whereas with the non-linked list implementation, we can insert primitives in the queue, etc.

  4. The doubly-linked list allows us to remove/insert items from the middle of the queue in constant time; we cannot do the same with the non-linked list implementation as is.

  5. All of the above are orders of magnitude more performant than a plain array when operating on an array with more than 10,000 elements or so.

I have some constant time queue implementations here: https://github.com/ORESoftware/linked-queue

Tim had a good suggestion, to make getByIndex() easier to use - we can do this:

  getByIndex(v: number) {

    if(!Number.isInteger(v)){
      throw new Error('Argument must be an integer.');
    }

    return this.lookup.get(v + this.first);
  }

that way to get the 5th element in the queue, all we need to do is:

getByIndex(4);
Alexander Mills
  • 90,741
  • 139
  • 482
  • 817
  • 1
    So, what I was talking about is possible after all ;) – Felix Kling Jun 05 '18 at 00:01
  • 1
    Just out of curiosity, why do you think this is better than a Linked List? I don't see accessing an element in `Map` being faster than fetching an element form the tail of a Linked List. According to [ECMAScript Specs](http://www.ecma-international.org/ecma-262/6.0/index.html#sec-map-objects) `Map` access time is on average less than O(n) versus O(1) of a Linked List – Minderov Jun 05 '18 at 00:12
  • @FelixKling yeah I didn't think about using incrementing/decrementing integers , but that makes it possible to reference previous items – Alexander Mills Jun 05 '18 at 00:23
  • @Minderov in this case the only way to know for sure is to try the two different solutions and then see which one is faster. – Alexander Mills Jun 05 '18 at 00:23
  • @AlexanderMills I agree. If you are going to test it, please come back with the results after. Very curious! – Minderov Jun 05 '18 at 00:31
  • @Minderov we can implement this solution without a map, and just POJSOs and that might be faster than a Map. Looking up keys with POJSOs should be O(1), and if not, that look up the keys in a linked list wouldn't be either so whatever. Here is a working linked-list implemenation https://github.com/ORESoftware/linked-queue/tree/master/src – Alexander Mills Jun 05 '18 at 00:33
  • linked-queue.ts is a queue implemented with doubly-linked list, whereas queue.ts is what I am working on right now. – Alexander Mills Jun 05 '18 at 00:33
  • @Minderov Map is 10% faster than POJSO, not sure why, but perhaps because with Map you don't have to convert a number to a string each time? aka, new Map().get(0), the 0 does not need to get converted to a string, whereas {}[0], the zero does get converted. – Alexander Mills Jun 05 '18 at 01:53
  • By the way: You don't need to track length. You can use `Map.prototype.size`. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/size – SomeCallMeTim Jun 05 '18 at 17:07
  • Also: Do you want `getByIndex()` to return the _relative_ index? As in, the first element in the list is index 0? In that case, you should add `first` to the index passed in. This works even if `first` is negative. – SomeCallMeTim Jun 05 '18 at 17:10
  • Yeah I can probably get rid of length, and just use `new Map().size`, you're right. – Alexander Mills Jun 07 '18 at 23:35