作者:禅与计算机程序设计艺术
数据结构(Data Structure)是指用来存储、组织和处理数据的集合。它是一个抽象的计算机科学概念,是一种对特定类型信息进行有效地组织、管理、访问及操纵的方式。常见的数据结构有栈、队列、数组、链表、哈希表、树、堆、图等。本文主要介绍了常见的数据结构中最常用的一些组合方式,并结合相应的代码实现。
2.基本概念术语说明
1.堆(Heap):堆是一个可以被看作是一棵完全二叉树(所有层次从左到右)的树结构。这种数据结构的特点是根节点的值最小或最大(取决于堆是否为最大堆),并且每个节点的值都要大于等于(或者小于等于)它的子节点的值。
2.双端队列(Deque):双端队列就是一个具有两端操作的队列,常见的双端队列包括队列和栈。在队列头部和尾部都可以插入或者删除元素,而其他位置则只能从头部或者尾部弹出。例如,栈和队列都是只允许在一端进行操作的线性数据结构,但双端队列却可以同时在头部和尾部进行操作。
3.优先级队列(Priority Queue):优先级队列是指按照某种顺序排列的队列。在优先级队列中,每个元素都有一个优先级,当有新的元素进入队列时,它会自动排序到适当的位置上去。优先级队列通常用二叉堆或者二叉搜索树来实现。
4.散列表(Hash Table):散列表(又称哈希表)是一个存储键-值对的无序结构。该结构通过把键映射到索引位置来快速查找值。在Python中,字典属于散列表的一种。
5.树(Tree):树是一种抽象数据类型,它是由一组节点组成的集合。其中,每一个节点代表一个元素,并且存在一条通向其他元素的路径。不同类型的树可以用于不同的任务,如二叉树、N叉树、满二叉树、伸展树、哈夫曼树、AVL树、红黑树等。本文将重点介绍二叉树、双端队列、优先级队列、散列表和树。
3.核心算法原理和具体操作步骤以及数学公式讲解
3.1 堆 (Heap)
堆(Heap)是一种特殊的树形数据结构。堆是一个经典的数据结构,主要应用于高速内存数据结构的实现以及搜索和排序算法的优化。堆分为两种:最大堆和最小堆。
最大堆:对于每个节点,其值都不大于(或不小于)其父节点的值。即,树的每个节点的值都大于(或者小于)它的所有后裔。这种结构通常可以用来解决最大和最小值的搜索问题,如二叉搜索树。
最小堆:对于每个节点,其值都不小于(或不大于)其父节点的值。即,树的每个节点的值都小于(或者大于)它的所有后裔。对于比较少出现较大的负值情况的情况,可以使用最小堆来优化性能。
根据堆的定义,我们可以创建堆的方法有很多,这里我们以数组表示法来实现一个最大堆,创建一个大小为n的最大堆的步骤如下:
-
从第一个非叶子节点(最后一个节点除外)开始调整堆(因为下标从0开始,所以最后一个非叶子节点下标为 n/2 - 1)。
-
假设当前节点的下标为 i,其左孩子下标为 j = 2i+1;右孩子下标为 k = 2i+2。
-
如果 j < n 且 arr[j] > arr[i],那么将arr[i]和arr[j]互换。
-
如果 k < n 且 arr[k] > arr[i],那么将arr[i]和arr[k]互换。
-
将当前节点下标设为 j 或 k 中的较大值,直至 j 和 k 越界或 arr[j] <= arr[i] 和 arr[k] <= arr[i]。
-
重复步骤2~5,直至全部节点调整完毕。
在Python中,使用list来实现一个最大堆如下所示:
class MaxHeap:
def __init__(self, nums=[]):
self.heap = [float('-inf')] + nums # 初始化堆,首位放一个负无穷,使得首位空出。
for i in range(len(nums)//2, -1, -1):
self._sift_down(i)
def push(self, val):
self.heap.append(val)
self._sift_up(len(self.heap)-1)
def pop(self):
if len(self.heap) == 1:
raise Exception('The heap is empty!')
last = self.heap.pop() # 取出末尾节点
if not self.heap: # 当堆为空时,直接返回None
return None
root = self.heap[0] # 堆顶元素赋值给root
self.heap[0] = last # 末尾节点赋值给堆顶元素
del last # 删除末尾节点
self._sift_down(0) # 下沉新根节点
return root # 返回堆顶元素
def _sift_up(self, idx):
while idx > 0 and self.heap[idx//2] < self.heap[idx]: # 当前节点比父节点小
self.heap[idx], self.heap[idx//2] = self.heap[idx//2], self.heap[idx] # 交换两个节点
idx //= 2 # 向上寻找
def _sift_down(self, idx):
while 2*idx + 1 < len(self.heap): # 有左孩子
child = 2*idx + 1 # 选择左孩子作为子节点
if child + 1 < len(self.heap) and self.heap[child+1] > self.heap[child]:
child += 1 # 选择右孩子作为子节点
if self.heap[child] >= self.heap[idx]: # 当前节点大于子节点,退出循环
break
self.heap[child], self.heap[idx] = self.heap[idx], self.heap[child] # 交换两个节点
idx = child # 更新当前节点下标
3.2 双端队列 (Dequeue)
双端队列(deque,double-ended queue)是一种基于双端链表的先入先出队列。双端队列也可以看作是一系列的栈组合起来形成的一个队列,即先进的先出,后进的先出。双端队列的操作分为两端两侧分别操作,实现简单、高效。
双端队列支持以下操作:
-
push_front(x),在队首加入元素 x;
-
push_back(x),在队尾加入元素 x;
-
front(), 获取队首元素;
-
back(), 获取队尾元素;
-
pop_front(), 删除队首元素;
-
pop_back(), 删除队尾元素。
双端队列的 Python 实现如下:
class Deque:
def __init__(self):
self.items = []
def isEmpty(self):
return self.items == []
def size(self):
return len(self.items)
def push_front(self, item):
self.items.insert(0,item)
def push_back(self, item):
self.items.append(item)
def pop_front(self):
return self.items.pop(0)
def pop_back(self):
return self.items.pop()
def peek_front(self):
return self.items[0]
def peek_back(self):
return self.items[-1]
if __name__=='__main__':
dq = Deque()
print("IsEmpty?",dq.isEmpty())
dq.push_back(1)
dq.push_back(2)
dq.push_front(3)
dq.push_front(4)
print(dq.size())
print(dq.peek_front(),dq.peek_back())
print(dq.pop_front(),dq.pop_back())
print(dq.peek_front(),dq.peek_back())
dq.push_back(7)
dq.push_front(8)
print(dq.size())
print(dq.pop_front(),dq.pop_back())
print(dq.peek_front(),dq.peek_back())
输出结果为:
IsEmpty? True
4
4 2
1 2
3 2
3
2
7 3
8 3
3.3 优先级队列 (PriorityQueue)
优先级队列是一种类似队列的数据结构,但是它的元素根据它们的优先级而不是FIFO顺序进行排序。优先级队列中的每个元素都有一个相关联的优先级。这些元素按照优先级来排序,优先级高的元素排在前面,优先级低的元素排在后面。
优先级队列主要有两种实现:
-
堆实现:利用堆数据结构来实现优先级队列。堆中的每个结点都是一个元素,并且堆保持每个结点的优先级。当从堆中取出元素时,其优先级最高的元素总是被返回。
-
斐波那契堆实现:斐波那契堆是一种复杂的数据结构,它支持动态增删改操作,而且复杂度为 O(logn)。斐波那契堆在很多场景下都比堆更快,尤其是在大量增删改操作时。
本文介绍的是堆实现优先级队列。首先,介绍一下二叉堆,然后介绍优先级队列的几种操作。
3.3.1 二叉堆
二叉堆是一类特殊的树结构。二叉堆是一个完全二叉树,所以除了最底层之外,其它层的节点都恰好有两个子节点。二叉堆的堆序是一个严格递减的顺序,这意味着树中任一节点的值都不大于(或者不小于)它的子女节点的值。
最大堆:满足堆序关系的二叉树叫做最大堆。每个节点的值都不大于其子女节点的值。
最小堆:满足堆序关系的二叉树叫做最小堆。每个节点的值都不小于其子女节点的值。
二叉堆的应用领域:
-
堆排序:堆排序是排序算法中最重要的一种,也是基于二叉堆的一种排序算法。它的最坏时间复杂度为O(nlgn),平均时间复杂度为O(lgn)。
-
求解最小/最大值:求解最小/最大值问题的二叉堆通常采用线性时间复杂度。因此,它可以在O(1)时间内找出最小值/最大值。
-
搜索:堆结构允许对任意节点进行快速检索,得到它的值,和它所有的子女节点值。
-
随机存取:由于二叉堆的高度比普通的二叉查找树高得多,所以访问任何节点的时间复杂度都很小。这样就可以在O(1)时间内存取任何节点。文章来源:https://uudwc.com/A/AAXpr
3.3.2 优先级队列
优先级队列是一个队列,其中每个元素都有一个优先级,并按优先级来排序。优先级队列中的元素被赋予不同的优先级,其中元素的优先级越高则排在越前面。文章来源地址https://uudwc.com/A/AAXpr
3.3.2.1 创建一个空的优先级队列
from typing import List
import heapq
class PriorityQueue:
def __init__(self):
self.__pq = [] # list to store elements in the priority queue
self.__count = 0 # counter of number of elements in the priority queue
def is_empty(self) -> bool:
"""Returns whether or not the priority queue is empty"""
return self.__count == 0
def put(self, item: int, priority: float) -> None:
"""Inserts an element into the priority queue with a given priority"""
entry = (-priority, self.__count, item)
heapq.heappush(self.__pq, entry)
self.__count += 1
def get(self) -> int:
"""Removes and returns the highest priority element from the priority queue"""
if self.is_empty():
raise Exception("Cannot remove from an empty priority queue")
_, _, item = heapq.heappop(self.__pq)
self.__count -= 1
return item
3.3.2.2 查看优先级队列的第一个元素
def peek(self) -> int:
"""Gets the highest priority element without removing it"""
if self.is_empty():
raise Exception("Cannot view top of an empty priority queue")
_, _, item = self.__pq[0]
return item
3.3.2.3 改变优先级
def change_priority(self, item: int, new_priority: float) -> None:
"""Changes the priority of an element within the priority queue"""
for index, (_, count, data) in enumerate(self.__pq):
if data == item:
entry = (-new_priority, count, item)
self.__pq[index] = entry
heapq._siftup(self.__pq, index)
heapq._siftdown(self.__pq, 0, index)
break
3.3.3 使用优先级队列的例子
p = PriorityQueue()
p.put(1, 5) # Inserting an element with priority 5
p.put(2, 3) # Inserting another element with priority 3
print(p.get()) # Removing and returning the first element with highest priority
# Output: 2
p.change_priority(1, 4) # Changing the priority of an element
print(p.peek()) # Viewing the highest priority element
# Output: 1