前端面试手写必备【实现常见八大数据结构一】

🧑‍💻 写在开头

篇幅较长建议收藏食用😁

本篇是我们手写前端代码的第一篇,本系列将会介绍前端面试中常见的手写题,包括实现常见数据结构、实现高频手写题,比如手写实现深拷贝、es5手写实现类的继承、手写实现前端常见设计模式、实现async&await等。

🥑 你能学到什么?

希望在你阅读本篇文章之后,不会觉得浪费了时间。如果你跟着读下来,你将会学到:

  • 手写实现常见数据结构的思路:数组、栈、队列、单向链表、双向链表、集合、字典、哈希表、二叉树等
  • 实现数据结构这部分的难点在于,哈希表的哈希函数的实现、二叉树的删除,由于篇幅限制我们将这两个难点放置到后续其他篇中实现中实现。

预告

下一篇,# 前端面试手写必备【实现哈希表&哈希分析】

🍎 推荐阅读

工程化系列

本系列是一个从0到1的实现过程,如果您有耐心跟着实现,您可以实现一个完整的react18 + ts5 + webpack5 + 代码质量&代码风格检测&自动修复 + storybook8 + rollup + git action实现的一个完整的组件库模板项目。如果您不打算自己配置,也可以直接clone组件库仓库切换到rollup_comp分支即是完整的项目,当前实现已经足够个人使用,后续我们会新增webpack5优化、按需加载组件、实现一些常见的组件封装:包括但不限于拖拽排序、瀑布流、穿梭框、弹窗等

🥝 一、数组

1.数组理论知识

  1. 定义:数组是一种由相同类型的元素组成的数据集合,这些元素按照一定的顺序排列,并且可以通过索引(下标)来访问。

  2. 下标访问:数组中的元素可以通过下标来访问,下标通常从0开始递增。这是因为数组在内存中是一段连续的存储空间,每个元素占据固定大小的内存单元。通过计算偏移量(索引 * 单个元素大小),可以快速定位数组中的任意元素。

  3. 内存存储:数组在内存中是连续存储的,即数组的各个元素依次存储在相邻的内存单元中。这种存储方式使得通过下标访问元素的时间复杂度为O(1),即具有很高的访问效率。

  4. 数组的特点包括:

    • 元素类型相同。
    • 连续存储,可以通过下标快速访问。
    • 大小固定,无法动态扩展。
  5. 动态数组:

    • 问题:动态数组是什么?它与静态数组有何区别?
    • 解答:动态数组是一种可以动态增长和缩小的数组,与静态数组不同的是,它的大小可以根据需要动态调整,通常通过重新分配内存来实现。
    • 数组在大多数编程语言中的大小固定,无法动态扩展的原因主要是因为数组在内存中是一段连续的存储空间,这段空间在创建数组时就已经确定了大小。当我们创建一个数组时,计算机会为它分配一段连续的内存空间来存储元素。这段内存空间的大小是在数组创建时确定的,通常是根据数组的类型和长度计算得出的。因此,一旦数组被创建,它的大小就固定了,无法直接在原来的内存空间上进行扩展。
    • 要想扩展数组,通常需要进行以下操作:分配一块新的内存空间,大小比原数组大。将原数组中的元素复制到新的内存空间中。释放原数组所占用的内存空间。

2.数组的优缺点

数组的优缺点

  • 问题:数组有哪些优点和缺点?

  • 解答:数组的优点包括:

    • 快速访问:通过下标可以快速访问任意位置的元素。
    • 简单高效:数组的存储和访问操作都非常简单高效。
  • 缺点包括:

    • 大小固定:无法动态扩展,可能导致内存浪费或需要重新分配更大的空间。
    • 插入和删除元素效率低:需要移动其他元素,效率较低。
  • 在大多数编程语言中,数组的内存空间是一段连续的存储空间,每个元素占据固定大小的内存。当我们在数组中删除或插入元素时,如果要保持数组的顺序不变,就需要移动元素。

    • 删除元素

      • 如果要删除数组中的某个元素,那么这个元素之后的所有元素都需要向前移动一个位置,以填补被删除元素的空缺。这是因为数组的内存空间是连续的,删除一个元素会造成后续元素的位置变化,为了保持数组的连续性和顺序不变,需要移动元素。
    • 插入元素

      • 如果要在数组中插入一个元素,那么插入位置之后的所有元素都需要向后移动一个位置,为新元素腾出空间。同样地,为了保持数组的连续性和顺序不变,需要移动元素。

3.JS中的数组

数组的标准定义是:一个存储元素的线性集合 (collection),元素可以通过索引来任意存取,索引通常是数字,用来计算元素之间存储位置的偏移量。几乎所有的编程语言都有类似的数据结构。然而 Javascript 的数组却略有不同。

JavaScript 中的数组是一种特殊的对象,用来表示偏移量的索引是该对象的属性,索引可能是整数。然而,这些数字索引在内部被转换为字符串类型,这是因为 JavaScript 对象中的属性名必须是字符串。数组在Javascript 中只是一种特殊的对象,所以效率上不如其他语言中的数组高。

Javaseript 中的数组,严格来说应该称作对象,是特殊的 Javascript 对象,在内部被归类为数组。由于 Array 在Javascript 中被当作对象,因此它有许多属性和方法可以在编程时使用。

创建数组

在 JavaScript 中,有几种常见的方式来创建数组:

  1. 使用数组字面量

    js 复制代码
    let arr = [1, 2, 3, 4]; // 创建一个包含4个元素的数组

    这种方式是最简单和常见的创建数组的方式,直接使用方括号**[]**来包裹数组元素,每个元素之间用逗号分隔。

  2. 通过构造函数创建

    js 复制代码
    let arr = new Array(1, 2, 3, 4); // 使用构造函数创建数组

    可以使用**new Array()**语法来创建数组,传入的参数是数组的元素。需要注意的是,如果传入的参数是一个数字,则表示创建一个具有该长度的稀疏数组,而不是包含该数字作为唯一元素的数组。

  3. 使用 Array.of() 方法

    js 复制代码
    let arr = Array.of(1, 2, 3, 4); // 使用 Array.of() 方法创建数组

    **Array.of()**方法是 ES6 新增的方法,用于创建具有指定元素的数组。

  4. 使用 Array.from() 方法

    js 复制代码
    let arr = Array.from([1, 2, 3, 4]); // 使用 Array.from() 方法将类数组对象或可迭代对象转换为数组

    Array.from() 方法是 ES6 新增的方法,可以将类数组对象或可迭代对象转换为数组。

这些是在 JavaScript 中创建数组的常见方式,每种方式都有其特点和适用场景,开发者可以根据实际需求选择合适的方式来创建数组。

