1. 双向链表
顾名思义就是链表中的内部节点不仅维护一个指向后面节点的next属性,还维护一个指向前一个节点的prev属性
2. 对比单向链表
- 双向链表的优势在于: 不仅能够向后遍历还可以向前遍历,这极大的方便了使用者
- 双向链表的劣势在于:
- 每一个node节点除了维护
next
属性指向下一个node还要维护一个prev
属性指向上一个node,这意味着更大的开销; - 并且双向链表实例除了维护
head
,还要再维护一个tail
指向链表的尾部; - 此外,双向链表的实现比起单向链表来说也更加的复杂!
- 每一个node节点除了维护
3. 常见方法
append: 同单向链表
insert: 同单向链表
get: 同单向链表
indexOf: 同单向链表
update: 同单向链表
removeAt: 同单向链表
remove: 同单向链表
isEmpty: 同单向链表
size: 同单向链表
toString: 同单向链表
forwardString(): 从头到尾打印整个链表
backwardString(): 从尾到头打印整个链表
getHead(): 获取头元素的信息
getTail(): 获取尾部元素的信息
4. 实现
class _DNode<T> {
prev: null | _DNode<T> = null;
next: null | _DNode<T> = null;
constructor(public ele: T){}
}
class DoublyLinkedList<T> {
head: null | _DNode<T> = null;
tail: null | _DNode<T> = null;
length = 0;
constructor(public _Node: typeof _DNode<T>){}
// 在链表末尾插入一个新的节点
append(ele: T): T {
const newNode = new this._Node(ele);
// 判断链表是不是为空
if(this.isEmpty()){
// 如果插入的节点是此双向链表的第一个元素,那么就需要将链表的头和尾都指向此元素
this.head = newNode;
this.tail = newNode;
} else {
// 如果不是的话:需要找出末尾的节点,然后将新节点的prev指向原来的尾节点,将原来的尾节点的next指向新节点
// 最后将链表的tail执行新节点
// 新节点的next仍然为初始值null
const oldLastNode = this.tail!;
oldLastNode.next = newNode;
newNode.prev = oldLastNode;
this.tail = newNode;
}
// 不要忘记修改链表的长度
this.length++;
return newNode.ele;
}
// 在双向链表的指定位置增加一个节点
insert(position: number, ele: T): null | T {
if(!this.isValid(position)) return null;
if(this.isEmpty()){
return this.append(ele);
}
// 如果插入的位置是尾部则直接调用append方法即可
if(position === this.size() - 1){
return this.append(ele);
}
const newNode = new this._Node(ele);
// 如果插入的位置是链表头: 先找到原来的头节点oldFirstNode,然后将this.head指向newNode; 将newNode和oldFirstNode相互指定
if(position === 0){
const oldFirstNode = this.head!;
this.head = newNode;
newNode.next = oldFirstNode;
oldFirstNode.prev = newNode;
this.length++;
return newNode.ele;
}
// 下面处理插入点在队列中间的情况
let current = this.head;
let index = 0;
while(current){
if(index + 1 === position){
// 找到插入位置的前一个节点
const prev = current;
// 找到插入之后的后一个节点(后一个节点可能是null,也就是说如果插入的位置刚好是链表的尾部的话)
const next = current.next || null;
// prev的next指向newNode,newNode的prev指向prev
// next的prev指向newNode,newNode的next指向next
prev.next = newNode;
newNode.prev = prev;
newNode.next = next;
if(next) next.prev = newNode;
break;
}
current = current.next;
index++;
}
this.length++;
return newNode.ele;
}
// 通过下标获取node的信息
get(position: number): null | T{
if(!this.isValid(position)) return null;
let current = this.head;
let index = 0;
while(current){
if(index === position){
return current.ele;
}
current = current.next;
index++;
}
return null;
}
// 通过元素的值获取此元素所在的node在链表中的序列
indexOf(ele: T): number{
let current = this.head;
let index = 0;
while(current){
if(current.ele === ele){
return index;
}
current = current.next;
index++;
}
return -1;
}
// 更改某个位置上node中的信息
update(position: number, ele: T): boolean {
if(!this.isValid(position)) return false;
let current = this.head;
let index = 0;
while(current){
if(index === position){
current.ele = ele;
return true;
}
current = current.next;
index++;
}
return false;
}
// 删除链表中某个位置对应的node
removeAt(position: number): null | T {
if(!this.isValid(position)) return null;
// 如果链表中就一个节点,并且下标值是valid的,那就证明要删除这个链表中的唯一值
if(this.size() === 1){
const rst = this.head!.ele;
this.head = null;
this.tail = null;
this.length--;
return rst;
}
// 如果删除的是首节点: 直接将this.head指向第二个节点就可以了;然后将第二个节点的prev指向null
if(position === 0){
const rst = this.head!.ele;
const secondNode = this.head!.next!;
this.head = secondNode;
secondNode.prev = null;
this.length--;
return rst;
}
// 如果删除的是尾节点:就先找到尾节点的前一个节点lastPrev,然后将lastPrev的next指向null,最后将this.tail指向lastPrev
if(position === this.size() - 1){
const rst = this.tail!.ele;
const lastPrev = this.tail!.prev!;
lastPrev.next = null;
this.tail = lastPrev;
this.length--;
return rst;
}
// 排除了上面的特殊情况之后剩下的情况
let current = this.head;
let index = 0;
while(current) {
if(index+1 === position){
// 找到待删除的元素的上一个(上一个节点也有可能是null,也就是说删除的刚好是第一个节点)
const prev = current;
// 找到待删除的元素的下一个(下一个节点可能是null,也就是说现在要删除的节点刚好是最后一个元素)
const next = current.next!.next!;
// 删除的法则就是,让上一个节点的next直接指向下一个节点,而让下一个节点的prev直接指向上一个节点
prev.next = next;
next.prev =prev;
this.length--;
return current.ele;
}
current = current.next;
index++;
}
return null;
}
// 根据ele的值删除对应的node节点
remove(ele: T): null | T {
const index = this.indexOf(ele);
return this.removeAt(index)
}
// 从头到尾依次打印
backwardString(): string{
let current = this.head;
let rst = '';
while(current){
rst += (current.ele as unknown as string).concat(" ");
current = current.next;
}
return rst;
}
// 单向链表中的打印
toString(): string {
return this.backwardString();
}
// 从尾到头依次打印
forwardString(): string{
let current = this.tail;
let rst = '';
while(current){
rst += (current.ele as unknown as string).concat(" ");
current = current.prev;
}
return rst;
}
// 判断链表是否为空
isEmpty(): boolean{
return !this.length;
}
// 链表的大小
size(): number {
return this.length;
}
// 判断下标值是否没有越界
isValid(position: number){
return !(position < 0 || position > this.size());
}
// 获取链表中第一个元素的ele
getHead(): null | T {
return this.head?.ele || null;
}
// 获取链表中最后一个元素的ele
getTail(): null | T {
return this.tail?.ele || null;
}
}
// test
const dlbArr = new DoublyLinkedList(_DNode);
dlbArr.append('a');
dlbArr.append('b');
dlbArr.append('c');
dlbArr.append('d');
dlbArr.append('e');
console.log(dlbArr.toString()); // a b c d e
dlbArr.insert(2,'x');
console.log(dlbArr.toString()); // a b x c d e
dlbArr.removeAt(3);
console.log(dlbArr.toString()); // a b x d e
dlbArr.update(3, 'y');
console.log(dlbArr.toString()); // a b x y e
console.log(dlbArr.get(3)); // y
dlbArr.remove('b');
console.log(dlbArr.toString()); // a x y e
dlbArr.remove('t');
console.log(dlbArr.toString()); // a x y e
console.log(dlbArr.indexOf('e')); // 3
console.log(dlbArr.size()); // 4
console.log(dlbArr.forwardString()); // e y x a
console.log(dlbArr.backwardString()); // a x y e
console.log(dlbArr.getHead()); // a
console.log(dlbArr.getTail()); // e
dlbArr.removeAt(0);
console.log(dlbArr.toString()); // x y e
dlbArr.removeAt(0);
console.log(dlbArr.toString()); // y e
dlbArr.insert(0, 't');
console.log(dlbArr.toString()); // t y e
dlbArr.insert(3, 't');
console.log(dlbArr.toString()); // t y e t
5. 小结
双向链表基于单向链表实现,比单向链表稍微复杂一些,但是本质不变;如果能够较好的掌握单向列表的原理,实现双向链表是没有难度的!