链表-单链表实现

关于链表

要存储多个元素的时候, 数组/列表 是最为常用的数据结构, 几乎每个编程语言都实现了数组或者列表.

但这种结构的缺点是, 通常数组大小是固定的, 即便类似 js Array 或者 Python 中的 list, 当我们从中间插入或者删除元素时成本很高.

数组特点是: 访问快 (有索引), 中间或者头部插入效率慢, 需要连续存储.

而链表这种结构, 里面的元素不是连续放置的. 每个元素由一个存储元素本身的节点 和 指向下一个元素的引用 (指针/链接) 组成. 其好处在于, 添加或者删除元素, 不需要移动其他元素, 但经常需要指针从头节点去遍历查找.

链表的特点是: 访问效率低 (都要遍历), 中间或头部插入效率高 (不用移动其他元素), 分散存储.

创建链表

要创建链表, 首先我们需要创建一个 Node 类来表示每个节点元素, 其存储自身的值 和 一个指针属性, 默认指向 null.

// 节点类
class Node {
  constructor(element) {
    this.element = element
    this.next = null
  }
}

然后是链表的初始状态, 用一个变量 count 来动态记录链表大小, 用 head 来指向头结点对象, 默认是 null.

// 链表类
class LinkedList {
  constructor(equalsFn = defaultEquals) {
    this.count = 0        // 链表元素数量
    this.head = null     // 头结点元素的引用
  }
}

链表常用方法

基本的方法包括获取链表的大小, 插入元素, 查询元素, 链表查看等.

  • size() 获取链表的大小
  • isEmpty() 判断链表是否为空
  • getHead() 获取头结点的值
  • toString() 以字符形式打印链表元素
  • getElement(index) 根据位置查询对应节点元素值

  • indexOf(element) 获取元素的位置, 不存在则返回 -1
  • push(element) 从链表尾部添加元素
  • insert(index, element) 从链表任意位置插入元素 (前插)
  • removeAt(index) 根据位置删除元素
  • remove(element) 根据元素值

共用方法

  • size() 获取链表的大小
  • isEmpty() 判断链表是否为空
  • getHead() 获取头结点的值
  • toString() 以字符形式打印链表元素
  • getElement(index) 根据位置查询对应节点元素值
// 节点类
class Node {
    constructor(element) {
        this.element = element
        this.nex = null 
    }
}

// 链表类
class LinkedList {
    constructor() {
        this.count = 0
        this.head = null
    }
    // 基础公用方法, 链表大小, 头节点, 打印值 ...
    size() {
        return this.count
    }
    
    isEmpty() {
        return this.count == 0
    }
    
    getHead() {
        // 这里的 element 就是 Node 的值
        return this.head.element
    }
    
    toString() {
        if (this.head == null) return undefined
        
        let str = `${this.head.element}`
        // current 指向头结点的下一个元素, 然后开始移动, 直至为空节点
        let current = this.head.next
        for (let i = 1; i < this.size() && current != null; i++) {
            str = `${str}, ${current.element}`
            current = current.next
        }
        return str
    }
    
    // 根据节点位置索引, 查询并返回该节点对象
    // 这个方法使用频繁, 不论是查询, 增删节点都会用到
    getElementAt(index) {
        // 越界检查
        if (index < 0 || index > this.count) return undefined
        // 从链表头部开始迭代 0 -> index, 取出元素值即可
        let node = this.head
        for (let i = 0; i < index && node != null) {
            node = node.next
        }
        return node 
    }
}

链尾插入元素

  • 如果是空链表, 直接让 head 指向该元素
  • 如果链表非空, 则移动指针到链尾, 指向该元素
  • 最后要记得更新链表长度 +1
// 链尾添加元素
push(element) {
    // 实例化节点元素 和 声明 current 指针, 默认指向 head 节点
    const node = new Node(element)
    let current = this.head

    // 当链表为空时, 让链表 head 指向 node 即可
    if (this.head == null) {
        this.head = node
    } else {
        // 移动指针到最后, 然后添加元素
        while (current.next != null) {
            current = current.next
        }
    }
    // 循环结束后, 只是指针处于链尾, 将其 next 指向新元素即可
    current.next = node
}
// 最后一定要记得更新链表长度哦
this.count += 1

