How to implement deque data structure in javascript?
Asked Answered
D

5

15

I'm Learning data structure with javascript

and my focus now on how to implement deque?

Edite: from comments below I get useful directions on how to implement deque based array. Is there a direction how to implement deque based object using class ?

enter image description here

I get understand some points like I need :

  • addFront()
  • removeFront()
  • peekFront()

  • addBack()
  • removeBack()
  • peekBack()

but I'm confused about some points :

  • how many pointers I need ? at least I know from queue I need two(head-tail) pointer but not sure if I need more in deque

  • which data type in javascript convenient in this case as a base? I saw some tutors in youtube talking about circular array for example which unknown for me in JS.

edite2:

I was following a book called: learning javascript data structures and algorithms 3rd edition

in chapter5 of this book the author started to implement Deque based on object only and some variables

but I didn't understand how he did that because the code encrypted but I can still reach to his files from and test his approach github repository

I can say that @trincot answer very close of book author approach

but when I compare the results I get this [1 = author - 2 = @trincot] : enter image description here

according to the book index taking about linked list comes in chapter6 so I didn't expect his solution will be based on something he didn't mentioned before

plz if I miss any point I will be grateful to tell me it ... thanks

Diatropism answered 4/2, 2020 at 7:56 Comment(18)
the default JS array already is such a structure: push(), pop(), shift(), unshift() as well as standard index accessing should give you all the tools you need.Burgundy
@Burgundy do you mean I don't need deque and just default JS array do the job ?Diatropism
@Burgundy I get understand that default JS array allow random access to data while deque will not allow to add,modify or delete from the middle just from the endDiatropism
@AymanMorsy, if that is your concern, then just invalidate all other methods that could be used on your array.Jez
An JS array supports all methods you need. If any other method is to be suppressed, then you will need a wrapper or something similar to restrict access (maybe also to map(), ...).Burgundy
@AymanMorsy - Just use an array as your base data structure (it automatically handles all pointers internally) and wrap it in a class/object to restrict access to the underlying arrayMartsen
this is great things to know, this comments so helpful on how to implement deque based array but let me being greedy how to implement deque based object also using class and constructor ?Diatropism
Which implementation are you looking for? One with pointers or one with circular array? They are different.Jez
@AymanMorsy What do you mean by "deque based object"?Purr
Deque supports both stack and queue operations and implemented either using a doubly linked list or circular array. So how you want to go its upto youLondalondon
@Purr I mean using Object as a container of data rather than array like trincot answer. could you read my question post again I added more detailsDiatropism
@ManjeetThakur can you take a look on my question post again I added more details... is that a third implementation approach from author ? thanksDiatropism
"I didn't understand how he did that because the code encrypted" - no it's not. You might have seen the minified code in the demo page though.Purr
@Purr the code encrypted but you can download it and test it locallyDiatropism
"when I compare the results I get this" - I don't see much of a difference. Sure, he named it items instead of data, but after all it's quite similar. No linked lists in either implementation.Purr
@AymanMorsy Please follow the link I just posted. There is no "encryption" at all.Purr
Let us continue this discussion in chat.Diatropism
The screenshot you added at the end of your post, just shows two different data structures which represent the same thing, but have front/back swapped. This does not affect the behaviour of the deque: you'll get the same output if you use the deque methods. One can implement a deque in several ways, and still have the expected behaviour.Jez
J
19

Although Wikipedia mentions that JavaScript has native support for deque operations via its Array class/prototype (push, pop, shift, unshift), this does not give the optimal time efficiency, which becomes important the larger your dataset is. If we regard the optimal time efficiency a requirement for a deque implementation, then JavaScript does not have a native implementation.

If you need good performance for larger datasets, you will want to write your own implementation. You can go for a doubly linked list, where you just need two "pointers". It should be said that in JavaScript we don't really speak of pointers, but of objects. Variables or properties that get an object as value, are in fact references in JavaScript.

Alternatively, you can go for a circular array. Since in JavaScript standard Arrays are not guaranteed to be consecutive arrays as for example is the case in C, you don't really need to use an Array instance for that. A plain object (or Map) will do.

So here are two possible implementations:

Doubly Linked List

class Deque {
    constructor() {
        this.front = this.back = undefined;
    }
    addFront(value) {
        if (!this.front) this.front = this.back = { value };
        else this.front = this.front.next = { value, prev: this.front };
    }
    removeFront() {
        let value = this.peekFront();
        if (this.front === this.back) this.front = this.back = undefined;
        else (this.front = this.front.prev).next = undefined;
        return value;
    }
    peekFront() { 
        return this.front && this.front.value;
    }
    addBack(value) {
        if (!this.front) this.front = this.back = { value };
        else this.back = this.back.prev = { value, next: this.back };
    }
    removeBack() {
        let value = this.peekBack();
        if (this.front === this.back) this.front = this.back = undefined;
        else (this.back = this.back.next).back = undefined;
        return value;
    }
    peekBack() { 
        return this.back && this.back.value;
    }
}

