2022年秋季,笔者初次接触数据结构与算法,当时只觉得书上写的内容晦涩难懂,加之自己的怠惰,很难理解所讲解的内容。所幸,期末的考核因为疫情放开,延迟到了2023年的春季开学,并且试卷的难度很低,60来分,混了个及格。
官方定义: 并没有…
民间定义:
“数据结构是数据对象,以及存在于该对象的实例和 组成实例的数据元素之间的各种联系。这些联系可以 通过定义相关的函数来给出。” --- 《数据结构、算法与应用》
“数据结构(data structure)是计算机中存储、组织数据的方式。通常情况下,精心选择的数据结构可以 带来最优效率的算法。” ---中文维基百科
“数据结构是ADT(抽象数据类型 Abstract Data Type)的物理实现。” --- 《数据结构与算法分析》
什么是抽象数据类型 ADT?
实际上 python 内置的 list 就可以看成一种抽象数据类型。
ADT: Abstract Data Type,抽象数据类型,我们在组合已有的数据结构来实现一种新的数据类型, ADT 定义了类型的数据和操作。
我们以抽象一个背包(Bag) 数据类型来说明,背包是一种容器类型,我们可以给它添加东西,也可以移除东西,并且我们想知道背包里有多少东西。于是我们可以定义一个新的数据类型叫做 Bag,在类中实现背包的所有功能.
常见数据结构如下:
数组(array),列表(list),元组(tuple),字典(dict),集合(set),栈(stack),队列(queue),树(tree)……
常见的数据结构较多, 每一种都有其对应的应用场景, 不同的数据结构的不同操作性能是不同的:
有的查询性能很快,有的插入速度很快,有的是插入头和尾速度很快
有的做范围查找很快,有的允许元素重复,有的不允许重复等等
在开发中如何选择,要根据具体的需求来选择
注意: 数据结构和语言无关, 基本常见的编程语言都有直接或者间接的使用上述常见的数据结构
算法的定义:
一个有限指令集, 每条指令的描述不依赖于语言
接受一些输入(有些情况下不需要输入)
产生输出
一定在有限步骤之后终止
算法通俗理解:
Algorithm这个单词本意就是解决问题的办法/步骤逻辑.
数据结构的实现, 离不开算法.
算法就是 解决问题的办法。
python内置列表数据结构
列表的方法
序号 | 方法 |
---|---|
1 | list.append(obj)在列表末尾添加新的对象 |
2 | list.count(obj)统计某个元素在列表中出现的次数 |
3 | list.extend(seq)在列表末尾一次性追加另一个序列中的多个值(用新列表扩展原来的列表) |
4 | list.index(obj)从列表中找出某个值第一个匹配项的索引位置 |
5 | list.insert(index, obj)将对象插入列表 |
6 | list.pop(index=-1)移除列表中的一个元素(默认最后一个元素),并且返回该元素的值 |
7 | list.remove(obj)移除列表中某个值的第一个匹配项 |
8 | list.reverse()反向列表中元素 |
9 | list.sort( key=None, reverse=False)对原列表进行排序 |
10 | list.clear()清空列表 |
11 | list.copy()复制列表 |
list.copy() 注:此拷贝方法,只对外层实现深拷贝,内层依然为浅拷贝
old = [1,[1,2,3],3]
new = old.copy()
new[0] = 3
new[1][0] =3
'''
---------------------
Before:
[1, [1, 2, 3], 3]
[1, [1, 2, 3], 3]
After:
[1, [3, 2, 3], 3]
[3, [3, 2, 3], 3]
---------------------
'''
参考文章:Python中List的复制(直接复制、浅拷贝、深拷贝)_如何把一个list复制到另一个list中python-CSDN博客https://blog.csdn.net/qq_24502469/article/details/104185122
python内置字典数据结构
字典的方法:
序号 | 函数及描述 |
---|---|
1 | dict.clear()删除字典内所有元素 |
2 | dict.copy()返回一个字典的浅复制 |
3 | dict.fromkeys()创建一个新字典,以序列seq中元素做字典的键,val为字典所有键对应的初始值 |
4 | dict.get(key, default=None)返回指定键的值,如果键不在字典中返回 default 设置的默认值 |
5 | key in dict如果键在字典dict里返回true,否则返回false |
6 | dict.items()以列表返回一个视图对象 |
7 | dict.keys()返回一个视图对象 |
8 | dict.setdefault(key, default=None)和get()类似, 但如果键不存在于字典中,将会添加键并将值设为default |
9 | dict.update(dict2)把字典dict2的键/值对更新到dict里 |
10 | dict.values()返回一个视图对象 |
11 | pop(key,default)删除字典 key(键)所对应的值,返回被删除的值。 |
12 | popitem()返回并删除字典中的最后一对键和值。 |
6.dict.items()以列表返回一个视图对象
列表格式为[(键1,值1),(键2,值2),(键3,值3),……,(键n,值n)] 列表内元素为 元组形式的键值对
python内置字典数据结构
集合的方法
方法 | 描述 |
---|---|
add() | 为集合添加元素 |
clear() | 移除集合中的所有元素 |
copy() | 拷贝一个集合 |
difference() | 返回多个集合的差集 |
difference_update() | 移除集合中的元素,该元素在指定的集合也存在。 |
discard() | 删除集合中指定的元素 |
intersection() | 返回集合的交集 |
intersection_update() | 返回集合的交集。 |
isdisjoint() | 判断两个集合是否包含相同的元素,如果没有返回 True,否则返回 False。 |
issubset() | 判断指定集合是否为该方法参数集合的子集。 |
issuperset() | 判断该方法的参数集合是否为指定集合的子集 |
pop() | 随机移除元素 |
remove() | 移除指定元素 |
symmetric_difference() | 返回两个集合中不重复的元素集合。 |
symmetric_difference_update() | 移除当前集合中在另外一个指定集合相同的元素,并将另外一个指定集合中不同的元素插入到当前集合中。 |
union() | 返回两个集合的并集 |
update() | 给集合添加元素 |
len() | 计算集合元素个数 |
栈(stack),它是一种运算受限的线性表,后进先出(LIFO)
LIFO(last in first out)表示就是后进入的元素, 第一个弹出栈空间. 类似于自动餐托盘, 最后放上的托盘, 往往先把拿出去使用.
其限制是仅允许在表的一端进行插入和删除运算。这一端被称为栈顶,相对地,把另一端称为栈底。
向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;
从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。
生活中类似于栈的
自助餐的托盘, 最新放上去的, 最先被客人拿走使用.
老师批改作业时,总是先改最表面的,也就是最后交的.收到很多的邮件(实体的), 从上往下依次处理这些邮件. (最新到的邮件, 最先处理)
注意: 不允许改变邮件的次序, 比如从最小开始, 或者处于最紧急的邮件, 否则就不再是栈结构了. 而是队列或者优先级队列结构.
栈常见的操作
push(element)
: 添加一个新元素class Stack(): def __init__(self): self.items = [] def push(self,data): self.items.append(data) def pop(self): return self.items.pop(-1) def peek(self): return self.items[-1] def is_empty(self): return self.size() == 0 def clear(self): self.items.clear() def size(self): return len(self.items) stack = Stack() stack.push(1) # 压栈操作,栈中元素为 [1] stack.push(2) # 压栈操作,栈中元素为 [1, 2] print(stack.peek()) # peek操作,输出栈顶元素 2 print(stack.pop()) # 出栈操作,输出栈顶元素 2,栈中元素为 [1] print(stack.is_empty()) # 判断栈是否为空,输出 False(因为栈中仍有元素) print(stack.size()) # 获取栈中元素的个数,输出 1
到栈顶位置.
pop()
:移除栈顶的元素,同时返回被移除的元素。
peek()
:返回栈顶的元素,不对栈做任何修改(这个方法不会移除栈顶的元素,仅仅返回它)。
isEmpty()
:如果栈里没有任何元素就返回true
,否则返回false
。
clear()
:移除栈里的所有元素。
size()
:返回栈里的元素个数。这个方法和数组的length
属性很类似。
队列(Queue),它是一种运算受限的线性表,先进先出(FIFO First In First Out)
队列是一种受限的线性结构
受限之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作
生活中类似的队列结构
生活中类似队列的场景就是非常多了, 比如在电影院, 商场, 甚至是厕所排队.
优先排队的人, 优先处理. (买票, 结账, WC)
队列常见的操作
enqueue(element)
:向队列尾部添加一个(或多个)新的项。
dequeue()
:移除队列的第一(即排在队列最前面的)项,并返回被移除的元素。
front()
:返回当前队列中第一个元素,也将是最先被移除的元素。队列不做任何变动(不移除元素,只返回元素信息——与Stack
类的peek
方法非常类似)。
isEmpty()
:如果队列中不包含任何元素,返回true
,否则返回false
。
size()
:返回队列包含的元素个数,与数组的length
属性类似.
class Queue():
def __init__(self):
self.items = []
def enqueue(self,element):
self.items.append(element)
def dequeue(self):
return self.items.pop(0)
def front(self):
return self.items[0]
def is_empty(self):
return self.size() == 0
def size(self):
return len(self.items)
q = Queue()
q.enqueue(1)#添加1
q.enqueue(2)#添加2
q.enqueue(3)#添加3
print(q.dequeue())#移除并返回1
print(q.front())#返回2(2为队首)
print(q.is_empty())#非空,返回False
print(q.size())#返回2,队列长度
优先级队列的特点:
我们知道, 普通的队列插入一个元素, 数据会被放在后端. 并且需要前面所有的元素都处理完成后才会处理前面的数据.
但是优先级队列, 在插入一个元素的时候会考虑该数据的优先级.(和其他数据优先级进行比较)
比较完成后, 可以得出这个元素正确的队列中的位置. 其他处理方式, 和队列的处理方式一样.
也就是说, 如果我们要实现优先级队列, 最主要是要修改添加方法. (当然, 还需要以某种方式来保存元素的优先级)
优先级队列应用也非常广泛
一个现实的例子就是机场登机的顺序
头等舱和商务舱乘客的优先级要高于经济舱乘客。
在有些国家,老年人和孕妇(或带小孩的妇女)登机时也享有高于其他乘客的优先级。
另一个现实中的例子是医院的(急诊科)候诊室。
医生会优先处理病情比较严重的患者。
通常,护士会鉴别分类,根据患者病情的严重程度放号。
计算机中, 我们也可以通过优先级队列来重新排序队列中任务的顺序
比如每个线程处理的任务重要性不同, 我们可以通过优先级的大小, 来决定该线程在队列中被处理的次序.
实现优先级队列相对队列主要有两方面需要考虑:
class PriorityElement():
def __init__(self,data,priority):
self.data = data
self.priority = priority
class PriorityQueue():
def __init__(self):
self.items = []
def enqueue(self,data,priority):
el = PriorityElement(data,priority)
for i in range(self.size()): #这里不需要考虑队列非空,
#如果队列为空,for 循环不会执行,因此不会进入 if 条件判断。
if self.items[i].priority > el.priority:
self.items.insert(i,el)
return
self.items.append(el)
def dequeue(self):
return self.items.pop(0)
def front(self):
return self.items[0]
def size(self):
return len(self.items)
class Person():
def __init__(self,name,age):
self.name = name
self.age = age
def test001():
q = PriorityQueue()
q.enqueue(Person('张三',20),1)
q.enqueue(Person('李四',21),2)
q.enqueue(Person('王五',22),1)
print(q.size()) #打印3
print(q.dequeue().data.name,q.dequeue().data.age)# 张三,22 打印22是因为调用过dequeue移除了张三这个对象
q.enqueue(Person('lucy',25),1)
print(q.front().data.name,q.front().data.age) #打印露西 25
test001()
链表是链式的存储多个元素.
但不同于列表, 链表中的元素在内存中不必是连续的空间
链表的每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(有些语言称为指针或者链接)组成
链表访问任何一个位置的元素时, 都需要从头开始访问.(无法跳过第一个元素访问任何一个元素)
生活中类似于链表的
链表类似于火车: 有一个火车头, 火车头会连接一个节点, 节点上有乘客, 并且这个节点会连接下一个节点, 以此类推
链表常见的操作
append(element)
:向列表尾部添加一个新的项
insert(position, element)
:向列表的特定位置插入一个新的项。
remove(element)
:从列表中移除一项。
indexOf(element)
:返回元素在链表中的索引。如果列表中没有该元素则返回-1
。
removeAt(position)
:从列表的特定位置移除一项。
isEmpty()
:如果链表中不包含任何元素,返回true
,如果链表长度大于0则返回false
。
size()
:返回链表包含的元素个数。与数组的length
属性类似。
class Node():
def __init__(self,data,next=None):
self.data = data
self.next = next
class Link():
def __init__(self):
self.head = None
self.count = 0
def is_empty(self):
return self.count == 0
def size(self):
return self.count
def append(self,data):
n1 = Node(data)#把传入的数据封装为一个Node节点对象
#如果是空链表,直接把self(链表对象)的head指向n1
if self.head is None:
self.head = n1
else:
#循环找到最后一个节点,将其next属性指向n1
current=self.head
while current.next is not None:#检验节点是否为空
current = current.next#不为空,则将下个节点赋值给current
current.next = n1#while循环中current.next为空,跳出循环,将n1赋值给current.next
self.count += 1
def indexof(self,element):
index = 0
current = self.head
while current is not None:
if current.data == element:#如果找到了element
return index#则返回index索引
else:#没找到
current = current.next#节点后移
index += 1#同时索引自增
return -1#while循环完,也没找到element,此时current为最后一个节点,返回-1
def insert_into(self,index,data):
if index<0 or index>self.count:#判断index是否合法
return
else:
n1 = Node(data)
#1.如果插入的是头部
if index == 0:
n1.next = self.head
self.head = n1
else:
current = self.head
position = 0
while position= self.count:
return
else:
#1.移除的是头部节点
if position == 0:
self.head = self.head.next
else:
current = self.head
index = 0
while index
单向链表:
只能从头遍历到尾或者从尾遍历到头(一般从头到尾)
也就是链表相连的过程是单向的. 实现的原理是上一个链表中有一个指向下一个的引用.
单向链表有一个比较明显的缺点:
我们可以轻松的到达下一个节点, 但是回到前一个节点是很难的. 但是, 在实际开发中, 经常会遇到需要回到上一个节点的情况(回溯很难)
举个例子: 假设一个文本编辑用链表来存储文本. 每一行用一个String对象存储在链表的一个节点中. 当编辑器用户向下移动光标时, 链表直接操作到下一个节点即可. 但是当用于将光标向上移动呢? 这个时候为了回到上一个节点, 我们可能需要从first开始, 依次走到想要的节点上.
双向链表
既可以从头遍历到尾, 又可以从尾遍历到头
也就是链表相连的过程是双向的. 那么它的实现原理, 你能猜到吗?
一个节点既有向前连接的引用, 也有一个向后连接的引用.
双向链表可以有效的解决单向链表中提到的问题.
双向链表有什么缺点呢?
每次在插入或删除某个节点时, 需要处理四个节点的引用, 而不是两个. 也就是实现起来要困难一些(其实对比后面的二叉树,双向链表则是小巫)
并且相当于单向链表, 必然占用内存空间更大一些.
但是这些缺点和我们使用起来的方便程度相比, 是微不足道的.