【从蛋壳到满天飞】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 (一个一个的工程)前端

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

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

栈 Statck

  1. 栈也是一种线性结构
  2. 相比数组来讲相应的操做更少,
    1. 栈对应的操做是数组的子集,
    2. 由于它的本质就是一个数组,
    3. 而且它有比数组更多的限制。
  3. 栈的本质就是一个数组
    1. 它将数据排开来放的,
    2. 添加元素的时候只能从栈的一端添加元素,
    3. 取出元素的时候也只能栈的一端取出元素,
    4. 这一端叫作栈顶,当这样的限定了数组,
    5. 从而造成了栈这种数据结构以后,
    6. 它能够在计算机世界中对于
    7. 组建逻辑产生很是很是重要的做用。
  4. 栈的操做
    1. 从栈顶添加元素,把元素一个一个的放入到栈中,
    2. 如添加值的时候为 一、二、3,
    3. 你取值的时候顺序则为 三、二、1,
    4. 由于你添加元素是只能从一端放入,
    5. 取出元素时也只能从一端取出,
    6. 而这一段就是栈顶,
    7. 栈的出口和入口都是同一个位置,
    8. 因此你只能按照先进后出、后进先出的顺序
    9. 添加数据或者取出数据,不存在插入和索引。
  5. 栈是一种后进先出的数据结构
    1. 也就是 Last In First Out(LIFO),
    2. 这样的一种数据结构,在计算机的世界里,
    3. 它拥有着难以想象的做用,
    4. 不管是经典的算法仍是算法设计都接触到
    5. 栈这种看似很简单但其实应用很是普遍的数据结构,

栈的简单应用

  1. 无处不在的 Undo 操做(撤销)面试

    1. 编辑器的撤销操做的原理就是靠一个栈来进行维护的,
    2. 如 将 每次输入的内容依次放入栈中 我 喜欢 你,
    3. 若是 你 字写错,你撤销一下,变成 我 喜欢,
    4. 再撤销一下 变成 我。
  2. 程序调用的系统栈算法

    1. 程序调用时常常会出如今一个逻辑中间
    2. 先终止而后跳到另一个逻辑去执行,
    3. 所谓的子函数的调用就是这个过程,
    4. 在这个过程当中计算机就须要使用一个
    5. 称为系统栈的一个数据结构来记录程序的调用过程。
    6. 例若有三个函数 A、B、C,
    7. 当 A 执行到一半的时候调用 B,
    8. 当 B 执行到一半的时候调用 C,
    9. C 函数能够执行运行完,
    10. C 函数运行完了以后继续运行未完成的 B 函数,
    11. B 函数运行完了就运行未完成 A 函数,
    12. A 函数运行完了就结束了。
    function A () {
          1 ...;
          2 B();
          3 ...;
       }
    
       function B () {
        1 ...;
        2 C();
        3 ...;
       }
    
       function C () {
        1 ...;
        2 ...;
        3 ...;
       }
    复制代码
  3. 系统栈记录的过程是:编程

    1. A 函数执行,在第二行中断了,由于要去执行函数 B 了,
    2. 这时候函数信息A2会被放入系统栈中,系统栈中显示:[A2]
    3. 而后 B 函数执行,在第二行也中断了,由于要去执行函数 C 了,
    4. 这时候函数信息 B2 会被放入系统栈中,系统栈中显示:[A2, B2]
    5. 而后 C 函数执行,C 函数没有子函数可执行,那么执行到底,函数 C 执行完毕,
    6. 从系统栈中取出函数 B 的信息,系统栈中显示:[A2]
    7. 根据从系统栈中取出的函数 B 的信息,从函数 B 原来中断的位置继续开始执行,
    8. B 函数执行完毕了,这时候会再从系统栈中取出函数 A 的,系统栈中显示:[]
    9. 根据从系统栈中取出的函数 A 的信息,从函数 A 原来中断的位置继续开始执行,
    10. A 函数执行完了,系统栈中已经没有函数信息了,好的,程序结束。
    11. 存入系统栈中的是函数执行时的一些信息,
    12. 因此取出来后,能够根据这些信息来继续完成
    13. 原来函数未执行完毕的那部分代码。
  4. 2 和 3 中解释的原理 就是系统栈最神奇的地方数组

    1. 在编程的时候进行子过程调用的时候,
    2. 当一个子过程执行完成以后,
    3. 能够自动的回到上层调用中断的位置,
    4. 而且继续执行下去。
    5. 都是靠一个系统栈来记录每一次调用过程当中
    6. 中断的那个调用的点来实现的。
  5. 栈虽然是一个很是简单的数据结构性能优化

    1. 可是它可以解决计算机领域很是复杂的一个问题,
    2. 这个问题就是这种子过程子逻辑的调用,
    3. 在编译器内部它运行实现的原理是什么,
    4. 深刻理解这个过程,
    5. 甚至可以帮助你理解一些更复杂的逻辑过程,
    6. 好比递归这样的一个过程,你会有更加深入的理解。

