JS中链表的基本操作

161 阅读3分钟

单向链表

  • 链表优点:

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

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

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

  • 链表缺点:

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

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

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

单链表常见操作

  • append(element) 向链表尾部添加一个新的项。
  • insert(position, element) 向链表的特定位置插入一个新的项。
  • get(position) 获取对应位置的元素。
  • indexOf(element) 返回元素在链表中的索引。如果链表中没有该元素就返回-1。
  • update(position, element) 修改某个位置的元素。
  • removeAt(position) 从链表的特定位置移除一项。
  • remove(element) 从链表中移除一项。
  • isEmpty() 如果链表中不包含任何元素,返回 true,如果链表长度大于 0 则返回 false。
  • size() 返回链表包含的元素个数,与数组的 length 属性类似。
  • toString() 由于链表项使用了 Node 类,就需要重写继承自 JavaScript 对象默认的 toString 方法,让其只输出元素的值。

单链表的封装

  • 封装节点
export class Node {
  constructor(data) {
    this.data = data;
    this.next = null;
  }
}
  • 封装单链表

    export class LinkedList {
    ​
      constructor() {
        // 初始链表长度为 0
        this.length = 0;
    ​
        // 初始 head 为 null,head 指向链表的第一个节点
        this.head = null;
      }
    
  • 常见操作

    • append

      append(data) {
          // 1、创建新节点
          const newNode = new Node(data);
      ​
          // 2、追加新节点
          if (this.length === 0) {
            // 链表长度为 0 时,即只有 head 的时候
            this.head = newNode;
          } else {
            // 链表长度大于 0 时,在最后面添加新节点
            let currentNode = this.head;
      ​
            // 当 currentNode.next 不为空时,
            // 循序依次找最后一个节点,即节点的 next 为 null 时
            while (currentNode.next !== null) {
              currentNode = currentNode.next;
            }
      ​
            // 最后一个节点的 next 指向新节点
            currentNode.next = newNode;
          }
      ​
          // 3、追加完新节点后,链表长度 + 1
          this.length++;
        }
      ​
      
    • insert

      if (position === 0) {
            // position = 0 的情况
            // 让新节点的 next 指向 原来的第一个节点,即 head
            newNode.next = this.head;
      ​
            // head 赋值为 newNode
            this.head = newNode;
          } else {
            // 0 < position <= length 的情况
      ​
            // 初始化一些变量
            let currentNode = this.head; // 当前节点初始化为 head
            let previousNode = null; // head 的 上一节点为 null
            let index = 0; // head 的 index 为 0
      ​
            // 在 0 ~ position 之间遍历,不断地更新 currentNode 和 previousNode
            // 直到找到要插入的位置
            while (index++ < position) {
              previousNode = currentNode;
              currentNode = currentNode.next;
            }
      ​
            // 在当前节点和当前节点的上一节点之间插入新节点,即它们的改变指向
            newNode.next = currentNode;
            previousNode.next = newNode;
          }
      ​
          // 更新链表长度
          this.length++;
          return newNode;
        }
      
    • 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(data),返回元素的index

      ​
        // indexOf(data) 返回指定 data 的 index,如果没有,返回 -1。
        indexOf(data) {
          let currentNode = this.head;
          let index = 0;
      ​
          while (currentNode) {
            if (currentNode.data === data) {
              return index;
            }
            currentNode = currentNode.next;
            index++;
          }
      ​
          return -1;
        }
      ​
      
    • update(position, data) 修改指定位置节点的 data

      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(position) 删除指定位置的节点,并返回删除的那个节点

      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;
            }
      ​
            // 巧妙之处,让上一节点的 next 指向到当前的节点的 next,相当于删除了当前节点。
            previousNode.next = currentNode.next;
          }
      ​
          // 3、更新链表长度 -1
          this.length--;
      ​
          return currentNode;
        }
      
    • remove(data) 删除指定 data 的节点,并返回删除的那个节点

      remove(data) {
          const index = this.indexOf(data);
          if(index === -1) return;
          return this.removeAt(index);
        }
      ​
      
    • 其他

      // isEmpty() 判断链表是否为空
        isEmpty() {
          return this.length === 0;
        }
      ​
        // size() 获取链表的长度
        size() {
          return this.length;
        }
      
    • toString() 方法

      toString() {
          let currentNode = this.head;
          let result = '';
      ​
          // 遍历所有的节点,拼接为字符串,直到节点为 null
          while (currentNode) {
          result += currentNode.data + ' ';
          currentNode = currentNode.next;
          }
      ​
          return result;
      }
      

