我是靠谱客的博主 飞快鱼,这篇文章主要介绍Python数据结构之列表、栈、队列、链表、字典列表栈队列链表python中的集合与字典,现在分享给大家,希望可以做个参考。

什么是数据结构

简单来说,数据结构就是设计数据以何种方式组织并存储在计算机中。比如:列表、集合与字典等都是一种数据结构。

列表

python的列表在其他编程语言中称为“数组”,不过二者是有区别的。在C语言中,必须指定数组的长度,并且数组中的元素具有相同的数据类型。而python中则没有这些限制,这是因为二者在内存中存储方式不一样。

数组

我们知道,计算机将内存分割为字节,每个字节可以存储8位的二进制信息:

1.png

每个字节都具有唯一的地址。如果内存中有n个字节,那么可以把地址看作0 ~ n-1的数,并用指针变量来存储地址,也就说,指针就是地址

2.png

程序中的每个变量在内存中占有一个或多个字节,把第一个字节的地址称为是该变量的地址。

用C语言新建一个整型数组int a[4]={10, 12, 16, 33}每个数字用4个字节表示,就相当于在内存中开辟长度为20的连续空间,如图所示。那么数组名a,就是指向该数组第一个元素的指针,对数组元素的取值只需要对指针进行算术运算即可:& 取地址运算符,&a就是a在内存中的地址,2000;* 间接寻址运算符(访问指针所指向对象的内容),这里*a就表示指针a当前所指向的对象的值,也就是数组中第一个元素10;通过对指针a进行自增,就可访问到数组中的所有元素,*(a+1)就可以取到数组第二个元素的值,12:

4.png

a+i就等同于&a[i],两者都表示指向数组a中第i个元素的指针,并且*(a+1)等价于a[i],两者都表示第i个元素的值。因此,数组的取下标操作就是指针指针算术运算的一种形式

列表

python中的列表可以混合存储任意数据类型,那么它是如何实现的呢?因为列表中存的是元素的内存地址,而不是元素的值,比如有列表li = [1, True, 'hello', {'k': 'v'}]。li中的元素在内存中并不是连续存储的,因此li存储这些元素所在的内存地址,对元素取值时,先在列表中找到元素的内存地址,再通过内存地址找到元素:

3.png

因此,比起数组,对列表元素的取值要慢一些。

另外,列表没有长度限制是如何实现的呢?当我们通过append方法向列表中添加元素时,如果列表满了,那么新申请2倍于原来列表的内存地址,并将原列表的值拷贝到新的内存地址中。

列表的删除,插入的时间复杂度是O(N)

介绍

什么是栈

栈(Stack)是一个数据集合,可以理解为只能在一端进行插入或删除操作的列表。

栈的特点

后进先出(last-in, first-out)

栈的基本操作

进栈(压栈):Push;出栈:pop;查看栈顶:gettop

5.png

出栈次序问题

一个栈的进栈序列为1,2,3,问下面哪种出栈序列不可能出现?

复制代码
1
2
3
4
5
6
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实现

不需要自己定义,使用列表结构即可:

  1. 进栈:append
  2. 出栈:pop
  3. 查看栈顶:li[-1] 直接取索引

系统堆栈

系统堆栈比较复杂,这里只是简单体会一下,函数嵌套调用时的压栈和出栈:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
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函数,观察如下:

  1. 第13行,调用foo,foo入栈:[foo]
  2. 第2行,foo中调用bar,bar入栈:[foo, bar]
  3. 第7行,bar中调用demo, demo入栈:[foo, bar, demo]
  4. 第11行,demo打印完信息,执行完毕,demo出栈:[foo, bar]
  5. 第8行,bar打印完信息,执行完毕,bar出栈:[foo]
  6. 第3行,foo中调用demo,demo入栈:[foo, demo]
  7. 第11行,demo打印完信息,执行完毕,demo出栈:[foo]
  8. 第4行,foo打印完信息,执行完毕,foo出栈,栈空:[ ]

解释器分配程序运行的堆和栈的比例,程序中的变量保存在堆中,内存溢出指的就是堆溢出。栈用来保存函数调用上下文,一般程序嵌套层不会太多,因此堆的比例远大于栈的比例。当递归层数过多(死递归),超出栈的大小时,就会出现栈溢出。

栈的应用

括号匹配问题

给一个字符串,其中包含小括号、中括号、大括号,求该字符串中的括号是否匹配。比如:()()[]{}匹配,([{()}])匹配,[](不匹配,[(])不匹配:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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表示围墙)。给出算法,求一条走出迷宫的路径。

复制代码
1
2
3
4
5
6
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] ]

6.png

思路分析

  1. 在一个迷宫节点(x, y)上,可以进行四个方向的探查:上maze[x-1][y], 下maze[x+1][y], 左maze[x][y-1], 右maze[x][y+1]
  2. 从一个节点开始,依次从四个方向,找下一个能走的点,当找不到能走的点时,退回上一个点寻找是否有其他方向的点。
  3. 创建一个空栈,首先将入口位置进栈。当栈不为空时循环:获取栈顶元素(当前位置),寻找下一个可走的相邻方块,如果找不到可走的相邻方块,说明当前位置是死胡同,进行回溯(当前节点位置出栈,看前面的节点点是否还有别的出路)
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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)。

