双向链表(DoublyLinkedList)结构
一、双向链表的介绍
既可以从头遍历到尾,也可以从尾遍历到头。链表相连的过程是双向的。实现原理是一个节点既有向前连接的引用,也有一个向后连接的引用
单向链表的特性
- 只能从头遍历到尾或者从尾遍历到头(一般从头到尾)
- 链表相连的过程是单向的,实现原理是上一个节点中有指向下一个节点的引用
- 单向链表有一个比较明显的缺点:可以轻松到达下一个节点,但回到前一个节点很难,在实际开发中, 经常会遇到需要回到上一个节点的情况。
- 由于双向链表的节点指向是双向的,所以双向链表可以有效的解决单向链表存在的问题
双向链表结构
- 双向链表不仅有head指针指向第一个节点,而且有tail指针指向最后一个节点
- 每一个节点由三部分组成:item储存数据、prev指向前一个节点、next指向后一个节点
- 双向链表的第一个节点的prev指向null
- 双向链表的最后一个节点的next指向null
双向链表的缺点
- 每次在插入或删除某个节点时,都需要处理四个引用,而不是两个,实现起来会困难些
- 相对于单向链表,所占内存空间更大一些
二、双向链表的封装
常见操作
append(element)
向链表尾部追加一个新元素insert(position, element)
向链表的指定位置插入一个新元素getElement(position)
获取指定位置的元素indexOf(element)
返回元素在链表中的索引。如果链表中没有该元素就返回 -1update(position, element)
修改指定位置上的元素removeAt(position)
从链表中的删除指定位置的元素remove(element)
从链表删除指定的元素isEmpty()
如果链表中不包含任何元素,返回trun
,如果链表长度大于 0 则返回false
size()
返回链表包含的元素个数,与数组的length
属性类似toString()
由于链表项使用了 Node 类,就需要重写继承自 JavaScript 对象默认的toString
方法,让其只输出元素的值forwardString()
返回正向遍历节点字符串形式backwordString()
返回反向遍历的节点的字符串形式
1.逐步封装双向链表
双向链表的插入(insert)和删除(removeAt)方法需要考虑的情况较多,是一个难点
1.1 先创建双向链表类DoublyLinkedList
创建内部类并添加基本属性,再实现单向链表的常用方法
与单向链表相比
- 双向链表内部类中新添加
this.prev
属性,指向该节点的上一个节点 - 双向链表新添加
this.tail
属性,该属性指向末尾的节点
function DoublyLinkedList() {
// 内部类
function Node(data) {
this.data = data;
this.prev = null;
this.next = null;
}
// 属性
this.head = null;
this.tail = null;
this.length = 0;
}
1.2 实现append()方法
// 1.向链表尾部追加数据
DoublyLinkedList.prototype.append = function (data) {
var newNode = new Node(data);
// 若是第一次添加,直接将head和tail指向新节点即可
if (this.length === 0) {
this.head = newNode;
this.tail = newNode;
} else {
// 让tail与新节点的prev建立连接
newNode.prev = this.tail;
// tail的next指向新节点
this.tail.next = newNode;
// 改变tail指针
this.tail = newNode;
}
this.length += 1;
};
append()方法图解
情况1:第一次添加 ==> 只需要让head和tail都指向新节点即可
情况2:不是第一次添加,需要改变tail指针的引用关系
- 通过
newNode.prev = this.tail
:建立指向1 - 通过
this.tail.next = newNode
:建立指向2
如图:
- 通过
this.tail = newNode
:建立指向3 - 要注意指针改变的顺序,最后修改tail指向,这样未修改前tail始终指向原链表的最后一个节点。
如图:
1.3 实现forwardString()、backwardString()、toString()方法
便于我们测试代码,先实现以上三个方法,这里就放在同一个代码块下了~其实toString()方法就是forwardString()方法
// 返回正向遍历节点字符串形式
DoublyLinkedList.prototype.forwardString = function () {
var current = this.head;
var resultString = "";
while (current) {
resultString += current.data + " ";
current = current.next;
}
return resultString;
};
// 返回反向遍历的节点的字符串形式
DoublyLinkedList.prototype.backwardString = function () {
var current = this.tail;
var resultString = "";
while (current) {
resultString += current.data + " ";
current = current.prev;
}
return resultString;
};
DoublyLinkedList.prototype.toString = function () {
return this.forwardString();
};
测试代码
// 测试代码
var dll = new DoublyLinkedList();
dll.append("10");
dll.append("20");
dll.append("30");
console.log(dll.toString()); // 10 20 30
console.log(dll.forwardString()); // 10 20 30
console.log(dll.backwardString()); // 30 20 10
1.4 实现insert()方法
// 2.向链表中插入数据
DoublyLinkedList.prototype.insert = function (position, data) {
// 1.边界判断
if (position < 0 || position > this.length) return false;
var newNode = new Node(data);
// 2.第一次添加
if (this.length === 0) {
this.head = newNode;
this.tail = newNode;
} else {
if (position === 0) {
// 3.1 在头部插入
this.head.prev = newNode;
newNode.next = this.head;
this.head = newNode;
} else if (position === this.length) {
// 3.2 在尾部插入
newNode.prev = this.tail;
this.tail.next = newNode;
this.tail = newNode;
} else {
// 3.3 在中间插入 0 < position < this.length
var current = this.head;
var index = 0;
// 找到要插入的位置
while (index++ < position) {
current = current.next;
}
// 让新节点的next 指向要插入位置的节点(current)
newNode.next = current;
// 让新节点的prev 指向 current的prev
newNode.prev = current.prev;
// 将新节点与 当前节点的 前一个节点的 next建立关系
current.prev.next = newNode;
// 让newNode成为current的前一个节点
current.prev = newNode;
}
}
this.length += 1;
return true;
};
insert方法图解
我们一起来分析,双向链表在进行插入时,都有哪些情况会出现
- 情况1:当是第一次添加的时候,只需要让head和tail都指向newNode即可
// 2.第一次添加
if (this.length === 0) {
this.head = newNode;
this.tail = newNode;
}
- 当不是第一次插入的时候,又可以细分为以下几种情况
- 2.1在头部插入 (position === 0)
- 2.2在尾部插入 (position === this.length)
- 2.3在中间插入 (0 < position < this.length)
2.1 在头部插入时
if (position === 0) {
// 3.1 在头部插入
this.head.prev = newNode;
newNode.next = this.head;
this.head = newNode;
}
- 在position === 0时,我们要将newNode插入到如图的位置,成为新的头节点
- 通过
this.head.prev = newNode
,将头节点的prev指向新节点,建立引用1 - 通过
newNode.next = this.head
,将新节点的next指向原来的头节点,建立引用2 - 此时可以发现,newNode已经与原来的头节点互相建立指向关系
- 最后通过
this.head = newNode
,将newNode成为新的头节点,建立引用3
2.2 在尾部插入时
if (position === this.length) {
// 3.2 在尾部插入
this.tail.next = newNode;
newNode.prev = this.tail;
this.tail = newNode;
}
- 在position === this.length时,我们要将newNode成为新的尾节点
- 通过
this.tail.next = newNode
,将原来的节点的next指向新节点,建立引用1 - 通过
newNode.prev = this.tail
,将新节点的prev,指向原来的尾节点,建立引用2
- 通过
this.tail = newNode
,将tail指向新的尾节点,建立引用3
2.3 在中间插入时
这种情况下,我们需要改变的引用关系稍微多一些
// 3.3 在中间插入 0 < position < this.length
var current = this.head;
var index = 0;
// 找到要插入的位置
while (index++ < position) {
current = current.next;
}
// 让新节点的next 指向要插入位置的节点(current)
newNode.next = current;
// 让新节点的prev 指向 current的prev
newNode.prev = current.prev;
// 将新节点与 当前节点的 前一个节点的 next建立关系
current.prev.next = newNode;
// 让newNode成为current的前一个节点
current.prev = newNode;
- 在0 < position < this.length时,我们假设在1的位置插入一个新的节点,如下图所示
- 我们先要找到要插入元素合适的位置,我们可以通过以下代码来找到插入的位置
- 我们找到current就是新的节点要插入的位置,所以下面要做的就是让新的节点与node1,node2建立起引用关系
- 通过
newNode.next = current
, 让新节点的next 指向要插入位置的节点(current),建立引用1 - 通过
newNode.prev = current.prev
,让新节点的prev 指向 current的prev,建立引用2
- 通过
current.prev.next = newNode
,将新节点与 当前节点的 前一个节点的 next建立关系,建立引用3 - 通过
current.prev = newNode
,让newNode成为current的前一个节点,建立引用4
测试代码
dll.insert(0, "000");
dll.insert(2, "222");
dll.insert(5, "555");
console.log(dll.toString()); // 000 10 222 20 30 555
1.5 实现get()方法
// 3.获取对应位置数据
DoublyLinkedList.prototype.get = function (position) {
if (position < 0 || position >= this.length) return null;
if (Math.floor(this.length / 2) > position) {
// 从前往后遍历
var index = 0;
var current = this.head;
while (index++ < position) {
current = current.next;
}
return current.data;
} else {
// 从后往前遍历
var index = this.length - 1;
var current = this.tail;
while (index-- > position) {
current = current.prev;
}
return current.data;
}
};
测试代码
console.log(dll.get(0)); // 000
console.log(dll.get(2)); // 222
console.log(dll.get(3)); // 20
console.log(dll.get(5)); // 555
1.6 实现indexOf()方法
// 4.获取数据对应的位置
DoublyLinkedList.prototype.indexOf = function (data) {
var current = this.head;
var index = 0;
while (current) {
if (current.data === data) return index;
current = current.next;
index += 1;
}
return -1;
};
测试代码
console.log(dll.indexOf("000")); // 0
console.log(dll.indexOf("222")); // 2
1.7 实现update()方法
其实就在get方法找到元素的基础上,赋值新的数据
// 5.更新方法
DoublyLinkedList.prototype.update = function (position, newData) {
if (position < 0 || position >= this.length) return false;
if (Math.floor(this.length / 2) > position) {
// 从前往后 遍历
var current = this.head;
var index = 0;
while (index++ < position) {
current = current.next;
}
current.data = newData;
return true;
} else {
// 从后往前 遍历
var current = this.tail;
var index = this.length - 1;
while (index-- > position) {
current = current.prev;
}
current.data = newData;
return true;
}
};
测试代码
dll.update(1, "111");
dll.update(3, "333");
dll.update(4, "444");
console.log(dll.toString()); // 000 111 222 333 444 555
1.8 实现removeAt()方法
// 6.删除指定位置
DoublyLinkedList.prototype.removeAt = function (position) {
if (position < 0 || position >= this.length) return null;
var current = this.head;
// 1.只有一个节点时
if (this.length === 1) {
this.head = null;
this.tail = null;
} else {
// 2.删除 头部节点
if (position === 0) {
this.head.next.prev = null;
this.head = this.head.next;
} else if (position === this.length - 1) {
// 3.删除尾部节点
this.tail.prev.next = null;
this.tail = this.tail.prev;
// 如果上面一种不好理解,也可以用下面这种写法
// current = this.tail;
// this.tail = this.tail.prev;
// this.tail.next = null;
} else {
// 4.删除中间
var index = 0;
while (index++ < position) {
current = current.next;
}
current.next.prev = current.prev;
current.prev.next = current.next;
}
}
this.length -= 1;
return current.data;
};
removeAt()方法图解
让我们一起回顾一下removeAt主要实现了什么功能?
removeAt()方法:删除指定位置的元素,删除成功返回删除的元素的数据,删除失败返回null
下面我们一起分析下,删除时会有哪些情况?
- 情况1:只有一个节点时,将head和tail指针,指向null即可
- 当链表的节点数量大于1时,会出现以下三种情况
- 2.1 删除 头部节点,也就是删除位置0上的节点
- 2.2 删除 尾部节点,也就是链表最后一位的节点
- 2.3 删除 中间节点
2.1 删除 头部节点
// 2.删除 头部节点
if (position === 0) {
this.head.next.prev = null;
this.head = this.head.next;
}
- 如下图所示,我们要删除的节点是Node1
- 通过
this.head.next.prev = null
,让Node1后面的节点指向null(图中引用1) ,与Node1失去引用关系 - 通过
this.head = this.head.next
,让head与Node1后面的节点建立引用2 - 经过上面两步的操作,Node1将没有别的节点指向它,最终会被垃圾回收机制给回收掉,也就是会被删除
2.2 删除尾部节点
if (position === this.length - 1) {
// 3.删除尾部节点
this.tail.prev.next = null;
this.tail = this.tail.prev;
}
- 通过
this.tail.prev.next = null
,让尾元素的前一个节点,与之失去引用关系1 - 通过
this.tail = this.tail.prev
,让尾元素的前一个节点与tail建立引用2,使之成为新的尾元素 - 之前的尾元素,由于没有其他元素指向它,最终会被垃圾回收机制删除掉
2.3 删除中间节点
// 4.删除中间
var index = 0;
while (index++ < position) {
current = current.next;
}
current.next.prev = current.prev;
current.prev.next = current.next;
- 先通过while循环找到要删除的元素,比如position = x,那么需要删除的节点就是Node(x+1),如下图所示,我们要做的就是断了前后元素的引用关系
- 通过
current.next.prev = current.prev
,建立新的引用1 - 通过
current.prev.next = current.next
,建立新的引用2 - current同样会被垃圾回收掉
测试代码
console.log(dll.removeAt(0)); // 000
console.log(dll.removeAt(1)); // 222
console.log(dll.removeAt(3)); // 555
console.log(dll.toString()); // 111 333 444
1.9 实现remove()方法
// 7.remove
DoublyLinkedList.prototype.remove = function (data) {
var position = this.indexOf(data);
return this.removeAt(position);
};
测试代码
console.log(dll.remove("111")); // 111
console.log(dll.toString()); // 333 444Ï
2. 封装完的双向链表
function DoublyLinkedList() {
// 内部类
function Node(data) {
this.data = data;
this.prev = null;
this.next = null;
}
// 属性
this.head = null;
this.tail = null;
this.length = 0;
// 1.向链表尾部追加数据
DoublyLinkedList.prototype.append = function (data) {
var newNode = new Node(data);
// 若是第一次添加,直接将head和tail指向新节点即可
if (this.length === 0) {
this.head = newNode;
this.tail = newNode;
} else {
// 让tail与新节点的prev建立连接
newNode.prev = this.tail;
// tail的next指向新节点
this.tail.next = newNode;
// 改变tail指针
this.tail = newNode;
}
this.length += 1;
};
// 2.向链表中插入数据
DoublyLinkedList.prototype.insert = function (position, data) {
// 1.边界判断
if (position < 0 || position > this.length) return false;
var newNode = new Node(data);
// 2.第一次添加
if (this.length === 0) {
this.head = newNode;
this.tail = newNode;
} else {
if (position === 0) {
// 3.1 在头部插入
this.head.prev = newNode;
newNode.next = this.head;
this.head = newNode;
} else if (position === this.length) {
// 3.2 在尾部插入
this.tail.next = newNode;
newNode.prev = this.tail;
this.tail = newNode;
} else {
// 3.3 在中间插入 0 < position < this.length
var current = this.head;
var index = 0;
// 找到要插入的位置
while (index++ < position) {
current = current.next;
}
// 让新节点的next 指向要插入位置的节点(current)
newNode.next = current;
// 让新节点的prev 指向 current的prev
newNode.prev = current.prev;
// 将新节点与 当前节点的 前一个节点的 next建立关系
current.prev.next = newNode;
// 让newNode成为current的前一个节点
current.prev = newNode;
}
}
this.length += 1;
return true;
};
// 3.获取对应位置数据
DoublyLinkedList.prototype.get = function (position) {
if (position < 0 || position >= this.length) return null;
if (Math.floor(this.length / 2) > position) {
// 从前往后遍历
var index = 0;
var current = this.head;
while (index++ < position) {
current = current.next;
}
return current.data;
} else {
// 从后往前遍历
var index = this.length - 1;
var current = this.tail;
while (index-- > position) {
current = current.prev;
}
return current.data;
}
};
// 4.获取数据对应的位置
DoublyLinkedList.prototype.indexOf = function (data) {
var current = this.head;
var index = 0;
while (current) {
if (current.data === data) return index;
current = current.next;
index += 1;
}
return -1;
};
// 5.更新方法
DoublyLinkedList.prototype.update = function (position, newData) {
if (position < 0 || position >= this.length) return false;
if (Math.floor(this.length / 2) > position) {
// 从前往后 遍历
var current = this.head;
var index = 0;
while (index++ < position) {
current = current.next;
}
current.data = newData;
return true;
} else {
// 从后往前 遍历
var current = this.tail;
var index = this.length - 1;
while (index-- > position) {
current = current.prev;
}
current.data = newData;
return true;
}
};
// 6.删除指定位置
DoublyLinkedList.prototype.removeAt = function (position) {
if (position < 0 || position >= this.length) return null;
var current = this.head;
// 1.只有一个节点时
if (this.length === 1) {
this.head = null;
this.tail = null;
} else {
// 2.删除 头部节点
if (position === 0) {
this.head.next.prev = null;
this.head = this.head.next;
} else if (position === this.length - 1) {
// 3.删除尾部节点
this.tail.prev.next = null;
this.tail = this.tail.prev;
// 如果上面一种不好理解,也可以用下面这种写法
// current = this.tail;
// this.tail = this.tail.prev;
// this.tail.next = null;
} else {
// 4.删除中间
var index = 0;
while (index++ < position) {
current = current.next;
}
current.next.prev = current.prev;
current.prev.next = current.next;
}
}
this.length -= 1;
return current.data;
};
// 7.remove
DoublyLinkedList.prototype.remove = function (data) {
var position = this.indexOf(data);
return this.removeAt(position);
};
DoublyLinkedList.prototype.size = function () {
return this.length;
};
DoublyLinkedList.prototype.isEmpty = function () {
return this.length === 0;
};
DoublyLinkedList.prototype.toString = function () {
return this.forwardString();
};
// 返回正向遍历节点字符串形式
DoublyLinkedList.prototype.forwardString = function () {
var current = this.head;
var resultString = "";
while (current) {
resultString += current.data + " ";
current = current.next;
}
return resultString;
};
// 返回反向遍历的节点的字符串形式
DoublyLinkedList.prototype.backwardString = function () {
var current = this.tail;
var resultString = "";
while (current) {
resultString += current.data + " ";
current = current.prev;
}
return resultString;
};
}
// 测试代码
var dll = new DoublyLinkedList();
dll.append("10");
dll.append("20");
dll.append("30");
console.log(dll.toString()); // 10 20 30
console.log(dll.forwardString()); // 10 20 30
console.log(dll.backwardString()); // 30 20 10
dll.insert(0, "000");
dll.insert(2, "222");
dll.insert(5, "555");
console.log(dll.toString()); // 000 10 222 20 30 555
console.log(dll.get(0)); // 000
console.log(dll.get(2)); // 222
console.log(dll.get(3)); // 20
console.log(dll.get(5)); // 555
console.log(dll.indexOf("000")); // 0
console.log(dll.indexOf("222")); // 2
dll.update(1, "111");
dll.update(3, "333");
dll.update(4, "444");
console.log(dll.toString()); // 000 111 222 333 444 555
console.log(dll.removeAt(0)); // 000
console.log(dll.removeAt(1)); // 222
console.log(dll.removeAt(3)); // 555
console.log(dll.toString()); // 111 333 444
console.log(dll.remove("111")); // 111
console.log(dll.toString()); // 333 444