数据结构和算法(五)之双向链表结构

数据结构和算法(五)之双向链表结构

一. 认识双向链表

双向链表介绍

  • 单项链表:

    • 只能从头遍历到尾或者从尾遍历到头(一般从头到尾)
    • 也就是链表相连的过程是单向的。实现的原理是上一个链表中有一个指向下一个的引用。
    • 单向链表有比较一个明显的缺点:
      • 我们可以轻松的到达下一个节点,但是回到前一个节点是很难的。但是,在实际开发中,经常会遇到需要回到上一个节点的情况。
      • 举个例子:假设一个文本编辑用链表来存储文本。每一行用一个String对象存储在链表的一个节点中。当编辑器用户向下移动光标时,链表直接操作到下一个节点即可。但是当用于将光标向上移动呢?这个时候为了回到上一个节点,我们可能需要从first开始,依次走到想要的节点上。
  • 双向链表

    • 既可以从头遍历到尾,又可以从尾遍历到头
    • 也就是链表相连的过程是双向的。
    • 一个节点即有向前连接的引用,也有一个向后连接的引用
    • 双向链表可以有效的解决单向链表中提到的问题
    • 双向链表有什么缺点呢?
      • 每次在插入或删除某个节点时,需要处理四个节点的引用,而不是两个。也就是实现起来要困难一些
      • 而且相当于单向链表,必然占用内存空间更大一些
      • 但是这些缺点和我们使用起来的方便程度相比,是微不足道的。
  • 双向链表的图解:

双向链表的创建

  • 我们来创建一个双向链表的类

    // 创建双向链表的构造函数
    function DoublyLinkedList() {
        // 创建节点构造函数
        function Node(element) {
            this.element = element
            this.next = null
            this.prev = null     // 新添加的
        }
        
        // 定义属性
        this.length = 0
        this.head = null
        this.tail = null   // 新添加的
    }
    
  • 代码解析:

    • 基本思路和单向链表比较相似,都是创建节点结构函数以及定义一些属性和方法。
    • 只是Node中添加一个this.prev属性,该属性用于指向上一个节点。
    • 另外属性中添加一个this.tail属性,该属性指向末尾的节点

二.操作双向链表

双向链表的操作和单向链表的方法都是类似的。

只是在实现的过程中,需要考虑更多节点之间的关系,所以变得比单向链表复杂了一些。

尾部追加数据(append方法)

  • 我们还是先来实现尾部追加数据的方法

    // 在尾部追加数据
    DoublyLinkedList.prototype.append = function (element) {
        // 1.根据元素创建节点
        let newNode = new Node(element)
        
        // 2.判断列表是否为空列表
        if(this.head == null){
            this.head = newNode
            this.tail = newNode 
        }else{
            this.tail.next = newNode
            newNode.prev = this.tail
            this.tail = newNode
        }
        
        this.length++
    
    }
    
  • 代码解析:

    • 代码1部分不用多讲,还是通过元素创建新的节点。
    • 代码2部分相比之前有一些复杂,但是还是两种情况。
    • 情况一:链表原来为空
      • 链表中原来如果没有数据,那么直接让headtail指向这个新的节点即可。
    • 情况二:链表中已经存在数据
      • 因为我们是要将数据默认追加到尾部,所以这个变得也很简单。
      • 首先tail中的next之前指向的是null。现在应该指向新的节点newNode:this.tail.next = newNode
      • 因为是双向链表,新节点的next/tail目前都是null。但是作为最后一个节点,需要有一个指向前一个节点的引用。所以这里我们需要newNode.prev = this.tail
      • 因为目前newNode已经变成了最后的节点,所以this.tail属性的引用应该指向最后:this.tail = newNode即可