栈的实现

  1. 栈这种数据结构很是有用
    1. 但实际上是很是简单的。
  2. MyStack
    1. void push(e):入栈
    2. E pop():出栈
    3. E peek():查看位于栈顶位置的元素
    4. int getSize():获取栈中实际元素的个数
    5. boolean isEmpty():栈是否为空
  3. 从用户的角度看
    1. 只要支持这些操做就行了,
    2. 用户无论你要怎样 resize,
    3. 他只要知道你这个数组是一个动态的,
    4. 他能够不停的往里面添加元素,
    5. 而且不会出现问题就 ok,
    6. 其实对于栈也是这样的,
    7. 对于具体的底层实现,用户不关心,
    8. 实际底层也有多种实现方式,
    9. 因此用户就更加不关心了。
  4. 为了让代码更加的清晰,
    1. 同时也是为了支持面向对象的一些特性,
    2. 好比说支持多态性,
    3. 那么就会这样的去设计,
    4. 定义一个接口叫作 IMyStack,
    5. 接口中有栈默认的全部方法,
    6. 而后再定义一个类叫作 MyStack,
    7. 让它去实现 IMyStack,
    8. 这样就能够在 MyStack 中完成对应的逻辑,
    9. 这个 MyStack 就是自定义的栈。
  5. 会复用到以前自定义数组对象。

栈的复杂度分析

  1. MyStack
    1. void push(e):O(1) 均摊
    2. E pop():O(1) 均摊
    3. E peek():O(1)
    4. int getSize():O(1)
    5. boolean isEmpty():O(1)

