【从蛋壳到满天飞】JS 数据结构解析和算法实现-链表

思惟导图

前言

【从蛋壳到满天飞】JS 数据结构解析和算法实现,所有文章大概的内容以下: Arrays(数组)、Stacks(栈)、Queues(队列)、LinkedList(链表)、Recursion(递归思想)、BinarySearchTree(二分搜索树)、Set(集合)、Map(映射)、Heap(堆)、PriorityQueue(优先队列)、SegmentTree(线段树)、Trie(字典树)、UnionFind(并查集)、AVLTree(AVL 平衡树)、RedBlackTree(红黑平衡树)、HashTable(哈希表)html

源代码有三个:ES6(单个单个的 class 类型的 js 文件) | JS + HTML(一个 js 配合一个 html)| JAVA (一个一个的工程)node

所有源代码已上传 github,点击我吧,光看文章可以掌握两成,动手敲代码、动脑思考、画图才能够掌握八成。git

本文章适合 对数据结构想了解而且感兴趣的人群,文章风格一如既往如此,就以为手机上看起来比较方便,这样显得比较有条理,整理这些笔记加源码,时间跨度也算将近半年时间了,但愿对想学习数据结构的人或者正在学习数据结构的人群有帮助。github

链表

  1. 链表是最基础的动态数据结构
  2. 链表是很是重要的线性数据结构
    1. 如下三种,底层都是依托静态数组,靠 resize 解决固定容量问题。
    2. 动态数组:所谓动态,是从用户的角度上来看的。
    3. 队列
  3. 链表是真正的动态数据结构
    1. 它是数据结构中的一个重点,
    2. 也有多是一个难点,
    3. 它是最简单的一种动态数据结构,
    4. 其它更高级的动态数据结构有 二分搜索树、Trie、
    5. 平衡二叉树、AVL、红黑树等等,
    6. 熟悉了最简单的动态数据结构,
    7. 那么对于更高级的也会比较容易掌握了。
  4. 对于链表来讲它涉及到了计算机领域一个很是重要的概念
    1. 更深刻的理解引用(或者指针),
    2. 这个概念和内存相关,
    3. 在 JS 里面不须要手动的管理内存,
    4. 可是对链表这种数据结构更加深刻的理解,
    5. 可让你对 引用、指针甚至计算机系统中
    6. 和内存管理相关不少话题有更加深刻的认识。
  5. 链表自己也是有它很是清晰的递归结构的,
    1. 只不过因为链表这种数据结构它自己是一种线性的数据结构,
    2. 因此依然能够很是容易的使用循环的方式来对链表进行操做,
    3. 可是链表自己因为它天生也是具备这种递归结构的性质,
    4. 因此它也能让你更加深刻的理解递归机制相应的这种数据结构,
    5. 在学习更加复杂的数据结构的时候,
    6. 对递归这种逻辑机制深刻的理解是必不可缺的。
  6. 链表这种数据结构自己就具备功能性
    1. 它能够用来组成更加复杂的数据结构,
    2. 好比 图结构、hash 表、栈(链表版)、队列(链表版),
    3. 因此他能够辅助组成其它数据结构。

什么是链表

  1. 数据存储在“节点”(Node)中
    1. 把数据存在一种单独的结构中,
    2. 这个结构一般管它叫作节点,
    3. 对于链表节点来讲他一般只有两部分,
    4. 一部分就是存储真正的数据,
    5. 另外一部分存储的是当前节点的下一个节点,
    6. 链表其实就像一个火车,每个节点都是一节车箱,
    7. 在车箱中存储数据,而车箱和车箱之间还会进行链接,
    8. 以使得这些数据是整合在一块儿的,
    9. 这样用户能够方便的在全部的这些数据上进行查询等等其它的操做,
    10. 数据与数据之间链接就是用下面的这个 next 来完成的
    class Node {
       e; //Element
       next; //Node
    }
    复制代码
  2. 链表的优势
    1. 真正的动态,不须要处理固定容量的问题,
    2. 它不像静态数组那样一会儿 new 出来一片空间,
    3. 也根本不须要考虑这个空间是否是不够用,
    4. 也根本不用去考虑这个空间是否是开多了。
  3. 对于链表来讲,你须要多少个数据。
    1. 你就能够生成多少个节点,
    2. 把它们挂接起来了,这就是所谓的动态。
  4. 链表的缺点
    1. 和数组相比,它丧失了随机访问的能力。
    2. 不能像数组那样给一个索引
    3. 就能直接访问对应索引位置的元素,
    4. 这是由于从底层机制上数组所开辟的空间
    5. 在内存里是连续分布的,
    6. 因此才可以直接去向索引对应的偏移
    7. 计算出相应的数据所存储的内存地址,
    8. 而后就可以直接用O(1)的复杂度取出这个元素,
    9. 可是链表不一样,链表因为是靠 next 一层一层链接的,
    10. 因此在计算机的底层,每个节点他所在的内存的位置是不一样的,
    11. 只可以靠这个 next 来一层一层的找到这个元素,
    12. 这就是链表最大的缺点。

数组和链表的对比

  1. 数组面试

    1. 数组最好永远索引有语意的状况,如 scores[2]
    2. 最大的优势:支持快速查询
  2. 链表算法

    1. 链表不适合用于索引有语意的状况。
    2. 最大的优势:动态存储。
  3. 对比数组

    1. 数组也能够没有语意,并非全部的时候索引是有语意的,
    2. 也并非全部有语意的这样的一个标志就适合作索引,如身份证号,
    3. 将一个静态数组改变为一个动态数组,
    4. 就是在应付不方便使用索引的时候有关数据存储的问题,
    5. 对于这样的存储数据的需求使用链表是更合适的,
    6. 由于链表最大的优势是动态存储。
  4. 要清楚何时使用数组这样的静态数据结构,数据结构

    1. 何时使用链表这类的动态数据结构。
  5. 简单的代码示例MyLinkedListide

    class MyLinkedListNode {
       constructor(element = null, next = null) {
          this.element = element;
          this.next = next;
       }
    
       // toString 2018-10-20-jwl
       toString() {
          return this.element.toString();
       }
    }
    class MyLinkedList {
       constructor() {}
    }
    复制代码

在链表中进行添加、插入操做

  1. 链表是经过节点来装载元素
    1. 而且节点和节点之间会进行链接。
    2. 若是想访问这条链表中全部的节点,
    3. 那么就必须把链表的头给存储起来,
    4. 一般这个链表的头叫作的 head,
    5. 应该说在MyLinkedList
    6. 应该有一个变量指向链表中的第一个节点。
  2. 给自定义数组添加元素是从数组尾部开始添加,
    1. 给链表添加元素是从数组头部开始添加,
    2. 由于自定义数组有维护一个 size,
    3. 这个 size 指向的是下一个待添加元素的位置,
    4. 因此你直接将 size 作索引直接赋值便可,
    5. 有 size 这个变量在跟踪数组的尾巴,
    6. 而对于链表来讲 有设置一个链表的头这样的一个变量,
    7. 也就是说有一个变量在跟踪链表的头,
    8. 并无一个变量去跟踪链表的尾巴,
    9. 因此在链表头部添加元素是很是方便。
  3. 添加操做原理
    1. 将一个新的元素挂接到链表头部,
    2. 同时不破坏如今链表的这个结构,
    3. 添加一个新元素 666,它的名字叫 node,
    4. 就只须要使用 node.next = head
    5. 也就是将新添加的元素的 next 指向链表的头,
    6. 这个时候新添加的元素也成为新的链表头,
    7. 也就是head = node
    8. 这样 head 就会指向新的元素 666,也就是 node 这个节点,
    9. 如此一来就完成了将 node 这个节点插入了整个链表的头部,
    10. node 这个变量是在一个函数中声明的,
    11. 因此函数结束以后这个变量的做用域也就结束了,
    12. 链表的 head 也等于 node,
    13. 链表中就新增了一个新元素。
  4. 在链表头部添加元素很是简单,
    1. 只须要建立节点,让节点的 next 指向 head,
    2. 而后让 head 从新指向这个节点,也就是让 head 变成新的元素,
    3. 由于链表是从头部添加元素的。
  5. 在链表中间添加元素,
    1. 定义一个 prev 的变量指向中间元素的前一个元素,
    2. 而后让新增长的元素这样,node.next = prev.next
    3. 以后这样,prev.next = node
    4. 这样一来就在 prev 这个元素和本来 prev 的下一个元素之间
    5. 插入了一个新元素,叫作 node,
    6. 整个过程就是将 prev 的 next(下一个元素)的引用
    7. 传递给新元素的 next(下一个元素),
    8. 而后将 prev 的 next(下一个元素)变动为这个新元素,
    9. 最后就将新元素插入到中间了。
    10. 这个过程的关键是找到待添加的节点的前一个节点,
    11. 这个就是 prev 变量要作的事情。
  6. 在链表的操做中不少时候顺序很是重要,
    1. 如在链表中插入一个元素,在指定的元素后面插入一个元素,
    2. 而且不影响整个链表结构,和在链表中间添加元素同样,
    3. 把本来的node.next = prev.nextprev.next = node
    4. 的顺序变一下,prev.next = node在前,
    5. node.next = prev.next 在后,这样一来逻辑就不成立了,
    6. 链表不只断开了,并且新节点的 next 指向的是本身,死循环了,
    7. 因此同样的代码,顺序颠倒了,获得的结果就是错误的。
  7. 在链表的 index(0-based)位置添加元素 e
    1. 经过索引来操做链表,在链表中不是一个经常使用的操做,
    2. 由于在选择使用链表的时候一般就选择不使用索引了,
    3. 可是这个需求在面试等一些考题中出现的几率很大,
    4. 因此他的主要做用更多的是一个练习。