正向反向遍历(forwardStriing方法、reverseString方法、toString方法)

  • 链表的遍历

    • 之前我们在单向链表中实现了一个toString方法,他是一种正向的遍历。
    • 现在,为了用户使用方便,我们实现三个方法
      • forwardString:正向遍历转成字符串的方法
      • reverseString:反向遍历转成字符串的方法
      • toString:正向遍历转成字符串的方法
  • 方法的相关实现:

    // 正向遍历的方法
    DoublyLinkedList.prototype.forwardString = function () {
    	let current = this.head
        let forwardStr = ""
        
        while(current) {
            forwardStr += "," + current.element
            current = current.next
        }
        
        return forwardStr.slice(1)
    }
    
    // 反向遍历的方法
    DoublyLinkedList.prototype.reverseString = function () {
        let current = this.tail
        let reverseStr = ""
        
        while (current) {
            reverseStr += "," + current.element
            current = current.prev
        }
        
        return reverseStr.slice(1)
    }
    
    // 实现toString方法
    DoublyLinkedList.prototype.toString = function () {
        return this.forwardString()
    }
    
  • 完成上面的代码后,测试append方法

    // 1.创建双向链表对象
    let list = new DoublyLinkedList()
    
    // 2.添加元素
    list.append("abc")
    list.append("cba")
    list.append("nba")
    list.append("mba")
    
    // 3.获取所有的遍历结果
    alert(list.forwardString())  // abc,cba,nba,mba
    alert(list.reverseString())  // mba,nba,cba,abc
    alert(list)                  // abc,cba,nba,mba
    