JavaScript 中常用的数组添加方法包括以下几种:

  1. 使用 push() 方法添加到末尾

    • push() 方法用于向数组的末尾添加一个或多个元素,并返回新数组的长度。该方法会改变原数组。

    示例:

    js 复制代码
    let arr = [1, 2, 3];
    arr.push(4); // arr 变为 [1, 2, 3, 4]
  2. 使用 unshift() 方法添加到开头

    • unshift() 方法用于向数组的开头添加一个或多个元素,并返回新数组的长度。该方法会改变原数组。

    示例:

    js 复制代码
    let arr = [2, 3, 4];
    arr.unshift(1); // arr 变为 [1, 2, 3, 4]
  3. 使用 splice() 方法插入元素

    • splice(start, deleteCount, item1, item2, ...) 方法除了可以删除元素外,还可以在指定位置插入新的元素。其中 start 是插入位置的索引,deleteCount 是删除的元素个数,item1, item2, ... 是要插入的元素。该方法会改变原数组。

    示例:

    js 复制代码
    let arr = [1, 2, 4];
    arr.splice(2, 0, 3); // arr 变为 [1, 2, 3, 4]
  4. 使用 concat() 方法连接数组

    • concat() 方法用于连接两个或多个数组,并返回一个新数组,不改变原数组。

    示例:

    js 复制代码
    let arr1 = [1, 2];
    let arr2 = [3, 4];
    let newArr = arr1.concat(arr2); // newArr 为 [1, 2, 3, 4],arr1 和 arr2 不变

这些方法提供了不同的添加元素到数组的方式,可以根据实际需求选择合适的方法进行操作。

JavaScript 中常用的数组删除方法包括以下几种:

  1. 使用 pop() 方法删除末尾元素

    • pop() 方法用于删除数组的最后一个元素,并返回被删除的元素。该方法会改变原数组的长度。

    示例:

    js 复制代码
    let arr = [1, 2, 3, 4];
    let removedElement = arr.pop(); // 返回 4,arr 变为 [1, 2, 3]
  2. 使用 shift() 方法删除第一个元素

    • shift() 方法用于删除数组的第一个元素,并返回被删除的元素。该方法会改变原数组的长度和索引。

    示例:

    js 复制代码
    let arr = [1, 2, 3, 4];
    let removedElement = arr.shift(); // 返回 1,arr 变为 [2, 3, 4]
  3. 使用 splice() 方法删除指定位置的元素

    • splice(start, deleteCount, item1, item2, ...) 方法可以删除数组中从 start 位置开始的 deleteCount 个元素,并可以在删除的位置插入新的元素。该方法会改变原数组。

    示例:

    js 复制代码
    // 从下标为1的位置删除两个元素
    let arr = [1, 2, 3, 4];
    let removedElements = arr.splice(1, 2); // 返回 [2, 3],arr 变为 [1, 4]
  4. 使用 filter() 方法删除符合条件的元素

    • filter(callback) 方法用于根据指定的条件过滤数组中的元素,并返回一个新数组,不改变原数组。可以结合条件判断函数来实现删除符合条件的元素。

    示例:

    js 复制代码
    let arr = [1, 2, 3, 4];
    let newArr = arr.filter(item => item !== 2); // newArr 为 [1, 3, 4],arr 不变

这些方法提供了不同的删除数组元素的方式,可以根据实际需求选择合适的方法进行操作。

JavaScript 中常用的数组修改方法包括以下几种:

  1. 直接通过索引修改元素

    • 可以直接通过索引来修改数组中的元素值。

    示例:

    js 复制代码
    let arr = [1, 2, 3];
    arr[1] = 4; // arr 变为 [1, 4, 3]
  2. 使用 splice() 方法替换元素

    • splice(start, deleteCount, item1, item2, ...) 方法除了可以删除和插入元素外,还可以替换指定位置的元素。其中 start 是替换位置的索引,deleteCount 是删除的元素个数,item1, item2, ... 是要替换的元素。该方法会改变原数组。

    示例:

    js 复制代码
    let arr = [1, 2, 3];
    arr.splice(1, 1, 4); // arr 变为 [1, 4, 3]
  3. 使用 map() 方法修改元素

    • map(callback) 方法会创建一个新数组,新数组的元素是原数组经过某种操作后的结果。可以结合回调函数对每个元素进行修改。

    示例:

    js 复制代码
    let arr = [1, 2, 3];
    let newArr = arr.map(item => item * 2); // newArr 为 [2, 4, 6],arr 不变
  4. 使用 forEach() 方法修改元素

    • forEach(callback) 方法会对数组的每个元素执行一次指定的函数。可以直接在回调函数中修改元素的值。

    示例:

    js 复制代码
    let arr = [1, 2, 3];
    arr.forEach((item, index, array) => {
      array[index] = item * 2;
    }); // arr 变为 [2, 4, 6]

这些方法提供了不同的修改数组元素的方式,可以根据实际需求选择合适的方法进行操作。

JavaScript 中查找数组元素可以使用多种方法,下面是一些常见的方法:

  1. indexOf()

    返回在数组中可以找到一个给定元素的第一个索引,如果不存在,则返回-1。

    js 复制代码
    const array = [2, 5, 9];
    console.log(array.indexOf(2));// 0
    console.log(array.indexOf(7));// -1
  2. find()

    返回数组中满足提供的测试函数的第一个元素的值。如果没有找到符合条件的元素,则返回undefined

    js 复制代码
    const array = [5, 12, 8, 130, 44];
    const found = array.find(element => element > 10);
    console.log(found);// 12
  3. findIndex()

    返回数组中满足提供的测试函数的第一个元素的索引。如果没有找到符合条件的元素,则返回-1。

    js 复制代码
    const array = [5, 12, 8, 130, 44];
    const isLargeNumber = (element) => element > 13;
    console.log(array.findIndex(isLargeNumber));// 3
  4. includes()

    用来判断一个数组是否包含一个指定的值,根据情况,如果包含则返回true,否则返回false。

    js 复制代码
    const array = [1, 2, 3];
    console.log(array.includes(2));// true
    console.log(array.includes(4));// false
  5. filter()

    创建一个新数组, 其包含通过所提供函数实现的测试的所有元素。

    js 复制代码
    const words = ['spray', 'limit', 'elite', 'exuberant', 'destruction', 'present'];
    const result = words.filter(word => word.length > 6);
    console.log(result); // ["exuberant", "destruction", "present"]
  6. some()

    测试数组中是不是至少有1个元素通过了被提供的函数测试。它返回的是一个布尔值。

    js 复制代码
    const array = [1, 2, 3, 4, 5];
    const even = (element) => element % 2 === 0;
    console.log(array.some(even));// true
  7. every()

    测试一个数组内的所有元素是否都能通过某个指定函数的测试。它返回一个布尔值。

    js 复制代码
    const isBelowThreshold = (currentValue) => currentValue < 40;
    const array1 = [1, 30, 39, 29, 10, 13];
    console.log(array1.every(isBelowThreshold));// true

这些方法都是 ECMAScript 5 或更高版本中引入的,因此在现代浏览器中应该都可以使用。需要注意的是,find()findIndex() 是 ECMAScript 6 中新增的方法。

遍历

  1. 使用 for 循环遍历

    js 复制代码
    let arr = [1, 2, 3];
    for (let i = 0; i < arr.length; i++) {
      console.log(arr[i]);
    }
  2. 使用 forEach() 方法遍历

    js 复制代码
    let arr = [1, 2, 3];
    arr.forEach(item => {
      console.log(item);
    });
  3. 使用 map() 方法遍历

    js 复制代码
    let arr = [1, 2, 3];
    let newArr = arr.map(item => {
      return item * 2;
    });
  4. 使用 for...of 循环遍历

    js 复制代码
    let arr = [1, 2, 3];
    for (let item of arr) {
      console.log(item);
    }

这些方法各有特点,可以根据实际需求选择合适的方法进行数组遍历。

