概述
什么是数据结构
简单来说,数据结构就是设计数据以何种方式组织并存储在计算机中。比如:列表、集合与字典等都是一种数据结构。
列表
python的列表在其他编程语言中称为“数组”,不过二者是有区别的。在C语言中,必须指定数组的长度,并且数组中的元素具有相同的数据类型。而python中则没有这些限制,这是因为二者在内存中存储方式不一样。
数组
我们知道,计算机将内存分割为字节,每个字节可以存储8位的二进制信息:
每个字节都具有唯一的地址。如果内存中有n个字节,那么可以把地址看作0 ~ n-1的数,并用指针变量来存储地址,也就说,指针就是地址
程序中的每个变量在内存中占有一个或多个字节,把第一个字节的地址称为是该变量的地址。
用C语言新建一个整型数组int a[4]={10, 12, 16, 33}
每个数字用4个字节表示,就相当于在内存中开辟长度为20的连续空间,如图所示。那么数组名a,就是指向该数组第一个元素的指针,对数组元素的取值只需要对指针进行算术运算即可:& 取地址运算符,&a
就是a在内存中的地址,2000;* 间接寻址运算符(访问指针所指向对象的内容),这里*a
就表示指针a当前所指向的对象的值,也就是数组中第一个元素10;通过对指针a进行自增,就可访问到数组中的所有元素,*(a+1)
就可以取到数组第二个元素的值,12:
a+i
就等同于&a[i]
,两者都表示指向数组a中第i个元素的指针,并且*(a+1)
等价于a[i]
,两者都表示第i个元素的值。因此,数组的取下标操作就是指针指针算术运算的一种形式
列表
python中的列表可以混合存储任意数据类型,那么它是如何实现的呢?因为列表中存的是元素的内存地址,而不是元素的值,比如有列表li = [1, True, 'hello', {'k': 'v'}]
。li中的元素在内存中并不是连续存储的,因此li存储这些元素所在的内存地址,对元素取值时,先在列表中找到元素的内存地址,再通过内存地址找到元素:
因此,比起数组,对列表元素的取值要慢一些。
另外,列表没有长度限制是如何实现的呢?当我们通过append方法向列表中添加元素时,如果列表满了,那么新申请2倍于原来列表的内存地址,并将原列表的值拷贝到新的内存地址中。
列表的删除,插入的时间复杂度是O(N)
栈
介绍
什么是栈
栈(Stack)是一个数据集合,可以理解为只能在一端进行插入或删除操作的列表。
栈的特点
后进先出(last-in, first-out)
栈的基本操作
进栈(压栈):Push;出栈:pop;查看栈顶:gettop
出栈次序问题
一个栈的进栈序列为1,2,3,问下面哪种出栈序列不可能出现?
123 (1进栈,1出栈;2进栈,2出栈;3进栈,3出栈,出栈序列:123)
132 (1 in, 1 out; 2 in, 3 in; 3 out, 2 out)
321 (1 in, 2 in, 3 in; 3 out, 2 out, 1 out)
231 (1 in, 2 in; 2 out, 3 in; 3 out, 1 out)
312 不可能出现
213 (1 in, 2 in; 2 out, 1 out; 3 in, 3 out)
栈的python实现
不需要自己定义,使用列表结构即可:
- 进栈:append
- 出栈:pop
- 查看栈顶:li[-1] 直接取索引
系统堆栈
系统堆栈比较复杂,这里只是简单体会一下,函数嵌套调用时的压栈和出栈:
def foo():
bar()
demo()
print('print form foo')
def bar():
demo()
print('print from bar')
def demo():
print('print from demo')
foo()
在IED的debug模式下调用foo函数,观察如下:
- 第13行,调用foo,foo入栈:[foo]
- 第2行,foo中调用bar,bar入栈:[foo, bar]
- 第7行,bar中调用demo, demo入栈:[foo, bar, demo]
- 第11行,demo打印完信息,执行完毕,demo出栈:[foo, bar]
- 第8行,bar打印完信息,执行完毕,bar出栈:[foo]
- 第3行,foo中调用demo,demo入栈:[foo, demo]
- 第11行,demo打印完信息,执行完毕,demo出栈:[foo]
- 第4行,foo打印完信息,执行完毕,foo出栈,栈空:[ ]
解释器分配程序运行的堆和栈的比例,程序中的变量保存在堆中,内存溢出指的就是堆溢出。栈用来保存函数调用上下文,一般程序嵌套层不会太多,因此堆的比例远大于栈的比例。当递归层数过多(死递归),超出栈的大小时,就会出现栈溢出。
栈的应用
括号匹配问题
给一个字符串,其中包含小括号、中括号、大括号,求该字符串中的括号是否匹配。比如:()()[]{}
匹配,([{()}])
匹配,[](
不匹配,[(])
不匹配:
def check(exp):
stack = [] # 创建栈
for char in exp:
if char in ['(', '[', '{']:
stack.append(char) # 压入栈
elif char == ')':
if stack and stack[-1] == '(':
stack.pop() # 弹出栈
else:
return False
elif char == ']':
if stack and stack[-1] == '[':
stack.pop()
else:
return False
elif char == '}':
if stack and stack[-1] == '{':
stack.pop()
else:
return False
if not stack: #如果栈为空,说明括号全部匹配
return True
return False
迷宫问题
给一个二维列表,表示迷宫(0表示通道,1表示围墙)。给出算法,求一条走出迷宫的路径。
maze = [
[1,1,1,1,1,1,1,1,1,1],
[1,0,0,1,0,0,0,1,0,1],
# ...
[1,1,1,1,1,1,1,1,1,1]
]
思路分析
- 在一个迷宫节点(x, y)上,可以进行四个方向的探查:上
maze[x-1][y]
, 下maze[x+1][y]
, 左maze[x][y-1]
, 右maze[x][y+1]
- 从一个节点开始,依次从四个方向,找下一个能走的点,当找不到能走的点时,退回上一个点寻找是否有其他方向的点。
- 创建一个空栈,首先将入口位置进栈。当栈不为空时循环:获取栈顶元素(当前位置),寻找下一个可走的相邻方块,如果找不到可走的相邻方块,说明当前位置是死胡同,进行回溯(当前节点位置出栈,看前面的节点点是否还有别的出路)
maze = [[1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 0, 0, 1, 0, 0, 0, 1, 0, 1],
[1, 0, 0, 1, 0, 0, 0, 1, 0, 1],
[1, 0, 0, 0, 0, 1, 1, 0, 0, 1],
[1, 0, 1, 1, 1, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 1, 0, 0, 0, 0, 1],
[1, 0, 1, 0, 0, 0, 1, 0, 0, 1],
[1, 0, 1, 1, 1, 0, 1, 1, 0, 1],
[1, 1, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]
# 对于maze中的元素,可以用maze[x][y]取值,x是行坐标,y就是列坐标
# 约定按四个方向探寻:
dirs = [lambda x, y: (x - 1, y), # 上
lambda x, y: (x + 1, y), # 下
lambda x, y: (x, y - 1), # 左
lambda x, y: (x, y + 1)] # 右
def maze_path(x1, y1, x2, y2):
"""
:param x1: 起点 行坐标
:param y1: 起点 列坐标
:param x2: 终点 行坐标
:param y2: 终点 列坐标
:return:
"""
stack = []
stack.append((x1, y1)) # 起点位置入栈
while stack: # 栈不为空时循环;栈空代表死迷宫(找遍所有的路,直到回退到起点位置,继续回退(出栈),栈空)
curNode = stack[-1] # 栈顶,表示当前位置
if curNode[0] == x2 and curNode[1] == y2: # 到达终点,栈中的元素就是最终路径
for node in stack: # 打印路径
print(node)
return True
for dir in dirs: # 依次进行4个方向的探寻
nextNode = dir(*curNode) # 下一个位置坐标
if maze[nextNode[0]][nextNode[1]] == 0: # 0代表通道,说明找到下一个方块
stack.append(nextNode) # 添加到栈顶,更新当前位置
maze[nextNode[0]][nextNode[1]] = -1 # 将0赋值为-1,标记为已经走过,防止死循环
break # 找到一个通道,退出
else: # 四个方向都找完,没有通道,那么开始回退
maze[curNode[0]][curNode[1]] = -1 # 标记死路
stack.pop() # 弹出栈顶,回退
print('死迷宫')
return False # 死迷宫
maze_path(1, 1, 8, 8)
通过栈解决迷宫问题,用的是回溯法(一条路走到黑,走不通则回退),是深度优先的模式。
队列
概念
队列(Queue)是一个数据集合,仅允许在列表的一端进行插入,另一端进行删除。
进行插入的一端称为队尾(rear),插入动作称为进队或入队。
进行删除的一端称为队头(front),删除动作称为出队。
队列的性质:先进先出(First-in, First-out)。
双向队列:队列的两端都允许进行进队和出队操作。
队列的实现
用列表可以简单的模拟队列:li= []
- 进队:li.append
- 出队:li.pop(0)
不过通过python列表模拟队列,效率太低了,时间复杂度是O(n)。下面我们看一下队列的底层实现原理。
实现思路
- 初步设想:数组 + 两个下标指针
- 创建一个数组和两个指针变量,front指向队首,rear指向队尾。初始时,front和rear都为0。
- 进队操作:元素写到li[rear]的位置,rear自增1
- 出队操作:返回li[front]的元素,front自减1
虽然这样的时间复杂读是O(1),但是这种实现有一个问题:
数组长度有限,当元素都出队后,rear和front值相同时,新元素无法入队了,即便是将列表开很大,但是front前面的空间都浪费了。解决方案:环形队列,将数组首尾逻辑上连接起来:
随着元素A, B, C, D,…的依次进队和出队,rear++和front++往后移,队列的长度是12,当有新元素依次进来时,超出队列长度的,只要让它想办法进入0的位置,就可以了。当队尾指针front == Maxsize + 1时,再前进一个位置就自动到0,实现方式是求余数运算:
队首指针前进1:front = (front + 1) % MaxSize
队尾指针前进1:rear = (rear + 1) % MaxSize
队空条件:rear == front
队满条件:(rear + 1) % MaxSize == front
对于队满的情况,需要额外处理,中间空一个,以区分rear和front。
deque
Python的deque模块可以实现队列,并且支持双向队列:
from collections import deque
li = [1,2,3,4]
queue = deque(li) # 从列表创建双向队列(也可以直接创建)
queue.append(5) # deque([1, 2, 3, 4, 5])
queue.popleft() # deque([2, 3, 4, 5])
# 双向队列对首进队,队尾出对
queue.appendleft('a') # deque(['a', 2, 3, 4, 5])
queue.pop() # deque(['a', 2, 3, 4])
另外,线程中的那个queue强调的是线程安全,与这个不一样。
用队列解决迷宫问题
还是上面的迷宫问题,如果用队列来实现,就是广度优先。
思路:从一个节点开始,寻找所有下面能继续走的节点。继续寻找,直到找到出口。
方法:
- 创建一个空队列,将起点位置进队。在队列不为空时循环:出队一次。如果当前位置为出口,则结束算法;否则找出当前方块的4个相邻方块中可走的方块,全部进队。
- 用一个二维数据结构path记录每一步是从哪一步来的(索引)。用-1 记录走过的路径 或 起始地方。每探寻一个节点,该节点出队,放到 path中,直到探寻到终点。最终通过索引,将路径串起来。
图示:
通过这种队列实现的广度优先寻找,最终路径一定是最短的。而之前的深度优先搜索,其结果具有一定的随机性,取决于探寻方向的顺序。
代码实现:
maze = [[1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 0, 0, 1, 0, 0, 0, 1, 0, 1],
[1, 0, 0, 1, 0, 0, 0, 1, 0, 1],
[1, 0, 0, 0, 0, 1, 1, 0, 0, 1],
[1, 0, 1, 1, 1, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 1, 0, 0, 0, 0, 1],
[1, 0, 1, 0, 0, 0, 1, 0, 0, 1],
[1, 0, 1, 1, 1, 0, 1, 1, 0, 1],
[1, 1, 0, 0, 0, 0, 0, 1, 0, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]
from collections import deque
def maze_path2(x1, y1, x2, y2):
queue = deque()
path = [] # 存放走过的
queue.append((x1, y1, -1))
# queue中元素:(1, 1, -1),前两位是坐标,第三位是其上一步在path中的索引。这里用-1标记起点。
maze[x1][y1] = -1
while queue:
curNode = queue.popleft()
path.append(curNode)
if curNode[0] == x2 and curNode[1] == y2:
print_path(path)
return True
for dir in dirs:
nextNode = dir(curNode[0], curNode[1])
if maze[nextNode[0]][nextNode[1]] == 0:
queue.append((*nextNode, len(path) - 1)) # len(path)-1) 记录来源索引
maze[nextNode[0]][nextNode[1]] = -1
return False
def print_path(path):
'''提取路径'''
curNode = path[-1]
realpath = []
print('迷宫路径为:')
while curNode[2] != -1:
realpath.append(curNode[0:2])
curNode = path[curNode[2]] # 通过索引找到上一步节点
realpath.append(curNode[0:2]) # 加入起点
realpath.reverse()
print(realpath)
maze_path2(1, 1, 8, 8)
链表
概念
链表中每一个元素都是一个对象,每个对象称为一个节点,包含有数据域key和指向下一个节点的指针next。通过各个节点之间的相互连接,最终串联成一个链表。
区别与数组/列表:数组和列表是连续存储的,删除和插入元素,其它元素需要补过去或者后移,时间复杂度是O(n);而链表则不会这样,它的时间复杂度是O(1)
定义节点和构造链表
# 定义节点类
class Node:
def __init__(self, val):
self.val = val
self.next = None
# 实例化节点
a = Node(1)
b = Node(2)
c = Node(3)
# 构造链表
a.next = b
b.next = c
# 取节点c的值
a.next.next.val #3
头节点
一般头不存元素(可以存链表的长度)
节点的遍历,插入和删除
遍历
def traversal(head):
curNode = head
while curNode is not None:
print(curNode.val)
curNode = curNode.next
插入
p = Node(5)
p.next = curNode.next # 先将插入节点与当前节点的下一个节点连起来
curNode.next = p # 再将当前节点与插入节点连起来
删除
P = curNode.next # 定位删除节点
curNode.next = curNode.next.next # 将当前节点与删除节点的下一个节点连起来
del p # 删除节点
建立链表
头插法
def create_link_head(li):
"""头插法:每个新节点,都插到头节点的后面"""
head = Node()
for num in li:
s = Node(num)
s.next = head.next
head.next = s
return head
head = create_link_head([1,2,3,4])
traversal(head)
""" 遍历链表
None
4
3
2
1
"""
尾插法
def create_link_tail(li):
"""尾插法:每个新节点,都插到最后一个节点后面"""
head = Node()
tail = head # tail指向尾节点
for num in li:
s = Node(num)
tail.next = s
tail = s
return head
head = create_link_tail([1,2,3,4])
traversal(head)
""" 遍历链表
None
1
2
3
4
"""
双链表
双链表中每个节点有两个指针:一个指向后面节点、一个指向前面节点。
定义节点
class Node:
def __init__(self, val=None):
self.val = val
self.next = None
self.prev = None
双链表节点的插入和删除
插入
p = Node(2)
# step1: 将插入节点与当前节点的下一个节点连起来
p.next = curNode.next
curNode.next.prev = p
# step2: 将插入节点与当前节点连起来
p.prev = curNode
curNode.next = p
删除
p = curNode.next
# step1: 当前节点与删除节点的下一个节点连起来
curNode.next = p.next
p.next.prev = curNode
# step2: del
del p
建立双链表
def create_dlink_tail(li):
head = Node()
tail = head
for num in li:
s = Node(num)
s.prev = tail
tail.next = s
tail = s
return head, tail
# 双向链表可以从两个方向遍历
def traversal_head(head):
curNode = head
while curNode is not None:
print(curNode.val)
curNode = curNode.next
def traversal_tail(tail):
curNode = tail
while curNode is not None:
print(curNode.val)
curNode = curNode.prev
head, tail = create_dlink_tail([1,2,3,4])
traversal_head(head) # None 1 2 3 4
traversal_tail(tail) # 4 3 2 1 None
二叉数就是链式存储
这里推荐一个网站:https://visualgo.net/zh 数据结构和算法动态可视化。
该图可以用如下代码表示
class BTNode:
"""二叉树(binary tree)节点类"""
def __init__(self, val):
self.val = val
self.lchild = None
self.rchild = None
a = BTNode(1)
b = BTNode(2)
c = BTNode(3)
d = BTNode(4)
a.lchild = b
b.rchild = c
b.lchild = d
二叉搜索树
也叫二叉查找树(Binary Search Tree)是一种特殊的二叉树,它改善了二叉树节点查找的效率。满足以下两个特征的就是二叉搜索树
对于任意一个节点 n:
- 其左子树(left subtree)下的每个后代节点(descendant node)的值都小于节点 n 的值;
- 其右子树(right subtree)下的每个后代节点的值都大于节点 n 的值。
比如:
二叉搜索树的查找复杂度是O(N),每次查找规模小一半。
二叉树遍历
二叉树遍历有三种方式:前序,中序,后序,也称为前根序遍历,中根序遍历,后根序遍历(这样可能还好理解一点,前中后本来就是相对于根结点来说的)
前根序遍历:先遍历根结点,然后遍历左子树,最后遍历右子树:根 > 左 > 右
ABDHECFG
中根序遍历:先遍历左子树,然后遍历根结点,最后遍历右子树: 左 > 根 >右
HDBEAFCG
后根序遍历:先遍历左子树,然后遍历右子树,最后遍历根节点:左 > 右 > 根
HDEBFGCA
注意:遍历是递归的,只要是有子节点的,就作为根对待,依据上述方式遍历。
二叉搜索树查找用的是中根序遍历。
这一部分参考了:http://blog.csdn.net/prince_jun/article/details/7699024
python中的集合与字典
哈希表
哈希表(Hash Table,又称为散列表),是一种线性的存储结构(类似于数组),根据关键码的值(key, value)直接进行访问。将对象的Key通过一个哈希函数hash(key)换成一个整型数字,然后将该数字对数组长度进行取余,取余结果就当作数组的下标,将对象的值value存储在以该数字为下标的数组空间里。(哈希函数又称为散列函数)
哈希函数的种类很多,这里不作研究。
例如:
数据集合{1,6,7,9},假设存在哈希函数hash(x)使得hash(1) = 0, hash(6) = 2, hash(7) = 4, hash(9) = 5,那么这个哈希表被存储为[1,None, 6, None, 7, 9]。当我们查找6所在的位置时,通过哈希函数获得该元素在哈希表中的下标(hash(6)=2),然后在下标为2的位置取出该元素。
哈希表查询速度非常的快,几乎是O(1)的时间复杂度。
哈希冲突
由于哈希表的下标范围是有限的,而元素关键字的值是接近无限的,因此可能会出现h(102) = 56, h(2003) = 56这种情况。此时,两个元素映射到同一个下标处,造成哈希冲突。
解决方案:
拉链法:将所有冲突的元素用链表连接
左边个数组,数组的每个成员包括一个指针,指向一个链表的头,当然这个链表可能为空,也可能元素很多。我们根据元素的一些特征把元素分配到不同的链表中去,也是根据这些特征,找到正确的链表,再从链表中找出这个元素。
开放寻址法:通过哈希冲突函数得到新的地址。在此不讨论。
python中的字典
在Python中,给出一个字典:d = {'name': 'lena', 'age': '18', 'hobby': '瓜子'}
,使用哈希表存储字典,通过哈希函数将字典中元素的key映射为数组/列表下标。假如hash(‘name’)=3, hash(‘age’)=1, h(‘hobby’)=4, 则哈希表存储为[None, 18, None, 'Lena', '瓜子']
。
在字典键值对数量不多的情况下,几乎不会发生哈希冲突,此时查找一个元素的时间复杂度为O(1)。
最后
以上就是飞快鱼为你收集整理的Python数据结构之列表、栈、队列、链表、字典列表栈队列链表python中的集合与字典的全部内容,希望文章能够帮你解决Python数据结构之列表、栈、队列、链表、字典列表栈队列链表python中的集合与字典所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复