Перейти к содержанию
Приглашаем вас участвовать в развитии русской версии! Подробнее здесь.

# 8.3 Задача Top-k

Question

Дан неупорядоченный массив `nums` длины \(n\) . Требуется вернуть наибольшие \(k\) элементов массива.

Для этой задачи мы сначала покажем два относительно прямолинейных способа решения, а затем более эффективный способ на основе кучи.

## 8.3.1 Метод 1: выбор через обход

Как показано на рисунке 8-6, можно выполнить \(k\) проходов по массиву и на каждом проходе извлекать соответственно \(1\)-й, \(2\)-й, \(\dots\) , \(k\)-й по величине элемент. Временная сложность такого подхода равна \(O(nk)\) .

Этот метод подходит только для случая \(k \ll n\) , потому что когда \(k\) приближается к \(n\) , его временная сложность стремится к \(O(n^2)\) , а это уже очень затратно.

Рисунок 8-6 Поиск наибольших k элементов через обход

Tip

Когда \(k = n\) , мы получаем полную упорядоченную последовательность, и в этот момент задача становится эквивалентной алгоритму «сортировка выбором».

## 8.3.2 Метод 2: сортировка

Как показано на рисунке 8-7, можно сначала отсортировать массив `nums` , а затем вернуть его крайние правые \(k\) элементов. Временная сложность такого метода равна \(O(n \log n)\) .

Очевидно, что этот способ делает слишком много, потому что нам нужно только найти наибольшие \(k\) элементов, а сортировать остальные элементы совсем не обязательно.

Рисунок 8-7 Поиск наибольших k элементов через сортировку

## 8.3.3 Метод 3: куча

Задачу Top-k можно решить гораздо эффективнее с помощью кучи, как показано на рисунке 8-8.

1. Инициализировать минимальную кучу, у которой вершина содержит наименьший элемент.
2. Сначала по очереди поместить в кучу первые \(k\) элементов массива.
3. Начиная с элемента номер \(k + 1\) , если текущий элемент больше элемента на вершине кучи, то извлечь вершину кучи и поместить в кучу текущий элемент.
4. После завершения обхода в куче будут храниться как раз наибольшие \(k\) элементов.

<1><2><3><4><5><6><7><8><9>

Рисунок 8-8 Поиск наибольших k элементов с помощью кучи

Пример кода приведен ниже:

Python C++Java C#Go Swift JS TS Dart Rust C Kotlin Ruby
top_k.py

    def top_k_heap(nums: list[int], k: int) -> list[int]:
        """Найти k наибольших элементов массива с помощью кучи"""
        # Инициализация минимальной кучи
        heap = []
        # Поместить первые k элементов массива в кучу
        for i in range(k):
            heapq.heappush(heap, nums[i])
        # Начиная с элемента k+1, поддерживать длину кучи равной k
        for i in range(k, len(nums)):
            # Если текущий элемент больше элемента на вершине кучи, извлечь вершину кучи и добавить текущий элемент в кучу
            if nums[i] > heap[0]:
                heapq.heappop(heap)
                heapq.heappush(heap, nums[i])
        return heap

top_k.cpp

    /* Найти k наибольших элементов массива с помощью кучи */
    priority_queue<int, vector<int>, greater<int>> topKHeap(vector<int> &nums, int k) {
        // Инициализация минимальной кучи
        priority_queue<int, vector<int>, greater<int>> heap;
        // Поместить первые k элементов массива в кучу
        for (int i = 0; i < k; i++) {
            heap.push(nums[i]);
        }
        // Начиная с элемента k+1, поддерживать длину кучи равной k
        for (int i = k; i < nums.size(); i++) {
            // Если текущий элемент больше элемента на вершине кучи, извлечь вершину кучи и добавить текущий элемент в кучу
            if (nums[i] > heap.top()) {
                heap.pop();
                heap.push(nums[i]);
            }
        }
        return heap;
    }

