MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JavaScript中的算法与数据结构优化

2021-11-013.0k 阅读

JavaScript 中的算法与数据结构优化

算法与数据结构基础

在深入探讨优化之前,我们先来回顾一下算法与数据结构的基本概念。数据结构是一种组织和存储数据的方式,以便能够高效地访问和修改数据。而算法则是为了解决特定问题而设计的一系列有限步骤。

在 JavaScript 中,常见的数据结构包括数组、对象、链表、栈、队列、树和图等。每种数据结构都有其独特的特点和适用场景。例如,数组是一种线性数据结构,适合存储和访问连续的数据元素,并且支持快速的随机访问。以下是一个简单的数组示例:

let numbers = [1, 2, 3, 4, 5];
console.log(numbers[2]); // 输出 3

对象则是一种键值对的集合,适用于存储和管理具有命名属性的数据。

let person = {
    name: 'John',
    age: 30,
    profession: 'Engineer'
};
console.log(person.name); // 输出 John

算法复杂度分析

在评估算法的优劣时,我们通常使用时间复杂度和空间复杂度来进行分析。时间复杂度衡量的是算法执行所需的时间随着输入规模的增长而增长的速度,而空间复杂度衡量的是算法执行过程中所需的额外空间随着输入规模的增长而增长的速度。

常见的时间复杂度有:

  • O(1) - 常数时间:无论输入规模如何,算法执行时间都是固定的。例如,访问数组中的一个元素。
let arr = [1, 2, 3];
console.log(arr[0]); // O(1)
  • O(n) - 线性时间:算法执行时间与输入规模成正比。例如,遍历数组中的所有元素。
let numbers = [1, 2, 3, 4, 5];
for (let i = 0; i < numbers.length; i++) {
    console.log(numbers[i]);
} // O(n)
  • O(n²) - 平方时间:常见于嵌套循环,算法执行时间与输入规模的平方成正比。例如,冒泡排序算法。
