前端学算法(上)- 用 Javascript 封装八大数据结构

虽说工作中基本很难用到,但面试它是真问啊。之前总是随便刷几十道 LeetCode 简单/中等就混过去了,这回时间比较充裕,开始系统的学一下。

对于算法,不要被吓到,需要你掌握的无非就:

  • 八种数据结构:基本实现,简单用法。
  • 五个基础排序:冒泡、快排、插入、归并、选择
  • 两个搜索算法:顺序搜索、二分搜索
  • 四个算法思想:分而治之、动态规划、贪心算法、回溯算法

视频资源:bilibili 随便找个「JS数据结构与算法网课

GitHub 代码:点击跳转,感兴趣一起学的小伙伴可以点个 Star ✨,我可以把我的相关学习资源发你。

一、Javascript 常用方法

01|数组

02|字符串

二、算法复杂度

常见复杂度大小比较: O(n^2) > O(nlogn) > O(n) > O(logn) > O(1)

常见时间复杂度对应关系:

  • O(n^2) :2层循环(嵌套循环)
  • O(nlogn) :快速排序(循环 + 二分)
  • O(n) :1层循环
  • O(logn) :二分

三、八大数据结构的 JS 实现

01|栈

栈是一种「后进先出 」的数据结构,JS 中没有栈,但可以用 Array 实现栈的所有功能

js 复制代码
class Stack {
  #items = []; // ES2020 private class fields,私有属性
  // 出栈
  pop() {
    return this.#items.pop();
  }
  // 入栈
  push(item) {
    this.#items.push(item);
  }
  // 查看栈顶元素
  peek() {
    return this.#items[this.#items.length - 1];
  }
  // 判断栈是否为空
  isEmpty() {
    return this.#items.length === 0;
  }
  // 清空栈
  clear() {
    this.#items = [];
  }
  //   获取栈的大小
  size() {
    return this.#items.length;
  }
}

常见面试题,栈实现进制转换:

js 复制代码
/**
 * 用栈实现进制转换
 * @param {number} decNumber - 基数
 * @param {number} base - 进制
 * @returns {string} - 转换后的字符串
 */
function convert(decNumber, base) {
  let remstack = new Stack();
  let number = decNumber;
  let string = "";
  let baseString = "0123456789ABCDEF";

  while (number > 0) {
    remstack.push(number % base);
    number = Math.floor(number / base);
  }

  while (!remstack.isEmpty()) {
    string += baseString[remstack.pop()];
  }
  return string;
}

console.log(convert(100345, 16));

LeetCode题目

02|队列

队列是一种「先进先出」的数据结构。

虽然 JavaScript 本身并未提供队列数据结构,但我们可以借助 Array 来实现队列的所有功能。特别是,我们可以使用 Array.shift() 方法来实现出队操作。

然而,当队列中的元素数量增多时,这种方法的性能会变得很差,因为每次出队操作都会导致数组中的每一项都向前移动一位。这里有改进版:Queue_improved.js

js 复制代码
class QueueSimple {
  #items = [];
  // 出队
  delQueue() {
    return this.#items.shift(); // 性能很差,每次出队都要把数组每一项前移
  }
  // 入队
  addQueue(item) {
    this.#items.push(item);
  }
  // 查看队首元素
  peek() {
    return this.#items.at(0); // ES2021 Array.prototype.at(),返回索引处的元素
    // return this.#items[0]; // 作用同上
  }
  // 判断队列是否为空
  isEmpty() {
    return this.#items.length === 0;
  }
  // 清空队列
  clear() {
    this.#items = [];
  }
  // 获取队列的大小
  size() {
    return this.#items.length;
  }
}

LeetCode题目

03|链表

链表是一种「线性数据结构,其中的元素不必在内存中连续存储,而是通过指针连接在一起 」。每个元素包含两个部分:数据和指向下一个节点的指针,可以用Object 模拟链表。

链表的优点是插入和删除元素的时间复杂度为 O(1),不需要移动其他元素。但是查找一个元素的时间复杂度为 O(n),因为可能需要遍历整个链表。并且存一个数据增加了指针的内存空间,空间开销大。