学习方式

  1. 若是刚接触链表,对链表不熟悉,
    1. 而后不少这种操做在脑子里不能很是快的反应出来,
    2. 不清楚他的意义是什么,那你可使用纸笔来稍微画一下,
    3. 每一步程序逻辑的执行意味着当前这个链表变成了什么样子,
    4. 这个过程可以帮助你更加深刻的理解程序,
    5. 包括在调试的时候来看你的程序到底为何出了错误时,
    6. 很是很是有帮助的,不要犯懒,为了更加深刻的理解你的程序,
    7. 不能只盯着你的代码去想,必定要写写画画,
    8. 必要的时候甚至根据你的程序进行具体的调试,
    9. 这些都是很是重要很是有帮助的。

代码示例 (class: MyLinkedList)

  1. MyLinkedList函数

    class MyLinkedListNode {
       constructor(element = null, next = null) {
          this.element = element;
          this.next = next;
       }
    
       // toString 2018-10-20-jwl
       toString() {
          return this.element.toString();
       }
    }
    
    class MyLinkedList {
       constructor() {
          this.head = null;
          this.size = 0;
       }
    
       // 获取链表中实际的节点个数
       getSise() {
          return this.size;
       }
    
       // 判断链表是否为空
       isEmpty() {
          return this.size === 0;
       }
    
       // 在链表头添加节点
       addFirst(element) {
          let node = new MyLinkedListNode(element, null);
          node.next = this.head;
          this.head = node;
          this.size++;
       }
    
       // 在链表指定索引处插入节点
       insert(index, element) {
          if (index < 0 || index > this.size) {
             throw new Error('add error. index < 0 or index > size');
          }
    
          if (index === 0) {
             this.addFirst(element);
          } else {
             // 第一个prev就是head
             let prev = this.head;
    
             // 变量i(索引)之因此要从 1 开始,由于索引为0的那个节点就是head,循环就不须要从0开始了,
             // 小于index是由于要找到指定索引位置的前一个节点
             // 循环是由于 要继续找到指定索引处的节点的前一个节点
             for (var i = 1; i < index; i++) {
                // 不停的切换引用,直到找到对应索引处节点的下一个节点
                prev = prev.next;
             }
    
             let node = new MyLinkedListNode(element, null);
             node.next = prev.next;
             prev.next = node;
             this.size++;
          }
       }
    
       // 扩展:在链表最后一个节点的位置添加节点
       addLast(element) {
          this.insert(this.size, element);
       }
    }
    复制代码

给链表设立虚拟头节点

  1. 在链表中进行指定索引处插入元素时
    1. 对链表头插入元素与对链表其它位置插入元素的逻辑有区别,
    2. 因此就致使每次操做都须要对索引进行判断,
    3. 若是你是在索引 0 的位置插入元素,那么就没有 prev(前一个元素),
    4. 天然就不可使用node.next = prev.nextprev.next = node了,
    5. 那时候你能够直接使用node.next = headhead = node
    6. 不过已经有了向链表头添加元素的方法了,
    7. 因此向头部插入元素时根本就不须要这么作,
    8. 这时候能够经过给链表设立虚拟头节点来解决这个问题。
  2. 为何对链表头插入元素那么特殊?
    1. 由于插入元素时,必须要找到该索引位置的节点以前的一个节点(prev),
    2. 而链表头(head)以前并无任何节点,因此逻辑上就会特殊一些。
    3. 不过在链表的具体实现中有一个很是经常使用的技巧,
    4. 能够把链表头的这种特殊的操做与其它的操做一块儿统一块儿来,
    5. 其实这个方法的想法很简单,既然它没有链表头以前的节点,
    6. 那就本身造一个链表头以前的节点,
    7. 这个链表头以前的节点不会用来存储任何元素,存储的数据为空,
    8. 这个空节点就称之为整个链表头的 head,也能够叫它 dummyHead,
    9. 由于它就是一个假的 head,它也是为链表设立的虚拟头节点,
    10. 这样一来 链表的第一个元素就是 dummyHead 的 next 所对应的那个节点,
    11. 由于 dummyHead 这个位置的元素是根本不存在的,
    12. 对用户来说也是根本没有意义的,
    13. 这只是为了编写逻辑方便而出现的一个虚拟的头节点,
    14. 因此一个这样的内部机制对用户来讲也是不可见的,
    15. 用户不知道链表中有没有虚拟的头节点,
    16. 这和自定义的循环队列有点像,自定义循环队列中有一个不可用的空间,
    17. 有意识地去浪费一个空间,为了编写逻辑更加的方便,
    18. 从而也让性能在总体上获得了提高。
  3. 有了 dummyHead 以后就不须要处理头节点这个特殊的操做
    1. 由于当你在头部插入元素时,能够这样
    2. node.next = dummyHead.nextdummyHead.next = node
    3. 这样你就解决了每次插入时都要进行一次逻辑判断了,
    4. 这样一来就说明了链表中全部的节点都有前一个节点了,
    5. 在初始的时候 dummyHead 指向的是索引为 0 的元素前一个位置的节点。
  4. 链表操做的实际原理
    1. 在没有使用虚拟头节点以前,一直都是在链表头 head 这个地方不停的添加节点,
    2. 而后将 head 这个变量不停的指向新添加的节点 node.next = head.next;head = node;
    3. 在使用了虚拟头节点以后,
    4. 一直是在链表虚拟头 dummyHead 这个地方后面一个位置不停的插入新节点,
    5. dummyHead 从未变更过,永远在链表的第一个位置,
    6. node.next = dummyHead.next; dummyHead.next = node;
    7. 可是dummyHead.next才是链表中第一个实际记录的节点,
    8. 用户会不知道虚拟节点的存在,由于 dummyHead 并不算在链表的实际节点中,
    9. 也就是 size 中不会维护 dummyHead,dummyHead 的存在是为了维护一致的插入操做。

代码示例 (class: MyLinkedList)

  1. MyLinkedList

    class MyLinkedListNode {
       constructor(element = null, next = null) {
          this.element = element;
          this.next = next;
       }
    
       // toString 2018-10-20-jwl
       toString() {
          return this.element.toString();
       }
    }
    
    class MyLinkedList {
       constructor() {
          this.dummyHead = new MyLinkedListNode(null, null);
          this.size = 0;
       }
    
       // 获取链表中实际的节点个数
       getSise() {
          return this.size;
       }
    
       // 判断链表是否为空
       isEmpty() {
          return this.size === 0;
       }
    
       // 在链表头添加节点
       addFirst(element) {
          // let node = new MyLinkedListNode(element, null);
          // node.next = this.head;
          // this.head = node;
          // this.size ++;
    
          // 改用虚拟头节点
          insert(0, element);
       }
    
       // 在链表指定索引处插入节点
       insert(index, element) {
          if (index < 0 || index > this.size) {
             throw new Error('add error. index < 0 or index > size');
          }
    
          // 第一个prev就是dummyHead
          let prev = this.dummyHead;
          // 以前变量i(索引)之因此要从 1 开始,由于索引为0的那个节点就是head,循环就不须要从0开始了,
          // 如今索引之因此要从 0 开始, 由于初始化时 多增长了一个虚拟的头节点
          // (由于这个索引为0的节点并非dummyHead,dummyHead这个节点并不记录为链表中的实际节点),
          // 小于index是由于要找到指定索引位置的前一个节点
          // 循环是由于 要继续找到指定索引处的节点的前一个节点
          for (var i = 0; i < index; i++) {
             // 不停的切换引用,直到找到对应索引处节点的下一个节点
             prev = prev.next;
          }
    
          let node = new MyLinkedListNode(element, null);
          node.next = prev.next;
          prev.next = node;
          this.size++;
       }
    
       // 扩展:在链表最后一个节点的位置添加节点
       addLast(element) {
          this.insert(this.size, element);
       }
    }
    复制代码

在链表中进行遍历、查询、修改操做

  1. 若是要找指定索引元素的前一个节点
    1. 那么就要从dummyHead开始遍历,
    2. 若是要找指定索引元素的那个节点,
    3. 那么就要从dummyHead.next开始遍历。