其他方法

  1. concat() 方法

    • concat() 方法用于连接两个或多个数组,并返回一个新数组。

    示例:

    js 复制代码
    let arr1 = [1, 2];
    let arr2 = [3, 4];
    let newArr = arr1.concat(arr2); // newArr 为 [1, 2, 3, 4]
  2. slice() 方法

    • slice(start, end) 方法用于从原数组中提取出指定范围的元素,返回一个新数组。

    示例:

    js 复制代码
    let arr = [1, 2, 3, 4];
    let newArr = arr.slice(1, 3); // newArr 为 [2, 3]
  3. indexOf() 和 lastIndexOf() 方法

    • indexOf(item) 方法用于返回数组中第一个匹配项的索引,如果没有找到匹配项则返回 -1。
    • lastIndexOf(item) 方法用于返回数组中最后一个匹配项的索引,如果没有找到匹配项则返回 -1。

    示例:

    js 复制代码
    let arr = [1, 2, 3, 4, 2];
    let index = arr.indexOf(2); // index 为 1
    let lastIndex = arr.lastIndexOf(2); // lastIndex 为 4
  4. join() 方法

    • join(separator) 方法用于将数组中的所有元素连接成一个字符串。可以指定一个分隔符来分隔数组元素,默认为逗号。

    示例:

    js 复制代码
    let arr = [1, 2, 3];
    let str = arr.join('-'); // str 为 "1-2-3"
  5. toString() 方法

    • toString() 方法用于将数组转换为字符串,并返回结果。

    示例:

    js 复制代码
    let arr = [1, 2, 3];
    let str = arr.toString(); // str 为 "1,2,3"
  6. filter() 方法

    • filter(callback) 方法用于筛选数组中符合指定条件的元素,并返回一个新数组。

    示例:

    js 复制代码
    let arr = [1, 2, 3, 4];
    let newArr = arr.filter(item => item % 2 === 0); // newArr 为 [2, 4]
  7. find() 和 findIndex() 方法

    • find(callback) 方法用于返回数组中第一个符合条件的元素,如果没有找到则返回 undefined。
    • findIndex(callback) 方法用于返回数组中第一个符合条件的元素的索引,如果没有找到则返回 -1。

    示例:

    js 复制代码
    let arr = [1, 2, 3, 4];
    let found = arr.find(item => item > 2); // found 为 3
    let foundIndex = arr.findIndex(item => item > 2); // foundIndex 为 2
  8. sort() 方法

    • sort(compareFunction) 方法用于对数组元素进行排序,默认按照字符串 Unicode 代码点排序。可以传入一个比较函数来指定排序规则。

    示例:

    js 复制代码
    let arr = [3, 1, 4, 2];
    arr.sort((a, b) => a - b); // arr 变为 [1, 2, 3, 4]
  9. reverse() 方法

    • reverse() 方法用于颠倒数组中元素的顺序,原地修改数组。

    示例:

    js 复制代码
    let arr = [1, 2, 3, 4];
    arr.reverse(); // arr 变为 [4, 3, 2, 1]
  10. reduce() 和 reduceRight() 方法

    • reduce(callback, initialValue) 方法用于累积数组的每个元素,并返回一个累加的结果。
    • reduceRight(callback, initialValue) 方法与 reduce() 类似,但是从数组的末尾开始累加。

    示例:

    js 复制代码
    let arr = [1, 2, 3, 4];
    let sum = arr.reduce((accumulator, currentValue) => accumulator + currentValue, 0); // sum 为 10
  11. 使用 every() 方法遍历

    • every(callback) 方法用于检测数组的所有元素是否都符合指定条件。它会对数组中的每个元素执行一次回调函数,直到找到一个不符合条件的元素为止。

    示例:

    js 复制代码
    let arr = [1, 2, 3];
    let result = arr.every(item => item > 0); // result 为 true
  12. 使用 some() 方法遍历

    • some(callback) 方法用于检测数组的是否至少有一个元素符合指定条件。它会对数组中的每个元素执行一次回调函数,直到找到一个符合条件的元素为止。

    示例:

    js 复制代码
    let arr = [1, 2, 3];
    let result = arr.some(item => item > 2); // result 为 true

4.实现数组结构

没什么可说的时间思路比较简单,看注释即可。

js 复制代码
/**
 * 基本数组结构
 */
class BasicArray {
  constructor() {
    this.length = 0;
    this.data = {};
  }

  /**
   * 末尾增加元素
   * @param {*} item
   */
  add(item) {
    this.data[this.length] = item;
    this.length++;
  }

  /**
   * 任意位置插入元素
   * @param {*} index
   * @param {*} item
   */
  insert(index, item) {
    if (index < 0) index = 0;
    if (index > this.length) index = this.length;
    // 从最后一个开始移动,让出i的位置
    if (index >= 0 && index <= this.length) {
      for (let i = this.length; i > index; i--) {
        this.data[i] = this.data[i - 1];
      }
      this.data[index] = item;
    }
    this.length++;
  }

  /**
   * 删除指定位置元素
   * @param {*} index
   * @returns
   */
  delete(index) {
    const deleted = this.data[index];
    // 用i后面的元素覆盖之前的元素
    for (let i = index; i < this.length - 1; i++) {
      this.data[i] = this.data[i + 1];
    }
    // 前面的元素被填充了,最后一个空出来了
    delete this.data[this.length - 1];
    // 记得长度-1
    this.length--;
    return deleted;
  }

  /**
   * 更新元素
   * @param {*} index
   * @param {*} item
   */
  update(index, item) {
    this.data[index] = item;
  }

  /**
   * 获取数组长度
   * @returns
   */
  getLength() {
    return this.length;
  }

  /**
   * 获取元素
   * @param {*} index
   * @returns
   */
  get(index) {
    return this.data[index];
  }

  /**
   * 获取整个数组
   * @returns
   */
  getArray() {
    return this.data;
  }

  /**
   * 打印帮助函数,用来判断是否修改了原数组
   * @param {*} operateName
   * @param {*} array
   * @param {*} callback
   */
  logHelper(operateName, array, callback) {
    const input = [...array];
    const result = callback();

    console.log({
      operation: operateName,
      arrayBefore: input,
      arrayAfter: array,
      mutates: this.mutatesArray(input, array),
      result,
    });
  }

  /**
   * 浅比较判断数组是否修改
   * @param {*} array1 原始数组
   * @param {*} array2 修改后的数组
   * @returns
   */
  mutatesArray(array1, array2) {
    // 长度不同肯定不相等
    if (array1.length !== array2.length) return true;
    for (let i = 0; i < array2.length; i++) {
      if (array1[i] !== array2[i]) {
        return true;
      }
    }
    return false;
  }
}

// // 基本数组使用示例
// const myArray = new BasicArray();
// myArray.insert(0, 'a');
// myArray.insert(1, 'b');
// myArray.insert(2, 'c');
// myArray.print(); // ['a', 'b', 'c']

// myArray.update(1, 'x');
// myArray.print(); // ['a', 'x', 'c']

// myArray.delete(0);
// myArray.print(); // ['x', 'c']

// console.log(myArray.getLength()); // 2

module.exports = BasicArray;

🍒 二、栈

1.理论知识

栈(Stack)是一种基于先进后出(LIFO,Last In First Out)原则的数据结构,类比为堆放盘子。在栈中,元素的添加和删除操作都发生在同一端,这一端被称为栈顶,另一端被称为栈底。