function bubbleSort(arr) {
    for (let i = 0; i < arr.length; i++) {
        for (let j = 0; j < arr.length - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                let temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
    return arr;
}
let numbers = [5, 4, 3, 2, 1];
console.log(bubbleSort(numbers)); // O(n²)
  • O(log n) - 对数时间:通常出现在二分查找等算法中,每次操作都能将问题规模减半。
function binarySearch(arr, target) {
    let left = 0;
    let right = arr.length - 1;
    while (left <= right) {
        let mid = Math.floor((left + right) / 2);
        if (arr[mid] === target) {
            return mid;
        } else if (arr[mid] < target) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    return -1;
}
let sortedNumbers = [1, 2, 3, 4, 5];
console.log(binarySearch(sortedNumbers, 3)); // O(log n)

空间复杂度方面,除了考虑算法执行过程中创建的临时变量所占空间外,还需要考虑递归调用栈的空间(如果算法使用了递归)。例如,一个简单的递归函数:

function factorial(n) {
    if (n === 0 || n === 1) {
        return 1;
    } else {
        return n * factorial(n - 1);
    }
}
console.log(factorial(5));

在这个例子中,递归调用栈的深度最大为 n,所以空间复杂度为 O(n)。

数组优化

减少数组访问次数

在循环中频繁访问数组的长度属性会增加额外的开销,因为每次访问都需要计算数组的长度。可以将数组长度存储在一个变量中,减少访问次数。

let numbers = [1, 2, 3, 4, 5];
// 不优化
for (let i = 0; i < numbers.length; i++) {
    console.log(numbers[i]);
}
// 优化
let len = numbers.length;
for (let i = 0; i < len; i++) {
    console.log(numbers[i]);
}

使用 Typed Arrays

JavaScript 的普通数组可以存储不同类型的数据,这带来了灵活性,但也增加了内存开销和性能损耗。Typed Arrays 是一种更高效的数组类型,它们只能存储单一类型的数据,并且与底层的二进制数据存储更加接近。例如,Uint8Array 用于存储无符号 8 位整数。

let normalArray = [1, 2, 3, 4, 5];
let typedArray = new Uint8Array([1, 2, 3, 4, 5]);

在进行大量数值计算时,Typed Arrays 通常会比普通数组表现更好。例如,计算数组元素的总和:

function sumNormalArray(arr) {
    let sum = 0;
    for (let i = 0; i < arr.length; i++) {
        sum += arr[i];
    }
    return sum;
}
function sumTypedArray(arr) {
    let sum = 0;
    for (let i = 0; i < arr.length; i++) {
        sum += arr[i];
    }
    return sum;
}
let normalNumbers = [1, 2, 3, 4, 5];
let typedNumbers = new Uint8Array([1, 2, 3, 4, 5]);
console.time('sumNormalArray');
console.log(sumNormalArray(normalNumbers));
console.timeEnd('sumNormalArray');
console.time('sumTypedArray');
console.log(sumTypedArray(typedNumbers));
console.timeEnd('sumTypedArray');

在这个简单的测试中,Typed Arrays 可能会显示出一定的性能优势,尤其是在处理大数据集时。

链表优化

链表是一种动态数据结构,它由一系列节点组成,每个节点包含数据和指向下一个节点的引用(在双向链表中还包含指向前一个节点的引用)。与数组相比,链表在插入和删除操作上具有优势,但在随机访问上效率较低。

双向链表的优势

双向链表允许在两个方向上遍历链表,这在某些场景下非常有用。例如,在实现一个需要频繁在链表头部和尾部进行插入和删除操作的数据结构时,双向链表可以避免在单向链表中需要遍历到链表尾部才能进行某些操作的开销。

class Node {
    constructor(value) {
        this.value = value;
        this.next = null;
        this.prev = null;
    }
}
class DoublyLinkedList {
    constructor() {
        this.head = null;
        this.tail = null;
    }
    addToHead(value) {
        let newNode = new Node(value);
        if (!this.head) {
            this.head = newNode;
            this.tail = newNode;
        } else {
            newNode.next = this.head;
            this.head.prev = newNode;
            this.head = newNode;
        }
    }
    addToTail(value) {
        let newNode = new Node(value);
        if (!this.tail) {
            this.head = newNode;
            this.tail = newNode;
        } else {
            newNode.prev = this.tail;
            this.tail.next = newNode;
            this.tail = newNode;
        }
    }
}

链表遍历优化

在遍历链表时,可以使用 while 循环而不是递归,因为递归可能会导致栈溢出问题,尤其是在链表较长的情况下。

function traverseLinkedList(node) {
    let current = node;
    while (current) {
        console.log(current.value);
        current = current.next;
    }
}
let list = new DoublyLinkedList();
list.addToHead(1);
list.addToTail(2);
traverseLinkedList(list.head);

栈和队列优化

栈的实现与优化

栈是一种后进先出(LIFO)的数据结构。在 JavaScript 中,可以使用数组来简单实现栈。但为了提高性能,可以避免在数组中间进行插入和删除操作,而是只在数组的一端进行操作。

class Stack {
    constructor() {
        this.items = [];
    }
    push(element) {
        this.items.push(element);
    }
    pop() {
        return this.items.pop();
    }
    peek() {
        return this.items[this.items.length - 1];
    }
    isEmpty() {
        return this.items.length === 0;
    }
}

在实际应用中,如果栈的规模较大,可以考虑使用链表来实现栈,以避免数组在扩容时的性能开销。

队列的实现与优化

队列是一种先进先出(FIFO)的数据结构。同样,可以使用数组来实现队列,但为了优化性能,在插入元素时使用 push 方法,在删除元素时使用 shift 方法,避免在数组中间进行操作。

class Queue {
    constructor() {
        this.items = [];
    }
    enqueue(element) {
        this.items.push(element);
    }
    dequeue() {
        return this.items.shift();
    }
    peek() {
        return this.items[0];
    }
    isEmpty() {
        return this.items.length === 0;
    }
}

如果队列的操作非常频繁,并且需要处理大量数据,也可以考虑使用链表来实现队列,以减少数组扩容带来的性能影响。

树结构优化

二叉搜索树的优化

二叉搜索树(BST)是一种特殊的二叉树,对于树中的每个节点,其左子树中的所有节点值都小于该节点值,右子树中的所有节点值都大于该节点值。这种特性使得在 BST 中进行查找、插入和删除操作的平均时间复杂度为 O(log n)。

为了优化二叉搜索树的性能,需要确保树的高度尽量平衡。如果二叉搜索树严重不平衡,其性能会退化到 O(n)。一种常见的平衡二叉搜索树是 AVL 树,它通过在插入和删除操作后进行旋转操作来保持树的平衡。

class TreeNode {
    constructor(value) {
        this.value = value;
        this.left = null;
        this.right = null;
        this.height = 1;
    }
}
class AVLTree {
    constructor() {
        this.root = null;
    }
    getHeight(node) {
        return node? node.height : 0;
    }
    getBalanceFactor(node) {
        return this.getHeight(node.left) - this.getHeight(node.right);
    }
    rightRotate(y) {
        let x = y.left;
        let T2 = x.right;
        x.right = y;
        y.left = T2;
        y.height = Math.max(this.getHeight(y.left), this.getHeight(y.right)) + 1;
        x.height = Math.max(this.getHeight(x.left), this.getHeight(x.right)) + 1;
        return x;
    }
    leftRotate(x) {
        let y = x.right;
        let T2 = y.left;
        y.left = x;
        x.right = T2;
        x.height = Math.max(this.getHeight(x.left), this.getHeight(x.right)) + 1;
        y.height = Math.max(this.getHeight(y.left), this.getHeight(y.right)) + 1;
        return y;
    }
    insert(value) {
        this.root = this._insert(this.root, value);
    }
    _insert(node, value) {
        if (!node) {
            return new TreeNode(value);
        }
        if (value < node.value) {
            node.left = this._insert(node.left, value);
        } else if (value > node.value) {
            node.right = this._insert(node.right, value);
        } else {
            return node;
        }
        node.height = 1 + Math.max(this.getHeight(node.left), this.getHeight(node.right));
        let balance = this.getBalanceFactor(node);
        // 左左情况
        if (balance > 1 && value < node.left.value) {
            return this.rightRotate(node);
        }
        // 右右情况
        if (balance < -1 && value > node.right.value) {
            return this.leftRotate(node);
        }
        // 左右情况
        if (balance > 1 && value > node.left.value) {
            node.left = this.leftRotate(node.left);
            return this.rightRotate(node);
        }
        // 右左情况
        if (balance < -1 && value < node.right.value) {
            node.right = this.rightRotate(node.right);
            return this.leftRotate(node);
        }
        return node;
    }
}

树的遍历优化

在遍历树时,深度优先搜索(DFS)和广度优先搜索(BFS)是两种常见的方式。DFS 可以使用递归或栈来实现,BFS 则通常使用队列来实现。对于大型树结构,合理选择遍历方式可以提高性能。例如,如果需要尽快找到某个特定深度的节点,BFS 可能更合适;如果需要处理整棵树的结构,DFS 可能更简洁。

// DFS 递归实现
function dfsRecursive(node) {
    if (node) {
        console.log(node.value);
        dfsRecursive(node.left);
        dfsRecursive(node.right);
    }
}
// DFS 栈实现
function dfsStack(node) {
    if (!node) return;
    let stack = [node];
    while (stack.length > 0) {
        let current = stack.pop();
        console.log(current.value);
        if (current.right) stack.push(current.right);
        if (current.left) stack.push(current.left);
    }
}
// BFS 队列实现
function bfsQueue(node) {
    if (!node) return;
    let queue = [node];
    while (queue.length > 0) {
        let current = queue.shift();
        console.log(current.value);
        if (current.left) queue.push(current.left);
        if (current.right) queue.push(current.right);
    }
}
let tree = new AVLTree();
tree.insert(5);
tree.insert(3);
tree.insert(7);
dfsRecursive(tree.root);
dfsStack(tree.root);
bfsQueue(tree.root);

图结构优化

图的存储优化

图可以使用邻接矩阵或邻接表来存储。邻接矩阵是一个二维数组,其中 matrix[i][j] 表示节点 i 和节点 j 之间是否有边相连。这种表示方法简单直观,但对于稀疏图(边的数量远小于节点数量的平方)会浪费大量的空间。邻接表则是通过链表或数组来存储每个节点的邻居节点,更适合存储稀疏图。

// 邻接表实现
class Graph {
    constructor() {
        this.adjList = {};
    }
    addVertex(vertex) {
        if (!this.adjList[vertex]) {
            this.adjList[vertex] = [];
        }
    }
    addEdge(vertex1, vertex2) {
        if (this.adjList[vertex1] && this.adjList[vertex2]) {
            this.adjList[vertex1].push(vertex2);
            this.adjList[vertex2].push(vertex1);
        }
    }
}

图的遍历优化

图的遍历也包括深度优先搜索和广度优先搜索,与树的遍历类似,但需要注意避免重复访问节点。可以使用一个数组或集合来记录已经访问过的节点。

// 图的 DFS
function graphDFS(graph, start) {
    let visited = {};
    function dfs(vertex) {
        if (!visited[vertex]) {
            console.log(vertex);
            visited[vertex] = true;
            let neighbors = graph.adjList[vertex];
            for (let i = 0; i < neighbors.length; i++) {
                dfs(neighbors[i]);
            }
        }
    }
    dfs(start);
}
// 图的 BFS
function graphBFS(graph, start) {
    let visited = {};
    let queue = [start];
    visited[start] = true;
    while (queue.length > 0) {
        let current = queue.shift();
        console.log(current);
        let neighbors = graph.adjList[current];
        for (let i = 0; i < neighbors.length; i++) {
            if (!visited[neighbors[i]]) {
                visited[neighbors[i]] = true;
                queue.push(neighbors[i]);
            }
        }
    }
}
let myGraph = new Graph();
myGraph.addVertex('A');
myGraph.addVertex('B');
myGraph.addVertex('C');
myGraph.addEdge('A', 'B');
myGraph.addEdge('B', 'C');
graphDFS(myGraph, 'A');
graphBFS(myGraph, 'A');

排序算法优化

快速排序优化

快速排序是一种高效的排序算法,平均时间复杂度为 O(n log n)。其基本思想是选择一个基准元素,将数组分为两部分,使得左边部分的元素都小于基准元素,右边部分的元素都大于基准元素,然后分别对左右两部分进行递归排序。

为了优化快速排序,可以采用随机选择基准元素的方法,以避免在某些特殊情况下(如数组已经有序)导致的性能退化。

function quickSort(arr) {
    if (arr.length <= 1) {
        return arr;
    }
    let pivotIndex = Math.floor(Math.random() * arr.length);
    let pivot = arr[pivotIndex];
    let left = [];
    let right = [];
    for (let i = 0; i < arr.length; i++) {
        if (i === pivotIndex) continue;
        if (arr[i] <= pivot) {
            left.push(arr[i]);
        } else {
            right.push(arr[i]);
        }
    }
    return [...quickSort(left), pivot, ...quickSort(right)];
}
let numbers = [5, 4, 3, 2, 1];
console.log(quickSort(numbers));

归并排序优化

归并排序也是一种时间复杂度为 O(n log n) 的排序算法,它采用分治思想,将数组不断分成两半,直到每个子数组只有一个元素,然后再将这些子数组合并成一个有序数组。

在合并过程中,可以使用临时数组来存储合并结果,以减少在原数组上频繁交换元素的开销。

function mergeSort(arr) {
    if (arr.length <= 1) {
        return arr;
    }
    let mid = Math.floor(arr.length / 2);
    let left = arr.slice(0, mid);
    let right = arr.slice(mid);
    left = mergeSort(left);
    right = mergeSort(right);
    return merge(left, right);
}
function merge(left, right) {
    let result = [];
    let i = 0;
    let j = 0;
    while (i < left.length && j < right.length) {
        if (left[i] < right[j]) {
            result.push(left[i]);
            i++;
        } else {
            result.push(right[j]);
            j++;
        }
    }
    return result.concat(left.slice(i)).concat(right.slice(j));
}
let numbers = [5, 4, 3, 2, 1];
console.log(mergeSort(numbers));

搜索算法优化

二分查找优化

二分查找是一种在有序数组中高效查找目标元素的算法,时间复杂度为 O(log n)。为了进一步优化二分查找,可以考虑使用插值查找。插值查找是基于二分查找的改进,它根据要查找的关键字 key 与查找区间两端点关键字 key1key2 的关系,自适应地选择查找点,而不是像二分查找那样固定地选择中间点。

function interpolationSearch(arr, target) {
    let low = 0;
    let high = arr.length - 1;
    while (low <= high && target >= arr[low] && target <= arr[high]) {
        let pos = low + Math.floor(((high - low) / (arr[high] - arr[low])) * (target - arr[low]));
        if (arr[pos] === target) {
            return pos;
        } else if (arr[pos] < target) {
            low = pos + 1;
        } else {
            high = pos - 1;
        }
    }
    return -1;
}
let sortedNumbers = [1, 2, 3, 4, 5];
console.log(interpolationSearch(sortedNumbers, 3));

哈希表查找优化

哈希表是一种基于哈希函数的数据结构,用于快速查找元素。在 JavaScript 中,对象和 Map 都可以看作是哈希表的实现。为了优化哈希表的查找性能,需要选择一个好的哈希函数,尽量减少哈希冲突。同时,在处理大量数据时,可以考虑使用链式哈希表(每个哈希桶是一个链表)或开放地址法来解决哈希冲突。

class HashTable {
    constructor(size) {
        this.size = size;
        this.table = new Array(size).fill(null);
    }
    hashFunction(key) {
        let sum = 0;
        for (let i = 0; i < key.length; i++) {
            sum += key.charCodeAt(i);
        }
        return sum % this.size;
    }
    set(key, value) {
        let index = this.hashFunction(key);
        if (!this.table[index]) {
            this.table[index] = [];
        }
        this.table[index].push([key, value]);
    }
    get(key) {
        let index = this.hashFunction(key);
        if (this.table[index]) {
            for (let i = 0; i < this.table[index].length; i++) {
                if (this.table[index][i][0] === key) {
                    return this.table[index][i][1];
                }
            }
        }
        return null;
    }
}
let hashTable = new HashTable(10);
hashTable.set('name', 'John');
console.log(hashTable.get('name'));

通过对以上各种数据结构和算法的优化,可以显著提高 JavaScript 程序在处理复杂数据和大规模计算时的性能。在实际应用中,需要根据具体的问题场景选择合适的数据结构和算法,并进行针对性的优化。