// demo
let deque = new Deque;
console.log(deque.peekFront()); // undefined
deque.addFront(1);
console.log(deque.peekBack()); // 1
deque.addFront(2);
console.log(deque.removeBack()); // 1
deque.addFront(3);
deque.addFront(4);
console.log(deque.peekBack()); // 2
deque.addBack(5);
deque.addBack(6);
console.log(deque.peekBack()); // 6
console.log(deque.removeFront()); // 4
console.log(deque.removeFront()); // 3
console.log(deque.removeFront()); // 2
console.log(deque.removeFront()); // 5
console.log(deque.removeFront()); // 6
console.log(deque.removeFront()); // undefined

Circular "Array"

class Deque {
    constructor() {
        this.data = {}; // Or Array, but that really does not add anything useful
        this.front = 0;
        this.back = 1;
        this.size = 0;
    }
    addFront(value) {
        if (this.size >= Number.MAX_SAFE_INTEGER) throw "Deque capacity overflow";
        this.size++;
        this.front = (this.front + 1) % Number.MAX_SAFE_INTEGER;
        this.data[this.front] = value;
    }
    removeFront()   {
        if (!this.size) return;
        let value = this.peekFront();
        this.size--;
        delete this.data[this.front];
        this.front = (this.front || Number.MAX_SAFE_INTEGER) - 1;
        return value;
    }
    peekFront()     { 
        if (this.size) return this.data[this.front];
    }
    addBack(value) {
        if (this.size >= Number.MAX_SAFE_INTEGER) throw "Deque capacity overflow";
        this.size++;
        this.back = (this.back || Number.MAX_SAFE_INTEGER) - 1;
        this.data[this.back] = value;
    }
    removeBack()   {
        if (!this.size) return;
        let value = this.peekBack();
        this.size--;
        delete this.data[this.back];
        this.back = (this.back + 1) % Number.MAX_SAFE_INTEGER;
        return value;
    }
    peekBack()     { 
        if (this.size) return this.data[this.back];
    }
}

// demo
let deque = new Deque;
console.log(deque.peekFront()); // undefined
deque.addFront(1);
console.log(deque.peekBack()); // 1
deque.addFront(2);
console.log(deque.removeBack()); // 1
deque.addFront(3);
deque.addFront(4);
console.log(deque.peekBack()); // 2
deque.addBack(5);
deque.addBack(6);
console.log(deque.peekBack()); // 6
console.log(deque.removeFront()); // 4
console.log(deque.removeFront()); // 3
console.log(deque.removeFront()); // 2
console.log(deque.removeFront()); // 5
console.log(deque.removeFront()); // 6
console.log(deque.removeFront()); // undefined

Methods will return undefined, when an attempt is made to retrieve a value from an empty deque.

In my tests the linked list solution came out as the winner as the data set grows. For small datasets sizes, the native array may turn out to be best. But it also depends on the engine and the data type that the deque elements have: the results for integers are different from the results for objects or strings. So I would advise to do some benchmarking on your actual data and deque manipulations to see which implementation will be best for your case.

Jez answered 4/2, 2020 at 10:12 Comment(12)
I suggest adding delete to the remove methods, so that this doesn't leak memory.Emanuel
@kaya3, good suggestion! Added delete in the second snippet.Jez
@Jez you are very close... I made some edite in my Q can you see it and give me an idea is that third possible implementation or just your answer and need some improve or some changing to reach the result I see above ... thanks so muchDiatropism
thanks alot trincot ... with the help of @Purr get the source code of the third implementation I hope to help anyone github.com/PacktPublishing/…Diatropism
@AymanMorsy, that implementation you refer to is really inferior. It performs a for loop to shift all items with one position, whenever you do an addFront and lowestCount is zero. That is really killing the performance gain you would expect to have from a deque. Moreover, JavaScript has an unshift method that does just all that, but more efficiently. An efficient implementation does not need to loop.Jez
yes, I heard from @Purr also that there some performance issue. for me as beginner I can't judge which is better :). I appropriate your opinion so this may help anyone later will read this book and implementing his approach... thanksDiatropism
downvoted because native Array does not have native deque operations...deque is constant time, Array is constant time for push/pop, but O(N) time for shift/unshift.Smallman
@Alexander, time complexity is an aspect of algorithm, not of data structure. This is also why Wikipedia doesn't mention time complexity as a requirement, but lists compeixity as an attribute of several different implementations, and lists JavaScript as having a deque implementation with the methods push, pop, unshift, shift.Jez
I don't believe deque is hyper-formalized, but this article does mention constant time operations as being indicative - en.wikipedia.org/wiki/Double-ended_queueSmallman
That's the article I referred to. Like I said, it lists several implementations which do not have the O(1) complexity for all four methods, and includes JavaScript as having an implementation.Jez
Is the first implementation a doubly-linked list? I can't traverse all elements by looping head.nextAerify
Yes, it is a doubly-linked list, although my code has no head, so I don't know what you mean with head.next. To iterate, start with node = deque.back and then walk via node = node.next.Jez
D
6

