475 lines
15 KiB
JavaScript
475 lines
15 KiB
JavaScript
/**
|
|
* @fileOverview Implementation of a doubly linked-list data structure
|
|
* @author Jason S. Jones
|
|
* @license MIT
|
|
*/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
var isEqual = require('lodash.isequal');
|
|
var Node = require('./lib/list-node');
|
|
var Iterator = require('./lib/iterator');
|
|
|
|
/**************************************************
|
|
* Doubly linked list class
|
|
*
|
|
* Implementation of a doubly linked list data structure. This
|
|
* implementation provides the general functionality of adding nodes to
|
|
* the front or back of the list, as well as removing node from the front
|
|
* or back. This functionality enables this implemention to be the
|
|
* underlying data structure for the more specific stack or queue data
|
|
* structure.
|
|
*
|
|
***************************************************/
|
|
|
|
/**
|
|
* Creates a LinkedList instance. Each instance has a head node, a tail
|
|
* node and a size, which represents the number of nodes in the list.
|
|
*
|
|
* @constructor
|
|
*/
|
|
function DoublyLinkedList() {
|
|
this.head = null;
|
|
this.tail = null;
|
|
this.size = 0;
|
|
|
|
// add iterator as a property of this list to share the same
|
|
// iterator instance with all other methods that may require
|
|
// its use. Note: be sure to call this.iterator.reset() to
|
|
// reset this iterator to point the head of the list.
|
|
this.iterator = new Iterator(this);
|
|
}
|
|
|
|
/* Functions attached to the Linked-list prototype. All linked-list
|
|
* instances will share these methods, meaning there will NOT be copies
|
|
* made for each instance. This will be a huge memory savings since there
|
|
* may be several different linked lists.
|
|
*/
|
|
DoublyLinkedList.prototype = {
|
|
|
|
/**
|
|
* Creates a new Node object with 'data' assigned to the node's data
|
|
* property
|
|
*
|
|
* @param {object|string|number} data The data to initialize with the
|
|
* node
|
|
* @returns {object} Node object intialized with 'data'
|
|
*/
|
|
createNewNode: function(data) {
|
|
return new Node(data);
|
|
},
|
|
|
|
/**
|
|
* Returns the first node in the list, commonly referred to as the
|
|
* 'head' node
|
|
*
|
|
* @returns {object} the head node of the list
|
|
*/
|
|
getHeadNode: function() {
|
|
return this.head;
|
|
},
|
|
|
|
/**
|
|
* Returns the last node in the list, commonly referred to as the
|
|
* 'tail'node
|
|
*
|
|
* @returns {object} the tail node of the list
|
|
*/
|
|
getTailNode: function() {
|
|
return this.tail;
|
|
},
|
|
|
|
/**
|
|
* Determines if the list is empty
|
|
*
|
|
* @returns {boolean} true if the list is empty, false otherwise
|
|
*/
|
|
isEmpty: function() {
|
|
return (this.size === 0);
|
|
},
|
|
|
|
/**
|
|
* Returns the size of the list, or number of nodes
|
|
*
|
|
* @returns {number} the number of nodes in the list
|
|
*/
|
|
getSize: function() {
|
|
return this.size;
|
|
},
|
|
|
|
/**
|
|
* Clears the list of all nodes/data
|
|
*/
|
|
clear: function () {
|
|
while (!this.isEmpty()) {
|
|
this.remove();
|
|
}
|
|
},
|
|
|
|
//################## INSERT methods ####################
|
|
|
|
/**
|
|
* Inserts a node with the provided data to the end of the list
|
|
*
|
|
* @param {object|string|number} data The data to initialize with the
|
|
* node
|
|
* @returns {boolean} true if insert operation was successful
|
|
*/
|
|
insert: function(data) {
|
|
var newNode = this.createNewNode(data);
|
|
if (this.isEmpty()) {
|
|
this.head = this.tail = newNode;
|
|
} else {
|
|
this.tail.next = newNode;
|
|
newNode.prev = this.tail;
|
|
this.tail = newNode;
|
|
}
|
|
this.size += 1;
|
|
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* Inserts a node with the provided data to the front of the list
|
|
*
|
|
* @param {object|string|number} data The data to initialize with the
|
|
* node
|
|
* @returns {boolean} true if insert operation was successful
|
|
*/
|
|
insertFirst: function(data) {
|
|
if (this.isEmpty()) {
|
|
this.insert(data);
|
|
} else {
|
|
var newNode = this.createNewNode(data);
|
|
|
|
newNode.next = this.head;
|
|
this.head.prev = newNode;
|
|
this.head = newNode;
|
|
|
|
this.size += 1;
|
|
}
|
|
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* Inserts a node with the provided data at the index indicated.
|
|
*
|
|
* @param {number} index The index in the list to insert the new node
|
|
* @param {object|string|number} data The data to initialize with the node
|
|
*/
|
|
insertAt: function (index, data) {
|
|
var current = this.getHeadNode(),
|
|
newNode = this.createNewNode(data),
|
|
position = 0;
|
|
|
|
// check for index out-of-bounds
|
|
if (index < 0 || index > this.getSize() - 1) {
|
|
return false;
|
|
}
|
|
|
|
// if index is 0, we just need to insert the first node
|
|
if (index === 0) {
|
|
this.insertFirst(data);
|
|
return true;
|
|
}
|
|
|
|
while (position < index) {
|
|
current = current.next;
|
|
position += 1;
|
|
}
|
|
|
|
current.prev.next = newNode;
|
|
newNode.prev = current.prev;
|
|
current.prev = newNode;
|
|
newNode.next = current;
|
|
|
|
this.size += 1;
|
|
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* Inserts a node before the first node containing the provided data
|
|
*
|
|
* @param {object|string|number} nodeData The data of the node to
|
|
* find to insert the new node before
|
|
* @param {object|string|number} dataToInsert The data to initialize with the node
|
|
* @returns {boolean} true if insert operation was successful
|
|
*/
|
|
insertBefore: function (nodeData, dataToInsert) {
|
|
var index = this.indexOf(nodeData);
|
|
return this.insertAt(index, dataToInsert);
|
|
},
|
|
|
|
/**
|
|
* Inserts a node after the first node containing the provided data
|
|
*
|
|
* @param {object|string|number} nodeData The data of the node to
|
|
* find to insert the new node after
|
|
* @param {object|string|number} dataToInsert The data to initialize with the node
|
|
* @returns {boolean} true if insert operation was successful
|
|
*/
|
|
insertAfter: function (nodeData, dataToInsert) {
|
|
var index = this.indexOf(nodeData);
|
|
var size = this.getSize();
|
|
|
|
// check if we want to insert new node after the tail node
|
|
if (index + 1 === size) {
|
|
|
|
// if so, call insert, which will append to the end by default
|
|
return this.insert(dataToInsert);
|
|
|
|
} else {
|
|
|
|
// otherwise, increment the index and insert there
|
|
return this.insertAt(index + 1, dataToInsert);
|
|
}
|
|
},
|
|
|
|
//################## REMOVE methods ####################
|
|
|
|
/**
|
|
* Removes the tail node from the list
|
|
*
|
|
* There is a significant performance improvement with the operation
|
|
* over its singly linked list counterpart. The mere fact of having
|
|
* a reference to the previous node improves this operation from O(n)
|
|
* (in the case of singly linked list) to O(1).
|
|
*
|
|
* @returns the node that was removed
|
|
*/
|
|
remove: function() {
|
|
if (this.isEmpty()) {
|
|
return null;
|
|
}
|
|
|
|
// get handle for the tail node
|
|
var nodeToRemove = this.getTailNode();
|
|
|
|
// if there is only one node in the list, set head and tail
|
|
// properties to null
|
|
if (this.getSize() === 1) {
|
|
this.head = null;
|
|
this.tail = null;
|
|
|
|
// more than one node in the list
|
|
} else {
|
|
this.tail = this.getTailNode().prev;
|
|
this.tail.next = null;
|
|
}
|
|
this.size -= 1;
|
|
|
|
return nodeToRemove;
|
|
},
|
|
|
|
/**
|
|
* Removes the head node from the list
|
|
*
|
|
* @returns the node that was removed
|
|
*/
|
|
removeFirst: function() {
|
|
if (this.isEmpty()) {
|
|
return null;
|
|
}
|
|
|
|
var nodeToRemove;
|
|
|
|
if (this.getSize() === 1) {
|
|
nodeToRemove = this.remove();
|
|
} else {
|
|
nodeToRemove = this.getHeadNode();
|
|
this.head = this.head.next;
|
|
this.head.prev = null;
|
|
this.size -= 1;
|
|
}
|
|
|
|
return nodeToRemove;
|
|
},
|
|
|
|
/**
|
|
* Removes the node at the index provided
|
|
*
|
|
* @param {number} index The index of the node to remove
|
|
* @returns the node that was removed
|
|
*/
|
|
removeAt: function (index) {
|
|
var nodeToRemove = this.findAt(index);
|
|
|
|
// check for index out-of-bounds
|
|
if (index < 0 || index > this.getSize() - 1) {
|
|
return null;
|
|
}
|
|
|
|
// if index is 0, we just need to remove the first node
|
|
if (index === 0) {
|
|
return this.removeFirst();
|
|
}
|
|
|
|
// if index is size-1, we just need to remove the last node,
|
|
// which remove() does by default
|
|
if (index === this.getSize() - 1) {
|
|
return this.remove();
|
|
}
|
|
|
|
nodeToRemove.prev.next = nodeToRemove.next;
|
|
nodeToRemove.next.prev = nodeToRemove.prev;
|
|
nodeToRemove.next = nodeToRemove.prev = null;
|
|
|
|
this.size -= 1;
|
|
|
|
return nodeToRemove;
|
|
},
|
|
|
|
/**
|
|
* Removes the first node that contains the data provided
|
|
*
|
|
* @param {object|string|number} nodeData The data of the node to remove
|
|
* @returns the node that was removed
|
|
*/
|
|
removeNode: function (nodeData) {
|
|
var index = this.indexOf(nodeData);
|
|
return this.removeAt(index);
|
|
},
|
|
|
|
//################## FIND methods ####################
|
|
|
|
/**
|
|
* Returns the index of the first node containing the provided data. If
|
|
* a node cannot be found containing the provided data, -1 is returned.
|
|
*
|
|
* @param {object|string|number} nodeData The data of the node to find
|
|
* @returns the index of the node if found, -1 otherwise
|
|
*/
|
|
indexOf: function(nodeData) {
|
|
this.iterator.reset();
|
|
var current;
|
|
|
|
var index = 0;
|
|
|
|
// iterate over the list (keeping track of the index value) until
|
|
// we find the node containg the nodeData we are looking for
|
|
while (this.iterator.hasNext()) {
|
|
current = this.iterator.next();
|
|
if (isEqual(current.getData(), nodeData)) {
|
|
return index;
|
|
}
|
|
index += 1;
|
|
}
|
|
|
|
// only get here if we didn't find a node containing the nodeData
|
|
return -1;
|
|
},
|
|
|
|
/**
|
|
* Returns the fist node containing the provided data. If a node
|
|
* cannot be found containing the provided data, -1 is returned.
|
|
*
|
|
* @param {object|string|number} nodeData The data of the node to find
|
|
* @returns the node if found, -1 otherwise
|
|
*/
|
|
find: function(nodeData) {
|
|
// start at the head of the list
|
|
this.iterator.reset();
|
|
var current;
|
|
|
|
// iterate over the list until we find the node containing the data
|
|
// we are looking for
|
|
while (this.iterator.hasNext()) {
|
|
current = this.iterator.next();
|
|
if (isEqual(current.getData(), nodeData)) {
|
|
return current;
|
|
}
|
|
}
|
|
|
|
// only get here if we didn't find a node containing the nodeData
|
|
return -1;
|
|
},
|
|
|
|
/**
|
|
* Returns the node at the location provided by index
|
|
*
|
|
* @param {number} index The index of the node to return
|
|
* @returns the node located at the index provided.
|
|
*/
|
|
findAt: function(index) {
|
|
// if idx is out of bounds or fn called on empty list, return -1
|
|
if (this.isEmpty() || index > this.getSize() - 1) {
|
|
return -1;
|
|
}
|
|
|
|
// else, loop through the list and return the node in the
|
|
// position provided by idx. Assume zero-based positions.
|
|
var node = this.getHeadNode();
|
|
var position = 0;
|
|
|
|
while (position < index) {
|
|
node = node.next;
|
|
position += 1;
|
|
}
|
|
|
|
return node;
|
|
},
|
|
|
|
/**
|
|
* Determines whether or not the list contains the provided nodeData
|
|
*
|
|
* @param {object|string|number} nodeData The data to check if the list
|
|
* contains
|
|
* @returns the true if the list contains nodeData, false otherwise
|
|
*/
|
|
contains: function (nodeData) {
|
|
if (this.indexOf(nodeData) > -1) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
},
|
|
|
|
//################## UTILITY methods ####################
|
|
|
|
/**
|
|
* Utility function to iterate over the list and call the fn provided
|
|
* on each node, or element, of the list
|
|
*
|
|
* @param {object} fn The function to call on each node of the list
|
|
* @param {bool} reverse Use or not reverse iteration (tail to head), default to false
|
|
*/
|
|
forEach: function(fn, reverse) {
|
|
reverse = reverse || false;
|
|
if (reverse) {
|
|
this.iterator.reset_reverse();
|
|
this.iterator.each_reverse(fn)
|
|
} else {
|
|
this.iterator.reset();
|
|
this.iterator.each(fn);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Returns an array of all the data contained in the list
|
|
*
|
|
* @returns {array} the array of all the data from the list
|
|
*/
|
|
toArray: function() {
|
|
var listArray = [];
|
|
this.forEach(function(node) {
|
|
listArray.push(node.getData());
|
|
});
|
|
|
|
return listArray;
|
|
},
|
|
|
|
/**
|
|
* Interrupts iteration over the list
|
|
*/
|
|
interruptEnumeration: function() {
|
|
this.iterator.interrupt();
|
|
}
|
|
};
|
|
|
|
module.exports = DoublyLinkedList;
|
|
|
|
}());
|