上篇文章 数据结构系列:列表?线性表?这俩货到底什么关系?我们主要介绍了线性表的经典实现之一:列表。本篇将主要分享栈和队列这两种线性表结构的特点和一些经典应用。代码都是基于python语言。
栈
- 概念
栈(stack)是一种数据结构,属于线性表的一种实现。既然是线性表,那么肯定数据之间也有一定的特征,栈的特征如下
- 数据项的加入和移除都限制在一端,这个端点一般称之为栈顶
- 专业点的描述就是:后进先出(LIFO),越早进入栈结构的数据越晚离开栈
- 生活中的例子(加深理解)
- 一摞盘子,想取的话只能先取最上面的一个,然后下一个,依次类推,但如果想放新的盘子上去,也只能将新盘子放在所有盘子的最上面,然后再摞一个,以此类推。这个(顶端)用来拿或者取盘子的位置,抽象出来一个概念,称之为栈顶。盘子最下面不参与任何的操作,称之为栈底。
- 子弹夹,永远只能从一端压入子弹,且从同一端弹出子弹。这样的结果就是,最先压进去的子弹会最后才被弹出,而最后压进去的子弹会最先弹出,这个弹出的位置就称之为栈顶
- 栈的实现(基于python)
栈的底层同样可以基于数组或者链表实现,但这里为了简单,就基于python的内置结构列表构造一个简单的栈结构。你要知道,作为任何数据结构,用什么实现不重要,重要的是你设计出来的栈结构的接口要满足后进先出的结构特点
# 我们这里简单的用python内置列表作为基础结构实现栈结构class Stack:"""利用列表(底层为数组)构建栈的抽象结构""" def __init__(self): self._items = [] def size(self): return len(self._items) def push(self, item): self._items.append(item) def pop(self): return self._items.pop() def is_empty(self): return len(self._items) == 0 def peek(self): return self._items[len(self._items) - 1]
这里要强调一下,虽然从代码上看,确实满足了栈的特点,但这并不是一个严格意义上的栈,因为内部的一些列表方法都是可以使用的,而在抽象结构中这些方法应该谨慎地使用。
- 栈的经典应用
- 编译器基本功能中的对于各种括号("(){}[]")的正确匹配
- 程序运行中作为数据的隔离
- 回溯算法
- 管理计算机内存支持函数和方法的调用
- 中缀表达式转换为后缀表达式;计算后缀表达式(思路如下)
-如果遇到操作数,我们就直接将其输出
-如果遇到操作符,则我们将其放入到栈中,遇到左括号时我们也将其放入栈中。
-如果遇到一个右括号,则将栈元素弹出,将弹出的操作符输出直到遇到左括号为止。注意,左括号只弹出并不输出。
-如果遇到任何其他的操作符,如(“+”, “*”,“(”)等,从栈中弹出元素直到遇到发现更低优先级的元素(或者栈为空)为止。弹出完这些元素后,才将遇到的操作符压入到栈中。有一点需要注意,只有在遇到" ) “的情况下我们才弹出” ( “,其他情况我们都不会弹出” ( "
-如果我们读到了输入的末尾,则将栈中所有元素依次弹出
我这里分享两个通过栈结构实现的经典应用:进制转换和中缀转后缀表达式
- 10进制转换为2/8/16进制的字符串格式
# 10进制转换为2/8/16进制的字符串格式def to_str(n, base): :param n: 十进制数 :param base: 想要转换的进制 :return: if n < base: return str(n) elif 2 <= base <= 10: return to_str(n // base, base) + str((n % base)) else: convert_string = "0123456789abcdef" return to_str(n // base, base) + convert_string[(n % base)]ret = to_str(541, 2)print(ret)
- 中缀转后缀表达式
# 中缀转后缀表达式def infix_to_suffix(expressions): """当前约定中缀表达式是以空格进行分割, exp: a + b * c""" s = Stack() exp_list = expressions.split(" ") # 存放操作数 num_list = [] # 自定义优先级,"("的存在是为了有多重括号存在时,保证所有的+-*/都可以顺利入栈 priority_dic = {"(": 1, "+": 2, "-": 2, "*": 3, "/": 3} for token in exp_list: if token not in "+-*/()": num_list.append(int(token)) elif token == "(": s.push(token) elif token == ")": while s.peek() != "(": num_list.append(s.pop()) # 将"("弹出 s.pop() else: while s.size() > 0 and priority_dic.get(token) <= priority_dic.get(s.peek()): num_list.append(s.pop()) s.push(token) # 检查栈中是否还有操作符存在,有的话,依依弹出即可 while not s.is_empty(): num_list.append(s.pop()) return "".join([str(i) for i in num_list])
- 计算后缀表达式
# 计算后缀def calc_suffix(expression): s = Stack() for token in expression: if token in "0123456789": s.push(int(token)) else: # 要考虑到减法和除法顺序不能颠倒的问题 num_first = s.pop() num_second = s.pop() tmp_result = convert_to_math(token, num_first, num_second) s.push(tmp_result) return s.pop()# 辅助函数,每两个弹出元素的计算结果def convert_to_math(token, num1, num2): if token == "*": return num1 * num2 elif token == "/": return num2 / num1 elif token == "-": return num2 - num1 elif token == "+": return num1 + num2
- 测试代码如下
# 测试数据convert_ret = infix_to_suffix("3 + 5 * 2 - 5 * ( 3 + 2 )")calcu_ret = calc_suffix(convert_ret)print(calcu_ret)
队列
队列是一种有次序的数据集合,表现为向队列中添加数据时,永远在尾端添加,而移除数据则永远从队首删除,专业化的表述为:先进先出的结构特点(FIFO),越早进入栈结构的数据越早离开栈
- 生活中的例子(加深理解)
- 排队买票,一般情况下,你应该从买票队伍的最后一个位置进入列队
- 一台打印机面向多个用户或是程序的情况
- 队列实现
队列也可以由数组或是链表结构设计实现,数组的实现(循环数组的方法)要难于链表,但是性能更优,这里还是基于python的列表实现
# 利用内置数据类型列表构建队列数据结构class Queue(): def __init__(self): self._items = [] def size(self): return len(self._items) def enqueue(self, data): self._items.insert(0, data) def dequeue(self): return self._items.pop() def is_empty(self): return len(self._items) == 0
- 经典应用
- 模拟打印机处理多任务
- 模拟道路交通,超市结账排队情况等
- CPU访问
- 多个进程访问同一个CPU,新进程会被添加到队列,这个队列就是等待CPU使用的进程
- 经典实现
约瑟夫/热土豆理论
热土豆问题:几个小朋友,围成一圈,用一个热土豆(或别的什么东西),数一个数就抛给下一个人,每数到3,手上有土豆的人就站出来,然后继续,问哪个位置的人最后剩下?
# 热土豆问题逻辑代码def hot_potato(lists, num): q = Queue() # 按人名将数据项入队 for person in lists: q.enqueue(person)# 直到只剩下一个人,游戏结束 while q.size() > 1: for i in range(num): q.enqueue(q.dequeue()) q.dequeue()# 将最后剩下的人返回 return q.dequeue()# 模拟热土豆的实例,参数分表代表参加游戏的人名以及土豆每传递几次就出队一个人last_person = hot_potato(["A", "B", "C", "D", "E", "F", "G", "H", "I", "L", "M", "N", "O"], 3)print(last_person)
生产者消费者模型
这应该是最常用的了,经常要构建消息队列
import threadingimport queue def producer(): """ 模拟生产者 :return: """ for i in range(10): print("生产包子%s" %i) q.put("包子 %s" % i) print("开始等待所有的包子被取走...") q.join() # 等待这个包子队列被消费完毕 print("所有的包子被取完了...") def consumer(n): """ 模拟消费者 :return: """ while q.qsize() > 0: print("%s 取到" % n, q.get()) q.task_done() # 每取一个包子,便告知队列这个任务执行完了 q = queue.Queue() p = threading.Thread(target=producer,)p.start() c1 = consumer("xiaochen")
总结
本文基于python,分享了关于栈结构和线性表的相关实现和特点,并辅以相关的代码用各自的结构实现经典应用。希望你对这两种线性表有了一定的了解(数据结构是干嘛的,就是提高效率的,某些场景通过数据结构的套路辅以相关的算法,就会容易很多)
我是一名奋战在编程界的pythoner,工作中既要和数据打交道,也要和erp系统,web网站保持友好的沟通……时不时的会分享一些提高效率的编程小技巧,在实际应用中遇到的问题以及解决方案,或者源码的阅读等等,欢迎大家一起来讨论!如果觉得写得还不错,欢迎关注点赞,谢谢。