【数据结构与算法】详解什么是双向链表,并用代码手动实现一个双向链表

本系列文章【数据结构与算法】所有完整代码已上传 github,想要完整代码的小伙伴可以直接去那获取,可以的话欢迎点个Star哦~下面放上跳转链接

上一篇文章讲解了链表的相关知识,并用代码实现了一个链表结构。那么本文将介绍一下另一种特殊的链表结构,叫做 双向链表。 顾名思义,普通的链表都是从 head 开始往后遍历结构内的元素,那么双向链表就是既可以从头开始遍历,又可以从结构的末尾开始遍历。

上一篇文章的跳转链接——【数据结构与算法】详解什么是链表,并用代码手动实现一个链表结构

本文就来详细讲解一下双向链表的概念以及如何实现一个双向链表。

  • 公众号:前端印象
  • 不定时有送书活动,记得关注~
  • 关注后回复对应文字领取:【面试题】、【前端必看电子书】、【数据结构与算法完整代码】、【前端技术交流群】

在这里插入图片描述

一、什么是双向链表

在上一篇文章中,我们用一个生活中的例子来解释了链表的概念,那么本文就延用这个例子,并对该例子做一些改动,来解释什么是 双向链表

我们来看一下这个例子:

在一个教室里,所有的课桌排成一列,如图
在这里插入图片描述
相信在你们的读书生涯中,老师肯定有要求你们记住自己的前后桌是谁。所以该例子中,老师就要求学生们记住自己的前后桌,其中坐在第一排的学生需要记住自己是第一排的学生以及自己的后桌是谁;最后一排的学生要记住自己的前桌是谁以及自己是最后一排的学生。如图:
在这里插入图片描述
这一列座位就相当于一个 双向链表。

假如有一天,老师还没记住每个学生的名字,于是就问:这一列第三个人叫什么名字?这时就要从第一个学生开始数,例如从图中坐在第一个的 小5 开始数:第一个人是 小5,他的后桌是 小7;因此第二个人就是 小7,他的后桌是 小1;因此第三个人就是 小1 了。此时老师问 小1:你的前桌叫什么名字?你的后桌叫什么名字?

因为刚开始老师就让每个学生记住了自己的前桌以及后桌,所以此时 小1 能很快地告诉老师他的前桌是 小7,他的后桌是 小6

但是,我们设想一下,如果是上一篇文章的 链表结构 的例子中,如果老师在得知了第三个人是 小1 以后,询问 小1 的前桌叫什么名字,小1 能回答上来吗?并不能,因为老师只让每个学生记住了自己的后桌,所以此时想要得知 小1 的前桌叫什么名字,只能这样:第三个学生叫 小1,那么他的前桌就坐在第二个位置,所以从第一个学生开始数,第一个学生是 小5,他的后桌是 小7;因此第二个学生就是 小7。当然本文举得例子中学生数量有点少,但一旦数量多起来了,每次问一个学生他的前桌叫什么名字的时候,都要从头开始数。

从中可以看出,让每个学生记住自己的前桌后桌是非常有必要的,因为在某些情况下,可以快速地解决问题。

上面讲了那么多,接下来我们就来看一下 双向链表 是什么样的,如图
在这里插入图片描述
可以看到,对比 链表结构双向链表 多了一个指针 tail,它是指向最后一个元素的,就相当于例子中的学生记住了自己是最后一排。

二、双向链表的方法

因为 双向链表 是链表的一种特殊形式,所以这两者的常用方法也是一样的,只不过实现方式不一样,那么我再列举一下有哪些常用方法吧,如下表

方法含义
append()向双向链表尾部追加元素
insert()在双向链表的某个位置插入元素
get()获取双向链表对应位置的元素
indexOf()获取某元素在双向链表中的索引
update()修改双向链表中某个位置上的元素的值
removeAt()移除双向链表中某位置上的某元素
remove()移除双向链表中的某元素
isEmpty()判断双向链表内是否为空
size()返回双向链表内元素个数
toString()以字符串的形式展示双向链表内的所有元素

