2023前端面试题总结:算法篇完整版

629 阅读7分钟

实现排序算法之冒泡排序

从数组的起始位置开始,依次比较相邻的两个元素,如果顺序不对就交换位置,直到整个数组排序完成。

function bubbleSort(arr) {
    const n = arr.length;
    for (let i = 0; i < n - 1; i++) {
        for (let j = 0; j < n - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
            }
        }
    }
    return arr;
}
// 时间复杂度:最好情况O(n),平均和最坏情况O(n^2)

实现排序算法之插入排序

从数组的第二个元素开始,依次将元素插入到已排序的部分中,直到整个数组排序完成。

function insertionSort(arr) {
    const n = arr.length;
    for (let i = 1; i < n; i++) {
        let current = arr[i];
        let j = i - 1;
        while (j >= 0 && arr[j] > current) {
            arr[j + 1] = arr[j];
            j--;
        }
        arr[j + 1] = current;
    }
    return arr;
}
// 时间复杂度:最好情况O(n),平均和最坏情况O(n^2)

实现排序算法之选择排序

在未排序的部分中选择最小(或最大)的元素,然后放到已排序部分的末尾。

function selectionSort(arr) {
    const n = arr.length;
    for (let i = 0; i < n - 1; i++) {
        let minIndex = i;
        for (let j = i + 1; j < n; j++) {
            if (arr[j] < arr[minIndex]) {
                minIndex = j;
            }
        }
        [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];
    }
    return arr;
}
// 时间复杂度:最好、平均和最坏情况均为O(n^2)

实现排序算法之快速排序

通过选择一个基准元素,将数组分为小于基准的部分和大于基准的部分,然后递归地对这两部分进行排序。

function quickSort(arr) {
    if (arr.length <= 1) {
        return arr;
    }
    const pivot = arr[0];
    const left = [];
    const right = [];
    for (let i = 1; i < arr.length; i++) {
        if (arr[i] < pivot) {
            left.push(arr[i]);
        } else {
            right.push(arr[i]);
        }
    }
    return quickSort(left).concat(pivot, quickSort(right));
}
// 时间复杂度:平均情况O(n log n),最坏情况O(n^2)

实现排序算法之归并排序

将数组分成两半,分别对每半进行排序,然后再合并两个有序的子数组。

function mergeSort(arr) {
    if (arr.length <= 1) {
        return arr;
    }
    const mid = Math.floor(arr.length / 2);
    const left = arr.slice(0, mid);
    const right = arr.slice(mid);
    return merge(mergeSort(left), mergeSort(right));
}

function merge(left, right) {
    let result = [];
    let leftIndex = 0;
    let rightIndex = 0;
    while (leftIndex < left.length && rightIndex < right.length) {
        if (left[leftIndex] < right[rightIndex]) {
            result.push(left[leftIndex]);
            leftIndex++;
        } else {
            result.push(right[rightIndex]);
            rightIndex++;
        }
    }
    return result.concat(left.slice(leftIndex), right.slice(rightIndex));
}
// 时间复杂度:平均、最好和最坏情况均为O(n log n)

实现排序算法之堆排序

将数组看成一个二叉堆,构建最大堆或最小堆,然后不断地从堆顶取出元素。

function heapSort(arr) {
    const n = arr.length;

    for (let i = Math.floor(n / 2) - 1; i >= 0; i--) {
        heapify(arr, n, i);
    }

    for (let i = n - 1; i > 0; i--) {
        [arr[0], arr[i]] = [arr[i], arr[0]];
        heapify(arr, i, 0);
    }

    return arr;
}

function heapify(arr, n, i) {
    let largest = i;
    const left = 2 * i + 1;
    const right = 2 * i + 2;

    if (left < n && arr[left] > arr[largest]) {
        largest = left;
    }

    if (right < n && arr[right] > arr[largest]) {
        largest = right;
    }

    if (largest !== i) {
        [arr[i], arr[largest]] = [arr[largest], arr[i]];
        heapify(arr, n, largest);
    }
}
// 时间复杂度:平均、最好和最坏情况均为O(n log n)

实现排序算法之基数排序

将数字按照位数切分,然后分别对各个位进行排序。

