数据结构与算法 - 链表

优质
小牛编辑
127浏览
2023-12-01

链表的概念

逻辑结构上一个挨一个的数据,在实际存储时,并没有像顺序表(数组)那样也相互紧挨着。恰恰相反,数据随机分布在内存中的各个位置,这种存储结构称为线性表的链式存储。

每个元素本身由两部分组成:

  1. 本身的信息,称为 数据域
  2. 指向直接后继的指针,称为 指针域

    image.png

内存分布

数据是连续存储的,一个挨着一个,连续的。链表是存储单元不一定是连续的, image.png

主要分类

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

单向链表

image.png

循环链表

image.png

双向链表

image.png

双向循环链表

image.png

链表和数组的区别

两者的区别

  • 数组静态分配内存,链表动态分配内存。
  • 数组在内存中是连续的,链表是不连续的。
  • 数组利用下标定位,查找的时间复杂度是O(1),链表通过遍历定位元素,查找的时间复杂度是O(N)。
  • 数组插入和删除需要移动其他元素,时间复杂度是O(N),链表的插入或删除不需要移动其他元素,时间复杂度是O(1)。

数组的优点

  • 随机访问性比较强,可以通过下标进行快速定位。随机访问性比较强
  • 查找速度快

数组的缺点

  • 插入和删除的效率低,需要移动其他元素。
  • 会造成内存的浪费,因为内存是连续的,所以在申请数组的时候就必须规定七内存的大小,如果不合适,就会造成内存的浪费。
  • 内存空间要求高,创建一个数组,必须要有足够的连续内存空间。
  • 数组的大小是固定的,在创建数组的时候就已经规定好,不能动态拓展。

链表的优点和缺点

  • 链表是通过指针将零散的内存块串连起来的。 所以链表不支持 随机访问,如果要找特定的项,只能从头开始遍历,直到找到某个项。 所以访问的时间复杂度为 O(n)。查找的效率低,因为链表是从第一个节点向后遍历查找。
  • 高效的插入和删除。 链表中插入或者删除一个数据,我们并不需要为了保持内存的连续性而搬移结点,因为链表的存储空间本身就不是连续的,只需要考虑相邻结点的指针改变。 所以,在链表中插入和删除一个数据是非常快速的,时间复杂度为 O(1)。
  • 内存利用率高,不会浪费内存,可以使用内存中细小的不连续的空间,只有在需要的时候才去创建空间。
  • 大小不固定,拓展很灵活。

实现