任意位置添加元素

这就要用到之前定义的 getElementAt(index) 方法了, 然后要根据插入位置做不同处理.

当插入的位置是 0, 即从头结点插入时:

node -> [];  [node]
或者: note -> [1, 2, 3] => [node, 1, 2, 3]

则:  this.head = node; node.next = current

当从中间插入时, 则要先找到这个元素, 并对其前, 后 元素进行先断链, 新增元素后再链接的操作:

node -> [1, 2, 3, 4], 要在位置 2 的位置插入:

[1, 2, node, 3, 4]

先获取目标位置的前一个元素 previous = p(2-1) = 2;
然后目标位置的元素 current = previous.next = 3;

然后断链插入操作, 让 current 往后 "挪" 一下即可.
previous.next = node
node.next = current

  // 任意位置插入元素
  insert(index, element) {
    // 越界检查
    if (index < 0 || index > this.count) return false 

    // 创建节点对象, 并判断是在哪个位置添加
    const node = new Node(element)
    
    if (index == 0) {
      // 如果是第一个位置添加, 则让 this.head -> node -> head_old 即可
      node.next = this.head 
      this.head = node
    } else {
      // 获取插入位置的前一个节点 previous, 以当前节点 current
      const previous = this.getElementAt(index - 1)
      const current = previous.next
      // 断链插入 node 以后, current 往后 "挪" 了一下
      previous.next = node 
      node.next = current
    }
    // 添加了都要更新链表长度
    this.count++
    return true
  }

任意位置删除元素

也是要用到上面定义的 getElementAt(index) 方法, 整体逻辑和插入逻辑是差不多的.

首先都是要根据位置, 获取到该元素节点 target, 然后根据位置进行分别断链处理即可.

  • 删除的是头元素, 直接让 head -> target.next
  • 删除的是非头元素, 让 target 前一个节点 -> target 的后一个节点就搞定
// 任意位置删除元素
removeAt(index) {
    // 索引越界检查
    if (index < 0 || index > this.count) return false
    let current = this.head
    
    // 判断 index 是否为 0, 对应不同的处理方式
    if (index == 0) {
        // 若移除的是头元素, 则将链表头指针指向下一个元素
    	this.head = current.next
    } else {
        // 找到目标位置的前一个元素, 后一个元素, 进行相连即可
        const previous = this.getElementAt(index -1)
        current = previous.next 
        previous.next = current.next
    }
    // 最后记得长度减1, 并返回被删除的元素
    this.count -= 1
    return current.element
}

返回元素的位置

即从头到尾遍历链表, 当找到时就返回位置, 没有找到则返回 -1.

indexOf(element) {
    let current = this.head
    for (let i = 0; i < this.count && current != null) {
        if (current.element = element) return i
        // 往后移动指针
        current = current.next
    }
    // 移动完都没找到就返回 -1
    return -1
}

删除元素

直接用上面的 indexOf(element) 找到元素的位置, 然后用 removeAt(index) 就搞定啦.

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

完整实现

// 节点类, 辅助
class Node {
  constructor(element) {
    this.element = element
    this.next = null
  }
}


// 链表类
class LinkedList {
  constructor() {
    this.count = 0        // 链表元素数量
    this.head = null      // 头结点元素的引用
  }
  // 链表大小, 是否为空, 返回头结点, toString 等
  size() {
    return this.count
  }

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

  getHead() {
    return this.head.element
  }

  toString() {
    if (this.head == null) return undefined

    let objString = `${this.head.element}`
    let current = this.head.next
    // 遍历节点将元素链接起来
    for (let i = 1; i < this.size() && current != null; i++) {
      objString = `${objString}, ${current.element}`
      current = current.next
    }
    return objString
  }

  // 根据元素位置索引, 查询并返回该元素
  getElementAt(index) {
    // 越界检查
    if (index < 0 || index > this.count) return undefined
    // 从链表头部开始迭代
    let node = this.head
    for (let i = 0; i < index && node != null; i++) {
      node = node.next
    }
    return node
  }

