python数据结构与算法-03_链表

链式结构

上一节讲到了支持随机访问的线性结构,这次我们开始讲链式结构, 视频里我会说下这两种结构的区别,然后讲解最常见的单链表和双链表。
之前在专栏文章那些年,我们一起跪过的算法题[视频]里实现过一个 lru_cache,
使用到的就是循环双端链表,如果感觉这篇文章有点难理解,我们这里将会循序渐进地来实现。
后边讲到哈希表的冲突解决方式的时候,我们会再次提到链表。

上一节我们分析了 list 的各种操作是如何实现的,如果你还有印象的话,list
在头部进行插入是个相当耗时的操作(需要把后边的元素一个一个挪个位置)。假如你需要频繁在数组两头增删,list 就不太合适。
今天我们介绍的链式结构将摆脱这个缺陷,当然了链式结构本身也有缺陷,比如你不能像数组一样随机根据下标访问,你想查找一个元素只能老老实实从头遍历。
所以嘛,学习和了解数据结构的原理和实现你才能准确地选择到底什么时候该用什么数据结构,而不是瞎选导致代码性能很差。

单链表

和线性结构不同,链式结构内存不连续的,而是一个个串起来的,这个时候就需要每个链接表的节点保存一个指向下一个节点的指针。
这里可不要混淆了列表和链表(它们的中文发音类似,但是列表 list 底层其实还是线性结构,链表才是真的通过指针关联的链式结构)。
看到指针你也不用怕,这里我们用的 python,你只需要一个简单赋值操作就能实现,不用担心 c 语言里复杂的指针。

先来定义一个链接表的节点,刚才说到有一个指针保存下一个节点的位置,我们叫它 next, 当然还需要一个 value 属性保存值

class Node(object):def __init__(self, value, next=None):self.value = valueself.next = next

然后就是我们的单链表 LinkedList ADT:

class LinkedList(object):""" 链接表 ADT[root] -> [node0] -> [node1] -> [node2]"""

实现我们会在视频中用画图来模拟并且手动代码实现,代码里我们会标识每个步骤的时间复杂度。这里请高度集中精力,
虽然链表的思想很简单,但是想要正确写对链表的操作代码可不容易,稍不留神就可能丢失一些步骤。
这里我们还是会用简单的单测来验证代码是否按照预期工作。

来看下时间复杂度:

链表操作平均时间复杂度
linked_list.append(value)O(1)
linked_list.appendleft(value)O(1)
linked_list.find(value)O(n)
linked_list.remove(value)O(n)

双链表

上边我们亲自实现了一个单链表,但是能看到很明显的问题,单链表虽然 append 是 O(1),但是它的 find 和 remove 都是 O(n)的,
因为删除你也需要先查找,而单链表查找只有一个方式就是从头找到尾,中间找到才退出。
这里我之前提到过如果要实现一个 lru 缓存(访问时间最久的踢出),我们需要在一个链表里能高效的删除元素,
并把它追加到访问表的最后一个位置,这个时候单链表就满足不了了,
因为缓存在 dict 里查找的时间是 O(1),你更新访问顺序就 O(n)了,缓存就没了优势。

这里就要使用到双链表了,相比单链表来说,每个节点既保存了指向下一个节点的指针,同时还保存了上一个节点的指针。

class Node(object):# 如果节点很多,我们可以用 __slots__ 来节省内存,把属性保存在一个 tuple 而不是 dict 里# 感兴趣可以自行搜索  python  __slots____slots__ = ('value', 'prev', 'next')def __init__(self, value=None, prev=None, next=None):self.value, self.prev, self.next = value, prev, next

对, 就多了 prev,有啥优势嘛?

  • 看似我们反过来遍历双链表了。反过来从哪里开始呢?我们只要让 root 的 prev 指向 tail 节点,不就串起来了吗?
  • 直接删除节点,当然如果给的是一个值,我们还是需要查找这个值在哪个节点? - 但是如果给了一个节点,我们把它拿掉,直接让它的前后节点互相指过去不就行了?哇欧,删除就是 O(1) 了,两步操作就行啦

