前端高频算法题

74 阅读8分钟
  1. 三数之和 题目:给定一个数组nums,判断 nums 中是否存在三个元素a,b,c,使得 a + b + c = target,找出所有满足条件且不重复的三元组合 输入:nums: [5, 2, 1, 1, 3, 4, 6] ;target:8 输出:[[1, 1, 6], [1, 2, 5], [1, 3, 4]]
function threeSum(nums, target) {
    // 结果数组
    const result = [];
    // 对数组进行排序,便于去重和双指针操作
    nums.sort((a, b) => a - b);
    const n = nums.length;
    
    // 遍历每个元素作为三元组的第一个元素
    for (let i = 0; i < n - 2; i++) {
        // 跳过重复的第一个元素
        if (i > 0 && nums[i] === nums[i - 1]) {
            continue;
        }
        
        // 左右双指针
        let left = i + 1;
        let right = n - 1;
        
        while (left < right) {
            const sum = nums[i] + nums[left] + nums[right];
            
            if (sum === target) {
                // 找到符合条件的三元组,加入结果
                result.push([nums[i], nums[left], nums[right]]);
                
                // 跳过重复的左指针元素
                while (left < right && nums[left] === nums[left + 1]) {
                    left++;
                }
                
                // 跳过重复的右指针元素
                while (left < right && nums[right] === nums[right - 1]) {
                    right--;
                }
                
                // 移动双指针
                left++;
                right--;
            } else if (sum < target) {
                // 和小于目标值,移动左指针增大和
                left++;
            } else {
                // 和大于目标值,移动右指针减小和
                right--;
            }
        }
    }
    
    return result;
}

// 测试
const nums = [5, 2, 1, 1, 3, 4, 6];
const target = 8;
console.log(threeSum(nums, target)); 
// 输出: [[1, 1, 6], [1, 2, 5], [1, 3, 4]]
  1. 当青蛙要跳上 n 级台阶时,它可以从 n-1 级台阶跳 1 级上去,也可以从 n-2 级台阶跳 2 级上去,依此类推,甚至可以直接从 0 级台阶跳 n 级上去。
// 方法1:使用递归,直接根据递推公式计算
function jumpWays1(n) {
    if (n <= 1) {
        return 1;
    }
    let result = 0;
    for (let i = 0; i < n; i++) {
        result += jumpWays1(i);
    }
    return result;
}

// 方法2:利用数学规律 f(n) = 2^(n-1)
function jumpWays2(n) {
    if (n === 0) return 1;
    return Math.pow(2, n - 1);
}

// 测试
console.log("跳1级台阶的方法数:", jumpWays1(1)); // 1
console.log("跳2级台阶的方法数:", jumpWays1(2)); // 2
console.log("跳3级台阶的方法数:", jumpWays1(3)); // 4
console.log("跳4级台阶的方法数:", jumpWays1(4)); // 8
console.log("跳5级台阶的方法数:", jumpWays2(5)); // 16
console.log("跳10级台阶的方法数:", jumpWays2(10)); // 512
  1. 查找字符串中最长无重复字符子串的长度
function lengthOfLongestSubstring(s) {
    // 存储字符最后出现的位置
    const charMap = new Map();
    // 最长子串长度
    let maxLength = 0;
    // 滑动窗口的起始位置
    let start = 0;
    
    for (let end = 0; end < s.length; end++) {
        const currentChar = s[end];
        
        // 如果字符已存在于当前窗口中,则移动窗口起始位置
        if (charMap.has(currentChar) && charMap.get(currentChar) >= start) {
            start = charMap.get(currentChar) + 1;
        }
        
        // 更新字符最后出现的位置
        charMap.set(currentChar, end);
        
        // 计算当前窗口长度并更新最大长度
        maxLength = Math.max(maxLength, end - start + 1);
    }
    
    return maxLength;
}

// 测试示例
console.log(lengthOfLongestSubstring("abcabcbb")); // 输出: 3 ("abc")
console.log(lengthOfLongestSubstring("bbbbb"));    // 输出: 1 ("b")
console.log(lengthOfLongestSubstring("pwwkew"));   // 输出: 3 ("wke")
console.log(lengthOfLongestSubstring(""));         // 输出: 0
console.log(lengthOfLongestSubstring("au"));       // 输出: 2
  1. 实现一个全排列
