JS算法题

JavaScript 算法面试题:

数组去重

  • 描述:给定一个数组,要求去除重复的元素。
  • 关键点:可以使用 Set 数据结构,或者使用双重循环、indexOf、filter 等方法。
    在 JavaScript 中实现数组去重(也就是移除数组中的重复元素)有多种方法。这里介绍几种常用的方法:

1、使用 Set

由于 ES6 中的 Set 是一个只含不同元素的集合,可以用它来快速去重。

const array = [1, 2, 2, 3, 4, 4, 5];
const uniqueArray = [...new Set(array)];
console.log(uniqueArray); // 输出: [1, 2, 3, 4, 5]

2、使用 filter 方法

filter 方法可用于遍历数组并返回一个通过测试(由提供的函数实现)的所有元素的新数组。检查元素的索引是否是第一次出现的索引,如果是,则保留该元素。

const array = [1, 2, 2, 3, 4, 4, 5];
const uniqueArray = array.filter((item, index) => array.indexOf(item) === index);
console.log(uniqueArray); // 输出: [1, 2, 3, 4, 5]

3、使用 reduce 方法

reduce 方法可用于累加数组中的元素,同时检查累加的结果中是否已包含当前元素,如果未包含,则加入。

const array = [1, 2, 2, 3, 4, 4, 5];
const uniqueArray = array.reduce((acc, current) => {
  if (!acc.includes(current)) {
    acc.push(current);
  }
  return acc;
}, []);
console.log(uniqueArray); // 输出: [1, 2, 3, 4, 5]

4、使用对象属性

通过将数组元素作为对象的属性,并利用对象属性的唯一性来去重。

const array = [1, 2, 2, 3, '4', '4', 5, '5'];
const uniqueObj = {};
array.forEach(item => {
  uniqueObj[item] = true;
});
const uniqueArray = Object.keys(uniqueObj).map(item => {
  // 尝试转换为数字类型,若失败则返回原字符串
  return isNaN(Number(item)) ? item : Number(item);
});
console.log(uniqueArray); // 输出: [1, 2, 3, "4", 5, "5"]

每种方法都有其适用场景。例如,使用 Set 是最简单和最现代的方法,但如果需要更复杂的去重逻辑(例如,基于对象属性),可能需要使用 filterreduce。选择哪种方法取决于具体的需求和偏好。

字符串反转

  • 描述:给定一个字符串,要求将其反转。
  • 关键点:可以使用数组的 reverse 方法,或者使用循环逐字符反转。
    在 JavaScript 中,字符串反转是一个常见的问题,可以通过多种方式解决。下面将介绍几种常用的方法来实现字符串反转。

方法 1:使用数组的 reverse() 方法

由于字符串是不可变的(immutable),我们首先需要将字符串转换为数组,使用数组的 reverse() 方法反转数组,然后再将数组转换回字符串。

function reverseString(str) {
  return str.split("").reverse().join("");
}

console.log(reverseString("hello")); // 输出: "olleh"

方法 2:使用循环

另一种方法是使用循环来构建新的反转字符串。这种方法不依赖于内置的数组方法,可能对理解字符串操作有更好的教育意义。

function reverseString(str) {
  let reversed = "";
  for (let character of str) {
    reversed = character + reversed;
  }
  return reversed;
}

console.log(reverseString("hello")); // 输出: "olleh"

方法 3:使用递归

递归是另一种解决方案,尽管它可能不是解决这个特定问题的最高效方法,但它展示了递归的用法。

function reverseString(str) {
  if (str === "") {
    return "";
  } else {
    return reverseString(str.substr(1)) + str[0];
  }
}

console.log(reverseString("hello")); // 输出: "olleh"

方法 4:使用 Array.reduce()

还可以利用 Array.reduce() 方法将字符串转换为数组,然后使用累加器从左到右构建反转的字符串。

function reverseString(str) {
  return str.split("").reduce((reversed, character) => character + reversed, "");
}

console.log(reverseString("hello")); // 输出: "olleh"

