python内置的数据结构
事实上python内置的两种结构是及其先进且易用的.
list
增加元素
append(x)添加元素extend(l)将l中所有元素添加入其中insert(i, x)在i处添加元素x
删除元素
remove(x)删除元素x,若没有该元素会出现ValueErrorpop([i])删除i处元素(默认最后一个),并返回这个元素clear()删除所有元素
查找元素
index(x[, start[, end]])寻找x,返回其位置k,其中 $start \leq k < end$.(同样有ValueError错误)count(x)统计x出现的次数
修改
sort()排序reverse()倒序
其他
copy()返回一个一样的list
dict
添加元素
update(dict2)将dict2加入其中
删除元素
popitem()随机删除一对,并返回(key, value)pop(key [,default])删除key及对应的值,并返回值,若不存在则返回defaultclear()删除所有元素
查看键或值
keys()返回所有键的列表values()返回所有值的列表item()返回由(key, value)组成的列表has_key(key)查找是否存在该键dict.get(key, default=None)返回值,不存在则返回default
其他
copy()fromkeys(seq[, value])以seq为键,value为值建一个字典,其中value默认为None
线性数据结构
每个元素的前后元素始终保持不变数据结构被称为线性数据结构.
常见的有: 栈,队列,双向队列,列表.
栈(stack)
其特点是后进先出(LIFO, Last In First Out).
栈的抽象数据类型
Stack()创建一个空的新栈,并返回一个空栈.push(x)将x添加到栈的顶部.pop()从栈中删除顶部的元素,并返回这个元素.peek()从栈返回顶部元素,但不会删除它.isEmpty()返回栈是否为空.size()返回栈中的元素的数量.
栈的python实现
这里仅为我个人的实现.
1 | class Stack: |
栈的实际用处
括号匹配
每次看到栈,就免不了括号匹配这个问题.
对于合法的括号嵌套,总是一层一层的,这意味着栈可以很好的保存它们.
1 | def par_checker(s): |
输出结果:
1 | True |
符号匹配
比刚刚的难度要大一些,这里需要匹配的还有[]和{}.
1 | def par_checker_2(s): |
简单的修改即可,输出结果:
1 | True |
十进制转二进制
转进制通常使用除法,且每次的余数恰好是从左到右.
用下标表示进制,那么:
$$150_{(10)} = 10010110_{(2)}$$
1 | def base_2_converter(x): |
可惜这个程序不能转换0.
如果是其他进制(16以下),可以写一个统一的程序:
1 | def base_converter(x, base = 2): |
中缀表达式转后缀表达式(前缀表达式)
中缀表达式转后缀表达式
这里有简单的案例,后缀和前缀不需要括号就能表明计算的顺序.
| 中缀表达式 | 后缀表达式 | 前缀表达式 |
|---|---|---|
| 5 + 6 | 5 6 + | + 5 6 |
| 5 + 6 * 8 | 5 6 8 * + | + 5 * 6 8 |
| (5 + 6) * 8 | 5 6 + 8 * | * + 5 6 8 |
可以看到,三种表达式的数字顺序都没有改变,也就是说只有运算符发生了变化.因此,转换时可以直接保存数字,而将运算符全都压入栈中,利用优先级决定弹出的顺序.
1 | def infix2postfix (infixStr): |
这里的实例是中缀转后缀,但是在实现过程中务必注意几个问题:
- 括号的问题,
(和)都是需要特殊处理的. - 最后栈可能没有清空,所以为了清空,在最后面加上一个
@或者=,并且当作一个低优先级符号处理比较方便.(当然最后搞个while把栈弹空也不是不行,但这样就显得代码有些长了.) - 数字的问题.如果只用识别
"123456789"或者"ABCDEFG"的话,数字比较好处理,但是如果利用符号作为间隔来识别数字,要注意两个符号相连和表达式开头就是(的情况.
#### 利用后缀表达式求值
其实方法是完全一样的,只不过需要一个数字栈.在符号弹出之后,直接把两个数字进行计算再压入栈中.
1 | from Stack import Stack |
只要运算的式子是正确的,就应该能算出正确结果了.
和转后缀的区别主要在于加入List变为实际的运算.
这里也有几个需要注意的小问题:
- 注意实际的运算是左结合还是右结合,尤其是 5^6 和 5-6 这样不满足交换律的运算.
- 最后的结果在栈中,当然也可以用
@或=来把结果弹出,如果里面的数字不止一个,那肯定出现了问题. - 向上面那样在传参时直接调用
pop()是一件极其糟糕的事情,极其不推荐.
队列(queue)
其特点是后进先出(FIFO, First In First Out).
队列的抽象数据类型
Queue()创建一个空的新队列,并返回一个空队列.enqueue(x)将x添加到队列的尾部.dequeue()从队列中删除头部的元素,并返回这个元素.isEmpty()返回队列是否为空.size()返回队列中的元素的数量.
队列的python实现
这里有个显然的问题,就是队列的开头在哪里.若规定0是开头,那么出队将会是 $\mathcal{O(n)}$,而入队则是$\mathcal{O(1)}$
1 | class Queue: |
实际上我只是将栈的实现做了极其简单的改动.
队列的实际用处
约瑟夫问题
一群人围成圈按1~7报数,其中报到7就出局,问最后一个出局的人是谁.
这个问题用队列是个极其简单的方法,不过真实的队列模拟实际上有些浪费.
1 | def solve(num, people): |
打印机
这是一个有些复杂的模拟了.
平均每天大约10名学生在任何给定时间打印两次,这些任务的长度范围从1到20页.每分钟以草稿质量可以处理10页,而高质量则只能每分钟处理5页.选择这两种速率的主要依据是平均等待时间,也就是需要模拟来计算的量.
建模实际有些麻烦.
首先是随机的任务.学生的任务是随机的,随机的时间和随机的页数(1~20),这需要一个额外的程序来生成.
接着是对每一秒进行模拟.
- 创建打印任务.并将任务的时间戳添加到队列.
- 分配给打印机任务.
- 计算这个任务的等待时间,并加入到另一个列表中等待处理.
最后是计算.从生成的等待时间列表中计算平均等待时间.
在这里,则需要几个类来着手解决了.
Printer
成员:
rate打印速率,1代表6秒一张,2代表12秒一张.task当前任务,没有时设为None.remain剩余时间.
函数:
__init__(pagerate)初始化三个成员,并设置速度.isBusy()返回是否繁忙.start(newtask)开始新任务.tick()让时间走一秒,并更改相关参数.
1 | class Printer: |
这里的start需要等到Task写好再解决.
Task
这个类用来表示单个的任务.
成员:
time任务开始排队的时间.pages打印的页数.
函数:
__init__(time)初始化成员.waitTime(now)计算任务的等待时间.
1 | import random |
接着就可以补写Printer中的start()了:
1 | def start(self, newtask): |
开始模拟
模拟的过程很简单,按照上面写步骤的即可进行.
1 | import random |
两种速度的输出大致如下:
高质量:
1 | 平均等待时间:74.28秒.剩余5个任务. |
高速度:
1 | 平均等待时间:40.71秒.剩余0个任务. |