基本操作:

  1. 压栈(Push) :向栈顶添加元素。
  2. 出栈(Pop) :从栈顶移除元素。
  3. 查看栈顶元素(peek) :查看栈顶元素,但不移除。
  4. 判空(isEmpty) :检查栈是否为空。
  5. 清空栈元素(clear):移除栈里的所有元素。
  6. 获取栈大小(Size) :获取栈中元素的数量。

应用场景:

  1. 函数调用栈:用于保存函数调用过程中的局部变量和返回地址。
  2. 表达式求值:将中缀表达式转换为后缀表达式并计算。
  3. 浏览器历史记录:浏览器使用栈来存储访问过的页面,可以通过后退按钮回到上一个页面。
  4. 撤销操作:编辑器和图形软件使用栈来实现撤销操作。

实现方式:

  1. 数组实现:使用数组来存储栈中的元素,栈顶对应数组的末尾。
  2. 链表实现:使用链表来存储栈中的元素,每个节点包含一个元素和指向下一个节点的指针。

时间复杂度:

  1. 压栈、出栈、查看栈顶元素的时间复杂度为 O(1)。
  2. 判空和获取栈大小的时间复杂度为 O(1)。

2.优缺点

优点:

  1. 简单高效:栈的操作简单明了,压栈、出栈等操作时间复杂度都是 O(1),效率高。
  2. 内存管理:栈内存管理由系统自动处理,无需手动分配和释放内存,减少内存泄漏的风险。
  3. 函数调用:函数调用时使用栈来保存局部变量和返回地址,方便实现递归和函数嵌套。
  4. 逆序输出:逆序输出数据时,栈可以提供一种简单的解决方案。

缺点:

  1. 容量限制:栈的大小在创建时固定,无法动态扩展,可能会导致栈溢出。
  2. 局限性:栈只能在栈顶进行操作,限制了一些数据结构的操作,如队列的快速插入和删除。
  3. 不灵活:由于栈的特性,一些场景下可能需要额外的数据结构来实现更复杂的功能,增加了复杂性和内存消耗。

3.实现栈结构

这里通过数组来实现,当然你也可以用链表来实现,思路是一致的,需要关注的地方在注释中。

js 复制代码
/**
 * 以数组尾部作为栈顶
 */
class BaseStack {
  stackData = [];

  initStackByArr(arr) {
    this.stackData = arr;
  }

  push(elem) {
    this.stackData.push(elem);
  }

  pop() {
    const elem = this.stackData.pop();
    return elem;
  }

  peek() {
    return this.stackData[this.stackData.length - 1];
  }

  isEmpty() {
    return this.stackData.length === 0;
  }

  size() {
    return this.stackData.length;
  }

  printf() {
    console.log(this.stackData);
  }
}

const stack = new BaseStack();
// 情况下代码模拟
stack.push(6);
stack.push(5);
stack.printf();
stack.pop(); // 5
stack.printf();
stack.push(4);
stack.printf();
stack.pop(); // 4
stack.push(3);
stack.printf();
stack.pop(); // 3
stack.pop(); // 6
stack.push(2);
stack.push(1);
stack.printf();
stack.pop(); // 1
stack.pop(); // 2

stack.printf();

🍉 三、队列

1.理论知识

队列(Queue)是一种基于先进先出(FIFO,First In First Out)原则的数据结构,类比为排队买票。在队列中,元素的添加操作(入队)发生在队尾,而删除操作(出队)发生在队头。

基本操作:

  1. 入队(Enqueue) :向队尾添加元素。
  2. 出队(Dequeue) :从队头移除元素。
  3. 查看队头元素(Front) :查看队头元素,但不移除。
  4. 查看队尾元素(Rear) :查看队尾元素,但不移除。
  5. 判空(isEmpty) :检查队列是否为空。
  6. 获取队列大小(Size) :获取队列中元素的数量。

应用场景:

  1. 任务调度:多任务系统中,使用队列来调度任务的执行顺序。
  2. 缓存:缓存中的数据通常按照访问顺序排列,使用队列来管理缓存数据的淘汰。
  3. 广度优先搜索:在图算法中,使用队列来存储待访问的节点。
  4. 打印队列:打印任务按照提交顺序排队执行,使用队列来管理打印任务。

实现方式:

  1. 数组实现:使用数组来存储队列中的元素,队头和队尾分别对应数组的两端。
  2. 链表实现:使用链表来存储队列中的元素,每个节点包含一个元素和指向下一个节点的指针。

时间复杂度:

  1. 入队、出队、查看队头和队尾元素的时间复杂度为 O(1)。
  2. 判空和获取队列大小的时间复杂度为 O(1)。

2.优缺点

优点:

  1. 顺序性:队列保持了元素的顺序性,保证了先进先出的特性。
  2. 简单高效:队列的操作简单明了,入队、出队等操作时间复杂度都是 O(1),效率高。
  3. 内存管理:队列内存管理由系统自动处理,无需手动分配和释放内存,减少内存泄漏的风险。
  4. 应用广泛:队列在计算机科学中有广泛的应用,如任务调度、广度优先搜索等。

缺点:

  1. 容量限制:队列的大小在创建时固定,无法动态扩展,可能会导致队列溢出。
  2. 不灵活:由于队列的特性,只能在队头和队尾进行操作,限制了一些数据结构的操作,如栈的快速插入和删除。
  3. 性能问题:在需要频繁插入和删除元素的场景中,队列可能不如链表等数据结构高效。

3.实现队列结构

实现很简单

js 复制代码
class BaseQueue {
  queueData = [];

  enqueue(elem) {
    this.queueData.push(elem);
  }

  dequeue() {
    const delElem = this.queueData.shift();
    return delElem;
  }

  front() {
    return this.queueData[0];
  }

  rear() {
    return this.queueData[this.queueData.length - 1];
  }

  isEmpty() {
    return this.queueData.length === 0;
  }

  size() {
    return this.queueData.length;
  }
}

🍑 四、单向链表

1.理论知识

单向链表(Singly Linked List)是一种常见的链式存储结构,它由一系列节点(Node)组成,每个节点包含两部分:数据部分和指针部分。数据部分用来存储节点的值,指针部分指向下一个节点,实现节点之间的连接。

基本操作:

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

特点:

  1. 动态性:链表的长度可以动态变化,不像数组有固定的长度限制。
  2. 插入和删除高效:在已知节点的情况下,插入和删除操作的时间复杂度为 O(1)。
  3. 空间利用率低:链表的每个节点需要额外的指针空间来存储下一个节点的地址,会占用一定的空间。
  4. 访问效率低:访问链表中的任意节点需要从头开始遍历,时间复杂度为 O(n)。

实现方式:

单向链表可以通过节点的结构来实现,每个节点包含一个数据域和一个指针域,指针指向下一个节点。链表的头部可以用一个指针来表示。

应用场景:

  1. LRU缓存:使用链表实现LRU缓存淘汰算法中的数据存储结构。
  2. 编辑器的撤销功能:编辑器中的撤销功能可以使用链表来保存编辑操作的历史记录。
  3. 图的邻接表:图的邻接表表示中,每个顶点的邻接点可以用链表来表示。

2.优缺点