接下来就用 JavaScript 来实现一下以上这些方法

三、用代码实现双向链表

(1)创建一个构造函数

首先创建一个大的构造函数,用于存放双向链表的一些属性和方法。

function DoubleLinkedList() {
    //属性
    this.head = null
    this.tail = null
    this.length = 0
}

其中,属性 head 表示双向链表中的第一个元素;属性 tail 表示双向链表中的最后一个元素

(2)创建内部构造函数

双向链表的每一个元素都有三个属性,即previtemnext,分别表示该元素的前一个元素是谁 、存储着该元素的值和该元素的后一个元素是谁。所以我们就在双向链表的构造函数内部创建一个内部构造函数用于之后创建元素的实例对象

function DoubleLinkedList() {
    //属性
    this.head = null
    this.tail = null
    this.length = 0

	//元素的构造函数
    function Node(item) {
        this.item = item
        this.next = null
        this.prev = null
    }
}

(3)实现append()方法

append()方法就是将元素添加到双向链表的末尾。

实现思路:

  1. 先创建新元素的实例对象 node
  2. 先判断双向链表内有无元素,若没有元素,则将属性 head 和 属性 tail 都指向 node,最后属性 length + 1
  3. 若双向链表中有元素了,则因为双向链表内多了一个指针 tail,所以我们要实现 append()方法也就方便了很多,只需要将 tail 指向 node,然后将原本的末尾元素 old_nodenext 指向 node,并将 nodeprev属性指向 old_node即可,然后属性 length + 1

为了方便大家理解,我用一个动图来向大家演示一下:
在这里插入图片描述
在这里插入图片描述

思路讲解完了,我们来实现一下该方法

function DoubleLinkedList() {
    //属性
    this.head = null
    this.tail = null
    this.length = 0

	//元素的构造函数
    function Node(item) {
        this.item = item
        this.next = null
        this.prev = null
    }

	//在末尾加入元素
    DoubleLinkedList.prototype.append = function (item) {
    	// 1.创建新元素的实例对象
        let node = new Node(item)
		
		// 2.判断双向链表内是否有元素
        if(this.length === 0) {
            this.head = node
            this.tail = node
        }
        else {
            node.prev = this.tail
            this.tail.next = node
            this.tail = node
        }
		
		// 3. 双向链表内元素 + 1
        this.length ++
    }
}

接下里我们来使用一下该方法

let dl = new DoubleLinkedList()

dl.append('js')
dl.append('python')

此时的链表内是这样的
在这里插入图片描述

(4)实现insert()方法

insert()方法就是在指定的索引位置插入元素。一共需要传入两个参数,第一个是 position,表示需要插入元素的位置;第二个参数是 item,表示元素的值

实现思路:

  1. 创建新的元素实例对象 node
  2. 判断指定的索引位置 position 是否越界,即是否小于0,或者大于双向链表的长度。若越界了,则直接返回false
  3. 判断 position 是否为0。若为0,则直接将双向链表原本的第一个元素 ,也就是 head所对应的元素 old_node 赋值给 nodenext属性,再将 node 赋值给 old_nodeprev 属性,然后将 node赋值给 head,表示现在链表的第一个元素为 node
  4. position 不为0,则遍历双向链表,同时记录遍历的索引 index 、遍历的上一个元素 prev 和在 index索引位置上的元素 current,当 index == position时,将 node赋值给 prevnext属性,将 current 赋值给 nodenext属性,再将 prev 赋值给 nodeprev 属性,最后将 node 赋值给 currentprev 属性
  5. 属性 length + 1

为了方便大家理解,我用一个动图来向大家演示一下:
在这里插入图片描述
在这里插入图片描述
看完了方法的实现思路,我们来用代码实现一下该方法