双向队列:队列的两端都允许进行进队和出队操作。

7.png

队列的实现

用列表可以简单的模拟队列:li= []

  1. 进队:li.append
  2. 出队:li.pop(0)

不过通过python列表模拟队列,效率太低了,时间复杂度是O(n)。下面我们看一下队列的底层实现原理。

实现思路

  1. 初步设想:数组 + 两个下标指针
  2. 创建一个数组和两个指针变量,front指向队首,rear指向队尾。初始时,front和rear都为0。
  3. 进队操作:元素写到li[rear]的位置,rear自增1
  4. 出队操作:返回li[front]的元素,front自减1

8.png

虽然这样的时间复杂读是O(1),但是这种实现有一个问题:

数组长度有限,当元素都出队后,rear和front值相同时,新元素无法入队了,即便是将列表开很大,但是front前面的空间都浪费了。解决方案:环形队列,将数组首尾逻辑上连接起来:

9.png

随着元素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模块可以实现队列,并且支持双向队列:

复制代码
1
2
3
4
5
6
7
8
9
10
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强调的是线程安全,与这个不一样。

用队列解决迷宫问题

还是上面的迷宫问题,如果用队列来实现,就是广度优先。

思路:从一个节点开始,寻找所有下面能继续走的节点。继续寻找,直到找到出口。

方法:

  1. 创建一个空队列,将起点位置进队。在队列不为空时循环:出队一次。如果当前位置为出口,则结束算法;否则找出当前方块的4个相邻方块中可走的方块,全部进队。
  2. 用一个二维数据结构path记录每一步是从哪一步来的(索引)。用-1 记录走过的路径 或 起始地方。每探寻一个节点,该节点出队,放到 path中,直到探寻到终点。最终通过索引,将路径串起来。

图示:

10.png

11.png

12.png

通过这种队列实现的广度优先寻找,最终路径一定是最短的。而之前的深度优先搜索,其结果具有一定的随机性,取决于探寻方向的顺序。

代码实现:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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)

定义节点和构造链表

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 定义节点类 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

头节点

13.png

一般头不存元素(可以存链表的长度)

节点的遍历,插入和删除

遍历

复制代码
1
2
3
4
5
def traversal(head): curNode = head while curNode is not None: print(curNode.val) curNode = curNode.next

插入

复制代码
1
2
3
4
5
p = Node(5) p.next = curNode.next # 先将插入节点与当前节点的下一个节点连起来 curNode.next = p # 再将当前节点与插入节点连起来

删除

复制代码
1
2
3
4
P = curNode.next # 定位删除节点 curNode.next = curNode.next.next # 将当前节点与删除节点的下一个节点连起来 del p # 删除节点

建立链表

头插法

16.png

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 """

尾插法

17.png

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 """

双链表

双链表中每个节点有两个指针:一个指向后面节点、一个指向前面节点。

18.png

定义节点

复制代码
1
2
3
4
5
class Node: def __init__(self, val=None): self.val = val self.next = None self.prev = None

双链表节点的插入和删除

19.png

插入

复制代码
1
2
3
4
5
6
7
p = Node(2) # step1: 将插入节点与当前节点的下一个节点连起来 p.next = curNode.next curNode.next.prev = p # step2: 将插入节点与当前节点连起来 p.prev = curNode curNode.next = p

删除

复制代码
1
2
3
4
5
6
p = curNode.next # step1: 当前节点与删除节点的下一个节点连起来 curNode.next = p.next p.next.prev = curNode # step2: del del p

建立双链表

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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 数据结构和算法动态可视化。

20.png

该图可以用如下代码表示

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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:

  1. 其左子树(left subtree)下的每个后代节点(descendant node)的值都小于节点 n 的值;
  2. 其右子树(right subtree)下的每个后代节点的值都大于节点 n 的值。

比如:

21.png

二叉搜索树的查找复杂度是O(N),每次查找规模小一半。

二叉树遍历

二叉树遍历有三种方式:前序,中序,后序,也称为前根序遍历,中根序遍历,后根序遍历(这样可能还好理解一点,前中后本来就是相对于根结点来说的)

22.png

  1. 前根序遍历:先遍历根结点,然后遍历左子树,最后遍历右子树:根 > 左 > 右

    ABDHECFG

  2. 中根序遍历:先遍历左子树,然后遍历根结点,最后遍历右子树: 左 > 根 >右

    HDBEAFCG

  3. 后根序遍历:先遍历左子树,然后遍历右子树,最后遍历根节点:左 > 右 > 根

    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这种情况。此时,两个元素映射到同一个下标处,造成哈希冲突。

解决方案:

  1. 拉链法:将所有冲突的元素用链表连接

    23.png

    左边个数组,数组的每个成员包括一个指针,指向一个链表的头,当然这个链表可能为空,也可能元素很多。我们根据元素的一些特征把元素分配到不同的链表中去,也是根据这些特征,找到正确的链表,再从链表中找出这个元素。

  2. 开放寻址法:通过哈希冲突函数得到新的地址。在此不讨论。

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中内容请搜索靠谱客的其他文章。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(75)

评论列表共有 0 条评论

立即
投稿
返回
顶部