本系列所有文章:
第一篇文章:学习数据结构与算法之栈与队列
第二篇文章:学习数据结构与算法之链表
第三篇文章:学习数据结构与算法之集合
第四篇文章:学习数据结构与算法之字典和散列表
第五篇文章:学习数据结构与算法之二叉搜索树

简单介绍链表

链表一种常见的数据结构,可以存储有序的元素集合。不同于数组,链表中元素在内存中不是连续放置,同时每一个链表元素中除了本身的节点还保存了指向下一个元素的引用,这些特点使得链表元素在动态增加和删除时不必移动其他元素,但是访问链表元素时必须从起点开始迭代列表直到找到目标元素。

下面是两种链表的JavaScript实现:

单向链表

下图就是一个典型的单向链表,这里注意:一般链表中的第一个元素会有一个head指针指向它,这也是我们操作链表必不可少的一环。

单向链表

(图片来源谷歌,侵删)

用JavaScript实现单向链表

与之前实现栈和队列一样,这里先声明一个构造函数:

function LinkedList () {
  var Node = function (element) {
    this.element = element
    // 保存指向下个元素的引用,默认为null
    this.next = null
  }
  
  // 链表长度
  var length = 0
  // head保存指向第一个元素的引用
  var head = null
}

链表需要实现以下方法:

  • append(element):向链表尾部添加元素
  • insert(position, element):向链表特定位置插入元素
  • removeAt(position):从链表特定位置移除一项
  • remove(element):在链表中移除某元素
  • indexOf(element):返回元素在链表中的索引,若不存在则返回-1
  • isEmpty():如果链表不包含任何元素就返回true,否则为false
  • size():返回链表长度
  • toString():返回元素的值转成字符串

实现append

类似数组的push方法,但是只能添加一个元素。实现方法的时候分两种情况考虑:1. 链表为空时添加第一个元素;2. 链表不为空时在尾部添加元素

this.append = function (element) {

  var node = new Node(element),
      current

  if (head === null) { // 当链表为空时
    // 将head指向新增的元素
    head = node
  } else { // 链表不为空
    // 使用一个current变量从head开始迭代链表
    current = head

    // 迭代链表,直到找到最后一项
    while (current.next) {
      current = current.next
    }

    // 找到最后一项,将其next赋为node,建立链接
    current.next = node
  }

  // 更新列表长度
  length++
}

实现removeAt

在链表中特定位置移除元素,实现时也需要考虑两种情况:1. 移除第一个元素;2. 移除其他元素(包括最后一个)

this.removeAt = function (position) {
  // 判断位置是否越界
  if (position > -1 && position < length) {
    var current = head,
        previous,
        index = 0

    // 如果删除了第一个元素,把head指向下一个元素就行了
    if (position === 0) {
      head = current.next
    } else {
      // 根据输入的位置查找要删除的元素
      while (index++ < position) {
        previous = current
        current = current.next
      }
      // 将上一个元素的next指向current的下一项,跳过current,实现移除current
      previous.next = current.next
    }

    // 更新列表长度
    length--

    // 返回删除的元素
    return current.element
  } else {
    return null
  }
}

实现insert

与removeAt类似的实现,大家可以先不看源码,自己按着removeAt的思路实现一遍

this.insert = function (position, element) {
  // 检查位置是否越界
  if (position >= 0 && position <= length) {
    var node = new Node(element),
        index = 0,
        previous,
        current = head

    // 在第一个位置添加
    if (position === 0) {

      node.next = current
      head = node

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

      node.next = current
      previous.next = node
    }

    // 更新列表长度
    length++

    return true
} else {
  return false
}
}

实现indexOf

根据元素查找在链表中的位置,没找到就返回-1

this.indexOf = function (element) {
  var current = head,
      index = 0

  while (current) {
    if (element === current.element) {
      return index
    }
    index++
    current = current.next
  }

  return -1
}

实现其他方法