每种方法都有其独特之处,而选择哪一种取决于个人偏好、特定情境下的性能考虑,以及对代码可读性的要求。在实际开发中,使用数组的 reverse() 方法因其简洁性而最为常见。

回文判断

  • 描述:检查给定的字符串是否是回文(正读和反读都相同的字符串)。
  • 关键点:要考虑大小写和非字母字符。
    在 JavaScript 中,判断一个字符串是否是回文(即正读和反读都一样的字符串)是一个常见的编程问题。一个字符串是回文的,如果它从前往后读和从后往前读是相同的。以下是一个简单的函数,用来判断给定的字符串是否是回文:
function isPalindrome(str) {
  str = str.replace(/\W/g, '').toLowerCase(); // 去除非字母数字字符并转小写
  return str === str.split('').reverse().join(''); // 判断字符串是否与其反转相等
}

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

这个函数首先使用正则表达式 \W 去除了字符串中的所有非字母数字字符(这包括空格和标点符号),并把所有字母转换成小写,以确保精确比对。然后,它通过分割字符串为字符数组,反转这个数组,再将其连接成字符串的方式来获取原字符串的反转。最后,它比较原字符串与反转后的字符串是否相同,从而判断是否是回文。

如果你想要一个不使用内建函数的解法,也可以采用双指针的方法来比较字符串的前半部分和后半部分:

function isPalindrome(str) {
  str = str.replace(/\W/g, '').toLowerCase(); // 去除非字母数字字符并转小写
  let left = 0;                               // 初始化左指针
  let right = str.length - 1;                 // 初始化右指针
  
  while (left < right) {                      // 当左指针小于右指针时循环
    if (str[left] !== str[right]) {           // 如果字符不匹配,则不是回文
      return false;
    }
    left++;                                   // 移动左指针
    right--;                                  // 移动右指针
  }
  
  return true;                                // 所有字符都被匹配,是回文
}

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

这个版本的 isPalindrome 函数同样首先清洗字符串,然后使用两个指针从字符串的两端向中间遍历,若在中途发现对应位置的字符不相等,则判断为非回文,并结束函数返回 false。如果所有对应位置的字符都相等,则在循环结束时返回 true,确认字符串是回文。

找出数组中最大/最小值

  • 描述:给定一个数字数组,找出其中的最大值或最小值。
  • 关键点:可以使用 Math.max/Math.min 与 spread 操作符,或者自己实现遍历逻辑。
    在 JavaScript 中,您可以使用多种方法来找出数组中的最大值和最小值。以下是一些通用的方法:

方法 1:使用 Math.max()Math.min() 与扩展运算符

let numbers = [1, 2, 3, 4, 5];
let max = Math.max(...numbers);
let min = Math.min(...numbers);

console.log(max); // 输出: 5
console.log(min); // 输出: 1

方法 2:使用 Array.prototype.reduce()

let numbers = [1, 2, 3, 4, 5];

let max = numbers.reduce((a, b) => Math.max(a, b));
let min = numbers.reduce((a, b) => Math.min(a, b));

console.log(max); // 输出: 5
console.log(min); // 输出: 1

方法 3:排序数组

这种方法不推荐用于仅找出最大或最小值,因为它的性能开销比前两种方法要大(因为它会对整个数组进行排序)。

let numbers = [1, 2, 3, 4, 5];
numbers.sort((a, b) => a - b);

let min = numbers[0];
let max = numbers[numbers.length - 1];

console.log(max); // 输出: 5
console.log(min); // 输出: 1

方法 4:使用循环

let numbers = [1, 2, 3, 4, 5];
let max = numbers[0];
let min = numbers[0];

for (let i = 1; i < numbers.length; i++) {
  if (numbers[i] > max) {
    max = numbers[i];
  }
  if (numbers[i] < min) {
    min = numbers[i];
  }
}

console.log(max); // 输出: 5
console.log(min); // 输出: 1

在现代 JavaScript 中,方法 1 是最简单和最常用的方式,但是如果您处理的数组非常大,或者您希望有更好的性能表现,可能需要使用循环(方法 4),因为它避免了创建额外的数组副本和大量的函数调用。