链表有多种类型,包括:

  • 单向链表:每个节点只有一个指向下一个节点的链接
  • 双向链表:每个节点有两个指针,一个指向前一个节点,一个指向下一个节点
  • 循环链表:最后一个节点指向第一个节点
js 复制代码
class Node {
  constructor(value) {
    this.value = value;
    this.next = null;
  }
}

export class LinkedList {
  constructor(value) {
    // head 是链表的第一个节点
    this.head = {
      value: value,
      next: null,
    };
  }

  // 尾部添加节点
  addNode(data) {
    const newNode = new Node(data);

    if (!this.head) {
      // 如果链表为空,说明是第一个节点,将新节点赋值给 head 即可
      this.head = newNode;
    } else {
      let current = this.head;
      // 如果不为空,则需要遍历链表,找到最后一个节点
      while (current.next !== null) {
        current = current.next;
      }
      // 将最后一个节点的 next 指向新节点
      current.next = newNode;
    }
  }

  // 基于节点值删除节点
  delNode(data) {
    let current = this.head; // 从头节点开始查找
    let previous = null; // 用于记录当前节点的前一个节点

    while (current !== null) {
      if (current.value === data) {
        if (previous === null) {
          // 如果删除的是头节点,将头节点指向下一个节点
          this.head = current.next;
        } else {
          previous.next = current.next; // 将前一个节点的 next 设置为下一个节点
        }
        return current.value;
      }

      previous = current;
      current = current.next;
    }
    return null;
  }

  // 任意位置插入节点
  // 有点像拆链子,拿掉要换的那节,然后把前后两节连起来
  insertNode(data, index) {
    let newNode = new Node(data);
    let current = this.head;
    let previous = null;
    let i = 0;

    if (index === 0) {
      newNode.next = current; // 将新节点的 next 指向当前节点
      this.head = newNode; // 更新头节点指向新节点
    } else {
      while (i++ < index) {
        // 遍历链表,找到要插入位置的节点
        previous = current;
        current = current.next;
      }
      previous.next = newNode;
      newNode.next = current;
    }
  }

  // 打印链表
  printList() {
    let current = this.head;
    let str = "";
    while (current !== null) {
      str += current.value + " -> ";
      current = current.next;
    }
    str += "null";
    console.log(str);
  }

  // 反转链表
  reverseList() {
    let current = this.head;
    let previous = null;
    let next = null;

    while (current !== null) {
      next = current.next; // 保存当前节点的下一个节点
      current.next = previous; // 将当前节点的 next 指向前一个节点
      previous = current; // previous 指向当前节点
      current = next; // current 指向下一个节点
    }
    this.head = previous; // 更新头节点
  }
}

04|集合

集合是一种「包含不重复元素」的数据结构。

它的主要特点是元素无序且唯一 ,ES6 中有集合:Set。集合常用操作:数组去重、求交集、判断某元素是否在集合中。

js 复制代码
export class Set {
  items = {};

  //   将item的key和value都设置为item
  add(item) {
    if (!this.has(item)) {
      this.items[item] = item;
      return true;
    }
    return false;
  }
  delete(item) {
    if (this.has(item)) {
      delete this.items[item];
      return true;
    }
    return false;
  }
  has(item) {
    return this.items.hasOwnProperty(item); // 判断是否有这个属性
  }
  clear() {
    this.items = {};
  }
  size() {
    return Object.keys(this.items).length;
  }
  values() {
    return Object.values(this.items);
  }
}
js 复制代码
// 去重
const arr = [1, 1, 2, 2]
const arr2 = [...new Set(arr)] // [1, 2]

// 判断元素是否在集合中
const set = new Set(arr)
const has = set.has(3) // false

// 求交集
const set2 = new Set([2, 3])
const set3 = new Set([...set].filter(item => set2.has(item))) // Set(1) {2}

// 求差集, 交集的取反
const set4 = new Set([...set].filter(item => !set2.has(item))) // Set(1) {1}

// 求并集
const set5 = new Set([...arr1, ...arr2] )// [1, 2]

LeetCode题目

05|字典

