javascript数据结构 -- 双向链表

126 阅读5分钟

1. 双向链表

顾名思义就是链表中的内部节点不仅维护一个指向后面节点的next属性,还维护一个指向前一个节点的prev属性

2. 对比单向链表

  • 双向链表的优势在于: 不仅能够向后遍历还可以向前遍历,这极大的方便了使用者
  • 双向链表的劣势在于:
    • 每一个node节点除了维护next属性指向下一个node还要维护一个prev属性指向上一个node,这意味着更大的开销;
    • 并且双向链表实例除了维护head,还要再维护一个tail指向链表的尾部;
    • 此外,双向链表的实现比起单向链表来说也更加的复杂!

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. 小结

双向链表基于单向链表实现,比单向链表稍微复杂一些,但是本质不变;如果能够较好的掌握单向列表的原理,实现双向链表是没有难度的!