代码示例(class: MyLinkedList, class: Main)

  1. MyLinkedList

    class MyLinkedListNode {
       constructor(element = null, next = null) {
          this.element = element;
          this.next = next;
       }
    
       // toString 2018-10-20-jwl
       toString() {
          return this.element.toString();
       }
    }
    
    class MyLinkedList {
       constructor() {
          this.dummyHead = new MyLinkedListNode(null, null);
          this.size = 0;
       }
    
       // 获取链表中实际的节点个数
       getSize() {
          return this.size;
       }
    
       // 判断链表是否为空
       isEmpty() {
          return this.size === 0;
       }
    
       // 在链表头添加节点
       addFirst(element) {
          // let node = new MyLinkedListNode(element, null);
          // node.next = this.head;
          // this.head = node;
          // this.size ++;
    
          // 改用虚拟头节点
          this.insert(0, element);
       }
    
       // 在链表指定索引处插入节点
       insert(index, element) {
          if (index < 0 || index > this.size) {
             throw new Error('add error. index < 0 or index > size');
          }
    
          // 第一个prev就是dummyHead
          let prev = this.dummyHead;
          // 以前变量i(索引)之因此要从 1 开始,由于索引为0的那个节点就是head,循环就不须要从0开始了,
          // 如今索引之因此要从 0 开始, 由于初始化时 多增长了一个虚拟的头节点
          // (由于这个索引为0的节点并非dummyHead,dummyHead这个节点并不记录为链表中的实际节点),
          // 小于index是由于要找到指定索引位置的前一个节点
          // 循环是由于 要继续找到指定索引处的节点的前一个节点
          for (var i = 0; i < index; i++) {
             // 不停的切换引用,直到找到对应索引处节点的下一个节点
             prev = prev.next;
          }
    
          let node = new MyLinkedListNode(element, null);
          node.next = prev.next;
          prev.next = node;
          this.size++;
       }
    
       // 扩展:在链表最后一个节点的位置添加节点
       addLast(element) {
          this.insert(this.size, element);
       }
    
       // 获取指定索引位置的元素
       get(index) {
          // 判断索引合法性
          if (index < 0 || index >= this.size) {
             throw new Error('get error. index < 0 or index >= size');
          }
    
          // 若是你要找指定索引节点的前一个节点 就使用dummyHead
          // 若是你要找到指定索引节点 就使用dummyHead.next
          // 由于duumyHead并非第一个节点,由于它是一个虚拟节点,
          // dummyHead.next才是真正被记录的第一个节点。
          let node = this.dummyHead.next;
          for (var i = 0; i < index; i++) {
             node = node.next;
          }
    
          return node.element;
       }
    
       // 获取头节点的元素
       getFirst() {
          return this.get(0);
       }
    
       // 获取尾节点的元素
       getLast() {
          return this.get(this.size - 1);
       }
    
       // 设置指定索引位置的元素值
       set(index, element) {
          // 判断索引合法性
          if (index < 0 || index >= this.size) {
             throw new Error('get error. index < 0 or index >= size');
          }
    
          // 从第一个真正被记录的节点开始,从0开始
          let node = this.dummyHead.next;
    
          // 索引为 0 时,实际上切换到的节点 它的索引为 1
          // i < index ,当索引为 index-1 时, 实际上切换到的节点 它的索引为index
          for (let i = 0; i < index; i++) {
             // 每一次切换 都只是改变引用
             // 不的在链表中找下一个节点
             node = node.next;
          }
    
          node.element = element;
       }
    
       // 全部节点中是否有包含该元素
       contains(element) {
          let node = this.dummyHead;
    
          while (node.next !== null) {
             if (node.next.element === element) return true;
             // 不停的向下切换
             node = node.next;
          }
    
          return false;
       }
    
       // 输出链表中的信息
       // @Override toString 2018-10-21-jwl
       toString() {
          let arrInfo = `LinkedList: size = ${this.size},\n`;
          arrInfo += `data = front [`;
          let node = this.dummyHead.next;
          while (node.next !== null) {
             arrInfo += `${node.element}->`;
             node = node.next;
          }
          arrInfo += 'NULL] tail';
    
          // 在页面上展现
          document.body.innerHTML += `${arrInfo}<br /><br /> `;
    
          return arrInfo;
       }
    }
    复制代码
  2. Main

    class Main {
       constructor () {
          /this.alterLine("MyLinkedList Area");
          let mylinkedList = new MyLinkedList();
    
          for (let i = 1; i <= 5 ; i++) {
             mylinkedList.addFirst(i);
             console.log(mylinkedList.toString());
          }
          mylinkedList.insert(2, 88888);
          console.log(mylinkedList.toString());
       }
    
       // 将内容显示在页面上
       show (content) {
          document.body.innerHTML += `${content}<br /><br />`;
       }
    
       // 展现分割线
       alterLine (title) {
          let line = `--------------------${title}----------------------`
          console.log(line);
          document.body.innerHTML += `${line}<br /><br />`;
       }
    }
    复制代码

在链表中进行删除操做

  1. 链表元素的删除
    1. 假设要删除索引为 2 位置的元素
    2. 就是要索引为 2 位置的元素的前一个元素,
    3. 还要索引为 2 位置的元素的后一个元素,
    4. 让前一个元素的 next 等于后一个元素,
    5. 也就是前一个元素的 next 等于索引为 2 的元素的 next。
    6. 也就是让 prev 指向待删除的那个节点的前一个节点,
    7. prev 这个节点的 next 就是要删除的那个节点 delNode,
    8. 而后让prev.next = delNode.next
    9. 这样就让待删除的那个节点的前一个节点的 next,
    10. 也就是直接指向了待删除的那个节点的后一个节点了,
    11. 而后让delNode.next = null 就完成了删除,
    12. 千万不要这样delNode = delNode.next
    13. 这个并非删除,而是将待删除节点的引用指向了下一个节点,
    14. 经过设置为 null 才是真正的与当前链表脱离关系。

代码示例(class: MyLinkedList, class: Main)

  1. MyLinkedList

    class MyLinkedListNode {
       constructor(element = null, next = null) {
          this.element = element;
          this.next = next;
       }
    
       // toString 2018-10-20-jwl
       toString() {
          return this.element.toString();
       }
    }
    
    class MyLinkedList {
       constructor() {
          this.dummyHead = new MyLinkedListNode(null, null);
          this.size = 0;
       }
    
       // 获取链表中实际的节点个数
       getSize() {
          return this.size;
       }
    
       // 判断链表是否为空
       isEmpty() {
          return this.size === 0;
       }
    
       // 在链表头添加节点
       addFirst(element) {
          // let node = new MyLinkedListNode(element, null);
          // node.next = this.head;
          // this.head = node;
          // this.size ++;
    
          // 改用虚拟头节点
          this.insert(0, element);
       }
    
       // 在链表指定索引处插入节点
       insert(index, element) {
          if (index < 0 || index > this.size) {
             throw new Error('add error. index < 0 or index > size');
          }
    
          // 第一个prev就是dummyHead
          let prev = this.dummyHead;
          // 以前变量i(索引)之因此要从 1 开始,由于索引为0的那个节点就是head,循环就不须要从0开始了,
          // 如今索引之因此要从 0 开始, 由于初始化时 多增长了一个虚拟的头节点
          // (由于这个索引为0的节点并非dummyHead,dummyHead这个节点并不记录为链表中的实际节点),
          // 小于index是由于要找到指定索引位置的前一个节点
          // 循环是由于 要继续找到指定索引处的节点的前一个节点
          for (var i = 0; i < index; i++) {
             // 不停的切换引用,直到找到对应索引处节点的下一个节点
             prev = prev.next;
          }
    
          let node = new MyLinkedListNode(element, null);
          node.next = prev.next;
          prev.next = node;
          this.size++;
       }
    
       // 扩展:在链表最后一个节点的位置添加节点
       addLast(element) {
          this.insert(this.size, element);
       }
    
       // 获取指定索引位置的元素
       get(index) {
          // 判断索引合法性
          if (index < 0 || index >= this.size) {
             throw new Error('get error. index < 0 or index >= size');
          }
    
          // 若是你要找指定索引节点的前一个节点 就使用dummyHead
          // 若是你要找到指定索引节点 就使用dummyHead.next
          // 由于duumyHead并非第一个节点,由于它是一个虚拟节点,
          // dummyHead.next才是真正被记录的第一个节点。
          let node = this.dummyHead.next;
          for (var i = 0; i < index; i++) {
             node = node.next;
          }
    
          return node.element;
       }
    
       // 获取头节点的元素
       getFirst() {
          return this.get(0);
       }
    
       // 获取尾节点的元素
       getLast() {
          return this.get(this.size - 1);
       }
    
       // 设置指定索引位置的元素值
       set(index, element) {
          // 判断索引合法性
          if (index < 0 || index >= this.size) {
             throw new Error('get error. index < 0 or index >= size');
          }
    
          // 从第一个真正被记录的节点开始,从0开始
          let node = this.dummyHead.next;
    
          // 索引为 0 时,实际上切换到的节点 它的索引为 1
          // i < index ,当索引为 index-1 时, 实际上切换到的节点 它的索引为index
          for (let i = 0; i < index; i++) {
             // 每一次切换 都只是改变引用
             // 不的在链表中找下一个节点
             node = node.next;
          }
    
          node.element = element;
       }
    
       // 全部节点中是否有包含该元素
       contains(element) {
          let node = this.dummyHead;
    
          while (node.next !== null) {
             if (node.next.element === element) return true;
             // 不停的向下切换
             node = node.next;
          }
    
          return false;
       }
    
       // 删除指定索引位置的节点
       remove(index) {
          // 验证索引的合法性
          if (index < 0 || index >= this.size) {
             throw new Error('remove error. index < 0 or index > this.size');
          }
    
          let node = this.dummyHead;
          for (let i = 0; i < index; i++) {
             node = node.next;
          }
    
          // 待删除的节点
          let delNode = node.next;
          // 给待删除那个节点的前一个的节点的next引用替换为
          // 但删除的这个节点的next
          node.next = delNode.next;
          // 或者这样也行
          // node.next = node.next.next;
    
          // 临时存储待删除的那个节点里的元素
          let element = delNode.element;
          // 清空 待删除的节点
          delNode = null;
          this.size--;
    
          return element;
       }
    
       // 扩展:移除链表头的元素
       removeFirst() {
          return this.remove(0);
       }
    
       // 扩展:移除链表尾部的元素
       removeLast() {
          return this.remove(this.size - 1);
       }
    
       // 输出链表中的信息
       // @Override toString 2018-10-21-jwl
       toString() {
          let arrInfo = `LinkedList: size = ${this.size},\n`;
          arrInfo += `data = front [`;
          let node = this.dummyHead.next;
          while (node.next !== null) {
             arrInfo += `${node.element}->`;
             node = node.next;
          }
          arrInfo += 'NULL] tail';
    
          // 在页面上展现
          document.body.innerHTML += `${arrInfo}<br /><br /> `;
    
          return arrInfo;
       }
    }
    复制代码
  2. Main

    class Main {
       constructor() {
          this.alterLine('MyLinkedList Area');
          let mylinkedList = new MyLinkedList();
    
          for (let i = 1; i <= 5; i++) {
             mylinkedList.addFirst(i);
             console.log(mylinkedList.toString());
          }
          mylinkedList.insert(2, 88888);
          console.log(mylinkedList.toString());
    
          mylinkedList.remove(2);
          console.log(mylinkedList.toString());
    
          mylinkedList.removeFirst();
          console.log(mylinkedList.toString());
    
          mylinkedList.removeLast();
          console.log(mylinkedList.toString());
       }
    
       // 将内容显示在页面上
       show(content) {
          document.body.innerHTML += `${content}<br /><br />`;
       }
    
       // 展现分割线
       alterLine(title) {
          let line = `--------------------${title}----------------------`;
          console.log(line);
          document.body.innerHTML += `${line}<br /><br />`;
       }
    }
    复制代码