function DoubleLinkedList() {
    //属性
    this.head = null
    this.tail = null
    this.length = 0

	//元素的构造函数
    function Node(item) {
        this.item = item
        this.next = null
        this.prev = null
    }

	//向指定位置插入元素
    DoubleLinkedList.prototype.insert = function (position, item) {
        // 1.判断是否越界
        if(position < 0 || position > this.length) {
            return false
        }
        // 2.创建新元素的实例对象
        let node = new Node(item)
        let index = 0
        let current = this.head
        let prev = null
        
        // 3.判断插入位置是否等于元素个数
        if(position === this.length) {
        	//当 position 与 length相等时,就相当于在末尾添加元素
            this.append(item)
        }

		// 4. 判断元素是否添加到第一个位置
        else if(position === 0) {
            node.next = this.head
            this.head.prev = node
            this.head = node
            this.length ++
        }
        else {
        	// 5.遍历链表,直到找到position位置上的元素
            while (index < position) {
                prev = current
                current = current.next
                index ++
            }
            // 6.插入新元素
            prev.next = node
            current.prev = node
            node.prev = prev
            node.next = current
            this.length ++
        }
    }
}

接下来我们来使用一下该方法

let dl = new DoubleLinkedList()

dl.insert(0, 'C++')

此时的双向链表是这样的
在这里插入图片描述
在执行一次 insert()方法

di.append('js')              //在末尾添加元素 js
dl.insert(1, 'python')       //在索引 1处插入元素python

此时的双向链表是这样的
在这里插入图片描述
最后我们再向索引为 3 的位置插入元素 java,因为此时 length = 3,即双向链表元素个数为 3,这就相当于在末尾添加元素

dl.insert(3, 'java')

所以此时的链表是这样的
在这里插入图片描述

(5)实现get()方法

get()方法就是获取对应位置上的元素。需要传入一个参数,即 position,表示需要获取元素的索引

实现思路:

  1. 判断 position 是否越界。若越界,则直接返回false
  2. 遍历链表,同时记录当前索引 index,当 index == position时,返回当前位置上的元素

该方法思路比较简单,几乎跟链表的 get()方法一样,我们来看一下

function DoubleLinkedList() {
    //属性
    this.head = null
    this.tail = null
    this.length = 0

	//元素的构造函数
    function Node(item) {
        this.item = item
        this.next = null
        this.prev = null
    }

	//获取对应位置上的元素
    DoubleLinkedList.prototype.get = function (position) {
        if(position < 0 || position > this.length - 1) {
            return false
        }
        let index = 0
        let current = this.head
        while (index < position) {
            current = current.next
            index ++
        }
        return current.item
    }
}

我们来使用一下该方法

let dl = new DoubleLinkedList()

dl.append('C++')
dl.append('js')
dl.append('python')

dl.get(-2)           //返回 false
dl.get(0)            //返回 C++
dl.get(2)            //返回 python
dl.get(3)            //返回 false

(6)实现indexOf()方法

indexOf()方法就跟数组的一样,获取某元素在双向链表中的索引值,若双向链表中不存在该元素,则返回 -1

这个方法思路很简单,就不详细讲解了,直接来看代码

function DoubleLinkedList() {
    //属性
    this.head = null
    this.tail = null
    this.length = 0

	//元素的构造函数
    function Node(item) {
        this.item = item
        this.next = null
        this.prev = null
    }

	//获取元素的索引
    DoubleLinkedList.prototype.indexOf = function (item) {
        let current = this.head
        let index = 0
        
        // 1.遍历双向链表
        while (index < this.length) {
        	// 2. 找到对应元素,返回索引值
            if(current.item === item) {
                return index
            }
            else {
                index ++
                current = current.next
            }
        }
        // 3.未找到对应元素,返回 -1
        return -1

    }
}

我们来使用一下该方法

let dl = new DoubleLinkedList()

dl.append('C++')
dl.append('js')
dl.append('python')

