JS算法系统学习第五章:链表结构

137 阅读4分钟

上一章:# JS算法系统学习第四章:队列结构

链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。

使用链表结构可以克服数组结构需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。

链表的特点

  1. 插入、删除数据效率高 O(1) 级别(只需更改指针指向即可),随机访问效率低 O(n) 级别(需要从链头至链尾进行遍历)。
  2. 和数组相比,内存空间消耗更大,因为每个存储数据的节点都需要额外的空间存储后继指针。

5.1. 单链表

每个节点只包含一个指针,即后继指针。

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

class LinkedList {
  // #
  constructor() {
    this.count = 0; // 计数:有几个结点
    this.head = null; // 链头
  }

  push(element) {
    const node = new Node(element)

    // hearder 是空
    if(this.head === null) {
      this.head = node
    } else {
     let current = this.head;
     while(current.next !== null){
        current = current.next
     }
     current.next = node 
    }
    this.count++;
  }

  // 指定位置删除
  removeAt(index){
    if(index >=0 && index < this.count){
      let current = this.head
      if(index === 0){
        this.head = this.head.next
      } else {
        // 让前一个 next = 当前的 next
        // let previous
        // for(let i=0;i<index;i++){
        //   previous = current
        //   current = current.next
        // }
        let previous = this.getNodeAt(index - 1)
        current = previous.next

        previous.next = current.next
      }
      this.count--
      return current.element
    }

    return
  }

  getNodeAt(index){
    if(index>=0 && index<this.count){
      let node = this.head

      for(let i=0; i<index; i++){
        node = node.next
      }
      return node
    }
    return
  }

  // 根据数据 返回索引的方法
  indexOf(element) {
    let current = this.head
    for(let i=0;i<this.count;i++) {
      if(this.equalFn(element, current.element)){
        return i
      }
      current = current.next
    }
    return -1
  }

  // 判断是否是同个元素
  equalFn(a, b){
    // 方式一:不适用于直接 push({a:1}) 的对象
    // return a===b
    // 方式二:比较复杂
    // 1.只能做一层对比:判断是对象,对象1[key] === 对象2[key]
    // 2.在1基础上,写一个递归函数,深度检查是否相等

    // 方式三:简单暴力,转换成字符串的方式比较。
    // 缺点:对于list.push({a:1,b:2}) list.push({b:2,a:1})不生效。
    return JSON.stringify(a) === JSON.stringify(b)

    // 方式四:imumutable(第三方js库)
  }

  remove(element){
    const index = this.indexOf(element)
    return this.removeAt(index)
  }

  insert(element, index) {
    if(index>=0 && index<=this.count) {
      const node = new Node(element)
      if(index === 0) {
        const current = this.head
        node.next = current
        this.head = node
      } else {
        const previous = this.getNodeAt(index - 1)
        const current = previous.next
        node.next = current
        previous.next = node
      }
      this.count++
      return true
    }

    return false
  }

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

  size(){
    return this.count
  }

  getHead(){
    return this.head
  }
}

var list = new LinkedList()

5.2. 单链表的应用

更松散,能解决的问题更多

  1. 在双端队列中的应用 - 回文检查
function test(str){
  const lowstr = str.toLocaleLowerCase().split(" ").join("")

  let dequeue = new LinkedList()

  for(let i=0;i<lowstr.length;i++){
    dequeue.push(lowstr[i])
  }

  let isEqual = true
  while(dequeue.size()>1){
    if(dequeue.removeAt(0) !== dequeue.removeAt(dequeue.size() - 1)){
      isEqual = false
      break;
    }
  }

  return isEqual
}

test("D   a       d")

2. 在队列中的应用 - 击鼓传花

function game(list,num) {
  let queue = new LinkedList()
  for(let i=0;i<list.length;i++) {
    queue.push(list[i])
  }

  while(queue.size()>1) {
    for(let i=0;i<num;i++){
      queue.push(queue.removeAt(0))
    }

    console.log(queue.removeAt(0),"淘汰了")
  }

  return queue.removeAt(0)
}


game(["user1","user2","user3","user4","user5"],7)