好,废话不多说,我们在视频里介绍怎么实现一个双链表 ADT。你可以直接在本项目的 docs/03_链表/double_link_list.py 找到代码。
最后让我们看下它的时间复杂度:(这里 CircularDoubleLinkedList 取大写字母缩写为 cdll)

循环双端链表操作平均时间复杂度
cdll.append(value)O(1)
cdll.appendleft(value)O(1)
cdll.remove(node),注意这里参数是 nodeO(1)
cdll.headnode()O(1)
cdll.tailnode()O(1)

源码

double_link_list.py

# -*- coding: utf-8 -*-class Node(object):__slots__ = ('value', 'prev', 'next')   # save memorydef __init__(self, value=None, prev=None, next=None):self.value, self.prev, self.next = value, prev, nextclass CircularDoubleLinkedList(object):"""循环双端链表 ADT多了个循环其实就是把 root 的 prev 指向 tail 节点,串起来"""def __init__(self, maxsize=None):self.maxsize = maxsizenode = Node()node.next, node.prev = node, nodeself.root = nodeself.length = 0def __len__(self):return self.lengthdef headnode(self):return self.root.nextdef tailnode(self):return self.root.prevdef append(self, value):    # O(1), 你发现一般不用 for 循环的就是 O(1),有限个步骤if self.maxsize is not None and len(self) >= self.maxsize:raise Exception('LinkedList is Full')node = Node(value=value)tailnode = self.tailnode() or self.roottailnode.next = nodenode.prev = tailnodenode.next = self.rootself.root.prev = nodeself.length += 1def appendleft(self, value):if self.maxsize is not None and len(self) >= self.maxsize:raise Exception('LinkedList is Full')node = Node(value=value)if self.root.next is self.root:   # emptynode.next = self.rootnode.prev = self.rootself.root.next = nodeself.root.prev = nodeelse:node.prev = self.rootheadnode = self.root.nextnode.next = headnodeheadnode.prev = nodeself.root.next = nodeself.length += 1def remove(self, node):      # O(1),传入node 而不是 value 我们就能实现 O(1) 删除"""remove:param node  # 在 lru_cache 里实际上根据key 保存了整个node:"""if node is self.root:returnelse:    #node.prev.next = node.nextnode.next.prev = node.prevself.length -= 1return nodedef iter_node(self):if self.root.next is self.root:returncurnode = self.root.nextwhile curnode.next is not self.root:yield curnodecurnode = curnode.nextyield curnodedef __iter__(self):for node in self.iter_node():yield node.valuedef iter_node_reverse(self):"""相比单链表独有的反序遍历"""if self.root.prev is self.root:returncurnode = self.root.prevwhile curnode.prev is not self.root:yield curnodecurnode = curnode.prevyield curnodedef test_double_link_list():dll = CircularDoubleLinkedList()assert len(dll) == 0dll.append(0)dll.append(1)dll.append(2)assert list(dll) == [0, 1, 2]assert [node.value for node in dll.iter_node()] == [0, 1, 2]assert [node.value for node in dll.iter_node_reverse()] == [2, 1, 0]headnode = dll.headnode()assert headnode.value == 0dll.remove(headnode)assert len(dll) == 2assert [node.value for node in dll.iter_node()] == [1, 2]dll.appendleft(0)assert [node.value for node in dll.iter_node()] == [0, 1, 2]if __name__ == '__main__':test_double_link_list()

linked_list.py