任意位置插入(insert方法)

  • 向双向链表的任意位置插入数据会有一些复杂,考虑的情况也会多一些

    // 在任意位置插入数据
    DoublyLinkedList.prototype.insert = function (position,element) {
    	// 1.判断越界的问题
        if(position < 0 || position > this.length) return false
        
        // 2.创建新的节点
        let newNode = new Node(element)
        
        // 3.判断插入的位置
        if(position === 0) {      // 在第一位置插入数据
            // 判断链表是否为空
            if(this.head == null){
                this.head = newNode
                this.tail = newNode
            }else{
                this.head.prev = newNode
                newNode.next = this.head
                this.head = newNode
            }
        }else if(position === this.length) {    // 插入到最后的情况
            // 思考:这种情况是否需要判断链表为空的情况呢?答案是不需要,为什么?
            this.tail.next = newNode
            newNode.prev = this.tail
            this.tail = newNode
        }else{    // 在中间位置插入数据
            // 定义属性
            let index = 0
            let current = this.head
            let previous = null
            
            // 查找正确的位置
            while(index++ < position) {
                previous = current
                current = current.next
            }
            
            // 交换节点的指向顺序
            newNode.next = current
            newNode.prev = previous
            current.prev = newNode
            previous.next = newNode
        }
        
        this.length++
        
        return true
    }
    
  • 代码深度解析,代码比较复杂,我们分成三种情况:

    • 情况一:将元素插入到头部(position === 0

      • 事实上,将元素插入到头部是比较简单。只是他有分成了两种情况。
      • 情况一:列表为空。那么直接让head/tail指向newNode即可
      • 情况二:列表不为空,这个时候需要修改原来headprev指向新节点。新节点的next指向原来的head。并且head现在要指向newNode

    • 情况二:将元素插入到尾部(position === length

      • 这种情况比较简答了,因为我们在append方法中已经处理过了。

      • 注意:这里不需要判断元素为空的情况,因为在position === 0的时候,我们已经处理过了。所以到这里的时候,肯定不为空。

    • 情况三:将元素插入到中间位置

      • 情况三是比较复杂一些的,但是我们理清楚他的逻辑关系也就比较简单了。
      • 首先,我们需要找到正确的插入位置。通过while循环,这个并不难,因为我们在单向链表的时候已经找过了。
      • 查找正确的位置后,需要进行插入操作。
      • 首先,你的newNodenext/prev必然要指向前后的节点,也就是currentprevious
      • 其次,而currentprev需要指向newNode,而previousnext需要指向newNode
      • 逻辑搞定,并没有想象的复杂,详细看图解。

  • 测试一下该方法

    // 4.insert方法测试
    list.insert(0,"100")
    list.insert(2,"200")
    list.insert(6,"300")
    alert(list)        // 100,abc,200,cba,mba,300
    
  • 课下思考:代码性能能否改进一点呢?

    • 如果我们position大于length/2,是否从尾部开始迭代性能更高一些呢?

位置移除数据(removeAt方法)

  • 我们继续来做下一个功能:通过下标值删除某个元素

    // 根据位置删除对应的元素
    DoublyLinkedList.prototype.removeAt = function (position) {
    	// 1.判断越界的问题
        if(position < 0 || position >= this.length) return null
        
        // 2.判断移除的位置
        let current = this.head
        if(position === 0){
            if(this.length === 1){
                this.head = null
                this.tail = null
            }else{
                this.head = this.head.next
                this.head.prev = null
            }
        }else if(position === this.length - 1){
            current = this.tail
            this.tail = this.tail.prev
            this.tail.next = null
        }else{
            let index = 0
            let previous = null
            
            while(index++ < position) {
                previous = current
                current = current.next
            }
            
            previous.next = current.next
            current.next.prev = previous
        }
        
        this.length--
        
        return current.element
    }
    
  • 代码深度解析,和插入一样,可以分成三种情况:

    • 情况一:删除头部的元素

      • 删除头部的元素也分成两种情况。
      • 情况一:链表只有一个元素,那么将head/tail直接设置为null即可
      • 情况二:链表有多个元素,这个时候删除头部的元素,head = head.next head.prev = null

    • 情况二:删除尾部的元素

      • 删除尾部的元素和删除头部有多个元素的情况比较相似。(也不需要考虑个数为1的情况,因为上一个情况已经考虑了)
      • tail设置为tailprev.tailnext设置为null即可。

    • 情况三:删除中间位置的元素

      • 这种情况就需要先找到正确的位置,还是使用while循环。
      • previousnext直接设置成currentnext,将current.nextprev设置成previous即可

  • 测试removeAt方法

    // 5.removeAt方法测试
    alert(list.removeAt(0))  //100
    alert(list.removeAt(1))  //200
    alert(list.removeAt(4))  //300
    alert(list)  // abc,cba,nba,mba
    

获取元素位置(indexOf方法)

  • 根据元素获取在链表中的位置

    // 根据元素获取在链表中的位置
    DoublyLinkedList.prototype.indexOf = function (element) {
    	// 1.定义变量保存信息
        let current = this.head
        let index = 0
        
        // 2.查找正确的信息
        while(current){
            if(current.element === element) {
                return index
            }
            index++
            current = current.next
        }
        
        // 3.来到这个位置,说明没有找到,则返回 -1
        return -1
    }
    
  • 代码测试:

    // indexOf方法测试
    alert(list.indexOf("abc")) // 0
    alert(list.indexOf("cba")) // 1
    alert(list.indexOf("nba")) //2
    

根据元素删除(remove方法)

  • 有了上面的indexOf方法,我们可以非常方便实现根据元素来删除信息

    // 根据元素删除
    DoublyLinkedList.prototype.remove = function (element) {
    	let index = this.indexOf(element)
        return this.removeAt(index)
    }
    
  • 测试代码:

    // remove方法测试
    alert(list.remove("abc"))  // abc
    alert(list)  // abc,nba,mba
    

其他方法实现(isEmpty方法、size方法、getHead方法、getTail方法)

  • 其他四个方法,放在一起

    // 判断是否为空
    DoublyLinkedList.prototype.isEmpty = function() {
    	return this.length === 0
    }
    
    // 获取链表长度
    DoublyLinkedList.prototype.size = function() {
        return this.length
    }
    
    // 获取第一个元素
    DoublyLinkedList.prototype.getHead = function() {
        return this.head.element
    }
    
    // 获取最后一个元素
    DoublyLinkedList.prototype.getTail = function() {
        return this.tail.element
    }
    
  • 代码测试

    alert(list.getHead())
    alert(list.getTail())
    alert(list.isEmpty())
    alert(list.size())
    

三.完整代码

// 创建双向链表的构造函数
function DoublyLinkedList() {
    // 创建节点构造函数
    function Node(element) {
        this.element = element
        this.next = null
        this.prev = null // 新添加的
    }

    // 定义属性
    this.length = 0
    this.head = null
    this.tail = null // 新添加的

    // 定义相关操作方法
    // 在尾部追加数据
    DoublyLinkedList.prototype.append = function (element) {
        // 1.根据元素创建节点
        let newNode = new Node(element)

        // 2.判断列表是否为空列表
        if (this.head == null) {
            this.head = newNode
            this.tail = newNode
        } else {
            this.tail.next = newNode
            newNode.prev = this.tail
            this.tail = newNode
        }

        // 3.length+1
        this.length++
    }

    // 在任意位置插入数据
    DoublyLinkedList.prototype.insert = function (position, element) {
        // 1.判断越界的问题
        if (position < 0 || position > this.length) return false

        // 2.创建新的节点
        let newNode = new Node(element)

        // 3.判断插入的位置
        if (position === 0) { // 在第一个位置插入数据
            // 判断链表是否为空
            if (this.head == null) {
                this.head = newNode
                this.tail = newNode
            } else {
                this.head.prev = newNode
                newNode.next = this.head
                this.head = newNode
            }
        } else if (position === this.length) { // 插入到最后的情况
            // 思考: 这种情况是否需要判断链表为空的情况呢? 答案是不需要, 为什么?
            this.tail.next = newNode
            newNode.prev = this.tail
            this.tail = newNode
        } else { // 在中间位置插入数据
            // 定义属性
            let index = 0
            let current = this.head
            let previous = null

            // 查找正确的位置
            while (index++ < position) {
                previous = current
                current = current.next
            }

            // 交换节点的指向顺序
            newNode.next = current
            newNode.prev = previous
            current.prev = newNode
            previous.next = newNode
        }

        // 4.length+1
        this.length++

        return true
    }

    // 根据位置删除对应的元素
    DoublyLinkedList.prototype.removeAt = function (position) {
        // 1.判断越界的问题
        if (position < 0 || position >= this.length) return null

        // 2.判断移除的位置
        let current = this.head
        if (position === 0) {
            if (this.length == 1) {
                this.head = null
                this.tail = null
            } else {
                this.head = this.head.next
                this.head.prev = null
            }
        } else if (position === this.length -1) {
            current = this.tail
            this.tail = this.tail.prev
            this.tail.next = null
        } else {
            let index = 0
            let previous = null

            while (index++ < position) {
                previous = current
                current = current.next
            }

            previous.next = current.next
            current.next.prev = previous
        }

        // 3.length-1
        this.length--

        return current.element
    }

    // 根据元素获取在链表中的位置
    DoublyLinkedList.prototype.indexOf = function (element) {
        // 1.定义变量保存信息
        let current = this.head
        let index = 0

        // 2.查找正确的信息
        while (current) {
            if (current.element === element) {
                return index
            }
            index++
            current = current.next
        }

        // 3.来到这个位置, 说明没有找到, 则返回-1
        return -1
    }

    // 根据元素删除
    DoublyLinkedList.prototype.remove = function (element) {
        let index = this.indexOf(element)
        return this.removeAt(index)
    }

    // 判断是否为空
    DoublyLinkedList.prototype.isEmpty = function () {
        return this.length === 0
    }

    // 获取链表长度
    DoublyLinkedList.prototype.size = function () {
        return this.length
    }

    // 获取第一个元素
    DoublyLinkedList.prototype.getHead = function () {
        return this.head.element
    }

    // 获取最后一个元素
    DoublyLinkedList.prototype.getTail = function () {
        return this.tail.element
    }

    // 遍历方法的实现
    // 正向遍历的方法
    DoublyLinkedList.prototype.forwardString = function () {
        let current = this.head
        let forwardStr = ""

        while (current) {
            forwardStr += "," + current.element
            current = current.next
        }

        return forwardStr.slice(1)
    }

    // 反向遍历的方法
    DoublyLinkedList.prototype.reverseString = function () {
        let current = this.tail
        let reverseStr = ""

        while (current) {
            reverseStr += "," + current.element
            current = current.prev
        }

        return reverseStr.slice(1)
    }

    // 实现toString方法
    DoublyLinkedList.prototype.toString = function () {
        return this.forwardString()
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yige001

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值