链表的时间复杂度分析

  1. 增:O(n):在只对链表头进行操做时为O(1)
  2. 删:O(n):在只对链表头进行操做时为O(1)
  3. 改:O(n)
  4. 查:O(n):只查链表头的元素时为O(1)
  5. 链表增删改查的时间复杂度
    1. 总体上要比数组增删改查的时间复杂度要差,
    2. 由于数组有一个优点,
    3. 你有索引的话你就能够快速访问,
    4. 而链表没有这样的优点
    5. 可是链表的优点是,
    6. 若是你只对链表头进行增删的操做,
    7. 那么它的时间复杂度是 O(1)级别的,
    8. 并且它总体是动态的,因此不会大量的浪费内存空间。 6.链表适合作的事情
    9. 不去修改、也不去查任意的元素,
    10. 就算查,也只能查链表头的元素,
    11. 查的时候,只有那样才是 O(1)级别的时间复杂度,
    12. 而且增长删除的时候只对链表头进行操做,
    13. 只有在这种时候才会和数组总体的时间复杂度是同样的,
    14. 不过链表总体是动态的,不会去大量的浪费内存空间,
    15. 因此它具备必定的优点。
  6. 链表还有诸多的改进的方式
    1. 因此应用链表在一些状况下仍然是有它的优点的,
    2. 最最重要的是 链表自己是一种最最基础的动态数据结构,
    3. 对这种动态数据结构要深入的理解和掌握,
    4. 对于学习更重要的动态数据结构是有巨大的帮助,
    5. 好比说二叉树、平衡二叉树、AVL、红黑树等等。

### 添加操做 O(n)

  1. addLast(e)O(n)
    1. 会从头遍历到尾,
    2. 找到最后一个节点以后才能添加元素
  2. addFirst(e)O(1)
    1. 直接从头部添加元素
  3. insert(index, e)O(n/2) = O(n)
    1. 会去遍历链表中每个节点,
    2. 检索到合适的位置才会添加这个元素。

删除操做 O(n)

  1. removeLast()O(n)
    1. 会去遍历链表中每个节点,
    2. 找到最后一个节点后才会删除
  2. removeFirst()O(1)
    1. 直接从头部删除这个节点
  3. remove(index)O(n/2) = O(n)
    1. 会去遍历链表中每个节点,
    2. 检索到合适的位置才会移除这个元素

修改操做 O(n)

  1. set(index, e)O(n)
    1. 会去遍历链表中每个节点,
    2. 找到了合适位置的节点后,
    3. 才会修改元素

查找操做 O(n)

  1. get(index)O(n)
    1. 会去遍历链表中每个节点,
    2. 找到了合适位置的节点后,
    3. 返回这个元素。
  2. contains(e)O(n)
    1. 会去遍历链表中每个节点,
    2. 返回 是否有相同元素的 bool 值。
  3. find(e)O(n)
    1. 这个方法对链表来讲是没有用的,
    2. 就算你拿到了那个索引你也不能快速访问。

使用链表来实现栈

  1. 对链表进行添加操做时
    1. 时间复杂度为O(n)
    2. 可是在只对链表头进行操做时为O(1)
  2. 对链表进行删除操做时
    1. 时间复杂度为O(n)
    2. 可是在只对链表头进行操做时为O(1)
  3. 对链表进行查询操做时
    1. 时间复杂度为O(n)
    2. 可是在只查链表头的元素时为O(1)
  4. 这些特性很符合栈的需求
    1. 栈是后进先出的,而且栈只查栈顶的元素,
    2. 因此可使用链表来实现栈这样的数据结构。

链表实现的栈

  1. MyLinkedListStack的接口。
    1. void push(E e):添加一个元素
    2. E pop():移除一个元素
    3. E peek():查看栈顶的元素
    4. int getSize():获取栈中实际的元素个数
    5. boolean isEmpty():判断栈是否为空

