AI大模型从0到1记录学习 数据结构和算法 day18

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))

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/76914.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

粘性定位(position:sticky)——微信小程序学习笔记

1. 简介 CSS 中的粘性定位&#xff08;Sticky positioning&#xff09;是一种特殊的定位方式&#xff0c;它可以使元素在滚动时保持在视窗的特定位置&#xff0c;类似于相对定位&#xff08;relative&#xff09;&#xff0c;但当页面滚动到元素的位置时&#xff0c;它会表现得…

通过使用 include 语句加载并执行一个CMake脚本来引入第三方库

通过使用 include 语句加载并执行一个CMake脚本来引入第三方库 当项目中使用到第三方库时&#xff0c;可以通过使用 include 语句来加载并执行一个CMake脚本&#xff0c;在引入的CMake脚本中进行第三方库的下载、构建和库查找路径的设置等操作&#xff0c;以这种方式简化项目中…

DNS正反向解析复习,DNS主从服务,转发服务及DNS和nginx联合案例

正向解析 1、配置主机名 [rootlocalhost ~]# dnf install bash-completion -y #一个按tap键补全的软件 [rootlocalhost ~]# hostnamectl hostname dns #改主机名为dns [rootlocalhost ~]# exit ssh root你的IP地址 要重启才会生效2、安装bind [rootdns ~]# dnf install b…

网络安全·第一天·IP协议安全分析

本篇博客讲述的是网络安全中一些协议缺陷以及相应的理论知识&#xff0c;本博主尽可能讲明白其中的一些原理以及对应的防卫措施。 学习考研408的同学也能进来看看&#xff0c;或许对考研有些许帮助&#xff08;按照考研现在的趋势&#xff0c;年年都有新题目&#xff0c;本文当…

【详解】Nginx配置WebSocket

目录 Nginx配置WebSocket 简介 准备工作 检查 Nginx 版本 配置 Nginx 支持 WebSocket 修改 Nginx 配置文件 解释配置项 测试配置 测试 WebSocket 连接 WebSocket 服务端 (Node.js) WebSocket 客户端 (HTML JavaScript) 运行测试 Nginx 配置文件示例 解释 测试配…

《轨道力学讲义》——第八讲:行星际轨道设计

第八讲&#xff1a;行星际轨道设计 引言 行星际轨道设计是探索太阳系的核心技术&#xff0c;它涉及如何规划和优化航天器从一个天体到另一个天体的飞行路径。随着人类探索太阳系的雄心不断扩大&#xff0c;从最初的月球探测到火星探测&#xff0c;再到更遥远的外太阳系探测&a…

操作系统学习笔记——[特殊字符]超详细 | 如何唤醒被阻塞的 socket 线程?线程阻塞原理、线程池、fork/vfork彻底讲明白!

&#x1f4a1;超详细 | 如何唤醒被阻塞的 socket 线程&#xff1f;线程阻塞原理、线程池、fork/vfork彻底讲明白&#xff01; 一、什么是阻塞&#xff1f;为什么线程会阻塞&#xff1f;二、socket线程被阻塞的典型场景&#x1f9e0; 解法思路&#xff1a; 三、线程的几种阻塞状…

第十六届蓝桥杯大赛软件赛省赛 Python 大学 B 组 满分题解

题面链接Htlang/2025lqb_python_b 个人觉得今年这套题整体比往年要简单许多&#xff0c;但是G题想简单了出大问题&#xff0c;预估50101015120860&#xff0c;道阻且长&#xff0c;再接再厉 代码仅供学习参考&#xff0c;满分为赛后洛谷中的测评&#xff0c;蓝桥杯官方测评待…

若依代码生成器原理velocity模板引擎(自用)

1.源码分析 代码生成器:导入表结构(预览、编辑、删除、同步)、生成前后端代码 代码生成器表结构说明&#xff1a; 若依提供了两张核心表来存储导入的业务表信息&#xff1a; gen_table&#xff1a;存储业务表的基本信息 &#xff0c;它对应于配置代码基本信息和生成信息的页…

如何制定有效的风险应对计划

制定有效的风险应对计划的核心在于&#xff1a; 识别潜在风险、评估风险的影响与概率、选择合适的应对策略、建立动态监控和反馈机制。 其中&#xff0c;识别潜在风险是最为关键的第一步。只有准确识别出可能的风险&#xff0c;才能在后续的评估、应对、监控等环节中做到有的放…