两数之和

  • 描述:给定一个整数数组和一个目标值,找出数组中和为目标值的两个数。
  • 关键点:可以使用哈希表记录访问过的数字及其索引。
    “两数之和” 是一个常见的编程问题,通常在算法面试中出现。给定一个整数数组 nums 和一个目标值 target,找出数组中和为目标值的两个整数,并返回它们的数组下标。

您可以使用哈希表来优化查找过程,从而实现线性时间复杂度的解决方案。以下是 JavaScript 语言中实现这个解决方案的方法:

function twoSum(nums, target) {
    let map = new Map(); // 创建一个哈希表

    // 遍历数组
    for (let i = 0; i < nums.length; i++) {
        let complement = target - nums[i]; // 计算补数
        
        // 如果哈希表中存在当前元素的补数,则返回结果
        if (map.has(complement)) {
            return [map.get(complement), i];
        }

        // 将当前元素及其索引添加到哈希表中
        map.set(nums[i], i);
    }

    // 如果找不到有效的两数之和,返回空数组或其他指示
    return [];
}

console.log(twoSum([2, 7, 11, 15], 9)); // 输出: [0, 1]

在这个解决方案中:

  1. 我们首先创建一个哈希表(Map)用于存储数组元素及其索引。
  2. 然后,我们遍历数组中的每个元素:
    • 我们计算当前元素的“补数”(即 target 减去当前元素的值)。
    • 如果这个补数已经存在于哈希表中,这意味着我们找到了一个有效的两数之和,我们返回这两个数的索引。
    • 如果补数不存在于哈希表中,我们将当前元素及其索引添加到哈希表,以便用于后续的查找。
  3. 如果遍历完成后仍未找到,返回空数组或其他指示找不到的信息。

这种方法的时间复杂度是 O(n),因为我们只需要遍历一次数组,空间复杂度也是 O(n),用于存储哈希表。

  1. 斐波那契数列

    • 描述:生成一个长度为 n 的斐波那契数列。
    • 关键点:可以使用递归,但更高效的方法是使用动态规划或迭代。
  2. 排序算法

    • 描述:实现一个常见的排序算法,如冒泡排序、选择排序、插入排序、归并排序、快速排序等。
    • 关键点:了解每种排序算法的原理和复杂度。

查找数组中的第 K 大元素

  • 描述:在未排序的数组中找到第 k 个最大的元素。
  • 关键点:可以排序数组后直接查找,但更有效的方法是使用快速选择算法。
    查找数组中的第 K 大元素是一类常见的算法问题,可以通过多种方法解决,包括排序、堆、快速选择等方法。每种方法都有其适用场景和性能特点。下面我将分别介绍几种方法的实现方式,以及它们的优缺点。

方法1:排序

最直观的方式是将数组排序,然后直接访问第 K 大的位置。在 JavaScript 中,可以使用数组的 sort() 方法进行排序,然后访问相应位置的元素。

function findKthLargest(nums, k) {
  nums.sort((a, b) => b - a); // 降序排序
  return nums[k - 1];
}

// 示例
console.log(findKthLargest([3, 2, 1, 5, 6, 4], 2)); // 输出: 5

这种方法非常简单,但排序的时间复杂度为 O(n log n),可能不是最优的解决方案。

方法2:使用最小堆

在 JavaScript 中,虽然没有内置的堆数据结构,但可以使用数组模拟一个最小堆,这种方法的时间复杂度是 O(n log k),适合处理大数据集。

class MinHeap {
  constructor() {
    this.heap = [];
  }

  insert(value) {
    this.heap.push(value);
    this.bubbleUp();
  }

  bubbleUp() {
    let index = this.heap.length - 1;
    const lastInsertedValue = this.heap[index];

    while (index > 0) {
      let parentIndex = Math.floor((index - 1) / 2);
      let parentValue = this.heap[parentIndex];

      if (lastInsertedValue >= parentValue) {
        break;
      }

      this.heap[parentIndex] = lastInsertedValue;
      this.heap[index] = parentValue;
      index = parentIndex;
    }
  }