function radixSort(arr) {
    const maxDigits = Math.max(...arr).toString().length;

    for (let i = 0; i < maxDigits; i++) {
        const buckets = Array.from({ length: 10 }, () => []);

        for (let j = 0; j < arr.length; j++) {
            const digit = getDigit(arr[j], i);
            buckets[digit].push(arr[j]);
        }

        arr = buckets.flat();
    }

    return arr;
}

function getDigit(num, place) {
    return Math.floor(Math.abs(num) / Math.pow(10, place)) % 10;
}
// 时间复杂度:最好、平均和最坏情况均为O(nk),其中 k 为最大位数

实现一个将列表转成树的方法

将一个列表(数组)转换为树形结构可以使用递归来实现。每个元素代表树中的一个节点,元素中的某个属性(例如父节点 ID 或者层级信息)用来建立节点之间的关系。

const dataList = [
    { id: 1, name: 'Node 1', parentId: null },
    { id: 2, name: 'Node 1.1', parentId: 1 },
    { id: 3, name: 'Node 1.2', parentId: 1 },
    { id: 4, name: 'Node 2', parentId: null },
    { id: 5, name: 'Node 2.1', parentId: 4 },
    { id: 6, name: 'Node 2.1.1', parentId: 5 },
];

function listToTree(list, parentId = null) {
    const tree = [];

    for (const item of list) {
        if (item.parentId === parentId) {
            const children = listToTree(list, item.id);
            if (children.length) {
                item.children = children;
            }
            tree.push(item);
        }
    }

    return tree;
}

const tree = listToTree(dataList);
console.log(tree);

实现判断节点是否在二叉树中

二叉查找树(Binary Search Tree,BST)是一种特殊的二叉树,其中每个节点的左子树的值都小于该节点的值,而右子树的值都大于该节点的值。

class TreeNode {
    constructor(value) {
        this.value = value;
        this.left = null;
        this.right = null;
    }
}

class BinarySearchTree {
    constructor() {
        this.root = null;
    }

    insert(value) {
        const newNode = new TreeNode(value);

        if (!this.root) {
            this.root = newNode;
            return this;
        }

        let current = this.root;
        while (true) {
            if (value === current.value) return undefined;
            if (value < current.value) {
                if (!current.left) {
                    current.left = newNode;
                    return this;
                }
                current = current.left;
            } else {
                if (!current.right) {
                    current.right = newNode;
                    return this;
                }
                current = current.right;
            }
        }
    }

    search(value) {
        if (!this.root) return false;

        let current = this.root;
        while (current) {
            if (value === current.value) return true;
            if (value < current.value) {
                current = current.left;
            } else {
                current = current.right;
            }
        }

        return false;
    }
}

const bst = new BinarySearchTree();

bst.insert(10);
bst.insert(5);
bst.insert(15);
bst.insert(3);
bst.insert(7);
bst.insert(12);
bst.insert(18);

console.log(bst.search(7)); // 输出: true
console.log(bst.search(9)); // 输出: false

实现获取二叉树的路径

class TreeNode {
    constructor(value) {
        this.value = value;
        this.left = null;
        this.right = null;
    }
}

class BinarySearchTree {
    constructor() {
        this.root = null;
    }

    insert(value) {
        const newNode = new TreeNode(value);

        if (!this.root) {
            this.root = newNode;
            return this;
        }

        let current = this.root;
        while (true) {
            if (value === current.value) return undefined;
            if (value < current.value) {
                if (!current.left) {
                    current.left = newNode;
                    return this;
                }
                current = current.left;
            } else {
                if (!current.right) {
                    current.right = newNode;
                    return this;
                }
                current = current.right;
            }
        }
    }

    findPath(value) {
        const path = [];
        this._findPathRecursive(this.root, value, path);
        return path;
    }

    _findPathRecursive(node, value, path) {
        if (!node) return false;

        path.push(node.value);

        if (value === node.value) {
            return true;
        } else if (value < node.value) {
            if (this._findPathRecursive(node.left, value, path)) {
                return true;
            }
        } else {
            if (this._findPathRecursive(node.right, value, path)) {
                return true;
            }
        }

        path.pop();
        return false;
    }
}

const bst = new BinarySearchTree();

