"堆结构及"优先队列"描述

一、优先队列的概念及实现 

是一种有序的树形结构,说明堆之前需要先说明一下“优先队列”这个重要的缓存结构。
"优先队列"和栈,队列类型,都是保存,访问元素的,但其特点是存入其中的每项数据都有一个"优先程度"的附加数值,即”优先级“。任何时候访问的元素都是结构中优先级最高的。抽象的看,优先队列描述的是一个有序集S=(D,<=),<=表示集合D上的一个全序,表示元素的优先关系。

一种简单实现优先队列的方法,是使用线性表技术。从使用的角度看,用户只关心优先队列的使用特性,并不很关心数据是否按优先级顺序进行存储。所以不难想到存在两种实现方案:

  1. 在存入数据时,保证表中元素按优先顺序排列。这种形式在存入元素时比较麻烦,访问时方便。
  2. 存入数据时不作处理,在访问时通过检索找到最优先的元素。这种无组织的方式存入时方便,但访问时麻烦。一种解决方法是可以通过记录优先元素来检索。

使用list来做优先是一种很简便的方式,list对象能根据存储元素的实际需要自动扩大存储区。但必须在合法的范围内使用下标表达式。所以,在需要新插入元素时,必须先确定正确的插入位置。

下面我们使用list结构来分别实现上面描述的两种方式:

第一种方式:

class PrioQue:
	def __init__(self, elist=None);
		self._elems = list(elist) if elist else []
		self._elems.sort(reverse=True)
	
	def enqueue(self, e):
		i = len(self._elems) - 1
		while i >= 0:
			if self._elems[i] <= e:
				i -= 1
			else:
				break
		self._elems.insert(i+1, e)
	
	def is_empty(self):
		return not self._elems
		
	def peek(self):
		if self.is_empty():
			raise ValueError
		return self._elems[-1]
	
	def dequeue(self):
		if self.is_empty():
			raise ValueError
		return self._elems.pop()

不难分析出该方式的复杂度:插入元素是O(n)操作,其它都是O(1)。

注意:python的list结构由于采用的是动态顺序表实现,所以在插入元素时发现存储区满,会换一块存储区,但即使换,它的复杂度也是O(n)。

第二种方式:

class PrioQue:
	def __init__(self, elist=None);
		self._elems = list(elist) if elist else []
		self._elems.sort(reverse=True)
	
	def enqueue(self, e):
		self._elems.append(e)
	
	def is_empty(self):
		return not self._elems
		
	def peek(self):
		if self.is_empty():
			raise ValueError
		min = self._elems[0]
		
		for i in range(1, len(self._elems)):
			if self._elems[i] < min:
				min = self._elems[i]
		return min
	
	def dequeue(self):
		if self.is_empty():
			raise ValueError
		min = self._elems[0]
		for i in range(1, len(self._elems)):
			if self._elems[i] < min:
				min = self._elems[i]
		self._elems.pop(min)
		return min

同样不难分析一出它的时间复杂度:

插入元素是O(1)(替换表存储需要O(n)时间),获取弹出元素是O(n)。

除了采用线性表实现,还可以使用链接表来实现优先队列,但主要的操作复杂度都与连续表类似。

由于线性排列结构无法避免移动元素或链接爬行步骤,所以实现效率都比较低。

由些我们引出了堆。

二、堆

采用树形结构实现优先队列的一种有效技术称为堆。从结构上看,堆就是结点里存储数据的完全二叉树,但堆中数据的存储要满足一种特殊的堆序,即任何一个结点里所存的数据要先于或等于其子结点里的数据,那么可以看出堆有的几个特性:

  1. 从树根到任何一个叶结点的路径上,各结点所存的数据按规定的优先关系递减
  2. 最优先的元素必定位于对的根结点里,即堆顶,O(1)时间就能得到
  3. 树中不同路径上的元素,不关心其顺序关系

除了上面的特性外,堆和完全二叉树还有几个重要的特性:
Q1.在一个堆的最后加上一个元素,整个结构还是完全二叉树,但由于未必满足堆序,所以未必是堆
Q2.去掉堆顶后,其余元素形成两个"子堆",堆序仍然成立
Q3.给去掉堆顶的两个堆加上一个根元素,得到的序列仍然是完全二叉树,但未必是堆
Q4.去掉一个堆最后的元素,剩下的元素仍是构成一个堆

