DEV Community

Cover image for Typescript Data Structures: Linked List

Typescript Data Structures: Linked List

Gleb Irovich on July 24, 2020

In today's episode of Typescript 101, we continue talking about data structures and their implementation in Typescript. Our today's patient is the ...
Collapse
 
matvs profile image
Mateusz Swiatkowski

Hi Gleb, nice and clear article :)

Could you elaborate on "Recursive functions are a great substitute of while loops for the tasks like traversing when we don't know the size of the list before we start iterating."?

I don't see what prior knowledge of the list size has to do with substituting while loop for a recursive call.

Traverse method could be easily implemented with while loop and it has an advantage of not worrying about exceeding a call stack.

public traverse(): T[] {
    const array: T[] = [];
    let currentNode = this.head;
    while(currentNode) {
        array.push(currentNode.data);
        currentNode = currentNode.next;
    }
    return array;
  }
Enter fullscreen mode Exit fullscreen mode

Also traverse method is a good candidate for using a technique called "tail recursion", but for JavaScript tail call is currently supported only by Safari/WebKit.

Collapse
 
glebirovich profile image
Gleb Irovich

Hey Mateusz, thanks for your feedback! Probably, the sentence is not very well structured. What I meant is that in the traverse example we don’t have prior knowledge of the size and therefore we cannot use for loops. However we can use a while loop or alternatively a recursive function.
It’s a great input regarding the call stack overflow πŸ‘πŸ‘πŸ‘, I definitely missed it out of sight, since I alway tend to be biased toward functional approaches.

Collapse
 
zilti_500 profile image
Daniel Ziltener

Here's the thing with double linked lists: they are just shitty vectors/"arraylists"/you-name-it. With double linked lists, you throw away all advantages you get with single linked lists, which is being able to add elements to the list at pretty much zero cost and having it be immutable on top of it, at the cost of random access being expensive.

With double linked lists, you still have the high cost of random access, but you are going to modify the list when adding elements.

It is definitely a neat idea in theory if what you want is a single linked list that is, well, linked in both directions, but other than that, I always felt a double-linked list is not something to be used in real-world scenarios (maybe except some fringe scenarios?).

And linked lists are definitely a very good example when making one's first steps in functional programming, and this blog post neatly demonstrates that. It is, I'd say, the data structure where even an object-oriented programmer will likely naturally resort to functional "patterns".

Good post, I just wanted to chime in with some considerations for real-world use.

Collapse
 
glebirovich profile image
Gleb Irovich

Hey Daniel thanks for your feedback! Actually writing those articles about data structures is a way to research on that topics. I also must admit, I don’t have any real scenarios where I would need a linked list, so I would be curious to know if you are using it in production and how.
Could you also elaborate on the idea of β€œimmutable lists” I find it very interesting, but I am not quite sure how it is supposed to work. Don’t you have to mutate at least the β€˜next’ field?

Collapse
 
zilti_500 profile image
Daniel Ziltener

So the way this immutability works in a single linked list is that when you have a list B-C-D, B points to C, and C points to D. C doesn't know about B at all, all it knows is its own value, and where C is! So when you want to "add" to the list you simply create a "cell" that contains the value of A and has "next" point to B. The original list doesn't know anything about being longer now. It is the simplest extensible immutable data structure possible.

As you can see, a major downside of this is that you can only prepend to the list, and not append. Doing the latter would mean that either the list has to be mutable and you can change the last cell to point to the new value instead of to nothing; or to copy the entire list with a different last cell; or do some "black magic" with complicated-but-fast tree structures that make it seem like you copied the entire collection without actually doing the full thing (Clojure does the latter with all its collections).

A good use case - and the only one I can think of off the top of my head - of single linked lists are lazy sequences. Many functional programming languages make use of that. To make an example from your post, the search function (often called filter in functional programming). It would return a lazy sequence and only continue to search/filter as you continue to consume the sequence. Accessing the "next" field then just triggers the next filtering step. That only really works with single-linked lists because they don't have true random access.

Thread Thread
 
glebirovich profile image
Gleb Irovich

Daniel, thanks for the detailed response! That was very insightful. Do you think one could achieve a behaviour similar to the lazy sequence by using JS generators, that yield next item from the list, when called?

Collapse
 
