数据结构与算法(五)单向链表

399 阅读4分钟

链表和数组

链表和数组一样,可以用于存储一系列的元素,但是链表和数组的实现机制完全不同。

数组

  • 存储多个元素,数组(或列表)可能是最常用的数据结构。

  • 几乎每一种编程语言都有默认实现数组结构,提供了一个便利的 [] 语法来访问数组元素。

  • 数组缺点:

    数组的创建需要申请一段连续的内存空间(一整块内存),并且大小是固定的,当前数组不能满足容量需求时,需要扩容。 (一般情况下是申请一个更大的数组,比如 2 倍,然后将原数组中的元素复制过去)

    在数组开头或中间位置插入数据的成本很高,需要进行大量元素的位移。

链表

  • 存储多个元素,另外一个选择就是使用链表。

  • 不同于数组,链表中的元素在内存中不必是连续的空间。

  • 链表的每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(有些语言称为指针)组成。

  • 链表优点:

    内存空间不必是连续的,可以充分利用计算机的内存,实现灵活的内存动态管理。

    链表不必在创建时就确定大小,并且大小可以无限延伸下去。

    链表在插入和删除数据时,时间复杂度可以达到 O(1),相对数组效率高很多。

  • 链表缺点:

    访问任何一个位置的元素时,需要从头开始访问。(无法跳过第一个元素访问任何一个元素)

    无法通过下标值直接访问元素,需要从头开始一个个访问,直到找到对应的元素。

    虽然可以轻松地到达下一个节点,但是回到前一个节点是很难的。

单向链表

单向链表类似于火车,有一个火车头,火车头会连接一个节点,节点上有乘客,并且这个节点会连接下一个节点,以此类推。

  • 链表的火车结构

image.png

  • 链表的数据结构

head 属性指向链表的第一个节点。 链表中的最后一个节点指向 null。 当链表中一个节点也没有的时候,head 直接指向 null

image.png

  • 给火车加上数据后的结构

image.png

链表入门

我们把链表的每一个节点看成js中的对象,我来写这样的一段代码,如下所示。

//简单的链表

let node1 = { data: 1, next: null }
let node2 = { data: 2, next: null }
let node3 = { data: 3, next: null }
let node4 = { data: 4, next: null }
let node5 = { data: 5, next: null }

let head = node1
node1.next = node2
node2.next = node3
node3.next = node4
node4.next = node5

console.log(head)

我们在浏览器端可以看见打印,1、2、3、4、5。是不是正好如我们所愿排排站。

image.png

下面我们再来一次封装把,用class构造出链表的节点, 同样可以达到想要的效果。


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

let node1 = new Node(1)
let node2 = new Node(2)
let node3 = new Node(3)
let node4 = new Node(4)
let node5 = new Node(5)


let head = node1
node1.next = node2
node2.next = node3
node3.next = node4
node4.next = node5

console.log(head)

image.png

接着再用一个循环,对代码进行封装一下,看起来不那么繁琐。变身,同样也达到了想要的效果。


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

let head = null
let vars = {}
//批量构造数据
for (let i = 1; i <= 5; i++) {
  let key = `node${i}`
  vars[key] = new Node(i)
}
//把数据组装成链表
for (let i = 1; i <= 5; i++) {
  let key = `node${i}`
  let keyNext = `node${i + 1}`
  if (i === 1) {
    head = vars[key]
  }
  vars[key].next = vars[keyNext]
}
console.log(head)

有了上面的基础知识,我们可以开始考虑,链表有哪些基本的操作,并尝试去实现他们,

链表常见操作

  • append(element) 向链表尾部添加一个新的项。
  • insert(position, element) 向链表的特定位置插入一个新的项。
  • get(position) 获取对应位置的元素。
  • indexOf(element) 返回元素在链表中的索引。如果链表中没有该元素就返回-1。
  • update(position, element) 修改某个位置的元素。
  • removeAt(position) 从链表的特定位置移除一项。
  • remove(element) 从链表中移除一项。
  • isEmpty() 如果链表中不包含任何元素,返回 trun,如果链表长度大于 0 则返回 false。
  • size() 返回链表包含的元素个数,与数组的 length 属性类似。
  • print() 以字符串的形式打印出链表中当前存在的节点data的值。

代码实现

在实现之前,我们先把链表的基本结构用代码写出来