top_k.java

    /* Найти k наибольших элементов массива с помощью кучи */
    Queue<Integer> topKHeap(int[] nums, int k) {
        // Инициализация минимальной кучи
        Queue<Integer> heap = new PriorityQueue<Integer>();
        // Поместить первые k элементов массива в кучу
        for (int i = 0; i < k; i++) {
            heap.offer(nums[i]);
        }
        // Начиная с элемента k+1, поддерживать длину кучи равной k
        for (int i = k; i < nums.length; i++) {
            // Если текущий элемент больше элемента на вершине кучи, извлечь вершину кучи и добавить текущий элемент в кучу
            if (nums[i] > heap.peek()) {
                heap.poll();
                heap.offer(nums[i]);
            }
        }
        return heap;
    }

top_k.cs

    /* Найти k наибольших элементов массива с помощью кучи */
    PriorityQueue<int, int> TopKHeap(int[] nums, int k) {
        // Инициализация минимальной кучи
        PriorityQueue<int, int> heap = new();
        // Поместить первые k элементов массива в кучу
        for (int i = 0; i < k; i++) {
            heap.Enqueue(nums[i], nums[i]);
        }
        // Начиная с элемента k+1, поддерживать длину кучи равной k
        for (int i = k; i < nums.Length; i++) {
            // Если текущий элемент больше элемента на вершине кучи, извлечь вершину кучи и добавить текущий элемент в кучу
            if (nums[i] > heap.Peek()) {
                heap.Dequeue();
                heap.Enqueue(nums[i], nums[i]);
            }
        }
        return heap;
    }

top_k.go

    /* Найти k наибольших элементов массива с помощью кучи */
    func topKHeap(nums []int, k int) *minHeap {
        // Инициализация минимальной кучи
        h := &minHeap{}
        heap.Init(h)
        // Поместить первые k элементов массива в кучу
        for i := 0; i < k; i++ {
            heap.Push(h, nums[i])
        }
        // Начиная с элемента k+1, поддерживать длину кучи равной k
        for i := k; i < len(nums); i++ {
            // Если текущий элемент больше элемента на вершине кучи, извлечь вершину кучи и добавить текущий элемент в кучу
            if nums[i] > h.Top().(int) {
                heap.Pop(h)
                heap.Push(h, nums[i])
            }
        }
        return h
    }

top_k.swift

    /* Найти k наибольших элементов массива с помощью кучи */
    func topKHeap(nums: [Int], k: Int) -> [Int] {
        // Инициализировать минимальную кучу и построить ее по первым k элементам
        var heap = Heap(nums.prefix(k))
        // Начиная с элемента k+1, поддерживать длину кучи равной k
        for i in nums.indices.dropFirst(k) {
            // Если текущий элемент больше элемента на вершине кучи, извлечь вершину кучи и добавить текущий элемент в кучу
            if nums[i] > heap.min()! {
                _ = heap.removeMin()
                heap.insert(nums[i])
            }
        }
        return heap.unordered
    }

top_k.js

    /* Добавление элемента в кучу */
    function pushMinHeap(maxHeap, val) {
        // Инвертировать знак элемента
        maxHeap.push(-val);
    }

    /* Извлечение элемента из кучи */
    function popMinHeap(maxHeap) {
        // Инвертировать знак элемента
        return -maxHeap.pop();
    }

    /* Доступ к элементу на вершине кучи */
    function peekMinHeap(maxHeap) {
        // Инвертировать знак элемента
        return -maxHeap.peek();
    }

    /* Извлечь элементы из кучи */
    function getMinHeap(maxHeap) {
        // Инвертировать знак элемента
        return maxHeap.getMaxHeap().map((num) => -num);
    }

    /* Найти k наибольших элементов массива с помощью кучи */
    function topKHeap(nums, k) {
        // Инициализация минимальной кучи
        // Обратите внимание: мы инвертируем все элементы кучи, чтобы с помощью максимальной кучи имитировать минимальную
        const maxHeap = new MaxHeap([]);
        // Поместить первые k элементов массива в кучу
        for (let i = 0; i < k; i++) {
            pushMinHeap(maxHeap, nums[i]);
        }
        // Начиная с элемента k+1, поддерживать длину кучи равной k
        for (let i = k; i < nums.length; i++) {
            // Если текущий элемент больше элемента на вершине кучи, извлечь вершину кучи и добавить текущий элемент в кучу
            if (nums[i] > peekMinHeap(maxHeap)) {
                popMinHeap(maxHeap);
                pushMinHeap(maxHeap, nums[i]);
            }
        }
        // Вернуть элементы кучи
        return getMinHeap(maxHeap);
    }