// 返回所有元素的值转成字符串
this.toString = function () {
  var current = head,
      string = ''

  while (current) {
    string += current.element
    current = current.next
  }

  return string
}

// 移除特定元素
this.remove = function (element) {
  var index = this.indexOf(element)
  return this.removeAt(index)
}

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

// 返回链表长度
this.size = function () {
  return length
}

// 返回第一个元素
this.getHead = function () {
  return head
}

以上是单向链表的实现,有兴趣的可以下载源码来看:

单向链表的实现-源代码

双向链表

双向链表和单向链表的区别就是每一个元素是双向的,一个元素中包含两个引用:一个指向前一个元素;一个指向下一个元素。除此之外,双向链表还有一个指向最后一个元素的tail指针,这使得双向链表可以从头尾两个方向迭代链表,因此更加灵活。如下图:

双向链表

(图片来源谷歌搜索,侵删)

用JavaScript实现双向链表

同单向链表一样,先声明一个构造函数

function DoubleLinkedList () {
  var Node = function (element) {
    this.element = element
    this.prev = null // 新增了一个指向前一个元素的引用
    this.next = null
  }

  var length = 0
  var head = null
  var tail = null //新增了tail指向最后一个元素
}

双向链表需要有以下方法:

  • append(element):向链表尾部添加元素
  • insert(position, element):向链表特定位置插入元素
  • removeAt(position):从链表特定位置移除一项
  • showHead():获取双向链表的头部
  • showLength():获取双向链表长度
  • showTail():获取双向链表尾部

实现insert

同单向链表类似,只不过情况更复杂了,你不仅需要额外考虑在第一个元素的位置插入新元素,还要考虑在最后一个元素之后插入新元素的情况。此外如果在第一个元素插入时,链表为空的情况也需要考虑。

this.insert = function (position, element) {
  // 检查是否越界
  if (position >= 0 && position <= length) {
    var node = new Node(element),
        current = head,
        previous,
        index = 0

    if (position === 0) { // 第一个元素的位置插入
      // 如果链表为空
      if (!head) {
        head = node
        tail = node
      } else {
        node.next = current
        current.prev = node
        head = node
      }
    } else if (position === length) { // 在最后一个元素之后插入
      current = tail
      node.prev = current
      current.next = node
      tail = node
    } else { // 在中间插入
      while (index++ < position) {
        previous = current
        current = current.next
      }

      node.next = current
      previous.next = node

      current.prev = node
      node.prev = previous
    }

    length++

    return true
  } else {
    return false
  }
}

实现removeAt

与insert类似,这里不多解释直接贴代码

this.removeAt = function (position) {
  // 检查是否越界
  if (position > -1 && position < length) {
    var current = head,
        previous,
        index = 0

    if (position === 0) { // 第一个元素
      head = current.next
      // 如果只有一个元素
      if (length === 1) {
        tail = null
      } else {
        head.prev = null
      }
    } else if (position === length - 1) { // 最后一个元素
      current = tail
      tail = current.prev
      tail.next = null
    } else {
      while (index++ < position) {
        previous = current
        current = current.next
      }

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

    length--

    return current.element
  } else {
    return null
  }
}

实现append

和单向链表的一样,只不过多了tail有一些不同

this.append = function (element) {
  var node = new Node(element),
      current = tail

  if (head === null) {
    head = node
    tail = node
  } else {
    node.prev = current
    current.next = node
    tail = node
  }

  length++
}

其他的代码都很简单,我就放上源代码地址,大家自行查阅吧~

双向链表的实现-源代码

小结

一开始写的时候有点不习惯,但是实现了一两个方法之后发现很多思路是类似的,于是举一反三地写出来,然后跟书上对比之后,发现还是有点差距的。

大家有兴趣也动手去实践一下再对比源码,可以认识到自己有哪些地方不足。

链表暂时就这样了,明天继续攻克集合!


LiuBoJun
264 声望18 粉丝

专注于知识库产品开发,在富文本编辑器领域有丰富经验