代码示例

  1. (class: MyLinkedList,class: MyLinkedListStack, class: Main)

  2. MyLinkedList

    class MyLinkedListNode {
       constructor(element = null, next = null) {
          this.element = element;
          this.next = next;
       }
    
       // toString 2018-10-20-jwl
       toString() {
          return this.element.toString();
       }
    }
    
    class MyLinkedList {
       constructor() {
          this.dummyHead = new MyLinkedListNode(null, null);
          this.size = 0;
       }
    
       // 获取链表中实际的节点个数
       getSize() {
          return this.size;
       }
    
       // 判断链表是否为空
       isEmpty() {
          return this.size === 0;
       }
    
       // 在链表头添加节点
       addFirst(element) {
          // let node = new MyLinkedListNode(element, null);
          // node.next = this.head;
          // this.head = node;
          // this.size ++;
    
          // 改用虚拟头节点
          this.insert(0, element);
       }
    
       // 在链表指定索引处插入节点
       insert(index, element) {
          if (index < 0 || index > this.size) {
             throw new Error('add error. index < 0 or index > size');
          }
    
          // 第一个prev就是dummyHead
          let prev = this.dummyHead;
          // 以前变量i(索引)之因此要从 1 开始,由于索引为0的那个节点就是head,循环就不须要从0开始了,
          // 如今索引之因此要从 0 开始, 由于初始化时 多增长了一个虚拟的头节点
          // (由于这个索引为0的节点并非dummyHead,dummyHead这个节点并不记录为链表中的实际节点),
          // 小于index是由于要找到指定索引位置的前一个节点
          // 循环是由于 要继续找到指定索引处的节点的前一个节点
          for (var i = 0; i < index; i++) {
             // 不停的切换引用,直到找到对应索引处节点的下一个节点
             prev = prev.next;
          }
    
          let node = new MyLinkedListNode(element, null);
          node.next = prev.next;
          prev.next = node;
          this.size++;
       }
    
       // 扩展:在链表最后一个节点的位置添加节点
       addLast(element) {
          this.insert(this.size, element);
       }
    
       // 获取指定索引位置的元素
       get(index) {
          // 判断索引合法性
          if (index < 0 || index >= this.size) {
             throw new Error('get error. index < 0 or index >= size');
          }
    
          // 若是你要找指定索引节点的前一个节点 就使用dummyHead
          // 若是你要找到指定索引节点 就使用dummyHead.next
          // 由于duumyHead并非第一个节点,由于它是一个虚拟节点,
          // dummyHead.next才是真正被记录的第一个节点。
          let node = this.dummyHead.next;
          for (var i = 0; i < index; i++) {
             node = node.next;
          }
    
          return node.element;
       }
    
       // 获取头节点的元素
       getFirst() {
          return this.get(0);
       }
    
       // 获取尾节点的元素
       getLast() {
          return this.get(this.size - 1);
       }
    
       // 设置指定索引位置的元素值
       set(index, element) {
          // 判断索引合法性
          if (index < 0 || index >= this.size) {
             throw new Error('get error. index < 0 or index >= size');
          }
    
          // 从第一个真正被记录的节点开始,从0开始
          let node = this.dummyHead.next;
    
          // 索引为 0 时,实际上切换到的节点 它的索引为 1
          // i < index ,当索引为 index-1 时, 实际上切换到的节点 它的索引为index
          for (let i = 0; i < index; i++) {
             // 每一次切换 都只是改变引用
             // 不的在链表中找下一个节点
             node = node.next;
          }
    
          node.element = element;
       }
    
       // 全部节点中是否有包含该元素
       contains(element) {
          let node = this.dummyHead;
    
          while (node.next !== null) {
             if (node.next.element === element) return true;
             // 不停的向下切换
             node = node.next;
          }
    
          return false;
       }
    
       // 删除指定索引位置的节点
       remove(index) {
          // 验证索引的合法性
          if (index < 0 || index >= this.size) {
             throw new Error('remove error. index < 0 or index > this.size');
          }
    
          let node = this.dummyHead;
          for (let i = 0; i < index; i++) {
             node = node.next;
          }
    
          // 待删除的节点
          let delNode = node.next;
          // 给待删除那个节点的前一个的节点的next引用替换为
          // 但删除的这个节点的next
          node.next = delNode.next;
          // 或者这样也行
          // node.next = node.next.next;
    
          // 临时存储待删除的那个节点里的元素
          let element = delNode.element;
          // 清空 待删除的节点
          delNode = null;
          this.size--;
    
          return element;
       }
    
       // 扩展:移除链表头的元素
       removeFirst() {
          return this.remove(0);
       }
    
       // 扩展:移除链表尾部的元素
       removeLast() {
          return this.remove(this.size - 1);
       }
    
       // 输出链表中的信息
       // @Override toString 2018-10-21-jwl
       toString() {
          let arrInfo = `LinkedList: size = ${this.size},\n`;
          arrInfo += `data = front [`;
          let node = this.dummyHead.next;
          while (node.next !== null) {
             arrInfo += `${node.element}->`;
             node = node.next;
          }
          arrInfo += 'NULL] tail';
    
          // 在页面上展现
          document.body.innerHTML += `${arrInfo}<br /><br /> `;
    
          return arrInfo;
       }
    }
    复制代码
  3. MyLinkedListStack

    class MyLinkedListStack {
       constructor() {
          this.myLinkedList = new MyLinkedList();
       }
    
       // 入栈
       push(element) {
          this.myLinkedList.addFirst(element);
       }
    
       // 出栈
       pop() {
          return this.myLinkedList.removeFirst();
       }
    
       // 查看栈顶元素
       peek() {
          return this.myLinkedList.getFirst();
       }
    
       // 查看栈中实际元素的个数
       getSize() {
          return this.myLinkedList.getSize();
       }
    
       // 判断栈是否为空
       isEmpty() {
          return this.myLinkedList.isEmpty();
       }
    
       // 输出栈中信息
       // @Override toString 2018-10-21-jwl
       toString() {
          let arrInfo = `LinkedListStack: size = ${this.getSize()},\n`;
          arrInfo += `data = stack top [`;
          let node = this.myLinkedList.dummyHead.next;
          for (var i = 1; i < this.getSize(); i++) {
             arrInfo += `${node.element},`;
             node = node.next;
          }
          if (!this.isEmpty()) {
             arrInfo += `${node.element}`;
          }
          arrInfo += ']';
    
          // 在页面上展现
          document.body.innerHTML += `${arrInfo}<br /><br /> `;
    
          return arrInfo;
       }
    }
    复制代码
  4. Main

    class Main {
       constructor() {
          this.alterLine('MyLinkedListStack Area');
          let myLinkedListStack = new MyLinkedListStack();
          for (let i = 1; i <= 5; i++) {
             myLinkedListStack.push(i);
             console.log(myLinkedListStack.toString());
          }
    
          console.log(myLinkedListStack.peek());
          this.show(myLinkedListStack.peek());
    
          for (let i = 0; i < 5; i++) {
             console.log(myLinkedListStack.toString());
             myLinkedListStack.pop();
          }
       }
    
       // 将内容显示在页面上
       show(content) {
          document.body.innerHTML += `${content}<br /><br />`;
       }
    
       // 展现分割线
       alterLine(title) {
          let line = `--------------------${title}----------------------`;
          console.log(line);
          document.body.innerHTML += `${line}<br /><br />`;
       }
    }
    
    window.onload = function() {
       // 执行主函数
       new Main();
    };
    复制代码

自定义数组栈对比自定义链表栈

  1. 自定义数组栈与自定义链表栈的性能相差不多
    1. 可是随着操做的次数增加,数组栈会慢慢强过链表栈,
    2. 自定义链表栈中有太多的 new 操做,
    3. new 操做在有一些系统上比较耗费性能的,
    4. 由于它在不停的在内存中寻找能够开辟空间的地方来进行开辟空间,
    5. 自定义数组栈中有比较多的扩容操做,
    6. 因此这个比较是相对比较复杂的,
    7. 和你的语法、操做系统、编译器、解释器都有关系,
    8. 不过他们的时间复杂度都是O(1)级别的,
    9. 因此他们之间的性能差别无非就 1-2 倍这样,
    10. 在最极端的状况下 3-5 倍就已经很难了,
    11. 不会有几百倍的巨大的差别,由于毕竟他们的时间复杂度同样。

