JavaScript数据结构之链表(单链表,双链表,循环链表)
链表是一种存储数据的工具,不同于数组,链表中的元素并不是连续存储的,因此不能通过下标去访问。
链表分为单(向)链表,双向链表,循环链表等
单链表
链表用来存储有序的元素集合,与数组不同,链表中的元素并非保存在连续的存储空间内,每个元素由一个存储元素本身的节点和一个指向下一个元素的指针构成。当要移动或删除元素时,只需要修改相应元素上的指针就可以了。对链表元素的操作要比对数组元素的操作效率更高。下面是链表数据结构的示意图:
实现单链表的第一步要先创建一个拥有一个保存节点的值
和一个保存下一个节点的指针
的辅助类。
let Node = function (element) {
this.element = element;
this.next = null;
};
查找链表元素
class LinkedList {
constructor() {
this.length = 0;
this.head = null;
}
getElementAt (position) {
//判断参数position的边界值,如果值超出了索引的范围(小于0或者大于length - 1),则返回null
if (position < 0 || position >= this.length) return null;
//从链表的head开始
let current = this.head;
//遍历整个链表直到找到对应索引位置的节点,然后返回这个节点
for (let i = 0; i < position; i++) {
current = current.next;
}
return current;
}
}
尾部添加新节点
class LinkedList {
constructor() {
this.length = 0;
this.head = null;
}
append (element) {
let node = new Node(element);
// 链表的head为null(这种情况表示链表为空),则直接将head指向新添加的元素
if (this.head === null) this.head = node;
else {
// 通过getElementAt()方法找到链表的最后一个节点,将该节点的next指针指向新添加的元素
let current = this.getElementAt(this.length - 1);
//新添加的元素的next指针默认为null,链表最后一个元素的next值为null
current.next = node;
}
//将节点挂到链表上之后,将链表的长度加1
this.length++;
}
}
任意位置添加节点
class LinkedList {
constructor() {
this.length = 0;
this.head = null;
}
insert (position, element) {
// 判断参数position的边界值,不能越界。
if (position < 0 || position > this.length) return false;
let node = new Node(element);
//当position的值为0时,表示要在链表的头部插入新节点
if (position === 0) {
node.next = this.head;
this.head = node;
}
else {
//首先找到插入位置的前一个节点previous node
let previous = this.getElementAt(position - 1);
//将新节点new node的next指针指向previous node的next所对应的节点
node.next = previous.next;
//再将previous node的next指针指向new node,这样就把新节点挂到链表中了
previous.next = node;
}
//更新length属性的值,将链表的长度加1
this.length++;
return true;
}
}
position的值为0时,在链表的头部插入新节点,对应的操作图如下:
插入的节点在链表的中间或者尾部,对应的操作图如下:
删除链表中指定位置的节点
class LinkedList {
constructor() {
this.length = 0;
this.head = null;
}
removeAt (position) {
// position不能超出边界值
if (position < 0 || position >= this.length) return null;
let current = this.head;
//position=0表示要删除的节点为链表的头部,只需要将head移到下一个节点
//如果当前链表只有一个节点,那么下一个节点为null,此时将head指向下一个节点等同于将head设置成null,删除之后链表为空
if (position === 0)
this.head = current.next;
else {
// 删除的节点在链表的中间部分,需要找出position所在位置的前一个节点,将它的next指针指向position所在位置的下一个节点
let previous = this.getElementAt(position - 1);
current = previous.next;
previous.next = current.next;
//删除节点只需要修改相应节点的指针,使断开位置左右相邻的节点重新连接上。被删除的节点由于再也没有其它部分的引用而被丢弃在内存中,等待垃圾回收器来清除
}
///链表的长度减1
this.length--;
return current.element;
}
}
从头部删除节点的操作如下:
从中间删除节点的操作图:
返回给定元素在链表中的索引位置
class LinkedList {
constructor() {
this.length = 0;
this.head = null;
}
indexOf (element) {
let current = this.head;
//从链表的头部开始遍历,直到找到和给定元素相同的元素,然后返回对应的索引号
for (let i = 0; i < this.length; i++) {
if (current.element === element) return i;
current = current.next;
}
//如果没有找到对应的元素,则返回-1
return -1;
}
}
翻转链表
function reverse( linkedList ){
var head = linkedList.head;
// 如果只有一个节点 或者 是空链表
if( head === null || head.next === null ){
return;
}
var p = head;
var q = p.next;
// 反转后的头结点变成尾节点
head.next = null;
while(q){
r = q.next;
q.next = p;
p = q;
q = r;
}
// 退出循环后 r = q.next = null, q.next = q; p=q; q=null;
// p指向原来节点的尾节点, 那么翻转后,尾节点变成头结点
linkedList.head = p;
}
双向链表
双向链表中的每一个元素拥有两个指针,一个用来指向下一个节点,一个用来指向上一个节点。在双向链表中,除了从头部开始遍历之外,还可以从尾部进行遍历。
由于双向链表具有单向链表的所有特性,因此可以继承前面的单向链表类,不过辅助类Node需要添加一个prev属性,用来指向前一个节点。
let Node = function (element) {
this.element = element;
this.next = null;
this.prev = null;
};
继承自LinkedList类的双向链表类的基本骨架:
class DoubleLinkedList extends LinkedList {
constructor() {
super();
this.tail = null;
}
}
添加新节点
append (element) {
let node = new Node(element);
// 如果链表为空,除了要将head指向当前添加的节点外,还要将tail也指向当前要添加的节点
if (this.head === null) {
this.head = node;
this.tail = node;
}
else {
// 当链表不为空时,直接将tail的next指向当前要添加的节点node,然后修改node的prev指向旧的tail,最后修改tail为新添加的节点
this.tail.next = node;
node.prev = this.tail;
this.tail = node;
}
//修改链表的长度,length的值加1
this.length++;
}
查找节点元素
getElementAt (position) {
if (position < 0 || position >= this.length) return null;
// 当要查找的元素的索引号大于链表长度的一半时,从链表的尾部开始遍历,用节点的prev指针,用来查找前一个节点
if (position > Math.floor(this.length / 2)) {
let current = this.tail;
for (let i = this.length - 1; i > position; i--) {
current = current.prev;
}
return current;
}
// 从前往后遍历
else {
return super.getElementAt(position);
}
}
插入节点
insert (position, element) {
if (position < 0 || position > this.length) return false;
// 插入到尾部
if (position === this.length) this.append(element);
else {
let node = new Node(element);
// 插入到头部
if (position === 0) {
if (this.head === null) {
this.head = node;
this.tail = node;
}
else {
node.next = this.head;
this.head.prev = node;
this.head = node;
}
}
// 插入到中间位置
else {
let current = this.getElementAt(position);
let previous = current.prev;
node.next = current;
node.prev = previous;
previous.next = node;
current.prev = node;
}
}
this.length++;
return true;
}
删除节点
removeAt (position) {
// position不能超出边界值
if (position < 0 || position >= this.length) return null;
let current = this.head;
let previous;
// 移除头部元素
if (position === 0) {
this.head = current.next;
this.head.prev = null;
if (this.length === 1) this.tail = null;
}
// 移除尾部元素
else if (position === this.length - 1) {
current = this.tail;
this.tail = current.prev;
this.tail.next = null;
}
// 移除中间元素
else {
current = this.getElementAt(position);
previous = current.prev;
previous.next = current.next;
current.next.prev = previous;
}
this.length--;
return current.element;
}
循环链表
循环链表的尾部指向它自己的头部。循环链表可以有单向循环链表,也可以有双向循环链表。
单向循环链表示意图:
//继承单链表中的类
class CircularLinkedList extends LinkedList.LinkedList {
constructor () {
super();
}
append (element) {
let node = new LinkedList.Node(element);
/
if (this.head === null) this.head = node;
else {
let current = this.getElementAt(this.length - 1);
current.next = node;
}
// 前面的过程与单链表添加相同,只需最后将新添加的元素的next指向head
node.next = this.head;
this.length++;
}
insert (position, element) {
// position不能超出边界值
if (position < 0 || position > this.length) return false;
let node = new LinkedList.Node(element);
if (position === 0) {
node.next = this.head;
let current = this.getElementAt(this.length - 1);
current.next = node;
this.head = node;
}
else {
let previous = this.getElementAt(position - 1);
node.next = previous.next;
previous.next = node;
}
this.length++;
return true;
}
removeAt (position) {
if (position < 0 || position >= this.length) return null;
let current = this.head;
if (position === 0) this.head = current.next;
else {
let previous = this.getElementAt(position - 1);
current = previous.next;
previous.next = current.next;
}
this.length--;
if (this.length > 1) {
let last = this.getElementAt(this.length - 1);
last.next = this.head;
}
return current.element;
}
}
双向循环链表示意图:
function DoublyCircularLinkedList(){
var Node = function(element){
this.element = element;
this.next = null;
this.prev = null;
};
var length = 0,
head = null,
tail = null;
this.append = function(element){
var node = new Node(element),
current,
previous;
if (!head) {
head = node;
tail = node;
head.prev = tail;
tail.next = head;
}else{
current = head;
while(current.next !== head){
previous = current;
current = current.next;
}
current.next = node;
node.next = head;
node.prev = current;
};
length++;
return true;
}
this.insert = function(position, element){
if(position >= 0 && position <= length){
var node = new Node(element),
index = 0,
current = head,
previous;
if(position === 0){
if(!head){
node.next = node;
node.tail = node;
head = node;
tail = node;
}else{
current.prev = node;
node.next = current;
head = node;
node.prev = tail;
}
}else if(position === length){
current = tail;
current.next = node;
node.prev = current;
tail = node;
node.next = head;
}else{
while(index++ < position){
previous = current;
current = current.next;
}
current.prev = node;
node.next = current;
previous.next = node;
node.prev = previous;
}
length++;
return true;
}else{
return false;
}
}
this.removeAt = function(position){
if(position > -1 && position < length){
var current = head,
index = 0,
previous;
if(position === 0){
current.next.previous = tail;
head = current.next;
}else if(position === length - 1){
current = tail;
current.prev.next = head;
head.prev = current.prev;
tail = current.prev;
}else{
while(index++ < position){
previous = current;
current = current.next;
}
previous.next = current.next;
current.next.prev = previous;
}
length--;
return true;
}else{
return false;
}
}
}