function permute(nums) {
    // 存储所有排列结果
    const result = [];
    
    // 回溯函数:current表示当前已选元素,remaining表示剩余可选元素
    function backtrack(current, remaining) {
        // 终止条件:如果没有剩余元素,说明当前排列已完成
        if (remaining.length === 0) {
            result.push([...current]);
            return;
        }
        
        // 遍历剩余元素,尝试每种可能的选择
        for (let i = 0; i < remaining.length; i++) {
            // 选择当前元素
            current.push(remaining[i]);
            
            // 递归处理剩余元素(排除已选择的元素)
            const newRemaining = [...remaining.slice(0, i), ...remaining.slice(i + 1)];
            backtrack(current, newRemaining);
            
            // 回溯:撤销选择,尝试下一种可能
            current.pop();
        }
    }
    
    // 从空数组和完整nums开始递归
    backtrack([], nums);
    return result;
}

// 测试示例
console.log(permute([1, 2, 3]));
// 输出: [
//   [1,2,3], [1,3,2],
//   [2,1,3], [2,3,1],
//   [3,1,2], [3,2,1]
// ]

console.log(permute(['a', 'b']));
// 输出: [ ['a','b'], ['b','a'] ]

  1. 实现一个快速排序
function quickSort(arr) {
    // 基本情况:如果数组长度小于等于1,则已经是排序好的
    if (arr.length <= 1) {
        return arr;
    }
    
    // 选择基准元素(这里选择中间的元素)
    const pivotIndex = Math.floor(arr.length / 2);
    const pivot = arr.splice(pivotIndex, 1)[0];
    
    // 小于基准的元素放左边,大于基准的元素放右边
    const left = [];
    const right = [];
    
    // 遍历剩余元素,分配到左右数组
    for (let i = 0; i < arr.length; i++) {
        if (arr[i] < pivot) {
            left.push(arr[i]);
        } else {
            right.push(arr[i]);
        }
    }
    
    // 递归排序左右数组,并与基准元素合并
    return quickSort(left).concat([pivot], quickSort(right));
}

// 测试示例
const numbers = [64, 34, 25, 12, 22, 11, 90];
console.log("排序前:", numbers);
console.log("排序后:", quickSort(numbers)); // [11, 12, 22, 25, 34, 64, 90]

// 测试字符串数组
const words = ["banana", "apple", "orange", "grape"];
console.log("排序前:", words);
console.log("排序后:", quickSort(words)); // ["apple", "banana", "grape", "orange"]


  1. 买卖股票的最佳时机
/**
 * 计算买卖股票的最佳时机(简洁版)
 * @param {number[]} prices - 股票价格数组,prices[i]表示第i天的股票价格
 * @return {number} 最大利润,如果不能获利则返回0
 */
function maxProfit(prices) {
  // 初始化最小价格为无穷大,最大利润为0
  let min = Infinity, max = 0;
  
  // 遍历所有价格
  for (const price of prices) {
    // 更新到目前为止的最低价格
    min = Math.min(min, price);
    // 更新最大利润(当前价格 - 最低价格 与 已有最大利润 的较大值)
    max = Math.max(max, price - min);
  }
  
  return max;
};

// 测试示例
console.log(maxProfit([7, 1, 5, 3, 6, 4])); // 输出: 5
console.log(maxProfit([7, 6, 4, 3, 1]));   // 输出: 0
console.log(maxProfit([1, 2]));           // 输出: 1
  1. 出1到10000之间的所有对称数字(回文数)
/**
 * 找出1到10000之间的所有对称数字(回文数)
 * @returns {number[]} 包含所有对称数字的数组
 */
const findPalindromes = () => {
  const palindromes = [];
  for (let i = 1; i <= 10000; i++) {
    // 直接在循环中检查是否为回文数
    const str = i.toString();
    if (str === str.split('').reverse().join('')) {
      palindromes.push(i);
    }
  }
  return palindromes;
};