  extractMin() {
    const min = this.heap[0];
    const last = this.heap.pop();
    if (this.heap.length > 0) {
      this.heap[0] = last;
      this.sinkDown(0);
    }
    return min;
  }

  sinkDown(index) {
    let smallest = index;
    const left = 2 * index + 1;
    const right = 2 * index + 2;

    if (left < this.heap.length && this.heap[left] < this.heap[smallest]) {
      smallest = left;
    }

    if (right < this.heap.length && this.heap[right] < this.heap[smallest]) {
      smallest = right;
    }

    if (smallest !== index) {
      [this.heap[smallest], this.heap[index]] = [this.heap[index], this.heap[smallest]];
      this.sinkDown(smallest);
    }
  }

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

function findKthLargest(nums, k) {
  let minHeap = new MinHeap();
  for (let num of nums) {
    minHeap.insert(num);
    if (minHeap.size() > k) {
      minHeap.extractMin();
    }
  }
  return minHeap.extractMin();
}

// 示例
console.log(findKthLargest([3, 2, 1, 5, 6, 4], 2)); // 输出: 5

方法3:快速选择算法

快速选择是基于快速排序的选择算法,其可以在平均 O(n) 的时间复杂度内解决这个问题,是一个非常高效的解决方案。

function findKthLargest(nums, k) {
  const n = nums.length;
  const kthPosition = n - k;

  function quickSelect(l, r) {
    const pivot = nums[r];
    let p = l;

    for (let i = l; i < r; i++) {
      if (nums[i] <= pivot) {
        [nums[i], nums[p]] = [nums[p], nums[i]];
        p++;
      }
    }

    [nums[p], nums[r]] = [nums[r], nums[p]];

    if (p > kthPosition) return quickSelect(l, p - 1);
    else if (p < kthPosition) return quickSelect(p + 1, r);
    return nums[p];
  }

  return quickSelect(0, n - 1);
}

// 示例
console.log(findKthLargest([3, 2, 1, 5, 6, 4], 2)); // 输出: 5

快速选择算法在平均情况下非常高效,但在最坏情况下的时间复杂度可能退化到 O(n^2)。可以通过随机选择基准元素来优化这个问题,从而使得算法在大多数情况下都非常高效。

有效的括号**

  • 描述:给定一个只包括 '('')''{''}''['']' 的字符串,判断字符串是否有效。
  • 关键点:使用栈的数据结构来处理匹配问题。
    为了判断一个只包含括号的字符串是否有效,我们可以利用栈的数据结构。一个有效的括号字符串意味着每个开括号都必须有一个对应的同类型的闭括号,并且括号的顺序要正确。

下面是一个 JavaScript 函数,用于检查一个括号字符串是否有效:

function isValid(s) {
    const stack = [];
    const mapping = { ')': '(', '}': '{', ']': '[' };

    for (let char of s) {
        if (mapping[char]) {
            const topElement = stack.length === 0 ? '#' : stack.pop();
            if (mapping[char] !== topElement) {
                return false;
            }
        } else {
            stack.push(char);
        }
    }

    return stack.length === 0;
}

// 示例
console.log(isValid("()")); // 应该返回 true
console.log(isValid("()[]{}")); // 应该返回 true
console.log(isValid("(]")); // 应该返回 false
console.log(isValid("([)]")); // 应该返回 false
console.log(isValid("{[]}")); // 应该返回 true

在这个函数中,我们首先定义一个栈 stack 和一个映射 mapping,映射包含了每种类型的闭括号对应的开括号。然后我们遍历字符串中的每个字符:

  • 如果当前字符是一个闭括号,我们检查栈顶的元素是否是对应的开括号:
    • 如果是,我们从栈中弹出这个元素;
    • 如果不是,或者栈为空,这表示括号是无效的,我们返回 false
  • 如果当前字符是一个开括号,我们将其推入栈中。

最后,我们检查栈是否为空。如果栈为空,这意味着所有的开括号都被正确闭合,字符串是有效的,我们返回 true。如果栈不为空,这意味着有一些开括号没有被闭合,因此我们返回 false

合并两个有序数组**

- 描述:将两个有序整数数组合并为一个有序数组。
- 关键点:使用“双指针”方法从两个数组的末尾开始合并,或者直接合并后排序。

合并两个有序数组通常涉及到将两个已经排序的数组组合成一个新的有序数组。这是一个常见的问题,可以高效地通过双指针方法解决。下面是使用 JavaScript 实现合并两个有序数组的方法:

function mergeSortedArrays(arr1, arr2) {
    let mergedArray = [];
    let index1 = 0, index2 = 0;

    // 遍历两个数组,直到一个数组的元素都被处理完
    while (index1 < arr1.length && index2 < arr2.length) {
        if (arr1[index1] < arr2[index2]) {
            mergedArray.push(arr1[index1]);
            index1++;
        } else {
            mergedArray.push(arr2[index2]);
            index2++;
        }
    }

    // 添加剩余元素
    while (index1 < arr1.length) {
        mergedArray.push(arr1[index1]);
        index1++;
    }
    while (index2 < arr2.length) {
        mergedArray.push(arr2[index2]);
        index2++;
    }

    return mergedArray;
}

// 示例
const array1 = [1, 3, 5];
const array2 = [2, 4, 6];
console.log(mergeSortedArrays(array1, array2));  // 输出 [1, 2, 3, 4, 5, 6]

这个函数的工作原理如下:

  1. 创建一个空数组 mergedArray 用于存放结果。
  2. 使用两个索引 index1index2 分别遍历两个输入数组 arr1arr2
  3. 比较两个数组当前位置的元素,将较小的元素添加到结果数组中,并移动相应数组的索引。
  4. 当一个数组的所有元素都被遍历完后,将另一个数组剩余的元素全部复制到结果数组中。

这种方法时间复杂度为 O(n + m),其中 n 和 m 分别是两个数组的长度。这是合并两个有序数组的最佳时间复杂度。

二叉树的遍历

- 描述:实现二叉树的前序、中序、后序遍历。
- 关键点:递归是最简单的实现方式,但也需要了解迭代(用栈实现)的方法。

二叉树遍历是数据结构中的一个基本操作,它按照特定顺序访问二叉树中的每个节点,确保每个节点被访问一次。遍历二叉树的方法主要有三种:前序遍历、中序遍历和后序遍历。除此之外,还有层序遍历,这是一种按照树的每一层从左到右访问的方法。下面是这些遍历方法的详细介绍和JavaScript代码示例:

1. 前序遍历 (Pre-order Traversal)

在前序遍历中,我们先访问当前节点,然后递归地进行左子树遍历,最后递归地进行右子树遍历。

function preorderTraversal(root) {
    const result = [];
    function traverse(node) {
        if (!node) return;
        result.push(node.val);  // 访问当前节点
        traverse(node.left);    // 遍历左子树
        traverse(node.right);   // 遍历右子树
    }
    traverse(root);
    return result;
}

2. 中序遍历 (In-order Traversal)

在中序遍历中,我们先递归地遍历左子树,然后访问当前节点,最后递归地遍历右子树。

function inorderTraversal(root) {
    const result = [];
    function traverse(node) {
        if (!node) return;
        traverse(node.left);    // 遍历左子树
        result.push(node.val);  // 访问当前节点
        traverse(node.right);   // 遍历右子树
    }
    traverse(root);
    return result;
}

3. 后序遍历 (Post-order Traversal)

在后序遍历中,我们先递归地遍历左子树,然后递归地遍历右子树,最后访问当前节点。

function postorderTraversal(root) {
    const result = [];
    function traverse(node) {
        if (!node) return;
        traverse(node.left);    // 遍历左子树
        traverse(node.right);   // 遍历右子树
        result.push(node.val);  // 访问当前节点
    }
    traverse(root);
    return result;
}

4. 层序遍历 (Level-order Traversal)

层序遍历按照树的层次从上到下访问节点。

function levelOrder(root) {
    if (!root) return [];
    const result = [];
    const queue = [root];  // 初始化队列

    while (queue.length) {
        const levelSize = queue.length;  // 当前层的节点数
        const currentLevel = [];

        for (let i = 0; i < levelSize; i++) {
            const node = queue.shift();  // 取出队列首元素
            currentLevel.push(node.val); // 访问当前节点
            if (node.left) queue.push(node.left);  // 左子节点入队
            if (node.right) queue.push(node.right); // 右子节点入队
        }

        result.push(currentLevel);
    }

    return result;
}

每种遍历方法都有它独特的应用场景,例如,在执行某些类型的搜索或处理任务时,选择合适的遍历方法可以优化性能。

二分查找

- 描述:在一个有序数组中找到特定元素的索引。
- 关键点:理解二分查找的逻辑,实现循环或递归的二分查找算法。

这些题目覆盖了大部分前端面试中可能出现的算法问题。然而,真正重要的是理解每个问题的底层原理和解决方法,以及能够根据这些原理适应和解决新问题的能力。在准备面试时,建议不仅仅是解决这些问题,还要深入了解它们的变体和相关概念。

