03、数据结构与算法(双向链表)

128 阅读11分钟

1、双向链表的概念

我们在学完单链表之后,双向链表可以说是手到擒来。与单向链表不同的是,每一个节点有三个区域组成:两个指针域,一个数据域。
前一个指针域(prev):存储前驱节点的内存地址。
后一个指针域(next):存储后继节点的内存地址。
数据域(val):存储节点数据。

微信图片_20240110165850.png

1.1 双向链表的特点

(1)双向性:每个节点除了存储自己的数据外,还包含两个指针域,分别存储前驱和后继的内存地址,因此可以在链表中向前或向后遍历。
(2)动态性:双向链表可以在运行时动态地添加、删除和修改节点,相对于数组等静态数据结构更加灵活。
(3)插入和删除操作高效:由于双向链表中的节点包含指向前驱和后继的的指针,因此插入和删除操作只需要调整节点前后的指针,而不需要像数组那样移动大量元素。
(4)空间利用率较高:相比单向链表,双向链表需要额外的指针来表示前一个节点,空间利用率略低一些,但在某些情况下方便了一些操作。
(5)双向遍历能力:双向链表可以从任意节点开始向前或向后遍历,这在某些场景中非常有用,例如需要反向迭代链表。

1.2 单向链表和双向链表的区别

双向链表在内存开销上相对于单向链表稍高。由于有两个指针,插入和删除操作涉及到更多的指针修改,相对于单向链表略微复杂。

单向链表与双向链表对比:

(1)内存

单向链表: 1,只包含一个指针域,指向后继节点,所以相对于双向链表具有更低的内存占用。
双向链表: 1,除了包含后继的指针域外,还包含了指向前驱的指针域,因此相对于单向链表需要更多的内存空间。

(2)操作效率

单向链表: 1,插入和删除对象简单且高效,只需要修改相关的节点指针方向;2,访问数据效率较低,只能顺着一个方向查找数据。
双向链表: 1,插入和删除操作稍微复杂一些,需要同时修改相关的前后节点指针方向;2,访问数据的效率更高,可以从任意位置开始向前或者向后查找;

应用场景

单向链表: 1,需要频繁的插入和删除操作,而不需要反向遍历的情况下使用,例如:实现队列或者栈等数据结构;
双向链表: 1,需要频繁的插入,删除和反向遍历的情况使用,例如:LRU缓存淘汰算法中的双向链表用于维护最近使用的数据库顺序。

1.3 双向链表的封装

双向链表常见的操作(方法):

  • append(element):向链表尾部添加一个新的项;
  • inset(position,element):向链表的特定位置插入一个新的项;
  • get(element):获取对应位置的元素;
  • indexOf(element):返回元素在链表中的索引,如果链表中没有元素就返回-1;
  • update(position,element):修改某个位置的元素;
  • removeAt(position):从链表的特定位置移除一项;
  • isEmpty():如果链表中不包含任何元素,返回trun,如果链表长度大于0则返回false;
  • size():返回链表包含的元素个数,与数组的length属性类似;
  • toString():返回反向遍历的节点的字符串形式;
  • forwardString():返回正向遍历节点字符串形式;

初始化链表类

//双向列表内部类;节点类
class  ListNode{
    val
    prev
    next
    constructor(val){
        this.val = val
        this.prev = null
        this.next = null
    }
}
// 双向链表类
class LinkList{
    head
    tail
    length
    constructor(){
        this.head = null
        this.tail = null
        this.length = 0
    }
}

append(element)方法封装:

//双向列表内部类;节点类
class  ListNode{
    val
    prev
    next
    constructor(val){
        this.val = val
        this.prev = null
        this.next = null
    }
}
// 双向链表类
class LinkList{
    head
    tail
    length
    constructor(){
        this.head = null
        this.tail = null
        this.length = 0
    }
    // append(element):向链表尾部添加一个新的项;
    append(element){
        let newNode = new ListNode(element)
        // 添加的是第一个节点
        if(this.length === 0){
            this.tail = newNode
            this.head = newNode
        }else{ // 添加的不是第一个节点
            newNode.prev = this.tail
            this.tail.next = newNode
            this.tail = newNode
        }
        this.length++
    }
}

过程讲解:
添加节点时分为多种情况:

  • 情况1:添加的是第一个节点:只需要让head和tail都指向新节点即可;
  • 情况2:添加的不是第一个节点,如下图所示:只需要改变相关引用的指向即可。 通过:newNode.prev = this.tail:将插入节点的prev指向链表的tail(尾部);
    通过:this.tail.next = newNode:将tail(尾部)的next节点;以上两步将插入的节点连接到链表;
    通过:this.tail = newNode;将tail的值变成插入的值(完成节点的尾部插入);
    要注意改变变量指向的顺序,最后修改tail指向,这样未修改前tail始终指向原链表的最后一个节点。

