JavaScript(ES6)数据结构与算法——链表

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

在学习链表之前我们先了解一下数组的不足(缺点):

  • 要存储多个元素,数组可能是最常用的数据结构。几乎每一种编程语言都有默认实现的数组结构,数组的创建通常需要申请一段连续的内存空间,并且大小是固定的(大多数语言是固定的),所以当数组不能满足容量需求时,需要扩容,一般情况下是申请一个更大的数组,将原来数组中元素复制过去。而且数组开头或中间位置插入数据的成本很高,需要进行大量元素的位移。尽管JavaScript中的Array类可以帮助我们做这些事,但背后的原理依然事这样的。

相对于数组链表的优势:

  • 链表的内存空间不是必须连续的,可以充分利用计算机的内存,实习灵活的内存动态管理,链表不必在创建的时候确定大小,并且大小可以无限制的延申下去,链表在插入和删除数据时,时间复杂度可达到O(1),相对于数组效率会高很多。

  • 但是相对于数组,列表也有一些缺点:链表访问任何一个位置的元素时,都需要从头开始访问,无法跳过第一个元素访问任何一个元素,无法通过下标直接访问元素,需要从头一个个访问,直到找到对应的元素。

什么是链表

在上面已经简单提及链表的结构,我们用图的方式在清晰的了解一下什么是链表结构。

在这里插入图片描述

如上图所示,在链表中,每一个元素都包含两个属性,即 该元素的值item 和 下一个元素next,next指向下一个节点。同时链表中有一个指针 head 指向链表第一个元素,最后一个元素则指向null。

链表中的常见操作

  • 向链表尾部添加一个新的项。append(element)
  • 向链表的特定位置插入一个新的项。insert(position,element)
  • 获取对应位置的元素。get(position)
  • 返回元素在链表中的索引。如果链表中没有该元素则返回-1。indexOf(element)
  • 修改某个位置的元素。update(position ,element)
  • 从列表的特定位置移除一项。removeAt(position)
  • 从链表中移除一项。remove(element)
  • 如果链表中不包含任何元素,返回true,如果链表的长度大于0则返回false。isEmpty()
  • 返回链表包含的元素个数。与数组length属性类似。size()

封装单向链表结构

class Node {
  constructor(element) {
    // 保存元素
    this.element = element;
    // 指向下一个元素
    this.next = null;
  }
}

class LinkedList {
  constructor() {
    this.head = null;
    this.length = 0;
  }

  // - 向链表尾部添加一个新的项。append(element)
  append(element) {
    // 根据element创建Node对象
    const newNode = new Node(element);
    // 追加到最后 判断链表结构head是否为空(为空就表示链表中没有任何节点),将新创建的节点赋值给head,此时表示该链表中已有节点
    if (!this.head) {
      this.head = newNode;
    }
    // 如果head不为空(表示链表中已有节点),最开始定义一个变量将头节点(head)赋值给current(也就是指向第一个节点),
    //利用while循环依次判断是否存在下一个节点,如果不存在,就将这个新创建的额节点赋值给最后一个节点的next属性
    else {
      let current = this.head;
      while (current.next) {
        current = current.next;
      }

      current.next = newNode;
    }
    this.length++;
  }

  // - 向链表的特定位置插入一个新的项。insert(position,element) position表示插入元素的位置
  insert(position, element) {
    // 判断输入的值是否越界
    if (position < 0 || position > this.length) return false;

    // 创建新的节点
    const newNode = new Node(element);

    // 插入元素
    // 判断插入的元素的位置是否在头节点(head)
    if (position === 0) {
      // 插入的节点在第一个就将此时指向头节点的节点赋值给插入节点的next属性
      newNode.next = this.head;
      //之后再将新插入的节点赋值给头节点
      this.head = newNode;
    }
    // 在其他位置插入节点 ,需要将插入位置的新节点前一个节点指向该新节点,再将新节点的next属性指向后一个节点
    else {
      let index = 0;
      let current = this.head;
      // 插入节点的前一个位置
      let previous = null;
      while (index++ < position) {
        previous = current;
        current = current.next;
      }

      previous.next = newNode;
      newNode.next = current;
    }
    this.length++;
    // 表示插入成功
    return true;
  }

  // - 获取对应位置的元素。get(position)
  get(position) {
    if (position < 0 || position > this.length - 1) return null;

    // 查找指定位置的元素时,需要一个一个的往后找,直到找到指定位置将此位置的节点next属性赋值给变量current
    let index = 0;
    let current = this.head;
    while (index++ < position) {
      current = current.next;
    }
    return current.element;
  }

  // - 返回元素在链表中的索引。如果链表中没有该元素则返回-1。indexOf(element)
  indexOf(element) {
    let current = this.head;
    let index = 0;

    while (current) {
      if (current.element === element) {
        return index;
      }
      index++;
      current = current.next;
    }
    return -1;
  }

  // - 修改某个位置的元素。update(position ,element)
  update(position, element) {
    let result = this.removeAt(position);
    this.insert(position, element);
    return result;
  }

  // - 从列表的特定位置移除一项。removeAt(position)
  removeAt(position) {
    if (position < 0 || position > this.length - 1) return null;

    let current = this.head;
    let previous = null;
    let index = 0;
    if (position === 0) {
      this.head = current.next;
    } else {
      while (index++ < position) {
        previous = current;
        current = current.next;
      }
      previous.next = current.next;
    }
    this.length--;
    return current.element;
  }