  1. 有序链表转换为二叉搜索树
    将有序链表转换为二叉搜索树涉及的关键是保持二叉搜索树的平衡,这样可以确保树的所有操作都能在 O(log n) 时间内完成。下面是将有序链表转换成高度平衡的二叉搜索树(BST)的步骤:

  2. 确定树的中间元素: 为了使BST平衡,链表中间的元素应该成为树的根节点。在有序数组中,这很容易做到,但在链表中稍微复杂一些,因为我们不能像在数组中那样直接访问中间元素。我们需要使用快慢指针的方法来找到链表的中间元素。

  3. 递归构造树: 一旦我们找到了树的根节点,我们就可以递归地对根节点的左半部分链表和右半部分链表执行同样的操作,从而构建左右子树。

  4. 处理边界情况: 当链表为空或只有一个元素时,需要正确处理这些边界情况。

下面是一个用JavaScript实现的示例代码,展示了如何将有序链表转换为二叉搜索树:

class ListNode {
  constructor(value) {
    this.value = value;
    this.next = null;
  }
}

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

// 通过快慢指针找到中间节点
function findMiddle(start, end) {
  let slow = start;
  let fast = start;

  while (fast !== end && fast.next !== end) {
    slow = slow.next;
    fast = fast.next.next;
  }

  return slow;
}

// 主函数
function sortedListToBST(head) {
  if (head === null) return null;
  if (head.next === null) return new TreeNode(head.value);

  // 找到中间节点
  let mid = findMiddle(head, null);

  // 创建树的节点
  let root = new TreeNode(mid.value);

  // 由链表左半部构造左子树
  root.left = sortedListToBST(head, mid);

  // 由链表右半部构造右子树
  root.right = sortedListToBST(mid.next, null);

  return root;
}

// 注意:这里假设链表已经通过某种方式创建并传递给sortedListToBST函数

在上述代码中,我们定义了两个类 ListNodeTreeNode 分别表示链表的节点和树的节点。函数 findMiddle 用于找到链表中间的节点。sortedListToBST 函数递归地构建了整棵树,将链表中间的节点作为树的根,并对左右两部分递归调用自身以构建整棵树。

这样,你就可以将一个有序的链表转换为一个高度平衡的二叉搜索树了。

遍历两个链表,然后对其中的值进行大数相加

要处理大数相加的问题,特别是当这些数字超出JavaScript中可以安全表示的整数范围(即 Number.MAX_SAFE_INTEGER,在JavaScript中约为 9,007,199,254,740,991)时,我们不能只简单地将链表中的数值转换为整型然后相加。因为这样可能会导致精度丢失。

我们需要逐位地处理这些数值,就像小学时我们学习的那样,在纸上从右向左,一位一位地相加数字。

下面是一个用JavaScript实现的示例,这个示例演示了如何对两个表示大数的链表进行相加,这两个链表的每个节点都包含了一个数字的一位:

class ListNode {
  constructor(value = 0, next = null) {
    this.value = value;
    this.next = next;
  }
}

function addTwoNumbers(l1, l2) {
  let head = new ListNode(0);
  let node = head;
  let carry = 0;

  while (l1 || l2) {
    let l1Value = l1 ? l1.value : 0;
    let l2Value = l2 ? l2.value : 0;

    let sum = l1Value + l2Value + carry;
    carry = Math.floor(sum / 10); // 计算进位
    node.next = new ListNode(sum % 10); // 创建新的节点存储当前位的结果

    // 移动指针
    node = node.next;
    if (l1) {
      l1 = l1.next;
    }
    if (l2) {
      l2 = l2.next;
    }
  }

  // 如果最后还有进位,就添加一个新的节点
  if (carry > 0) {
    node.next = new ListNode(carry);
  }

  return head.next; // 返回头节点的下一个节点,因为头节点是初始化时候的0
}

// 测试用例
// 创建代表数字 123 的链表
let l1 = new ListNode(3);
l1 = new ListNode(2, l1);
l1 = new ListNode(1, l1);

// 创建代表数字 456 的链表
let l2 = new ListNode(6);
l2 = new ListNode(5, l2);
l2 = new ListNode(4, l2);

// 相加
let result = addTwoNumbers(l1, l2);

// 输出结果
let str = "";
while (result) {
  str += result.value;
  result = result.next;
}
console.log(str.split("").reverse().join("")); // 应该输出 "579"

在这个实现中,我们从两个链表的开始位置(即最低位)开始工作,一次处理一个节点,直到我们到达链表的末尾。如果在处理完所有的节点之后,仍然有一个进位,我们将其作为一个新的节点添加到结果链表的末尾。

这个方法可以处理任何大小的数字,因为我们没有试图把整个数字转换为一个JavaScript Number 类型,而是通过模拟手工加法的方式来逐位相加。这种方法是大数加法的常见解决方案,因为它可以处理超过编程语言整数类型限制的数字。

最长递增子序列

最长递增子序列(Longest Increasing Subsequence,简称LIS)问题是一个典型的计算机算法问题,用于查找一个给定序列中的最长递增子序列的长度。递增子序列是指一个序列的元素按照升序排列,但元素不必相邻。

一个常见的解决这个问题的算法是动态规划。动态规划算法的时间复杂度为 O(n^2),其中 n 是序列的长度。此外,也有时间复杂度为 O(n log n) 的算法,这里先提供一个时间复杂度为 O(n^2) 的动态规划解法。

以下是使用JavaScript实现的最长递增子序列的动态规划算法:

function lengthOfLIS(nums) {
  if (nums.length === 0) return 0;

  // dp 数组,dp[i] 表示以 nums[i] 结尾的最长递增子序列的长度
  const dp = new Array(nums.length).fill(1);
  let maxLIS = 1;

  for (let i = 1; i < nums.length; i++) {   // 遍历数组
    for (let j = 0; j < i; j++) {            // 对于每个 i,从 0 到 i-1 逐个查看 j
      if (nums[i] > nums[j]) {               // 如果可以将 nums[i] 添加到以 nums[j] 结尾的序列
        dp[i] = Math.max(dp[i], dp[j] + 1);  // 更新 dp[i]
      }
    }
    maxLIS = Math.max(maxLIS, dp[i]);        // 更新整个序列的最大长度
  }

  return maxLIS;
}

// 示例
const nums = [10, 9, 2, 5, 3, 7, 101, 18];
console.log(lengthOfLIS(nums));  // 应当输出 4

在这个算法中,我们使用一个数组 dp 来存储到当前元素为止的最长递增子序列的长度。对于数组中的每个元素 nums[i],我们检查在它之前的所有元素 nums[j](其中 j < i)以找到最长的递增子序列,然后根据情况更新 dp[i]

记住,LIS 不一定是唯一的,这种方法仅计算出最长递增子序列的长度,而不是序列本身。如果你想重构出实际的序列,你需要在算法中添加额外的步骤来追踪序列的构建。

对于一个更高效的解法,可以使用二分查找来降低时间复杂度到 O(n log n),但实现起来更复杂一些。如果你想了解这种解法,请告诉我,我可以帮你详细解释。

实现一个订阅模式有on和emit

实现Promise.all

js对象转树

要实现从 JavaScript 对象数组转换为树形结构,最常见的场景是将具有父子关系的扁平数据转换为层次化的树形数据。这种转换在处理例如菜单、组织结构或任何需要层次展示的数据时非常有用。

下面提供一个简单的函数arrayToTree,它可以将形如:

[
  { id: 1, parentId: null, name: "Root" },
  { id: 2, parentId: 1, name: "Child 1" },
  { id: 3, parentId: 1, name: "Child 2" },
  { id: 4, parentId: 2, name: "Grandchild 1" }
]

这样的对象数组转换为树形结构。该函数假定每个对象都有一个唯一的id和一个指向其父对象的parentIdparentIdnull或者不匹配任何对象的id时,该对象被视为根节点。

function arrayToTree(items) {
  // 创建一个映射,以便快速通过id访问节点
  const itemMap = {};

  // 首先,将所有节点存储到映射中
  for (const item of items) {
    itemMap[item.id] = { ...item, children: [] };
  }

  // 再次遍历所有节点,建立父子关系
  const tree = [];
  for (const item of items) {
    if (item.parentId === null) {
      // 没有parentId的是根节点
      tree.push(itemMap[item.id]);
    } else {
      // 有parentId的是子节点,将其添加到父节点的children数组中
      if (itemMap[item.parentId]) { // 确保父节点存在
        itemMap[item.parentId].children.push(itemMap[item.id]);
      }
    }
  }

  return tree;
}

// 示例数据
const items = [
  { id: 1, parentId: null, name: "Root" },
  { id: 2, parentId: 1, name: "Child 1" },
  { id: 3, parentId: 1, name: "Child 2" },
  { id: 4, parentId: 2, name: "Grandchild 1" }
];

// 转换为树形结构
const tree = arrayToTree(items);
console.log(tree);

在这个函数中:

  • 首先,通过一个循环,我们将所有节点存储到一个映射(itemMap)中,使我们能够通过id快速访问每个节点。同时,我们为每个节点增加一个children数组,用于存储其子节点。
  • 然后,我们再次遍历所有节点,根据parentId建立父子关系。对于每个节点,如果它有parentId,我们就找到对应的父节点,并将当前节点添加到父节点的children数组中。
  • 对于parentIdnull的节点,我们将其视为根节点,直接添加到最终的树数组中。

递归实现

将 JavaScript 对象数组转换为树形结构是一个有趣且常见的编程任务,尤其是在处理具有层级关系的数据时,例如菜单、组织结构图等。以下是一个递归函数arrayToTree的实现示例,这个函数将会把含有父子关系的扁平数据转换为树形结构。

在这个实现中,我们假设每个对象有一个唯一的id和一个parentIdparentId用于表示该对象的父对象的 ID。根对象的parentIdnull

function arrayToTree(items, parentId = null) {
    return items
      .filter(item => item.parentId === parentId)
      .map(item => ({
        ...item,
        children: arrayToTree(items, item.id)
      }));
}

// 示例数据
const items = [
  { id: 1, parentId: null, name: "Root" },
  { id: 2, parentId: 1, name: "Child 1" },
  { id: 3, parentId: 1, name: "Child 2" },
  { id: 4, parentId: 2, name: "Grandchild 1" }
];

// 转换为树形结构
const tree = arrayToTree(items);
console.log(JSON.stringify(tree, null, 2));

这个函数的工作原理如下:

  1. 它首先通过filter方法找出所有当前层级的节点(即所有parentId等于当前parentId参数的节点)。
  2. 对于每个这样的节点,它使用map方法创建一个新对象,该对象包含原节点的所有属性以及一个新的children属性。
  3. children属性通过递归调用arrayToTree函数计算得到,传入当前节点的id作为下一个层级的parentId
  4. 当一个节点没有子节点时(即filter结果为空数组时),递归自然结束,因为map将不会执行任何操作。

这种方法的优势在于它的简洁性和表达力,但在处理非常大的数据集时需要注意性能,因为频繁的数组操作(如filtermap)可能会导致较高的计算成本。对于大多数常见用例,这种实现方式既高效又足够快。

  • 19
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值