单链表

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<script>
  /**
   单向链表

   append(element):向链表尾部添加新项
   insert(position, element):向链表的特定位置插入一个新的项

   removeAt(position):从链表的特定位置移除一项
   remove(element):从链表中移除一项

   indexOf(element):返回元素在链表中的索引。如果链表中没有该元素则返回-1

   isEmpty():如果链表中不包含任何元素,返回true,如果链表长度大于0,返回false
   size():返回链表包含的元素个数,与数组的length属性类似

   getHead():返回链表的第一个元素
   toString():由于链表使用了Node类,就需要重写继承自JavaScript对象默认的toString()方法,让其只输出元素的值
   print():打印链表的所有元素
   */

  class Node {
    constructor(element = null, next = null) {
      this.element = element
      this.next = next
    }
  }

  class SinglyList {
    constructor() {
      // 头指针
      this.head = null
      // 尾指针
      this.tail = null
      this.length = 0
    }

    /**
     * 尾插法 插入元素
     * @param element
     */
    append(element) {
      let newNode = new Node(element)
      if (this.head === null) {
        this.head = newNode
        this.tail = newNode
      } else {
        // let curNode = this.head
        // while (curNode.next) {
        //   curNode = curNode.next
        // }
        //
        // // 找到最后一个节点
        // curNode.next = newNode

        this.tail.next = newNode
        this.tail = newNode
      }

      this.length++
    }

    /**
     * 头插法 插入元素
     * @param value
     * @returns {SinglyList}
     */
    prepend(value) {
      // Make new node to be a head.
      const newNode = new Node(value, this.head)
      this.head = newNode

      // If there is no tail yet let's make new node a tail.
      if (!this.tail) {
        this.tail = newNode
      }
      this.length++
      return this
    }

    /**
     * 向单向链表中插入某个元素
     * @param  {Number} position 插入的位置
     * @param  {Any} element     插入的元素
     * @return {Boolean}        成功返回true,失败返回false
     */
    insert(position, element) {
      if (position < 0 || position > this.length) {
        return false
      }

      let newNode = new Node(element)
      let curNode = this.head
      let index = 0
      let preNode = null

      if (position === 0) {
        newNode.next = curNode
        this.head = newNode
      } else {
        // 排除不需要插入的节点
        while (index++ < position) {
          preNode = curNode      // 保存上一节点
          curNode = curNode.next // 保存当前节点
        }

        // 找到节点
        newNode.next = curNode
        preNode.next = newNode
      }

      this.length++
      return true
    }


    removeAt(postion) {
      if (postion <= -1 || postion > this.length) return null

      let curNode = this.head
      let prvNode
      let index = 0
      if (postion === 0) {
        // 因为之前head指向第一个元素,现在把head修改为指向第二个元素。
        // 核心概念在于链表前后全靠指针链接,而非数组一般。
        // 所以只需要改变head的元素。

        this.head = curNode.next
      } else {
        // 跟插入操作类似处理
        while (index++ < postion) {
          prvNode = curNode
          curNode = curNode.next
        }

        prvNode.next = curNode.next
      }

      this.length--
      return curNode.data
    }

    /**
     * 移除给定的元素
     * @param  {Any} element    要移除的元素
     * @return {Number}         返回值>=0表示移除成功
     */
    remove(element) {
      let index = this.indexOf(element)
      if (index > -1) {
        return this.removeAt(index)
      } else {
        return null
      }
    }

    isEmpty() {
      return this.length === 0
    }

    size() {
      return this.length
    }

    getHead() {
      if (!this.isEmpty()) {
        return this.head
      }
    }

    toString() {
      let curNode = this.head
      let str = ''
      while (curNode) {
        str += ' ' + curNode.data
        curNode = curNode.next
      }

      // 删除前面第一个空格slice(1)
      return str.slice(1)
    }

    /**
     * 单向连表
     * 已知一个单链表的头结点,找到该链表中,倒数第 K 个结点。
     * @param k
     * @returns {*}
     */
    //
    theKthNode(k) {
      if (k < 0) {
        return null;
      }

      let fast = this.head
      let slow = this.head
      let i = k;

      // fast 指针,先走 K 步
      for (; i > 0 && fast != null; i--) {
        fast = fast.next
      }

      if (i > 0) {
        // 链表长度,小于 K
        return null;
      }
      // fast、slow 同步走
      while (fast != null){
        slow = slow.next
        fast = fast.next
      }
      return slow
    }

    toArray() {
      let list = []
      let current = this.head
      while (current) {
        list.push(current.element)
        current = current.next
      }
      return list
    }

    /**
     * @return {LinkedListNode}
     */
    deleteTail() {
      const deletedTail = this.tail;

      if (this.head === this.tail) {
        // There is only one node in linked list.
        this.head = null;
        this.tail = null;

        return deletedTail;
      }

      // If there are many nodes in linked list...

      // Rewind to the last node and delete "next" link for the node before the last one.
      let currentNode = this.head;
      while (currentNode.next) {
        if (!currentNode.next.next) {
          currentNode.next = null;
        } else {
          currentNode = currentNode.next;
        }
      }

      this.tail = currentNode;

      return deletedTail;
    }

    /**
     * @return {LinkedListNode}
     */
    deleteHead() {
      if (!this.head) {
        return null
      }

      const deletedHead = this.head

      if (this.head.next) {
        this.head = this.head.next
      } else {
        this.head = null
        this.tail = null
      }

      return deletedHead
    }

    find({value = undefined, callback = undefined}) {
      if (!this.head) {
        return null;
      }

      let currentNode = this.head;

      while (currentNode) {
        // callback返回值为 false 就一直查找
        if (callback && callback(currentNode.element)) {
          return currentNode;
        }

        // If value is specified then try to compare by value..
        if (value !== undefined && currentNode.element === value) {
          return currentNode;
        }

        currentNode = currentNode.next;
      }

      return null;
    }

    delete(value) {
      if (!this.head) {
        return null;
      }

      let deletedNode = null;

      // If the head must be deleted then make next node that is differ
      // from the head to be a new head.
      while (this.head && this.head.element.key === value) {
        deletedNode = this.head;
        this.head = this.head.next;
      }

      let currentNode = this.head;

      if (currentNode !== null) {
        // If next node must be deleted then make next node to be a next next one.
        while (currentNode.next) {
          if (currentNode.next.element.key  === value) {
            deletedNode = currentNode.next;
            currentNode.next = currentNode.next.next
          } else {
            currentNode = currentNode.next
          }
        }
      }

      // Check if tail must be deleted.
      if (this.tail.element.key === value) {
        this.tail = currentNode;
      }

      this.length --
      return deletedNode;
    }

  }

  window.singly = new SinglyLinkedList()
  singly.append('A')
  singly.append('B')
  singly.append('D')

  let node = singly.theKthNode(2)
  console.log(node)
</script>
</body>
</html>