根据堆中元素的大小顺序,可以将堆分为小顶堆和大顶堆。
小元素优先的叫小顶堆,堆中每个结点的数据都<=其子结点数据
大元素优先的叫大顶堆,堆中每个结点的数据都>=其子结点数据

在实现堆结构的过程中,我们需要考虑是否有一种操作可以在向堆插入新元素(Q1)或新的堆顶(Q2)的时候,仍然可以保持正确的堆序。答案是向下筛选和向上筛选。
向下筛选就是从堆顶向下做一次调整堆序的操作,对应的向上筛选就是由子结点向堆顶做堆序调整的操作

下面我们来看一个两个筛选的具体操作

插入元素和向上筛选:
不断用新加入的元素e与其父结点的数据进行比较,如果e较小就交换两个元素的位置,不断上移直到e大于或等于其父结点的数据或到达根结点为止,由于向上筛选操作中比较和交换的次数不会超过树的最长路径,所以可以在O(log n)时间完成


弹出元素和向下筛选:
由于堆顶元素就是最优先元素,所以弹出的元素是就它,根据Q2,剩下两个“子堆”。根据Q3,Q4取原堆最后一个元素放到堆顶,执行一次向下筛选,恢复堆序。
假设e是新的堆顶元素,A,B是子堆,

  1. 用e与两个“子堆”的顶元素比较,最小的那个作为新的堆顶;若e不是最小,最小的必然为两个子堆的根;设的根最小,移到堆顶,相当于删去了A的顶元素;把e放入去掉堆顶的A;B的根最小情况同理处理
  2. 如果某次比较中e最小或e落到底,那么以e为顶的树已成为堆,即恢复了堆序

三、使用"堆"实现"优先队列"

回到优先队列上,使用堆来实现,可以分为三个步骤:
1.弹出堆顶O(1)
2.取堆最后一个元素作为完全二叉树新根O(1)
3.向下筛选就是从堆顶向下做一次调整堆序的操作,对应的向上筛选就是由子结点向堆顶做堆序调整的操作O(log n)

我们知道线性结构可以存入一颗完全二叉树,所以也可以存入一个完整堆。

现在使用一个list来定义一个堆,以表首做为堆顶,在表尾加入元素。

class PrioQueue(object):
	def __init__(self, elist=None):
		self._elems = list(elist) if elist else []
		if elist:
			self.buildheap()
		
		def is_empty(self):
			return not self._elems
			
		def peek(self):
			if self.is_empty():
				raise ValueError
			return self._elems[0]
			
		def enqueue(self, e):
			self._elems.append(None)
			self.siftup(e, len(self._elems) - 1)
			
		def siftup(self, e, last):
			elems, i, j = self._elems, last, (last - 1) // 2
			
			while i > 0 and e < elems[j]:
				elems[i] = elems[j]
				i, j = j, (j - 1) // 2
			elems[i] = e
		 
		 def dequeue(self):
			if self.is_empty():
				raise ValueError
			elems = self._elems
			e0 = elems[0]
			e = elems.pop()
			
			if len(elems) > 0:
				self.siftdown(e, 0, len(elems))
			return e0
		
		def siftdown(self, e, begin, end):
			elems, i, j = self._elems, begin, begin * 2 + 1
			while j < end:
				if j + 1 < end and elems[j+1] < elems[j]:
					j += 1
				if e < elems[j]:
					break
				elems[i] = elems[j]
				i, j = j, 2 * j + 1
			elems[i] = e
			
		def buildheap(self):
			end = len(self._elems)
			for i in range(end // 2, -1, -1):
				self.siftdown(self._elems[i], i, end)

来分析一下复杂度:

设被处理的完全二叉树有n个元素,高度为h,则子树共计大约n/2^h,调整每一颗这样的子树为一个堆,根元素的移动距离不超过h-1,对操作的移动求和,可知堆构建的复杂性是O(n)。

总结一下基于堆构建的优先队列的复杂度:
创建操作的时间复杂度是O(n),该操作仅需做一次。
插入和弹出操作的复杂度是O(log n)

注意:插入可以导致list对象替换元素存储区,最坏出现O(n),操作中只用到了一个简单变量,所以空间复杂度是O(1)

 

有关堆排序的整理会在“排序”一文作整理。

本文是学习“堆”数据结构时,做的一个整理笔记,由《数据结构与算法,python语言描述》一书内容整理而出,。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值