insert(position,element)方法封装:

//双向列表内部类;节点类
class  ListNode{
    val
    prev
    next
    constructor(val){
        this.val = val
        this.prev = null
        this.next = null
    }
}
// 双向链表类
class LinkList{
    head
    tail
    length
    constructor(){
        this.head = null
        this.tail = null
        this.length = 0
    }
    // inset(position,element):向链表的特定位置插入一个新的项;
    inset(position,element){
        if(position < 0 || position > this.length){
            return false
        }
        let newNode = new ListNode(element)

        if(this.length === 0){ // 与append添加第一个节点一样
            this.tail = newNode
            this.head = newNode
        }else{
            if(position === 0){  
                this.head.prev = newNode 
                newNode.next = this.head 
                this.head = newNode
            }else if(position === this.length){  // 与append添加不是第一个节点一样
                newNode.prev = this.tail
                this.tail.next = newNode
                this.tail = newNode
            }else{
                let current = this.head
                let index = 0
                while(index < position){
                    current = current.next
                    index += 1
                }
                newNode.prev = current.prev
                newNode.next = current
                current.prev.next = newNode
                current.prev = newNode
                // console.log(newNode,'------')
            }
        }
        this.length += 1
        return true   // 插入成功返回true
    }
}

过程讲解:
插入节点可分为多种情况:

当原链表为空时

  • 情况1:插入的新节点是链表的第一个节点;只需要让head和tail都指向newNode即可(和在尾部插入第一个节点一样)。

当原链表不为空时

  • 情况2:当position == 0,即在链表的首部添加节点。
    首先,通过:this.head.prev = newNode,将头部节点的prev指针指向插入节点;
    然后,通过:newNode.next = this.head,将插入节点的next指针指向原本的头部节点;
    最后,通过:this.head = newNode,将头部节点的值改为插入节点的值;

  • 情况3:position == this.length,即在链表的尾部添加节点。
    通过:newNode.prev = this.tail:将插入节点的prev指向链表的tail(尾部);
    通过:this.tail.next = newNode:将tail(尾部)的next节点;以上两步将插入的节点连接到链表;
    通过:this.tail = newNode;将tail的值变成插入的值(完成节点的尾部插入);

  • 情况4:0 < position < this.length,即在链表的中间插入新节点
    首先,需要定义变量current按照之前的思路,通过while循环找到position位置的后一个节点,循环结束后index = position;
    如下图所示:当position = 1时,current就指向了Node2。这样操作current就等同于间接地操作Node2,还可以通过current.prev间接获取Node1。得到了newNode的前一个节点和后一个节点就可以通过改变它们的prev和next变量的指向来插入newNode了。

微信图片_20240110182348.png

get(position) 方法封装:

//双向列表内部类;节点类
class  ListNode{
    val
    prev
    next
    constructor(val){
        this.val = val
        this.prev = null
        this.next = null
    }
}
// 双向链表类
class LinkList{
    head
    tail
    length
    constructor(){
        this.head = null
        this.tail = null
        this.length = 0
    }
    // get(position):获取对应位置的元素;
    get(position){
        if(position < 0 || position > this.length){
            return null
        }

        
        if(Math.floor(this.length / 2) > position){
            let index = 0
            let current = this.head 
            while(index < position){
                current = current.next
                index += 1
            }
            return current.val
        }else{
            let current = this.tail
            let index = this.length - 1

            while(index > position){
                current = current.prev
                index -= 1
            }
            return current.val
        }
    }
}

过程讲解:
定义两个变量current和index,按照之前的思路通过while循环遍历分别获取当前节点和对应的索引值index,直到找到需要获取的position位置后的一个节点,此时index = pos =x,然后return current.val即可。

如果链表的节点数量很多时,这种查找方式效率不高,改进方法为:

  • 当this.length / 2 > position:从头(head)开始遍历;
  • 当this.length / 2 < position:从尾(tail)开始遍历;

indexOf(element) 方法封装

//双向列表内部类;节点类
class  ListNode{
    val
    prev
    next
    constructor(val){
        this.val = val
        this.prev = null
        this.next = null
    }
}
// 双向链表类
class LinkList{
    head
    tail
    length
    constructor(){
        this.head = null
        this.tail = null
        this.length = 0
    }
    // indexOf(element):返回元素在链表中的索引,如果链表中没有元素就返回-1;
    indeOf(element){
        let current = this.head
        let index = 0
        while(current){
            if(element === current.val){
                return index
            }
            current = current.next
            index += 1
        }
        return -1
    }
}

