5
\$\begingroup\$

A doubly linked list contains elements that include pointers to the previous and next element along with a value. This data structure is implemented with methods similar to what you would find in an array: insert or remove elements, read the value at an index or find an index that contains a value.

Just looking to see if my implementation is good. Did I make any mistakes? Did I do anything unusual?

function Node(v, p, n) {
  this.v = v;
  this.p = p;
  this.n = n;
}

function DoublyLinkedList() {
  this.size = 0;
  this.head = null;
  this.tail = null;

  this.append = (v) => {
    if (this.size === 0) {
      this.head = this.tail = new Node(v, null, null);
    } else {
      this.tail.n = new Node(v, this.tail, null);
      this.tail = this.tail.n;
    }
    this.size++;
  };

  this.prepend = (v) => {
    if (this.size === 0) {
      this.head = this.tail = new Node(v, null, null);
    } else {
      this.head.p = new Node(v, null, this.head);
      this.head = this.head.p;
    }
    this.size++;
  };

  this.find = (v) => {
    let current = this.head;
    let index = -1;

    while (current) {
      index++;
      if (current.v === v) {
        return index;
      } else {
        current = current.n;
      }
    }
    return -1;
  };

  this.read = (v) => {
    if (v >= this.size) return null;
    let current = this.head;
    let step = 0;

    while (step < v) {
      step++;
      current = current.n;
    }
    return current.v;
  };

  this.insert = (v, i) => {
    let current = this.head;
    let step = 0;

    if (i === 0) {
      this.prepend(v);
    } else if (i === this.size) {
      this.append(v);
    } else {
      while (step < i) {
        step++;
        current = current.n;
      }
      current.p.n = current.p = new Node(v, current.p, current);
      this.size++;
    }
  };

  this.remove = (i) => {
    let current = this.head;
    let step = 0;

    if (i === 0 && this.size === 1) {
      this.head = this.tail = null;
    } else if (i === 0) {
      this.head = this.head.n;
      this.head.p = null;
    } else if (i === this.size - 1) {
      this.tail = this.tail.p;
      this.tail.n = null;
    } else {
      while (step < i) {
        step++;
        current = current.n;
      }
      current.p.n = current.n;
      current.n.p = current.p;
      current.v = current.p = current.n = null;
    }
    this.size--;
  };
}
```
\$\endgroup\$
3
  • 2
    \$\begingroup\$ I'd suggest using classes, unless browser compatibility is your concern. developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… \$\endgroup\$
    – slepic
    Commented Jun 29, 2022 at 6:07
  • 2
    \$\begingroup\$ I would add some checks to both remove and insert to make sure you are getting better error messages whenever you try to do something stupid such as remove(-1) or DoublyLinkedList().insert({}, 1) \$\endgroup\$
    – munHunger
    Commented Jun 29, 2022 at 8:46
  • \$\begingroup\$ New question with improvements \$\endgroup\$
    – user85459
    Commented Jun 29, 2022 at 21:11

1 Answer 1

3
\$\begingroup\$

First, a bug: read() doesn't properly check negative indices; list.read(-42) currently returns the first value (or throws an exception on an empty list).

As slepic pointed out, unless you want to support dinosaur browsers, you should be using class syntax instead of constructor functions; The way you have it right now, every DoublyLinkedList you create has a copy of every method stored on each individual object. The performance implications are not good. The class syntax automatically puts methods on the prototype instead, which also allows inheritance. (You don't need to understand prototypes unless you're doing something fancy, just stick to class syntax.)

I'm not sure I like using single letters for your member and parameter names. You definitely shouldn't be using "i" as a parameter name, since i is typically used as the index variable in a loop, making it very confusing.

insert(value, index) is confusing, the idiom would be insert(index, value).

The name "Node" already exists in JavaScript. This isn't a problem if your code is in a module, but otherwise might accidentally shadow the original.

Your code should probably return undefined for missing elements instead of null, here're some reasons:

  1. Users might want to store null in the list.
  2. It's consistent with Array and Map.
  3. It plays nice with the optional chaining operator ?. (which returns undefined).
  4. typeof null === "object" (all sorts of horrifying).

I also prefer using undefined internally so that I can pretend null doesn't exist.

size should be externally readonly; This means that the list can change its own size, but the user can't accidentally set size to some incorrect number. You can do this by using a private field and a public getter.

There are a couple bits of code that can be factored out: Getting a Node from an index and inserting before/after a given Node. This simplifies code and prevents mistakes/unmaintainability due to repetition.

class DoublyLinkedList {
    // The hashtag at the start of member names makes them private
    // (you might also come across code that uses underscores _ instead).
    // Predeclaring field names like this is optional for public fields.
    #head;
    #tail;
    // size is private to avoid user messing with it;
    // We'll give them access with a getter instead:
    #size;

    // This is a getter; It acts like a normal field, but the user
    // can only read it (i.e. they cannot set the size themselves).
    get size() { return this.#size; }

    // This is assuming you don't want to expose these functions publicly.
    #getNode(index) {
        if (index < 0 || index >= this.#size) return undefined;

        let current = this.#head;
        for (let step = 0; step < index; ++step) {
            current = current.n;
        }

        return current;
    }

    #insertBefore(node, value) {
        const newNode = new Node(value, node.p, node);

        if (node.p) node.p.n = newNode;
        else this.#head = newNode;

        node.p = newNode;

        ++this.#size;
    }

    // Doesn't really refactor anything, but it'd be weird not to have:
    #insertAfter(node, value) {
        const newNode = new Node(value, node, node.n);

        if (node.n) node.n.p = newNode;
        else this.#tail = newNode;

        node.n = newNode;

        ++this.#size;
    }

    // With this, you can refactor your class as:

    constructor() {
        this.#size = 0;
        this.#head = undefined;
        this.#tail = undefined;
    }

    append(value) {
        if (this.#size === 0) {
            this.#head = this.#tail = new Node(value, undefined, undefined);
            this.#size = 1;
            return;
        }

        this.#insertAfter(this.#tail, value);
    }

    prepend(value) {
        if (this.#size === 0) return this.append(value); // Reusing append

        this.#insertBefore(this.#head, value);
    }

    find(value) {
        let current = this.#head;

        // Using a for-loop is cleaner:
        for (let i = 0; current; ++i) {
            if (current.v === value) return i;
            current = current.n;
        }

        return -1;
    }

    read(index) { // Original's parameter had a typo: read(v) -> read(index)
        const node = this.#getNode(index);
        if (node) return node.v;

        return undefined;

        // Shorter using optional chaining: return this.#getNode(index)?.v;
    }

    // This function should return true/false to indicate success.
    // I'd personally throw an Error on failure instead,
    // since something's gone wrong if you have the wrong index.
    // Your code does unintentionally throw an Error as-is,
    // but the error message isn't appropriate.
    // Also note the swapped parameter order.
    insert(index, value) {
        if (index < 0 || index > this.#size) return false;

        if (index === this.#size) {
            this.append(value);
            return true;
        }

        this.#insertBefore( this.#getNode(index), value );

        return true;
    }

    // Following Array's conventions, this should return the removed value
    // or undefined if it didn't exist (could also throw Error).
    remove(index) {
        const node = this.#getNode(index);
        if (!node) return undefined;

        // Instead of having many special cases based on the index and list's size,
        // just check if node the has neighbours and act accordingly.
        // This is cleaner than having cases for every possible combination
        // of the p and n nodes existing.

        if (node.p) node.p.n = node.n;
        else this.#head = node.n;

        if (node.n) node.n.p = node.p;
        else this.#tail = node.p;

        --this.#size;

        return node.v;
    }
}

A few suggestions:

  • Currently, none of your code really needs the list to be doubly linked; You could switch to a singly linked list instead and save memory/performance/complexity, or you could add things like a reverse iterator which lets the user take advantage of it.
  • You could modify your functions to check if the index is more than halfway through the list and iterate backward if so. This would half the average number of nodes you need to traverse (though it might perform worse on small lists).

Don't let my rant discourage you though, good work!

\$\endgroup\$
6
  • \$\begingroup\$ I find your answer rather educative, well done! \$\endgroup\$
    – coderodde
    Commented Jun 29, 2022 at 10:30
  • \$\begingroup\$ Same, love " I can pretend null doesn't exist." \$\endgroup\$
    – konijn
    Commented Jun 29, 2022 at 12:48
  • \$\begingroup\$ -1 Your advice to use class syntax is ill informed nor have you encapsulated your list object, it can not be trusted, (underscores do not prevent access) and there are several poor style habits in the example code \$\endgroup\$
    – Blindman67
    Commented Jun 29, 2022 at 13:35
  • 1
    \$\begingroup\$ @Blindman67 It would be great if you could explain what the class suggestion is missing and which style habits need improving. As for private members, there's the underscore convention (only madmen call underscore functions externally), private members with # (which used to have bad dev-tool support, but I now realize this is fixed), or closures (overkill). Op will likely run into underscores at some point, and their API would probably be defined by JSDoc/TypeScript anyway, but I'll edit my answer to use #. \$\endgroup\$
    – Ifthel
    Commented Jun 29, 2022 at 18:27
  • 1
    \$\begingroup\$ @ToniJarjour I added an important edit (besides private fields) showing the use of a getter to make size readonly. This way users can't break your class by setting size themselves. \$\endgroup\$
    – Ifthel
    Commented Jun 29, 2022 at 19:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.