代码示例

  1. (class: MyArray, class: MyStack, 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. 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;
       }
    }
    复制代码
  4. Main

    class Main {
       constructor() {
          this.alterLine('MyStack Area');
    
          let ms = new MyStack(10);
          for (let i = 1; i <= 10; i++) {
             ms.push(i);
             console.log(ms.toString());
          }
    
          console.log(ms.peek());
          this.show(ms.peek());
    
          while (!ms.isEmpty()) {
             console.log(ms.toString());
             ms.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. undo 操做-编辑器
  2. 系统调用栈-操做系统
  3. 括号匹配-编译器

以编程的方式体现栈的应用

  1. 括号匹配-编译器

    1. 不管是写表达式,这个表达式中有小括号、中括号、大括号,
    2. 天然会出现括号套括号的状况发生,
    3. 在这种状况下就必定会产生一个括号匹配的问题,
    4. 若是括号匹配是不成功的,那么编译器会进行报错。
  2. 编译器是如何检查括号匹配的问题?

    1. 原理是使用了一个栈。
  3. 能够经过解答 Leetcode 中的一个问题,

    1. 同时来看栈在括号匹配这个问题中的应用。
    2. Leetcode 是总部在美国硅谷一家
    3. 很是有年头又同时有信誉度的面向 IT 公司
    4. 面试这样一个在线的平台,
    5. 只须要注册一个 Leetcode 用户后,
    6. 就能够看到 Leetcode 上有很是多的问题,
    7. 对于每个问题会规定输入和输出以后,
    8. 而后就能够编写属于本身的逻辑,
    9. 更重要的是能够直接把你编写的这个程序
    10. 提交给这个网站,
    11. 这个网站会自动的判断你的逻辑书写的是否正确,
    12. 英文网址:leetcode.com
    13. 2017 中文网址:leetcode-cn.com
  4. leetcode.comleetcode-cn.com的区别

    1. leetcode-cn.com支持中文,
    2. leetcode-cn.com的题目数量没有英文版的多。
    3. leetcode-cn.com的探索栏目的内容没有英文版的多。
    4. leetcode-cn.com中的题目没有社区讨论功能,但英文版的有。
  5. leetcode 中第二十号题目:有效的括号

    1. 如:{ [ ( ) ] }
    2. 从左往右,先将左侧的括号入栈,
    3. 而后遇到右侧的括号时就查看栈顶的左侧括号进行匹配,
    4. 若是能够匹配表示括号有效,不然括号无效,
    5. 括号有效那么就将栈顶的左侧括号取出,
    6. 而后继续从左往右,左侧括号就入栈,右侧括号就匹配,
    7. 匹配成功就让左侧括号出栈,匹配失败就是无效括号。
    8. 其实栈顶元素反映了在嵌套的层级关系中,
    9. 最新的须要匹配的元素。
    10. 这个算法很是的简单,可是也很是的实用。
    11. 不少工具中都有这样的逻辑来检查括号的匹配。
    class Solution {
       isValid(s) {
          // leetcode 20. 有效的括号
          /** * @param {string} s * @return {boolean} */
          var isValid = function(s) {
             let stack = [];
    
             // 以遍历的方式进行匹配操做
             for (let i = 0; i < s.length; i++) {
                // 是不是正括号
                switch (s[i]) {
                   case '{':
                   case '[':
                   case '(':
                      stack.push(s[i]);
                      break;
                   default:
                      break;
                }
                // 是不是反括号
                switch (s[i]) {
                   case '}':
                      if (stack.length === 0 || stack.pop() !== '{') {
                         console.log('valid error. not parentheses. in');
                         return false;
                      }
                      break;
                   case ']':
                      if (stack.length === 0 || stack.pop() !== '[') {
                         console.log('valid error. not parentheses. in');
                         return false;
                      }
                      break;
                   case ')':
                      if (stack.length === 0 || stack.pop() !== '(') {
                         console.log('valid error. not parentheses. in');
                         return false;
                      }
                      break;
                   default:
                      break;
                }
             }
    
             // 是否所有匹配成功
             if (stack.length === 0) {
                return true;
             } else {
                console.log('valid error. not parentheses. out');
                return false;
             }
          };
    
          return isValid(s);
       }
    }
    复制代码
    class Main {
       constructor() {
          // this.alterLine("MyStack Area");
    
          // let ms = new MyStack(10);
          // for (let i = 1; i <= 10 ; i++) {
          // ms.push(i);
          // console.log(ms.toString());
          // }
    
          // console.log(ms.peek());
          // this.show(ms.peek());
    
          // while (!ms.isEmpty()) {
          // console.log(ms.toString());
          // ms.pop();
          // }
    
          this.alterLine('leetcode 20. 有效的括号');
          let s = new Solution();
          this.show(s.isValid('{ [ ( ) ] }'));
          this.show(s.isValid(' [ ( ] ) '));
       }
    
       // 将内容显示在页面上
       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();
    };
    复制代码
  6. leetcode 是一个很是好的准备面试的一个平台

    1. 同时它也是算法竞赛的一个入门的地方。
    2. 你能够经过题库来进行训练,
    3. 题库的右边有关于这些题目的标签,
    4. 你能够选择性的去练习,
    5. 并且能够根据难度来进行排序这些题目,
    6. 你不必定要所有答对,
    7. 由于这些题目不只仅只有一个标签。
  7. 若是你想使用你本身写的类,

    1. 那么你能够你本身写的自定义栈做为内部类来进行使用,
    2. 例如 把自定义栈的代码放到 Solution 类中,
    3. 那样也是可使用,
    4. 还样就顺便测试了你本身数据结构实现的逻辑是否正确。

学习方法讨论

  1. 不要完美主义。掌握好“度”。
    1. 太过于追求完美会把本身逼的太紧,
    2. 会产生各类焦虑的心态,. 最后甚至会怀疑本身,
    3. 温故而知新,不要中止不前,
    4. 掌握好这个度,不存在你把那些你认为彻底掌握了,
    5. 而后就成了某一个领域的专家,
    6. 相反一旦你产生很浓厚的厌恶感,
    7. 那么就意味着你即将会放弃或者已经选择了放弃,
    8. 虽然你以前想把它作到 100 分,
    9. 可是因为你的放弃让它变为 0 分。
  2. 学习本着本身的目标去。
    1. 不要在学的过程当中偏离了本身的目标。
    2. 要分清主次。
  3. 难的东西,你能够慢慢的回头看一看。
    1. 那样才会更加的柳暗花明,
    2. 更能提高本身的收获。

队列 Queue

  1. 队列也是一种线性的数据结构
    1. 依然就是将数据排成一排。
  2. 相比数组,队列对应的操做是数组的子集。
    1. 与栈只能在同一端添加元素和取出元素有所不一样,
    2. 在队列中只能从一端(队尾)添加元素,
    3. 只能从另外一端(队首)取出元素。
  3. 例如你去银行取钱
    1. 你须要排队,入队的人不容许插队,
    2. 因此他要从队尾开始排队,
    3. 而前面取完钱的会从队首离开,
    4. 而后后面的人再往前移动一位,
    5. 最后重复这个过程,
    6. 直到没人再排队取钱了。
  4. 队列是一种先进先出的数据结构(先到先得)
    1. First In First Out(FIFO) 先进先出

队列的实现

  1. Queue
    1. void enqueue(E):入队
    2. E dequeue():出队
    3. E getFront():查看队首的元素
    4. int getSize():获取队列中的实际元素大小
    5. boolean isEmpty():获取队列是否为空的 bool 值
  2. 写一个接口叫作 IMyQueue
    1. 让 MyQueue 实现这个接口
    2. 这样就符合了面向对象的特性。

代码示例

  1. class: MyArray, class: MyQueue, 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. Main

    class Main {
       constructor() {
          this.alterLine('MyQueue Area');
          let mq = new MyQueue(10);
          for (let i = 1; i <= 10; i++) {
             mq.enqueue(i);
             console.log(mq.toString());
          }
    
          console.log(mq.getFront());
          this.show(mq.getFront());
    
          while (!mq.isEmpty()) {
             console.log(mq.toString());
             mq.dequeue();
          }
       }
    
       // 将内容显示在页面上
       show(content) {
          document.body.innerHTML += `${content}<br /><br />`;
       }
    
       // 展现分割线
       alterLine(title) {
          let line = `--------------------${title}----------------------`;
          console.log(line);
          document.body.innerHTML += `${line}<br /><br />`;
       }
    }
    复制代码

队列的复杂度分析

  1. MyQueue
    1. void enqueue(E)O(1) 均摊
    2. E dequeue()O(n) 出队的性能消耗太大了
    3. E getFront()O(1)
    4. int getSize()O(1)
    5. boolean isEmpty()O(1)
  2. 出队的性能消耗太大了
    1. 若是有一百万条数据,每次都要操做一百万次,
    2. 那么须要优化它,要让他出队的时候时间复杂度为O(1)
    3. 而且还要让他入队的时候时间复杂度依然是O(1)
    4. 可使用循环队列的方式来解决这个问题。

循环队列

  1. 自定义队列的性能是有局限性的
    1. 出队操做时的时间复杂度为O(n)
    2. 要把他变为O(1)
  2. 当取出队列的第一个元素后,
    1. 第一个元素后面全部的元素位置不动,
    2. 这样一来时间复杂度就为O(1)了,
    3. 下一次再取元素的时候从第二个开始,
    4. 取完第二个元素以后,
    5. 第二个元素后面全部的元素位置也不动,
    6. 入队的话直接往队尾添加元素便可。
  3. 循环队列的使用
    1. 你能够先用一个数字变量 front 指向队首,
    2. 而后再用一个数字变量 tail 指向队尾,
    3. front 指向的是队列中的第一个元素,
    4. tail 指向的是队列中最后一个元素的后一个位置,
    5. 当队列总体为空的时候,它们才会指向同一个位置,
    6. 因此front == tail时队列就为空,
    7. 若是有一个元素入队了,
    8. front 会指向这个元素,
    9. 而 tail 会指向这个元素后一个位置(也就是 tail++),
    10. 而后再有一个元素入队了,
    11. front 仍是指向第一个元素的位置,
    12. 而 tail 会指向第二个元素的后一个位置(仍是 tail++),
    13. 而后再来四个元素入队了,
    14. front 仍是指向第一个元素的位置,
    15. 而 tail 会指向第六个元素的后一个位置(tail++四次),
    16. 以后 要出队两个元素,
    17. front 会指向第三个元素的位置(也就是 front++两次),
    18. front 从指向第一个元素变成指向第三个元素的位置,
    19. 由于前两个已经出队了,
    20. 这时候再入队一个元素,
    21. tail 会指向第七个元素的后一个位置(仍是 tail++),
    22. 这时队列的容量已经满了,可能须要扩容,
    23. 可是因为队列中有两个元素已经出队了,
    24. 那这两个位置空出来了,这时就须要利用这两个位置的空间了,
    25. 这就是循环队列了,以循环的方式重复利用空间,
    26. 自定义队列使用自定义数组实现的,
    27. 其实就是把数组当作一个环,数组中一共能够容纳 8 个元素,
    28. 索引是 0-7,那么 7 以后的索引应该是 0,tail 应该指向 0,
    29. 而不是认为整个数组的空间已经满了,
    30. 应该使用 tail 对数组的容量进行求余计算,
    31. tail 为 8,容量也为 8,求余以后为 0,因此 tail 应该指向 0,
    32. 这时再入队一个元素,tail 指向这个元素的后一个位置,即 1,
    33. 这时候若是再入队一个元素,那么此时 tail 和 front 相等,
    34. 可是那并不能证实队列为空,反而是队列满了,
    35. 因此须要在队列满以前进行判断,tail+1==front
    36. 就表示队列已满,当数组中只剩最后一个空间了,
    37. 队列就算是满的,由于再入队就会让 tail 与 front 相等,
    38. 而那个条件是队列已空才成立的,虽然对于整个数组空间来讲,
    39. 是有意识地浪费了一个空间,可是减小了很大的时间消耗,
    40. 因此当(tail+1)%c==front时就能够扩容了,
    41. tail+1==front变成(tail+1)%c==front是由于
    42. tail 从数组的末端跑到前端是有一个求余的过程,
    43. 例如 front 指向的是第一个元素,而 tail 指向的第六个元素以后的位置,
    44. 那么此时 front 为 0,tail 为 7,容量为 8,还有一个浪费掉的空间,
    45. 这时候(tail+1)%c==front,因此队列满了,
    46. 这就是循环队列全部的具体实现必须遵照的规则,
    47. 全部的 front 和 tail 向后移动的过程都要是这种循环的移动,
    48. 例如钟表,11 点钟的下一个钟头为 12 点钟,也能够管它叫作 0 点,
    49. 以后又会变成 1 点、2 点、3 点、4 点依次类推,
    50. 因此整个循环队列的索引也是像钟表同样造成了一个环,
    51. 只不过不必定有 12 刻度,而刻度的数量是由数组的容量(空间总数)决定的,
    52. 这就是循环队列的原理。
  4. 使用循环队列以后,
    1. 出队操做再也不是总体往前移动一位了
    2. 而是经过改变 front 的指向,
    3. 入队操做则是改变 tail 的指向,
    4. 整个操做循环往复,
    5. 这样一来出队入队的时间复杂度都为O(1)了。

循环队列的简单实现解析

  1. 循环队列 MyLoopQueue
    1. 他的实现与 MyQueue 有很大的不一样,
    2. 因此就不使用 MyArray 自定义动态数组了。
  2. 循环队列要从底层从新开始写起
    1. data:一个数组。
    2. front: 指向队头有效元素的索引。
    3. tail: 指向队尾有效元素的后一个位置的索引。
    4. size: 经过 front 和 tail 也能够作到循环。
    5. 可是使用 size 可以让逻辑更加的清晰明了。
  3. 循环队列实现完毕以后,
    1. 你能够不使用 size 来进行循环队列的维护,
    2. 而完彻底全的使用 front 和 tail,
    3. 这样难度会稍微的难一点,
    4. 由于具体逻辑须要特别的当心,
    5. 会有一些小陷阱。
    6. 能够试着添加 resize 数组扩容缩容功能到极致,
    7. 能够锻炼逻辑能力、程序编写调试能力等等。

循环队列的实现

  1. 入队前先判断队列是否已经满了
    1. 判断方式 (tail + 1) % data.length == front
    2. 判断分析 (队尾指向的索引 + 1)余以数组的容量是否为队首指向的索引,
  2. 从用户的角度上来看
    1. 队列里就是有这么多元素,
    2. 一侧是队首一侧是队尾,
    3. 其它的内容包括实际的数组的大小是用户指定的容量大小+1,
    4. 这些实现细节,用户是所有不知道的,给用户屏蔽掉了,
    5. 这就是封装自定义数据结构的目的所在,
    6. 用户在具体使用这些自定义数据结构的时候,
    7. 只须要了解接口中所涉及到的这些方法便可,
    8. 至于它的内部细节用户彻底能够不用关心。

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

  1. 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(Math.floor(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(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;
       }
    }
    复制代码
  2. Main

    class Main {
       constructor() {
          this.alterLine('MyLoopQueue Area');
          let mlq = new MyQueue(10);
          for (let i = 1; i <= 10; i++) {
             mlq.enqueue(i);
             console.log(mlq.toString());
          }
    
          console.log(mlq.getFront());
          this.show(mlq.getFront());
    
          while (!mlq.isEmpty()) {
             console.log(mlq.toString());
             mlq.dequeue();
          }
       }
    
       // 将内容显示在页面上
       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)
    1. 使用循环队列的方式后,
    2. 出队时时间复杂度为O(1)
    3. 复杂度的分析只是一个抽象上的理论结果,
    4. 具体这个变化在性能上意味着会有一个质的飞跃,
    5. 队列中元素越多,性能就更可以体现出来。

自定义队列的时间复杂度对比

  1. MyQueue:数组队列,使用了自定义数组
    1. void enqueue(E)O(1) 均摊
    2. E dequeue()O(n) 出队的性能消耗太大了
    3. E getFront()O(1)
    4. int getSize()O(1)
    5. boolean isEmpty()O(1)
  2. MyLoopQueue:循环队列,没有使用自定义数组
    1. void enqueue(E)O(1) 均摊
    2. E dequeue()O(1) 均摊
    3. E getFront()O(1)
    4. int getSize()O(1)
    5. boolean isEmpty()O(1)

循环队列的复杂度分析

  1. 经过设置循环队列底层的机制
    1. 虽然稍微比数组队列要复杂一些,
    2. 可是这些复杂的工做是值得的,
    3. 由于他使得在数组队列中,
    4. 出队本该有O(n)的复杂度变为了O(1)的复杂度,
    5. 可是这个O(1)为均摊的时间复杂度,
    6. 由于出队仍是会涉及到缩容的操做,
    7. 在缩容的过程当中仍是免不了对队列中全部的元素进行一次遍历,
    8. 可是因为不可能每一次操做都会触发缩容操做来遍历全部的元素,
    9. 因此应该使用均摊复杂度的分析方式,那样才更加合理。
  2. 循环队列中全部的操做都是O(1)的时间复杂度。
  3. O(n)的复杂度要比O(1)要慢,
    1. 可是具体会慢多少能够经过程序来进行测试,
    2. 这样就可以知道在算法领域和数据结构领域
    3. 要费这么大的劲去研究更加优化的操做
    4. 这背后实际的意义到底在哪里。
  4. 让这两个队列进行入队和出队操做,
    1. 操做的次数为 100000 次,
    2. 经过在同一台机器上的耗时状况,
    3. 就可以知道性能有什么不一样。
  5. 数据队列与循环队列十万次入队出队操做后的结果是:
    1. MyQueue,time:15.463472711s
    2. MyLoopQueue,time:0.009602136s
    3. 循环队列就算操做一亿次,
    4. 时间也才MyLoopQueue,time:2.663835877s
    5. 这个差距主要是在出队的操做中体现出来的,
    6. 这个性能差距是上千倍,因此这也是性能优化的意义。
  6. 测试性能时,不要只测试一次,你能够测试 100 次
    1. 取平均值便可,由于这不光和你的程序相关,
    2. 还会和你当前计算机的状态有关,
    3. 特别是在两个算法的时间复杂度一致时,
    4. 测试性能时可能出入会特别大,
    5. 由于这有多方面缘由、如语法、语言、编译器、解释器等等,
    6. 这些都会致使你代码真正运行的逻辑机制
    7. 和你理论分析的是不同的,
    8. 可是当两个算法的时间复杂度不一致时,
    9. 这时候测试性能的结果确定会有巨大的差别,
    10. O(1)O(n)O(n)O(n^2)O(n)O(logn)

代码示例

  1. PerformanceTest

    class PerformanceTest {
       constructor() {}
    
       testQueue(queue, openCount) {
          let startTime = Date.now();
    
          let random = Math.random;
          for (var i = 0; i < openCount; i++) {
             queue.enqueue(random() * openCount);
          }
    
          while (!queue.isEmpty()) {
             queue.dequeue();
          }
    
          let endTime = Date.now();
    
          return this.calcTime(endTime - startTime);
       }
    
       // 计算运行的时间,转换为 天-小时-分钟-秒-毫秒
       calcTime(result) {
          //获取距离的天数
          var day = Math.floor(result / (24 * 60 * 60 * 1000));
    
          //获取距离的小时数
          var hours = Math.floor((result / (60 * 60 * 1000)) % 24);
    
          //获取距离的分钟数
          var minutes = Math.floor((result / (60 * 1000)) % 60);
    
          //获取距离的秒数
          var seconds = Math.floor((result / 1000) % 60);
    
          //获取距离的毫秒数
          var milliSeconds = Math.floor(result % 1000);
    
          // 计算时间
          day = day < 10 ? '0' + day : day;
          hours = hours < 10 ? '0' + hours : hours;
          minutes = minutes < 10 ? '0' + minutes : minutes;
          seconds = seconds < 10 ? '0' + seconds : seconds;
          milliSeconds =
             milliSeconds < 100
                ? milliSeconds < 10
                   ? '00' + milliSeconds
                   : '0' + milliSeconds
                : milliSeconds;
    
          // 输出耗时字符串
          result =
             day +
             '天' +
             hours +
             '小时' +
             minutes +
             '分' +
             seconds +
             '秒' +
             milliSeconds +
             '毫秒' +
             ' <<<<============>>>> 总毫秒数:' +
             result;
    
          return result;
       }
    }
    复制代码
  2. Main

    class Main {
       constructor() {
          this.alterLine('Queues Comparison Area');
          let mq = new MyQueue();
          let mlq = new MyLoopQueue();
          let performanceTest = new PerformanceTest();
    
          let mqInfo = performanceTest.testQueue(mq, 10000);
          let mlqInfo = performanceTest.testQueue(mlq, 10000);
    
          this.alterLine('MyQueue Area');
          console.log(mqInfo);
          this.show(mqInfo);
    
          this.alterLine('MyLoopQueue Area');
          console.log(mlqInfo);
          this.show(mlqInfo);
       }
    
       // 将内容显示在页面上
       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. 如在业务方面你须要排队,
    3. 或者更加专业的一些领域,
    4. 好比 网络数据包的排队、
    5. 操做系统中执行任务的排队等,
    6. 均可以使用队列。
  2. 队列自己是一个很复杂的问题
    1. 对于排队来讲,队首到底怎么定义,
    2. 是有多样的定义方式的,也正由于如此,
    3. 因此存在广义队列这个概念,
    4. 这两种自定义队列
    5. 在组建计算机世界的其它算法逻辑的时候
    6. 也是有重要的应用的,最典型的应用是广度优先遍历。
相关文章
相关标签/搜索