【程序人生】数据结构杂记(八)
说在前面
个人读书笔记
排序算法的稳定性Stable
稳定排序:对于相等的元素,在排序后,原来靠前的元素依然靠前。相等元素的相对位置没有发生改变。
可以实现稳定排序:插入排序、归并排序
无法实现稳定排序:快速排序、堆排序
优先级队列
按照事先约定的优先级,可以始终高效查找并访问优先级最高数据项的数据结构,统称作优先级队列(priority queue)。
优先级队列,这类结构将操作对象限定于当前的全局极值者。比如,在全体北京市民中,查找年龄最长者;或者在所有鸟类中,查找种群规模最小者,等等。这种根据数据对象之间相对优先级对其进行访问的方式,与此前的访问方式有着本质区别,称作循优先级访问(call-by-priority)。
“全局极值”本身就隐含了“所有元素可相互比较”这一性质。然而,优先级队列并不会也不必忠实地动态维护这个全序,却转而维护一个偏序(partial order)关系。
优先级队列中的数据项称作词条(entry);而与特定优先级相对应的数据属性,也称作关键码(key)。不同应用中的关键码,特点不尽相同:
有时限定词条的关键码须互异,有时则允许词条的关键码雷同;有些词条的关键码一成不变,有些则可动态修改;有的关键码只是一个数字、一个字符或一个字符串,而复杂的关键码则可能由多个基本类型组合而成;多数关键码都取作词条内部的某一成员变量,而有的关键码则并非词条的天然属性。
无论具体形式如何,作为确定词条优先级的依据,关键码之间必须可以比较大小——注意,这与词典结构完全不同,后者仅要求关键码支持判等操作。因此对于优先级队列,必须以比较器的形式兑现对应的优先级关系。
堆
堆多用于动态数据的维护。
基于列表或向量等结构的实现方式,之所以无法同时保证插入操作和删除优先级最高成员的操作的高效率,原因在于其对优先级的理解过于机械,以致始终都保存了全体词条之间的全序关系。实际上,尽管优先级队列的确隐含了“所有词条可相互比较”这一条件,但从操作接口层面来看,并不需要真正地维护全序关系。比如执行删除优先级最高成员的操作时,只要能够确定全局优先级最高的词条即可;至于次高者、第三高者等其余词条,目前暂时不必关心。
有限偏序集的极值必然存在,故此时借助堆(heap)结构维护一个偏序关系即足矣。堆有多种实现形式,最基本的一种形式是完全二叉堆(complete binary heap)。
完全二叉堆
完全二叉堆应满足两个条件:
- 首先,其逻辑结构须等同于完全二叉树,此即所谓的“结构性”。如此,堆节点将与词条一一对应。
- 其次,就优先级而言,堆顶以外的每个节点都不高(大)于其父节点,此即所谓的“堆序性”。
大顶堆与小顶堆
由堆序性不难看出,堆中优先级最高的词条必然始终处于堆顶位置。因此,堆结构的得到优先级最大成员的操作总是可以在时间内完成。
堆序性也可对称地约定为“堆顶以外的每个节点都不低(小)于其父节点”,此时同理,优先级最低的词条,必然始终处于堆顶位置。为以示区别,通常称前(后)者为大(小)顶堆。(优先级最高的是堆顶——大堆顶;优先级最低的是堆顶——小堆顶)
小顶堆和大顶堆是相对的,而且可以相互转换。
高度
结构等同于完全二叉树的堆,必然不致太高。n个词条组成的堆的高度。
基于向量的紧凑表示
尽管二叉树不属于线性结构,但作为其特例的完全二叉树,却与向量有着紧密的对应关系。
由上图可见,完全二叉堆的拓扑联接结构,完全由其规模n确定。按照层次遍历的次序,每个节点都对应于唯一的编号;反之亦然。故若将所有节点组织为一个向量,则堆中各节点(编号)与向量各单元(秩)也将彼此一一对应。
这一实现方式的优势首先体现在,各节点在物理上连续排列,故总共仅需O(n)空间。而更重要地是,利用各节点的编号(或秩),也可便捷地判别父子关系。
具体地,若将节点的编号(秩)记作,则根节点及其后代节点的编号分别为:
…
更一般地,不难验证,完全二叉堆中的任意节点,必然满足:
向上取整,用数学符号 ⌈⌉表示。
向下取整,用数学符号 ⌊⌋表示。
完全二叉堆的模板类
为简化后续算法的描述及实现,可如下图所示预先设置一系列的宏定义。
借助多重继承的机制,定义完全二叉堆模板类如下:
既然全局优先级最高的词条总是位于堆顶,故只需返回向量的首单元,即可在时间内完成getMax()操作。
c++实现
#include<iostream>
#include<algorithm>
#include<cassert>
using namespace std;
template<typename Item>
class MaxHeap{
private:
Item* data;
int count;
int capacity;
// 上滤操作
void shiftUp(int k)
{
while(k>1 && data[k/2] < data[k])
{
swap(data[k/2], data[k]);
k /= 2;
}
}
// 下滤操作
void shiftDown(int k)
{
while(2*k <= count)
{
int j = 2*k;
if (j + 1 <= count && data[j+1] > data[j])
{
j += 1;
}
if (data[k] >= data[j])
{
break;
}
swap(data[k], data[j]);
k = j;
}
}
public:
// 声明堆的最大容量
MaxHeap(int capacity)
{
data = new Item[capacity + 1];
count = 0;
this->capacity = capacity;
}
~MaxHeap()
{
delete [] data;
}
// 返回堆的元素个数
int size()
{
return count;
}
// 返回堆是否为空
bool isEmpty()
{
return count == 0;
}
// 插入元素
void insert(Item item)
{
assert(count + 1 <= capacity);
data[count+1] = item;
count++;
shiftUp(count);
}
// 删除元素
Item extractMax()
{
assert(count > 0);
Item ret = data[1];
swap(data[1], data[count]);
count --;
shiftDown(1);
return ret;
}
};
int main()
{
MaxHeap<int> maxheap = MaxHeap<int>(100);
srand(time(NULL));
for (int i = 0; i < 15; i++)
{
maxheap.insert(rand()%100);
}
while( !maxheap.isEmpty())
{
cout << maxheap.extractMax() << " ";
}
cout<<endl;
return 0;
}
python实现
# -*- coding:utf-8 -*-
class MaxHeap:
def __init__(self, data):
"""
输入一个列表, 建堆
:param data: 列表
"""
self.__data = data
self.__count = len(data)
self.__floyd()
# 交换self.__data中的两个索引对应的值
def __swap(self, a, b):
c = self.__data[a]
self.__data[a] = self.__data[b]
self.__data[b] = c
# 对索引为k(第k+1个值)的值, 上滤操作
def __shift_up(self, k):
while (k > 0) and (self.__data[(k-1)//2] < self.__data[k]):
self.__swap((k-1)//2, k)
k = (k-1)//2
# 对索引为k(第k+1个值)的值, 下滤操作
def __shift_down(self, k):
while 2*k + 1 < self.__count:
j = 2*k + 1
if (2*k + 2 < self.__count) and (self.__data[j] < self.__data[2*k + 2]):
j = 2*k + 2
if self.__data[k] > self.__data[j]:
break
self.__swap(k, j)
k = j
# Floyd算法建堆
def __floyd(self):
k = list(range((self.__count - 2) // 2 + 1))[::-1]
for each in k:
self.__shift_down(each)
# 打印堆内容
def print_heap(self):
for each in self.__data:
print(each, end=" ")
# 堆插入操作
def insert(self, num):
self.__data.append(num)
self.__count = self.__count + 1
self.__shift_up(self.__count - 1)
# 删除堆顶操作
def extract_max(self):
assert self.__count > 0, "The Max Heap is Empty"
num = self.__data[0]
self.__swap(0, self.__count - 1)
self.__data = self.__data[:-1]
self.__count = self.__count - 1
self.__shift_down(0)
return num
max_heap = MaxHeap([23, 5, 3, 345, 2, 544, 34, 56, 3, 5, 5, 234, 56, 24, 75, 3, 56])
# 测试打印
max_heap.print_heap()
print("\n")
# 测试删除堆顶
print(max_heap.extract_max())
# 测试打印
max_heap.print_heap()
print("\n")
# 测试删除堆顶
print(max_heap.extract_max())
# 测试打印
max_heap.print_heap()
print("\n")
# 测试插入
max_heap.insert(12)
# 测试打印
max_heap.print_heap()
print("\n")
# 测试股层噶任意
max_heap.insert(123)
# 测试打印
max_heap.print_heap()
print("\n")
# 测试删除堆顶
while True:
print(max_heap.extract_max())
元素插入
插入算法分为两个步骤:
首先,调用向量的标准插入接口,将新词条接至向量的末尾。
尽管此时上图所示,新词条的引入并未破坏堆的结构性,但只要新词条不是堆顶,就有可能与其父亲违反堆序性。当然,其它位置的堆序性依然满足。故以下将调用percolateUp()函数,对新接入的词条做适当调整,在保持结构性的前提下恢复整体的堆序性。
上滤
不妨假定原堆非空,于是新词条的父亲(深色节点)必然存在。根据在向量中对应的秩,可以简便地确定词条对应的秩,即。
此时,若经比较判定,则堆序性在此局部以至全堆均已满足,插入操作因此即告完成。
反之,若,则可在向量中令和互换位置。如上图所示,如此不仅全堆的结构性依然满足,而且和之间的堆序性也得以恢复。
当然,此后与其新的父亲,可能再次违背堆序性。若果真如此,不妨继续套用以上方法,如上图所示令二者交换位置。当然,只要有必要,此后可以不断重复这种交换操作。
每交换一次,新词条都向上攀升一层,故这一过程也形象地称作上滤(percolate up)。
当然,至多上滤至堆顶。一旦上滤完成,则如上图所示,全堆的堆序性必将恢复。
由上可见,上滤调整过程中交换操作的累计次数,不致超过全堆的高度。而在向量中,每次交换操作只需常数时间,故上滤调整乃至整个词条插入算法整体的时间复杂度,均为。
示例
实现
元素删除
删除算法也分为两个步骤:
首先,既然待删除词条总是位于堆顶,故可直接将其取出并备份。此时如上图所示,堆的结构性将被破坏。为修复这一缺陷,可如图所示,将最末尾的词条转移至堆顶。
新的堆顶可能与其孩子(们)违背堆序性——尽管其它位置的堆序性依然满足。故调用percolateDown()函数调整新堆顶,在保持结构性的前提下,恢复整体的堆序性。
下滤
若新堆顶不满足堆序性,则可如上图所示,将与其(至多)两个孩子中的大者(图中深色节点)交换位置。与上滤一样,由于使用了向量来实现堆,根据词条的秩可便捷地确定其孩子的秩。此后,堆中可能的缺陷依然只能来自于词条——它与新孩子可能再次违背堆序性。
若果真如此,不妨继续套用以上方法,将与新孩子中的大者交换,结果如图所示。实际上,只要有必要,此后可如图和不断重复这种交换操作。
因每经过一次交换,词条都会下降一层,故这一调整过程也称作下滤(percolate down)。
与上滤同理,这一过程也必然终止。届时如图所示,全堆的堆序性必将恢复;而且,下滤乃至整个删除算法的时间复杂度也为。
示例
实现
建堆
很多算法中输入词条都是成批给出,故在初始化阶段往往需要解决一个共同问题:
给定一组词条,高效地将它们组织成一个堆。这一过程也称作“建堆”(heapification)。
蛮力法
从空堆起反复调用标准insert()接口,即可将输入词条逐一插入其中,并最终完成建堆任务,但其消耗的时间却过多。
时间复杂度:
自上而下的上滤
对任何一棵完全二叉树,只需自顶而下、自左向右地针对其中每个节点实施一次上滤,即可使之成为完全二叉堆。
时间复杂度:
Floyd算法
为得到更快的建堆算法,先考查一个相对简单的问题:
任给堆和,以及另一独立节点,如何高效地将转化为堆?从效果来看,这相当于以为中介将堆和合二为一,故称作堆合并操作。
如上图,首先为满足结构性,可将这两个堆当作的左、右子树,联接成一棵完整的二叉树。此时若与孩子和满足堆序性,则该二叉树已经就是一个不折不扣的堆。
实际上,此时的场景完全等效于,在delMax()操作中摘除堆顶,再将末位词条转移至堆顶。故只需对p实施下滤操作,即可将全树转换为堆。
如果将以上过程作为实现堆合并的一个通用算法,则在将所有词条组织为一棵完全二叉树后,只需自底而上地反复套用这一算法,即可不断地将处于下层的堆逐对地合并成更高一层的堆,并最终得到一个完整的堆。按照这一构思,即可实现Floyd建堆算法。
可见,该算法的实现十分简洁:
只需自下而上、由深而浅地遍历所有内部节点,并对每个内部节点分别调用一次下滤算法percolateDown()
示例如下:
时间复杂度:
蛮力算法与Floyd算法恰好相反——若将前者理解为“自上而下的上滤”,则后者即是“自下而上的下滤”
c++实现
#include<iostream>
#include<algorithm>
#include<cassert>
using namespace std;
template<typename Item>
class MaxHeap{
private:
Item* data;
int count;
int capacity;
// 上滤操作
void shiftUp(int k)
{
while(k>1 && data[k/2] < data[k])
{
swap(data[k/2], data[k]);
k /= 2;
}
}
// 下滤操作
void shiftDown(int k)
{
while(2*k <= count)
{
int j = 2*k;
if (j + 1 <= count && data[j+1] > data[j])
{
j += 1;
}
if (data[k] >= data[j])
{
break;
}
swap(data[k], data[j]);
k = j;
}
}
public:
// 声明堆的最大容量
MaxHeap(int capacity)
{
data = new Item[capacity + 1];
count = 0;
this->capacity = capacity;
}
// Floyd算法建堆
MaxHeap(Item arr[], int n)
{
data = new Item[n+1];
capacity = n;
for (int i = 0; i < n; i++)
{
data[i+1] = arr[i];
}
count = n;
for (int i = count/2; i >= 1; i--)
{
shiftDown(i);
}
}
~MaxHeap()
{
delete [] data;
}
// 返回堆的元素个数
int size()
{
return count;
}
// 返回堆是否为空
bool isEmpty()
{
return count == 0;
}
// 插入元素
void insert(Item item)
{
assert(count + 1 <= capacity);
data[count+1] = item;
count++;
shiftUp(count);
}
// 删除元素
Item extractMax()
{
assert(count > 0);
Item ret = data[1];
swap(data[1], data[count]);
count --;
shiftDown(1);
return ret;
}
};
int main()
{
int a [15];
srand(time(NULL));
for (int i = 0; i < 15; i++)
{
a[i] = rand()%100;
}
MaxHeap<int> maxheap = MaxHeap<int>(a, 15);
while( !maxheap.isEmpty())
{
cout << maxheap.extractMax() << " ";
}
cout<<endl;
return 0;
}
python实现
# -*- coding:utf-8 -*-
class MaxHeap:
def __init__(self, data):
"""
输入一个列表, 建堆
:param data: 列表
"""
self.__data = data
self.__count = len(data)
self.__floyd()
# 交换self.__data中的两个索引对应的值
def __swap(self, a, b):
c = self.__data[a]
self.__data[a] = self.__data[b]
self.__data[b] = c
# 对索引为k(第k+1个值)的值, 上滤操作
def __shift_up(self, k):
while (k > 0) and (self.__data[(k-1)//2] < self.__data[k]):
self.__swap((k-1)//2, k)
k = (k-1)//2
# 对索引为k(第k+1个值)的值, 下滤操作
def __shift_down(self, k):
while 2*k + 1 < self.__count:
j = 2*k + 1
if (2*k + 2 < self.__count) and (self.__data[j] < self.__data[2*k + 2]):
j = 2*k + 2
if self.__data[k] > self.__data[j]:
break
self.__swap(k, j)
k = j
# Floyd算法建堆
def __floyd(self):
k = list(range((self.__count - 2) // 2 + 1))[::-1]
for each in k:
self.__shift_down(each)
# 打印堆内容
def print_heap(self):
for each in self.__data:
print(each, end=" ")
# 堆插入操作
def insert(self, num):
self.__data.append(num)
self.__count = self.__count + 1
self.__shift_up(self.__count - 1)
# 删除堆顶操作
def extract_max(self):
assert self.__count > 0, "The Max Heap is Empty"
num = self.__data[0]
self.__swap(0, self.__count - 1)
self.__data = self.__data[:-1]
self.__count = self.__count - 1
self.__shift_down(0)
return num
max_heap = MaxHeap([23, 5, 3, 345, 2, 544, 34, 56, 3, 5, 5, 234, 56, 24, 75, 3, 56])
# 测试打印
max_heap.print_heap()
print("\n")
# 测试删除堆顶
print(max_heap.extract_max())
# 测试打印
max_heap.print_heap()
print("\n")
# 测试删除堆顶
print(max_heap.extract_max())
# 测试打印
max_heap.print_heap()
print("\n")
# 测试插入
max_heap.insert(12)
# 测试打印
max_heap.print_heap()
print("\n")
# 测试股层噶任意
max_heap.insert(123)
# 测试打印
max_heap.print_heap()
print("\n")
# 测试删除堆顶
while True:
print(max_heap.extract_max())
就地堆排序
对于向量中的n个词条,如何借助堆的相关算法,实现高效的排序?这类算法也称作堆排序(heapsort)算法。
算法的总体思路和策略与选择排序算法基本相同:
将所有词条分成未排序和已排序两类,不断从前一类中取出最大者,顺序加至后一类中。算法启动之初,所有词条均属于前一类;此后,后一类不断增长;当所有词条都已转入后一类时,即完成排序。
这里的待排序词条既然已组织为向量,不妨将其划分为前缀和与之互补的后缀,分别对应于上述未排序和已排序部分。与常规选择排序算法一样,在算法启动之初覆盖所有词条,而为空。新算法的不同之处在于,整个排序过程中,无论包含多少词条,始终都组织为一个堆。
另外,整个算法过程始终满足如下不变性:
中的最大词条不会大于中的最小词条——除非二者之一为空,比如算法的初始和终止时刻。
首先如上图,取出首单元词条,将其与末单元词条交换。既是当前堆中的最大者,同时根据不变性也不大于中的任何词条,故如此交换之后必处于正确的排序位置。故如图,此时可等效地认为向前扩大了一个单元,相应地缩小了一个单元。然后需对实施一次下滤调整,即可使整体的堆序性重新恢复。
示例
- Floyd算法建堆
- 就地堆排序
实现
c++实现
#include<iostream>
#include<algorithm>
#include<cassert>
using namespace std;
template<typename T>
void __shiftDown(T arr[], int n, int k)
{
while(2*k + 1 < n)
{
int j = 2*k + 1;
if (j+1 < n && arr[j+1] > arr[j])
{
j += 1;
}
if (arr[k] >= arr[j])
{
break;
}
swap(arr[k], arr[j]);
k = j;
}
}
template<typename T>
void heapSort(T arr[], int n)
{
for(int i = (n-2)/2; i>=0; i--)
{
__shiftDown(arr, n, i);
}
for (int i = n -1; i>0; i--)
{
swap(arr[0], arr[i]);
__shiftDown(arr, i, 0);
}
}
int main()
{
int a [15];
srand(time(NULL));
for (int i = 0; i < 15; i++)
{
a[i] = rand()%100;
}
heapSort(a, 15);
for (int i = 0; i<15; i++)
{
cout<<a[i]<<" ";
}
cout<<endl;
return 0;
}
python实现
# -*- coding:utf-8 -*-
test_data = [23, 5, 3, 345, 2, 544, 34, 56, 3, 5, 5, 234, 56, 24, 75, 3, 56]
def swap(data, i, j):
a = data[i]
data[i] = data[j]
data[j] = a
def shift_down(data, k, n):
while 2 * k + 1 < n:
j = 2 * k + 1
if (2 * k + 2 < n) and (data[j] < data[2 * k + 2]):
j = 2 * k + 2
if data[k] > data[j]:
break
swap(data, k, j)
k = j
def heap_sort(data):
# floyd建堆
k = list(range((len(data) - 2) // 2 + 1))[::-1]
for each in k:
shift_down(data, each, len(data))
for each in range(len(data)):
i = len(data) - 1 - each
swap(data, 0, i)
shift_down(data, 0, i)
heap_sort(test_data)
print(test_data)
结语
如果您有修改意见或问题,欢迎留言或者通过邮箱和我联系。
手打很辛苦,如果我的文章对您有帮助,转载请注明出处。