链表-双向链表
之前实现的是单向链表, 即每个节点有一个元素值和一个指向下一个元素的指针, 是单向的. head ->a ->b ->c1
.
现在来做一个双向的, 即对每个节点来说, 有两个指针, 一个链向下一个元素, 另一个链向上一个元素. head -> <- b -> <- c
.
链表初始化
基本的操作套路和单链表是差不多的啦.
// 节点类
class Node {
constructor(element, next = null, prev = null) {
this.element = element
this.next = next
// 新增
this.prev = prev
}
}
// 链表类
class doublyLinkedList {
constructor() {
this.count = 0 // 链表大小
this.head = undefined // 指向头节点元素
this.tail = undefined // 指向尾节点元素
}
}
公共方法
链表的长度, 查询头, 尾元素, 打印链表, 根据元素值查索引, 根据索引查询值等链表公用方法.
// 链表长度
size() {
return this.count
}
getHead() {
return this.head
}
getTail() {
return this.tail
}
// 将链表节点元素以字符串形式打印
toString() {
if (this.head == null) return undefined
let str = `${this.head.element}`
let current = this.head.next
for (let i = 1; i < this.count && current != null; i++) {
str = `${str}, ${currrent.element}`
current = current.next
}
return str
}
// 查询元素的索引
indexOf(element) {
let current = this.head
for (let i = 0; i < index && current != null; i++) {
if (current.element == element) return i
current = current.next
}
// 没找到则返回 -1
return -1
}
// 根据索引返回对应节点
getElementAt(index) {
if (index < 0 || index > this.count) return undefined
let node = this.head
for (let i = 0; i < index && node != null; i++) {
node = node.next
}
return node
}
从任意位置插入元素
- 头部插入
- 空链表, 让 head, tail 指针都指向目标元素 node
- 非空链表, 让 node 变成首元素 (前插)
- 尾部插入, 通过 tail 先定位到尾部元素, 然后追加 node, 要记得 prev 指向
- 中间插入, 找出目标元素的前后元素进行断链, 插入, 再链接
// 从任意位置插入元素
insert(index, element) {
// 越界检查
if (index < 0 || index > this.count) return false
const node = new Node(element)
let current = this.head // 让 current 默认指向头节点
if (index == 0) {
// 链首插入
if (this.head == null) {
// 空链表则让 head, tail 都指向 node 即可
this.head = node
this.tail = node
} else {
// 头结点有值时, 则让 node 前插
node.next = current
current.prev = node
this.head = node
} else if (inde == this.count) {
// 尾部插入, 通过 tail 定位到尾部元素, 然后追加 node
current = this.tail
current.next = node
node.prev = current
this.tail = node
} else {
// 中间插入, 调整 previous, current 关系即可
current = this.getElementAt(index)
previous = current.prev
previous.next = node
node.prev = previous
node.next = current
current.prev = node
}
}
// 记得要更新链表长度
this.count++
return true
}
关键就是要能在脑海里想象出这个节点, 链接的画面就很轻松写出来了.
从任意位置删除元素
- 头节点删
- 空链表, 返回 false
- 链表有一个元素, 更新 head, tail
- 链表多个元素, 将后面"补位"的元素的 prev 指向 null
- 尾节点删, 通过 tail 定位到尾元素, 让尾元素的 prev 指向 原来的倒2元素, next 则指向 null
- 从中间删, 对 previous, current, current.next 进行中间跳跃
// 从任意位置删除元素
removeAt(index) {
// 越界检查, 空链表检查
if (index < 0 || index > this.count) return false
if (this.count == 0) return undefined
let current = this.head // 老规矩, 让 current 默认指向头节点
if (index == 0) {
// 删除头节点, 则让 this.head -> this.head.next 即可
this.head = current.next
// 如果链表只有一个元素, 则让 tail -> null
if (this.count == 1) {
this.tail = null
} else {
// 链表有多个元素, 后面补位的 prev 则指向 null
current.next.prev = null
}
} else if (index == this.count - 1) {
// 删除尾节点, 定位到尾节点, 让原来到倒2元素变成尾节点即可
current = this.tail
this.tail = current.prev
current.prev.next = null
} else {
// 删除中间节点, 让 previous 越过 current 指向 current.next 即可
current = this.getElementAt(index)
const previous = current.prev
previous.next = current.next
current.next.prev = previous
}
// 还是要记得更新链表长度
this.count--
return current.element
}
// 顺带根据 indexOf(element) 实现 remove(element) 方法
remove(element) {
const index = indexOf(element)
return this.removeAt(index)
}
感觉这个删除节点好像比增加节点要简单一些呢.
完整实现
// 节点类
class Node {
constructor(element) {
this.element = element
this.next = null
// 新增
this.prev = null
}
}
// 链表类
class doublyLinkedList {
constructor() {
this.count = 0 // 链表大小
this.head = undefined // 指向头节点元素
this.tail = undefined // 指向尾节点元素
}
size() {
return this.count
}
getHead() {
return this.head.element
}
getTail() {
return this.tail.element
}
// 将元素的值用字符串形式打印出来
toString() {
if (this.head == null) return undefined
let str = `${this.head.element}`
let current = this.head.next
// 遍历节点将元素连接起来
for (let i = 1; i < this.count && current != null; i++) {
str = `${str}, ${current.element}`
current = current.next
}
return str
}
// 根据索引, 查询并返回节点
getElementAt(index) {
if (index < 0 || index > this.count) return undefined
let node = this.head
for (let i = 0; i < index && node != null; i++) {
node = node.next
}
return node
}
// 在任意位置插入元素
insert(index, element) {
// 越界检查
if (index < 0 || index > this.count) return false
// 实例化节点对象, 根据位置 (头, 尾, 中间) 分情况处理
// current 指针在多处会用到, 用来指向目标位置的元素
const node = new Node(element)
let current = this.head
if (index == 0) {
// 头结点插入, 如果当前是空链表, head, tail 都指向改节点即可
if (this.head == null) {
this.head = node
this.tail = node
} else {
// 头结点插入, 如果当前头结点有值, 则进行前插元素
node.next = current
current.prev = node
this.head = node
}
} else if (index == this.count) {
// 尾部插入, 直接通过 tail 定位到尾部元素往后追加 node 即可
current = this.tail
current.next = node
node.prev = current
this.tail = node
} else {
// 中间插入则需要用 getElementAt(index) 定位目标前后的元素断链哦
current = this.getElementAt(index)
const previous = current.prev
previous.next = node
node.prev = previous
node.next = current
current.prev = node
}
// 不管从哪插, 一定要更新长度
this.count++
return true
}
// 从任意位置移除元素
removeAt(index) {
// 越界检查, 链表为空检查
if (index < 0 || index > this.count) return undefined
if (this.count == 0) return undefined
let current = this.head
if (index == 0) {
// 场景1: 被删的是头节点, 则让 this.head -> current.next 即可
this.head = current.next
// 如果只有一个元素, 则要更新 tail 也指向 null
if (this.count == 1) {
this.tail = undefined
} else {
// 原来位置为1 的元素已被干掉, 后面补位的元素的 prev 则指向为 null
current.next.prev = undefined
}
} else if (index == this.count -1) {
// 场景2: 被删的是尾部节点, 直接让 current 通过 tail 找到尾元素操作
// 让尾元素的 prev 指向原来的倒数第二元素, next 则指向 null
current = this.tail
this.tail = current.prev
current.prev.next = null
} else {
// 场景3: 被删的是中间节点, 则对 previous, current, current.next 中间跳跃
current = this.getElementAt(index)
const previous = current.prev
previous.next = current.next
current.next.prev = previous
}
// 记得删除元素要更新链表长度哦
this.count--
return current.element
}
// 查找元素位置
indexOf(element) {
let current = this.head
for (let i = 0; i < this.count && current != null; i++) {
if (current.element == element) return i
// 移动指针
current = current.next
}
return -1
}
// 删除元素
remove(element) {
const index = this.indexOf(element)
return this.removeAt(index)
}
}
// test
const list = new doublyLinkedList()
list.insert(0, 888)
list.insert(0, 666)
list.insert(0, 111)
list.insert(0, 'first')
console.log('链表元素是:', list.toString());
console.log('链表长度是: ', list.size())
console.log('tail: ', list.getTail());
console.log('从位置1处添加 999:', list.insert(1, 999));
console.log('链表元素是:', list.toString());
console.log('链表长度是: ', list.size());
console.log('tail: ', list.getTail());
console.log('从尾部处添加 nb:', list.insert(list.size(), 'nb'));
console.log('链表元素是:', list.toString());
console.log('链表长度是: ', list.size());
console.log('tail: ', list.getTail());
console.log('从头部处添加 nb1:', list.insert(0, 'nb1'));
console.log('链表元素是:', list.toString());
console.log('tail: ', list.getTail());
// 删除相关
console.log('从头部删除元素: ', list.removeAt(0));
console.log('链表元素是:', list.toString());
console.log('tail: ', list.getTail());
console.log('从尾部删除元素: ', list.removeAt(list.size() - 1));
console.log('链表元素是:', list.toString());
console.log('tail: ', list.getTail());
console.log('从位置 1 处删除元素: ', list.removeAt(1));
console.log('链表元素是:', list.toString());
console.log('tail: ', list.getTail());
// 查询元素 666 的位置
console.log('查询元素 666 的位置: ', list.indexOf(666)); // 2
console.log('查询元素 hhh 的位置: ', list.indexOf('hhh')); // -1
// 删除元素
console.log('删除元素 666 : ', list.remove(666));
console.log('链表元素是:', list.toString());
console.log('删除元素 hhh : ', list.remove('hhh'));
console.log('链表元素是:', list.toString());
测试结果如下:
PS F:\algorithms> node .\double_linked_list.js
链表元素是: first, 111, 666, 888
链表长度是: 4
tail: 888
从位置1处添加 999: true
链表元素是: first, 999, 111, 666, 888
链表长度是: 5
tail: 888
从尾部处添加 nb: true
链表元素是: first, 999, 111, 666, 888, nb
链表长度是: 6
tail: nb
从头部处添加 nb1: true
链表元素是: nb1, first, 999, 111, 666, 888, nb
tail: nb
从头部删除元素: nb1
链表元素是: first, 999, 111, 666, 888, nb
tail: nb
从尾部删除元素: nb
链表元素是: first, 999, 111, 666, 888
tail: 888
从位置 1 处删除元素: 999
链表元素是: first, 111, 666, 888
tail: 888
查询元素 666 的位置: 2
查询元素 hhh 的位置: -1
删除元素 666 : 666
链表元素是: first, 111, 888
删除元素 hhh : undefined
链表元素是: first, 111, 888
至此, 双向链表的基本实现也就搞定啦.
耐心和恒心, 总会获得回报的.