dl.indexOf('C++')           //返回 0
dl.indexOf('js')            //返回 1
dl.indexOf('python')        //返回 2
dl.indexOf('java')          //返回 -1

(7)实现update()方法

update()方法就是用于修改双向链表中某位置上的元素的值。因此该方法需要传入两个参数,第一个参数是 position,表示需要修改的元素的索引;第二个参数是 NewItem,表示修改后的值

该方法的实现思路跟普通链表一模一样,所以就不讲解具体的实现思路了,直接来看代码吧

function DoubleLinkedList() {
    //属性
    this.head = null
    this.tail = null
    this.length = 0

	//元素的构造函数
    function Node(item) {
        this.item = item
        this.next = null
        this.prev = null
    }

	//修改某位置上的元素
    DoubleLinkedList.prototype.update = function (position, item) {
    	
    	// 1.判断是否越界
        if(position < 0 || position >= this.length) {
            return false
        }
        let index = 0
        let current = this.head
        
        // 2.遍历双向链表,直到找到对应位置上的元素
        while (index < position) {
            index ++
            current = current.next
        }
        // 3.修改position索引位置上的元素的值
        current.item = item
        return true
    }
}

我们来使用一下该方法

let dl = new DoubleLinkedList()

dl.append('C++')
dl.append('js')
dl.append('python')

此时的链表是这样的
在这里插入图片描述
现在调用一下 update()方法,修改索引位置为 2 的元素的值

dl.update(2, 'java')

此时的链表是这样的
在这里插入图片描述

(8)实现removeAt()方法

removeAt()方法就是用于移除双向链表中某位置上的某元素。该方法只需要传入一个参数 position,表示需要移除元素的索引

实现思路:

  1. 先判断 position 是否越界,若越界了,则直接返回 false 表示移除元素失败
  2. 若没有越界,则判断 position 是否为 0,若等于 0,则直接将第一个链表的 next 值赋值给 head,然后 length - 1
  3. position 不等于 0而等于 length - 1,则将末尾元素的前一个元素,即 tailprev对应的元素的 next 属性设置成 null,并将 tail 指向当前 tailprev,最后 length - 1
  4. position 既不等于 0,又不等于 length - 1,则遍历双向链表,同时记录当前索引 index,遍历的当前元素 currentcurrent的上一个元素 prev
  5. index === position时,将 current 的下一个元素,即 currentnext 属性值赋值给 prevnext 属性,同时将 current 的下一个元素的 prevprev 属性设置成 prev,最后 length - 1

为了让大家更好地理解该方法的实现思路,我制作了一个动图来帮助大家理解,如图
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
思路讲完了,我们直接来看代码

function DoubleLinkedList() {
    //属性
    this.head = null
    this.tail = null
    this.length = 0

	//元素的构造函数
    function Node(item) {
        this.item = item
        this.next = null
        this.prev = null
    }

	//移除某位置上的元素
    DoubleLinkedList.prototype.removeAt = function (position) {
        // 1.判断是否越界
        if(position < 0 || position >= this.length) {
            return false
        }
        
        // 2.判断清除的元素是否为链表的唯一元素
        if(position === 0 && position === this.length - 1) {
            this.head = null
            this.tail = null
        }
        // 3.判断清除的元素是否为链表的第一个元素
        else if(position === 0) {
            this.head.next.prev = null
            this.head = this.head.next
        }
        // 4.判断清除的元素是否为链表的最后一个元素
        else if(position === this.length - 1) {
            this.tail.prev.next = null
            this.tail = this.tail.prev
        }
        else {
            let current = this.head
            let prev = null
            let index = 0
            // 5.遍历双向链表
            while (index < position) {
                index ++
                prev = current
                current = current.next
            }
            // 6.删除对应位置上的元素
            prev.next = current.next
            current.next.prev = prev
        }
        // 7.元素个数 - 1
        this.length --
        return true

    }
}

我们来使用一下该方法

let dl = new DoubleLinkedList()

