Git Product home page Git Product logo

phase-4-data-structures-doubly-linked-list's Introduction

Doubly Linked List Data Structure

Learning Goals

  • Identify the use cases for a doubly linked list
  • Demonstrate common methods for a doubly linked list
  • Differentiate between a doubly linked list and a singly linked list

Introduction

We learned in the last lesson about singly linked lists, their use cases, and the general concept behind linked lists. In this lesson, we're going to dive into what a doubly linked list is, and what the difference is between singly and doubly linked lists.

Defining a Doubly Linked List

In the previous lessons we learned how unlike arrays, linked lists are not indexed, and in order to search from node to node, we need use pointers to go from one node onto the next node. But what if we wanted to go back a node? In a singly linked list, a node doesn't know which node came before it, because it doesn't have a pointer pointing to the previous node. Doubly linked lists have pointers to the next node as well as the previous node. Let's take a look at how we would build this out in our Node class:

class Node
  attr_accessor :data, :next_node, :prev_node

  def initialize(data, next_node = nil, prev_node = nil)
    @data = data
    @next_node = next_node
    @prev_node = prev_node
  end
end

All we really had to do was a prev_node attribute, and now we have two pointers on our Node class, so that each node points in two directions: to the next node in the list, and to the previous node. While this is a really small and easy change to make to the structure of our node, by doing this change we are able to make our linked list much more useful, efficient, and dynamic!

Pup Linked List

Singly vs Doubly Linked Lists

One way we can improve the time complexity of our singly linked list implementation is by adding additional references to nodes in the list. For example, consider the following SinglyLinkedList class:

class SinglyLinkedList
  attr_accessor :head

  def initialize
    @head = nil
  end

  def append(node)
    if head.nil?
      self.head = node
    else
      # traverse the entire list to find the last node
      curr = head
      while curr.next_node
        curr = curr.next_node
      end
      # add a new node at the end
      # 1 -> 2 -> 3
      curr.next_node = node
      # 1 -> 2 -> 3 -> 4
    end
  end
end

The time complexity of its append method is O(n), since we need to traverse each element of the linked list in order to find the last node and remove it. We can make the append method efficient by keeping track of the list's tail node in addition to the head:

class SinglyLinkedList
  attr_accessor :head, :tail

  def initialize
    @head = nil
    @tail = nil
  end

  def append(node)
    if head.nil?
      self.head = node
      self.tail = node
    else
      # no need to traverse! we can access the last node directly (self.tail)
      # 1 -> 2 -> 3
      self.tail.next_node = node
      # 1 -> 2 -> 3 -> 4
      # we also need to make sure to keep track of the new tail
      self.tail = node
    end
  end
end

After this refactor, the time complexity of our append method is O(1), since we no longer need to traverse the list in order to find the tail before appending a new node. The tradeoff to keeping references to additional nodes in our list, like the tail, is it takes more space to keep track of these additional references. Adding additional reference can also increase the written complexity of our code for certain methods, since we need to maintain those references correctly.

A doubly linked list makes insertion and removal more efficient in certain cases by keeping references to the previous node in addition to the next node.

Let's say we have a singly linked list, and we wanted to remove the last item. We would have to traverse the entire list in order to find the second to last node in the list and assign it as the new tail, since we can't go directly to the tail and work backwards:

class SinglyLinkedList
  attr_accessor :head, :tail

  def initialize
    @head = nil
    @tail = nil
  end

  def delete_tail
    return if head.nil?

    # traverse the entire list to find the second-to-last node (prev)
    curr = head
    prev = nil
    while curr.next_node
      prev = curr
      curr = curr.next_node
    end

    # remove the last node by removing the link between the second-to-last node and the tail
    # 1 -> 2 -> 3
    prev.next_node = nil
    # 1 -> 2
  end
end

In this case, the time complexity for removing a node from the end of the list is O(n) since we need to traverse the list to find the second-to-last node.

With a doubly linked list, we already have a pointer on each node pointing to the previous node, so we can just take one step backwards from the tail by using .prev_node without needing to iterate:

class DoublyLinkedList
  attr_accessor :head, :tail

  def initialize
    @head = nil
    @tail = nil
  end
  
  def delete_tail
    return if head.nil?
    
    if head == tail
      self.head = nil
      self.tail = nil
    else
      # access the second-to-last node (self.tail.prev_node)
      prev = self.tail.prev_node
      # update the tail and next_node pointers
      # 1 <-> 2 <-> 3
      prev.next_node = nil
      self.tail = prev
      # 1 <-> 2
    end
  end
end

After this refactor, our time complexity for removing a node from the end of the list is O(1), since we don't need to traverse the entire list to find the new tail.

The tradeoff is that we now need to maintain these references between nodes correctly for all our linked list methods. For example, the append method for a doubly linked list is more complicated than for a singly linked list, since we have to keep track of the next_node and prev_node correctly any time a node is added:

class DoublyLinkedList
  attr_accessor :head, :tail

  def initialize
    @head = nil
    @tail = nil
  end

  def append(node)
    if head.nil?
      self.head = node
      self.tail = node
    else
      node.prev_node = self.tail
      tail.next_node = node
      self.tail = node
    end
  end
end

Conclusion

Singly linked lists and doubly linked lists are very similar. Unlike arrays, they are not indexed, and they use pointers to reference their nodes. Singly linked lists are one-directional, while doubly linked lists go both ways. Having the extra prev_node pointer in a doubly linked list can be useful in the following scenarios:

  • Removing an item form the end of a list
  • Reversing the list (traversal from tail to head)
  • Implementing "previous/next" operations or "undo/redo" operations (like building a playlist or a text editor)

The trade-offs are that a doubly linked list takes up more memory than to a singly linked list, since we have to keep track of multiple pointers on each node; and the code for a doubly linked list is also more complicated to write and maintain because of the added complexity of keeping both the next_node and prev_node references.

Resources

phase-4-data-structures-doubly-linked-list's People

Contributors

graciemcguire avatar ihollander avatar

Watchers

 avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.