代码示例

  1. (class: MyLinkedList, class: MyLinkedListStack, class: MyArray, class: MyStack, class: Main)

  2. MyLinkedList

    class MyLinkedListNode {
       constructor(element = null, next = null) {
          this.element = element;
          this.next = next;
       }
    
       // toString 2018-10-20-jwl
       toString() {
          return this.element.toString();
       }
    }
    
    class MyLinkedList {
       constructor() {
          this.dummyHead = new MyLinkedListNode(null, null);
          this.size = 0;
       }
    
       // 获取链表中实际的节点个数
       getSize() {
          return this.size;
       }
    
       // 判断链表是否为空
       isEmpty() {
          return this.size === 0;
       }
    
       // 在链表头添加节点
       addFirst(element) {
          // let node = new MyLinkedListNode(element, null);
          // node.next = this.head;
          // this.head = node;
          // this.size ++;
    
          // 改用虚拟头节点
          this.insert(0, element);
       }
    
       // 在链表指定索引处插入节点
       insert(index, element) {
          if (index < 0 || index > this.size) {
             throw new Error('add error. index < 0 or index > size');
          }
    
          // 第一个prev就是dummyHead
          let prev = this.dummyHead;
          // 以前变量i(索引)之因此要从 1 开始,由于索引为0的那个节点就是head,循环就不须要从0开始了,
          // 如今索引之因此要从 0 开始, 由于初始化时 多增长了一个虚拟的头节点
          // (由于这个索引为0的节点并非dummyHead,dummyHead这个节点并不记录为链表中的实际节点),
          // 小于index是由于要找到指定索引位置的前一个节点
          // 循环是由于 要继续找到指定索引处的节点的前一个节点
          for (var i = 0; i < index; i++) {
             // 不停的切换引用,直到找到对应索引处节点的下一个节点
             prev = prev.next;
          }
    
          let node = new MyLinkedListNode(element, null);
          node.next = prev.next;
          prev.next = node;
          this.size++;
       }
    
       // 扩展:在链表最后一个节点的位置添加节点
       addLast(element) {
          this.insert(this.size, element);
       }
    
       // 获取指定索引位置的元素
       get(index) {
          // 判断索引合法性
          if (index < 0 || index >= this.size) {
             throw new Error('get error. index < 0 or index >= size');
          }
    
          // 若是你要找指定索引节点的前一个节点 就使用dummyHead
          // 若是你要找到指定索引节点 就使用dummyHead.next
          // 由于duumyHead并非第一个节点,由于它是一个虚拟节点,
          // dummyHead.next才是真正被记录的第一个节点。
          let node = this.dummyHead.next;
          for (var i = 0; i < index; i++) {
             node = node.next;
          }
    
          return node.element;
       }
    
       // 获取头节点的元素
       getFirst() {
          return this.get(0);
       }
    
       // 获取尾节点的元素
       getLast() {
          return this.get(this.size - 1);
       }
    
       // 设置指定索引位置的元素值
       set(index, element) {
          // 判断索引合法性
          if (index < 0 || index >= this.size) {
             throw new Error('get error. index < 0 or index >= size');
          }
    
          // 从第一个真正被记录的节点开始,从0开始
          let node = this.dummyHead.next;
    
          // 索引为 0 时,实际上切换到的节点 它的索引为 1
          // i < index ,当索引为 index-1 时, 实际上切换到的节点 它的索引为index
          for (let i = 0; i < index; i++) {
             // 每一次切换 都只是改变引用
             // 不的在链表中找下一个节点
             node = node.next;
          }
    
          node.element = element;
       }
    
       // 全部节点中是否有包含该元素
       contains(element) {
          let node = this.dummyHead;
    
          while (node.next !== null) {
             if (node.next.element === element) return true;
             // 不停的向下切换
             node = node.next;
          }
    
          return false;
       }
    
       // 删除指定索引位置的节点
       remove(index) {
          // 验证索引的合法性
          if (index < 0 || index >= this.size) {
             throw new Error('remove error. index < 0 or index > this.size');
          }
    
          let node = this.dummyHead;
          for (let i = 0; i < index; i++) {
             node = node.next;
          }
    
          // 待删除的节点
          let delNode = node.next;
          // 给待删除那个节点的前一个的节点的next引用替换为
          // 但删除的这个节点的next
          node.next = delNode.next;
          // 或者这样也行
          // node.next = node.next.next;
    
          // 临时存储待删除的那个节点里的元素
          let element = delNode.element;
          // 清空 待删除的节点
          delNode = null;
          this.size--;
    
          return element;
       }
    
       // 扩展:移除链表头的元素
       removeFirst() {
          return this.remove(0);
       }
    
       // 扩展:移除链表尾部的元素
       removeLast() {
          return this.remove(this.size - 1);
       }
    
       // 输出链表中的信息
       // @Override toString 2018-10-21-jwl
       toString() {
          let arrInfo = `LinkedList: size = ${this.size},\n`;
          arrInfo += `data = front [`;
          let node = this.dummyHead.next;
          while (node.next !== null) {
             arrInfo += `${node.element}->`;
             node = node.next;
          }
          arrInfo += 'NULL] tail';
    
          // 在页面上展现
          document.body.innerHTML += `${arrInfo}<br /><br /> `;
    
          return arrInfo;
       }
    }
    复制代码
  3. MyLinkedListStack

    class MyLinkedListStack {
       constructor() {
          this.myLinkedList = new MyLinkedList();
       }
    
       // 入栈
       push(element) {
          this.myLinkedList.addFirst(element);
       }
    
       // 出栈
       pop() {
          return this.myLinkedList.removeFirst();
       }
    
       // 查看栈顶元素
       peek() {
          return this.myLinkedList.getFirst();
       }
    
       // 查看栈中实际元素的个数
       getSize() {
          return this.myLinkedList.getSize();
       }
    
       // 判断栈是否为空
       isEmpty() {
          return this.myLinkedList.isEmpty();
       }
    
       // 输出栈中信息
       // @Override toString 2018-10-21-jwl
       toString() {
          let arrInfo = `LinkedListStack: size = ${this.getSize()},\n`;
          arrInfo += `data = stack top [`;
          let node = this.myLinkedList.dummyHead.next;
          for (var i = 1; i < this.getSize(); i++) {
             arrInfo += `${node.element},`;
             node = node.next;
          }
          if (!this.isEmpty()) {
             arrInfo += `${node.element}`;
          }
          arrInfo += ']';
    
          // 在页面上展现
          document.body.innerHTML += `${arrInfo}<br /><br /> `;
    
          return arrInfo;
       }
    }
    复制代码
  4. MyArray

    class MyArray {
       // 构造函数,传入数组的容量capacity构造Array 默认数组的容量capacity=10
       constructor(capacity = 10) {
          this.data = new Array(capacity);
          this.size = 0;
       }
    
       // 获取数组中的元素实际个数
       getSize() {
          return this.size;
       }
    
       // 获取数组的容量
       getCapacity() {
          return this.data.length;
       }
    
       // 判断数组是否为空
       isEmpty() {
          return this.size === 0;
       }
    
       // 给数组扩容
       resize(capacity) {
          let newArray = new Array(capacity);
          for (var i = 0; i < this.size; i++) {
             newArray[i] = this.data[i];
          }
    
          // let index = this.size - 1;
          // while (index > -1) {
          // newArray[index] = this.data[index];
          // index --;
          // }
    
          this.data = newArray;
       }
    
       // 在指定索引处插入元素
       insert(index, element) {
          // 先判断数组是否已满
          if (this.size == this.getCapacity()) {
             // throw new Error("add error. Array is full.");
             this.resize(this.size * 2);
          }
    
          // 而后判断索引是否符合要求
          if (index < 0 || index > this.size) {
             throw new Error(
                'insert error. require index < 0 or index > size.'
             );
          }
    
          // 最后 将指定索引处腾出来
          // 从指定索引处开始,全部数组元素所有日后移动一位
          // 从后往前移动
          for (let i = this.size - 1; i >= index; i--) {
             this.data[i + 1] = this.data[i];
          }
    
          // 在指定索引处插入元素
          this.data[index] = element;
          // 维护一下size
          this.size++;
       }
    
       // 扩展 在数组最前面插入一个元素
       unshift(element) {
          this.insert(0, element);
       }
    
       // 扩展 在数组最后面插入一个元素
       push(element) {
          this.insert(this.size, element);
       }
    
       // 其实在数组中添加元素 就至关于在数组最后面插入一个元素
       add(element) {
          if (this.size == this.getCapacity()) {
             // throw new Error("add error. Array is full.");
             this.resize(this.size * 2);
          }
    
          // size其实指向的是 当前数组最后一个元素的 后一个位置的索引。
          this.data[this.size] = element;
          // 维护size
          this.size++;
       }
    
       // get
       get(index) {
          // 不能访问没有存放元素的位置
          if (index < 0 || index >= this.size) {
             throw new Error('get error. index < 0 or index >= size.');
          }
          return this.data[index];
       }
    
       // 扩展: 获取数组中第一个元素
       getFirst() {
          return this.get(0);
       }
    
       // 扩展: 获取数组中最后一个元素
       getLast() {
          return this.get(this.size - 1);
       }
    
       // set
       set(index, newElement) {
          // 不能修改没有存放元素的位置
          if (index < 0 || index >= this.size) {
             throw new Error('set error. index < 0 or index >= size.');
          }
          this.data[index] = newElement;
       }
    
       // contain
       contain(element) {
          for (var i = 0; i < this.size; i++) {
             if (this.data[i] === element) {
                return true;
             }
          }
          return false;
       }
    
       // find
       find(element) {
          for (var i = 0; i < this.size; i++) {
             if (this.data[i] === element) {
                return i;
             }
          }
          return -1;
       }
    
       // findAll
       findAll(element) {
          // 建立一个自定义数组来存取这些 元素的索引
          let myarray = new MyArray(this.size);
    
          for (var i = 0; i < this.size; i++) {
             if (this.data[i] === element) {
                myarray.push(i);
             }
          }
    
          // 返回这个自定义数组
          return myarray;
       }
    
       // 删除指定索引处的元素
       remove(index) {
          // 索引合法性验证
          if (index < 0 || index >= this.size) {
             throw new Error('remove error. index < 0 or index >= size.');
          }
    
          // 暂存即将要被删除的元素
          let element = this.data[index];
    
          // 后面的元素覆盖前面的元素
          for (let i = index; i < this.size - 1; i++) {
             this.data[i] = this.data[i + 1];
          }
    
          this.size--;
          this.data[this.size] = null;
    
          // 若是size 为容量的四分之一时 就能够缩容了
          // 防止复杂度震荡
          if (Math.floor(this.getCapacity() / 4) === this.size) {
             // 缩容一半
             this.resize(Math.floor(this.getCapacity() / 2));
          }
    
          return element;
       }
    
       // 扩展:删除数组中第一个元素
       shift() {
          return this.remove(0);
       }
    
       // 扩展: 删除数组中最后一个元素
       pop() {
          return this.remove(this.size - 1);
       }
    
       // 扩展: 根据元素来进行删除
       removeElement(element) {
          let index = this.find(element);
          if (index !== -1) {
             this.remove(index);
          }
       }
    
       // 扩展: 根据元素来删除全部元素
       removeAllElement(element) {
          let index = this.find(element);
          while (index != -1) {
             this.remove(index);
             index = this.find(element);
          }
    
          // let indexArray = this.findAll(element);
          // let cur, index = 0;
          // for (var i = 0; i < indexArray.getSize(); i++) {
          // // 每删除一个元素 原数组中就少一个元素,
          // // 索引数组中的索引值是按照大小顺序排列的,
          // // 因此 这个cur记录的是 原数组元素索引的偏移量
          // // 只有这样才可以正确的删除元素。
          // index = indexArray.get(i) - cur++;
          // this.remove(index);
          // }
       }
    
       // @Override toString 2018-10-17-jwl
       toString() {
          let arrInfo = `Array: size = ${this.getSize()},capacity = ${this.getCapacity()},\n`;
          arrInfo += `data = [`;
          for (var i = 0; i < this.size - 1; i++) {
             arrInfo += `${this.data[i]}, `;
          }
          if (!this.isEmpty()) {
             arrInfo += `${this.data[this.size - 1]}`;
          }
          arrInfo += `]`;
    
          // 在页面上展现
          document.body.innerHTML += `${arrInfo}<br /><br /> `;
    
          return arrInfo;
       }
    }
    复制代码
  5. MyStack

    class MyStack {
       constructor(capacity = 10) {
          this.myArray = new MyArray(capacity);
       }
    
       // 入栈
       push(element) {
          this.myArray.push(element);
       }
    
       // 出栈
       pop() {
          return this.myArray.pop();
       }
    
       // 查看栈顶的元素
       peek() {
          return this.myArray.getLast();
       }
    
       // 栈中实际元素的个数
       getSize() {
          return this.myArray.getSize();
       }
    
       // 栈是否为空
       isEmpty() {
          return this.myArray.isEmpty();
       }
    
       // 查看栈的容量
       getCapacity() {
          return this.myArray.getCapacity();
       }
    
       // @Override toString 2018-10-20-jwl
       toString() {
          let arrInfo = `Stack: size = ${this.getSize()},capacity = ${this.getCapacity()},\n`;
          arrInfo += `data = [`;
          for (var i = 0; i < this.myArray.size - 1; i++) {
             arrInfo += `${this.myArray.data[i]}, `;
          }
          if (!this.isEmpty()) {
             arrInfo += `${this.myArray.data[this.myArray.size - 1]}`;
          }
          arrInfo += `] stack top is right!`;
    
          // 在页面上展现
          document.body.innerHTML += `${arrInfo}<br /><br /> `;
    
          return arrInfo;
       }
    }
    复制代码
  6. Main

    class Main {
       constructor() {
          this.alterLine('Stacks Comparison Area');
          let myStack = new MyStack();
          let myLinkedListStack = new MyLinkedListStack();
          let performanceTest = new PerformanceTest();
    
          let myStackInfo = performanceTest.testStack(myStack, 100000);
          let myLinkedListStackInfo = performanceTest.testStack(
             myLinkedListStack,
             100000
          );
    
          this.alterLine('MyStack Area');
          console.log(myStackInfo);
          this.show(myStackInfo);
    
          this.alterLine('MyLinkedListStack Area');
          console.log(myLinkedListStackInfo);
          this.show(myLinkedListStackInfo);
       }
    
       // 将内容显示在页面上
       show(content) {
          document.body.innerHTML += `${content}<br /><br />`;
       }
    
       // 展现分割线
       alterLine(title) {
          let line = `--------------------${title}----------------------`;
          console.log(line);
          document.body.innerHTML += `${line}<br /><br />`;
       }
    }
    class Student {
       constructor(studentName, studentScore) {
          this.name = studentName;
          this.score = studentScore;
       }
    
       toString() {
          let studentInfo = `Student(name: ${this.name}, score: ${this.score})`;
          return studentInfo;
       }
    }
    
    window.onload = function() {
       // 执行主函数
       new Main();
    };
    复制代码