bst.insert(10);
bst.insert(5);
bst.insert(15);
bst.insert(3);
bst.insert(7);
bst.insert(12);
bst.insert(18);

console.log(bst.findPath(7)); // 输出: [10, 5, 7]
console.log(bst.findPath(12)); // 输出: [10, 15, 12]
console.log(bst.findPath(9)); // 输出: []

实现斐波那契数列

斐波那契数列是一个经典的递归数列,其中每个数都是前两个数之和。通常情况下,斐波那契数列的前两个数是 0 和 1 递归实现

function fibonacciRecursive(n) {
    if (n <= 0) return 0;
    if (n === 1) return 1;
    return fibonacciRecursive(n - 1) + fibonacciRecursive(n - 2);
}

迭代实现

function fibonacciIterative(n) {
    if (n <= 0) return 0;
    if (n === 1) return 1;

    let prevPrev = 0;
    let prev = 1;
    let current = 0;

    for (let i = 2; i <= n; i++) {
        current = prev + prevPrev;
        prevPrev = prev;
        prev = current;
    }

    return current;
}

使用数组缓存实现

function fibonacciWithCache(n, cache = {}) {
    if (n in cache) return cache[n];
    if (n <= 0) return 0;
    if (n === 1) return 1;

    cache[n] = fibonacciWithCache(n - 1, cache) + fibonacciWithCache(n - 2, cache);
    return cache[n];
}

实现一个求最长递增子序列的值

求解最长递增子序列(Longest Increasing Subsequence,LIS)是一个经典的动态规划问题。最长递增子序列是指在一个序列中找到一个递增的子序列,使得该子序列的长度最大

function longestIncreasingSubsequence(arr) {
    const n = arr.length;
    const dp = new Array(n).fill(1); // 初始化每个元素为长度为 1 的子序列

    for (let i = 1; i < n; i++) {
        for (let j = 0; j < i; j++) {
            if (arr[i] > arr[j]) {
                dp[i] = Math.max(dp[i], dp[j] + 1);
            }
        }
    }

    return Math.max(...dp);
}
const arr = [10, 22, 9, 33, 21, 50, 41, 60, 80];
console.log(longestIncreasingSubsequence(arr)); 
// 输出: 6(最长递增子序列是 [10, 22, 33, 50, 60, 80])

在上述代码中,dp 数组用于存储以每个元素结尾的最长递增子序列的长度。外层循环遍历每个元素,内层循环遍历所有比当前元素小的元素,如果当前元素能够接在前面的元素后面形成递增子序列,就更新 dp 数组的值。

实现获取字符串的所有排列组合

获取字符串的所有排列组合可以使用递归方法来实现

function getAllPermutations(input) {
    const result = [];
    const inputArr = input.split('');

    function permute(arr, currentIndex) {
        if (currentIndex === arr.length - 1) {
            result.push(arr.join(''));
            return;
        }

        for (let i = currentIndex; i < arr.length; i++) {
            [arr[currentIndex], arr[i]] = [arr[i], arr[currentIndex]];
            permute([...arr], currentIndex + 1);
            [arr[currentIndex], arr[i]] = [arr[i], arr[currentIndex]]; // 回溯
        }
    }

    permute(inputArr, 0);
    return result;
}
const inputString = 'abc';
const permutations = getAllPermutations(inputString);
console.log(permutations);
// ['abc', 'acb', 'bac', 'bca', 'cab', 'cba']

实现版本号排序

版本号排序是一个常见的问题,可以将版本号拆分成数字部分,然后按照每个数字进行比较和排序

function compareVersion(version1, version2) {
    const v1 = version1.split('.').map(Number);
    const v2 = version2.split('.').map(Number);

    const maxLength = Math.max(v1.length, v2.length);

    for (let i = 0; i < maxLength; i++) {
        const num1 = i < v1.length ? v1[i] : 0;
        const num2 = i < v2.length ? v2[i] : 0;

        if (num1 < num2) {
            return -1;
        } else if (num1 > num2) {
            return 1;
        }
    }

    return 0;
}

function sortVersions(versions) {
    return versions.sort(compareVersion);
}

const versions = ['1.3.0', '1.1.2', '1.2.1', '1.4.2'];
const sortedVersions = sortVersions(versions);
console.log(sortedVersions); // 输出: ['1.1.2', '1.2.1', '1.3.0', '1.4.2']

