Структуры данных и алгоритмы Серия вопросов для собеседования - Двоичная куча

алгоритм опрос

Эта серия представляет собой краткое изложение структур данных и алгоритмов, когда я искал работу много лет назад. Здесь есть базовые части и классические вопросы для интервью от крупных компаний. Впервые опубликовано на CSDN. Теперь он организован в серию для справки друзей, которым это нужно.Если есть какая-либо ошибка, пожалуйста, поправьте меня. Полный кодовый адрес этой серии находится по адресуздесь.

0 Обзор

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

1 Определение двоичной кучи

Используйте массивы для реализации двоичных куч, двоичные кучи имеют два свойства, гдеLENGTH(A)представляет массивAдлина иHEAP_SIZE(A)представляет количество элементов кучи, хранящихся в A, гдеLENGTH(A) <= HEAP_SIZE(A), то есть хотяA[0,1,...N-1]может содержать допустимые значения, ноA[HEAP_SIZE(A)-1]Элементы после этого не принадлежат соответствующей куче.

Корень дерева, соответствующий двоичной куче, равенA[0], учитывая индекс i узла, можно легко вычислить его родительский узел и дочерний узел.Обратите внимание, что в следующих примерах диаграмм мы помечаем элементы так, чтобы они начинали отсчет с 1, а код реализации начинал отсчет с 0.

#define PARENT(i) ( i > 0 ? (i-1)/2 : 0)
#define LEFT(i) (2 * i + 1)
#define RIGHT(i) (2 * i + 2)

Примечание. Каждый слой дерева, соответствующий куче, заполнен, поэтому высотаhКоличество элементов в куче не более1+2+2^2+...2^h = 2^(h+1) - 1(полное бинарное дерево), минимальное количество элементов равно1+2+...+2^(h-1) + 1 = 2^h. из-за количества элементов2^h <= n <= 2^(h+1) -1,такh <= lgn < h+1,следовательноh = lgn. То есть высота двоичной кучи, содержащей n элементов, равнаlgn.

2 Сохраняйте свойства кучи

В этой статье в основном создается максимальная куча, и принцип минимальной кучи аналогичен. Чтобы сохранить свойства кучи,maxHeapify(int A[], int i)функция создания массива кучиAотбросить максимальную кучу так, чтобыiКорневое поддерево становится максимальной кучей.

void maxHeapify(int A[], int i, int heapSize)
{
    int l = LEFT(i);
    int r = RIGHT(i);

    int largest = i;

    if (l <= heapSize-1 && A[l] > A[i]) {
        largest = l;
    }

    if (r <= heapSize-1 && A[r] > A[largest]) {
        largest = r;
    }

    if (largest != i) { // 最大值不是i,则需要交换i和largest的元素,并递归调用maxHeapify。
        swapInt(A, i, largest);
        maxHeapify(A, largest, heapSize);
    }
}
  • На каждом шаге алгоритма из элементаA[i]а такжеA[left]так же какA[right]Выберите среди них наибольший и сохраните его в нижнем индексеlargestсередина. еслиA[i]максимум, тоiКорневое поддерево уже является максимальной кучей, и программа завершается.

  • в противном случае,iДочерний узел имеет самый большой элемент,A[i]а такжеA[largest]swap, чтобы i и его дочерние элементы удовлетворяли свойству max-heap. Кроме того, индексlargestЗначение узла после свопа становитсяA[i], поддерево с корнем в этом узле может нарушать свойство максимальной кучи, поэтому необходимо рекурсивно вызывать поддеревоmaxHeapify()функция.

когдаmaxHeapify()функция работает наiявляется корневым узлом, размерnКогда на поддереве , время выполнения корректируется дляA[i],A[left],A[right]времяO(1), плюс кiрекурсивно вызывать поддерево с корнем в дочернем узлеmaxHeapifyвремя.iРазмер поддерева, узлом которого является корень, не более2n/3(когда нижний слой заполнен только наполовину), поэтому его можно толкнутьT(N) <= T(2N/3) + O(1),такT(N)=O(lgN).

На следующем рисунке показан бегmaxHeapify(heap, 2)пример.A[] = {16, 4, 10, 14, 7, 9, 3, 2, 8, 1}, размер кучи10.

保持最大堆性质

3 Создайте максимальную кучу