  // - 从链表中移除一项。remove(element)
  remove(element) {
    const index = this.indexOf(element);
    if (index === -1) return;
    this.removeAt(index);
  }

  // - 如果链表中不包含任何元素,返回true,如果链表的长度大于0则返回false。isEmpty()
  isEmpty() {
    return this.length === 0;
  }

  // - 返回链表包含的元素个数。与数组length属性类似。size()
  size() {
    return this.length;
  }
}

module.exports = new LinkedList();

测试

const linkedList = require("./linkdeList");

// 测试向链表添加节点
linkedList.append("a");
linkedList.append("b");
linkedList.append("c");
linkedList.append("d");
linkedList.append("e");

console.log(linkedList);

// 测试向指定位置插入节点
const res = linkedList.insert("1", "f");

console.log(linkedList, res);

// 测试获取指定位置的元素
console.log(linkedList.get(1));

// 测试通过元素获取下标值{}
console.log(linkedList.indexOf("b"));

// 测试从列表的特定位置移除一项
console.log(linkedList.removeAt(4));
console.log(linkedList);

// 测试修改某个位置的元素
console.log(linkedList.update(1, "www"));
console.log(linkedList);

// 测试从链表中移除一项
console.log(linkedList.remove("a"));
console.log(linkedList);

在这里插入图片描述

认识双向列表

  • 为什么要使用双向链表

单向链表只能从头遍历到尾或者从尾遍历到头,也就是链表相连过程是 单向的,实现的原理是上一个节点中有一个指向下一个的引用。

单向链表有一个明显的缺点,我们可以轻松的拿到下一个节点,但是返回前一个节点是很难的(需要从头在开始遍历),但是在实际开发中,经常会遇到回到上一个节点的情况。所以双向链表可以解决单向链表很难拿到上一个节点的情况。

  • 双向链表实现的原理

双向链表基于单向链表实现,其实就是在每个节点中添加一个指向上一个节点的引用(prev),第一个节点的prev指向的是null,最后一个节点的next指向的也是null(如下图),这样双向链表就可以有效的解决上面提出的问题。

在这里插入图片描述

但是双向链表也有一些缺点,每次在插入或者删除某个节点时,需要处理四个引用,而不是两个,实现起来也比较麻烦。并且相当于单向链表,必然占用的内存空间更大一些。

封装双向链表

根据双向链表的要求,我们只需要继承单向链表来重写添加,插入,根据下标删除三个方法

const { LinkedList, Node } = require("./linkdeList");

class DoublyNode extends Node {
  constructor(element) {
    super(element);
    this.prev = null;
  }
}

class DoublyLinkedList extends LinkedList {
  constructor() {
    super();
    this.tail = null;
  }

  // - 向链表尾部添加一个新的项。append(element)
  append(element) {
    const newNode = new DoublyNode(element);

    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,element)
  insert(position, element) {
    if (position < 0 || position > this.length) return false;

    const newNode = new DoublyNode(element);

    if (position === 0) {
      if (this.head == null) {
        this.head = newNode;
        this.tail = newNode;
      } else {
        newNode.next = this.head;
        this.head.prev = newNode;
        this.head = newNode;
      }
    } else if (position === this.length) {
      this.tail.next = newNode;
      newNode.prev = this.tail;
      this.tail = newNode;
    } else {
      let index = 0;
      let current = this.head;
      let previous = null;

      while (index++ < position) {
        previous = current;
        current = current.next;
      }

      previous.next = newNode;
      newNode.prev = previous;
      newNode.next = current;
      current.prev = newNode;
    }
  }

  // - 获取对应位置的元素。get(position)
  // - 返回元素在链表中的索引。如果链表中没有该元素则返回-1。indexOf(element)
  // - 修改某个位置的元素。update(position ,element)
  // - 从列表的特定位置移除一项。removeAt(position)
  removeAt(position) {
    if (position < 0 || position > this.length - 1) return null;

    let current = 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) {
      current = this.tail;
      this.tail.prev.next = null;
      this.tail = this.tail.prev;
    } else {
      let index = 0;
      let previous = null;
      while (index++ < position) {
        previous = current;
        current = current.next;
      }
      previous.next = current.next;
      current.next.prev = previous;
    }
    return current.element;
  }

  // - 从链表中移除一项。remove(element)
  // - 如果链表中不包含任何元素,返回true,如果链表的长度大于0则返回false。isEmpty()
  // - 返回链表包含的元素个数。与数组length属性类似。size()
}

module.exports = DoublyLinkedList;

测试

const DoublyLinkedList = require("./doubly_linkedList");

const doublyLinkedList = new DoublyLinkedList();

doublyLinkedList.append("a");
doublyLinkedList.append("b");
doublyLinkedList.append("c");
doublyLinkedList.append("d");
doublyLinkedList.append("e");
doublyLinkedList.append("f");

console.log(doublyLinkedList);

doublyLinkedList.insert(1, "g");
console.log(doublyLinkedList);

console.log(doublyLinkedList.get(1));

console.log(doublyLinkedList.indexOf("g"));

console.log(doublyLinkedList.removeAt(1));

console.log(doublyLinkedList.update(0, "nnn"));

console.log(doublyLinkedList.remove("f"));

在这里插入图片描述
在这里插入图片描述


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值