[学习笔记] 2. 数据结构
视频地址:https://www.bilibili.com/video/BV1uA411N7c5
数据结构是指相互之间存在着一种或多种关系的数据元素的集合和该集合中数据元素之间的关系组成。简单来说,数据结构就是设计数据以何种方式组织并存储在计算机中。
比如:列表、集合与字典等都是一种数据结构。
N.Wirth: “程序 = 数据结构 + 算法”
数据结构按照其逻辑结构可以分为:①线性结构;②树结构;③图结构。
- 线性结构:数据结构中的元素存在 一对一 的相互关系
- 树结构:数据结构中的元素存在 一对多 的相互关系
- 图结构:数据结构中的元素存在 多对多 的相互关系
1. 列表/数组
列表(其他语言成为数组)是一种基本数据类型。
关于列表的问题:
- 列表中的元素是如何存储的?
- 列表的基本操作:按下标(索引)查找、插入元素、删除元素…这些操作的时间复杂度是多少?
- 查找:O(1)O(1)O(1)
- 插入:O(n)O(n)O(n)
- 删除:O(n)O(n)O(n)
append
:O(1)O(1)O(1)
扩展:Python的列表是如何实现的?
数组与列表有两点不同:
- 数组元素类型相同
- 数组长度固定
2. 栈(stack)
栈(Stack)是一个数据集合,可以理解为只能在一端进行插入或删除操作的列表。
- 栈的特点:先进后出,后进先出LIFO(Last-In, First-Out)
- 栈的概念:栈顶、栈底
- 栈的基本操作:
- 进栈(压栈):
push
- 出栈:
pop
- 取栈顶:
gettop
- 进栈(压栈):
2.1 栈的实现
使用一般的列表结构(list
)即可实现栈。
- 进栈:
ls.append
- 出栈:
ls.pop
- 取栈顶:
ls[-1]
2.2 栈的基本实现代码
class Stack:def __init__(self):self.stack = []def push(self, elem):self.stack.append(elem)def pop(self):if self.stack:return self.stack.pop()else:raise "Empty Value"def gettop(self):if self.stack:return self.stack[-1]else:return Noneif __name__ == "__main__":stack = Stack()stack.push(1)stack.push(2)stack.push(3)print(stack.pop())print(stack.gettop())
2.3 栈的应用 —— 括号匹配问题
括号匹配问题:给一个字符串,其中包含小括号、中括号和大括号,求该字符串中的括号是否匹配。
例如:
()()[]{} 匹配
([{()}]) 匹配
[]( 不匹配
[(]) 不匹配
思路:有左括号先放到栈中,看下一个;如果是右括号,那么看和栈顶是否匹配,如果匹配,出栈。看完所有元素后,如果栈是空栈,那么说明是合法的,如果不为空,那么是非法的。
特殊情况:如果直接来了一个右括号,直接非法!
代码:
class Stack():def __init__(self):self.stack = []def push(self, elem):self.stack.append(elem)def pop(self):if self.stack:return self.stack.pop()else:raise "Empty Value"def gettop(self):if self.stack:return self.stack[-1]else:raise "Empty Value"def is_empty(self):return len(self.stack) == 0def brace_match(s):match_dict = {')': '(',']': '[','}': '{'}stack = Stack()for char in s:if char in "([{":stack.push(char)elif char in ")]}":if stack.is_empty():return Falseelif stack.gettop() == match_dict[char]:stack.pop()else: # 不匹配return Falseelse:raise "Illegal Value"# 看一下栈里是否有元素if stack.is_empty():return Trueelse:return Falseif __name__ == "__main__":test_1 = "()()[]{}"test_2 = "([{()}])"test_3 = "[]("test_4 = "[(])"print(brace_match(test_1))print(brace_match(test_2))print(brace_match(test_3))print(brace_match(test_4))"""TrueTrueFalseFalse
"""
3. 队列(Queue)
队列(Queue)是一个数据集合,仅允许在列表的一端进行插入,另一端进行删除。
- 进行插入的一端称为队尾(rear),插入动作称为进队或入队
- 进行删除的一端称为对头(front),删除动作称为出队
- 队列的性质:先进先出FIFO(First-In, First-Out)
3.1 队列的实现
队列是否能用列表简单实现?为什么?
列表不好实现,出队3次以后,再入队就没法入队了。但如果把这个队列的头和尾连起来,这个问题就解决了!
3.2 队列的实现方式 —— 环形队列
Q:队满的时候为什么有一个位置是空的?
A:为了判断这个队是空队还是满队:
- 空队列:
front == rear
- 满队列:
front == rear + 1
Q:rear
进队到11
时,怎么变为0
?
A:使用%
-> rear = rear % len(queue)
-> rear %= 12
Q:那么front
呢?
A:也是一样的,也是front %= len(queue)
环形队列:当队尾指针front = MaxSize - 1
时,再前进一个位置就自动到0
。
- 队首指针前进1:
front = (front + 1) % MaxSize
- 队尾指针前进1:
rear = (rear + 1) % MaxSize
- 队空条件:
rear == front
- 队满条件:
(rear + 1) % MaxSize == front
MaxSize
为队列的长度
3.3 队列的代码实现
class Queue:def __init__(self, size=100):# 队列的长度需要固定self.size = size # 队列的长度self.queue = [0 for i in range(size)]self.rear = 0 # 队尾(进队)self.front = 0 # 队首(出队)# 入队def push(self, elem):if not self.is_filled():self.rear = (self.rear + 1) % self.sizeself.queue[self.rear] = elem # 进队else:raise "Filled Queue Error"# 出队def pop(self):if not self.is_empty():self.front = (self.front + 1) % self.size# 这里不删除旧的元素了,因为环形的,有值后会覆盖它!return self.queue[self.front]else:raise "No Value Error"# 判断队空def is_empty(self):if self.rear == self.front:return Trueelse:return False# 判断队满def is_filled(self):if (self.rear + 1) % self.size == self.front:return Trueelse:return False# 获取队尾def getrear(self):return self.queue[self.rear]# 获取队首def getfront(self):return self.queue[(self.front + 1) % 12]if __name__ == "__main__":q = Queue(5)for i in range(4):q.push(i)print(q.is_filled())print(q.pop())print(q.is_empty())print(q.getfront())print(q.getrear())
3.4 双向队列
双向队列的两端都支持进队和出队操作,其基本操作如下:
- 队首进队
- 队首出队
- 队尾进队
- 队尾出队
3.5 Python队列内置模块
使用方法from collections import deque
,具体操作如下:
- 创建队列:
queue = deque()
- 进队:
append()
- 出队:
popleft()
- 双向队列队首进队:
appendleft()
- 双向队列队尾出队:
pop()
注意:这里并不是
import queue
模块,因为queue
模块一般用在多线程、多进程中,用来保持线程安全的。如果我们想要解题,那么我们就使用from collections import deque
deque
: 全称dequeue
,de表示双向,即双向队列
deque
是从左到右队首和堆尾!
一般情况下,单向队列用的比较多
from collections import dequeq = deque()# 单向队列
q.append(1) # 队尾进队
print(q.popleft()) # 队首出队
# print(q.popleft()) # IndexError: pop from an empty deque# 双向队列
q.appendleft(2) # 队首进队
print(q.pop())
值得注意的是,deque()
在创建的时候是可以传入值的,如下:
from collections import deque# 不传入值
q1 = deque()
q1.append(1)
q1.append((1, 2, 3))
q1.append([1, 2, 3])
print(q1) # deque([1, (1, 2, 3), [1, 2, 3]])
q1.popleft()
q1.popleft()
print(q1) # deque([[1, 2, 3]])# 传入值
q2 = deque([1, 2, 3])
print(q2) # deque([1, 2, 3])
q2.append(4)
print(q2) # deque([1, 2, 3, 4])
q2.append([5, 6, 7]) # deque([1, 2, 3, 4, [5, 6, 7]])
print(q2)# 传入值和最大值
q3 = deque([1, 2, 3], maxlen=5)
print(q3) # deque([1, 2, 3], maxlen=5)
q3.append(4)
q3.append(5)
print(q3) # deque([1, 2, 3, 4, 5], maxlen=5)
q3.append(6)
print(q3) # deque([2, 3, 4, 5, 6], maxlen=5) -> 自动将1pop掉了
3.6 使用deque
实现Linux的tail
命令
Linux的tail
命令即在terminal中输出文本文件的后五行。
from collections import dequedef tail(n):with open('test.txt', 'r') as f:q = deque(f, n)return qif __name__ == "__main__":# print(tail(5)) # deque(['gdfsfdgh567u65g\n', 'dsfgfdgh456t24t\n', 'sdgfstg453rt234t2\n', 'gdfs344534y45\n', 'qqqfdu6574'], maxlen=5)for line in tail(5):print(line, end="")"""gdfsfdgh567u65gdsfgfdgh456t24tsdgfstg453rt234t2gdfs344534y45qqqfdu6574
"""
3.7 栈和队列的应用 —— 迷宫问题
给一个二维列表,表示迷宫(0表示通道,1表示围墙)。给出算法,求一条走出迷宫的路径。
maze
: 英[meɪz] 美[meɪz]
n. 迷宫; 纷繁复杂的规则; 复杂难懂的细节; 迷宫图;
vt. 使困惑; 使混乱; 迷失;
3.7.1 方法1:栈 —— 深度优先搜索
- 回溯法
- 思路:从一个节点开始,任意找下一个能走的点,当找不到能走的点时,退回上一个点寻找是否有其他方向的点。
- 使用栈存储当前路径。
def maze_path(maze, x1, y1, x2, y2):# 创建栈stack = []# 将起点放到栈中stack.append((x1, y1))# 定义四种寻找方式directions = [lambda x, y: (x + 1, y), # 上lambda x, y: (x - 1, y), # 下lambda x, y: (x, y - 1), # 左lambda x, y: (x, y + 1), # 右]# 当栈不为空,说明能继续找while len(stack) > 0:# 设定当前节点curNode = stack[-1]# 判断当前节点是否为终点if curNode[0] == x2 and curNode[1] == y2:print("Find Solution!")for path in stack:print(path, end="->")return Trueelse: # 如果不是终点,按照四个方向依次寻找for direction in directions:# 得到下一个节点nextNode = direction(curNode[0], curNode[1])# 判断下一个节点是否为0if maze[nextNode[0]][nextNode[1]] == 0: # 说明是下一个节点# 压栈stack.append(nextNode)# 进行标记以防止回退maze[nextNode[0]][nextNode[1]] = 2# 已经找到下一个节点,停止该节点的搜索breakelse: # 如果不是,继续搜索下一个方向passelse: # 如果四个方向都没有找到下一个节点stack.pop()else: # 栈为空print("No Solution!")return Falseif __name__ == "__main__":maze = [[1, 1, 1, 1, 1, 1, 1, 1, 1, 1], # 1[1, 0, 0, 1, 0, 0, 0, 1, 0, 1], # 2[1, 0, 0, 1, 0, 0, 0, 1, 0, 1], # 3[1, 0, 0, 0, 0, 1, 1, 0, 0, 1], # 4[1, 0, 1, 1, 1, 0, 0, 0, 0, 1], # 5[1, 0, 0, 0, 1, 0, 0, 0, 0, 1], # 6[1, 0, 1, 0, 0, 0, 1, 0, 0, 1], # 7[1, 0, 1, 1, 1, 0, 1, 1, 0, 1], # 8[1, 1, 0, 0, 0, 0, 0, 0, 0, 1], # 9[1, 1, 1, 1, 1, 1, 1, 1, 1, 1], # 10]maze_path(maze, 1, 1, 8, 8)"""
Find Solution!
(1, 1)->(2, 1)->(3, 1)->(4, 1)->(5, 1)->(5, 2)->(5, 3)->(6, 3)->(6, 4)->(6, 5)->(7, 5)->(8, 5)->(8, 6)->(8, 7)->(8, 8)->
"""
3.7.2 方法2:队列 —— 广度优先搜索
思路:从一个节点开始,寻找所有接下来能继续走的点,继续不断寻找,直到找到出口。
多种可能同时进行
- 使用队列存储当前正在考虑的节点。
from collections import dequedef print_path(path):curNode = path[-1] # path的最后一个元素就是终点real_path = [] # 存放真正的路径# 找到其他节点while curNode[2] != -1:real_path.append((curNode[0], curNode[1]))curNode = path[curNode[2]]else:real_path.append((curNode[0], curNode[1])) # 起点real_path.reverse()for node in real_path:print(node, end="->")def maze_path_queue(maze, x1, y1, x2, y2):# 创建队列queue = deque()# 存放起点queue.append((x1, y1, -1)) # x坐标,y坐标,哪个位置让它来的path = [] # 存放出队的节点# 定义四种寻找方式directions = [lambda x, y: (x + 1, y), # 上lambda x, y: (x - 1, y), # 下lambda x, y: (x, y - 1), # 左lambda x, y: (x, y + 1), # 右]while len(queue) > 0:curNode = queue.popleft() # 队首出栈path.append(curNode)# 判断curNode是否为终点if curNode[0] == x2 and curNode[1] == y2:print("Found Solution: ")print_path(path)return True# 搜索四个方向for direction in directions:nextNode = direction(curNode[0], curNode[1])# 判断nextNode是否能走通if maze[nextNode[0]][nextNode[1]] == 0:# 能走,进队,并记录哪个节点带它来的queue.append((nextNode[0], nextNode[1], len(path) - 1))# 标记为已走过maze[nextNode[0]][nextNode[1]] = 2else:print("No Solution")return Falseif __name__ == "__main__":maze = [[1, 1, 1, 1, 1, 1, 1, 1, 1, 1], # 1[1, 0, 0, 1, 0, 0, 0, 1, 0, 1], # 2[1, 0, 0, 1, 0, 0, 0, 1, 0, 1], # 3[1, 0, 0, 0, 0, 1, 1, 0, 0, 1], # 4[1, 0, 1, 1, 1, 0, 0, 0, 0, 1], # 5[1, 0, 0, 0, 1, 0, 0, 0, 0, 1], # 6[1, 0, 1, 0, 0, 0, 1, 0, 0, 1], # 7[1, 0, 1, 1, 1, 0, 1, 1, 0, 1], # 8[1, 1, 0, 0, 0, 0, 0, 0, 0, 1], # 9[1, 1, 1, 1, 1, 1, 1, 1, 1, 1], # 10]maze_path_queue(maze, 1, 1, 8, 8)"""
Found Solution:
(1, 1)->(2, 1)->(3, 1)->(4, 1)->(5, 1)->(5, 2)->(5, 3)->(6, 3)->(6, 4)->(6, 5)->(7, 5)->(8, 5)->(8, 6)->(8, 7)->(8, 8)->
"""
4. 链表(Linked List)
链表是由一系列节点组成的元素集合。每个节点包含两部分,数据域item和指向下一个节点的指针next。通过节点之间的相互连接,最终串联成一个链表。
class Node():def __init__(self, item):self.item = itemself.next = None
定义链表
class Node:def __init__(self, item):self.item = itemself.next = Noneif __name__ == "__main__":# 创建节点a = Node(1)b = Node(2)c = Node(3)# 将节点连起来a.next = bb.next = c# 通过a这个节点找到b和cprint(a.next.item) # 2print(a.next.next.item) # 3
4.1 链表的创建与遍历
上面创建链表的方式是手动的,这样太慢了。创建链表的方式有:头插法和尾插法。
- 头插法:在head前面插入,插入完毕后,head指向新的头
- 尾插法:在tail后面插入,插入完毕后,tail指向新的尾
class Node():def __init__(self, item):self.item = itemself.next = Nonedef create_linkedlist_head(ls):# 新建头节点head = Node(ls[0])for elem in ls[1:]:node = Node(elem)node.next = headhead = nodereturn headdef create_linkedlist_tail(ls):head = Node(ls[0])# 此时头节点和尾节点是同一指向tail = headfor elem in ls[1:]:# 为不同元素创建节点node = Node(elem)tail.next = nodetail = nodereturn headdef print_linklist(lk):while lk:print(lk.item, end=", ")lk = lk.nextif __name__ == '__main__':lk_head = create_linkedlist_head([1, 2, 3])print_linklist(lk_head) # 3, 2, 1,print("")lk_tail = create_linkedlist_tail([1, 2, 3, 4, 5])print_linklist(lk_tail) # 1, 2, 3, 4, 5,
4.2 链表节点的插入
数组插入需要挪动,再插入,这样的时间复杂度比较高(O(n)O(n)O(n)),而对于一个链表而言,先把需要插入的地方的链子解开,插入后连起来就行了。
p.next = curNode.next
curNode.next = p
4.3 链表节点的删除
p = curNode.nextcurNode.next = curNode.next.next # 写成curNode.next = p.next也行
del p # 其实删不删都可以
5. 双链表
双链表的每个节点有两个指针:一个指向后一个节点,另一个指向前一个节点。
class Node():def __init__(self, item=None):self.item = itemself.next = Noneself.prior = None
5.1 双链表节点的插入
p.next = curNode.next
curNode.next.prior = p
p.prior = curNode
curNode.next = p
5.2 双链表节点的删除
p = curNode.next
curNode.next = p.next
p.next.prior = curNode
del p
6. 链表 —— 复杂度分析
顺序表(列表/数组)与链表对比:
Item | 顺序表 | 链表 |
---|---|---|
按元素值查找 | O(n)O(n)O(n) | O(n)O(n)O(n) |
按下标查找 | O(1)O(1)O(1) | O(n)O(n)O(n) |
在某个元素后插入 | O(n)O(n)O(n) | O(1)O(1)O(1) |
删除某个元素 | O(n)O(n)O(n) | O(1)O(1)O(1) |
- 链表在插入和删除的操作上明显快与顺序表
- 链表的内存可以更灵活地分配:
- 试利用链表重新实现栈和队列
- 链表这种链式存储的数据结构对树和图的结构有很大的启发性
7. 哈希表
哈希表是一个通过哈希函数来计算数存储位置的数据结构,通常支持如下操作:
insert(key, value)
: 插入键值对(key, value)get(key)
: 如果存在键为key
的键值对则返回其value
,否则返回空值delete(key)
: 删除键为key
的键值对
哈希表其实就是一个
dict
7.1 直接寻址表
当关键字的全域 UUU 比较小时,直接寻址是一种简单而有效的方法。
UUU是一个集合,里面包含了所有可能出现的关键词
KKK是一个集合,里面包含了所有实际出现的关键词
TTT是一个列表,它和UUU一一对应
直接寻址技术的缺点:
- 当域UUU很大时,需要消耗大量内存,很不实际
- 如果域UUU很大而实际出现的
key
很少,则大量空间被浪费 - 无法处理关键字不是数字的情况
Q:那我们应该怎么解决这些问题呢?
A:在直接寻址表上加一个哈希函数就形成了哈希表
7.2 哈希
直接寻址表:key
为k
的元素放到k
位置上
改进的直接寻址表:哈希(Hashing)
- 构建大小为mmm的寻址表TTT
key
为k
的元素放到h(k)h(k)h(k)位置上- h(k)h(k)h(k)是一个函数,其将域UUU映射到表T[0,1,...,m−1]T[0, 1, ..., m-1]T[0,1,...,m−1]
7.3 哈希表
哈希表(Hash Table,又称为散列表),是一种线性表的存储结构。哈希表由一个直接寻址表和一个哈希函数组成。哈希函数h(k)h(k)h(k)将元素关键字kkk作为自变量,返回元素的存储下标。
假设有一个长度为7的哈希表,哈希函数$h(k) = k % 7 。元素集合。元素集合。元素集合{14, 22, 3, 5}$的存储方式如下图:
元素集合{14,22,3,5}\{14, 22, 3, 5\}{14,22,3,5}是指key,没有value
h(k)=k%7∈[0,6]h(k) = k \% 7 \in [0, 6]h(k)=k%7∈[0,6]
Q: 如果有一个key=0
,那么上面的表中key=0
已经有key
了,怎么办?
A:这么情况称为哈希冲突
7.4 哈希冲突
由于哈希表的大小是有限的,而要存储的值的数量是无限的,因此对于任何哈希函数而言,都会出现两个不同元素映射到同一个位置上的情况,这种情况叫做哈希冲突。
比如 h(k)=k%7h(k) = k \% 7h(k)=k%7, h(0)=h(7)=h(14)=...h(0) = h(7) = h(14) = ...h(0)=h(7)=h(14)=...
7.4.1 解决哈希冲突1 —— 开放寻址法
开放寻址法:如果哈希函数返回的位置已经有值,则可以向后探查新的位置来存储这个值。
- 线性探查:如果位置 iii 被占用,则探查 i+1,i+2,...i + 1, i + 2, ...i+1,i+2,...
- 二次探查:如果位置 iii 被占用,则探查 i+12,i−12,i+21,i−22,...i + 1^2, i - 1^2, i + 2^1, i - 2^2, ...i+12,i−12,i+21,i−22,...
- 二度哈希(Double Hash):有 nnn 个哈希函数,当使用第1个哈希函数h1h_1h1发生冲突时,则尝试使用h2,h3,...h_2, h_3, ...h2,h3,...
7.4.2 解决哈希冲突2 —— 拉链法
拉链法:哈希表每个位置都连接一个链表,当冲突发生时,冲突的元素将被加到该位置链表的最后。
之前是每个位置存放一个元素,现在不是元素而是链表
7.5 常见的哈希函数
- 除法哈希法:
- h(k)=k%mh(k) = k \% mh(k)=k%m
- ->
lambda k: k % m
- 乘法哈希法:
- h(k)=floor(m×(A×key%1))h(k) = \rm{floor}(m\times (A \times \rm{key} \% 1))h(k)=floor(m×(A×key%1))
- ->
lambda k: floor(m*(A*key%1))
%1
: 取小数部分
- 全域哈希法:
- ha,b(k)=((a×key+b)%p)%mh_{a, b}(k) = ((a\times \rm{key} + b) \% p) \% mha,b(k)=((a×key+b)%p)%m
- ->
lambda a, b, k: ((a * key + b) % p) % m
- a,b=1,2,...,p−1a, b = 1, 2, ..., p-1a,b=1,2,...,p−1
7.6哈希表的应用 —— 集合与字典
字典与集合都是通过哈希表来实现的。
a = {'name': 'Alex', 'age': 18, 'gender': 'Male'}
使用哈希表存储字典,通过哈希函数将字典的键映射为下标。假设 h(‘name’) = 3, h(‘age’) = 1, h(‘gender’) = 4, 则哈希表存储为 [None, 18, None, ‘Alex’, ‘Man’]
如果发生哈希冲突,则通过拉链法或开放寻址法解决。
7.7 哈希表的应用 —— MD5算法
MD5(Message-Digest Algorithm 5)曾经是密码学中常用的哈希函数,可以把任意长度的数据映射为128位的哈希值,其曾经包含如下特征:
- 同样的消息,其MD5值必定相同;
- 可以快速计算出给定消息的MD5值;
- 除非暴力的枚举所有可能的消息,否则不可能从哈希值反推出消息本身;
- 两条消息之间即使只有微小的差别,其对应的MD5值也应该是完全不同、完全不相关区的;
- 不能在有意义的时间内人工构造两个不同的消息使其具有相同的MD5值。
Message-Digest: 消息摘要
哈希只有加密,没有解密
MD5值相同的两个文件,但二者也有可能是不同的,因为有哈希冲突的存在,而且128位的哈希值是有限的(虽然有限,但是人工制造两个哈希值相同的不同文件基本上是不可能的)
现在MD5已经被破解了,不再安全了,但仍然在大规模使用
7.7.1 应用举例 —— 文件的哈希值
算出文件的哈希值,若两个文件的哈希值相同,则可以认为这两个文件是相同的,因此:
- 用户可以利用它来验证下载的文件是否完整
- 云存储服务商可以利用它来判断用户要上传的文件是否已经存在于服务器上,从而实现秒传的功能,同时也可以避免存储过多相同的文件副本
7.8 哈希表的应用 —— SHA-2算法
历史上MD5和SHA-1曾经是使用最为广泛的 Cryptographic Hash Function,但是随着密码学的发展,这两个哈希函数的安全性相继受到了各种挑战。因此现在安全性较重要的场合推荐使用SHA-2等新的、更安全的哈希函数。
- SHA-2包含了一系列的哈希函数:SHA-224,SHA-256,SHA-384,SHA-512,SHA-512/224,SHA-512/256,其对应的哈希长度分别为224,256,384或512位。
- SHA-2具有和MD5类似的性质(参见MD5算法的特征)。
SHA: Secure Hash Algorithm
SHA-2目前还没有被破解,因此更加安全
7.8.1 应用举例 —— SHA-2算法
在比特币(Bitcoin)系统中,所有参与者需要共同解决如下问题:对于一个给定的字符串U
,给定的目标哈希值H
,需要计算出一个字符串V
,使得U+V
的哈希值与H
的差小于一个给定值D
。此时,只能通过暴力枚举V
来猜测。
首先计算出结果的人可以获得一定奖金,而某人首先计算成功的概率与其拥有的计算量成正比,所以其获得奖金的期望值与其拥有的计算量成正比。
8. 树与二叉树
树是一种数据结构,比如目录结构。树是一种可以递归定义的数据结构,是由nnn个节点组成的集合:
- 如果n=0n=0n=0,那这是一颗空树;
- 如果n>0n > 0n>0,那存在1个节点作为树的根节点,其他节点可以分为mmm个集合,每个集合本身又是一棵树。
8.1 一些概念
- 根节点:A
- 叶子节点:没有孩子节点的节点(到头了,没有分叉了),比如 B,C,P,Q,K,L,M,N
- 树的深度(高度):最深的
- 节点的度:指定节点对应的分叉数,E的度=2
- 树的度:哪个节点分的叉最多,那树的就是几 -> max(节点的度)=6
- 孩子节点/父节点:相对的概念
- 子树:EIJPQ是一颗以E为根节点的树
8.2 用树创建一个文件夹系统
# 树是由一堆节点构成,所以节点是树的核心
class Node:def __init__(self, name, node_type='dir'):# 链式存储的方式self.name = nameself.type = node_type # "dir" or "file"self.children = []self.parent = None # 指向父节点# 让Node可以被打印def __repr__(self):return self.nameclass FileSystemTree:def __init__(self):self.root = Node("/") # 根节点: 这里仿照的是Linux系统的rootself.now = self.root # 在哪个文件夹中def mkdir(self, name):# name必须以/结尾if name[-1] != "/":name += "/"# 创建Nodenode = Node(name)# 和现在的文件夹相连self.now.children.append(node)node.parent = self.now# 展现当前目录下的所有目录def ls(self):print(self.now.children)# return self.now.children# change directory(只支持相对路径)def cd(self, name):if name in ["..", "../"]: # 返回上一级目录self.now = self.now.parentreturn True# name必须以/结尾if name[-1] != "/":name += "/"for child in self.now.children:if child.name == name: # 如果有这个目录self.now = child # 切换目录return Trueelse:raise ValueError("invalid directory")if __name__ == "__main__":n = Node('hello')n2 = Node('world')n.children.append(n2)n2.parent = ntree = FileSystemTree()tree.mkdir("var/")tree.mkdir("bin/")tree.mkdir("usr/")tree.ls() # [var/, bin/, usr/]tree.cd("bin/")tree.mkdir("python/")tree.ls() # [python/]tree.cd("../")tree.ls() # [var/, bin/, usr/]
8.3 二叉树
二叉树的链式存储:将二叉树的节点定义为一个对象,节点之间通过类似链表的链接方式来连接。
8.3.1 节点定义:
class BiTreeNode:def __init__(self, data):self.data = dataself.lchild = Noneself.rchild = None
二叉树的度是2
class BiTreeNode:def __init__(self, data):self.data = dataself.lchild = None # 左孩子节点self.rchild = None # 右孩子节点if __name__ == "__main__":a = BiTreeNode("A")b = BiTreeNode("B")c = BiTreeNode("C")d = BiTreeNode("D")e = BiTreeNode("E")f = BiTreeNode("F")g = BiTreeNode("G")e.lchild = ae.rchild = ga.rchild = cc.lchild = bc.rchild = dg.lchild = froot = eprint(root.lchild.rchild.data) # C
8.3.2 二叉树的遍历
二叉树的遍历方式:
- 前序遍历:EACBDGF —— 自己 -> 左 -> 右
- 中序遍历:ABCDEGF —— 左 -> 自己 -> 右
- 后序遍历:BDCAFGE —— 左 -> 右 -> 自己
- 层次遍历:EAGCFBD —— 每一层,从左到右
前序、中序、后序都是指遍历自己的位置
给出三者的任意两者都可以推出这棵树
中序遍历后一定是升序的!(有序!)
from collections import dequeclass BiTreeNode:def __init__(self, data):self.data = dataself.lchild = None # 左孩子节点self.rchild = None # 右孩子节点def pre_order(root):# 前序遍历: 自己 -> 左 -> 右if root: # 如果root不为空print(root.data, end=" ") # 自己pre_order(root.lchild) # 左pre_order(root.rchild) # 右def in_order(root):# 中序遍历: 左 -> 自己 -> 右if root:in_order(root.lchild)print(root.data, end=" ")in_order(root.rchild)def post_order(root):# 后序遍历: 左 -> 右 -> 自己if root:post_order(root.lchild)post_order(root.rchild)print(root.data, end=" ")def level_order(root):# 层次遍历# 先创建队列,并添加根节点queue = deque()queue.append(root)while len(queue) > 0: # 只要队列不空node = queue.popleft() # 出队并且得到是哪个出队了print(node.data, end=" ") # 打印出队的元素if node.lchild: # 看一下出队的节点是否有左孩子queue.append(node.lchild)if node.rchild: # 看一下出队的节点是否有右孩子queue.append(node.rchild)if __name__ == "__main__":a = BiTreeNode("A")b = BiTreeNode("B")c = BiTreeNode("C")d = BiTreeNode("D")e = BiTreeNode("E")f = BiTreeNode("F")g = BiTreeNode("G")e.lchild = ae.rchild = ga.rchild = cc.lchild = bc.rchild = dg.lchild = froot = eprint(root.lchild.rchild.data) # Cpre_order(root) # E A C B D G Fprint("")in_order(root) # A B C D E F G print("")post_order(root) # B D C A F G E print("")level_order(root) # E A G C F B D
8.4 二叉搜索树(Binary Search Tree, BST)
二叉搜索树是一颗二叉树,且满足以下性质:设 x
是二叉树的一个节点,如果 y
是 x
左子树的任意一个节点,那么 y.key ≤ x.key
;如果 y
是 x
右子树的一个节点,那么 y.key ≥ x.key
。
简单理解:任意一个节点的左侧的值都比该节点小,右边都比该节点大
二叉搜索树的操作:
- 查询:O(logn)O(\log^n)O(logn)
- 插入:O(logn)O(\log^n)O(logn)
- 删除
import randomclass BiTreeNode:def __init__(self, data):self.data = dataself.lchild = Noneself.rchild = Noneself.parent = Noneclass BinarySearchTree:def __init__(self, ls=None):self.root = None # 根节点if ls:for val in ls:self.insert_no_rec(val) # 非递归的方法(更快)# self.root = self.insert(self.root, val) # 递归的方法def insert(self, node, val):"""node: 递归时遍历的所有节点val: 插入的值"""if node is None: # 如果node为空(写成 if not node也可以)node = BiTreeNode(val) # node为空则需要创建一个节点elif val < node.data: # 说明插入的值比遍历的节点小 -> 左边node.lchild = self.insert(node.lchild, val)node.lchild.parent = nodeelif val > node.data:node.rchild = self.insert(node.rchild, val)node.rchild.parent = nodeelse: # 相等的情况passreturn nodedef insert_no_rec(self, val):p = self.rootif not p: # p是空的(空树的情况下)self.root = BiTreeNode(val)returnwhile True:if val < p.data:if p.lchild: # 左子树存在p = p.lchildelse: # 左子树不存在p.lchild = BiTreeNode(val)p.lchild.parent = preturnelif val > p.data:if p.rchild:p = p.rchildelse:p.rchild = BiTreeNode(val)p.rchild.parent = preturnelse:returndef pre_order(self, root):if root:print(root.data, end=" ")self.pre_order(root.lchild)self.pre_order(root.rchild)def in_order(self, root):if root:self.in_order(root.lchild)print(root.data, end=" ")self.in_order(root.rchild)def post_order(self, root):if root:self.post_order(root.lchild)self.post_order(root.rchild)print(root.data, end=" ")def query(self, node, val):"""递归版本需要node"""if not node: # 如果node为空(递归的终止条件)return Noneelif node.data > val:return self.query(node.lchild, val)elif node.data < val:return self.query(node.rchild, val)else:return nodedef query_no_rec(self, val):p = self.rootwhile p:if p.data > val:p = p.lchildelif p.data < val:p = p.rchildelse:return pelse:return Noneif __name__ == '__main__':tree = BinarySearchTree([4, 6, 7, 9, 2, 1, 3, 5, 8])tree.pre_order(tree.root) # 4 2 1 3 6 5 7 9 8print("")tree.in_order(tree.root) # 1 2 3 4 5 6 7 8 9print("")tree.post_order(tree.root) # 1 3 2 5 8 9 7 6 4print("")ls = list(range(500))random.shuffle(ls)tree = BinarySearchTree(ls)tree.in_order(tree.root) # 0 1 2 3 4 5 ... 99 (排好序了!)print()ls = list(range(0, 500, 2))random.shuffle(ls)tree = BinarySearchTree(ls)print(tree.query(tree.root, 4).data) # 4print(tree.query_no_rec(3)) # None
8.4.1 二叉搜索树 —— 删除操作
- 如果要删除的节点是叶子节点:直接删除
- 如果要删除的节点只有一个孩子:将此节点的父亲与孩子连接,然后删除该节点。
如果要删除的节点是根节点,且这个根只有一个孩子节点,那么直接更新根就行。
- 如果要删除的节点有两个孩子:将其右子树的最小节点(该节点最多有一个右孩子)删除,并替换当前节点。
在右子树中,往左走到头就是最小节点
这部分的代码如下:
min_node = node.rchildwhile min_node.lchild: # 如果min_node有左孩子min_node = min_node.lchild # 让左孩子替换min_node
下面是整体的代码:
import randomclass BiTreeNode:def __init__(self, data):self.data = dataself.lchild = Noneself.rchild = Noneself.parent = Noneclass BinarySearchTree:def __init__(self, ls=None):self.root = None # 根节点if ls:for val in ls:self.insert_no_rec(val) # 非递归的方法(更快)# self.root = self.insert(self.root, val) # 递归的方法def insert(self, node, val):"""node: 递归时遍历的所有节点val: 插入的值"""if node is None: # 如果node为空(写成 if not node也可以)node = BiTreeNode(val) # node为空则需要创建一个节点elif val < node.data: # 说明插入的值比遍历的节点小 -> 左边node.lchild = self.insert(node.lchild, val)node.lchild.parent = nodeelif val > node.data:node.rchild = self.insert(node.rchild, val)node.rchild.parent = nodeelse: # 相等的情况passreturn nodedef insert_no_rec(self, val):p = self.rootif not p: # p是空的(空树的情况下)self.root = BiTreeNode(val)returnwhile True:if val < p.data:if p.lchild: # 左子树存在p = p.lchildelse: # 左子树不存在p.lchild = BiTreeNode(val)p.lchild.parent = preturnelif val > p.data:if p.rchild:p = p.rchildelse:p.rchild = BiTreeNode(val)p.rchild.parent = preturnelse:returndef pre_order(self, root):if root:print(root.data, end=" ")self.pre_order(root.lchild)self.pre_order(root.rchild)def in_order(self, root):if root:self.in_order(root.lchild)print(root.data, end=" ")self.in_order(root.rchild)def post_order(self, root):if root:self.post_order(root.lchild)self.post_order(root.rchild)print(root.data, end=" ")def query(self, node, val):"""递归版本需要node"""if not node: # 如果node为空(递归的终止条件)return Noneelif node.data > val:return self.query(node.lchild, val)elif node.data < val:return self.query(node.rchild, val)else:return nodedef query_no_rec(self, val):p = self.rootwhile p:if p.data > val:p = p.lchildelif p.data < val:p = p.rchildelse:return pelse:return Nonedef __remove_node_1(self, node):# 情况1:node是叶子结点(没有孩子)if not node.parent: # 如果节点的父节点为空(那就是root节点)self.root = None # 删除根节点if node == node.parent.lchild: # node是它父亲的左孩子node.parent.lchild = None # 删除左孩子else: # 右孩子node.parent.rchild = None # 删除右孩子def __remove_node_2_1(self, node):# 情况2-1:node只有一个左孩子节点if not node.parent: # 根节点# 左孩子就是新的rootself.root = node.lchildnode.lchild.parent = Noneelif node == node.parent.lchild: # 是左孩子node.parent.lchild = node.lchildnode.lchild.parent = node.parentelse: # 是右孩子node.parent.rchild = node.lchild # node只有左孩子,没有右孩子node.lchild.parent = node.parentdef __remove_node_2_2(self, node):# 情况2-2:node只有一个右孩子if not node.parent: # 是根节点self.root = node.rchildelif node == node.parent.lchild: # 是左孩子node.parent.lchild = node.rchildnode.rchild.parent = node.parentelse: # 是右孩子node.parent.rchild = node.rchildnode.rchild.parent = node.parentdef delete(self, val):if self.root: # 如果不是空树node = self.query_no_rec(val) # 找到val对应的nodeif not node: # 如果val对应的node不存在raise ValueError(f"数值{val}不存在!")if not node.lchild and not node.rchild: # node是叶子节点(没有孩子)self.__remove_node_1(node)elif not node.rchild: # 只有一个孩子(只有左孩子)-> 2-1self.__remove_node_2_1(node)elif not node.lchild: # 只有一个右孩子 -> 2-2self.__remove_node_2_2(node)else: # 两个孩子都有# 找右子树最小的节点min_node = node.rchild# 一直找左孩子节点while min_node.lchild:min_node = min_node.lchild# 直接替换即可node.data = min_node.data# 删除min_node -> 还需要判断if min_node.rchild: # 只有右孩子节点self.__remove_node_2_2(min_node)else: # 是叶子节点self.__remove_node_1(min_node)if __name__ == '__main__':tree = BinarySearchTree([4, 6, 7, 9, 2, 1, 3, 5, 8])tree.pre_order(tree.root) # 4 2 1 3 6 5 7 9 8print("")tree.in_order(tree.root) # 1 2 3 4 5 6 7 8 9print("")tree.post_order(tree.root) # 1 3 2 5 8 9 7 6 4print("")ls = list(range(500))random.shuffle(ls)tree = BinarySearchTree(ls)tree.in_order(tree.root) # 0 1 2 3 4 5 ... 99 (排好序了!)print()ls = list(range(0, 500, 2))random.shuffle(ls)tree = BinarySearchTree(ls)print(tree.query(tree.root, 4).data) # 4print(tree.query_no_rec(3)) # Nonetree = BinarySearchTree([1, 4, 2, 5, 3, 8, 6, 9, 7])tree.in_order(tree.root) # 1 2 3 4 5 6 7 8 9print()tree.delete(4)tree.delete(1)tree.delete(3)tree.delete(8)tree.in_order(tree.root) # 2 5 6 7 9
二叉搜索树的效率
平均情况下,二叉搜索树进行搜索的时间复杂度为 O(lgn)O(\lg^n)O(lgn)。
但是在最坏情况下,二叉搜索树可能非常偏斜(极端情况下退化到 O(n)O(n)O(n))。解决方案有:
- 随机化插入
- AVL树
8.5 AVL树
AVL树:AVL树是一棵自平衡的二叉搜索树。
AVL树具有以下性质:
- 根的左右子树的高度差的绝对值不能超过1
- 根的左右子树都是平衡二叉树
高度差:针对每个节点的左右孩子数之差(有孩子为1,没孩子为0)
balance factor,平衡因子:记录了左右子树的高度之差。对于10而言,左=0,右=1,所以balance factor = 0 - 1 = -1
平衡二叉树:简单理解为树的深度一致
8.6 AVL树 —— 插入
插入一个节点可能会破坏AVL树的平衡,可以通过旋转操作来进行修正。
可视化VAL
插入一个节点后,只有从插入节点到根节点的路径上的节点的平衡可能被改变。我们需要找出第一个破坏了平衡条件的节点,称之为K
。K
的两棵子树的高度差为2。
不平衡的情况有4种。
8.6.1 左旋
不平衡是由于对K的右孩子的右子树插入导致的:左旋。
8.6.2 右旋
不平衡是由于对K的左孩子的左子树插入导致的:右旋。
8.6.3 右旋-左旋
不平衡是由于对K的右孩子的左子树插入导致的:右旋-左旋。
8.6.4 左旋-右旋
不平衡是由于对K的左孩子的右子树插入导致的:左旋-右旋。
- 左左:右旋
- 右右:左旋
- 左右:左右
- 右左:右左
8.7 二叉搜索树扩展应用 —— B树
B树(B-Tree):B树是一棵自平衡的多路搜索树,常用于数据库的索引。
比17和35小的走左边,在范围内的走中间,大的走右边。
相关文章:

[学习笔记] 2. 数据结构
数据结构视频地址:https://www.bilibili.com/video/BV1uA411N7c5 数据结构是指相互之间存在着一种或多种关系的数据元素的集合和该集合中数据元素之间的关系组成。简单来说,数据结构就是设计数据以何种方式组织并存储在计算机中。 比如:列表、集合与字…...

[学习笔记] 3. 算法进阶
算法进阶视频地址:https://www.bilibili.com/video/BV1uA411N7c5 1. 贪心算法 贪心算法(又称贪婪算法),是指在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑 —— 所做…...

做自媒体真的能赚到钱吗?真的能赚到几十万吗?
自媒体在当今社会已经成为一个热门话题,越来越多的人开始尝试做自媒体,希望能够通过自媒体赚到钱。但是,做自媒体真的能赚到钱吗?能赚到几十万吗?下面我们来一一解答。 首先,做自媒体确实可以赚到钱。随着互…...

QT使用QListWidget显示多张图片
Qt系列文章目录 文章目录Qt系列文章目录前言一、QListWidget 和 QListView 的差异二、显示效果1.操作工作区界面1.主界面头文件2. 主界面实现界面2.左边图片目录展示界面1.图片目录头文件2.图片目录实现文件2.属性窗口区1.属性窗口头文件2.属性窗口实现文件3 源码下载前言 QLi…...

python 打印进度条
import time recv_size0 total_size1024while recv_size < total_size:time.sleep(0.1)recv_size1024#打印进度条percentrecv_size / total_sizeres int(50 * percent) * #print(\r[%-50s] %d%% % (res,int(100 * percent)),end) # end 打印以‘’结尾,打印% 需…...

【微小说】大学日记
感谢B站up主“看见晴晴了吗”的视频提供的灵感,链接:https://www.bilibili.com/video/BV1tA411m7Kc 整篇故事完全虚构,如有雷同纯属巧合。 2019年8月25日 星期天 晴 今天是我进入大学的第一天。早晨,我画了美美的妆,穿…...

ArrayList扩容机制解析
1.ArrayList的成员变量 首先我们先了解一下ArrayList的成员变量。 // 默认初始化大小 private static final int DEFAULT_CAPACITY 10;// 空数组(用于空实例) // 比如List<String> ls new ArrayList<>(0); private static final Object[…...

jsp-----web应用与开发
jsp基本语法 jsp页面的基本结构 定义变量 <%! %> 表达式:变量、常量、表达式 <% %>代码块、程序段【jsp程序代码即jsp脚本】 <% %>注释 隐藏注释 不会显示在客户的浏览器上,即jsp页面运行后页面上看不到注释内容。同时也不会出…...

洛谷 P1201 [USACO1.1]贪婪的送礼者Greedy Gift Givers
题目链接:P1201 [USACO1.1]贪婪的送礼者Greedy Gift Givers - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 题目描述 对于一群 n 个要互送礼物的朋友,GY 要确定每个人送出的钱比收到的多多少。在这一个问题中,每个人都准备了一些钱来送礼物…...

php设计模式-组合模式的运用
介绍 PHP的组合模式是一种设计模式,用于将对象组合成树形结构以表示“部分-整体”的层次结构。该模式允许客户端统一处理单个对象和组合对象,使得客户端在处理对象时不需要知道对象是否为单个对象还是组合对象。 在组合模式中,有两种类型的…...

一文教会你如何简单使用Fegin进行远程服务调用
文章目录1、fegin的基本介绍2、fegin的基本使用步骤3、项目中的实际运用4、测试前言在分布式微服务中,少不了会进行不同服务之间的相互调用,比如A服务要调用B服务中的接口,如何简单方便的实现呢?fegin可以来帮助。 1、fegin的基本…...

OpenAI——CLIPs(代码使用示例)
OpenAI——CLIPs(打通NLP与CV) Open AI在2021年1月份发布Contrastive Language-Image Pre-training(CLIP),基于对比文本-图像对对比学习的多模态模型,通过图像和它对应的文本描述对比学习,模型能够学习到文本-图像对的匹配关系。它开源、多模态、zero-s…...

什么样的人更适合创业?那类人创业更容易成功?
创业是一项充满风险和机遇的事业,成功的创业者需要具备一定的素质和能力。那么,什么样的人更适合创业?哪类人创业更容易成功呢?本文将为您介绍几个适合创业的人群和成功创业者的共同特点。 具有创新精神的人 创业需要不断创新&am…...

JavaApi操作ElasticSearch(强烈推荐)
ElasticSearch 高级 1 javaApi操作es环境搭建 在elasticsearch官网中提供了各种语言的客户端:https://www.elastic.co/guide/en/elasticsearch/client/index.html 而Java的客户端就有两个: 不过Java API这个客户端(Transport Client&#…...

NFT的前景,元宇宙的发展
互联网的普及和数字技术的广泛应用,成为消费升级的新动力,在不断创造出更好的数字化生活的同时,也改变了人们的消费习惯、消费内容、消费模式,甚至是消费理念,数字经济时代的文化消费呈现出新的特征。 2020年有关机构工…...

C#基础教程20 预处理器指令
文章目录 C#预处理指令教程简介预处理指令格式指令名 参数预处理指令类型条件编译指令if#if 条件表达式宏定义指令总结C#预处理指令教程 简介 预处理指令是在编译代码之前进行的一种处理,可以让程序员在编译前根据需要对代码进行一些修改、调整或者控制。C#语言中的预处理指令…...

【FPGA】Verilog:时序电路设计 | 二进制计数器 | 计数器 | 分频器 | 时序约束
前言:本章内容主要是演示Vivado下利用Verilog语言进行电路设计、仿真、综合和下载 示例:计数器与分频器 功能特性: 采用 Xilinx Artix-7 XC7A35T芯片 配置方式:USB-JTAG/SPI Flash 高达100MHz 的内部时钟速度 存储器&#…...

国外SEO策略指南:确保你的网站排名第一!
如果你想在谷歌等搜索引擎中获得更高的排名并吸引更多的流量和潜在客户,那么你需要了解一些国外SEO策略。 下面是一些可以帮助你提高谷歌排名的关键策略。 网站结构和内容优化 谷歌的搜索算法会考虑网站的结构和内容。 因此,你需要优化网站结构&…...

Tik Tok新手秘籍,做好五点可轻松起号
新手做TikTok需要有一个具体的规划布局,如果没有深思熟虑就上手开始的话,很有可能会导致功亏一篑,甚至是浪费时间。因此,想要做好 TikTok,就必须从最基本的运营细节开始,一步一步来,下面为大家分…...

【Linux】网络入门
🎇Linux: 博客主页:一起去看日落吗分享博主的在Linux中学习到的知识和遇到的问题博主的能力有限,出现错误希望大家不吝赐教分享给大家一句我很喜欢的话: 看似不起波澜的日复一日,一定会在某一天让你看见坚持…...

回溯法——力扣题型全解【更新中】
(本文源自网上教程的笔记) 回溯基础理论 回溯搜索法,它是一种搜索的方式。 回溯是递归的副产品,只要有递归就会有回溯。 所以以下讲解中,回溯函数也就是递归函数,指的都是一个函数。 回溯法的效率 虽然…...

【华为机试真题详解 Python实现】分奖金【2023 Q1 | 100分】
文章目录 前言题目描述输入描述输出描述示例 1题目解析参考代码前言 《华为机试真题详解》专栏含牛客网华为专栏、华为面经试题、华为OD机试真题。 如果您在准备华为的面试,期间有想了解的可以私信我,我会尽可能帮您解答,也可以给您一些建议! 本文解法非最优解(即非性能…...

netlink进行网卡重命名
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <unistd.h> #include <sys/socket.h> #include <linux/if.h> #include <linux/netlink.h>#define MAX_PAYLOAD 1024 // 最大负载长…...

2023年春【数据分析与挖掘】文献精读(一)-1:针对COVID-19,使用聚类方法有效提取生物特性关联进而识别预防COVID-19的药物
分享给大家——动漫《画江湖之不良人》第四季片尾,主人公 李星云所说的一段话: 悠悠众生,因果循环,大道至简,世间若尽是不如意事, 越是执着,便越是苦,不如安下心来,看该看的风景,做好该做之事。 初行娆疆,所悟如此, 就像曾经有一位紫衣姑娘,第一次来中原时,一样…...

【Go自学第三节】Go的范围(Range)用法
Go 语言中 range 关键字用于 for 循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素。在数组和切片中它返回元素的索引和索引对应的值,在集合中返回 key-value 对。 在讲Go语言的range之前,我们先回顾下Python中range的用法 for i …...

【备战面试】每日10道面试题打卡-Day6
本篇总结的是计算机网络知识相关的面试题,后续也会更新其他相关内容 文章目录1、HTTP 与 HTTPS 有哪些区别?2、HTTPS的加密过程是什么?3、GET与POST有什么区别?4、讲讲HTTP各个版本的区别?5、HTTP与FTP的区别ÿ…...

Stable Diffusion 个人推荐的各种模型及设置参数、扩展应用等合集(不断更新中)
一、说明 | 表示或者 表示 以上 二、模型 适用风景、房子、车子等漫画类风格 模型的VAE不要用模型附带的,好像就是naifu的官方vae,很老了,用 vae-ft-mse-840000-ema-pruned.ckpt 或者是 kl-f8-anime2.ckpt; 嵌入模型要下载作者…...

Salesforce 2023财年逆风增长,现金流达历史最高!
在过去的一年里,Salesforce一直是华尔街最关注的公司之一。3月1日,CRM领域的全球领导者Salesforce公布了截至2023年1月31日的第四季度和整个财年的业绩。 Salesforce主席兼首席执行官Marc Benioff表示: Salesforce全年实现了314亿美元的收入…...

2023年3月全国数据治理工程师认证DAMA-CDGA/CDGP考试怎么通过?
弘博创新是DAMA中国授权的数据治理人才培养基地,贴合市场需求定制教学体系,采用行业资深名师授课,理论与实践案例相结合,快速全面提升个人/企业数据治理专业知识与实践经验,通过考试还能获得数据专业领域证书。 DAMA认…...

【安卓软件】KMPlayer-一款完美的媒体播放器 可以播放所有格式的字幕和视频
KM PlayerKM Player是一款未编码的视频播放器,让您无需编码即可方便地播放各种格式的视频,并为您的新体验添加了字幕支持、视频播放速度和手势等功能。KMPlayer 拥有美观和直观的设计,让您可以更方便地管理和播放视频!功能高品质视…...