A2A协议实现详解及示例

A2A协议概述 A2A (Agent2Agent) 是Google推出的一个开放协议&#xff0c;旨在使AI智能体能够安全地相互通信和协作。该协议打破了孤立智能体系统之间的壁垒&#xff0c;实现了复杂的跨应用自动化。[1] A2A协议的核心目标是让不同的AI代理能够相互通信、安全地交换信息以及在各…

【中级软件设计师】前趋图 (附软考真题)

【中级软件设计师】前趋图 (附软考真题) 目录 【中级软件设计师】前趋图 (附软考真题)一、历年真题三、真题的答案与解析答案解析 复习技巧&#xff1a; 若已掌握【前趋图】相关知识&#xff0c;可直接刷以下真题&#xff1b; 若对知识一知半解&#xff0c;建议略读题目&#x…

调节磁盘和CPU的矛盾——InnoDB的Buffer Pool

缓存的重要性 无论是用于存储用户数据的索引【聚簇索引、二级索引】还是各种系统数据&#xff0c;都是以页的形式存放在表空间中【对一个/几个实际文件的抽象&#xff0c;存储在磁盘上】如果需要访问某页的数据&#xff0c;就会把完整的页数据加载到内存中【即使只访问页中的一…

springboot和springcloud的区别

1. ‌目的与功能‌ ‌1)Spring Boot‌: 主要用于快速构建独立的、生产级的 Spring 应用程序。它通过自动配置和嵌入式服务器等特性,简化了微服务的开发、启动和部署,使开发者能够专注于业务逻辑而非繁琐的配置。‌Spring Boot是一个快速开发的框架,旨在简化Java应用程序的开…

耘想WinNAS:以聊天交互重构NAS生态,开启AI时代的存储革命

一、传统NAS的交互困境与范式瓶颈 在传统NAS&#xff08;网络附加存储&#xff09;领域&#xff0c;用户需通过复杂的图形界面或命令行工具完成文件管理、权限配置、数据检索等操作&#xff0c;学习成本高且效率低下。例如&#xff0c;用户若需搜索特定文件&#xff0c;需手动…

在断网的时候,websocket 一直在CLOSING 状态

现象 websocket 先连接成功&#xff0c;然后断网。 由于维护了一套心跳机制&#xff0c;前端发起心跳&#xff0c;如果一段时间内没有收到服务端返回的心跳。则表示连接断开。 用心跳的方式处理断网的兜底情况。 然而&#xff0c;此时网络是断开的&#xff0c;在代码中直接调…

基于AWS的大模型调用场景:10大成本优化实战方案

大模型训练与推理是AI领域的计算密集型场景&#xff0c;如何在AWS上实现高性能与低成本的双重目标&#xff1f;本文从实例选型、弹性伸缩、存储优化等角度&#xff0c;分享10个经过验证的AWS成本优化策略&#xff0c;帮助企业节省30%以上成本。 一、大模型场景的成本痛点分析 计…

【网络原理】TCP/IP协议五层模型

目录 一. 协议的分层 二. OSI七层网络协议 三. TCP/IP五层网络协议 四. 网络设备所在分层 五. 封装 六. 分用 七. 传输中的封装和分用 八. 数据单位术语 一. 协议的分层 常见的分层为两种OSI七层模型和TCP/IP五层模型 为什么要协议分层&#xff1f; 在网络通信中&…

科技快讯 | 阿里云百炼MCP服务上线;英伟达官宣:CUDA 工具链将全面原生支持 Python

李飞飞团队最新AI报告&#xff1a;中美模型性能差距近乎持平 4月8日&#xff0c;斯坦福大学以人为本人工智能研究所发布《2025年人工智能指数报告》。报告显示&#xff0c;2023年AI性能显著提升&#xff0c;AI应用加速&#xff0c;投资增长&#xff0c;中美AI模型差距缩小。报告…

猫咪如厕检测与分类识别系统系列【三】融合yolov11目标检测

✅ 前情提要 家里养了三只猫咪&#xff0c;其中一只布偶猫经常出入厕所。但因为平时忙于学业&#xff0c;没法时刻关注牠的行为。我知道猫咪的如厕频率和时长与健康状况密切相关&#xff0c;频繁如厕可能是泌尿问题&#xff0c;停留过久也可能是便秘或不适。为了更科学地了解牠…