# -*- coding: utf-8 -*-class Node(object):def __init__(self, value=None, next=None):   # 这里我们 root 节点默认都是 None,所以都给了默认值self.value = valueself.next = nextdef __str__(self):"""方便你打出来调试,复杂的代码可能需要断点调试"""return '<Node: value: {}, next={}>'.format(self.value, self.next)__repr__ = __str__class LinkedList(object):""" 链接表 ADT[root] -> [node0] -> [node1] -> [node2]"""def __init__(self, maxsize=None):""":param maxsize: int or None, 如果是 None,无限扩充"""self.maxsize = maxsizeself.root = Node()     # 默认 root 节点指向 Noneself.tailnode = Noneself.length = 0def __len__(self):return self.lengthdef append(self, value):    # O(1)if self.maxsize is not None and len(self) >= self.maxsize:raise Exception('LinkedList is Full')node = Node(value)    # 构造节点tailnode = self.tailnodeif tailnode is None:    # 还没有 append 过,length = 0, 追加到 root 后self.root.next = nodeelse:     # 否则追加到最后一个节点的后边,并更新最后一个节点是 append 的节点tailnode.next = nodeself.tailnode = nodeself.length += 1def appendleft(self, value):if self.maxsize is not None and len(self) >= self.maxsize:raise Exception('LinkedList is Full')node = Node(value)if self.tailnode is None:  # 如果原链表为空,插入第一个元素需要设置 tailnodeself.tailnode = nodeheadnode = self.root.nextself.root.next = nodenode.next = headnodeself.length += 1def __iter__(self):for node in self.iter_node():yield node.valuedef iter_node(self):"""遍历 从 head 节点到 tail 节点"""curnode = self.root.nextwhile curnode is not self.tailnode:    # 从第一个节点开始遍历yield curnodecurnode = curnode.next    # 移动到下一个节点if curnode is not None:yield curnodedef remove(self, value):    # O(n)""" 删除包含值的一个节点,将其前一个节点的 next 指向被查询节点的下一个即可:param value:"""prevnode = self.root    #for curnode in self.iter_node():if curnode.value == value:prevnode.next = curnode.nextif curnode is self.tailnode:  # NOTE: 注意更新 tailnodeif prevnode is self.root:self.tailnode = Noneelse:self.tailnode = prevnodedel curnodeself.length -= 1return 1  # 表明删除成功else:prevnode = curnodereturn -1  # 表明删除失败def find(self, value):    # O(n)""" 查找一个节点,返回序号,从 0 开始:param value:"""index = 0for node in self.iter_node():   # 我们定义了 __iter__,这里就可以用 for 遍历它了if node.value == value:return indexindex += 1return -1    # 没找到def popleft(self):    # O(1)""" 删除第一个链表节点"""if self.root.next is None:raise Exception('pop from empty LinkedList')headnode = self.root.nextself.root.next = headnode.nextself.length -= 1value = headnode.valueif self.tailnode is headnode:   # 勘误:增加单节点删除 tailnode 处理self.tailnode = Nonedel headnodereturn valuedef clear(self):for node in self.iter_node():del nodeself.root.next = Noneself.length = 0self.tailnode = Nonedef reverse(self):"""反转链表"""curnode = self.root.nextself.tailnode = curnode  # 记得更新 tailnode,多了这个属性处理起来经常忘记prevnode = Nonewhile curnode:nextnode = curnode.nextcurnode.next = prevnodeif nextnode is None:self.root.next = curnodeprevnode = curnodecurnode = nextnodedef test_linked_list():ll = LinkedList()ll.append(0)ll.append(1)ll.append(2)ll.append(3)assert len(ll) == 4assert ll.find(2) == 2assert ll.find(-1) == -1assert ll.remove(0) == 1assert ll.remove(10) == -1assert ll.remove(2) == 1assert len(ll) == 2assert list(ll) == [1, 3]assert ll.find(0) == -1ll.appendleft(0)assert list(ll) == [0, 1, 3]assert len(ll) == 3headvalue = ll.popleft()assert headvalue == 0assert len(ll) == 2assert list(ll) == [1, 3]assert ll.popleft() == 1assert list(ll) == [3]ll.popleft()assert len(ll) == 0assert ll.tailnode is Nonell.clear()assert len(ll) == 0assert list(ll) == []def test_linked_list_remove():ll = LinkedList()ll.append(3)ll.append(4)ll.append(5)ll.append(6)ll.append(7)ll.remove(7)print(list(ll))def test_single_node():# https://github.com/PegasusWang/python_data_structures_and_algorithms/pull/21ll = LinkedList()ll.append(0)ll.remove(0)ll.appendleft(1)assert list(ll) == [1]def test_linked_list_reverse():ll = LinkedList()n = 10for i in range(n):ll.append(i)ll.reverse()assert list(ll) == list(reversed(range(n)))def test_linked_list_append():ll = LinkedList()ll.appendleft(1)ll.append(2)assert list(ll) == [1, 2]if __name__ == '__main__':test_single_node()test_linked_list()test_linked_list_append()test_linked_list_reverse()