优点:

  1. 动态性:链表的长度可以动态变化,不像数组有固定的长度限制。
  2. 插入和删除高效:在已知节点的情况下,插入和删除操作的时间复杂度为 O(1)。
  3. 不需要移动元素:与数组相比,删除和插入元素时无需移动其他元素,操作简单高效。
  4. 存储空间不浪费:链表在插入和删除元素时,不会浪费额外的存储空间。

缺点:

  1. 访问效率低:访问链表中的任意节点需要从头开始遍历,时间复杂度为 O(n)。
  2. 空间复杂度高:每个节点需要额外的指针空间来存储下一个节点的地址,会占用一定的空间。
  3. 不支持随机访问:链表不支持像数组那样通过索引直接访问元素,需要从头开始逐个访问。
  4. 缓存不友好:由于访问效率低,对于需要频繁访问的数据,使用链表存储会导致缓存命中率降低。

3.数组&链表对比

数组和链表是两种常见的数据结构,它们在存储和访问数据时有着不同的特点和适用场景:

  • 数组:
    • 优点:
      • 提供了便利的[]语法来访问元素。
      • 内存连续,访问速度快。
    • 缺点:
      • 大小固定,需要扩容时需申请新内存并复制元素。
      • 插入和删除元素时需要移动其他元素。
  • 链表:
    • 优点:
      • 内存不连续,灵活利用内存空间。
      • 大小动态,无需预先分配内存。
      • 插入和删除元素时间复杂度为 O(1),效率高。
    • 缺点:
      • 访问任意位置元素需从头开始遍历。
      • 无法直接通过下标访问元素,需要遍历查找。

数组适合元素访问频繁、大小固定的场景;而链表适合大小不固定、插入和删除操作频繁的场景。

4.实现单向链表结构

1.定义链表类&节点结构

js 复制代码
class Node {
  // 数据
  elem;
  // 下一个节点指针
  next;
  constructor(elem) {
    this.elem = elem;
    this.next = null;
  }
}

class LinkedList {
  // 头指针
  head = null;
  length = 0;
}

2.append尾部追加数据

尾部增加共两种情况,如下:

  • 链表本身为空, 新添加的数据时唯一的节点.
  • 链表不为空, 需要向其他节点后面追加节点.
js 复制代码
 // 队尾增加方法
  append(elem) {
    // 根据传入的elem创建节点
    const newNode = new Node(elem);
    // 判断是否为空链表
    if (this.head === null) {
      this.head = newNode;
    } else {
      // 找到最后一个节点的位置
      let current = this.head;
      while (current.next) {
        current = current.next;
      }
      current.next = newNode;
    }
    // 链表长度加1
    this.length++;
  }

3.toString方法

从头节点开始遍历节点,然后拼接成字符串。

js 复制代码
 toString() {
    let current = this.head;
    let listString = "";
    while (current) {
      listString += "," + current.elem;
      current = current.next;
    }
    return listString.slice(1);
  }

4.insert(position, element)任意位置插入

插入位置可以分为三种,我们使用的是没有虚拟头节点的链表,所以需要特殊处理头部位置插入,另外两种可以合并为一种。因为是但链表,所以我们需要两个指针定位节点。

  • 头部位置插入
  • 中间位置插入
  • 尾部位置插入
js 复制代码
// 任意位置增加: 需要两个指针一个计数器
  insert(position, element) {
    if (position < 0 || position > this.length) return false;
    const newNode = new Node(element);
    let current = this.head;
    let preview = null;
    // 计数器
    let index = 0;
    if (position === 0) {
      newNode.next = current;
      this.head = newNode;
    } else {
      // 查找节点位置
      while (index++ < position) {
        preview = current;
        current = current.next;
      }
      // 插入节点
      preview.next = newNode;
      newNode.next = current;
    }
    // 链表长度+1
    this.length++;
    return true;
  }

5.removeAt(position)根据位置删除元素

删除也是三种情况,头部,尾部、中间位置

  • 移除头部元素
  • 移除中间元素
  • 移除尾部元素