字典(在 JavaScript 中通常被称为 Map)是一种存储键值对的数据结构。它的主要特点是通过键来查找值,这种结构提供了快速查找、删除和更新键值的能力。

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

  // 对象做key值会导致隐式转换,所以需要将key转换为字符串
  toStrFn(key) {
    if (key === null) {
      return "NULL";
    } else if (key === undefined) {
      return "UNDEFINED";
    } else if (typeof key === "string" || key instanceof String) {
      return `${key}`;
    } else {
      return JSON.stringify(key);
    }
  }

  hasKey(key) {
    return (
      this.items[this.toStrFn(key)] !== null ||
      this.items[this.toStrFn(key)] !== undefined
    );
  }

  set(key, value) {
    if (key != null && value != null) {
      this.items[this.toStrFn(key)] = new ValuePair(key, value);
    }
  }

  get(key) {
    const valuePair = this.items[this.toStrFn(key)];
    return valuePair == null ? undefined : valuePair.value;
  }

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

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

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

  // 额外的一些辅助方法:返回key、返回value、返回键值对
  keys() {
    return this.keyValues().map((valuePair) => valuePair.key);
  }

  values() {
    return this.keyValues().map((valuePair) => valuePair.value);
  }

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

class ValuePair {
  constructor(key, value) {
    this.key = key;
    this.value = value;
  }
}

ES6中,Map 对象提供了一些方法来执行常见的字典操作

  • set(key, value):添加或更新字典中的一个键值对。
  • delete(key):移除字典中的一个键值对。
  • get(key):获取字典中的一个值。
  • has(key):返回一个布尔值,表示某个键是否在字典中。
  • clear():清空字典中的所有键值对。
c 复制代码
// 字典
const map = new Map()

// 增
map.set('key1', 'value1')

var newObj = {a:1}	// 需明确具体的引用地址
map.set(newObj, 'value2') // 正确,可以通过map.get(newObj)获取到value2的值
map.set({a:1}, 'value2') // 错误,无法获取到具体值,因为key的引用地址变了

// 删
map.delete('key1')
map.clear()

// 改
map.set('key1', 'value222')

// 查
map.get('key1')

LeetCode题目

06|树

树是一种「以分层的方式存储,且非线性」的数据结构。

它是由节点组成的,其中最上层是根节点,其余的节点可以分为多个不交叉的子树。在前端开发中,树形结构的应用非常广泛,例如 DOM 就是一种树形结构。另外,像级联选择、树形控件等UI组件,也是基于树形结构实现的。

如下图,是一个二叉树搜素树:

  • 二叉树的一种,每个节点最多只有两个子节点,一个左侧,一个右侧;
  • 但要求左侧子节点的值比父节点小,右侧子节点比父节点大
  • 最小值在最左侧;
  • 最大值在最右侧;

在 JavaScript 中,虽然没有内置的树数据结构,但我们可以使用 ObjectArray 来构建树。

js 复制代码
class Node {
  constructor(value) {
    this.value = value;
    this.left = null;
    this.right = null;
  }
}

// 二叉搜索树
export class BST {
  constructor() {
    this.root = null;
  }