过程讲解:
以(current)作为条件,通过while循环遍历链表中的所有节点(停止条件为current = null)。在遍历每个节点时将current指向的当前节点的data和传入的val进行比较即可。

update(position,element) 方法封装:

//双向列表内部类;节点类
class  ListNode{
    val
    prev
    next
    constructor(val){
        this.val = val
        this.prev = null
        this.next = null
    }
}
// 双向链表类
class LinkList{
    head
    tail
    length
    constructor(){
        this.head = null
        this.tail = null
        this.length = 0
    }
    // update(position,element):修改某个位置的元素;
    update(position,element){
        if(position < 0 || position >= this.length){
            return false
        } 
        if(Math.floor(this.length / 2) > position){
            let index = 0
            let current = this.head 
            while(index < position){
                current = current.next
                index += 1
            }
            current.val = element
        }else{
            let current = this.tail
            let index = this.length - 1
            while(index > position){
                current = current.prev
                index -= 1
            }
           current.val = element
        }
        return true
    }
}

过程讲解:
以(index++ < position)为条件,通过while循环遍历链表中的节点(停止条件为index = position)。循环结束后,current指向需要修改的节点。

removeAt(position) 方法封装

//双向列表内部类;节点类
class  ListNode{
    val
    prev
    next
    constructor(val){
        this.val = val
        this.prev = null
        this.next = null
    }
}
// 双向链表类
class LinkList{
    head
    tail
    length
    constructor(){
        this.head = null
        this.tail = null
        this.length = 0
    }
    // removeAt(position):从链表的特定位置移除一项;
    removeAt(position){
        if(position < 0 || position >= this.length){
            return false
        }
        if(this.length === 1){
            this.head = null
            this.tail = null
        }else{
            if(position === 0){
                let current = this.head
                current = current.next
                current.prev = null
                this.head = current
            }else if(position === this.length - 1){
                let current = this.tail
                current = current.prev
                current.next = null
                this.tail = current
            }else{
                let index = 0
                let current = this.head
                while(index < position){
                    current = current.next
                    index += 1
                }
                current.next.prev = current.prev
                current.prev.next = current.next
            }
        }
        this.length -= 1
        return true
    }
}

过程讲解:
删除节点时有多种情况:
当链表的length = 1时

  • 情况1:删除链表中的所有节点:只需要让链表的head和tail指向null即可。

    当链表的length > 1时

  • 情况2:删除链表中的第一个节点:
    通过:this.head.next.prev = null,将第二个节点的prev指针指向空;
    通过:this.head = this.head.next,将第一个节点变成第二个节点;
    虽然Node1有引用指向其它节点,但是没有引用指向Node1,那么Node1会被自动回收。

  • 情况3:删除链表中的最后一个节点:
    通过:this.tail.prev.next = null,将倒数第二个节点next指针指向空;
    通过:this.tail = this.tail.prev,将最后一个节点变成倒数第二个节点;

  • 情况4:删除链表中间的节点:
    通过while循环找到需要删除的节点,比如position = x,那么需要删除的节点就是Node(x+1)
    通过:current.next.prev = current.prev,将指定位置的下一个节点的prev指针指向指定节点的上一个节点;
    通过:current.prev.next = current.next,将指定节点的上一个节点的next指针指向指定节点的下一个节点;
    这样就没有引用指向Node(x+1)了(current虽指向Node(x+1),但current时临时变量,该方法执行完就会被销毁),随后Node(x+1)就会被自动删除。

剩余方法的代码封装

//双向列表内部类;节点类
class  ListNode{
    val
    prev
    next
    constructor(val){
        this.val = val
        this.prev = null
        this.next = null
    }
}
// 双向链表类
class LinkList{
    head
    tail
    length
    constructor(){
        this.head = null
        this.tail = null
        this.length = 0
    }
    // isEmpty():如果链表中不包含任何元素,返回trun,如果链表长度大于0则返回false;
    isEmpty(){
        return this.length === 0
    }
    // size():返回链表包含的元素个数,与数组的length属性类似;
    size(){
        return this.length
    }

    // forwardString():返回正向(tail节点开始)遍历节点字符串形式;
    forwardString(){
        let current = this.tail
        let str = ''
        while(current){
            str = str + current.val + ''
            current = current.prev
        }

        return str
    }
    // toString():返回反向(head节点开始)遍历的节点的字符串形式;
    toString(){
        let current = this.head
        let str = ''
        while(current){
            str = str + current.val + ''
            current = current.next
        }

        return str
    }
}

双向链表完整代码封装

