3.3.1 栈的概述
栈(Stack)是一个线性结构,其维护了一个有序的数据列表,列表的一端称为栈顶(top),另一端称为栈底(bottom)。栈对数据的操作有明确限定,插入元素只能从栈顶进行,删除元素也只能栈顶开始逐个进行,通常将插入元素称为入栈(push),删除元素称为出栈(pop)。正是由于上述规定,栈保证了后进先出的原则(LIFO,Last-In-First-Out)。
栈的底层实现既可以选择数组也可以选择链表,只要能保证后进先出的原则即可。
3.3.2 栈的功能定义
方法 说明
size() 返回栈中元素个数
is_empty() 判断栈是否为空
push(item) 将新元素压入栈中
pop() 获取栈顶元素,并将栈顶元素弹出栈
peek() 获取栈顶元素,但不弹出栈
3.3.3 栈的实现
使用动态数组实现一个栈。
class Stack:
def init(self):
“”“初始化栈”“”
self.__size = 0
self.__items = []
@property
def size(self):"""获取栈元素个数"""return self.__sizedef is_empty(self):"""判断栈是否为空"""return self.__size == 0def push(self, item):"""入栈"""self.__items.append(item)self.__size += 1def pop(self):"""出栈"""if self.is_empty():raise Exception("栈为空")item = self.__items[self.__size - 1]del self.__items[self.__size - 1]self.__size -= 1return itemdef peek(self):"""访问栈顶元素"""if self.is_empty():raise Exception("栈为空")return self.__items[self.__size - 1]
3.3.4 栈的应用
1)有效括号
力扣20题https://leetcode.cn/problems/valid-parentheses/description/
(1)题目描述
给定一个只包括“(”,“)”,“[”,“]”,“{”,“}”的字符串s,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
每个右括号都有一个对应的相同类型的左括号。
(2)示例
示例 1:
输入:s = “()”
输出:true
示例 2:
输入:s = “()[]{}”
输出:true
示例 3:
输入:s = “(]”
输出:false
示例 4:
输入:s = “([])”
输出:true
(3)思路分析
遇到左括号则入栈,遇到右括号则出栈一个左括号与之匹配,如果能够匹配则继续,如果匹配失败或者栈为空则返回False。
(4)代码实现
class Solution:
def isValid(self, s):
stack = []
for i in s:
match i:
case “(” | “[” | “{”:
stack.append(i)
case “)”:
if (not stack) or (stack.pop() != “(”):
return False
case “]”:
if (not stack) or (stack.pop() != “[”):
return False
case “}”:
if (not stack) or (stack.pop() != “{”):
return False
return True if not stack else False
if name == “main”:
solution = Solution()
s = “()[]{}”
print(s, solution.isValid(s))
s = “(]”
print(s, solution.isValid(s))
s = “([)]”
print(s, solution.isValid(s))
s = “{[]}”
print(s, solution.isValid(s))
3.4 队列
3.4.1 队列的概述
队列(Queue)也是一个线性结构,其同样维护了一个有序的数据列表,队列的一端称为队首,另一端称为队尾。队列也对数据操作做出了明确限定,插入元素只能从队尾进行,删除元素只能从队首进行,通常将插入操作称为入队(enqueue),将删除操作称为出队(dequeue)。也正是由于上述限制,队列保证了先进先出(FIFO,First-In-First-Out)的原则。
队列的底层实现既可以选择数组也可以选择链表,只要能保证先进先出的原则即可。
常见的队列包括两种:
单向队列:只能从一端插入数据,从另一端删除数据,遵循先进先出。
双向队列:在队列的两端都可以进行插入和删除操作。
3.4.2 队列的功能定义
方法 说明
size() 返回队列中元素个数
is_empty() 判断队列是否为空
push(item) 向队尾添加元素
pop() 从队首取出元素
peek() 访问队首元素
3.4.3 队列的实现
使用链表实现一个单向队列。
class Node:
def init(self, data):
self.data = data
self.next = None
class Queue:
def init(self):
“”“初始化队列”“”
self.__head = None
self.__tail = None
self.__size = 0
@property
def size(self):"""获取队列元素个数"""return self.__sizedef is_empty(self):"""判断队列是否为空"""return self.__size == 0def push(self, data):"""入队"""node = Node(data)if self.is_empty():self.__head = nodeself.__tail = nodeelse:self.__tail.next = nodeself.__tail = nodeself.__size += 1def pop(self):""" "出队"""if self.is_empty():raise Exception("队列为空")data = self.__head.dataself.__head = self.__head.nextself.__size -= 1return datadef peek(self):"""访问队首元素"""if self.is_empty():raise Exception("队列为空")return self.__head.data
3.5 哈希表
3.5.1 哈希表的概述
哈希表(Hash Table,也叫散列表),由一系列键值对(key-value pairs)组成,并且可以通过键(key)查找对应的值(value)。哈希表通过建立key与value之间的映射,实现高效的查询,我们向哈希表中输入一个key,可以在O(1)的时间内获取对应的value。
例如通过客户id获取客户姓名:
哈希表常见的一个操作是根据key来查找value,考虑到数组查询效率最高,选择基于数组实现哈希表。利用哈希函数计算key的哈希值,然后将哈希值映射到数组索引。在实现过程中我们可能会遇到如下问题:
如何将一个个key映射到数组的索引?
如果多个key映射到数组同一个索引怎么办?
数组长度是固定的,如果后续元素过多,大于数组长度怎么办?
1)哈希函数
哈希表的核心组件是哈希函数。该函数将key转换为一个数组索引。哈希函数的目标是尽量均匀地将所有可能的key分布到表的不同位置,以减少冲突的发生。
哈希函数的执行步骤分为两步:
通过某种哈希算法计算出key的哈希值。
哈希值对数组长度取余,获取key对应的数组索引。
index = hash(key) % capacity
例如我们使用一个简单的哈希算法 hash(key)=key 将客户id映射到一个长度为8的数组的索引,即 index = key % 8 。
常见的哈希算法:
通用哈希算法:除法哈希、乘法哈希、MurmurHash、CityHash。
加密哈希算法:MD5(已被成功攻击)、SHA-1(已被成功攻击)、SHA-2、SHA-3。
文件完整性检查算法:Adler-32、CRC32。
2)哈希冲突
哈希函数可能会将不同的键值映射到同一个索引位置,这就是所谓的哈希冲突。处理冲突的方式有多种,最常见的两种是链式法(Chaining)和开放寻址法(Open Addressing)。
(1)链式法
将发生碰撞的每个键值对作为一个节点(Node)组成一个链表(Linked List),然后将链表的头节点保存在数组的目标位置中。这样一来,向字典中写入数据时,若发现数组的目标位置已有数据,那么就将当前的键值对作为一个节点插入链表;从字典中读取数据时,则从数组的目标位置获取链表,并进行遍历,直到找到目标数据。
(2)开放寻址法
当发生冲突时根据某种探查策略寻找下一个空槽位。常见的探查策略包括:
线性探查(Linear Probing):如果当前位置已经被占用,就探查下一个位置。
二次探查(Quadratic Probing):以平方的步长进行探查。
双重哈希(Double Hashing):使用另一个哈希函数来计算新的索引。
3)负载因子
负载因子(Load Factor)是哈希表中元素个数与表的大小的比率。当负载因子过高时,可能需要进行扩容操作,以保持操作的效率。
较小的负载因子可以减少冲突的可能性,较大的负载因子可以提高哈希表的内存利用率。通常情况下负载因子在0.7~0.8是一个比较好的选择。
3.5.2 哈希表的功能定义
方法 说明
size() 返回哈希表中键值对个数
is_empty() 判断哈希表是否为空
put(key, value) 向哈希表插入键值对
remove(key) 从哈希表中根据键删除键值对
get(key) 从哈希表中根据键获取值
for_each(func) 遍历哈希表中的键值对
3.5.3 哈希表的实现
class Node:
def init(self, key, value):
self.key = key
self.value = value
self.next = None
class HashTable:
def __init__(self):"""初始化哈希表"""self.__capacity = 8 # 数组长度self.__size = 0 # 键值对个数self.__load_factor = 0.7 # 负载因子self.__table = [None] * self.__capacitydef display(self):"""显示哈希表内容"""for i, node in enumerate(self.__table):print(f"Index {i}: ", end="")current = nodewhile current:print(f"({current.key}, {current.value}) -> ", end="")current = current.nextprint("None")print()def __hash(self, key):"""哈希函数,根据key计算索引"""return hash(key) % self.__capacitydef __grow(self):"""哈希表负载因子超过阈值时进行扩容"""self.__capacity = self.__capacity * 2self.__table, old_table = [None] * self.__capacity, self.__tableself.__size = 0# 将旧哈希表中的元素重新插入到新的哈希表中for node in old_table:current = nodewhile current:self.put(current.key, current.value)current = current.next@property
def size(self):"""获取哈希表键值对个数"""return self.__sizedef is_empty(self):"""判断哈希表是否为空"""return self.__size == 0def put(self, key, value):"""插入键值对,处理哈希冲突"""# 如果负载因子超过阈值则进行扩容if self.__size / self.__capacity > self.__load_factor:self.__grow()index = self.__hash(key)new_node = Node(key, value)# 如果当前位置为空,直接插入if self.__table[index] is None:self.__table[index] = new_nodeelse:# 否则,发生哈希冲突,链式存储current = self.__table[index]while current and current.next:# 如果键已经存在,更新值if current.key == key:current.value = valuereturncurrent = current.next# 如果键不存在,插入到链表尾部current.next = new_nodeself.__size += 1def remove(self, key):"""删除键值对"""index = self.__hash(key)current = self.__table[index]prev = Nonewhile current:if current.key == key:if prev:# 删除非头节点prev.next = current.nextelse:# 删除头节点self.__table[index] = current.nextself.__size -= 1return Trueprev = currentcurrent = current.nextreturn Falsedef get(self, key):"""访问键值对"""index = self.__hash(key)current = self.__table[index]while current:if current.key == key:return current.valuecurrent = current.nextreturn Nonedef for_each(self, func):"""遍历哈希表"""for node in self.__table:current = nodewhile current:func(current.key, current.value)current = current.next
3.6 树
3.6.1 树的概述
树(Tree)由一系列具有层次关系的节点(Node)组成。
树的常见术语:
父节点:节点的上层节点。
子节点:节点的下层节点。
根节点:位于树的顶端,没有父节点的节点。
叶节点:位于树的底端,没有子节点的节点。
边:连接两个节点的线段。
节点的度:节点的子节点数量。
节点的层:从根开始定义起,根为第1层,根的子节点为第2层,以此类推。
节点的深度:从根节点到该节点所经过的边的数量,根的深度为0。
节点的高度:从距离该节点最远的叶节点到该节点所经过的边的数量,所有叶节点的高度为0。
树的深度(高度):从根节点到最远叶节点所经过的边的数量。
3.6.2 二叉树简介
树形结构中最具代表性的一种就是二叉树(Binary Tree)。二叉树规定,每个节点最多只能有两个子节点,两个子节点分别被称为左子节点和右子节点。以左子节点为根节点的子树被称为左子树,以右子节点为根节点的子树被称为右子树。
3.6.3 二叉树存储结构
1)二叉树的数组存储
采用数组结构存储二叉树,访问与遍历速度较快。但不适合存储数据量过大的树,且增删效率较低,而且树中存在大量None的情况下空间利用率较低,因此不是主流方式。
2)二叉树的链表存储
3.6.4 常见的二叉树
1)完全二叉树
完全二叉树只有最下面一层的节点未被填满,且靠左填充。
2)满二叉树
满二叉树所有层的节点都被完全填满,满二叉树也是一种完全二叉树。
3)平衡二叉树
平衡二叉树中任意节点的左右子树高度之差不超过1。
4)二叉搜索树
二叉搜索树中的每个节点的值,大于其左子树中的所有节点的值,并且小于右子树中的所有节点的值。
5)AVL树
AVL 树是一种自平衡的二叉搜索树,插入和删除时会进行旋转操作来保证树的平衡性。
6)红黑树
红黑树是一种特殊的二叉搜索树,除了二叉搜索树的要求外,它还具有以下特性:
每个节点或者是黑色,或者是红色。
根节点是黑色。
每个叶节点都是黑色。这里叶节点是指为空(None)的节点。
红色节点的两个子节点必须是黑色的。即从每个叶到根的所有路径上不能有两个连续的红色节点。
从任一个节点到其每个叶的所有路径上包含相同数目的黑色节点。
7)堆
堆(Heap)是一种满足特定条件的完全二叉树,主要可分为两种类型:
大顶堆:每个父节点的值都大于等于其子节点的值。根节点为树中的最大值。
小顶堆:每个父节点的值都小于等于其子节点的值。根节点为树中的最小值。
8)霍夫曼树
霍夫曼树又称最优二叉树,是一种带权路径长度最短的二叉树,通常用于数据压缩,它的构建基于字符出现频率的概率。
9)B树
B树是一种自平衡的多路查找树。虽然它不是严格意义上的二叉树,但与二叉树的结构类似。经常用于数据库、文件系统等需要磁盘访问的应用。
10)B+树
B+树是B树的优化版本。它通过将数据集中存储在叶子节点并通过链表连接来实现高效的范围查询,并且非叶子节点仅存储索引,提高了磁盘利用率。
3.6.5 二叉搜索树的功能定义
方法 说明
size() 返回树中节点个数
is_empty() 判断树是否为空
search(item) 查找节点是否存在
add(item) 向二叉搜索树中插入节点
remove(item) 从二叉搜索树中删除节点
for_each(func, order) 按指定方式遍历二叉树
3.6.6 二叉树的创建
from collections import deque
class Node:
“”“二叉树节点”“”
def __init__(self, data):self.data = dataself.left = Noneself.right = None
class BinarySearchTree:
“”“二叉搜索树”“”
def __init__(self):"""初始化二叉树"""self.__root = Noneself.__size = 0def print_tree(self):"""打印树的结构"""# 先得到树的层数def get_layer(node):"""递归计算树的层数"""if node is None:return 0else:left_depth = get_layer(node.left)right_depth = get_layer(node.right)return max(left_depth, right_depth) + 1layer = get_layer(self.__root)# 层序遍历并打印queue = deque([(self.__root, 1)])current_level = 1while queue:node, level = queue.popleft()if level > current_level:print()current_level += 1if node:print(f"{node.data:^{20*layer//2**(level-1)}}", end="")else:print(f"{"N":^{20*layer//2**(level-1)}}", end="")if level < layer:if node:queue.append((node.left, level + 1))queue.append((node.right, level + 1))else:queue.append((None, level + 1))queue.append((None, level + 1))print()@property
def size(self):"""返回树中节点的个数"""return self.__sizedef is_empty(self):"""判断树是否为空"""return self.__size == 0
3.6.7 二叉搜索树的查找操作
查找时先与当前节点比较大小,等于则找到了目标节点,小于则向左子节点查找,大于则向右子节点查找。如果查找到None仍未找到则说明该节点不在树中。
后续插入与删除操作也会用到查找,所以此处提供一个__search_pos()方法,返回查找到的节点和其父节点供后续使用。
def search(self, item):
“”“查找节点是否存在”“”
return self.__search_pos(item)[0] is not None
def __search_pos(self, item):"""查找节点,返回(节点,父节点)。如果节点不存在则为None,此时父节点为一个叶节点"""parent = Nonecurrent = self.__rootwhile current:if item == current.data:breakparent = currentcurrent = current.left if item < current.data else current.rightreturn current, parent
3.6.8 二叉搜索树的插入操作
插入时先执行查找操作,查找时保存当前节点的父节点。如果找到了节点则说明树中已有此元素,退出。如果找到了None,此时None的父节点为叶节点,应将该元素插入该叶节点的子节点。
def add(self, item):"""插入节点"""node = Node(item)if self.is_empty():self.__root = nodeelse:current, parent = self.__search_pos(item)# 如果节点之前已存在则返回if current:return# 如果节点之前不存在,则插入父节点的左节点或右节点if parent.data > item:parent.left = nodeelse:parent.right = nodeself.__size += 1
3.6.9 二叉搜索树的删除操作
需要保证删除节点后仍然保证二叉搜索树的性质。删除操作需要根据目标节点的子节点数量为0、1、2分三种情况。
1)目标节点的子节点数量为0
直接删除目标节点。
2)目标节点的子节点数量为1
将目标节点替换为其子节点。
3)目标节点的子节点数量为2
使用目标节点的右子树最小节点、或左子树最大节点替换目标节点。
4)代码实现
def remove(self, item):
“”“删除节点”“”
current, parent = self.__search_pos(item)
if not current:
return
# 如果删除的是叶节点(没有子节点)if not current.left and not current.right:if parent:if parent.left == current:parent.left = Noneelse:parent.right = Noneelse:# 如果没有父节点,说明是根节点self.__root = None# 如果删除的节点只有一个子节点elif not current.left or not current.right:child = current.left if current.left else current.rightif parent:if parent.left == current:parent.left = childelse:parent.right = childelse:# 如果没有父节点,说明是根节点self.__root = child# 如果删除的节点有两个子节点else:# 找到中序后继(右子树中最小的节点)successor = self.__get_min(current.right)successor_data = successor.data# 删除中序后继节点self.remove(successor_data)# 用中序后继的值替代当前节点current.data = successor_dataself.__size -= 1def __get_min(self, node):"""找到当前子树的最小节点"""current = nodewhile current.left:current = current.leftreturn current
3.6.10 二叉树的遍历
1)深度优先
深度优先搜索(DFS,Depth First Search)尽可能地深入每一个分支,直到不能再深入为止,然后回溯到上一个节点,继续尝试其他的分支。
(1)前序遍历
先访问当前节点,再访问节点的左子树,再访问节点的右子树。
def dfs(node):
“”“前序遍历”“”
if node is None:
return
print(node) # 访问当前节点
dfs(node.left) # 访问节点的左子树
dfs(node.right) # 访问节点的右子树
(2)中序遍历
先访问节点的左子树,再访问当前节点,再访问节点的右子树。
二叉搜索树中序遍历的结果是有序的。
def dfs(node):
“”“中序遍历”“”
if node is None:
return
dfs(node.left) # 访问节点的左子树
print(node) # 访问当前节点
dfs(node.right) # 访问节点的右子树
(3)后续遍历
先访问节点的左子树,再访问节点的右子树,再访问当前节点。
def dfs(node):
“”“后序遍历”“”
if node is None:
return
dfs(node.left) # 访问节点的左子树
dfs(node.right) # 访问节点的右子树
print(node) # 访问当前节点
2)广度优先
(1)层序遍历
广度优先搜索(BFS,Breadth First Search)从起始节点开始,首先访问该节点的所有子节点,然后再访问子节点的子节点,依此类推,逐层访问节点。
广度优先搜索一般使用队列实现,每访问一个节点,就将该节点的子节点添加进队列中。
3)代码实现
def for_each(self, func, order=“inorder”):
“”“遍历树,默认中序遍历”“”
match order:
case “inorder”:
self.__inorder_traversal(func)
case “preorder”:
self.__preorder_traversal(func)
case “postorder”:
self.__postorder_traversal(func)
case “levelorder”:
self.__levelorder_traversal(func)
def __inorder_traversal(self, func):"""深度优先搜索:中序遍历"""def inorder(node):if node:inorder(node.left)func(node.data)inorder(node.right)inorder(self.__root)def __preorder_traversal(self, func):"""深度优先搜索:前序遍历"""def preorder(node):if node:func(node.data)preorder(node.left)preorder(node.right)preorder(self.__root)def __postorder_traversal(self, func):"""深度优先搜索:后序遍历"""def postorder(node):if node:postorder(node.left)postorder(node.right)func(node.data)postorder(self.__root)def __levelorder_traversal(self, func):"""广度优先搜索:层序遍历"""queue = deque()queue.append(self.__root)while queue:node = queue.popleft()func(node.data)if node.left:queue.append(node.left)if node.right:queue.append(node.right)
3.6.11 完整代码
from collections import deque
class Node:
“”“二叉树节点”“”
def __init__(self, data):self.data = dataself.left = Noneself.right = None
class BinarySearchTree:
“”“二叉搜索树”“”
def __init__(self):"""初始化二叉树"""self.__root = Noneself.__size = 0def print_tree(self):"""打印树的结构"""# 先得到树的层数def get_layer(node):"""递归计算树的层数"""if node is None:return 0else:left_depth = get_layer(node.left)right_depth = get_layer(node.right)return max(left_depth, right_depth) + 1layer = get_layer(self.__root)# 层序遍历并打印queue = deque([(self.__root, 1)])current_level = 1while queue:node, level = queue.popleft()if level > current_level:print()current_level += 1if node:print(f"{node.data:^{20*layer//2**(level-1)}}", end="")else:print(f"{"N":^{20*layer//2**(level-1)}}", end="")if level < layer:if node:queue.append((node.left, level + 1))queue.append((node.right, level + 1))else:queue.append((None, level + 1))queue.append((None, level + 1))print()@property
def size(self):"""返回树中节点的个数"""return self.__sizedef is_empty(self):"""判断树是否为空"""return self.__size == 0def search(self, item):"""查找节点是否存在"""return self.__search_pos(item)[0] is not Nonedef __search_pos(self, item):"""查找节点,返回(节点,父节点)。如果节点不存在则为None,此时父节点为一个叶节点"""parent = Nonecurrent = self.__rootwhile current:if item == current.data:breakparent = currentcurrent = current.left if item < current.data else current.rightreturn current, parentdef add(self, item):"""插入节点"""node = Node(item)if self.is_empty():self.__root = nodeelse:current, parent = self.__search_pos(item)# 如果节点之前已存在则返回if current:return# 如果节点之前不存在,则插入父节点的左节点或右节点if parent.data > item:parent.left = nodeelse:parent.right = nodeself.__size += 1def remove(self, item):"""删除节点"""current, parent = self.__search_pos(item)if not current:return# 如果删除的是叶节点(没有子节点)if not current.left and not current.right:if parent:if parent.left == current:parent.left = Noneelse:parent.right = Noneelse:# 如果没有父节点,说明是根节点self.__root = None# 如果删除的节点只有一个子节点elif not current.left or not current.right:child = current.left if current.left else current.rightif parent:if parent.left == current:parent.left = childelse:parent.right = childelse:# 如果没有父节点,说明是根节点self.__root = child# 如果删除的节点有两个子节点else:# 找到中序后继(右子树中最小的节点)successor = self.__get_min(current.right)successor_data = successor.data# 删除中序后继节点self.remove(successor_data)# 用中序后继的值替代当前节点current.data = successor_dataself.__size -= 1def __get_min(self, node):"""找到当前子树的最小节点"""current = nodewhile current.left:current = current.leftreturn currentdef for_each(self, func, order="inorder"):"""遍历树,默认中序遍历"""match order:case "inorder":self.__inorder_traversal(func)case "preorder":self.__preorder_traversal(func)case "postorder":self.__postorder_traversal(func)case "levelorder":self.__levelorder_traversal(func)def __inorder_traversal(self, func):"""深度优先搜索:中序遍历"""def inorder(node):if node:inorder(node.left)func(node.data)inorder(node.right)inorder(self.__root)def __preorder_traversal(self, func):"""深度优先搜索:前序遍历"""def preorder(node):if node:func(node.data)preorder(node.left)preorder(node.right)preorder(self.__root)def __postorder_traversal(self, func):"""深度优先搜索:后序遍历"""def postorder(node):if node:postorder(node.left)postorder(node.right)func(node.data)postorder(self.__root)def __levelorder_traversal(self, func):"""广度优先搜索:层序遍历"""queue = deque()queue.append(self.__root)while queue:node = queue.popleft()func(node.data)if node.left:queue.append(node.left)if node.right:queue.append(node.right)
3.7 图
3.7.1 图的概述
前面我们学习了线性结构和树,线性结构局限于只有一个直接前驱和一个直接后继的关系,树也只能有一个直接前驱,也就是父节点,当我们需要表示多对多的关系时,就需要用到图了,图是比树更普遍的结构,可以认为树是一种特殊的图。图由节点和边组成。
图的常见术语:
节点:也称为顶点,是图的基础部分。
边:连接两个节点,也是图的基础部分。可以是单向的,也可以是双向的。
权重:边可以添加“权重”变量。
邻接:两节点之间存在边,则称这两个节点邻接。
度:一个节点的边的数量。入度为指向该节点的边的数量,出度为该节点指向其他节点的边的数量。
路径:从一节点到另一节点所经过的边的序列。
环:首尾节点相同的路径。
3.7.2 图的分类
1)有向图和无向图
有向图:边是单向的。
无向图:边是双向的。
2)连通图和非连通图
连通图:从某个节点出发,可以到达其余任意节点。
非连通图:从某个节点出发,有节点不可达。
3.7.3 图的常用表示法
1)邻接矩阵
邻接矩阵用一个n×n的矩阵来表示有n个节点之间的关系,矩阵的每一行(列)代表一个节点,矩阵m行n列的值代表是否存在由m指向n的边。邻接矩阵适合存储稠密图。
2)邻接表
邻接表存储n个链表、列表或其他容器,每个容器存储该节点的所有邻接节点。邻接表适合存储稀疏图,空间效率高,尤其在处理边远少于节点的图时表现优越,但在进行边查找时不如邻接矩阵高效。
3.7.4 图的遍历
1)广度优先搜索
广度优先搜索(BFS,Breadth First Search)从起始节点开始,首先访问该节点的所有邻接节点,然后再访问邻接节点的邻接节点,依此类推,逐层访问节点。
从图的起始节点开始,首先访问该节点,并标记为已访问。
然后依次访问所有未被访问的邻接节点,并将它们加入到队列中。
当队列中的节点被访问时,继续访问它的邻接节点,并将新的节点加入队列。
直到队列为空,表示所有节点都已被访问。
2)深度优先搜索
深度优先搜索(DFS,Depth First Search)尽可能地深入到图的每一个分支,直到不能再深入为止,然后回溯到上一个节点,继续尝试其他的分支。
从图的一个起始节点开始,访问这个节点并标记为已访问。
对于每个未访问的邻接节点,递归地执行 DFS,直到没有未访问的邻接节点。
当回溯到一个节点时,继续访问它的其他邻接节点。
第 4 章 常用算法
4.1 查找算法
4.1.1 二分查找
1)算法原理
二分查找又称折半查找,适用于有序列表。其利用数据的有序性,每轮缩小一半搜索范围,直至找到目标元素或搜索区间为空为止。
2)代码实现
def binary_search(arr, target):
left, right = 0, len(arr) - 1
while left <= right:mid = left + (right - left) // 2if arr[mid] == target:return mid # 找到目标值,返回索引elif arr[mid] < target:left = mid + 1 # 目标值在右半部分else:right = mid - 1 # 目标值在左半部分return -1 # 未找到目标值
3)复杂度分析
(1)时间复杂度
在循环中,区间每轮缩小一半,因此时间复杂度为O(logn)。
(2)空间复杂度
使用常数大小的额外空间,空间复杂度为O(1)。
4.1.2 查找多数元素
力扣169题https://leetcode.cn/problems/majority-element/description/
返回数组中数量超过半数的元素,要求时间复杂度O(n)、空间复杂度O(1)。
示例:
输入:nums = [2,2,1,1,1,2,2]
输出:2
1)思路分析
为了严格符合复杂度要求,可以使用多数投票算法,多数投票算法也叫摩尔投票算法。摩尔投票算法的核心思想是对立性和抵消,它基于这样一个事实:如果一个元素在数组中出现的次数超过数组长度的一半,那么在不断消除不同元素对的过程中,这个多数元素最终会留下来。
具体来说,算法维护两个变量:一个是候选元素 candidate,另一个是该候选元素的计数 count。在遍历数组的过程中,遇到与候选元素相同的元素时,计数加 1;遇到不同的元素时,计数减 1。当计数减为 0 时,说明当前候选元素被抵消完,需要更换候选元素为当前遍历到的元素,并将计数重置为 1。
算法步骤
初始化:
选择数组的第一个元素作为初始候选元素 candidate。
将计数 count 初始化为 1。
遍历数组:
从数组的第二个元素开始遍历。
若当前元素与候选元素相同,count 加 1。
若当前元素与候选元素不同,count 减 1。
当 count 变为 0 时,将当前元素设为新的候选元素,并将 count 重置为 1。
返回结果:
遍历结束后,candidate 即为多数元素。
代码实现
def majorityElement(nums):
# 初始化候选元素为数组的第一个元素
candidate = nums[0]
# 初始化候选元素的票数为 1
count = 1
# 从数组的第二个元素开始遍历
for num in nums[1:]:
if num == candidate:
# 如果当前元素与候选元素相同,票数加 1
count += 1
else:
# 如果当前元素与候选元素不同,票数减 1
count -= 1
if count == 0:
# 当票数为 0 时,更新候选元素为当前元素,并将票数重置为 1
candidate = num
count = 1
return candidate
测试示例
nums = [2, 2, 1, 1, 1, 2, 2]
print(majorityElement(nums))