top_k.ts

    /* Добавление элемента в кучу */
    function pushMinHeap(maxHeap: MaxHeap, val: number): void {
        // Инвертировать знак элемента
        maxHeap.push(-val);
    }

    /* Извлечение элемента из кучи */
    function popMinHeap(maxHeap: MaxHeap): number {
        // Инвертировать знак элемента
        return -maxHeap.pop();
    }

    /* Доступ к элементу на вершине кучи */
    function peekMinHeap(maxHeap: MaxHeap): number {
        // Инвертировать знак элемента
        return -maxHeap.peek();
    }

    /* Извлечь элементы из кучи */
    function getMinHeap(maxHeap: MaxHeap): number[] {
        // Инвертировать знак элемента
        return maxHeap.getMaxHeap().map((num: number) => -num);
    }

    /* Найти k наибольших элементов массива с помощью кучи */
    function topKHeap(nums: number[], k: number): number[] {
        // Инициализация минимальной кучи
        // Обратите внимание: мы инвертируем все элементы кучи, чтобы с помощью максимальной кучи имитировать минимальную
        const maxHeap = new MaxHeap([]);
        // Поместить первые k элементов массива в кучу
        for (let i = 0; i < k; i++) {
            pushMinHeap(maxHeap, nums[i]);
        }
        // Начиная с элемента k+1, поддерживать длину кучи равной k
        for (let i = k; i < nums.length; i++) {
            // Если текущий элемент больше элемента на вершине кучи, извлечь вершину кучи и добавить текущий элемент в кучу
            if (nums[i] > peekMinHeap(maxHeap)) {
                popMinHeap(maxHeap);
                pushMinHeap(maxHeap, nums[i]);
            }
        }
        // Вернуть элементы кучи
        return getMinHeap(maxHeap);
    }

top_k.dart

    /* Найти k наибольших элементов массива с помощью кучи */
    MinHeap topKHeap(List<int> nums, int k) {
      // Инициализировать минимальную кучу, поместив в нее первые k элементов массива
      MinHeap heap = MinHeap(nums.sublist(0, k));
      // Начиная с элемента k+1, поддерживать длину кучи равной k
      for (int i = k; i < nums.length; i++) {
        // Если текущий элемент больше элемента на вершине кучи, извлечь вершину кучи и добавить текущий элемент в кучу
        if (nums[i] > heap.peek()) {
          heap.pop();
          heap.push(nums[i]);
        }
      }
      return heap;
    }

top_k.rs

    /* Найти k наибольших элементов массива с помощью кучи */
    fn top_k_heap(nums: Vec<i32>, k: usize) -> BinaryHeap<Reverse<i32>> {
        // BinaryHeap — это максимальная куча; с помощью Reverse элементы инвертируются, чтобы реализовать минимальную кучу
        let mut heap = BinaryHeap::<Reverse<i32>>::new();
        // Поместить первые k элементов массива в кучу
        for &num in nums.iter().take(k) {
            heap.push(Reverse(num));
        }
        // Начиная с элемента k+1, поддерживать длину кучи равной k
        for &num in nums.iter().skip(k) {
            // Если текущий элемент больше элемента на вершине кучи, извлечь вершину кучи и добавить текущий элемент в кучу
            if num > heap.peek().unwrap().0 {
                heap.pop();
                heap.push(Reverse(num));
            }
        }
        heap
    }