lru_cache.py

"""
python3 only
LRU cache
"""
from collections import OrderedDict
from functools import wrapsdef fib(n):if n <= 1:  # 0 or 1return nreturn f(n - 1) + f(n - 2)  # 由于涉及到重复计算,这个递归函数在 n 大了以后会非常慢。 O(2^n)"""
下边就来写一个缓存装饰器来优化它。传统方法是用个数组记录之前计算过的值,但是这种方式不够 Pythonic
"""def cache(func):"""先引入一个简单的装饰器缓存,其实原理很简单,就是内部用一个字典缓存已经计算过的结果"""store = {}@wraps(func)def _(n):   # 这里函数没啥意义就随便用下划线命名了if n in store:return store[n]else:res = func(n)store[n] = resreturn resreturn _@cache
def f(n):if n <= 1:  # 0 or 1return nreturn f(n - 1) + f(n - 2)"""
问题来了,假如空间有限怎么办,我们不可能一直向缓存塞东西,当缓存达到一定个数之后,我们需要一种策略踢出一些元素,
用来给新的元素腾出空间。
一般缓存失效策略有
- LRU(Least-Recently-Used): 替换掉最近请求最少的对象,实际中使用最广。cpu缓存淘汰和虚拟内存效果好,web应用欠佳
- LFU(Least-Frequently-Used): 缓存污染问题(一个先前流行的缓存对象会在缓存中驻留很长时间)
- First in First out(FIFO)
- Random Cache: 随机选一个删除LRU 是常用的一个,比如 redis 就实现了这个策略,这里我们来模拟实现一个。
要想实现一个 LRU,我们需要一种方式能够记录访问的顺序,并且每次访问之后我们要把最新使用到的元素放到最后(表示最新访问)。
当容量满了以后,我们踢出最早访问的元素。假如用一个链表来表示的话:[1] -> [2] -> [3]假设最后边是最后访问的,当访问到一个元素以后,我们把它放到最后。当容量满了,我们踢出第一个元素就行了。
一开始的想法可能是用一个链表来记录访问顺序,但是单链表有个问题就是如果访问了中间一个元素,我们需要拿掉它并且放到链表尾部。
而单链表无法在O(1)的时间内删除一个节点(必须要先搜索到它),但是双端链表可以,因为一个节点记录了它的前后节点,
只需要把要删除的节点的前后节点链接起来就行了。
还有个问题是如何把删除后的节点放到链表尾部,如果是循环双端链表就可以啦,我们有个 root 节点链接了首位节点,
只需要让 root 的前一个指向这个被删除节点,然后让之前的最后一个节点也指向它就行了。使用了循环双端链表之后,我们的操作就都是 O(1) 的了。这也就是使用一个 dict 和一个 循环双端链表 实现LRU 的思路。
不过一般我们使用内置的 OrderedDict(原理和这个类似)就好了,要实现一个循环双端链表是一个不简单的事情,因为链表操作很容易出错。补充:其实 lru 有个缺点就是额外的链表比较占用空间,如果你感兴趣的话可以看看 redis 如何实现的 lru 算法
"""class LRUCache:def __init__(self, capacity=128):self.capacity = capacity# 借助 OrderedDict 我们可以快速实现一个 LRUCache,OrderedDict 内部其实也是使用循环双端链表实现的# OrderedDict 有两个重要的函数用来实现 LRU,一个是 move_to_end,一个是 popitem,请自己看文档self.od = OrderedDict()def get(self, key, default=None):val = self.od.get(key, default)  # 如果没有返回 default,保持 dict 语义self.od.move_to_end(key)   # 每次访问就把key 放到最后表示最新访问return valdef add_or_update(self, key, value):if key in self.od:  # updateself.od[key] = valueself.od.move_to_end(key)else:  # insertself.od[key] = valueif len(self.od) > self.capacity:  # fullself.od.popitem(last=False)def __call__(self, func):"""一个简单的 LRU 实现。有一些问题需要思考下:- 这里为了简化默认参数只有一个数字 n,假如可以传入多个参数,如何确定缓存的key 呢?- 这里实现没有考虑线程安全的问题,要如何才能实现线程安全的 LRU 呢?当然如果不是多线程环境下使用是不需要考虑的- 假如这里没有用内置的 dict,你能使用 redis 来实现这个 LRU 吗,如果使用了 redis,我们可以存储更多数据到服务器。而使用字典实际上是缓存了Python进程里(localCache)。- 这里只是实现了 lru 策略,你能同时实现一个超时 timeout 参数吗?比如像是memcache 实现的 lazy expiration 策略- LRU有个缺点就是,对于周期性的数据访问会导致命中率迅速下降,有一种优化是 LRU-K,访问了次数达到 k 次才会将数据放入缓存"""def _(n):if n in self.od:return self.get(n)else:val = func(n)self.add_or_update(n, val)return valreturn _@LRUCache(10)
def f_use_lru(n):if n <= 1:  # 0 or 1return nreturn f_use_lru(n - 1) + f_use_lru(n - 2)def test():import timebeg = time.time()for i in range(34):print(f(i))print(time.time() - beg)beg = time.time()for i in range(34):print(f_use_lru(i))print(time.time() - beg)# TODO 要怎么给 lru 写单测?if __name__ == '__main__':test()######################################### 使用双链表实现 LRUcache ####################################################
"""
一般面试中不会让我们直接用内置结构,所以这里提供一个自己实现的双链表+map lru 缓存。这也是力扣上的一道真题:
[146] LRU 缓存 https://leetcode-cn.com/problems/lru-cache/description/
"""class ListNode:def __init__(self, key=None, value=None):self.key = keyself.value = valueself.prev = self.next = Noneclass List:def __init__(self):"""循环双链表。注意增加了虚拟头尾结点 head,tail 方便处理"""self.head = ListNode()self.tail = ListNode()self.head.prev = self.head.next = self.tailself.tail.next = self.tail.prev = self.headdef delete_node(self, node):  # 删除指定节点node.prev.next = node.nextnode.next.prev = node.prevdef add_to_head(self, node):  # 指定节点添加到 self.head 后nextnode = self.head.nextnode.next = nextnodenode.prev = self.headself.head.next = nodenextnode.prev = nodeclass LRUCache(object):def __init__(self, capacity):"""思路:循环双链表 + 字典:type capacity: int"""self.map = dict()self.ll = List()self.capacity = capacitydef get(self, key):""":type key: int:rtype: int"""if key not in self.map:return -1node = self.map[key]self.ll.delete_node(node)self.ll.add_to_head(node)return node.valuedef put(self, key, value):""":type key: int:type value: int:rtype: None"""if key in self.map: # 更新不会改变元素个数,这里不用判断是否需要剔除node = self.map[key]node.value = value  # 修改结构体会也会修改 map 对应 value 的引用self.ll.delete_node(node)self.ll.add_to_head(node)else:if len(self.map) >= self.capacity:  # 直接用 len(self.map) ,不需要self.size 字段了tailnode = self.ll.tail.prevself.ll.delete_node(tailnode)del self.map[tailnode.key]node = ListNode(key, value)self.map[key] = nodeself.ll.add_to_head(node)

小问题:

  • 这里单链表我没有实现 insert 方法,你能自己尝试实现吗? insert(value, new_value),我想在某个值之前插入一个值。你同样需要先查找,所以这个步骤也不够高效。
  • 你能尝试自己实现个 lru cache 吗?需要使用到我们这里提到的循环双端链表
  • 借助内置的 collections.OrderedDict,它有两个方法 popitem 和 move_to_end,我们可以迅速实现一个 LRU cache。请你尝试用 OrderedDict 来实现。
  • python 内置库的哪些数据结构使用到了本章讲的链式结构?

相关阅读

那些年,我们一起跪过的算法题- Lru cache[视频]

勘误:

视频中 LinkedList.remove 方法讲解有遗漏, linked_list.py 文件已经修正,请读者注意。具体请参考 fix linked_list & add gitigonre。视频最后增加了一段勘误说明。

Leetcode

反转链表 reverse-linked-list

这里有一道关于 LRU 的练习题你可以尝试下。
LRU Cache

合并两个有序链表 merge-two-sorted-lists

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

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

相关文章

考研分享第3期 | 211本378分上岸大连理工电子信息经验贴

考研分享第3期 | 211本378分上岸大连理工电子信息经验贴 一、个人信息 姓名&#xff1a;Ming 本科院校&#xff1a;某211学校电子信息工程学院 电子科学与技术专业 上岸院校&#xff1a;大连理工大学 电子信息与电气工程学部 电子信息&#xff08;0854&#xff09; 择校意…

数据中心:精密空调监控,这招太高效了!

在当今日益复杂的工业环境中&#xff0c;精密空调系统的监控和管理变得至关重要。随着科技的迅猛发展&#xff0c;各行各业对温度、湿度和空气质量等参数的高度控制需求不断增加。 精密空调监控系统通过实时数据采集、分析和反馈&#xff0c;为企业提供了可靠的手段来确保生产环…

给你一个整数 n,请你判断该整数是否是 2 的幂次方。

题意&#xff1a; 给你一个整数 n&#xff0c;请你判断该整数是否是 2 的幂次方。如果是&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 如果存在一个整数 x 使得 n 2x &#xff0c;则认为 n 是 2 的幂次方。 示例 1&#xff1a; 输入&#xff1a;n 1…

settings.json配置

settings.json配置 {"editor.tabSize": 2,"git.ignoreWindowsGit27Warning": true,"workbench.editor.untitled.hint": "hidden","security.workspace.trust.untrustedFiles": "open","[vue]": {"…

dll文件【C#】

加载方法&#xff1a; [DllImport("controlcan.dll")] public static extern UInt32 VCI_OpenDevice(UInt32 DeviceType, UInt32 DeviceInd, UInt32 Reserved); 文件存放位置&#xff1a; 一般放Debug文件夹下。 运行错误&#xff1a; 原因是CPU位数选择不对&…

不变式和橄榄树-UMLChina建模知识竞赛第4赛季第20轮

DDD领域驱动设计批评文集 做强化自测题获得“软件方法建模师”称号 《软件方法》各章合集 参考潘加宇在《软件方法》和UMLChina公众号文章中发表的内容作答。在本文下留言回答。 只要最先答对前3题&#xff0c;即可获得本轮优胜。第4题为附加题&#xff0c;对错不影响优胜者…

科普测量开关电源输出波形的三种方法及电源波形自动化测试步骤

开关电源波形测试就是对开关电源的输出波形进行检测和分析&#xff0c;观察开关电源参数变化&#xff0c;以此来判断开关电源的性能是否符合要求。好的开关电源对于设备以及整个电路的正常运行是非常重要的&#xff0c;因此开关电源输出波形测试是开关电源测试的重要环节&#…

数据同步工具调研选型:SeaTunnel 与 DataX 、Sqoop、Flume、Flink CDC 对比

产品概述 Apache SeaTunnel 是一个非常易用的超高性能分布式数据集成产品&#xff0c;支持海量数据的离线及实时同步。每天可稳定高效同步万亿级数据&#xff0c;已应用于数百家企业生产&#xff0c;也是首个由国人主导贡献到 Apache 基金会的数据集成顶级项目。 SeaTunnel 主…

无人值守的共享台球室:微信小程序实现自助服务

随着科技的发展和互联网的普及&#xff0c;无人值守的共享台球室已经成为一种新型的娱乐方式。通过微信小程序&#xff0c;消费者可以实现在线预订、自助结账、评价反馈等一站式服务&#xff0c;带来更加便捷、高效的体验。本文将探讨如何利用微信小程序实现无人值守的共享台球…

vue滚动到指定位置

一、首先获取需要滚动的外层容器的宽度 let boxWidth this.$refs.box.offsetWidth二、计算出容器内单个元素的宽度&#xff0c;以12个为例 let itemWidth boxWidth / 12三、计算出容器内单个元素的宽度&#xff0c;以横向滚动&#xff0c;内部12个元素为例 this.$refs.box…

【Ubuntu】安装Docker

一、查看系统基本情况 1.1 查看系统版本 指令一 lsb_release -aNo LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 20.04.6 LTS Release: 20.04 Codename: focal 指令二 cat /etc/lsb-release DISTRIB_IDUbuntu DISTRIB_RELEASE20.04 DISTRIB_CODE…

Flutter笔记 - 关于 fit 属性以及相关知识的总结

Flutter笔记 关于 fit 属性以及相关知识的总结 作者&#xff1a;李俊才 &#xff08;jcLee95&#xff09;&#xff1a;https://blog.csdn.net/qq_28550263 邮箱 &#xff1a;291148484163.com 本文地址&#xff1a;https://blog.csdn.net/qq_28550263/article/details/13434451…

软考高级职称哪个好考?明确给你答案

软考考试分为初、中、高三级&#xff0c;其中高级5个方向分别为系统分析师、信息系统项目管理师、网络规划设计师、系统架构设计师、系统规划与管理师。软考高级职称考什么好&#xff1f;有很多人是因为要评高级职称而选择参考软考高级资格考试&#xff0c;那么软考高级里哪个资…

16个值得推荐的.NET ORM框架

什么是ORM? ORM 是 Object Relational Mapping 的缩写&#xff0c;译为“对象关系映射”&#xff0c;是一种程序设计技术&#xff0c;用于实现面向对象编程语言里不同类型系统的数据之间的转换。它解决了对象和关系型数据库之间的数据交互问题&#xff0c;ORM的作用是在关系型…

txt2xml

import os import cv2 import pathlib from xml.dom.minidom import Document # 支持多种格式图片,可以自己添加 def search_file_name(file_path, img_root): file_format [.jpg, .JPG, .png, .PNG, .JPEG, .jpeg] file_path pathlib.Path(file_path) file_name…

数字化企业各业务模块模型

1.计划 1.1采购计划执行情况 序号 采购计划号 采购订单号 业务员 供应商 物料 数量 金额 计划入库日期 实际入库日期 状态 针对企业执行中或者未关闭的采购计划进行统计与分析&#xff0c;主要目的在于引领企业员工与领导关注长期在途的采购…

Android修行手册 - 可变参数中星号什么作用(冷知识)

点击跳转>Unity3D特效百例点击跳转>案例项目实战源码点击跳转>游戏脚本-辅助自动化点击跳转>Android控件全解手册点击跳转>Scratch编程案例点击跳转>软考全系列 &#x1f449;关于作者 专注于Android/Unity和各种游戏开发技巧&#xff0c;以及各种资源分享&…

如何选择工业以太网交换机?

现在市面上有许多品牌的工业以太网交换机&#xff0c;并且类型也相当复杂。在选择交换机的时候&#xff0c;需要考虑多个因素&#xff0c;比如工作环境、是否需要冗余配置、是否需要网管功能、是否需要非网管功能、以及未来的维护和扩展性等。通常情况下&#xff0c;当选择工业…

Istio学习笔记- 服务网格

Istio 服务网格 参考&#xff1a;Istio / Istio 服务网格 Istio 使用功能强大的 Envoy 服务代理扩展了 Kubernetes&#xff0c;以建立一个可编程的、可感知的应用程序网络。Istio 与 Kubernetes 和传统工作负载一起使用&#xff0c;为复杂的部署带来了标准的通用流量管理、遥…

别试错了,是该关注一下软件内在质量了

太多这种例子了&#xff0c;老板们早上出的新想法&#xff0c;恨不得第二天就能上线。。每个互联网公司都试图突破固定领地&#xff0c;不断地尝试新的业务&#xff0c;一旦发现不行&#xff0c;就立刻砍掉&#xff0c;名曰“试错”。 研发部门&#xff0c;为了应对压力&#…