glebirovich profile image
Gleb Irovich

As guess you can keep track of the tail instead of the head and linking new items to prev, so that the added ones remain unchanged. Reverse linking, is it something you had in mind?

Collapse
 
prakashreddy profile image
Prakash-Reddy08

Delete method should look like this

if (!node.prev) {
            this.head = node.next;
        } else {
            const prevNode = node.prev;
            prevNode.next = node.next;
            if (node.next && node.next.prev) {
                node.next.prev = prevNode;
            }
        }
Enter fullscreen mode Exit fullscreen mode
Collapse
 
mlynchdev profile image
mlynchdev

I think your delete method is missing some steps.

public deleteNode(node: Node<T>): void {
    if (!node.prev) {
      this.head = node.next;
// Here node.next will still point at the deleted node (doubly linked list)
// suggest node.next.prev = null or something like this.
    } else {
      const prevNode = node.prev;
      prevNode.next = node.next;
// Here node.next will still point at the deleted node (doubly linked list)
// suggest node.next.prev = node.prev    
    }
  }
Enter fullscreen mode Exit fullscreen mode
Collapse
 
cihatsaman profile image
Cihat Saman

Hi Gleb , article was clear and so useful :)
May I know how can I use the deleteNode function ? Because I can see , you did not use on end of the whole code with example. How can I call that function I mean which arguments should I use with that deleteNode(node) function? May I know what is a sample node ?
Thanks :)

Collapse
 
glebirovich profile image
Gleb Irovich • Edited

Hey Cihat! Thank you for the feedback.
deleteNode as you can see from the type definition requires reference to the node stored in the list. Here is an example of searching and deleting a particular node:

interface Post {
  title: string;
}
const linkedList = new LinkedList<Post>();

linkedList.insertAtEnd({ title: 'Post A' });
linkedList.insertAtEnd({ title: 'Post B' });
linkedList.insertInBegin({ title: 'Post C' });
linkedList.insertInBegin({ title: 'Post D' });

const node = linkedList.search(({ title }) => title === 'Post A');
linkedList.deleteNode(node);
console.log(linkedList.traverse()); // [{ title : "Post D" }, { title : "Post C" }, { title : "Post B" }];
Enter fullscreen mode Exit fullscreen mode
Collapse
 
cihatsaman profile image
Cihat Saman

Thanks @glebirovich for quick answer :)

Collapse
 
barzilaysapir profile image
Sapir Barzilay • Edited

Hi, great article.
I don't get it, why on insertToBegin of a double linked list,
if the list is empty you're using :
this.head = new Node(),
and on the same condition when insertAtEnd, you're using:
this.head = node.
or maybe it's just an example you can do it both ways?
Thought I got it at first but then I got a little confused, would like to clear this up!
Thanks.

Collapse
 
glebirovich profile image
Gleb Irovich

Hey Sapir,

Thanks for pointing out this method. You are right, I should have assigned node, instead of instantiating a new class in the if clause:

  public insertInBegin(data: T): Node<T> {
    const node = new Node(data);
    if (!this.head) {
      this.head = node;
    } else {
      this.head.prev = node;
      node.next = this.head;
      this.head = node;
    }
    return node;
  }
Enter fullscreen mode Exit fullscreen mode

It's a correct way because it also ensures that we return a reference to the node, which is assigned to the head. Thanks, I updated the code!

Collapse
 
barzilaysapir profile image
Sapir Barzilay • Edited

Got it! thank you.

Collapse
 
chukwuemekaigbokwe profile image
Chukwuemeka Igbokwe

Nice article πŸ‘πŸ‘

Collapse
 
glebirovich profile image
Gleb Irovich

Thanks Alex!

Collapse
 
nsholmes profile image
NsHolmes

Hi Gleb,
Thanks for the article. I like the recursive function in the insertAtEnd. Shouldn't the recursive call to getLast(node) be getLast(node.next)? So that the next node will be checked?

Collapse
 
glebirovich profile image
Gleb Irovich

Hey! You are totally right, nice catch. I will update the code!

Collapse
 
rapdash profile image
Frederick "Fritz" Johnson

Created an account just to thank you for posting this. Fantastic stuff.

Collapse
 
glebirovich profile image
Gleb Irovich

Hey Frederick! Thank you a lot!