  insert(value) {
    const newNode = new Node(value);
    if (this.root === null) {
      this.root = newNode;
    } else {
      this.#insertNode(this.root, newNode);
    }
  }
  // 基于递归判断大小插入节点
  #insertNode(node, newNode) {
    if (newNode.value < node.value) {
      if (node.left === null) {
        node.left = newNode;
      } else {
        this.insertNode(node.left, newNode);
      }
    } else {
      if (node.right === null) {
        node.right = newNode; // 任何右子节点的值都大于或等于该节点的值
      } else {
        this.insertNode(node.right, newNode);
      }
    }
  }

  // 先序遍历: 根节点 -> 左子树 -> 右子树
  preOrderTraverse(callback) {
    this.#preOrderTraverseNode(this.root, callback);
  }
  #preOrderTraverseNode(node, callback) {
    if (node !== null) {
      callback(node.value);
      this.#preOrderTraverseNode(node.left, callback);
      this.#preOrderTraverseNode(node.right, callback);
    }
  }

  // 中序遍历: 左子树 -> 根节点 -> 右子树
  inOrderTraverse(callback) {
    this.#inOrderTraverseNode(this.root, callback);
  }
  #inOrderTraverseNode(node, callback) {
    if (node !== null) {
      this.#inOrderTraverseNode(node.left, callback);
      callback(node.value);
      this.#inOrderTraverseNode(node.right, callback);
    }
  }

  // 后序遍历: 左子树 -> 右子树 -> 根节点
  postOrderTraverse(callback) {
    this.#postOrderTraverseNode(this.root, callback);
  }
  #postOrderTraverseNode(node, callback) {
    if (node !== null) {
      this.#postOrderTraverseNode(node.left, callback);
      this.#postOrderTraverseNode(node.right, callback);
      callback(node.value);
    }
  }

  min() {
    return this.#minNode(this.root);
  }
  #minNode(node) {
    let current = node;
    while (current !== null && current.left !== null) {
      current = current.left;
    }
    return current.value;
  }
  max() {
    return this.#maxNode(this.root);
  }
  #maxNode(node) {
    let current = node;
    while (current !== null && current.right !== null) {
      current = current.right;
    }
    return current.value;
  }
  // 搜索树中是否存在某个值
  search(value) {
    return this.#searchNode(this.root, value);
  }
  #searchNode(node, value) {
    if (node === null) {
      return false;
    }
    if (value < node.value) {
      return this.#searchNode(node.left, value);
    } else if (value > node.value) {
      return this.#searchNode(node.right, value);
    } else {
      return true;
    }
  }

  remove(value) {
    this.root = this.#removeNode(this.root, value);
  }
  #removeNode(node, value) {
    if (node === null) {
      return null;
    }
    if (value < node.value) {
      node.left = this.#removeNode(node.left, value);
      return node;
    } else if (value > node.value) {
      node.right = this.#removeNode(node.right, value);
      return node;
    } else {
      // 没有子节点,直接移除
      if (node.left === null && node.right === null) {
        node = null;
        return node;
      }
      // 只有一个子节点的节点,将其子节点提升为当前节点的位置
      if (node.left === null) {
        node = node.right;
        return node;
      } else if (node.right === null) {
        node = node.left;
        return node;
      }
      // 移除有两个子节点的节点,找到右子树中的最小节点,将其替换为当前节点
      const aux = this.#findMinNode(node.right);
      node.value = aux.value;
      node.right = this.#removeNode(node.right, aux.value);
      return node;
    }
  }
  #findMinNode(node) {
    let current = node;
    while (current !== null && current.left !== null) {
      current = current.left;
    }
    return current;
  }
}

遍历: 中序遍历是从小到大展示节点值

rust 复制代码
    8
   / \
  3   10
 / \    \
1   6    14
   / \   /
  4   7 13

先序遍历(根节点 -> 左子树 -> 右子树):8, 3, 1, 6, 4, 7, 10, 14, 13
中序遍历(左子树 -> 根节点 -> 右子树):1, 3, 4, 6, 7, 8, 10, 13, 14
后续遍历(左子树 -> 右子树 -> 根节点):1, 4, 7, 6, 3, 13, 14, 10, 8

删除:

  • 无子节点:直接删除
  • 有一个子节点:将其子节点提升为当前节点的位置
  • 有两个子节点:找到右子树中的最小节点,将其替换为当前节点

LeetCode题目

07|堆

堆是一种特殊的二叉树

  • 是一棵完全二叉树,表示树每一层都有左侧和右侧子节点(除最后一层叶节点外)
  • 二叉堆不是最小堆就是最大堆
    • 最小堆所有节点都小于等于他的子节点,最小的元素位于根节点;
    • 最大堆所有节点都大于等于他的子节点,最大的元素位于根节点;

由于堆是一种完全二叉树,我们可以使用数组来表示堆。数组的索引可以用来表示堆中的父节点和子节点之间的关系。例如,对于一个索引为 i 的节点(索引从 0 开始):

  • 它的父节点的索引是 (i - 1) / 2(向下取整)
  • 它的左子节点的索引是 2 * i + 1
  • 它的右子节点的索引是 2 * i + 2
js 复制代码
// 封装一个最小堆
class MinHeap {
  heap = []; // 存储堆数据

