数据结构的内容:逻辑结构、存储结构、运算集合。
数据类型:一个值的集合和定义在此集合上的一组操作的总称。
抽象数据类型:abstract data type,用户经行软件设计时从问题的数学模型中抽象出来的逻辑数据结构和逻辑数据结构上的运算,而不考虑计算机具体存储结构和运算的具体实现算法。此模块包含定义、表示和实现。也就是数据对象的定义、数据关系的定义、基本操作的定义。
算法特征:有穷性、确定性、可行性、输入、输出
算法设计的要求:正确性、可读性、健壮性、高效率和低存储的要求。
时间复杂度,空间复杂度。给一个程序要会算,常考。
抽象数据类型(ADT): 带有一组操作的一些对象的集合;对象指数据;
集合ADT的操作:添加(add),删除(remove),包含(contain);
两种操作:并(union)、查找(find)
线性表 是最常用且最简单的一种数据结构
定义:是n个数据元素的有限序列
特点:同一性、有穷性、有序性
线性表主要由顺序表示或链式表示
实现线性表的两种方式:
前驱:Ai-1前驱 Ai
后继: Ai 后继Ai-1
优点:可以通过下标(index)来访问或者修改元素,比较高效。
缺点:插入和删除花费的开销比较大。
数组的插入与删除操作:
存在许多情形,表是通过在高端进行插入操作建成,其后只发生对数组的访问,此时数组是表的一种恰当表达方式
优点:添加或删除元素时,只需要改变相关节点的Next指向,效率很高。
单链表:如上图
循环单链表:链表的最后一个节点指向第一个节点,整体构成一个链环;
双向链表:节点中包含两个指针部分,一个指向前驱元,一个指向后继元;
循环双向链表:节点中包含两个指针部分,一个指向前驱元,一个指向后继元,最后一个节点指向第一个节点。
头节点:在前端的节点
尾节点:在末端的节点
栈与队列也是常见的数据结构,是特殊的线性表。
树类结构是非常重要的非线性数据结构。树与二叉树最常用。
树是由n(n>=1)个有限节点组成一个具有层次关系的集合。
特点
每个节点有零个或多个字节点
根结点: 没有父节点的节点
非根结点:每个非根节点有且只有一个父节点
子节点:每个子节点可以分为多个不相交的子树
定义:二叉树是每个节点最多有两棵子树的树结构。子树被称为“左子树”和“右子树”。常用于实现二叉查找树和二叉堆。
性质:
二叉树的应用中,常要求在树中查找具有某种特征的节点,或对树中全部节点进行某种处理,此时涉及二叉树的遍历。
3个基本单元:根节点,左子树,右子树
先序遍历:先访问根节点,再先序遍历左子树,最后遍历右子树。(根-左-右)
中序遍历:先中序遍历左子树,再访问根节点,最后中序遍历右子树。(左-根-右)
后序遍历:先后序遍历左子树,在后续遍历右子树,最后访问根节点。(左-右-根)
层序遍历:所有深度为d的节点要在深度d+1的节点之前执行;用到队列、属于广度优先。
二叉树每个节点最多有2个节点,树则无限制,二叉树的平均深度为O(sqrt(N));
二叉树中子树分为左子树和右子树,二叉树是有序的(即使某节点只有一棵子树也要指明是左/右);
树决不能为空,至少有一个节点,但二叉树可以为空。
定义
性质:
二叉查找树的平均深度为O(log(N))
当给定值相同但顺序不同时,所构建的二叉查找树形态是不同的
不同形态平衡二叉树的ASL不同
二叉查找树删除节点分析
首先需要定位包含该元素的节点,以及它的父节点。
若无子节点,即叶子节点可以直接删除;
若有子节点,则考虑两种情况:
平衡二叉树又称AVL树,它或者是一棵空树,或者是具有下列性质的二叉树:它的左子树和右子树都是平衡二叉树,且左子树和右子树的深度之差的绝对值不超过1;
AVL树是最先发明的自平衡二叉查找树算法。在AVL中任何节点的两个儿子子树的高度最大差别为1,所以它也被称为高度平衡树
特点:
查找、插入和删除在平均和最坏情况下都是O(log n);
增加和删除可能需要通过一次或多次树旋转来重新平衡这个树。
红黑树是平衡二叉树的一种,它保证在最坏情况下基本动态集合操作的事件复杂度为O(log n)
红黑树和平衡二叉树区别如下:
(1) 红黑树放弃了追求完全平衡,追求大致平衡,在与平衡二叉树的时间复杂度相差不大的情况下,保证每次插入最多只需要三次旋转就能达到平衡,实现起来也更为简单;
(2) 平衡二叉树追求绝对平衡,条件比较苛刻,实现起来比较麻烦,每次插入新节点之后需要旋转的次数不能预知。
图是一种较线性表和树更为复杂的数据结构。
在线性表中,元素之间仅有线性关系;
在树形结构中,数据之间有明显的层次关系;
在图形结构中,节点之间的关系可以是任意的,图中任意两个数据元素之间都可能相关。
三大类:邻接矩阵、邻接表、十字链表
图的邻接矩阵存储方式:使用两个数组Array来表示图
一维数组:存储图中顶点信息
二维数组:(邻接矩阵)存储图中的边或弧的信息
无向图的边数组是一个对称矩阵。所谓对称矩阵就是n阶矩阵的元满足aij = aji。即从矩阵的左上角到右下角的主对角线为轴,右上角的元和左下角相对应的元全都是相等的。
从这个矩阵中,很容易知道图中的信息:
(1)可以判断任意两顶点是否有边无边;
(2)知道某个顶点的度,其实就是这个顶点vi在邻接矩阵中第i行或(第i列)的元素之和;
(3)求顶点vi的所有邻接点就是将矩阵中第i行元素扫描一遍,arc[i][j]为1就是邻接点;
有向图: 讲究入度和出度,顶点vi的入度为1,正好是第i列各数之和。顶点vi的出度为2,即第i行的各数之和。
实例:图G是网图,有n个顶点,则邻接矩阵是一个n*n的方阵,定义为:
wij表示(vi,vj)上的权值;
无穷大表示一个计算机允许的、大于所有边上权值的值,也就是一个不可能的极限值。
实例:带权重的有向图
实例:带权重的有向图
实例:带权重的无向图
一维数组:图中的顶点
链表:图中每个顶点的所有邻接点构成的一个线性表。由于临界点的个数不定,所以用单链表存储,五香兔成为顶点vi的边表,有向图成为顶点vi作为弧尾的出边表。
时间复杂度:n个顶点e条边,O(n+e)
实例:无向图
实例:无向图
实例:有向图
顶点表:各个结点由data和firstedge两个域表示,data是数据域,存储顶点的信息,firstedge是指针域,指向边表的第一个结点,即此顶点的第一个邻接点。
边表:结点由adjvex和next两个域组成。adjvex是邻接点域,存储某顶点的邻接点在顶点表中的下标,next则存储指向边表中下一个结点的指针。
实例:带权值的网图
在边表结点定义中再增加一个weight的数据域,存储权值信息即可。
优点:
对于,稀疏图,邻接表比邻接矩阵更节约空间。
缺点:
不容易判断两个顶点是有关系(边),顶点的出度容易,但是求入度需要遍历整个邻接表。
十字链表:把邻接表和逆邻接表结合起来,解决有向图用邻接表存储的缺陷。
容易找到以v为尾的弧,也容易找到以v为头的弧,因而比较容易求得顶点的出度和入度。
邻接表:对于有向图来说,是有缺陷的。
关心了出度问题,想了解入度就必须要遍历整个图才知道,反之,逆邻接表解决了入度却不了解出度情况。
重新定义顶点表节点结构:
firstin表示入边表头指针,指向该顶点的入边表中第一个结点
firstout表示出边表头指针,指向该顶点的出边表中的第一个结点
重新定义边表 结构
tailvex:弧起点在顶点表的下表
headvex:弧终点在顶点表的下标
headlink:入边表指针域,指向终点相同的下一条边
taillink:边表指针域,指向起点相同的下一条边
如果是网,还可以增加一个weight域来存储权值
实例:有向图
实例:有向图
重点:需要解释虚线箭头的含义。
它其实就是此图的逆邻接表的表示。
对于v0来说,它有两个顶点v1和v2的入边。因此的firstin指向顶点v1的边表结点中headvex为0的结点,如上图圆圈1。接着由入边结点的headlink指向下一个入边顶点v2,如上图圆圈2。对于顶点v1,它有一个入边顶点v2,所以它的firstin指向顶点v2的边表结点中headvex为1的结点,如上图圆圈3。
有向图应用中,十字链表是非常好的数据结构模型
邻接多重表:适用于无向图的存储结构
邻接多重表是无向图的另一种链式存储结构。我们之前也说了使用邻接矩阵来存储图比价浪费空间,但是如果我们使用邻接表来存储图时,对于无向图又有一些不便的地方,例如我们需要对一条已经访问过的边进行删除或者标记等操作时,我们除了需要找到表示同一条边的两个结点。这会给我们的程序执行效率大打折扣,所以这个时候,邻接多重表就派上用场啦。
首先,邻接多重表同样是对邻接表的一个改进得到来的结构,它同样需要一个头结点保存每个顶点的信息和一个表结点,保存每条边的信息,他们的结构如下:
其中,头结点的结构和邻接表一样,而表结点中就改变比较大了,其中mark为标志域,例如标志是否已经访问过,ivex和jvex代表边的两个顶点在顶点表中的下标,ilink指向下一个依附在顶点ivex的边,jlink指向下一个依附在顶点jvex的边,weight在网图的时候使用,代表该边的权重。
深度优先遍历,也有称为深度优先搜索,简称DFS。
类似树的先序遍历
它从图中某个结点v出发,访问此顶点,然后从v的未被访问的邻接点出发深度优先遍历图,直至图中所有和v有路径相通的顶点都被访问到。若图中尚有顶点未被访问,则另选图中一个未曾被访问的顶点作起始点,重复上述过程,直至图中的所有顶点都被访问到为止。
对上面的图G1进行深度优先遍历,从顶点A开始:
第1步:访问A。
第2步:访问(A的邻接点)C。
在第1步访问A之后,接下来应该访问的是A的邻接点,即"C,D,F"中的一个。但在本文的实现中,顶点ABCDEFG是按照顺序存储,C在"D和F"的前面,因此,先访问C。
第3步:访问(C的邻接点)B。
在第2步访问C之后,接下来应该访问C的邻接点,即"B和D"中一个(A已经被访问过,就不算在内)。而由于B在D之前,先访问B。
第4步:访问(C的邻接点)D。
在第3步访问了C的邻接点B之后,B没有未被访问的邻接点;因此,返回到访问C的另一个邻接点D。
第5步:访问(A的邻接点)F。
前面已经访问了A,并且访问完了"A的邻接点B的所有邻接点(包括递归的邻接点在内)";因此,此时返回到访问A的另一个邻接点F。
第6步:访问(F的邻接点)G。
第7步:访问(G的邻接点)E。
因此访问顺序是:A -> C -> B -> D -> F -> G -> E
因此访问顺序是:A -> B -> C -> E -> D -> F -> G
时间复杂度
广度优先遍历,又称为广度优先搜索,简称BFS。
类似树的层序遍历。
因此访问顺序是:A -> C -> D -> F -> B -> G -> E
因此访问顺序是:A -> B -> C -> E -> F -> D -> G
时间复杂度
字典dict类:表示键和值之间的关系,成为关联数组,或映射map
映射ADT与python的dict类一致
M[k]: __ getitem __
M[k] = v: __ setitem__
del M[k]: 删除键值为k的元组
len(M): 返回M中元组的数量
iter(m): 迭代生成其中一个映射所包含的所有键的序列,__ iter __, for k in M
K in M
M.get(k,d = None)
M.setdefault(k, d)
M.pop(k, d=None)
M.popitem()
M.clear()
M.keys()
M.values()
M.items()
M.update(M2)
M ==M2
M != M2
Mapping类: dict类支持的所有不变方法
MutableMapping类:dict类支持的所有可变方法
class MapBase(MutableMapping):
class _item:
__slots__ = '_key', '_value'
def __init__(self, k, v):
self._key = k
self._value = v
def __eq__(self, other):
return self._key == other._key
def __ne__(self, other):
return not (self == other)
def __It__(self, other):
return self._key < other._key
一个用列表作为非排序表的map是心啊方法
class UnsortedTableMap(MapBase):
def __init__(self):
self._table = []
def __getitem__(self, k):
for item in self._table:
if k == item._key:
return item._value
raise KeyError('Key Error: ' + repr(k))
def __setitem__(self, k, v):
for item in self._table:
if k == item._key:
item._value = v
return
self._table.append(self._Item(k, v))
def _delitem(self, k):
for j in range(len(self._table)):
if k == self._table[j]._key:
self._table.pop(j)
return
raise KeyError('Key Error: ' + repr(k))
def __len__(self):
return len(self._table)
def __iter__(self):
for item in self._table:
yield item._key
时间复杂度:O(n)
散列表ADT: 哈希表(hash table)
散列表的实现叫作散列(hashing)
散列是一种用于以常数平均时间执行插入、删除、查找的技术。
思想:用数组支持按照下标随机访问数据的特性实现的一种数据结构,时间复杂度是O(1)。是数组的一种扩展。
散列表中使用散列函数把元素的键值映射为下标,将数据存储在数组中对应的下标中。
查询元素的时候用同样的散列函数,将键值转化为数组下标,从而读取到位置。
哈希(散列)函数h:其目标是把每个键k映射到[0, N-1]区间内的常熟,N是哈希表的桶数组的容量,是用来把关键字Key进行散列的一个方法,称为hash function。
组成:
设计基本要求:
哈希冲突:计算hash值,两个不同的key得到两个一样的hash值
常用的哈希冲突解决方案有两类:开放定址法与分离链接法
在链接法中,把哈希到同一槽中的所有元素都放在一个链表中,如下图所示,槽j中有一个指针,它指向存储所有哈希到j的元素的链表的表头;如果不存在这样的元素,槽j中为NIL。
分析(查找一个关键字)
给定一个能存放n个元素的,具有m个槽位的散列表T,定义T的装载因子α为n/m,即一个链表的平均存储元素数量,α可以大于,等于或者小于1.
用链接法散列的最坏情况性能很差:所有的n个关键字都散列到同一个槽中,从而产生一个长度为n的链表,这时,最坏情况下查找时间为θ(n),再加上计算散列函数的时间,如果就和用一个链表来链接所有的元素差不多了。
散列方法的平均性能依赖于所选取的散列函数h,将所有关键字集合分布在m个槽位上的均匀程度。
在平均情况下,查找一个关键字有两个结果:查找成功和查找不成功。
在简单均匀散列的情况下,任何尚未被存储在表中的关键字k都等可能地被散列到m个槽中的任何一个,因此,当查找一个关键字k时,在不成功的情况下,查找的期望时间就是查找到链表T[h(k)]末尾的期望时间,这一时间的期望长度为α,于是一次不成功的查找平均要检查α个元素,并且所需要的总时间(包括计算h(k)的时间)为θ(1+α)
在查找成功的情况下,平均需要的时间也是θ(1+α)。
上述的分析意味着,如果散列表中槽数至少与表中的元素成正比(比如说,当要散列的元素的数量增加时,散列表T的槽数也要保持同样比例的增长),则有n = Ο(m),从而α= n/m = Ο(m) / m = Ο(1),所以查找操作平均时间需要常数时间。如果散列的元素的数量增加了,但是散列表的槽数没有增长,此时n = Ο(m) 就不成立,散列表的操作时间就和之前的不一样了。
在开放寻址法中,所有的元素都存放在散列表中,也就是说,每个表项或包含动态集合的一个元素,或包含NIL。当查找某个元素时,要系统地检查所有的表项,知道查找到所需要的元素,或者最终查明该元素不在表中。不像链接法,这里既没有链表,也没有元素存放在散列表外,因此在开放寻址法中,散列表可能会被填满,以至于不能插入任何新的元素,因此装载因子α = n / m绝对不会超过1,通常α < 0.5;也就是说,要散列的元素绝对不会多于槽的数量。
探测散列表
核心思想:出现哈希冲突,重新探测一个空闲位置将其插入。
探查方式:
插入数据
查找数据
给定一个普通的散列函数 h ’ : U → { 0, 1, …, m - 1 }(称为辅助散列函数),线性探查方法采用的散列函数为:
h(k, i) = ( h’(k) + i) mod m , i = 0, 1, …, m - 1
给定一个关键字 k ,第一个探查的槽是 T[h’(k)],亦即,由辅助散列函数所给出的槽。接下来探查的是槽 T[h’(k) + 1], …,直到槽 T[m - 1],然后又绕到槽 T[0], T[1], …直到最后探查槽 T[h’(k) - 1]。在线性探查方法中,初始探查位置确定了整个序列,故只有 m 种不同的探查序列。
线性探查方法很容易实现,但它存在一个问题,称作一次聚集。随着时间的推移,连续被占用的槽不断增加,平均查找时间也随着不断增加。聚集现象很容易出现,这是因为当一个空槽前有 i 个满的槽时,该空槽作为下一个将被占用槽的概率是( i + 1 ) / m 。连续被占用槽的序列将会越来越长,因而平均查找时间也会随之增加。
消除线性探测法的一次聚集问题;
平方探测采用如下形式的散列函数(二次)
h(k, i) = (h’(k) + c1 i + c2i2) mod m
其中 h’是一个辅助散列函数, c1 和 c2 为辅助常数(不等于0), i = 0,1,…,m - 1。初始的探查位置为 T[h’(k)],后续的探查位置要在此基础上加上一个偏移量,该偏移量以二次的方式依赖于探查号 i 。这种探查方法的效果要比线性探查好很多,但是,如果两个关键字的初始探查位置相同,那么他们的探查序列也是相同的,这是因为 h (k1,0) = h (k2,0)蕴含着 h(k1,i) = h(k2,i)。这一性质可导致一种程度较轻的聚集现象,称为二次聚集。二次探查也只有m个不同的探查序列。
双重哈希(散列) 是用于开放寻址法的最好方法之一,它采用如下形式的散列函数:
h(k,i) = (h1(k) + i·h2(k)) mod m
其中 h1 和 h2 为辅助哈希(散列)函数。初始探查位置为 T[ h1(k)],后续的探查位置在此基础上加上偏移量 h2(k) 模 m 。
为能查找整个散列表,值 h2(k)要与表的大小m互质。确保这个条件成立的一种方法是取m为2的幂,并设计一个总产生奇数的 h2 。另一种方法是取m为质数,并设计一个总是产生较m小的正整数的函数 h2 。例如,可以取m为素数,m’略小于m,如下:
h1(k) = k mod m
h2(k) = 1 + (k mod m’)
分析
相对于链接法,开放寻址法的好处在于不需要用到指针,而是计算出槽的序列,于是,不用存储指针而节省的空间,使得可以用同样地空间来提供更多的槽,潜在地减少了冲突,提高了检索速度。
从开放寻址法的散列表中删除元素比较困难,当从槽i中删除关键字时,不能仅仅将NIL置于其中来标识它为空,如果这样做就会出现问题:在插入关键字k时,发现槽i被占用了,则k会插入到后面的位置上;此时将槽i中的关键字删除后,就无法检索到关键字k了,有一个解决方法就是,在槽i中置一个特定的值DELETED替代NIL来标记空槽。当使用特殊的值DELETED时,查找时间就不再依赖于装载因子了,为此,在必须删除关键字的应用中,更常见的方法是采用链接法来解决冲突。
每个桶中期望的键的数量是n/N向上取整,如果n是O(n),则键的数量是O(1)
class HashMapBase(MapBase):
def __init__(self, cap=11, p=109345121):
self._table = cap * [None]
self._n = 0
self._prime = p
self._scale = 1 + randrange(p-1)
self._shift = randrange(p)
def _hash_function(self, k):
return (hash(k)*self._scale+self._shift) % self._prime % len(self._table)
def __len__(self):
return self._n
def __getitem__(self, k):
j = self._hash_function(k)
return self._bucket_getitem(j, k)
def __setitem__(self, k):
j = self._hash_function(k)
self._bucket_setitem(j, k, v)
if self._n > len(self._table) // 2:
self._resize(2*len(self._table)-1)
def __delitem(self, k):
j = self._hash_function(k)
self._bucket_delitem(j, k)
self._n -= 1
def _resize(self, c):
old = list(self.items())
self._table = c * [None]
self._n = 0
for (k, v) in old:
self[k] = b
# 分离链表实现的具体hashmap类
class ChainHashMap(HashMapBase):
def _bucket_getitem(self, j, k):
bucket = self._table[j]
if bucket is None:
raise KeyError('Key Error: ' + repr(k))
return bucket[k]
def _bucket_setitem(self, j, k, v):
if bucket is None:
self._table[j] = UnsortedTableMap()
oldsize = len(self._table[j])
self._table[j][k] = v
if len(self._table[j]) > oldsize:
self._n += 1
def _bucket_delitem(self, j, k):
bucket = self._table[j]
if bucket is None:
raise KeyError('Key Error: ' + repr(k))
del bucket[k]
def __iter__(self):
for bucket in self._table:
if bucket is not None:
for key in bucket:
yield key
# 线性探测
class ProbeHashMap(HashMapBase):
_AVAIL = object()
def _is_available(self, j):
return self._table[j] is None or self._table[j] is ProbeHashMap._AVAIL
def _find_slot(self, j, k):
firstAvail = None
while True:
if self._is_available(j):
if firstAvail is None:
firstAvail = j
if self._table[j] is None:
return (False, firstAvail)
elif k == self._table[j],_key:
return (True, j)
j = (j+1) % len(self._table)
def _bucket_getitem(self, j, k):
found, s = self._find_slot(j, k)
if not found:
raise KeyError('Key Error: ' + repr(k))
return self._table[s]._value
def _bucket_setitem(self, j, k, v):
found, s = self._find_slot(j, k)
if not found:
self._table[s] = self._Item(k, v)
self._n += 1
else:
self._table[s].value = v
def _bucket_delitem(self, j, k):
found, s = self._find_slot(j, k)
if not found:
raise KeyError('Key Error: ' + repr(k))
self._table[s] = ProbeHashMap._AVAIL
def __iter__(self):
for j in range(len(self._table)):
if not self._is_available(j):
yield self._table[j]._key
添加行为:
M.find_min()
M.find_max()
M.find_It(k)
M.find_le(k)
M.find_gt(k)
M.find_ge(k)
M.find_range(start, stop)
iter(M)
reversed(M)
优先级队列是一种抽象数据类型,它是一种排序的机制
优先级队列:这个集合允许插入任意的元素,并删除拥有最高优先级的元素;当一个元素被插入优先级队列中时,通过一个关联键为该元素赋予一定的优先级;键值最小的元素将是下一个从队列中移除的元素。
功能强大:自动排序
两个核心操作:
抽象数据类型实现:元素和他的优先级,key-value
效果:在维护一个动态的队列;可以收集一些元素,并快速取出键值最大的元素,对其操作后移出队列,然后再收集更多的元素,再处理当前键值最大的元素,如此这般。
组合设计模式
class PriorityQueueBase:
class __Item:
__slots__ = '_key', '_value'
def __init__(self, k, v):
self._key = k
self._value = v
def __It__(self, other):
return self._key < other._key
def is_empty(self):
return len(self) == 0
数组实现(无序)
►思想:
我们维护一个数组,因为不考虑数组顺序,所以我们的插入算法就很简单了。
对于查找最大值,我们利用了选择排序,在找到最大值后,将其与最后一个元素交换,并使长度-1.
数组实现(有序)
►思想:
由于我们维护一个有序数组,所以每次插入元素的时候都要给他找到一个合适位置,来保证数组有序性,删除操作就会很简单了。
二叉堆(堆):以二进制堆的数据机构实现更加有效的优先级队列,堆是一棵完全二叉树T,该树在它的位置上存储了集合中的元组并且满足两个属性:关系属性以存储键的形式在T中定义;结构属性以树T自身形状的方式定义。
两个属性:
堆的高度:
使用h表示T的高度,堆T有n个元组,它的高度为h= ⌊log n⌋
两个性质:
大顶堆:在二叉堆中每一个节点的值都要保证大于等于另外子节点的值,即头重脚轻
小顶堆:自上而下依次升高,即每一个节点的值都小于等于其子节点的值
例:大顶堆,根节点一定是所有元素中最大的一个,即优先性最高的,当我们取走后,取代其位置的也应是下一个最大的元素
说明:
这是一个堆有序的二叉树。所谓堆有序就是一棵二叉树的每个节点都大于等于它的两个子节点。
►我们来组织一种堆有序的二叉树,这种有序的结构便于我们实现了优先队列。
我们可以使用指针来表示,但是这并不是最方便的。通过观察二叉有序堆,我们会发现它是一种完全二叉树,并且完全二叉树可以用数组来表示。
基于数组实现二叉有序堆(优先级队列)
具体方法就是将二叉树的节点按照层序顺序放入数组中,根节点位置在0,它的子节点位置在1,2.依次类推。
►两条重要的性质:
在一个二叉堆中,位置为K的节点的父节点的位置为|K/2|,而它的两个子节点位置为2K+1和2K+2
一颗大小为N的完全二叉树的高度为|Log N|
在堆中增加一个元组
新节点放在树的最有节点相邻位置,如果底层节点已满,应存放在新一层的最左位置
插入元组后堆向上冒泡
若破坏Heap-Order属性,,需重新调整树(p243)
移除键值最小的元组
键值最小的元组存放在堆的根结点,将最后位置叶子节点p与根结点交换,通过删除堆的最后位置叶子结点p确保堆的形状满足完全二叉树属性
删除操作后堆向下冒泡
将p初始化为堆T的根:
1)若p没有有孩子,另c表示p的左孩子
2)若p有两个孩子,另c作为p的具有较小键值的孩子
分析
# PriorityQueueBase类的扩展
class HeapPriorityQueue(PriorityQueueBase):
def _parent(self, j):
return (j - 1) // 2
def _left(self, j):
return 2*j + 1
def _right(self, j):
return 2*j + 2
def _has_left(self, j):
return self._left(j) < len(self._data)
def _has_right(self, j):
return self._right(j) < len(self._data)
def _swap(self, i, j):
self._data[i], self_data[j] = self_data[j], self._data[i]
def _upheap(self, j):
parent = self._parent(j)
if j > 0 and self._data[j] < self._data[parent]:
self._swap(j, parent)
self._upheap(self, parent)
def _downheap(self, j):
if self._has_left(j):
left = self._left(j)
small_child = left
if self._has_right(j):
right = self._right(j)
if self._data[right] < self.data[left]:
small_child = right
if self._data[small_child] < self._data[j]:
self._swap(j, small_child)
self._downheap(small_child)
# 基于数组的堆实现优先级队列
def __init__(self):
self._data = []
def __len(self):
return len(self._data)
def add(self, key, value):
self._data.append(self._Item(key, value))
self._upheap(len(self._data)-1)
def min(self):
if self.is_empty():
raise Empty("Priority queue is empty")
item = self._data[0]
return (item._key, item._value)
def remove_min(self):
if self.is_empty():
raise Empty("Priority queue is empty")
self._swap(0, len(self._data)-1)
item = self._data.pop()
self._downheap(0)
return (item._key, item._value)
►说明:
如果堆的有序化因为某个节点X变得比它的父节点更大而打破,我们需要交换它和它的父节点来修复堆,但是可能交换后X还是很大,所以我们需要X一次次的它的祖先节点进行比较,直到找打它最合适的位置。
根据二叉堆的性质,我们不难发现只要记住位置为K的节点的父节点为 |K/2|,一切都很简单了。
►说明:
如果堆的有序话因为某个节点X变得比它的两个子节点或其一更小而打破,我们需要交换它和它的子节点中较大的节点来修复堆,但是可能交换后X还是很小,所以我们需要X一次次的它的子节点进行比较并交换,直到找打它最合适的位置。
键的数量为n,n=2h+1-1,即堆是每一层都满的完全二叉树
def __init__(self, contents=()):
self._data = [self._Item(k, v) for k, v in contents]
if len(self._data) > 1:
self._heapify()
def _heapify(self):
start = self._parent(len(self)-1)
for j in range (start, -1, -1):
self._downheap(j)
时间复杂度:O(n)
python有heapq模块,提供对给予堆的优先级队列的支持
严格弱序:
漫反射特性:k < k
传递属性:如果k1def pq_sort(C):
n = len(C)
P = PriorityQueue()
for j in range(n):
element = C.delete(C.first())
P.add(element, element)
for j in range(n):
(k, v) = P.remove_min()
C.add_last(v)