Linked List is a very commonly used linear data structure which consists of group of nodes in a sequence. Each node holds its own data and the address of the next node hence forming a chain like structure.
Linked Lists are used to create trees and graphs.
There are 3 different implementations of Linked List available, they are:
Singly linked lists contain nodes which have a data part as well as an address part i.e. next
, which points to the next node in the sequence of nodes.
The operations we can perform on singly linked lists are insertion, deletion and traversal.
@ToString @NoArgsConstructor @AllArgsConstructor @Data public class Customer { private String firstName; private String lastName; private String email; }
@ToString @Data public class SingleNode { private Customer data; private SingleNode next; public SingleNode(Customer data) { this(data, null); } public SingleNode(Customer data, SingleNode next) { this.data = data; this.next = next; } }
@NoArgsConstructor @Data public class MySingleLinkedList { int size = 0; private SingleNode head; private SingleNode tail; public MySingleLinkedList(SingleNode head) { append(head); } /** * add to end of list<br> * 1. If the Linked List is empty then we simply, add the new Node as the Head of the Linked List.<br> * 2. If the Linked List is not empty then we find the last node, and make it' next to the new Node, hence making * the new node the last Node. * * Time complexity of append is O(n) where n is the number of nodes in the linked list. Since there is a loop from * head to end, the function does O(n) work. This method can also be optimized to work in O(1) by keeping an extra * pointer to the tail of the linked list. */ public void append(SingleNode node) { if (node == null) { return; } if (this.head == null) { this.head = node; this.tail = node; } else { this.tail.setNext(node); this.tail = node; } size++; } /** * append to front of list<br> * 1. The first Node is the Head for any Linked List.<br> * 2. When a new Linked List is instantiated, it just has the Head, which is Null.<br> * 3. Else, the Head holds the pointer to the first Node of the List.<br> * 4. When we want to add any Node at the front, we must make the head point to it.<br> * 5. And the Next pointer of the newly added Node, must point to the previous Head, whether it be NULL(in case of * new List) or the pointer to the first Node of the List.<br> * 6. The previous Head Node is now the second Node of Linked List, because the new Node is added at the front.<br> */ public void prepend(SingleNode node) { if (node == null) { return; } if (this.head == null) { this.head = node; } else { node.setNext(this.head); this.head = node; } size++; } /** * add at position of list */ public void add(int index, SingleNode node) { if (node == null) { return; } int count = 0; SingleNode currentNode = this.head; if (this.head.equals(currentNode)) { /* * adding to the head */ node.setNext(currentNode); this.head = node; } else if (this.tail.equals(currentNode)) { this.tail.setNext(node); this.tail = node; } SingleNode previous = null; while (currentNode.getNext() != null) { if (index == count) { break; } previous = currentNode; currentNode = currentNode.getNext(); count++; } node.setNext(currentNode); previous.setNext(node); size++; } /** * is empty? */ public boolean isEmpty() { return size <= 0 || this.head == null; } /** * exist? search by email */ public boolean existByEmail(String email) { if (email == null || email.length() <= 0) { return false; } if (this.head == null) { return false; } else { SingleNode currentNode = this.head; /** * if email not found, keep checking the next node<br> * when email is found, break out of while loop */ while (!currentNode.getData().getEmail().equalsIgnoreCase(email.toLowerCase())) { /** * next is null, so email is not found */ if (currentNode.getNext() == null) { return false; } currentNode = currentNode.getNext(); } return true; } } /** * get node on index */ public SingleNode get(int index) { if (index < 0 || index >= size) { throw new IllegalArgumentException("index out of bounds"); } if (index == 0) { return this.head; } else if (index == (size - 1)) { return this.tail; } else { int count = 0; SingleNode currentNode = this.head; while (currentNode.getNext() != null) { currentNode = currentNode.getNext(); count++; if (index == count) { break; } } return currentNode; } } /** * remove node base on index<br> * 1. If the Node to be deleted is the first node, then simply set the Next pointer of the Head to point to the next * element from the Node to be deleted.<br> * 2. If the Node is in the middle somewhere, then find the Node before it, and make the Node before it point to the * Node next to it.<br> */ public void removeAt(int index) { if (index < 0 || index >= size) { throw new IllegalArgumentException("index out of bounds"); } if (index==0) { this.head = this.head.getNext(); return; } int count = 1; SingleNode previous = this.head; SingleNode currentNode = this.head.getNext(); SingleNode next = null; while (count < index) { previous = currentNode; currentNode = currentNode.getNext(); next = currentNode.getNext(); count++; } if (currentNode.equals(this.tail)) { this.tail = previous; this.tail.setNext(null); } else { // drop currentNode previous.setNext(next); } } /** * remove all<br> * set head to null */ public void removeAll() { this.head = null; this.tail = null; } /** * print out list */ public void printList() { if (this.head == null) { System.out.println("list is empty"); } int count = 0; SingleNode node = this.head; while (node != null) { System.out.println("index: " + count); System.out.println("data: " + node.getData().toString()); node = node.getNext(); if (node == null) { System.out.println("tail: " + tail.getData().toString()); System.out.println("end of list\n"); } System.out.println("\n"); count++; } } }
In a doubly linked list, each node contains a data part and two addresses, one for the previous node and one for the next node.
@AllArgsConstructor @ToString @Data public class DoubleNode { private DoubleNode prev; private Customer data; private DoubleNode next; public DoubleNode(Customer data) { this(null, data, null); } }
@NoArgsConstructor @Data public class MyDoubleLinkedList { int size = 0; private DoubleNode head; private DoubleNode tail; public MyDoubleLinkedList(DoubleNode head) { append(head); } /** * add to end of list<br> * 1. If the Linked List is empty then we simply, add the new Node as the Head of the Linked List.<br> * 2. If the Linked List is not empty then we find the last node, and make it' next to the new Node, hence making * the new node the last Node. */ public void append(DoubleNode node) { if (node == null) { return; } if (this.head == null) { this.head = node; this.tail = node; } else { DoubleNode currentNode = this.head; while (currentNode.getNext() != null) { currentNode = currentNode.getNext(); } // order doesn't matter, as long as the last operation is tail=node this.tail.setNext(node); node.setPrev(this.tail); this.tail = node; } size++; } /** * append to front of list<br> * 1. The first Node is the Head for any Linked List.<br> * 2. When a new Linked List is instantiated, it just has the Head, which is Null.<br> * 3. Else, the Head holds the pointer to the first Node of the List.<br> * 4. When we want to add any Node at the front, we must make the head point to it.<br> * 5. And the Next pointer of the newly added Node, must point to the previous Head, whether it be NULL(in case of * new List) or the pointer to the first Node of the List.<br> * 6. The previous Head Node is now the second Node of Linked List, because the new Node is added at the front.<br> */ public void prepend(DoubleNode node) { if (node == null) { return; } if (this.head == null) { this.head = node; } else { // order doesn't matter, as long as the last operation is head=node this.head.setPrev(node); node.setNext(this.head); this.head = node; } size++; } /** * add at position of list */ public void add(int index, DoubleNode node) { if (node == null) { return; } int count = 0; DoubleNode currentNode = this.head; if (index == 0) { node.setNext(currentNode); this.head = node; } else if (index == (size - 1)) { this.tail.setNext(node); this.tail = node; } else { DoubleNode previous = null; while (currentNode.getNext() != null) { if (index == count) { break; } previous = currentNode; currentNode = currentNode.getNext(); count++; } node.setNext(currentNode); previous.setNext(node); } size++; } /** * is empty? */ public boolean isEmpty() { return size <= 0 || this.head == null; } /** * exist? search by email */ public boolean existByEmail(String email) { if (email == null || email.length() <= 0) { return false; } if (this.head == null) { return false; } else { DoubleNode currentNode = this.head; /** * if email not found, keep checking the next node<br> * when email is found, break out of while loop */ while (!currentNode.getData().getEmail().equalsIgnoreCase(email.toLowerCase())) { /** * next is null, so email is not found */ if (currentNode.getNext() == null) { return false; } currentNode = currentNode.getNext(); } return true; } } /** * get node on index */ public DoubleNode get(int index) { if (index < 0 || index >= size) { throw new IllegalArgumentException("index out of bounds"); } if (index == 0) { return this.head; } if (index == 0) { return this.head; } else if (index == (size - 1)) { return this.tail; } else { int count = 0; DoubleNode currentNode = this.head; while (currentNode.getNext() != null) { currentNode = currentNode.getNext(); count++; if (index == count) { break; } } return currentNode; } } /** * remove node base on index<br> * 1. If the Node to be deleted is the first node, then simply set the Next pointer of the Head to point to the next * element from the Node to be deleted.<br> * 2. If the Node is in the middle somewhere, then find the Node before it, and make the Node before it point to the * Node next to it.<br> */ public void remove(int index) { if (index < 0 || index >= size) { throw new IllegalArgumentException("index out of bounds"); } int count = 0; if (index == 0) { this.head = this.head.getNext(); this.head.setPrev(null); } else if (index == (size - 1)) { this.tail = this.tail.getPrev(); this.tail.setNext(null); } else { DoubleNode previous = null; DoubleNode currentNode = this.head; DoubleNode next = null; while (currentNode.getNext() != null) { if (index == count) { break; } previous = currentNode; currentNode = currentNode.getNext(); next = currentNode.getNext(); count++; } previous.setNext(next); next.setPrev(previous); } } /** * remove all<br> * set head to null */ public void removeAll() { this.head = null; this.tail = null; } /** * print out list */ public void printList() { if (this.head == null) { System.out.println("list is empty"); } int count = 0; DoubleNode node = this.head; while (node != null) { System.out.println("index: " + count); System.out.println("prev: " + ((node.getPrev() != null) ? node.getPrev().getData().toString() : "")); System.out.println("current: " + node.getData().toString()); System.out.println("next: " + ((node.getNext() != null) ? node.getNext().getData().toString() : "")); node = node.getNext(); if (node == null) { System.out.println("tail: " + tail.getData().toString()); System.out.println("end of list\n"); } System.out.println("\n"); count++; } } }
In circular linked list the last node of the list holds the address of the first node hence forming a circular chain.
Data Structure | Time Complexity | Space Complexity | |||||||
---|---|---|---|---|---|---|---|---|---|
Average | Worst | Worst | |||||||
Access | Search | Insertion | Deletion | Access | Search | Insertion | Deletion | ||
Singly-Linked List | Θ(n) |
Θ(n) |
Θ(1) |
Θ(1) |
O(n) |
O(n) |
O(1) |
O(1) |
O(n) |
Doubly-Linked List | Θ(n) |
Θ(n) |
Θ(1) |
Θ(1) |
O(n) |
O(n) |
O(1) |
O(1) |
O(n) |