JS数据结构-实现双向链表

186 阅读3分钟

链表

一种数据结构:每个节点保存一个数据,节点之间通过指针引用组成链表

相对于数组

优点:增删链表中的节点简单,只需要修改前后节点的引用,而数组则需要调整操作的节点之后的每个节点的位置

缺点:查询节点比数组慢,需要从链表头开始找,而数组可以通过下标定位

代码实现

  1. 首先得有一个创建节点的方法
  function createNode(data) {
    return {
      prev: null,
      data,
      next: null,
    };
  }
  1. 还得有一个创建链表结构的方法
  function createDoublyLinkedList(...items) {
    const initLinkedList = {
      head: null,
      tail: null,
      length: 0,
      append,
      insert,
      del,
      splice,
      get,
      toString,
    };
    items.forEach((item) => {
      const node = createNode(item);
      if (!initLinkedList.head) {
        initLinkedList.head = node;
      }
      if (initLinkedList.tail) {
        node.prev = initLinkedList.tail;
        initLinkedList.tail.next = node;
      }
      initLinkedList.tail = node;
      initLinkedList.length++;
    });
    return initLinkedList;
  }
  1. 然后是实现链表的基本api,先实现一个获取指定位置节点的方法,提供给其他api用(链表就这缺点,通过索引查找元素麻烦)
  // 查询目标节点,接受负数,-1就是最后一个节点(内部方法)
  function getNode(position) {
    if (Math.abs(position) > this.length || position % 1 !== 0) {
      return false;
    }
    let currentIndex;
    let current;
    if (position >= 0) {
      currentIndex = 0;
      current = this.head;
      while (currentIndex !== position) {
        current = current.next;
        currentIndex++;
      }
    } else {
      currentIndex = this.length - 1;
      current = this.tail;
      while (currentIndex !== this.length + position) {
        current = current.prev;
        currentIndex--;
      }
    }
    return current;
  }
  1. get方法---查询指定位置元素
  // 查询指定位置的数据,接受负数
  function get(position) {
    const current = getNode.call(this, position);
    if (current === false) {
      return false;
    }
    return current.data;
  }
  1. append方法---往链表后面添加元素
  // 添加节点
  function append(data) {
    const node = createNode(data);
    if (this.length === 0) {
      this.head = node;
    } else {
      let current = this.tail;
      current.next = node;
      node.prev = current;
    }
    this.tail = node;
    this.length++;
  }
  1. insert方法---在链表指定位置插入元素
  // 插入节点,接受负数
  function insert(position, data) {
    const node = createNode(data);
    const current = getNode.call(this, position);
    if (current === false) {
      return false;
    }
    // 修改节点指向
    const prevNode = current ? current.prev : this.tail;
    node.prev = prevNode;
    node.next = current;
    if (current) {
      current.prev = node;
    } else {
      this.tail = node;
    }
    if (prevNode) {
      prevNode.next = node;
    } else {
      this.head = node;
    }
    this.length++;
    return true;
  }
  1. del方法---删除指定位置的链表元素
  // 删除节点
  function del(position) {
    // position 等于链表长度的位置没有节点可以删除
    if (position >= this.length) {
      return false;
    }
    const current = getNode.call(this, position);
    if (!current) {
      return false;
    }
    const prevNode = current.prev;
    const nextNode = current.next;
    if (prevNode) {
      prevNode.next = nextNode;
    } else {
      this.head = nextNode;
    }
    if (nextNode) {
      nextNode.prev = prevNode;
    } else {
      this.tail = prevNode;
    }
    this.length--;
    return true;
  }
  1. splice方法---和数组splice方法一样,可以在指定位置删除、替换元素
  // 与数组的splice方法相同,可以删除,替换数据
  function splice(position, count, ...items) {
    let current = getNode.call(this, position);
    // 保存真实删除的节点数
    let delCount = 0;
    // 找到删除count元素后要链接的上下节点
    const prevNode = current ? current.prev : this.tail;
    let nextNode = current;
    /** 删除操作 start */
    if (current) {
      // 删除count个元素,如果有的话
      if (typeof count === "number") {
        while (delCount < count && nextNode) {
          nextNode = nextNode.next;
          delCount++;
        }
      } else {
        // count不是数字,就从position删到最后
        nextNode = null;
        delCount = this.length - position;
      }
    }
    /** 删除操作 end */
    /** 添加操作 start */
    // 把需要添加的数据先组成一个链表
    const sonLinkedList = createDoublyLinkedList(...items);
    // 将子链表连接到主链表
    if (prevNode) {
      prevNode.next = sonLinkedList.head;
      sonLinkedList.head.prev = prevNode;
    } else {
      this.head = sonLinkedList.head;
    }
    if (nextNode) {
      nextNode.prev = sonLinkedList.tail;
      sonLinkedList.tail.next = nextNode;
    } else {
      this.tail = sonLinkedList.tail;
    }
    /** 添加操作 end */
    this.length = this.length - delCount + sonLinkedList.length;
  }
  1. toString方法---打印链表数据
  // 打印链表
  function toString() {
    const dataArr = [];
    let current = this.head;
    while (current) {
      dataArr.push(current.data);
      current = current.next;
    }
    return dataArr.toString(); // 这里暂时没考虑节点data为引用数据类型的tostring展示
  }

这样一个具备基本增删改查的双向链表就完成了

注意事项:节点是链表的节点,元素是节点存的值,也是toString出来的值,使用者操作元素而不会感知节点