Мы можем знать, что массивA[0, 1, ..., N-1]середина,A[N/2, ..., N-1]Элементами являются все листовые узлы дерева. как на фото выше6-10Все узлы являются листовыми узлами. Каждый листовой узел можно рассматривать как максимальную кучу только с одним элементом, поэтому нам нужно вызывать только другие узлы.maxHeapify()функция.

void buildMaxHeap(int A[], int n)
{
    int i;
    for (i = n/2-1; i >= 0; i--) {
        maxHeapify(A, i, n);
    }
}

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

Инвариант цикла: перед запуском цикла for узелi+1、i+2...N-1являются корнями максимальной кучи.

Инициализация: прежде чем цикл for начнет повторяться,i = N/2-1, УзелN/2, N/2+1, ..., N-1Они оба являются листовыми узлами и корнем максимальной кучи.

держать: потому что узелiМетки дочерних узлов больше, чемiБольшие, согласно определению инварианта цикла, эти дочерние узлы являются корнем максимальной кучи, поэтому вызовитеmaxHeapify()назад,iстановится корнем максимальной кучи, иi+1, i+2, ..., N-1По-прежнему сохраняйте максимальные свойства кучи.

Завершение: когда процесс завершается, i=0, поэтому узел0, 1, 2, ..., N-1Оба являются корнем максимальной кучи, в частности, узел 0 является корнем максимальной кучи.

建立最大堆

Хотя каждый звонокmaxHeapify()время - этоO(lgN), в целомO(N)звонки, но говорят, что время выполненияO(NlgN)является неточным, если быть точным, время работы равноO(N), это не будет доказано здесь. Для конкретного процесса доказательства, пожалуйста, обратитесь к «Введение в алгоритмы».

4 куча сортировки

начатьbuildMaxHeap()Функция создает максимальную кучу, потому что самый большой элемент массива находится вA[0], непосредственно комбинируя его сA[N-1]Поменяйте местами, чтобы достичь конечной правильной позиции. УдалитьA[N-1], размер кучиheapSizeВычесть 1, позвонитьmaxHeapify(heap, 0, --heapSize)Сохраняйте свойство max-heap, пока размер кучи не уменьшится с N до 1.

void heapSort(int A[], int n)
{
    buildMaxHeap(A, n);
    int heapSize = n;
    int i;
    for (i = n-1; i >= 1; i--) {
        swapInt(A, 0, i);
        maxHeapify(A, 0, --heapSize);
    }
}

5 Приоритетная очередь

Наконец, реализована очередь с максимальным приоритетом.Есть четыре основные операции, а именно:

  • insert(PQ, key): Вставить ключ в очередь.
  • maximum(PQ): возвращает элемент с самым большим ключом в очереди.
  • extractMax(PQ): удалить и вернуть элемент с самым большим ключом в очереди
  • increaseKey(PQ, i, key): увеличить значение ключа в очереди i до ключа

определить структуру здесьPriorityQueueПростота в эксплуатации.

typedef struct PriorityQueue {
    int capacity;
    int size;
    int elems[];
} PQ;

Код реализации операции конечной приоритетной очереди выглядит следующим образом:

/**
 * 从数组创建优先级队列
 */
PQ *newPQ(int A[], int n)
{
    PQ *pq = (PQ *)malloc(sizeof(PQ) + sizeof(int) * n);
    pq->size = 0;
    pq->capacity = n;

    int i;
    for (i = 0; i < pq->capacity; i++) {
        pq->elems[i] = A[i];
        pq->size++;
    }
    buildMaxHeap(pq->elems, pq->size);

    return pq;
}

int maximum(PQ *pq)
{
    return pq->elems[0];
}

int extractMax(PQ *pq)
{
    int max = pq->elems[0];
    pq->elems[0] = pq->elems[--pq->size];
    maxHeapify(pq->elems, 0, pq->size);
    return max;
}

PQ *insert(PQ *pq, int key)
{
    int newSize = ++pq->size;
    if (newSize > pq->capacity) {
        pq->capacity = newSize * 2;
        pq = (PQ *)realloc(pq, sizeof(PQ) + sizeof(int) * pq->capacity);
    }
    pq->elems[newSize-1] = INT_MIN;
    increaseKey(pq, newSize-1, key);
    return pq;
}

void increaseKey(PQ *pq, int i, int key)
{
    int *elems = pq->elems;
    elems[i] = key;

    while (i > 0 && elems[PARENT(i)] < elems[i]) {
        swapInt(elems, PARENT(i), i);
        i = PARENT(i);
    }
}

использованная литература

  • Введение в алгоритмы Глава 6 Сортировка кучей