dl.append('C++')
dl.append('js')
dl.append('python')

dl.removeAt(2)         //删除索引为 2位置上的元素

此时的双向链表是这样的
在这里插入图片描述

(9)实现remove()方法

remove()方法就是用于移除双向链表链表中的某元素,并返回被删除元素所在的索引位置,若链表中没有对应元素,则返回 false 。该方法需要传入一个参数 data用于查找链表中对应的元素

实现思路:

  1. 利用上面封装的 indexOf()方法,将 data 作为参数传入,获取到 data 在链表中的索引 index
  2. 再利用上面封装的 removeAt()方法,将 index 作为参数传入,就可以实现 remove()方法的功能了。

我们简单来写一下代码实现该方法

function DoubleLinkedList() {
    //属性
    this.head = null
    this.tail = null
    this.length = 0

	//元素的构造函数
    function Node(item) {
        this.item = item
        this.next = null
        this.prev = null
    }

	//移除某元素
    DoubleLinkedList.prototype.remove = function (data) {
    	// 1.获取data在双向链表中的索引
        let index = this.indexOf(data)
		
		// 2.利用removeAt()方法删除双向链表中的data
        this.removeAt(index)
        
        // 3.返回被删除元素data在双向链表中的索引
        return index
    }
}

我们来使用一下该方法

let dl = new DoubleLinkedList()

dl.append('C++')
dl.append('js')
dl.append('python')

dl.remove('js')            //返回 1,此时length = 2
dl.remove('python')        //返回 1,此时length = 1

此时的链表是这样的
在这里插入图片描述

(10)实现isEmpty()方法

isEmpty()方法就是判断双向链表中是否有元素。若有元素,则返回 false;反之,返回 true

该方法的实现思路很简单,直接判断属性 length 是否等于 0 就可以了。

话不多说,我们赶紧写代码实现一下该方法

function DoubleLinkedList() {
    //属性
    this.head = null
    this.tail = null
    this.length = 0

	//元素的构造函数
    function Node(item) {
        this.item = item
        this.next = null
        this.prev = null
    }

	//判断双向链表是否为空
    DoubleLinkedList.prototype.isEmpty = function () {
        if(this.length === 0) {
            return true
        }
        else {
            return false
        }
    }
}

我们来使用一下该方法

let dl = new DoubleLinkedList()

dl.isEmpty()             //返回 true,此时双向链表中无元素

dl.append('C++')
dl.append('js')
dl.append('python')

dl.isEmpty()             //返回 false,此时双向链表中有3个元素

(11)实现size()方法

szie()方法就是返回链表内的元素个数

我们来实现一下该方法

function DoubleLinkedList() {
    //属性
    this.head = null
    this.tail = null
    this.length = 0

	//元素的构造函数
    function Node(item) {
        this.item = item
        this.next = null
        this.prev = null
    }

	//返回双向链表元素个数
    DoubleLinkedList.prototype.size = function () {
        return this.length
    }
}

我们来使用一下该方法

let dl = new DoubleLinkedList()

dl.size()             //返回 0,此时双向链表中无元素

dl.append('C++')
dl.append('js')
dl.append('python')

dl.size()             //返回 3,此时双向链表中有3个元素

(12)实现toString()方法

toString()方法就是以字符串的形式展示双向链表内的所有元素

实现思路很简单,就是遍历双向链表中的每一个元素,并将每个元素以字符串的形式连接起来即可

我们来实现一下该方法

function DoubleLinkedList() {
    //属性
    this.head = null
    this.tail = null
    this.length = 0

	//元素的构造函数
    function Node(item) {
        this.item = item
        this.next = null
        this.prev = null
    }

	//展示双向链表数据
    DoubleLinkedList.prototype.toString = function () {
        let current = this.head
        let string = ''
        while (current) {
            string += current.item + ' '
            current = current.next
        }
        return string
    }
}

四、双向链表的补充