  insert(value) {
    this.heap.push(value);
    this.#shiftUp();
  }
  // 与父节点逐级对比,若小于则交换位置
  #shiftUp() {
    let index = this.heap.length - 1; // 当前节点索引,数组长度减一
    let parentIndex = Math.floor((index - 1) / 2); // 父节点索引,向下取整
    // 当前节点值小于父节点值时,交换位置,直到根节点
    while (this.heap[parentIndex] > this.heap[index]) {
      [this.heap[parentIndex], this.heap[index]] = [
        this.heap[index],
        this.heap[parentIndex],
      ];
      index = parentIndex;
      parentIndex = Math.floor((index - 1) / 2);
    }
  }

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

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

  remove() {
    if (this.isEmpty()) {
      return null;
    }
    if (this.size() === 1) {
      return this.heap.shift();
    }
    const removedValue = this.heap[0];
    this.heap[0] = this.heap.pop();
    this.#shiftDown();
    return removedValue;
  }
  #shiftDown() {
    let index = 0;
    let left = 2 * index + 1;
    while (left < this.heap.length) {
      let right = 2 * index + 2;
      let smallerIndex =
        right < this.heap.length && this.heap[right] < this.heap[left]
          ? right
          : left;
      if (this.heap[index] < this.heap[smallerIndex]) {
        break;
      }
      [this.heap[index], this.heap[smallerIndex]] = [
        this.heap[smallerIndex],
        this.heap[index],
      ];
      index = smallerIndex;
      left = 2 * index + 1;
    }
  }

  // 查找某个值的索引
  findTaeget(value) {
    return this.#findTaegetNode(this.heap, value);
  }
  #findTaegetNode(heap, value) {
    for (let i = 0; i < heap.length; i++) {
      if (heap[i] === value) {
        return i;
      }
    }
    return -1;
  }
}

LeetCode题目

08|图

图是一种非线性的数据结构,由节点和边组成。每条边连接两个节点,表示它们之间的关系。图可以表示任何二元关系,例如网络、道路、航班等。

  • 无/有向图:图的每条边规定一个方向,那么得到的图称为有向图;相反,边没有方向的图称为无向图
  • 带权图:在带权图中,每条边都有一个权重(weight)[非负实数]
  • 图中某两个顶点之间的边数多于一条,又允许顶点通过同一条边和自己关联,则称为多重图

在 JavaScript 中,虽然没有内置的图数据结构,我们封装一个最简单的图:

js 复制代码
class Graph {
  constructor() {
    this.vertices = []; // 用于存储图中的顶点
    this.adjList = new Map(); // 用于存储图中的边
  }

  // 添加顶点
  addVertex(v) {
    this.vertices.push(v);
    this.adjList.set(v, []); // 初始化邻接表
  }

  // 添加边
  addEdge(v, w) {
    this.adjList.get(v).push(w); // 添加边
    this.adjList.get(w).push(v); // 对于无向图,需要添加两条边
  }

  // 输出图
  toString() {
    let s = "";
    for (let i = 0; i < this.vertices.length; i++) {
      s += this.vertices[i] + " -> ";
      let neighbors = this.adjList.get(this.vertices[i]);
      for (let j = 0; j < neighbors.length; j++) {
        s += neighbors[j] + " ";
      }
      s += "\n";
    }
    return s;
  }
}

LeetCode题目

相关推荐
一丝晨光22 分钟前
C++、Ruby和JavaScript
java·开发语言·javascript·c++·python·c·ruby
夜流冰23 分钟前
工具方法 - 面试中回答问题的技巧
面试·职场和发展
Front思24 分钟前
vue使用高德地图
javascript·vue.js·ecmascript
zqx_71 小时前
随记 前端框架React的初步认识
前端·react.js·前端框架
惜.己1 小时前
javaScript基础(8个案例+代码+效果图)
开发语言·前端·javascript·vscode·css3·html5
什么鬼昵称2 小时前
Pikachu-csrf-CSRF(get)
前端·csrf
长天一色2 小时前
【ECMAScript 从入门到进阶教程】第三部分:高级主题(高级函数与范式,元编程,正则表达式,性能优化)
服务器·开发语言·前端·javascript·性能优化·ecmascript
NiNg_1_2342 小时前
npm、yarn、pnpm之间的区别
前端·npm·node.js
秋殇与星河2 小时前
CSS总结
前端·css
NiNg_1_2342 小时前
Vue3 Pinia持久化存储
开发语言·javascript·ecmascript