返回

用不同方式实现JavaScript中查找数组第 k 大数字的方法

前端

在 JavaScript 中查找数组中的第 k 大数字:四种高效方法

JavaScript 广泛用于处理和分析数据。在处理数据时,查找数组中的第 k 大数字是一项常见的任务。本文将探讨四种高效的方法来实现此目标,即:排序、堆、快速选择和分治算法。每种方法都提供独特的权衡,并且针对不同的数据集表现出色。

排序

排序是一种简单而直接的方法,它涉及对数组进行排序,然后返回第 k 个元素。虽然该方法易于实现,但它具有 O(n log n) 的时间复杂度,其中 n 是数组的大小。对于大型数组,此方法可能变得低效。

function findKthLargestBySorting(arr, k) {
  arr.sort((a, b) => b - a);
  return arr[k - 1];
}

堆是一种二叉树,其中每个节点的值大于或等于其子节点的值。使用堆,我们可以将数组构建为二叉堆,然后依次弹出堆顶元素,直到剩下 k 个元素。此方法的时间复杂度为 O(n log k)。

function findKthLargestByHeap(arr, k) {
  const heap = new BinaryHeap(arr);
  for (let i = 0; i < arr.length - k; i++) {
    heap.pop();
  }
  return heap.peek();
}

class BinaryHeap {
  constructor(arr) {
    this.heap = arr;
    this.buildHeap();
  }

  buildHeap() {
    for (let i = Math.floor(this.heap.length / 2) - 1; i >= 0; i--) {
      this.heapify(i);
    }
  }

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

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

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

    if (largest !== i) {
      [this.heap[i], this.heap[largest]] = [this.heap[largest], this.heap[i]];
      this.heapify(largest);
    }
  }

  pop() {
    const last = this.heap.length - 1;
    [this.heap[0], this.heap[last]] = [this.heap[last], this.heap[0]];
    const popped = this.heap.pop();
    this.heapify(0);
    return popped;
  }

  peek() {
    return this.heap[0];
  }
}

快速选择

快速选择是一种基于快速排序算法的分治算法。该算法选择一个枢纽元素,并将其放置在数组的正确位置,使得所有小于枢纽元素的元素位于枢纽元素的左侧,所有大于枢纽元素的元素位于枢纽元素的右侧。该过程递归地应用于枢纽元素的左右两侧,直到找到第 k 大元素。快速选择算法的平均时间复杂度为 O(n),最坏时间复杂度为 O(n^2)。

function findKthLargestByQuickSelect(arr, k) {
  return quickSelect(arr, 0, arr.length - 1, k);
}

function quickSelect(arr, left, right, k) {
  if (left === right) {
    return arr[left];
  }

  const pivotIndex = partition(arr, left, right);

  if (pivotIndex === k - 1) {
    return arr[pivotIndex];
  } else if (pivotIndex < k - 1) {
    return quickSelect(arr, pivotIndex + 1, right, k);
  } else {
    return quickSelect(arr, left, pivotIndex - 1, k);
  }
}

function partition(arr, left, right) {
  const pivot = arr[right];
  let i = left;

  for (let j = left; j < right; j++) {
    if (arr[j] < pivot) {
      [arr[i], arr[j]] = [arr[j], arr[i]];
      i++;
    }
  }

  [arr[i], arr[right]] = [arr[right], arr[i]];

  return i;
}

分治算法

分治算法是一种分治算法,它将数组划分为更小的子数组,并递归地应用相同的算法于每个子数组。该算法选择一个枢纽元素,并将其放置在数组的正确位置,使得所有小于枢纽元素的元素位于枢纽元素的左侧,所有大于枢纽元素的元素位于枢纽元素的右侧。该过程递归地应用于枢纽元素的左右两侧,直到找到第 k 大元素。分治算法的时间复杂度为 O(n log n)。

function findKthLargestByDivideAndConquer(arr, k) {
  return divideAndConquer(arr, 0, arr.length - 1, k);
}

function divideAndConquer(arr, left, right, k) {
  if (left === right) {
    return arr[left];
  }

  const pivotIndex = partition(arr, left, right);

  if (pivotIndex === k) {
    return arr[pivotIndex];
  } else if (pivotIndex < k) {
    return divideAndConquer(arr, pivotIndex + 1, right, k);
  } else {
    return divideAndConquer(arr, left, pivotIndex - 1, k);
  }
}

function partition(arr, left, right) {
  const pivot = arr[right];
  let i = left;

  for (let j = left; j < right; j++) {
    if (arr[j] < pivot) {
      [arr[i], arr[j]] = [arr[j], arr[i]];
      i++;
    }
  }

  [arr[i], arr[right]] = [arr[right], arr[i]];

  return i;
}

性能比较

以下是四种方法的性能比较:

方法 时间复杂度 空间复杂度
排序 O(n log n) O(1)
O(n log k) O(k)
快速选择 O(n) (平均) / O(n^2) (最坏) O(1)
分治算法 O(n log n) O(log n)

结论

在 JavaScript 中查找数组中的第 k 大数字有多种方法,每种方法都有其优点和缺点。排序方法简单易懂,但对于大型数组效率低下。堆方法在空间效率方面有所权衡,但时间效率更高。快速选择算法在平均情况下是最快的,但最坏情况下效率较低。分治算法的性能与排序方法相似,但空间效率更高。选择最合适的方法取决于数组大小和所需的性能水平。

常见问题解答

1. 哪种方法对于小型数组最有效?

对于小型数组,排序方法通常是最有效的,因为它简单易懂,并且对于较小的数组具有良好的性能。

2. 哪种方法对于大型数组最有效?

对于大型数组,快速选择算法在平均情况下是最有效的,因为它具有 O(n) 的时间复杂度。

3. 哪种方法在最坏情况下表现最差?

在最坏情况下,快速选择算法的表现最差,因为其时间复杂度为 O(n^2)。

4. 哪种方法需要最小的空间?

快速选择算法需要最小的空间,因为它只使用 O(1) 的额外空间。

5. 哪种方法最通用?

快速选择算法是最通用的,因为它可以处理各种输入,包括非排序和重复的数组。