  // 链尾添加元素
  push(element) {
    // 元素添加进节点, 和声明 current 指针, 默认指向 head
    const node = new Node(element)
    let current = this.head

    // 当尾空链表时, 此时的元素是第一个, 则让链表的 head 指向该 node
    if (this.head == null) {
      this.head = node
    } else {
      // 链表中已有元素, 移动 current 指针到最后
      while (current.next != undefined) {
        current = current.next
      }

      current.next = node
    }
    this.count++
  }

  // 任意位置插入元素
  insert(index, element) {
    // 越界检查
    if (index < 0 || index > this.count) return false 

    const node = new Node(element)
    if (index == 0) {
      node.next = this.head 
      this.head = node
    } else {
      // 获取目标位置的前一个元素, 当前元素, 下一个元素
      const previous = this.getElementAt(index - 1)
      const current = previous.next

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

  // 移除指定位置的元素 
  removeAt(index) {
    // 索引越界检查
    if (index < 0 || index > this.count) return undefined
    // 用一个指针默认指向链表头元素
    let current = this.head
    if (index == 0) {
      // 如果移除的是第一项, 则将链表的头元素指向下一个元素即可
      this.head = current.next
    } else {
      const previous = this.getElementAt(index - 1)
      current = previous.next
      previous.next = current.next
    }
    this.count--
    return current.element
  }

  // 返回元素的位置 
  indexOf(element) {
    let current = this.head
    for (let i = 0; i < this.count && current != null; i++) {
      if (current.element === element) return i 
      // 移动指针
      current = current.next 
    }
    return -1
  }

  // 移除元素
  remove(element) {
    const index = this.indexOf(element)
    return this.removeAt(index)
  }
}

// test 
const list = new LinkedList()
console.log('链表元素是:',  list.toString());
list.push(10)
console.log('链表元素是:',  list.toString());

list.push(20)
list.push(30)
list.push(40)
console.log('链表元素是:',  list.toString());
console.log('链表的长度是: ', list.count);
console.log('删除的元素是: ', list.removeAt(0))
console.log('链表元素是:',  list.toString());
console.log('链表的长度是: ', list.count);

console.log('在位置 2 的地方添加 666:', list.insert(2, 666))
console.log('在位置 0 的地方添加 999:', list.insert(0, 999))
console.log('此时的头结点值是: ', list.getHead());
console.log('链表元素是:',  list.toString());
console.log('链表的长度是: ', list.count);

console.log('查找 666 的索引位置是: ', list.indexOf(666))
console.log('查找 888 的索引位置是: ', list.indexOf(888))

console.log('删除掉元素 666: ', list.remove(666))
console.log('链表元素是:',  list.toString());
console.log('链表的长度是: ', list.count);
console.log('此时的头结点值是: ', list.getHead());

console.log('从头节点插入 nb: ',  list.insert(0, 'nb'))
console.log('此时链表元素是:',  list.toString());
console.log('此时的头结点值是: ', list.getHead());

console.log('从位置 2 出插入 222: ', list.insert(2, 222))
console.log('此时链表元素是:',  list.toString());
console.log('此时的头结点值是: ', list.getHead());

输出:

PS F:\algorithms> node .\linkedList.js
链表元素是: undefined
链表元素是: 10
链表元素是: 10, 20, 30, 40
链表的长度是:  4
删除的元素是:  10
链表元素是: 20, 30, 40    
链表的长度是:  3
在位置 2 的地方添加 666: true
在位置 0 的地方添加 999: true
此时的头结点值是:  999
链表元素是: 999, 20, 30, 666, 40
链表的长度是:  5
查找 666 的索引位置是:  3
查找 888 的索引位置是:  -1
删除掉元素 666:  666
链表元素是: 999, 20, 30, 40
链表的长度是:  4
此时的头结点值是:  999
从头节点插入 nb:  true
此时链表元素是: nb, 999, 20, 30, 40
此时的头结点值是:  nb
从位置 2 出插入 222:  true
此时链表元素是: nb, 999, 222, 20, 30, 40
此时的头结点值是:  nb

至此, 单链表相关的基本实现就搞定了, 实际应用中基本不用, 但关键在于理解这个过程和训练编程思维.

posted @ 2024-06-03 13:03  致于数据科学家的小陈  阅读(10)  评论(0编辑  收藏  举报