// 执行并输出结果
const result = findPalindromes();
console.log(`1到10000中的对称数字共有${result.length}个:`);
console.log(result);
  1. 接雨水 (要求计算在给定的高度数组中,雨后能接住的雨水总量)
/**
 * 计算能接住的雨水总量
 * @param {number[]} height - 高度数组,表示各个位置的高度
 * @return {number} 能接住的雨水总量
 */
function trap(height) {
    // 边界情况处理
    if (height.length === 0) return 0;
    
    let left = 0;                  // 左指针,从数组开头开始
    let right = height.length - 1;  // 右指针,从数组末尾开始
    let leftMax = 0;               // 左侧已遍历的最大高度
    let rightMax = 0;              // 右侧已遍历的最大高度
    let water = 0;                 // 接住的雨水总量
    
    // 当左右指针相遇时停止遍历
    while (left < right) {
        // 左侧高度小于右侧高度,处理左侧
        if (height[left] < height[right]) {
            // 如果当前左高度大于左侧最大高度,更新左侧最大高度
            if (height[left] >= leftMax) {
                leftMax = height[left];
            } else {
                // 否则,当前位置可以接住的雨水量为左侧最大高度减去当前高度
                water += leftMax - height[left];
            }
            left++;  // 左指针右移
        } else {
            // 右侧高度小于等于左侧高度,处理右侧
            if (height[right] >= rightMax) {
                rightMax = height[right];
            } else {
                water += rightMax - height[right];
            }
            right--;  // 右指针左移
        }
    }
    
    return water;
}

// 测试示例
console.log(trap([0,1,0,2,1,0,1,3,2,1,2,1])); // 输出: 6
console.log(trap([4,2,0,3,2,5]));             // 输出: 9
console.log(trap([]));                        // 输出: 0
  1. 打家劫舍 你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。

给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。

/**
 * 计算环形排列的房屋中能偷窃的最大金额
 * @param {number[]} nums - 每个房屋中的金额数组
 * @return {number} 不触动警报的情况下能偷窃的最大金额
 */
function rob(nums) {
    // 边界情况处理
    if (nums.length === 0) return 0;
    if (nums.length === 1) return nums[0];
    
    /**
     * 计算线性排列的房屋中能偷窃的最大金额
     * @param {number[]} arr - 线性排列的房屋金额数组
     * @return {number} 最大偷窃金额
     */
    function robLinear(arr) {
        let prev = 0;   // 前一个房屋的最大金额
        let curr = 0;   // 当前的最大金额
        
        for (const num of arr) {
            // 临时保存当前最大值
            const temp = curr;
            // 更新当前最大值:要么不偷当前房屋(保持原样),要么偷当前房屋(加上前前房屋的最大值)
            curr = Math.max(curr, prev + num);
            // 更新前一个房屋的最大值
            prev = temp;
        }
        
        return curr;
    }
    
    // 情况1:不偷最后一个房屋,计算[0, n-2]范围的最大值
    const case1 = robLinear(nums.slice(0, nums.length - 1));
    // 情况2:不偷第一个房屋,计算[1, n-1]范围的最大值
    const case2 = robLinear(nums.slice(1));
    
    // 返回两种情况的最大值
    return Math.max(case1, case2);
}

// 测试示例
console.log(rob([2,3,2]));    // 输出: 3
console.log(rob([1,2,3,1]));  // 输出: 4
console.log(rob([1,2,3]));    // 输出: 3
console.log(rob([5]));        // 输出: 5
console.log(rob([1,5,3,7]));  // 输出: 12 (5+7)
  1. 有效的括号 题目:给定一个只包括 '(',')','{','}','[',']' 的字符串 s,判断字符串是否有效。有效字符串需满足: 左括号必须用相同类型的右括号闭合。 左括号必须以正确的顺序闭合。
function isValid(s) {
  const stack = [];
  const map = { ')': '(', '}': '{', ']': '[' };
  
  for (const char of s) {
    if (char in map) {
      // 右括号:检查栈顶是否匹配
      if (stack.pop() !== map[char]) return false;
    } else {
      // 左括号:入栈
      stack.push(char);
    }
  }
  
  return stack.length === 0; // 栈空则所有括号匹配
}