Dequeue implementation in a simple way:

const dequeue = [];

// push element from rear end
dequeue.push(3); // [3]
dequeue.push(8); // [3, 8]

// push element from front end
dequeue.unshift(5); // [5, 3, 8]
dequeue.unshift(11); // [11, 5, 3, 8]     

// pop element from rear end
dequeue.pop(); // [11, 5, 3]

// pop element from front end
dequeue.shift(); // [5, 3]
Dim answered 18/8, 2020 at 9:54 Comment(2)
This is wrong, in Deque pushleft should be O(1) time, but JS array unshift() takes O(n).Cybele
Calling this wrong may be too harsh. Many definitions of deque do not put requirements on time complexity, and merely consider time complexity a property of a given implementation. See Wikipedia.Jez
C
4

The tricot's Doubly Linked List, but refactored and typed:

type DequeNode<T> = {
  value: T;
  prev?: DequeNode<T>;
  next?: DequeNode<T>;
};

class Deque<T = any> {
  front?: DequeNode<T>;
  back?: DequeNode<T>;

  constructor(...initialValues: T[]) {
    initialValues.forEach(initialValue => {
      this.addBack(initialValue);
    });
  }

  addFront(value: T) {
    if (!this.front) {
      this.front = this.back = { value };
      return;
    }

    this.front = this.front.next = { value, prev: this.front };
  }

  removeFront() {
    if (!this.front) {
      return;
    }

    const value = this.peekFront();

    if (this.front === this.back) {
      this.front = undefined;
      this.back = undefined;
      return value;
    }

    (this.front = this.front.prev!).next = undefined;
    return value;
  }

  peekFront() {
    return this.front?.value;
  }

  addBack(value: T) {
    if (!this.front) {
      this.front = this.back = { value };
      return;
    }

    this.back = this.back!.prev = { value, next: this.back };
  }

  removeBack() {
    if (!this.back) {
      return;
    }

    const value = this.peekBack();

    if (this.front === this.back) {
      this.front = undefined;
      this.back = undefined;
      return value;
    }

    (this.back = this.back.next!).prev = undefined;
    return value;
  }

  peekBack() {
    return this.back?.value;
  }
}
Contrast answered 17/12, 2022 at 0:59 Comment(0)
U
0

As with any other attempt at understanding new stuff, it is helpful to have a comparative approach.

JS arrays are deques because you can modify the front out-of-the-box. You don't get this in Python where the out-of-the-box list structure supports only modification in the back (calling it append and pop). If you need to start adding and removing front items you need to explicitly add support for deques (by adding from collections import deque at the top of the module ) and create the object with a dedicated constructor (d = deque([1,2,3]).Only then you can perform popleft and appendleft operations (called unshift and shift in JS)

Again, none of this is required in JS, the implementation of JS array supports this OOTB. To get a feeling for terminology across the language landscape see the Wikipedia's table at

https://en.wikipedia.org/wiki/Double-ended_queue#Operations

Ukase answered 21/3, 2022 at 12:32 Comment(2)
The concern is the time-complexity of JavaScript's array operations. Popping an item from the front of a true Deque would have a complexity of O(1), whereas popping from the front of a simple array is O(n) due to each successive item needing to be shifted by one position left.Listed
for more on the above, see leetcode.com/explore/learn/card/fun-with-arrays/525/…Ukase
E
0

You can easily do so using typescript and javascript. Here is a simple implementation with the possibility of fixed deque as in Python.

export class Deque<T> extends Array<T> {
  private readonly size: number;
  constructor(size?: number) {
    super();
    this.size = size;
  }
  override push(...items: T[]): number {
    if (!this.size || this.length < this.size) return super.push(...items);
    while (this.length > this.size - items.length) {
      super.shift();
    }
    super.push(...items);
  }
}

Use as follows:

const open: Deque<number> = new Deque(5);
Emeritaemeritus answered 10/12, 2023 at 1:6 Comment(1)
Instead of silently dropping elements when they don't fit into your size, I would recommend throwing an exceptionPurr

© 2022 - 2024 — McMap. All rights reserved.