其实平常的时候 双向链表 的应用场景比较多,而且它某些时候比普通的链表数据操作的效率要高。

例如:假设有一个 length = 9999 的普通链表,我们要删除该链表索引为 9997 的元素,那么我们只能从链表的第一个元素,即从 head 指向的元素开始遍历链表,直到找到索引为 9997 的元素,一共要遍历9998次,这样效率是非常低的。

那么如果把普通链表换成双向链表呢?当我们得知要删除索引为 9997 的元素时,我们可以先计算 99979999 - 1 = 9998 的差值 、99970 的差值,若前者差值小,则可以从双向链表的 tail 指向的元素往前遍历;反之,从 head 指向的元素往后遍历。如图
在这里插入图片描述

五、总结

双向链表的讲解就到这里了,希望大家对双向链表有了更深一层的理解。下一篇文章我将讲解一下哈希表

大家可以关注我,之后我还会一直更新别的数据结构与算法的文章来供大家学习,并且我会把这些文章放到【数据结构与算法】这个专栏里,供大家学习使用。

然后大家可以关注一下我的微信公众号:前端印象,等这个专栏的文章完结以后,我会把每种数据结构和算法的笔记放到公众号上,大家可以去那获取。

或者也可以去我的github上获取完整代码,欢迎大家点个Star

我是Lpyexplore,创作不易,喜欢的加个关注,点个收藏,给个赞~ 带你们在Python爬虫的过程中学习Web前端

已标记关键词 清除标记
1.算法是程序的灵魂,优秀的程序在对海量数据处理时,依然保持高速计算,就需要高效的数据结构算法支撑。2.网上数据结构算法的课程不少,但存在两个问题:1)授课方式单一,大多是照着代码念一遍,数据结构算法本身就比较难理解,对基础好的学员来说,还好一点,对基础不好的学生来说,基本上就是听天书了2)说是讲数据结构算法,但大多是挂羊头卖狗肉,算法讲的很少。 本课程针对上述问题,有针对性的进行了升级 3)授课方式采用图解+算法游戏的方式,让课程生动有趣好理解 4)系统全面的讲解了数据结构算法, 除常用数据结构算法外,还包括程序员常用10大算法:二分查找算法(非递归)、分治算法、动态规划算法、KMP算法、贪心算法、普里姆算法、克鲁斯卡尔算法、迪杰斯特拉算法、弗洛伊德算法、马踏棋盘算法。可以解决面试遇到的最短路径、最小生成树、最小连通图、动态规划等问题及衍生出的面试题,让你秒杀其他面试小伙伴3.如果你不想永远都是代码工人,就需要花时间来研究下数据结构算法。教程内容:本教程是使用Java来讲解数据结构算法,考虑到数据结构算法较难,授课采用图解加算法游戏的方式。内容包括: 稀疏数组、单向队列、环形队列、单向链表双向链表、环形链表、约瑟夫问题、栈、前缀、中缀、后缀表达式、中缀表达式转换为后缀表达式、递归与回溯、迷宫问题、八皇后问题、算法的时间复杂度、冒泡排序、选择排序、插入排序、快速排序、归并排序、希尔排序、基数排序(桶排序)、堆排序、排序速度分析、二分查找、插值查找、斐波那契查找、散列、哈希表、二叉树、二叉树与数组转换、二叉排序树(BST)、AVL树、线索二叉树、赫夫曼树、赫夫曼编码、多路查找树(B树B+树和B*树)、图、图的DFS算法和BFS、程序员常用10大算法、二分查找算法(非递归)、分治算法、动态规划算法、KMP算法、贪心算法、普里姆算法、克鲁斯卡尔算法、迪杰斯特拉算法、弗洛伊德算法马踏棋盘算法。学习目标:通过学习,学员能掌握主流数据结构算法实现机制,开阔编程思路,提高优化程序的能力。
©️2020 CSDN 皮肤主题: 博客之星2020 设计师:CY__ 返回首页