数据结构-链表的生成和应用

前言

看过很多关于链表操作的文章,也很认真的背过,但由于没有应用层面的了解和使用场景的了解,映像并不深刻,基本上过段时间就忘,更不要说深入了解,那面试官拐个弯问你对链表的了解 怎么办,fiber节点之间是如何建立联系的?队列可以怎么实现?node可写流的缓存区域可以怎么实现?我想不通过索引,不移动不相干的元素来实现元素增删操作?所以,特意去看下了链表的相关知识和伪代码的编写,希望正在阅读的你也能从我的总结里有所收获

链表

image.png

是什么

1.线性链式存储结构的数据(链表)常与线性顺序存储结构的数据(数组)做比较

  • 链表:链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的next指向。链表的特点和数组相反:访问元素慢,新增或删除元素快。
  • 数组:数组顺序是由下标决定的,因此访问数组的元素速度很快;但是,往数组添加或删除元素时,需要把数组中的其他元素向前或向后移动,速度比较慢
  • 链表&数组比较:在元素很多,经常要添加或删除元素,但不经常访问元素的场景下,用数组的性能比较低。这种场景下,可以用链表。

2.链表拥有很多结点,每个结点前半部分是数据域,后半部分是指针域(next),指针域指针指向下一个结点

分类

  1. 单向链表:多个节点 上一个节点的next属性指向下一个节点 当没有下一个指向时候 next 指向null

  2. 双向链表:尾部节点 的pre 指向上一个节点 直到head 节点的pre 指向null

  3. 单向循环链表:在单链表的基础上 最后一个节点的next 指向头部 节点

  4. 双向循环链表:结合2 和3的情况下 头部节点的pre 指向尾部节点,尾部节点的next指向头部节点

  5. 环形链表:表中某一个结点的指针域next指向上一个结点,形成一个环。

代码实现

生成链表节点

/**
 * element 存储的数据
 * next 存储的下一个 数据的指针
 */
class Node {
  constructor(element, next) {
    this.element = element;
    this.next = next;
  }
}
复制代码

生成链表

初始化表头数据constructor()
/**
 * 存储数据
 * add 新增
 * remove 移除
 * reverse 反转
 */
class LinkedList {
  constructor() {
    this.head = null;
    this.size = 0;//链表长度
  }
 }
 module.exports = LinkedList;
复制代码
新增节点add()

思路:

  1. 添加的时候要考虑是否是第一次新增,
  • 是:将head 指向 新节点, next指向null(最初的head的next)

  • 否:上一个节点的next指向 自己

/**
 * @description 链表新增节点
 * index:插入位置
 * element 插入元素
 * 只有一个参数时候 默认是需要新增的元素,把index赋值给elemnt
 */
add(index, element) {
    if (arguments.length === 1) {
      //参数长度为1时 默认为是需要新增的元素,把index赋值给elemnt
      // index处理成this.size
      element = index;
      index = this.size;
    }
    let head = this.head;
    if (this.size === 0) {
      // 如果 是第一次 新建 将head 指向 新节点, next指向null(最初的head的next)
      this.head = new Node(element, head);
    } else {
      //  获取前一个节点this._node()待实现
      let preNode = this._node(index - 1); //索引从0开始 始终比长度小1
      if (!preNode) return;
      //   让当前新节点的next指向上一个节点的next,上一个节点的next指向 自己
      preNode.next = new Node(element, preNode.next);
    }

    this.size++;
  }
复制代码
获取前一个节点_node()
//依据index通过遍历 获取上一个节点 
_node(index) {
    let current = this.head;
    for (let i = 0; i < index; i++) {
      //遍历直到i=的index
      current = current.next;
    }
    return current;
  }
复制代码
删除节点remove()

思路:找到要删除的节点的上一个节点,让他的next指向下一个节点(就是当前索引指向的节点)的下一个节点

remove(index) {
    let removeNode;
    if (index === 0) {
      removeNode = this.head;
      if (removeNode !== null) {
        this.head = this.head.next;
        this.size--;
      }
    } else {
      let preNode = this._node(index - 1);
      preNode.next = preNode.next.next;
      this.size--;
    }

    return removeNode;
  }
复制代码
链表反转 reverse()
  • 递归实现
   reverse() {
    // 递归 但耗内存
    function r(head) {
      if (head === null || head.next === null) {
        return head;
      }
      let newHead = r(head.next);
      head.next.next = head;
      head.next = null;
      return newHead;
    }
    return r(this.head);
  }
复制代码
  • while循环实现
 reverse() {
    let head = this.head;
    if (head === null || head.next === null) {
      return head;
    }
    let newHead = null;
    while (head !== null) {
      let temp = head.next;
      head.next = newHead;
      newHead = head;
      head = temp;
    }
    return newHead;
  }
复制代码
  • 小结:非要讨论两种实现方式的优劣 递归会稍耗内存些,往往链表都用于底层代码的实现,那么需要更多考虑性能问题,所以while循环更合适些

实例测试

let li = new LinkedList();
li.add(1);
li.add(2);
li.add(3);
li.add(4);
li.add(1, 200);

console.dir(li, { depth: 100 });
li.remove(2)
console.dir(li, { depth: 100 });
console.dir(li.reverse(), { depth: 100 });
复制代码
  • 测试效果
  1. 连续add后的打印链表

image.png
2.删除节点2后的打印效果,size为4 节点2已经不存在

image.png
3.反转链表

image.png

场景应用–生成队列

const LinkedList = require("链表的文件位置");

// 用链表 生成队列
class Queue {
  constructor() {
    // 有参数 new LinkedList(),没参数 用 new LinkedList
    this.ll = new LinkedList();
  }
  poll() {
    //删除头部
    let removeNode = this.ll.remove(0);
    return removeNode && removeNode.element;
  }
  offer(element) {
    //添加 尾部
    this.ll.add(element);
  }
}
module.exports = Queue;
复制代码

题外话:new 构造函数后面有()和没()的区别———-(留个坑,后面写一个new的原理)

有实参有括号就会重载构造函数,用户自定义传入的参数会赋给构造函数做实例自己的初始化默认值

总结

链表的常用使用场景包括(我百度的)

  1. LRU 缓存淘汰算法
  2. 解决哈希冲突的哈希表
  3. 各类缓冲池 和 栈,队列 的实现

目前位置我看到的代码中存在的链表的就是react 源码中fiber节点的实现
下一篇文章我将分享 以链表实现的队列为基础去实现node中的可写流

最后如果觉得本文有帮助 记得点赞三连哦 十分感谢

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享