数据结构与算法-链表(LinkedList)

308 阅读4分钟

前面介绍了列表对数据排序,当时底层存储数据的数据结构是数组。这里将讨论另外一种列表:链表,它的底层存储数据的数据结构是对象。

产生链表的背景

数组不总是组织数据的最佳数据结构。因为

  • 在很多编程语言中,数组的长度是固定的,所以当数组已经填满时,再加入新的元素就会很困难,在数组中,添加和删除元素也能麻烦,需要将数组中的其它元素向前或向后平移。但是javascript的数组不存在这样的问题。
  • javascript中的数组被实现成了对象,与其它语言(c++、Java)相比,效率很低。

如果数组在实际使用时很慢,就可以考虑使用链表来替代它。除了对数据的随机访问,链表可以用在任何可以使用一维数组的情况下。当然,如果需要随机访问,数组仍然是最优的选择。

定义链表

链表是由一组节点组成的集合。每个节点都可以使用一个对象的引用指向它的后继。指向另一个节点的引用叫做:链。

数组元素靠他们的位置进行引用,链表元素则是靠相互之间的关系进行引用。遍历链表,就是跟着链接,从链表的首元素一直走到尾元素(不包括头节点)

在链表中插入一个节点的效率很高,向链表中插入一个节点,需要修改它前面的节点,使其指向新加入的节点,而新加入的节点则指向原来前驱指向的节点。

在链表中删除一个节点也很快,将待删除的元素的前驱节点指向待删除元素的后继节点,同时将待删除元素指向null,元素就删除成功了。

代码实现一个基于对象的链表

// Node类用来表示节点
function Node(element) {
    this.element = element // element用来保存节点上的数据
    this.next = null // next用来保存指向下一个节点的链接
}

// LinkedList类提供对链表进行操作的方法
function LinkedList() {
    this.head = new Node('head')
}
LinkedList.prototype = {
    constructor: LinkedList,
    // 查找节点(遍历链表,查找给定数据)
    find: function (item) {
        var currNode = this.head
        while (currNode.element !== item) {
            currNode = currNode.next
        }
        return currNode
    },
    // 插入节点 
    insert: function (newElement, item) {
        var newNode = new Node(newElement)
        var current = this.find(item) // 找到插入节点的位置
        newNode.next = current.next // 将新节点的next属性设置未‘后’节点的next属性对应的值
        current.next = newNode // 设置‘后’节点的next属性指向新节点
    },
    // 查找要删除节点的前一个节点
    findPrev: function (item) {
        var currNode = this.head
        while (!(currNode.next === null) && (currNode.next.element !== item)) {
            currNode = currNode.next
        }
        return currNode
    },
    // 删除节点
    remove: function (item) {
        var prevNode = this.findPrev(item)
        if (!(prevNode.next === null)) {
            prevNode.next = prevNode.next.next // 让前一个节点指向了待删除节点的后一个节点
        }
    },
    // 显示链表中的元素
    dispaly: function () {
        var currNode = this.head
        while (!(currNode.next === null)) {
            console.log(currNode.next.element)
            currNode = currNode.next
        }
    }
}

// 使用
var people = new LinkedList()
people.insert('wu', 'head')
people.insert('qin', 'wu')
people.insert('hao', 'qin')
people.insert('is', 'hao')
people.insert('a', 'is')
people.insert('good', 'a')
people.insert('boy', 'good')
people.dispaly()
console.log('-------------------------------------')
people.remove('good')
people.dispaly()

双向列表

如果算法中需要频繁地找某结点的前趋结点,单链表的解决方式是遍历整个链表,增加算法的时间复杂度,影响整体效率。

为了快速便捷地解决这类问题,在单向链表的基础上,给各个结点额外配备一个指针变量,用于指向每个结点的直接前趋元素。这样的链表被称为“双向链表”或者“双链表”。

双向链表中的结点有两个指针域,一个指向直接前趋,一个指向直接后继。(链表中第一个结点的前趋结点为NULL,最后一个结点的后继结点为NULL)

// Node类用来表示节点
function Node(element) {
    this.element = element // element用来保存节点上的数据
    this.next = null // next用来保存指向下一个节点的链接
    this.previous = null // previous用来保存指向上一个节点的链接
}

// LinkedListBoth类提供对双向链表进行操作的方法
function LinkedListBoth() {
    this.head = new Node('head')
}
LinkedListBoth.prototype = {
    constructor: LinkedListBoth,
    // 查找节点(遍历链表,查找给定数据)
    find: function (item) {
        var currNode = this.head
        while (currNode.element !== item) {
            currNode = currNode.next
        }
        return currNode
    },
    // 插入节点 
    insert: function (newElement, item) {
        var newNode = new Node(newElement)
        var current = this.find(item) // 找到插入节点的位置
        newNode.next = current.next // 将新节点的next属性设置未‘后’节点的next属性对应的值
        newNode.previous = current
        current.next = newNode // 设置‘后’节点的next属性指向新节点
    },
    // 查找要删除节点的前一个节点
    //findPrev: function (item) {
    //    var currNode = this.head
    //    while (!(currNode.next === null) && (currNode.next.element !== item)) {
    //        currNode = currNode.next
    //    }
    //    return currNode
    //},
    // 删除节点
    remove: function (item) {
        var currNode = this.find(item)
        if (!(currNode.next === null)) {
            currNode.previous.next = currNode.next
            currNode.next.previous = currNode.previous
            currNode.next = null
            currNode.previous = null
        }
    },
    // 显示链表中的元素
    dispaly: function () {
        var currNode = this.head
        while (!(currNode.next === null)) {
            console.log(currNode.next.element)
            currNode = currNode.next
        }
    },
    // 查找最后节点
    findLast: function () {
        var currNode = this.head
        while (!(currNode.next === null)) {
            currNode = currNode.next
        }
        return currNode
    },
    // 反序显示双向链表中的元素
    dispReverse: function () {
        var currNode = this.head
        currNode = this.findLast()
        while (!(currNode.previous === null)) {
            console.log(currNode.element)
            currNode = currNode.previous
        }
    }
}

// 使用
var people = new LinkedListBoth()
people.insert('wu', 'head')
people.insert('qin', 'wu')
people.insert('hao', 'qin')
people.insert('is', 'hao')
people.insert('a', 'is')
people.insert('good', 'a')
people.insert('boy', 'good')
people.dispaly()
console.log('-------------------------------------')
people.remove('good')
people.dispaly()
console.log('-------------------------------------')
people.dispReverse()

循环链表

如果你希望可以从后向前遍历列表,但是又不想付出额外代表来创建一个双向列表,那么就需要使用循环链表。从循环链表的尾节点向后移动,就等于从后向前遍历链表。