3. 在栈中的应用 - 进制转换

function convert(decNumber, base){
  let remStack = new LinkedList()
  let number = decNumber
  let string = ""
  let baseString = "0123456789ABCDEF"

  while(number > 0) {
    remStack.push(number%base)
    number = Math.floor(number/base)
  }

  while(!(remStack.isEmpty())){
    string += baseString[remStack.removeAt(remStack.size() - 1)]
  }

  return string
}

convert(50, 2) // 110010
convert(50, 8) // 62
convert(500, 16) // 1F4

5.3. 双向链表

节点除了存储数据外,还有两个指针分别指向前一个节点地址(前驱指针 prev)和下一个节点地址(后继指针 next)

class DoublyNode extends Node {
  constructor(element) {
    super(element)
    this.prev = null
  }
}

class DoublyLinkedList extends LinkedList{
  // #
  constructor() {
    super()
    this.tail = null // 尾巴
  }

  push(element) {
    const node = new DoublyNode(element)
    // prev next

    // hearder 是空
    if(this.head === null) {
      this.head = node
      this.tail = node
    } else {
     this.tail.next = node
     node.prev = this.tail
     this.tail = node
    }
    this.count++;
  }


  insert(element, index) {
    if(index>=0 && index<=this.count) {
      const node = new DoublyNode(element)
      let current = this.head
      if(index === 0) {
        if(this.head === null) {
          this.head = node;
          this.tail = node;
        } else {
          node.next = this.head;
          this.head.prev = node;

          this.head = node
        }
      } else if(index === this.count){
        current = this.tail
        current.next = node
        node.prev = current
        this.tail = node
      } else {
        const previous = this.getNodeAt(index - 1)
        current = previous.next

        node.next = current
        current.prev = node
        previous.next = node
        node.prev = previous
      }
      this.count++
      return true
    }

    return false
  }

  // 指定位置删除
  removeAt(index){
    if(index >=0 && index < this.count){
      let current = this.head
      if(index === 0){
        this.head = this.head.next
        if(this.count === 1){
          this.tail = null
        }else{
          this.head.prev = undefined
        }
      } else if(index === this.count-1){
        current = this.tail;
        this.tail = current.prev;

        this.tail.next = undefined;
      } else {
        let previous = this.getNodeAt(index - 1)
        current = previous.next
        previous.next = current.next

        current.next.prev = previous
      }
      this.count--
      return current.element
    }

    return
  }

  getHead(){
    return this.head
  }

  getTail(){
    return this.tail
  }
}

var list = new DoublyLinkedList()

5.4. 循环链表

循环链表和链表之间唯一的区别在于,最后一个元素指向下一个元素的指针(tail.next)不是引用 undefined,而是指向第一个元素(head)

class CirularLinkedList extends LinkedList{
  constructor() {
    super()
  }

  push(element) {
    const node = new Node(element)
    let current;

    // hearder 是空
    if(this.head === null) {
      this.head = node
    } else {
      current = this.getNodeAt(this.size() - 1)
      current.next = node
    }
    node.next = this.head
    this.count++;
  }


  insert(element, index) {
    if(index>=0 && index<=this.count) {
      const node = new Node(element)
      let current = this.head
      if(index === 0) {
        if(this.head === null) {
          this.head = node;
          node.next = this.head
        } else {
          node.next = current
          current = this.getNodeAt(this.size() - 1)
          this.head = node
          current.next = this.head
        }
      } else {
        const previous = this.getNodeAt(index - 1)
        node.next = previous.next

        previous.next = node
      }
      this.count++
      return true
    }

    return false
  }

  // 指定位置删除
  removeAt(index){
    if(index >=0 && index < this.count){
      let current = this.head
      if(index === 0){
        if(this.size() === 1){
          this.head = undefined
        }else{
          let last = this.getNodeAt(this.size() - 1)
          this.head = this.head.next
          last.next = this.head
        }
      } else {
        const previous = this.getNodeAt(index -1)
        current = previous.next
        previous.next = current.next
      }
      this.count--
      return current.element
    }
    return
  }
}

var list = new CirularLinkedList()