使用链表来实现队列

  1. 对链表进行添加操做时
    1. 时间复杂度为O(n)
    2. 只对链表头进行操做时为O(1)
    3. 对链表尾部进行操做时为O(n)
  2. 对链表进行删除操做时
    1. 时间复杂度为O(n)
    2. 只对链表头进行操做时为O(1)
    3. 对链表尾部进行操做时为O(n)
  3. 对链表进行查询操做时
    1. 时间复杂度为O(n)
    2. 只查链表头的元素时为O(1)
    3. 查链表尾部的元素时为O(n)
  4. 队列中的操做
    1. 在线性结构的一端插入元素,
    2. 在另一端删除元素,
    3. 因此必然会在线性结构的两端同时操做,
    4. 此时就会有一端的操做的复杂度是O(n)级别,
    5. 这个问题在用自定义数组实现时也遇到了,
    6. 也正由于如此产生了循环队列,
    7. 经过改进使用数组来实现队列的方式,
    8. 因此链表也能够进行改进。
  5. 改进链表
    1. 不能使用以前的链表来进行队列的实现,
    2. 设置一个 head 变量指向链表的头部第一个节点,
    3. 设置一个 tail 变量指向链表的尾部第一个节点,
    4. 有了 tail 这个变量,那么在链表的尾部添加一个元素很是容易,
    5. 有了 head 和 tail 以后在两端添加节点是很是容易的,
    6. 可是没法使用O(1)去删除尾部的节点,
    7. 从 tail 这一端删除元素并不容易,
    8. 可是若是将 head 做为队首,将 tail 做为队尾,
    9. 队列操做是从队尾进队首出的,
    10. 只须要从队尾 tail 插入元素,从队首 head 删除元素,
    11. 这样一来就能够实现队列的功能。
  6. 链表中再也不有插入的功能因此不须要 dummyHead
    1. 可是因为没有 dummyHead,因此须要注意链表为空的状况。
  7. 让自定义动态数组队列与自定义链表队列进行对比
    1. 自定义动态数组队列的性能最差
    2. 自定义动态数组循环队列与自定义链表队列的性能相近。
  8. 与链表相关的有一个很是重要的内容
    1. 就是递归,由于链表自己具备自然的递归性质,
    2. 同时它又是一种很是简单的数据结构,
    3. 因此链表是一种很是好的
    4. 研究学习递归的逻辑机制的的数据结构。