top_k.c

    /* Добавление элемента в кучу */
    void pushMinHeap(MaxHeap *maxHeap, int val) {
        // Инвертировать знак элемента
        push(maxHeap, -val);
    }

    /* Извлечение элемента из кучи */
    int popMinHeap(MaxHeap *maxHeap) {
        // Инвертировать знак элемента
        return -pop(maxHeap);
    }

    /* Доступ к элементу на вершине кучи */
    int peekMinHeap(MaxHeap *maxHeap) {
        // Инвертировать знак элемента
        return -peek(maxHeap);
    }

    /* Извлечь элементы из кучи */
    int *getMinHeap(MaxHeap *maxHeap) {
        // Инвертировать все элементы кучи и записать их в массив res
        int *res = (int *)malloc(maxHeap->size * sizeof(int));
        for (int i = 0; i < maxHeap->size; i++) {
            res[i] = -maxHeap->data[i];
        }
        return res;
    }

    /* Извлечь элементы из кучи */
    int *getMinHeap(MaxHeap *maxHeap) {
        // Инвертировать все элементы кучи и записать их в массив res
        int *res = (int *)malloc(maxHeap->size * sizeof(int));
        for (int i = 0; i < maxHeap->size; i++) {
            res[i] = -maxHeap->data[i];
        }
        return res;
    }

    // Функция поиска k наибольших элементов массива на основе кучи
    int *topKHeap(int *nums, int sizeNums, int k) {
        // Инициализация минимальной кучи
        // Обратите внимание: мы инвертируем все элементы кучи, чтобы с помощью максимальной кучи имитировать минимальную
        int *empty = (int *)malloc(0);
        MaxHeap *maxHeap = newMaxHeap(empty, 0);
        // Поместить первые k элементов массива в кучу
        for (int i = 0; i < k; i++) {
            pushMinHeap(maxHeap, nums[i]);
        }
        // Начиная с элемента k+1, поддерживать длину кучи равной k
        for (int i = k; i < sizeNums; i++) {
            // Если текущий элемент больше элемента на вершине кучи, извлечь вершину кучи и добавить текущий элемент в кучу
            if (nums[i] > peekMinHeap(maxHeap)) {
                popMinHeap(maxHeap);
                pushMinHeap(maxHeap, nums[i]);
            }
        }
        int *res = getMinHeap(maxHeap);
        // Освободить память
        delMaxHeap(maxHeap);
        return res;
    }

top_k.kt

    /* Найти k наибольших элементов массива с помощью кучи */
    fun topKHeap(nums: IntArray, k: Int): Queue<Int> {
        // Инициализация минимальной кучи
        val heap = PriorityQueue<Int>()
        // Поместить первые k элементов массива в кучу
        for (i in 0..<k) {
            heap.offer(nums[i])
        }
        // Начиная с элемента k+1, поддерживать длину кучи равной k
        for (i in k..<nums.size) {
            // Если текущий элемент больше элемента на вершине кучи, извлечь вершину кучи и добавить текущий элемент в кучу
            if (nums[i] > heap.peek()) {
                heap.poll()
                heap.offer(nums[i])
            }
        }
        return heap
    }

top_k.rb

    ### Поиск k наибольших элементов массива с помощью кучи ###
    def top_k_heap(nums, k)
      # Инициализация минимальной кучи
      # Обратите внимание: мы инвертируем все элементы кучи, чтобы с помощью максимальной кучи имитировать минимальную
      max_heap = MaxHeap.new([])

      # Поместить первые k элементов массива в кучу
      for i in 0...k
        push_min_heap(max_heap, nums[i])
      end

      # Начиная с элемента k+1, поддерживать длину кучи равной k
      for i in k...nums.length
        # Если текущий элемент больше элемента на вершине кучи, извлечь вершину кучи и добавить текущий элемент в кучу
        if nums[i] > peek_min_heap(max_heap)
          pop_min_heap(max_heap)
          push_min_heap(max_heap, nums[i])
        end
      end

      get_min_heap(max_heap)
    end

Визуализация кода

Во весь экран >

Всего выполняется \(n\) операций добавления и извлечения из кучи, а максимальная длина кучи равна \(k\) , поэтому временная сложность равна \(O(n \log k)\) . Этот метод очень эффективен: когда \(k\) мало, временная сложность стремится к \(O(n)\). Когда \(k\) велико, она все равно не превышает \(O(n \log n)\) .

Кроме того, этот метод подходит и для сценариев с динамическим потоком данных. При непрерывном поступлении новых данных мы можем продолжать поддерживать содержимое кучи, тем самым динамически обновляя наибольшие \(k\) элементов.

##### Оставляйте свои идеи, вопросы и предложения в комментариях