实现数组中n个数之和等于目标值,输出这n个数

要在数组中找到 n 个数之和等于目标值,可以使用递归回溯的方法来实现

function findNNumbersWithSum(nums, targetSum, n) {
    const result = [];
    backtrack(nums, targetSum, n, 0, [], result);
    return result;
}

function backtrack(nums, targetSum, n, startIndex, currentCombination, result) {
    if (n === 0 && targetSum === 0) {
        result.push([...currentCombination]);
        return;
    }

    for (let i = startIndex; i < nums.length; i++) {
        if (nums[i] > targetSum) {
            continue; // 剪枝:当前数大于剩余目标值,跳过
        }

        currentCombination.push(nums[i]);
        backtrack(nums, targetSum - nums[i], n - 1, i + 1, currentCombination, result);
        currentCombination.pop();
    }
}

const nums = [2, 3, 7, 4, 8, 5];
const targetSum = 12;
const n = 3;

const result = findNNumbersWithSum(nums, targetSum, n);
console.log(result);
// [[2, 3, 7], [3, 4, 5]]

findNNumbersWithSum 函数用于寻找数组中 n 个数之和等于目标值的组合。backtrack 函数用于进行递归回溯,尝试所有可能的组合。在满足条件时,将当前组合添加到结果数组中。

实现一个判断输入是不是回文字符串的函数

回文字符串是指从前往后读和从后往前读都相同的字符串。换句话说,就是一个字符串在忽略非字母和数字字符、忽略大小写的情况下,正着读和倒着读都是一样的。例如,"level"、"deified"、"madam" 都是回文字符串

function isPalindrome(str) {
    str = str.toLowerCase().replace(/[^a-z0-9]/g, ''); // 将字符串转为小写并去除非字母和数字字符
    const length = str.length;

    for (let i = 0; i < Math.floor(length / 2); i++) {
        if (str[i] !== str[length - 1 - i]) {
            return false;
        }
    }

    return true;
}
console.log(isPalindrome("A man, a plan, a canal, Panama")); // 输出: true
console.log(isPalindrome("racecar")); // 输出: true
console.log(isPalindrome("hello")); // 输出: false

实现获取字符串中无重复字符的最长子串

要获取字符串中无重复字符的最长子串,可以使用滑动窗口的方法。滑动窗口是一个用于维护子串的窗口,通过移动窗口的左右边界来寻找满足条件的子串

function lengthOfLongestSubstring(s) {
    const charIndexMap = new Map(); // 用于存储字符和其最近出现的索引
    let maxLength = 0;
    let left = 0; // 滑动窗口的左边界

    for (let right = 0; right < s.length; right++) {
        const char = s[right];

        if (charIndexMap.has(char) && charIndexMap.get(char) >= left) {
            left = charIndexMap.get(char) + 1; // 更新左边界,避免重复字符
        }

        maxLength = Math.max(maxLength, right - left + 1);
        charIndexMap.set(char, right);
    }

    return maxLength;
}
console.log(lengthOfLongestSubstring("abcabcbb")); // 输出: 3 ("abc")
console.log(lengthOfLongestSubstring("bbbbb")); // 输出: 1 ("b")
console.log(lengthOfLongestSubstring("pwwkew")); // 输出: 3 ("wke")

lengthOfLongestSubstring 函数使用了滑动窗口的思想,通过维护一个字符到其最近出现索引的映射,以及左边界来计算最长子串的长度。

实现删除字符串中的所有相邻重复项

要删除字符串中的所有相邻重复项,可以使用栈来实现。遍历字符串,如果当前字符与栈顶字符相同,则弹出栈顶字符,否则将当前字符入栈。最终,栈中的字符就是删除相邻重复项后的结果

function removeAdjacentDuplicates(str) {
    const stack = [];
    
    for (const char of str) {
        if (stack.length && stack[stack.length - 1] === char) {
            stack.pop();
        } else {
            stack.push(char);
        }
    }

    return stack.join('');
}
console.log(removeAdjacentDuplicates("abbaca")); // 输出: "ca"
console.log(removeAdjacentDuplicates("azxxzy")); // 输出: "ay"