//双向列表内部类;节点类
class  ListNode{
    val
    prev
    next
    constructor(val){
        this.val = val
        this.prev = null
        this.next = null
    }
}
// 双向链表类
class LinkList{
    head
    tail
    length
    constructor(){
        this.head = null
        this.tail = null
        this.length = 0
    }
    // append(element):向链表尾部添加一个新的项;
    append(element){
        let newNode = new ListNode(element)
        // 添加的是第一个节点
        if(this.length === 0){
            this.tail = newNode
            this.head = newNode
        }else{ // 添加的不是第一个节点
            newNode.prev = this.tail
            this.tail.next = newNode
            this.tail = newNode
        }
        this.length++
    }
    // inset(position,element):向链表的特定位置插入一个新的项;
    inset(position,element){
        if(position < 0 || position > this.length){
            return false
        }
        let newNode = new ListNode(element)

        if(this.length === 0){ // 与append添加第一个节点一样
            this.tail = newNode
            this.head = newNode
        }else{
            if(position === 0){  
                let current = this.head
                this.head = newNode
                current.prev = this.head
                this.head.next = current
            }else if(position === this.length){  // 与append添加不是第一个节点一样
                newNode.prev = this.tail
                this.tail.next = newNode
                this.tail = newNode
            }else{
                let current = this.head
                let index = 0
                while(index < position){
                    current = current.next
                    index += 1
                }
                newNode.prev = current.prev
                newNode.next = current
                current.prev.next = newNode
                current.prev = newNode
                // console.log(newNode,'------')
            }
        }
        this.length += 1
        return true   // 插入成功返回true
    }
    // get(position):获取对应位置的元素;
    get(position){
        if(position < 0 || position > this.length){
            return null
        }

        
        if(Math.floor(this.length / 2) > position){
            let index = 0
            let current = this.head 
            while(index < position){
                current = current.next
                index += 1
            }
            return current.val
        }else{
            let current = this.tail
            let index = this.length - 1

            while(index > position){
                current = current.prev
                index -= 1
            }
            return current.val
        }
    }
    // indexOf(element):返回元素在链表中的索引,如果链表中没有元素就返回-1;
    indeOf(element){
        let current = this.head
        let index = 0
        while(current){
            if(element === current.val){
                return index
            }
            current = current.next
            index += 1
        }
        return -1
    }
    // update(position,element):修改某个位置的元素;
    update(position,element){
        if(position < 0 || position >= this.length){
            return false
        } 
        if(Math.floor(this.length / 2) > position){
            let index = 0
            let current = this.head 
            while(index < position){
                current = current.next
                index += 1
            }
            current.val = element
        }else{
            let current = this.tail
            let index = this.length - 1
            while(index > position){
                current = current.prev
                index -= 1
            }
           current.val = element
        }
        return true
    }
    // removeAt(position):从链表的特定位置移除一项;
    removeAt(position){
        if(position < 0 || position >= this.length){
            return false
        }
        if(this.length === 1){
            this.head = null
            this.tail = null
        }else{
            if(position === 0){
                let current = this.head
                current = current.next
                current.prev = null
                this.head = current
            }else if(position === this.length - 1){
                let current = this.tail
                current = current.prev
                current.next = null
                this.tail = current
            }else{
                let index = 0
                let current = this.head
                while(index < position){
                    current = current.next
                    index += 1
                }
                current.next.prev = current.prev
                current.prev.next = current.next
            }
        }
        this.length -= 1
        return true
    }
    // isEmpty():如果链表中不包含任何元素,返回trun,如果链表长度大于0则返回false;
    isEmpty(){
        return this.length === 0
    }
    // size():返回链表包含的元素个数,与数组的length属性类似;
    size(){
        return this.length
    }

    // forwardString():返回正向遍历(tail开始)节点字符串形式;
    forwardString(){
        let current = this.tail
        let str = ''
        while(current){
            str = str + current.val + ''
            current = current.prev
        }

        return str
    }
    // toString():返回反向遍历(head开始)的节点的字符串形式;
    toString(){
        let current = this.head
        let str = ''
        while(current){
            str = str + current.val + ''
            current = current.next
        }

        return str
    }
}
  
const linkList = new LinkList()
linkList.append('33333')
linkList.append('22222')
linkList.inset(1,'11111')
// linkList.inset(3,'77777')
// linkList.inset(3,'张三')
// linkList.update(4,'李四')
linkList.removeAt(1)

console.log(linkList.toString(),linkList.forwardString(),linkList.get(2),linkList.indeOf('77777'),linkList,linkList.size())

以上是本人对双向链表的理解,如果错误请多多指正