双向链表

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<script>
  /**
   双向链表提供了两种迭代列表的方法:
   从头到尾
   从尾到头。
   我们可以访问一个特定节点的下一个或前一个元素。
   在单向链表中,如果迭代链表时错过了要找的元素,就需要回到链表起点,重新开始迭代。
   在双向链表中,可以从任一节点,向前或向后迭代,这是双向链表的一个优点。

   */

  class Node {
    constructor(data = null, next = null, prev = null) {
      this.element = data
      this.next = next
      this.prev = prev
    }
  }

  class DoublyLinkedList {
    constructor() {
      this.head = null
      this.tail = null
      this.length = 0
    }

    append(element) {
      let newNode = new Node(element)
      if (this.head === null) {
        this.head = newNode
        this.tail = newNode
      } else {
        this.tail.next = newNode  // 将队尾元素next指向插入的元素
        newNode.prev = this.tail  // 插入的新元素上一级指向原来的队尾元素素
        this.tail = newNode       // 新元素成为队尾
      }
      this.length++
      return this
    }

    // 正向遍历的方法
    forwardString() {
      let curNode = this.head
      let str = ''
      while (curNode) {
        str += ' ' + curNode.element
        curNode = curNode.next
      }
      return str.slice(1)
    }

    // 反向遍历的方法
    reverseString() {
      let curNode = this.tail
      let str = ''
      while (curNode) {
        str += ' ' + curNode.element
        curNode = curNode.prev
      }

      return str.slice(1)
    }

    toString() {
      return this.forwardString()
    }

    getHead() {
      return this.head.element
    }

    getTail() {
      return this.tail.element
    }
    /**
     * 向单向链表中插入某个元素
     * @param  {Number} position 要插入的位置
     * @param  {Any} element  要插入的元素
     * @return {Boolean}          插入成功返回true,失败返回false
     */
    insert(position, element) {
      if (position < 0 || position > this.length) {
        return false
      }

      let newNode = new LinkedListNode(element)
      let curNode = this.head
      let index = 0
      let preNode = null

      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 {

        // 不需要插入的节点
        while (index++ < position) {
          preNode = curNode       // 保存上一节点
          curNode = curNode.next  // 保存当前节点
        }

        // 找到节点
        newNode.next = curNode
        newNode.prev = preNode

        curNode.prev = newNode
        preNode.next = newNode

      }

      this.length++
      return true
    }


    /**
     * 寻找某个元素在单向链表中的位置
     * @param  {Any} element 要寻找的元素
     * @return {Number}      返回值>=0则代表找到相应位置
     */
    indexOf(elemnt) {
      let curNode = this.head
      let index = 0

      while (curNode) {

        // 找到
        if (curNode.element === elemnt) {
          return index
        }

        //继续找
        index++
        curNode = curNode.next
      }

      // 没有找到
      return -1
    }

    find({value = undefined, callback = undefined}) {
      if (!this.head) {
        return null;
      }

      let currentNode = this.head;

      while (currentNode) {
        // callback返回值为 false 就一直查找
        if (callback && callback(currentNode.element)) {
          return currentNode;
        }

        // If value is specified then try to compare by value..
        if (value !== undefined && currentNode.element === value) {
          return currentNode;
        }

        currentNode = currentNode.next;
      }

      return null;
    }

    /**
     * 移除指定位置的元素
     * @param postion
     * @returns {*}
     */
    removeAt(postion) {
      if (postion <= -1 || postion > this.length) return null

      let curNode = this.head
      let prvNode
      let index = 0
      if (postion === 0) {
        // 因为之前head指向第一个元素,现在把head修改为指向第二个元素。
        // 核心概念在于链表前后全靠指针链接,而非数组一般。
        // 所以只需要改变head的元素。

        this.head = curNode.next
      } else {
        // 跟插入操作类似处理
        while (index++ < postion) {
          prvNode = curNode
          curNode = curNode.next
        }

        prvNode.next = curNode.next
      }

      this.length--
      return curNode.element
    }

    /**
     * 移除给定的元素
     * @param  {Any} element     要移除的元素
     * @return {Number}         返回值>=0表示移除成功
     */
    remove(element) {
      let index = this.indexOf(element)
      if (index > -1) {
        return this.removeAt(index)
      } else {
        return null
      }
    }

    isEmpty() {
      return this.length === 0
    }

    size() {
      return this.length
    }

    clear () {
      this.head = null
      this.tail = null
      this.length = 0
    }


    toArray() {
      let list = []
      let current = this.head
      while (current) {
        list.push(current.element)
        current = current.next
      }
      return list
    }

    /**
     * 反转链表
     * @returns {LinkedList}
     */
    reverse() {
      let currNode = this.head
      let prevNode = null
      let nextNode = null

      while (currNode) {
        // Store next node.
        nextNode = currNode.next

        // Change next node of the current node so it would link to previous node.
        currNode.next = prevNode

        // Move prevNode and currNode nodes one step forward.
        prevNode = currNode
        currNode = nextNode
      }

      // Reset head and tail.
      this.tail = this.head
      this.head = prevNode

      // 返回当前实例对象
      return this
    }

    fromArray (values) {
      if (!Array.isArray(values)) return false
      values.forEach(value => this.append(value))
      return this
    }

  }


  let list = new DoublyLinkedList()
  list.append('A')
  list.append('B')
  list.append('D')
  list.append({ value: 1, key: 'test1' })
      .append({ value: 2, key: 'test2' })
      .append({ value: 3, key: 'test3' })


  let node = list.find({
    callback: (value) => {
      return value.key === 'test1'
    }
  })

  console.log(node)
</script>
</body>
</html>

链表操作摘要

  • 单链表反转
  • 单链表反转从位置 m 到 n 的部分
  • 链表中环的检测
  • 合并两个有序的链表
  • 合并K个排序链表
  • 删除链表倒数第n个节点
  • 求链表的中间结点
  • 求链表环的入口节点
  • 两两交换链表中的节点
  • K 个一组翻转链表