链表

发布于 2024-10-29 19:47:11 字数 7098 浏览 23 评论 0

链表的优势

链表是一种特殊的列表,不同于数组,数组不总是组织数据的最佳数据结构,原因如下:

  • 在有些变成语言中:
    • 数组的长度是需要预先设定好的,也就是说数组的长度是固定的,想要扩展会很困难。
    • 向数组中添加或删除元素也很麻烦,因为需要将数组的元素向前或向后循环位移。
  • 在 JavaScript 中
    • JavaScript 显然不存在上述两个数组缺点的说法,因为数组的长度是可变的,另外可以使用 splice 方法轻松的对数组进行添加删除的操作。
    • 但也正因如此,我们不要忘了,在 JavaScript 中,数组也是对象,js 中的数组是作为对象被实现的,所以与其他语言(如:C++、java) 相比,性能会差很多。

另外,数组中元素的存储地址是连续的,但是当我们的数据量很大时,可能会很难寻找到空间足够大又连续的内存空间,而链表的存储方式不要求地址连续。

但并不是说链表一定比数组好,数组的优势在于随机访问,比如我们访问数组的第 20 个元素,可以很快的访问到,然而链表就会比较困难,另外在链表中,我们也不会说第几元素,我们会用链表中节点之间的引用来描述元素间的关系。

链表的概念

节点(Node)

链表中的元素称为节点

节点间的引用称为链

节点的种类

  • 单向链表
  • 单向循环链表
  • 双向链表
  • 双向循环链表

JavaScript 实现

单向链表

通常情况下,我们会选择一个头结点(Head) 作为链表的接入点,如果用一张图来表示单向链表,那么应该是这样的:

我们需要两个类,一个节点类(Node),另外一个是链表类(Llist),先创建 Node 类,如下:

function Node (data) {
    this.data = data
    this.next = null
}

然后是链表类 Llist

在创建 Llist 之前,我们要明确单向链表的属性和方法:

  • 属性:
    • head (头节点的引用)
  • 方法:
    • insertAfter
function Llist () {
    // 头节点
    this.head = new Node('head')
}
Llist.prototype = {
    constructor: Llist,
    // 插入节点:将 newNode 节点 插入到 node 节点 之后
    insertAfter: function (newNode, data) {

    },
    // 删除节点
    remove: function (data) {

    },
    // 查找节点
    find: function (data) {

    },
    // 显示全部节点
    display: function () {

    }
}

首先,我们来看一下 insertAfter 方法, insertAfter 方法用来将新节点 newNode 插入到 data 属性为给定值的节点的后面。所以我们需要 find 方法,该方法用来寻找 data 属性值为给定值的节点, find 方法实现如下

find: function (data) {
    var current = this.head
    while (current && current.data != data) {
        current = current.next
    }
    return current
}

有了 find 方法,我们就可以轻松实现 insertAfter 方法了:

insertAfter: function (newNode, data) {
    var target = this.find(data)
    newNode.next = target.next
    target.next = newNode
}

remove 方法用来删除 data 属性值为给定值的节点,删除节点的思路是:找到要删除的节点的前一个节点,然后让该节点的 next 属性指向要删除的节点的 next 属性所指向的节点,为此,我们需要一个辅助方法 findPrev 用来寻找要删除节点的前一个节点:

findPrev: function (data) {
    var current = this.head
    while (current && current.next && current.next.data != data) {
        current = current.next
    }
    return current
}

有了 findPrev 我们就可以实现 remove 方法了:

remove: function (data) {
    var prev = this.findPrev(data)
    if (prev.next) {
        prev.next = prev.next.next
    }
}

最后,我们来写一个遍历所有节点的方法,用来显示节点数据:

display: function () {
    var current = this.head
    while (current.next) {
        console.log(current.next.data)
        current = current.next
    }
}

接下来,我们就可以写一个测试脚本了:

var list = new Llist()
var new1 = new Node('new1')
var new2 = new Node('new2')
var new3 = new Node('new3')
list.insertAfter(new1, 'head')
list.insertAfter(new2, 'new1')
list.insertAfter(new3, 'new1')
list.display()  // 输出:new1 new3 new2
list.remove('new3')
list.display()  // 输出:new1 new2

双向链表

双向链表与单向链表相比,每一个节点多出一个属性 prev ,该属性指向当前节点的前一个节点,头节点的 prev 属性指向 Null

双向链表相比于单向链表,在删除节点的时候,效率会更高,还记我们在单向链表中删除节点时需要使用 findPrev 方法去查找目标节点的前一个节点吗?在双向链表中,我们仅仅通过目标节点的 prev 属性即可访问其前一个节点,而不需要遍历查找,效率自然会高,但在插入节点的时候我们需要做更多的事情,除了保证 next 属性指向正确之外,还要保证 prev 属性指向正确,下面是完整的双向链表实现:

function Llist () {
    // 头节点
    this.head = new Node('head')
}
Llist.prototype = {
    constructor: Llist,
    // 插入节点:将 newNode 节点 插入到 node 节点 之后
    insertAfter: function (newNode, data) {
        var target = this.find(data)
        newNode.next = target.next
        if (target.next) {
            target.next.prev = newNode
        }
        target.next = newNode
        newNode.prev = target
    },
    // 删除节点
    remove: function (data) {
        var target = this.find(data)
        target.prev.next = target.next
        target.next.prev = target.prev
        target.next = target.prev = null
    },
    // 查找节点
    find: function (data) {
        var current = this.head
        while (current && current.data != data) {
            current = current.next
        }
        return current
    },
    // 显示全部节点
    display: function () {
        var current = this.head
        while (current.next) {
            console.log(current.next.data)
            current = current.next
        }
        console.log('======')
    }
}

单向循环链表

我们仅仅需要添加一行代码,就可以把单向链表修改为单向循环链表。

还记的单向链表中的头节点的 next 属性最初指向的是什么吗?是 Null ,当我们在头节点后面添加一个节点之后,头节点的 next 便指向了新添加的节点,而新添加的节点会指向 Null ,也就是说,随着节点的添加,头节点最初的指向,总会传递给最后一个节点。既然如此,如果我们在初始化链表的时候,使头节点的 next 属性指向自身会怎么样?答案是:随着节点的添加,最后一个节点总会指向头节点。这样我们就得到了一个单向循环链表:

只需要在 Llist 类中添加一句:

this.head.next = this.head

完整代码如下:

function Llist () {
    this.head = new Node('head')
    this.head.next = this.head
}

最终效果如下图:

双向循环链表

对于双向循环链表,我们同样可以通过对双向链表的修改得到,我们来看看双向链表的示意图:

对于双向循环链表,我们期望应该是这样的:

我们增加了两条线(图中的 线 1 和 线 2),对于第一条线,我们同样可以利用创造单向循环链表的方式来构建,即:

function Llist () {
    this.head = new Node('head')
    this.head.next = this.head      // 这句话是重点
}

对于线 2,我们可以在插入节点的方法 insertAfter 中做文章,如下:

insertAfter: function (newNode, data) {
    var target = this.find(data)
    newNode.next = target.next
    if (target.next) {
        target.next.prev = newNode
    }
    target.next = newNode
    newNode.prev = target

    // 这里是新添加的内容
    if (newNode.next === this.head) {
        this.head.prev = newNode
    }
}

我们添加了一个 if 语句,这段代码的意思是,当我们在添加完节点之后,对新节点进行判断,如果新节点的 next 属性指向头节点,那么说明新添加的节点是最后一个节点,那么我们只需要让头节点的 prev 属性指向该节点即可了,这样我们就得到了一个双向循环链表。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

陈年往事

暂无简介

文章
评论
28 人气
更多

推荐作者

身边

文章 0 评论 0

qq_oxT0yE

文章 0 评论 0

卷着的草席

文章 0 评论 0

£冰雨忧蓝°

文章 0 评论 0

我还不会笑

文章 0 评论 0

Unbroken

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文