//链表的节点 node
class Node {
  constructor(data) {
    this.data = data
    this.next = null
  }
}


class LinkedList {
  constructor() {
    //链表的头部
    this.head = null
    //记录链表的长度
    this.length = 0
  }
}

append()方法

  //插入
  append(data) {
    // 1.如果链表中没有数据就直接把head执行新的节点
    // 2.如果链表中有数据就找到最后一个节点,并把最后一个节点的next指向新的节点
    let newNode = new Node(data)
    if (this.length === 0) {
      this.head = newNode
    } else {
      //遍历这链表找到最后一个节点
      let current = this.head
      //如果下一个节点存在,就一直遍历
      while (current.next) {
        current = current.next
      }
      current.next = newNode
    }
    this.length++
  }

过程图解

  • 首先让 currentNode 指向第一个节点。

image.png

  • 通过 while 循环使 current 指向最后一个节点,最后通过 current.next = newNode,让最后一个节点指向新节点 newNode

image.png

代码测试

我们同样也得到了之前手动构造出来的结果。

const LinkedList = require('./linked-list')
const linkedList = new LinkedList()

linkedList.append(1)
linkedList.append(2)
linkedList.append(3)
linkedList.append(4)
console.log(linkedList)

console.log(linkedList.print())

image.png

insert()方法

  insert(position, data) {
    //对插入位置进行检测
    if (position < 0 || position > this.length) {
      return false
    }

    //创建新的节点
    let newNode = new Node(data)
    if (position === 0) {
      newNode.next = this.head
      this.head = newNode
    } else {
      //找到前一个节点
      let index = 0
      let prevNode = null
      let current = this.head
      // 在 0 ~ position 之间遍历,不断地更新 currentNode 和 previousNode
      // 直到找到要插入的位置
      while (index < position) {
        prevNode = current
        current = current.next
        index++
      }
      prevNode.next = newNode
      newNode.next = current
    }
    this.length++
  }

代码测试

const LinkedList = require('./linked-list')


const linkedList = new LinkedList()


linkedList.append(1)
linkedList.append(2)
linkedList.append(3)
linkedList.append(4)

linkedList.insert(0,100)

linkedList.insert(4, 99)
console.log(linkedList.print())

getData()

  getData(position) {
    // 1、position 越界判断
    if (position < 0 || position >= this.length) return null;

    // 2、获取指定 position 节点的 data
    let currentNode = this.head;
    let index = 0;

    while (index++ < position) {
      currentNode = currentNode.next;
    }
    // 3、返回 data
    return currentNode.data;
  }

indexOf()

  indexOf(data) {

    let currentNode = this.head;
    let index = 0;

    while (currentNode) {
      if (currentNode.data === data) {
        return index;
      }
      currentNode = currentNode.next;
      index++;
    }

    return -1;
  }

update()

  update(position, data) {
    // 涉及到 position 都要进行越界判断
    // 1、position 越界判断
    if (position < 0 || position >= this.length) return false;

    // 2、痛过循环遍历,找到指定 position 的节点
    let currentNode = this.head;
    let index = 0;
    while (index++ < position) {
      currentNode = currentNode.next;
    }

    // 3、修改节点 data
    currentNode.data = data;

    return currentNode;
  }

removeAt()

  removeAt(position) {
    // 1、position 越界判断
    if (position < 0 || position >= this.length) return null;

    // 2、删除指定 position 节点
    let currentNode = this.head;
    if (position === 0) {
      // position = 0 的情况
      this.head = this.head.next;

    } else {
      // position > 0 的情况
      // 通过循环遍历,找到指定 position 的节点,赋值到 currentNode

      let previousNode = null;
      let index = 0;

      while (index < position) {
        previousNode = currentNode;
        currentNode = currentNode.next;
        index++
      }

      // 巧妙之处,让上一节点的 next 指向到当前的节点的 next,相当于删除了当前节点。
      previousNode.next = currentNode.next;
    }

    // 3、更新链表长度 -1
    this.length--;

    return currentNode;
  }

remove()

remove(data) {
    this.removeAt(this.indexOf(data));
}

isEmpty()

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

size()

size() {
    return this.length;
}

while循环的时候,到底是判断current还是current.next在于循环之后还要不要操作current 如果要操作current的话就的取current.next 这样循环结束后 current才有值。