双向链表

  • 既可以从头遍历到尾,也可以从尾遍历到头。

  • 链表相连的过程是双向的。实现原理是一个节点既有向前连接的引用,也有一个向后连接的引用。

  • 双向链表可以有效的解决单向链表存在的问题。

  • 双向链表缺点:

    • 每次在插入或删除某个节点时,都需要处理四个引用,而不是两个,实现起来会困难些。
    • 相对于单向链表,所占内存空间更大一些。
    • 但是,相对于双向链表的便利性而言,这些缺点微不足道。

双向链表的封装

  • 封装双向链表

    // 继承单向链表的特性
    import { LinkedList, Node } from '../LinkedList/linkedList';
    ​
    // 双向链表的节点类(继承单向链表的节点类)
    class DoublyNode extends Node {
      constructor(element) {
        // 在父类中初始化data和next
        super(element);
        this.prev = null;
      }
    }
    ​
    // 双向链表类(继承单向链表类)
    export class DoublyLinkedList extends LinkedList {
    ​
      constructor() {
        // 继承父类中的属性和方法
        super();
        this.tail = null;
      }
    ​
    
  • 双向链表常见操作

    • append(element) 往双向链表尾部追加一个新的元素

      append(element) {
      ​
          // 1、创建双向链表节点
          const newNode = new DoublyNode(element);
      ​
          // 2、追加元素
          if (this.head === null) {
            this.head = newNode;
            this.tail = newNode;
          } else {
            // !!跟单向链表不同,不用通过循环找到最后一个节点
            // 巧妙之处
            this.tail.next = newNode;
            newNode.prev = this.tail;
            this.tail = newNode;
          }
      ​
          this.length++;
        }
      ​
      
    • insert(position, data) 插入元素

      insert(position, element) {
          // 1、position 越界判断
          if (position < 0 || position > this.length) return false;
      ​
          // 2、创建新的双向链表节点
          const newNode = new DoublyNode(element);
      ​
          // 3、判断多种插入情况
          if (position === 0) { // 在第 0 个位置插入
      ​
            if (this.head === null) {
              this.head = newNode;
              this.tail = newNode;
            } else {
              //== 巧妙之处:相处腾出 this.head 空间,留个 newNode 来赋值 ==//
              newNode.next = this.head;
              this.head.perv = newNode;
              this.head = newNode;
            }
      ​
          } else if (position === this.length) { // 在最后一个位置插入
      ​
            this.tail.next = newNode;
            newNode.prev = this.tail;
            this.tail = newNode;
          } else { // 在 0 ~ this.length 位置中间插入
      ​
            let targetIndex = 0;
            let currentNode = this.head;
            let previousNode = null;
      ​
            // 找到要插入位置的节点
            while (targetIndex++ < position) {
              previousNode = currentNode;
              currentNode = currentNode.next;
            }
      ​
            // 交换节点信息
            previousNode.next = newNode;
            newNode.prev = previousNode;
      ​
            newNode.next = currentNode;
            currentNode.prev = newNode;
          }
      ​
          this.length++;
      ​
          return true;
        }
      
    • getData() 继承单向链表

      getData(position) {
          return super.getData(position);
        }
      
    • indexOf() 继承单向链表

      indexOf(data) {
          return super.indexOf(data);
        }
      
    • removeAt() 删除指定位置的节点

      removeAt(position) {
          // 1、position 越界判断
          if (position < 0 || position > this.length - 1) return null;
      ​
          // 2、根据不同情况删除元素
          let currentNode = this.head;
          if (position === 0) { // 删除第一个节点的情况
      ​
            if (this.length === 1) { // 链表内只有一个节点的情况
              this.head = null;
              this.tail = null;
            } else { // 链表内有多个节点的情况
              this.head = this.head.next;
              this.head.prev = null;
            }
      ​
          } else if (position === this.length - 1) { // 删除最后一个节点的情况
      ​
            currentNode = this.tail;
            this.tail.prev.next = null;
            this.tail = this.tail.prev;
      ​
          } else { // 删除 0 ~ this.length - 1 里面节点的情况
      ​
            let targetIndex = 0;
            let previousNode = null;
              // 注意循环的条件
            while (targetIndex++ < position) {
              previousNode = currentNode;
              currentNode = currentNode.next;
            }
      ​
            previousNode.next = currentNode.next;
            currentNode.next.perv = previousNode;
      ​
          }
      ​
          this.length--;
          return currentNode.data;
        }
      ​
      
    • update(position, data) 修改指定位置的节点

      update(position, data) {
          // 1、删除 position 位置的节点
          const result = this.removeAt(position);
      ​
          // 2、在 position 位置插入元素
          this.insert(position, data);
          return result;
        }
      
    • 其他

        // remove(data) 删除指定 data 所在的节点(继承单向链表)
        remove(data) {
          return super.remove(data);
        }
      ​
        // isEmpty() 判断链表是否为空
        isEmpty() {
          return super.isEmpty();
        }
      ​
        // size() 获取链表的长度
        size() {
          return super.size();
        }
      ​