代码示例

  1. ( class: MyArray, class: MyQueue, class: MyLoopQueue, class: MyLinkedListQueue, class: Main)

  2. MyArray

    class MyArray {
       // 构造函数,传入数组的容量capacity构造Array 默认数组的容量capacity=10
       constructor(capacity = 10) {
          this.data = new Array(capacity);
          this.size = 0;
       }
    
       // 获取数组中的元素实际个数
       getSize() {
          return this.size;
       }
    
       // 获取数组的容量
       getCapacity() {
          return this.data.length;
       }
    
       // 判断数组是否为空
       isEmpty() {
          return this.size === 0;
       }
    
       // 给数组扩容
       resize(capacity) {
          let newArray = new Array(capacity);
          for (var i = 0; i < this.size; i++) {
             newArray[i] = this.data[i];
          }
    
          // let index = this.size - 1;
          // while (index > -1) {
          // newArray[index] = this.data[index];
          // index --;
          // }
    
          this.data = newArray;
       }
    
       // 在指定索引处插入元素
       insert(index, element) {
          // 先判断数组是否已满
          if (this.size == this.getCapacity()) {
             // throw new Error("add error. Array is full.");
             this.resize(this.size * 2);
          }
    
          // 而后判断索引是否符合要求
          if (index < 0 || index > this.size) {
             throw new Error(
                'insert error. require index < 0 or index > size.'
             );
          }
    
          // 最后 将指定索引处腾出来
          // 从指定索引处开始,全部数组元素所有日后移动一位
          // 从后往前移动
          for (let i = this.size - 1; i >= index; i--) {
             this.data[i + 1] = this.data[i];
          }
    
          // 在指定索引处插入元素
          this.data[index] = element;
          // 维护一下size
          this.size++;
       }
    
       // 扩展 在数组最前面插入一个元素
       unshift(element) {
          this.insert(0, element);
       }
    
       // 扩展 在数组最后面插入一个元素
       push(element) {
          this.insert(this.size, element);
       }
    
       // 其实在数组中添加元素 就至关于在数组最后面插入一个元素
       add(element) {
          if (this.size == this.getCapacity()) {
             // throw new Error("add error. Array is full.");
             this.resize(this.size * 2);
          }
    
          // size其实指向的是 当前数组最后一个元素的 后一个位置的索引。
          this.data[this.size] = element;
          // 维护size
          this.size++;
       }
    
       // get
       get(index) {
          // 不能访问没有存放元素的位置
          if (index < 0 || index >= this.size) {
             throw new Error('get error. index < 0 or index >= size.');
          }
          return this.data[index];
       }
    
       // 扩展: 获取数组中第一个元素
       getFirst() {
          return this.get(0);
       }
    
       // 扩展: 获取数组中最后一个元素
       getLast() {
          return this.get(this.size - 1);
       }
    
       // set
       set(index, newElement) {
          // 不能修改没有存放元素的位置
          if (index < 0 || index >= this.size) {
             throw new Error('set error. index < 0 or index >= size.');
          }
          this.data[index] = newElement;
       }
    
       // contain
       contain(element) {
          for (var i = 0; i < this.size; i++) {
             if (this.data[i] === element) {
                return true;
             }
          }
          return false;
       }
    
       // find
       find(element) {
          for (var i = 0; i < this.size; i++) {
             if (this.data[i] === element) {
                return i;
             }
          }
          return -1;
       }
    
       // findAll
       findAll(element) {
          // 建立一个自定义数组来存取这些 元素的索引
          let myarray = new MyArray(this.size);
    
          for (var i = 0; i < this.size; i++) {
             if (this.data[i] === element) {
                myarray.push(i);
             }
          }
    
          // 返回这个自定义数组
          return myarray;
       }
    
       // 删除指定索引处的元素
       remove(index) {
          // 索引合法性验证
          if (index < 0 || index >= this.size) {
             throw new Error('remove error. index < 0 or index >= size.');
          }
    
          // 暂存即将要被删除的元素
          let element = this.data[index];
    
          // 后面的元素覆盖前面的元素
          for (let i = index; i < this.size - 1; i++) {
             this.data[i] = this.data[i + 1];
          }
    
          this.size--;
          this.data[this.size] = null;
    
          // 若是size 为容量的四分之一时 就能够缩容了
          // 防止复杂度震荡
          if (Math.floor(this.getCapacity() / 4) === this.size) {
             // 缩容一半
             this.resize(Math.floor(this.getCapacity() / 2));
          }
    
          return element;
       }
    
       // 扩展:删除数组中第一个元素
       shift() {
          return this.remove(0);
       }
    
       // 扩展: 删除数组中最后一个元素
       pop() {
          return this.remove(this.size - 1);
       }
    
       // 扩展: 根据元素来进行删除
       removeElement(element) {
          let index = this.find(element);
          if (index !== -1) {
             this.remove(index);
          }
       }
    
       // 扩展: 根据元素来删除全部元素
       removeAllElement(element) {
          let index = this.find(element);
          while (index != -1) {
             this.remove(index);
             index = this.find(element);
          }
    
          // let indexArray = this.findAll(element);
          // let cur, index = 0;
          // for (var i = 0; i < indexArray.getSize(); i++) {
          // // 每删除一个元素 原数组中就少一个元素,
          // // 索引数组中的索引值是按照大小顺序排列的,
          // // 因此 这个cur记录的是 原数组元素索引的偏移量
          // // 只有这样才可以正确的删除元素。
          // index = indexArray.get(i) - cur++;
          // this.remove(index);
          // }
       }
    
       // @Override toString 2018-10-17-jwl
       toString() {
          let arrInfo = `Array: size = ${this.getSize()},capacity = ${this.getCapacity()},\n`;
          arrInfo += `data = [`;
          for (var i = 0; i < this.size - 1; i++) {
             arrInfo += `${this.data[i]}, `;
          }
          if (!this.isEmpty()) {
             arrInfo += `${this.data[this.size - 1]}`;
          }
          arrInfo += `]`;
    
          // 在页面上展现
          document.body.innerHTML += `${arrInfo}<br /><br /> `;
    
          return arrInfo;
       }
    }
    复制代码
  3. MyQueue

    class MyQueue {
       constructor(capacity = 10) {
          this.myArray = new MyArray(capacity);
       }
    
       // 入队
       enqueue(element) {
          this.myArray.push(element);
       }
    
       // 出队
       dequeue() {
          return this.myArray.shift();
       }
    
       // 查看队首的元素
       getFront() {
          return this.myArray.getFirst();
       }
    
       // 查看队列中实际元素的个数
       getSize() {
          return this.myArray.getSize();
       }
    
       // 查看 队列当前的容量
       getCapacity() {
          return this.myArray.getCapacity();
       }
    
       // 查看队列是否为空
       isEmpty() {
          return this.myArray.isEmpty();
       }
    
       // 输出队列中的信息
       // @Override toString 2018-10-20-jwl
       toString() {
          let arrInfo = `Queue: size = ${this.getSize()},capacity = ${this.getCapacity()},\n`;
          arrInfo += `data = front [`;
          for (var i = 0; i < this.myArray.size - 1; i++) {
             arrInfo += `${this.myArray.data[i]}, `;
          }
          if (!this.isEmpty()) {
             arrInfo += `${this.myArray.data[this.myArray.size - 1]}`;
          }
          arrInfo += `] tail`;
    
          // 在页面上展现
          document.body.innerHTML += `${arrInfo}<br /><br /> `;
    
          return arrInfo;
       }
    }
    复制代码
  4. MyLoopQueue

    class MyLoopQueue {
       constructor(capacity = 10) {
          // 初始化新数组
          this.data = new Array(capacity);
          // 初始化 队首、队尾的值 (索引)
          this.front = this.tail = 0;
          // 队列中实际元素个数
          this.size = 0;
       }
    
       // 扩容
       resize(capacity) {
          let newArray = new Array(capacity);
          let index = 0;
    
          for (let i = 0; i < this.size; i++) {
             // 索引可能会越界,因而就要取余一下,
             // 若是越界了,就从队首开始
             index = (this.front + i) % this.getCapacity();
             newArray[i] = this.data[index];
          }
    
          this.data = newArray;
          this.front = 0;
          this.tail = this.size;
       }
    
       // 入队
       enqueue(element) {
          // 判断队列中是否已满
          if ((this.tail + 1) % this.getCapacity() === this.front) {
             this.resize(this.getCapacity() * 2);
          }
    
          this.data[this.tail] = element;
          this.tail = (this.tail + 1) % this.getCapacity();
          this.size++;
       }
    
       // 出队
       dequeue() {
          // 判断队列是否为空
          if (this.isEmpty()) {
             throw new Error("can't dequeue from an empty queue.");
          }
    
          let element = this.data[this.front];
          this.data[this.front] = null;
          this.front = (this.front + 1) % this.getCapacity();
          this.size--;
    
          // 当size 为容量的四分之一时就缩容一倍
          if (this.size === Math.floor(this.getCapacity() / 4)) {
             this.resize(Math.floor(this.getCapacity() * 2));
          }
          return element;
       }
    
       // 查看队首的元素
       getFront() {
          if (this.isEmpty()) {
             throw new Error('queue is empty.');
          }
    
          return this.data[front];
       }
    
       // 查看实际的元素个数
       getSize() {
          return this.size;
       }
    
       // 查看容量
       getCapacity() {
          return this.data.length;
       }
    
       // 队列是否为空
       isEmpty() {
          // return this.size === 0;
          return this.front == this.tail;
       }
    
       // 输出循环队列中的信息
       // @Override toString 2018-10-20-jwl
       toString() {
          let arrInfo = `LoopQueue: size = ${this.getSize()},capacity = ${this.getCapacity()},\n`;
          arrInfo += `data = front [`;
          for (var i = 0; i < this.myArray.size - 1; i++) {
             arrInfo += `${this.myArray.data[i]}, `;
          }
          if (!this.isEmpty()) {
             arrInfo += `${this.myArray.data[this.myArray.size - 1]}`;
          }
          arrInfo += `] tail`;
    
          // 在页面上展现
          document.body.innerHTML += `${arrInfo}<br /><br /> `;
    
          return arrInfo;
       }
    }
    复制代码
  5. MyLinkedListQueue

    class MyLinkedListQueue {
       constructor() {
          this.front = this.tail = null;
          this.size = 0;
       }
    
       // 入队
       enqueue(element) {
          // 判断队尾是否为空
          if (this.tail === null) {
             // 第一个节点 便是尾也是头
             this.tail = new MyLinkedListNode(element, null);
             this.front = this.tail;
          } else {
             let node = new MyLinkedListNode(element, null);
             this.tail.next = node;
             this.tail = node;
          }
          this.size++;
       }
    
       // 出队
       dequeue() {
          // 判断队首是否为空
          if (this.front === null) {
             throw new Error('front is empty.');
          }
    
          let delNode = this.front;
          let element = delNode.element;
          this.front = this.front.next;
          delNode = null;
          if (this.front === null)
             // 若是头为空了,那么尾部也为空
             this.tail = null;
          this.size--;
    
          return element;
       }
    
       // 查看队首的元素
       getFront() {
          // 判断队首是否为空
          if (this.front === null) {
             throw new Error('front is empty.');
          }
    
          return this.front.element;
       }
    
       // 查看队列中实际元素的个数
       getSize() {
          return this.size;
       }
    
       // 判断队列是否为空
       isEmpty() {
          return this.size === 0;
       }
    
       // 输出队列中的信息
       // @Override toString 2018-10-21-jwl
       toString() {
          let arrInfo = `LinkedListQueue: size = ${this.getSize()},\n`;
          arrInfo += `data = front [`;
          let node = this.front;
          for (var i = 1; i < this.getSize(); i++) {
             arrInfo += `${node.element},`;
             node = node.next;
          }
          if (!this.isEmpty()) {
             arrInfo += `${node.element}`;
          }
          arrInfo += '] tail';
    
          // 在页面上展现
          document.body.innerHTML += `${arrInfo}<br /><br /> `;
    
          return arrInfo;
       }
    }
    复制代码
  6. Main

    class Main {
       constructor() {
          this.alterLine('MyLinkedListQueue Area');
          let myLinkedListQueue = new MyLinkedListQueue();
          for (let i = 1; i <= 5; i++) {
             myLinkedListQueue.enqueue(i);
             console.log(myLinkedListQueue.toString());
          }
    
          console.log(myLinkedListQueue.getFront());
          this.show(myLinkedListQueue.getFront());
    
          for (let i = 0; i < 5; i++) {
             console.log(myLinkedListQueue.toString());
             myLinkedListQueue.dequeue();
          }
       }
    
       // 将内容显示在页面上
       show(content) {
          document.body.innerHTML += `${content}<br /><br />`;
       }
    
       // 展现分割线
       alterLine(title) {
          let line = `--------------------${title}----------------------`;
          console.log(line);
          document.body.innerHTML += `${line}<br /><br />`;
       }
    }
    class Student {
       constructor(studentName, studentScore) {
          this.name = studentName;
          this.score = studentScore;
       }
    
       toString() {
          let studentInfo = `Student(name: ${this.name}, score: ${this.score})`;
          return studentInfo;
       }
    }
    
    window.onload = function() {
       // 执行主函数
       new Main();
    };
    复制代码
相关文章
相关标签/搜索