js 复制代码
// 任意位置删除: 需要两个指针一个计数器
  removeAt(position) {
    // 下标从0开始
    if (position < 0 || position >= this.length) return null;

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

6.indexOf获取元素位置

遍历查找判断就行了

js 复制代码
// 获取元素位置: 需要一个计数器,从头结点开始找
  indexOf(elem) {
    let current = this.head;
    let index = 0;
    while (current) {
      if (current.elem === elem) return index;
      index++;
      current = current.next;
    }
    return -1;
  }

7.remove根据元素删除

js 复制代码
remove(elem) {
    const index = this.indexOf(elem);
    return this.removeAt(index);
}

8.其他方法

js 复制代码
isEmpty() {
    return this.length === 0;
}

size() {
    return this.length;
}

getFirst() {
    return this.head.elem;
}

完整代码

js 复制代码
class LinkedList {
  head = null;
  length = 0;

  // 队尾增加方法
  append(elem) {
    const newNode = new Node(elem);
    if (this.head === null) {
      this.head = newNode;
    } else {
      let current = this.head;
      while (current.next) {
        current = current.next;
      }
      current.next = newNode;
    }
    this.length++;
  }

  // 任意位置增加: 需要两个指针一个计数器
  insert(position, element) {
    if (position < 0 || position > this.length) return false;
    const newNode = new Node(element);
    let current = this.head;
    let preview = null;
    // 计数器
    let index = 0;
    if (position === 0) {
      newNode.next = current;
      this.head = newNode;
    } else {
      while (index++ < position) {
        preview = current;
        current = current.next;
      }

      preview.next = newNode;
      newNode.next = current;
    }
    this.length++;
    return true;
  }

  // 任意位置删除: 需要两个指针一个计数器
  removeAt(position) {
    // 下标从0开始
    if (position < 0 || position >= this.length) return null;

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

  // 获取元素位置: 需要一个计数器,从头结点开始找
  indexOf(elem) {
    let current = this.head;
    let index = 0;
    while (current) {
      if (current.elem === elem) return index;
      index++;
      current = current.next;
    }
    return -1;
  }

  // 根据元素删除
  remove(elem) {
    const index = this.indexOf(elem);
    return this.removeAt(index);
  }

  isEmpty() {
    return this.length === 0;
  }

  size() {
    return this.length;
  }

  getFirst() {
    return this.head.elem;
  }

  toString() {
    let current = this.head;
    let listString = "";
    while (current) {
      listString += "," + current.elem;
      current = current.next;
    }
    return listString.slice(1);
  }
}

class Node {
  elem;
  next;
  constructor(elem) {
    this.elem = elem;
    this.next = null;
  }
}

// 测试链表
// 1.创建链表
const list = new LinkedList();

// 2.追加元素
list.append(15);
list.append(10);
list.append(20);
console.log(list.toString());

list.insert(0, 100);
list.insert(4, 200);
list.insert(2, 300);

// 3.打印链表的结果
console.log(list.toString());

// 5.测试removeAt方法
list.removeAt(0);
list.removeAt(1);
list.removeAt(3);

// 3.打印链表的结果
console.log(list.toString());

// 6.测试indexOf方法
console.log("✅ ~ list.indexOf(15):", list.indexOf(15)); // 0
console.log("✅ ~ list.indexOf(10) :", list.indexOf(10)); // 1
console.log("✅ ~ list.indexOf(20):", list.indexOf(20)); // 2
console.log("✅ ~ list.indexOf(100):", list.indexOf(100)); // -1

list.remove(15);
// 3.打印链表的结果
console.log(list.toString());

console.log("✅ ~ list.isEmpty():", list.isEmpty());
console.log("✅ ~ list.size():", list.size());
console.log("✅ ~ list.getFirst():", list.getFirst());

🍐 五、双向链表

1.理论知识

双向链表(Doubly Linked List),每个节点除了包含指向下一个节点的指针外,还包含指向前一个节点的指针。这使得双向链表可以从任一端开始遍历,并且在插入和删除节点时更加灵活。双向链表跟单向链表是类似的,只是需要维护两个指针域。

基本操作:

跟单链表一致,增加如下方法:

  • forwardString: 正向遍历转成字符串的方法
  • reverseString: 反向遍历转成字符串的方法

特点:

  1. 双向遍历:双向链表可以从前向后或从后向前遍历。
  2. 插入和删除高效:在已知节点的情况下,插入和删除操作的时间复杂度为 O(1)。
  3. 占用额外空间:每个节点需要额外的指针空间来存储前一个节点的地址,会占用一定的空间。
  4. 实现简单:相对于单向链表,双向链表在插入和删除节点时不需要修改前一个节点的指针,实现更加简单。

实现方式:

双向链表的节点结构包含三个部分:数据域、指向前一个节点的指针和指向后一个节点的指针。链表的头部和尾部分别用一个指针来表示。

应用场景:

  1. LRU缓存:双向链表常用于LRU缓存淘汰算法中的数据存储结构。
  2. 编辑器的撤销功能:编辑器中的撤销功能可以使用双向链表来保存编辑操作的历史记录。
  3. 双向队列:双向链表可以用于实现双向队列,支持在队头和队尾进行插入和删除操作。

2.优缺点

优点:

  1. 双向遍历:可以从前向后或从后向前遍历,灵活性高。
  2. 插入和删除高效:在已知节点的情况下,插入和删除操作的时间复杂度为 O(1)。
  3. 删除操作更方便:相对于单向链表,删除节点时无需遍历查找前一个节点,实现更加简单。
  4. 支持反向查找:可以从后向前查找节点,某些场景下可以提高效率。

缺点:

  1. 占用额外空间:每个节点需要额外的指针空间来存储前一个节点的地址,增加了内存开销。
  2. 实现复杂度高:相对于单向链表,双向链表的实现稍显复杂,需要额外考虑前一个节点的指针。

3.单链表&双链表对比

  1. 插入和删除操作

    • 单链表:插入和删除节点时,需要修改前一个节点的指针指向新节点或删除节点的下一个节点,操作稍显复杂。
    • 双链表:插入和删除节点时,不仅需要修改前一个节点的指针,还需要修改后一个节点的前向指针,但删除节点的操作相对更简单。
  2. 访问效率

    • 单链表:访问任意位置的节点时,需要从头开始逐个遍历,时间复杂度为 O(n)。
    • 双链表:可以从前向后或从后向前遍历,访问效率更高,但需要额外的空间存储前一个节点的指针。
  3. 空间复杂度

    • 单链表和双链表在存储同样数量的节点时,双链表需要更多的存储空间,因为每个节点多了一个指向前一个节点的指针。

4.实现双链表结构

1.定义双链表类&节点类

js 复制代码
class DbLinkedList {
  length = 0;
  // 头结点
  head = null;
  // 尾结点
  tail = null;
}

class MyNode {
  elem;
  // 上一个节点指针
  pre;
  // 下一个节点指针
  next;
  constructor(elem) {
    this.elem = elem;
    this.pre = null;
    this.next = null;
  }
}

2.append尾部追加数据

  • 链表为空
  • 链表不为空,插入在中间、插入在尾部

js 复制代码
append(elem) {
    const newNode = new MyNode(elem);
    if (this.head === null) {
      this.head = newNode;
      this.tail = newNode;
    } else {
      // 连接节点
      this.tail.next = newNode;
      // 处理pre指针
      newNode.pre = this.tail;
      // 将tail移动到新增的节点
      this.tail = newNode;
      // 置空next
      this.tail.next = null;
    }
    this.length++;
  }

3.forwordString正向遍历

js 复制代码
// 正向遍历转成字符串的方法
  forwardString() {
    let current = this.head;
    let resultStr = "";
    while (current) {
      resultStr += "," + current.elem;
      // 这里指针是next
      current = current.next;
    }
    return resultStr.slice(1);
  }

4.reverseString反向遍历

js 复制代码
// 反向遍历转成字符串的方法
  reverseString() {
    let current = this.tail;
    let resultStr = "";
    while (current) {
      resultStr += "," + current.elem;
      // 注意这里指针是pre
      current = current.pre;
    }
    return resultStr.slice(1);
  }

5.toString

js 复制代码
// 正向遍历转成字符串的方法
  toString() {
    this.forwardString();
  }

6.insert任意位置插入

  • 头部插入的两种情况
  • 插入到尾部,为空上面头部的时候已经判断过了,这里直接插入
  • 插入到中间
js 复制代码
// 任意位置插入
  insert(position, element) {
    if (position < 0 || position > this.length) return false;
    const newNode = new MyNode(element);

    if (position === 0) {
      if (this.head === null) {
        this.head = newNode;
        this.tail = newNode;
      } else {
        // 连接新节点
        this.head.pre = newNode;
        newNode.next = this.head;
        // 移动头指针到新节点
        this.head = newNode;
      }
    } else if (position === this.length) {
      // 连接节点
      this.tail.next = newNode;
      newNode.pre = this.tail;
      // 移动指针tail到新节点
      this.tail = newNode;
    } else {
      let index = 0;
      let current = this.head;
      let preview = null;

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

      // 连接前面两个指针
      current.pre.next = newNode;
      newNode.pre = current.pre;

      // 连接后面两个指针
      newNode.next = current;
      current.pre = newNode;
    }
    this.length++;
    return true;
  }

7.removeAt指定位置移除

  • 删除头结点,两种情况
  • 删除尾结点
  • 删除中间的节点
js 复制代码
//任意位置移除
  removeAt(position) {
    if (position < 0 || position >= this.length || this.length === 0)
      return null;

    let current = this.head;
    if (position === 0) {
      if (this.length === 1) {
        this.head = null;
        this.tail = null;
      } else {
        // 先移动指针到下一个为止
        this.head = this.head.next;
        // 断开前一个的指针
        this.head.pre.next = null;
        this.head.pre = null;
      }
    } else if (position === this.length - 1) {
      current = this.tail;
      // 先断开前一个指针的next,这样还可以通过pre找到
      this.tail.pre.next = null;
      // 移动到前一个
      this.tail = this.tail.pre;
      // 断开next
      this.tail.next = null;
    } else {
      let index = 0;
      let previous = null;
      while (index++ < position) {
        previous = current;
        current = current.next;
      }
      // 断开current
      current.pre.next = current.next;
      current.next.pre = current.pre;
    }
    this.length--;
    return current.elem;
  }

8.indexOf获取元素位置

js 复制代码
// 获取元素位置
  indexOf(elem) {
    let current = this.head;
    let index = 0;
    while (current) {
      if (current.elem === elem) return index;
      index++;
      current = current.next;
    }
    return -1;
  }

9.remove删除指定元素

js 复制代码
// 根据元素删除
  remove(element) {
    const index = this.indexOf(element);
    return this.removeAt(index);
  }

10.其他方法

js 复制代码
// 判断是否为空
  isEmpty() {
    return this.length === 0;
  }

  // 获取链表长度
  size() {
    return this.length;
  }

  // 获取第一个元素
  getHead() {
    return this.head.elem;
  }

  // 获取最后一个元素
  getTail() {
    return this.tail.elem;
  }

11.完整代码

js 复制代码
class DbLinkedList {
  length = 0;
  head = null;
  tail = null;

  append(elem) {
    const newNode = new MyNode(elem);
    if (this.head === null) {
      this.head = newNode;
      this.tail = newNode;
    } else {
      // 连接节点
      this.tail.next = newNode;
      // 处理pre指针
      newNode.pre = this.tail;
      // 将tail移动到新增的节点
      this.tail = newNode;
      // 置空next
      this.tail.next = null;
    }
    this.length++;
  }

  // 正向遍历转成字符串的方法
  forwardString() {
    let current = this.head;
    let resultStr = "";
    while (current) {
      resultStr += "," + current.elem;
      current = current.next;
    }
    return resultStr.slice(1);
  }
  // 反向遍历转成字符串的方法
  reverseString() {
    let current = this.tail;
    let resultStr = "";
    while (current) {
      resultStr += "," + current.elem;
      current = current.pre;
    }
    return resultStr.slice(1);
  }
  // 正向遍历转成字符串的方法
  toString() {
    this.forwardString();
  }

  // 任意位置插入
  insert(position, element) {
    if (position < 0 || position > this.length) return false;
    const newNode = new MyNode(element);

    if (position === 0) {
      if (this.head === null) {
        this.head = newNode;
        this.tail = newNode;
      } else {
        // 连接新节点
        this.head.pre = newNode;
        newNode.next = this.head;
        // 移动头指针到新节点
        this.head = newNode;
      }
    } else if (position === this.length) {
      // 连接节点
      this.tail.next = newNode;
      newNode.pre = this.tail;
      // 移动指针tail到新节点
      this.tail = newNode;
    } else {
      let index = 0;
      let current = this.head;
      let preview = null;

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

      // 连接前面两个指针
      current.pre.next = newNode;
      newNode.pre = current.pre;

      // 连接后面两个指针
      newNode.next = current;
      current.pre = newNode;
    }
    this.length++;
    return true;
  }

  //任意位置移除
  removeAt(position) {
    if (position < 0 || position >= this.length || this.length === 0)
      return null;

    let current = this.head;
    if (position === 0) {
      if (this.length === 1) {
        this.head = null;
        this.tail = null;
      } else {
        // 先移动指针到下一个为止
        this.head = this.head.next;
        // 断开前一个的指针
        this.head.pre.next = null;
        this.head.pre = null;
      }
    } else if (position === this.length - 1) {
      current = this.tail;
      // 先断开前一个指针的next,这样还可以通过pre找到
      this.tail.pre.next = null;
      // 移动到前一个
      this.tail = this.tail.pre;
      // 断开next
      this.tail.next = null;
    } else {
      let index = 0;
      let previous = null;
      while (index++ < position) {
        previous = current;
        current = current.next;
      }
      // 断开current
      current.pre.next = current.next;
      current.next.pre = current.pre;
    }
    this.length--;
    return current.elem;
  }

  // 获取元素位置
  indexOf(elem) {
    let current = this.head;
    let index = 0;
    while (current) {
      if (current.elem === elem) return index;
      index++;
      current = current.next;
    }
    return -1;
  }

  // 根据元素删除
  remove(element) {
    const index = this.indexOf(element);
    return this.removeAt(index);
  }

  // 判断是否为空
  isEmpty() {
    return this.length === 0;
  }

  // 获取链表长度
  size() {
    return this.length;
  }

  // 获取第一个元素
  getHead() {
    return this.head.elem;
  }

  // 获取最后一个元素
  getTail() {
    return this.tail.elem;
  }
}

class MyNode {
  elem;
  pre;
  next;
  constructor(elem) {
    this.elem = elem;
    this.pre = null;
    this.next = null;
  }
}

// 1.创建双向链表对象
const DBLink = new DbLinkedList();

// 2.追加元素
DBLink.append("abc");
DBLink.append("cba");
DBLink.append("nba");
DBLink.append("mba");

// 3.获取所有的遍历结果
// console.log("✅ ~ DBLink.forwardString():", DBLink.forwardString());
// console.log("✅ ~ DBLink.reverseString():", DBLink.reverseString());
// console.log("✅ ~ DBLink:", DBLink);

// 4.insert方法测试
DBLink.insert(0, "100");
DBLink.insert(2, "200");
DBLink.insert(6, "300");
console.log("✅ ~ DBLink.forwardString():", DBLink.forwardString());

// 5.removeAt方法测试
console.log("✅ ~ DBLink.removeAt(0):", DBLink.removeAt(0));
console.log("✅ ~ DBLink.removeAt(1):", DBLink.removeAt(1));
console.log("✅ ~ DBLink.removeAt(4):", DBLink.removeAt(4));

🥝 六、集合

1.集合的理论知识

集合(Set)是一种数据结构,用于存储一组唯一的值,这些值按照插入的顺序排列,没有重复的元素。在集合中,元素的顺序是不重要的,而且集合通常没有索引。

基本操作:

  1. 添加元素add(value):向集合中添加一个新元素。
  2. 删除元素remove(value):从集合中删除一个元素。
  3. 检查元素是否存在has(value):检查集合中是否存在某个元素。
  4. 清空集合clear():清空集合中的所有元素。
  5. 获取集合大小size():获取集合中包含的元素数量。
  6. 获取集合中的所有值values():返回一个包含集合中所有值的数组。

集合的应用场景:

  • 去重:集合可以用来去除数组中的重复元素。
  • 数学集合操作:如并集、交集、差集等。
  • 缓存数据:可以用集合来存储缓存的数据,以确保数据的唯一性。

注意事项:

  • 集合不允许重复元素,如果添加重复元素,集合会自动去除重复值。
  • 在使用自定义对象时,要注意对象的引用是否相同,因为集合中的元素是通过引用来比较是否相等的。

2.集合的优缺点

优点:

  1. 唯一性:集合中的元素是唯一的,可以避免重复元素的存在。
  2. 无序性:集合中的元素没有固定的顺序,这在某些场景下是一个优势。
  3. 快速查找 :使用 has() 方法可以快速查找集合中是否包含某个元素。
  4. 动态性:集合的大小和内容可以动态调整,无需预先分配空间。

缺点:

  1. 无法直接访问 :集合中的元素没有索引,无法直接通过索引访问元素,只能通过遍历或 has() 方法来查找元素。
  2. 不支持键值对:集合中只包含值,不支持键值对的形式,因此在需要存储键值对的情况下,需要使用 Map 数据结构。
  3. 性能问题:在一些操作上,集合的性能可能不如数组,比如遍历集合中的元素需要一定的时间复杂度。

3.js中的集合

在 JavaScript 中,集合(Set)是一种数据结构,类似于数组,但是集合中的元素是唯一且无序的。这意味着集合中不允许有重复的值,并且集合中的元素没有固定的顺序。JavaScript 中的集合是通过 Set 对象来实现的。

创建集合:

可以使用 Set 构造函数来创建一个空集合,也可以将一个数组或类数组对象传递给 Set 构造函数来创建包含初始值的集合。

js 复制代码
let set = new Set(); // 创建空集合
let set2 = new Set([1, 2, 3, 4, 5]); // 创建包含初始值的集合

基本操作:

  • 添加元素 :使用 add() 方法向集合中添加元素,如果元素已经存在于集合中,则不会重复添加。
js 复制代码
set.add(1);
set.add(2);
set.add(3);
  • 删除元素 :使用 delete() 方法从集合中删除指定元素。
js 复制代码
set.delete(2);
  • 判断元素是否存在 :使用 has() 方法判断集合中是否存在指定元素。
js 复制代码
set.has(1); // true
set.has(2); // false
  • 清空集合 :使用 clear() 方法清空集合中的所有元素。
js 复制代码
set.clear();
  • 获取集合大小 :使用 size 属性获取集合中元素的数量。
js 复制代码
set.size; // 2
  • 遍历集合 :可以使用 forEach() 方法或 for...of 循环来遍历集合中的元素。
js 复制代码
set.forEach((value) => {
  console.log(value);
});

for (let value of set) {
  console.log(value);
}

4.实现集合结构

用对象来模拟,这里主要需要注意键值的存储形式obj[value] = value

js 复制代码
class MySet {
  items = {};
  has(value) {
    return Object.prototype.hasOwnProperty.call(this.items, value);
  }

  add(value) {
    if (this.has(value)) return false;
    this.items[value] = value;
    return true;
  }

  remove(value) {
    if (!this.has(value)) return false;
    delete this.items[value];
    return true;
  }

  clear() {
    this.items = {};
  }

  size() {
    return Object.keys(this.items).length;
  }

  values() {
    // 这里是因为item[value] = value;
    return Object.keys(this.items);
  }
}

// 测试和使用集合类
const set = new MySet();

// 添加元素
set.add(1);
console.log(set.values());
set.add(1);
console.log(set.values());

set.add(100);
set.add(200);
console.log(set.values());

// 判断是否包含元素
console.log(set.has(100)); // true

// 删除元素
set.remove(100);
console.log(set.values()); // 1, 200

// 获取集合的大小
console.log(set.size()); // 2
set.clear();
console.log(set.size()); // 0

🍉 七、字典

1.理论知识

字典(Dictionary),也称为映射(Map),是一种键值对(key-value)的数据结构,用于存储一组唯一的键和与之相关联的值。在字典中,键是唯一的,每个键都对应一个值。

基本操作:

  1. set(key,value)添加键值对:向字典中添加一个新的键值对。
  2. remove(key)删除键值对:从字典中删除指定键的键值对。
  3. get(key)获取值:根据键获取对应的值。
  4. has(key)检查键是否存在:检查字典中是否存在指定的键。
  5. size()获取键值对数量:获取字典中键值对的数量。
  6. keys()获取所有键:获取字典中所有的键或所有的值。
  7. values()获取所有值:获取字典中所有的值。

字典的应用场景:

  • 存储键值对:字典可以用来存储一组键值对,如对象的属性。
  • 快速查找:通过键可以快速查找对应的值,比起数组来说,查找速度更快。
  • 缓存数据:可以用字典来缓存一些计算结果,避免重复计算。

注意事项:

  • 字典中的键是唯一的,如果添加相同的键,后面的值会覆盖前面的值。
  • 在使用自定义对象作为键时,要确保对象的引用相同,否则会导致无法正确获取值的问题。

2.字典的优缺点

优点:

  1. 快速查找:字典可以根据键快速查找对应的值,查找效率高。
  2. 灵活性:字典中的键可以是任意类型的,包括原始类型和对象引用,值也可以是任意类型的。
  3. 动态性:字典的大小和内容可以动态调整,无需预先分配空间。
  4. 数据组织:适用于需要以键值对形式存储数据的场景,如对象属性、配置信息等。

缺点:

  1. 内存消耗:相比于数组,字典需要额外的内存空间来存储键值对的关系,可能会占用较多的内存。
  2. 性能问题:在一些操作上,字典的性能可能不如数组,比如遍历字典中的所有键值对可能需要较长的时间。
  3. 无序性 :字典中的键值对是无序的,无法保证插入顺序和遍历顺序一致。

3.js中的字典

在 JavaScript 中,可以使用 Map 对象来实现字典功能,但是实现的本质不是一样的,下面的章节我们会介绍如何实现的。

4.实现字典结构

js 复制代码
class Dictionary {
  items = {};

  set(key, value) {
    this.items[key] = value;
  }

  has(key) {
    return Object.prototype.hasOwnProperty.call(this.items, key);
  }

  remove(key) {
    if (!this.has(key)) return false;
    delete this.items[key];
    return true;
  }

  get(key) {
    return this.has(key) ? this.items[key] : undefined;
  }

  keys() {
    return Object.keys(this.items);
  }

  values() {
    return Object.values(this.items);
  }

  size() {
    return this.keys().length;
  }

  clear() {
    this.items = {};
  }
}

// 创建字典对象
const dict = new Dictionary();

// 在字典中添加元素
dict.set("age", 18);
dict.set("name", "Coderwhy");
dict.set("height", 1.88);
dict.set("address", "广州市");

// 获取字典的信息
console.log(dict.keys()); // age,name,height,address
console.log(dict.values()); // 18,Coderwhy,1.88,广州市
console.log(dict.size()); // 4
console.log(dict.get("name")); // Coderwhy

// 字典的删除方法
dict.remove("height");
console.log(dict.keys()); // age,name,address

// 清空字典
dict.clear();

🍋 写在最后

如果您看到这里了,并且觉得这篇文章对您有所帮助,希望您能够点赞👍和收藏⭐支持一下作者🙇🙇🙇,感谢🍺🍺!如果文中有任何不准确之处,也欢迎您指正,共同进步。感谢您的阅读,期待您的点赞👍和收藏⭐!

感兴趣的同学可以关注下我的公众号ObjectX前端实验室

🌟 少走弯路 | ObjectX前端实验室 🛠️「精选资源|实战经验|技术洞见」

相关推荐
ThisIsClark几秒前
【后端面试总结】MySQL主从复制逻辑的技术介绍
mysql·面试·职场和发展
XiaoLeisj25 分钟前
【递归,搜索与回溯算法 & 综合练习】深入理解暴搜决策树:递归,搜索与回溯算法综合小专题(二)
数据结构·算法·leetcode·决策树·深度优先·剪枝
Jasmine_llq44 分钟前
《 火星人 》
算法·青少年编程·c#
程序猿进阶1 小时前
深入解析 Spring WebFlux:原理与应用
java·开发语言·后端·spring·面试·架构·springboot
闻缺陷则喜何志丹1 小时前
【C++动态规划 图论】3243. 新增道路查询后的最短距离 I|1567
c++·算法·动态规划·力扣·图论·最短路·路径
Lenyiin1 小时前
01.02、判定是否互为字符重排
算法·leetcode
鸽鸽程序猿1 小时前
【算法】【优选算法】宽搜(BFS)中队列的使用
算法·宽度优先·队列
Jackey_Song_Odd1 小时前
C语言 单向链表反转问题
c语言·数据结构·算法·链表
Watermelo6172 小时前
详解js柯里化原理及用法,探究柯里化在Redux Selector 的场景模拟、构建复杂的数据流管道、优化深度嵌套函数中的精妙应用
开发语言·前端·javascript·算法·数据挖掘·数据分析·ecmascript