优先队列是一种使用比较普遍的数据结构。不一样于通常的队列,优先队列的元素都具备优先级,优先级高的元素会被优先选取。利用这个特色,咱们能够根据元素值的大小来设置优先级,值最大/最小的拥有最高的优先级。这样,咱们就能够快速地获取队列中最大/最小的元素。这篇文章我将着重比较三种常见的,构造优先队列的数据结构 - Binary Heap(二叉堆), Leftist Heap(左倾堆)和Skew Heap(斜堆)。html
这篇文章的完成借鉴了不少网上的资料,其中最主要的是这几篇:
二叉堆(一)之 图文解析 和 C语言的实现
Priority Queues (Heaps)
数据结构与算法(五)node
我这里直接给出维基百科中关于Binary Heap的解释:算法
二叉堆是一种特殊的堆,二叉堆是彻底二叉树或者是近似彻底二叉树。二叉堆知足堆特性:父节点的键值老是保持固定的序关系于任何一个子节点的键值,且每一个节点的左子树和右子树都是一个二叉堆。数组
当父节点的键值老是大于或等于任何一个子节点的键值时为最大堆。当父节点的键值老是小于或等于任何一个子节点的键值时为最小堆。数据结构
我这里补充一下彻底二叉树的概念:彻底二叉树是指除了树的最下层外,全部层的节点都达到最大,而且最下层不满的节点都位于左支。函数
在构造Binary Heap时,咱们通常都使用数组而不是链表(网上也不多有用链表实现Binary Heap的资料),我这里也用数组来构造Binary Heap。Binary Heap分为最大堆和最小堆,在本文我只介绍最小堆,最大堆和最小堆的实现基本同样。spa
咱们先来看个最小堆的例子:code
咱们能够看到,全部节点的值都小于等于其子节点的值。这里须要注意,构造Binary Heap时,咱们能够用两种形式的数列1)使用index = 0的元素;2)不使用index = 0的元素htm
上面的例子使用的是第二种形式,下面全部关于最小堆的代码是基于第一种形式。这两种形式有一个很小的区别,在1)中:blog
index = x节点的左子节点的index = 2 * x + 1
index = x节点的右子节点的index = 2 * x + 2
index = x节点的父节点的index = floor((x - 1) / 2)
在2)中:
index = x节点的左子节点的index = 2 * x
index = x节点的右子节点的index = 2 * x + 1
index = x节点的父节点的index = floor(x / 2)
Min Heap通常支持插入,删除,建立和查找函数。咱们这里详细讲解下插入(建立)和删除。
插入能够分为两步:
第一步,在数列的末尾添加须要插入的值。
第二步,比较该节点与其父节点的大小,若是比其父节点大,插入结束;若是比其父节点小,交换这两个节点并重复步骤2直到插入结束或者该节点成为根节点。
咱们经过下面这个示意图来看看具体是怎样将14插入到最小堆的的:
了解了如何插入后,咱们分析下插入操做的时间复杂度:
在最好的状况下,插入节点的值大于其父节点,咱们不须要对堆进行调整,插入完成,时间复杂度为O(1)。
在最坏的状况下,插入的节点值比根节点还小,那么咱们须要将该节点一直交换到根节点,所以时间复杂度是O(h),其中h是最小堆的高度。根据彻底二叉树的性质,有N个节点的彻底二叉树的高度为log(N + 1),所以O(h) = O(log(N + 1)) = O(logN)。关于彻底二叉树高度的证实请参考这篇博文:二叉查找树(一)之 图文解析 和 C语言的实现
综上,最小堆的插入算法平均时间复杂度是O(logN)。
下面是插入操做的代码:
/**************************************************************************************** * Insert Operation ***************************************************************************************/ void min_heap_up_update(int key) { int p_node_index, new_node_index; /* set inserted node's init index */ new_node_index = heap_size; /* get inserted node's father node's index and key */ p_node_index = (new_node_index - 1 ) / 2; while (new_node_index > 0) { if (min_heap[p_node_index]<= key) { break; } else { /* please note we do not swap key between father node and child * node, we only assign father node's key to its child node's key */ min_heap[new_node_index] = min_heap[p_node_index]; new_node_index = p_node_index; p_node_index = (p_node_index - 1) / 2; } } /* at his point, we assign key to the inserted node */ min_heap[new_node_index] = key; } void min_heap_insert(int key) { if (heap_size == MAX_SIZE) { printf("Min Heap is full...\n"); return; } min_heap[heap_size] = key; min_heap_up_update(key); heap_size++; }
在代码的实现上,咱们并无不断的交换符合条件的父节点和子节点,咱们只是在最后肯定了新节点的位置后,咱们才将这个节点的key设置为咱们须要的key。在最小堆的代码中,咱们用 heap_size
这个全局变量表示当前堆的大小,用 min_heap[]
这个全局数组表示最小堆。
这里的删除指的是删除最小值,也就是删除根节点。删除的操做和插入的操做相似,只是插入是经过向上更新最小堆,而删除是经过向下更新最小堆。删除操做能够分为两步:
第一步,用最小堆的最后一个节点去取代根节点。
第二步,用更新后的第一个节点与其较小的子节点比较,若是该节点比其较小的子节点小,删除操做结束;不然交换这两个节点并重复步骤2直到删除操做结束。
删除操做的时间复杂度和插入同样:
在最好的状况下,删除的时间复杂度为O(1) - 好比整个最小堆的节点都有相同的key,咱们只须要比较一次。
在最坏的状况下,咱们须要将根节点交换到堆的最下一层,所以时间复杂度是O(logN)。
综上,最小堆的删除算法平均时间复杂度是O(logN)。
下面是删除操做的代码:
/**************************************************************************************** * Delete Operation ***************************************************************************************/ void min_heap_down_update(int position) { int c_node_index, cur_node_index, cur_node_val; cur_node_index = position; cur_node_val = min_heap[cur_node_index]; c_node_index = 2 * cur_node_index + 1; while (c_node_index < heap_size) { /* if node has two children we choose the one with smaller key */ if ((c_node_index < heap_size - 1) && (min_heap[c_node_index] > min_heap[c_node_index + 1])) c_node_index = c_node_index + 1; if (cur_node_val <= min_heap[c_node_index]) { break; } else { min_heap[cur_node_index] = min_heap[c_node_index]; cur_node_index = c_node_index; c_node_index = 2 * c_node_index + 1; } } min_heap[cur_node_index] = cur_node_val; } void min_heap_remove() { if (heap_size == 0) { printf("Min Heap is empty...\n"); return; } min_heap[0] = min_heap[heap_size - 1]; min_heap_down_update(0); heap_size--; }
同插入操做相似,在代码中咱们并无不断的交换父子节点的值,只是在删除结束后,咱们才更新节点的值。
咱们能够简单的经过不断的插入节点来完成最小堆的构造,根据插入操做的复杂度,要构造一个N个节点的最小堆须要的时间复杂度是O(N*log(N))。有没有更快速的方法来构造最小堆呢?方法是有的,咱们来看看如何使用O(N)的时间来构造一个包含N个节点的最小堆。
插入的方法是自下而上的构造最小堆,咱们这里的方法是自上而下的构造最小堆。要知足最小堆成立,咱们须要保证全部的节点往下都构成最小堆。所以,咱们能够将须要添加到最小堆的数按任意顺序放入最小堆的数组(此时不是最小堆),而后经过不断的调整来使其成为最小堆。这么作有一个好处,咱们只须要调整前N/2的节点。为何呢?由于堆中的后N/2的节点是叶节点,它们已是最小堆了,所以咱们只须要调整前N/2的节点便可将该堆调整成最小堆。
咱们来分析下时间复杂度,我这里直接引用数据结构与算法(五)中的内容:
根据计算,这么作能够达到O(N)的时间复杂度。
下面是最小堆建造的代码:
for (int i = heap_size / 2; i >=0; i--) min_heap_down_update(i);
min_heap_down_update()
是在删除操做中实现的。