news 2026/10/31 7:33:12

栈和队列区别详解:后进先出、先进先出与真实应用场景

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
栈和队列区别详解:后进先出、先进先出与真实应用场景

简介:这是一份面向数据结构初学者与算法练习者的“列车进站”调度编程资料,围绕主铁轨与辅助铁轨构成的“丁”字形调度场景,要求利用栈与队列思想把任意入站车厢序列调整为1~n顺序出站,适合课程实验、栈队列综合练习与调度过程分析。压缩包共9个文件,约128KB,包含C++源程序、头文件、工程配置及可执行文件等,其中源代码负责核心调度逻辑,头文件组织栈、队列、节点与辅助操作,工程文件便于直接编译调试。现有1942人学习,说明该题具有较高的课程实验参考价值。资料完整呈现在限定轨道规则下的调度求解思路、数据结构组织方式与可运行工程结构,便于对照理解入栈、出栈、入队、出队及状态转换,也可作为同类铁路车厢重排问题的实现参考。

1. 列车进站:栈和队列到底在说什么

去火车站坐车,有两件小事几乎人人都碰过:买票要排队,先来的先买到;进站台时碰上尽头式的,最后开进去的那列车,往往得第一个倒出来。这两件再普通不过的事,恰好把计算机里最基础的两个概念演了个遍——栈(Stack)是后进先出,队列(Queue)是先进先出。名字听着像玄学,其实它规定的只有一件事:谁先来、谁先走。搞懂这两条规则,很多看着复杂的东西——撤销按钮、括号匹配、打印任务排队、函数调用——背后都是同一个道理在支撑。这份资料挑了“列车进站”这个场景做引子,把两个结构从形状讲到用途,读完再回头看代码里的 stack 和 queue,就不会再发懵。

2. 栈为什么“后进先出”:从一叠盘子到一节尽头式站台

2.1 尽头式站台:只能从同一头进出

打个比方。老式火车站的尽头式站台,铁轨到站台尽头就断了,只有一个进出口。列车一辆接一辆开进去,第一辆停在最里面,第二辆停在它外面,第三辆再停在外面。等到要发车的时候,最外面那辆——也就是最后开进来的那辆——必须先走,否则里面的车根本出不来。想拿最里面那辆车?对不起,得先把压在上面的车一辆辆挪走。

栈就是这么一个结构。它的正式说法是:只允许在一端进行插入和删除操作的线性表。允许操作的那一头叫栈顶,另一头叫栈底。往栈顶放元素叫入栈(push),从栈顶拿走元素叫出栈(pop)。入和出都只在栈顶发生,栈底是封死的,你连看都看不到。

这里有个很容易被忽略的细节:栈顶不是一个“位置”,而是一个“边界”。每次入栈,栈顶就往上挪一格;每次出栈,栈顶就往下退一格。栈里的元素数量叫栈的深度,深度为零的时候叫空栈。空栈再出栈就会出错,这个错在程序里通常叫“栈下溢”;反过来,如果栈的容量固定且已经装满了还继续入栈,就叫“栈上溢”。两个词听着唬人,其实就是“没东西可拿了”和“装不下了”。

一叠洗干净扣在沥水架上的盘子,是另一个常见的比喻:你总是拿最上面那个,也总是把新洗的放在最上面。盘子越多,你想拿到最底下那个就越费劲,因为要先把上面全部搬开。栈的所有脾气,都写在这个动作里了。

2.2 后进先出不是反直觉,它是“回到上一步”的刚需

很多人第一次听“后进先出”会觉得别扭:凭什么后来的反而先走?这不公平啊。但换个角度就顺了——栈要解决的不是“公平”,而是“回到上一步”。

最典型的场景是撤销。你在编辑器里依次做了三件事:加粗(A)、改字号(B)、换颜色(C)。现在你按一次撤销,应该撤掉哪个?当然是 C,因为那是你最后做的。再按一次,撤掉 B;再按一次,撤掉 A。这个顺序恰好就是 C → B → A,也就是后进先出。要是撤销按先进先出的顺序来,第一次按下去撤的是最早那个加粗,你大概会当场翻车——明明刚改的是颜色,撤销却动了三步之前的操作。

括号匹配是另一个干净的例子里。写代码或者写公式时,括号必须成对且正确嵌套:({[]})是合法的,({[}])就乱了。判断办法就是拿一个栈:遇到左括号就入栈,遇到右括号就看栈顶是不是对应的左括号,是就出栈,不是就说明错了;全部扫完后栈必须是空的。为什么必须用栈?因为最后打开的那个括号,必须最先被闭合。这正是后进先出。

函数调用也是同一套逻辑。函数 A 里调用 B,B 里调用 C,程序会先把 A 的现场压进一个“调用栈”,再压 B 的,再压 C 的。C 执行完,最先返回的是 C,然后才是 B,最后才是 A。你看到报错信息里那一长串“调用堆栈”,就是栈里从顶到底的内容倒着打印出来的。理解了这一点,再看递归太深导致的“栈溢出”,就不是黑匣子了——那就是站台停满了,新车开不进去。

2.3 入栈、出栈、看栈顶:三个动作撑起整个结构

栈对外暴露的操作少得可怜,核心就三个:入栈、出栈、看栈顶。看栈顶(peek / top)和出栈的区别是:看只读不取,栈顶元素还在;出栈是读走并移除。此外通常还有判空和取大小两个辅助操作。

用 Python 的列表实现一个栈是最省事的做法,因为列表尾部的追加和删除本身就是 O(1):

# 用 Python 列表模拟栈:把列表尾部当作栈顶 stack = [] stack.append("列车A") # 入栈:A 停到站台最里面 stack.append("列车B") # 入栈:B 停在 A 外面 stack.append("列车C") # 入栈:C 停在最外面,成为新栈顶 top = stack[-1] # 看栈顶:只读不取,得到 "列车C" print(top, len(stack)) # 输出 列车C 3 out = stack.pop() # 出栈:C 最先离开,out 是 "列车C" print(out, stack) # 输出 列车C ['列车A', '列车B']

这段代码里最值得说的一句是“把列表尾部当作栈顶”。为什么不是头部?因为 Python 的pop()默认从尾部弹出,是 O(1);而pop(0)从头部弹出,要把后面所有元素整体前移一位,是 O(n)。数据一多,这个差别就很明显了。所以栈的实现要么用动态数组从尾部操作,要么用链表从头部操作,总之要保证入栈出栈都不搬家。

参数和边界也要交代清楚:append之前不用判断容量,Python 的列表会自动扩容;但如果你用固定长度的数组手写栈,就必须自己维护一个top下标,入栈前判满、出栈前判空。判断顺序搞反,是初学者最常见的翻车点之一——先pop()再判空,遇到空栈直接抛出异常。

提示:栈的入栈和出栈都只碰栈顶,所以只要实现得当,两个操作的时间复杂度都是 O(1)。任何需要“遍历整个栈才能完成”的操作,都说明你可能选错了结构。

2.4 栈的边界:它看得见顶上,看不见底下

栈的优点是简单、快、语义明确;它的局限也同样明确:只能访问栈顶。

这意味着两件事。第一,你没法随机访问。想直接取栈里第 3 个元素?做不到,只能把上面两个先弹出去,取完再压回去。第二,查找一个元素是不是在栈里,最坏要把整个栈清空一遍,是 O(n)。所以栈不适合“我要经常查某条记录”的场景,它适合“我只关心最近发生的那个”的场景。

还有一个隐藏的坑是容量。用链表或者动态数组实现的栈可以一直长,但函数调用栈是操作系统预先划好的一块固定内存,递归层数一深,它就撑不住了。这就是为什么写递归要有终止条件,也是为什么处理超深树的时候,有人宁愿手写一个显式的栈来模拟递归——把“系统替我压栈”换成“我自己压栈”,可控性更高。

3. 队列为什么“先进先出”:一条不许插队的通道

3.1 穿行式站台:从一头进、从另一头出

再回到火车站,这回换个站台。有些站台的铁轨是穿过去的,一头进、另一头出,列车从 A 口开进来,从 B 口开出去,中间不停留或者只短暂停靠。先进来的车先到出口,后进来的车排在后面。没有人能插队,因为轨道只有一条。

队列就是这个形状。正式说法是:只允许在一端插入、在另一端删除的线性表。允许插入的那头叫队尾(rear),允许删除的那头叫队头(front)。新元素从队尾进来叫入队(enqueue),从队头离开叫出队(dequeue)。

和栈对比一下就清楚了:栈是“同一头进出”,队列是“一头进、另一头出”。这个结构上的差别,直接导致了顺序上的差别——栈是后进先出,队列是先进先出。这不是两条人为规定,而是两个形状各自的必然结果。

买票排队是最贴近生活的例子。窗口只有一个,谁先站到队头谁先买。你后面来了一个人,他不可能比你早买到,除非有人插队——而队列这个结构,天生就是反插队的。也正因为如此,队列常被用来建模“公平”。

3.2 先进先出解决的是秩序,不是速度

栈解决的是“回到上一步”,队列解决的是“按顺序处理”。这两件事听着都挺朴素,但在系统里分量很重。

想象一个打印任务队列。三个人几乎同时点了打印,文件大小差很多。如果按“谁的文件小谁先打”,那第一个人可能等很久;如果按“谁最后点谁先打”,那更没道理。合理的做法就是先来先服务:谁先提交,谁先打。这就是先进先出在起作用。它不保证最快完成,但保证每个人都不会被无限期插队,这就是秩序。

再往深一点,队列在程序里最常见的身份是缓冲区。生产者造数据的速度和消费者处理数据的速度往往对不上,中间就需要一个队列来过渡:生产快了就先堆在队列里,消费快了就等队列里有货。队列在这里起的是“削峰”和“解耦”的作用——两边不用互相等待,节奏可以不一样。

广度优先搜索是队列在算法里最漂亮的应用之一。从一个点出发,先看它一步能到的所有点,再看两步能到的,一层一层往外扩。要保证“先发现的先处理”,就得用队列。如果换成栈,顺序就变成一条路走到黑,那就成了深度优先。同一张图,换个容器,走法完全不同。

3.3 循环队列:让队尾绕回队头

用数组实现队列,会遇到一个很实际的问题:出队之后,队头前面的空间就空着了,但队尾已经走到数组尽头,没法再往后放。如果每次都把剩下的元素整体往前挪,那出队就变成 O(n),队列的性能优势全没了。

解法是把数组看成一个环:队尾走到末尾之后,绕回下标 0 继续用。这就是循环队列,也叫环形缓冲区。

# 用固定长度数组实现循环队列 class CircleQueue: def __init__(self, cap): self.data = [None] * cap # 底层数组 self.cap = cap # 容量 self.front = 0 # 队头下标:下一个出队的位置 self.count = 0 # 当前元素个数,用它区分空和满 def enqueue(self, x): if self.count == self.cap: # 队满 raise OverflowError("队列已满") tail = (self.front + self.count) % self.cap # 算出队尾位置并绕回 self.data[tail] = x # 新元素放到队尾 self.count += 1 def dequeue(self): if self.count == 0: # 队空 raise IndexError("队列为空") x = self.data[self.front] # 取队头元素 self.front = (self.front + 1) % self.cap # 队头后移并绕回 self.count -= 1 return x

这里最关键的一行是取模% self.cap。它把线性的下标折成了一个圈:走到cap - 1之后,下一个就是 0。另一个关键设计是用count来记录元素个数,而不是靠front == tail判断空满。为什么?因为如果只靠两个指针相等来判断,空队列和满队列的指针状态是一样的,你分不清。常见的替代方案是故意空出一个位置不用,用“队尾的下一个就是队头”表示满,但那样会浪费一格容量。用count更直观,也不浪费空间。

参数上要留意:cap一旦定死,队列的最大长度就固定了。缓冲区场景通常就是这么设计的——宁可满了报错或者阻塞,也不让内存无限增长。

3.4 队列的亲戚们:双端队列与优先队列

基础队列有两个亲戚,名字很像,脾气差很远。

第一个是双端队列(deque)。它两头都能进、两头都能出,相当于把栈和队列的能力合并了。你可以只在一头进出,把它当栈用;也可以一头进一头出,把它当队列用。滑动窗口求最大值那类问题,标准解法就是双端队列。

第二个是优先队列。这个要特别小心,因为它虽然叫队列,却不遵守先进先出。优先队列出队的顺序由优先级决定,谁优先级高谁先走,跟到达时间没关系。医院急诊分诊是最贴切的比喻:不是谁先来谁先看,而是谁病情重谁先看。优先队列通常用“堆”这种结构来实现,插入和取出最高优先级元素都是 O(log n)。

所以看到“队列”两个字,别急着默认先进先出。得看它前面修饰的是什么词——双端队列还是队列,优先队列已经不是严格意义上的队列了。

4. 栈和队列的易混区别:四个最容易踩的坑

4.1 误区一:以为两者只是方向相反

易混点:很多人把栈和队列理解成“一个从左进、一个从右进”,觉得两者是同一个东西的两个朝向。

为什么混:它们确实都是线性结构,元素排成一条线;在代码里也常常都能用数组或者链表实现,接口看起来都是“放进去、取出来”,只差一个方法名。

怎么区分:真正的差别不在“从哪边进”,而在“你能碰到谁”。栈只开放一端,所以你永远只能碰最新的那个,后进先出是这个约束的必然结果;队列开放两端,但你只能从一头进、另一头出,所以你只能碰最早的那个,先进先出同样是必然结果。换句话说,栈约束的是“访问点只有一个”,队列约束的是“进出点分开”。把这条想清楚,就不会再把它们当成镜像关系了。顺带一提,栈和队列都属于“操作受限的线性表”这一类,受限的方向不一样,用途就分道扬镳了。

4.2 误区二:把“栈”和“堆”混为一谈

易混点:内存管理里常说“栈上分配”“堆上分配”,听起来像一对搭档;数据结构里又有一个“堆”。三四个词撞在一起,很容易乱。

为什么混:中文里“栈”和“堆”这两个字被反复借用,指的根本不是同一个层面的东西。

怎么区分:把它们拆开看就清楚了。数据结构里的栈,是后进先出的容器;内存里的栈区,是函数调用时存放局部变量和返回地址的一块区域,它的分配释放确实符合后进先出的规律,所以借了“栈”这个名字;内存里的堆区,是给程序动态申请内存用的,随意分配、随意释放,跟顺序没关系;数据结构里的堆,是一棵满足特定大小关系的完全二叉树,常用来实现优先队列。四者里只有“数据结构栈”和“内存栈区”沾点亲,其余都是同名不同物。有次听到有人很肯定地说“堆是先进先出的”,那就是把内存堆和队列搞混了,这种混淆在初学阶段特别常见。

4.3 误区三:把队列和“消息队列”画等号

易混点:听说某个系统用了“消息队列”,就以为它内部只是维护了一个先进先出的数组。

为什么混:名字里都带“队列”,而且消息队列确实也讲究先来后到。

怎么区分:基础队列是内存里的一段结构,生命周期跟着进程走,进程一挂,队列就没了。消息队列是一套独立的中间件,解决的是跨进程、跨机器之间的通信问题,它要考虑消息持久化、确认机制、失败重试、堆积上限这些事。判断标准很简单:如果问题只是“同一段程序里,谁先来谁先处理”,那用基础队列就够了;如果问题是“两个服务之间不想互相等,还要保证消息不丢”,那才轮到消息队列上场。把后者当成前者用,或者反过来,都会带来不必要的复杂度。

4.4 误区四:以为数组能顶替所有结构

易混点:既然栈和队列都能用数组模拟,那干脆直接用数组加下标操作,何必多此一举?

为什么混:功能上确实能做到,写起来也不难,看起来没必要引入额外的概念。

怎么区分:数组能做,不等于该这么做。栈和队列的价值一半在性能,一半在语义约束。你用数组直接操作,代码里到处是“从尾部删”“从头部删”的下标运算,读代码的人得自己想清楚这里到底是栈还是队列;而一旦声明成栈或者队列,意图就摆在明面上了,别人一看就知道“这里只关心最近的那个”或者“这里要按顺序来”。性能上也别忽略:从数组头部删除是 O(n),循环队列是 O(1),数据量一大就是天壤之别。结构的名字本身就是一种文档,这句话在协作里尤其值钱。

5. 从抽象到落地:栈和队列在真实场景里的分工

5.1 栈:撤销、括号匹配、函数调用

栈在工程里出现的频率高得惊人,只是常常不叫这个名字。

编辑器的撤销栈是最直白的例子。每一次编辑动作压入栈,撤销就是弹栈。如果再配一个“重做栈”,把撤销掉的动作用另一个栈接住,就组成了一对互相倒腾的栈:撤销时从撤销栈弹到重做栈,重做时反过来。很多软件的撤销重做只能退有限步,原因也简单——栈有深度上限,太老的记录被丢掉了。

表达式求值是另一个经典场景。人写的中缀表达式3 + 4 * 2,计算机直接算容易搞错优先级,通常先转成后缀表达式3 4 2 * +,再用一个栈从左往右扫:遇到数字就入栈,遇到运算符就弹出两个数算完再压回去。整个过程的中间结果都堆在栈里,最后栈里剩的那个数就是答案。

还有一处藏得很深:程序的函数调用本身就在用一个栈。每进一层函数,就把参数、返回地址、局部变量打包压进去;函数返回,就整块弹出来。你写的递归之所以能“自己回到上一层”,靠的就是这个结构在背后记账。理解了这一层,递归就不再是玄学,而是一次次明明白白的压栈和弹栈。

5.2 队列:任务排队、缓冲、广度优先搜索

队列的用武之地,基本都跟“按顺序、别插队”有关。

任务调度是最常见的一类。一个服务收到一批请求,不可能同时处理,就排成队列一个个来。操作系统的进程调度、线程池的任务队列、打印机的待打文档,本质都是同一件事:把并发的请求串行化,按到达顺序消化。

缓冲区是第二类。日志采集、视频播放、网络收包,生产速度和消费速度很难刚好匹配,中间垫一个队列,快的一方就不会被慢的一方拖住。播放器里那个“缓冲中”的圈,背后就是一个队列在攒数据。

第三类是图上的逐层遍历,也就是广度优先搜索。下面这段代码值得逐行看:

# 用队列做广度优先搜索:先访问离起点近的节点 from collections import deque def bfs(graph, start): visited = {start} q = deque([start]) # 起点入队 order = [] while q: node = q.popleft() # 队头出队:先到先处理 order.append(node) for nxt in graph[node]: if nxt not in visited: visited.add(nxt) q.append(nxt) # 新发现的节点排到队尾 return order

popleft()是从左端弹出,append()是从右端追加,一进一出正好构成先进先出。如果把popleft()换成pop(),代码照样能跑、不报错,但访问顺序就从“一层一层”变成了“一条道走到黑”——广度优先悄悄变成了深度优先。这类错误最难查,因为它不崩,只是答案不一样。

5.3 用错结构往往不报错,却很难受

这是最值得记住的一条经验:栈和队列用反了,程序通常不会当场崩,而是慢慢地别扭。

用列表当队列使,从头部删除,功能没错,但数据一多就卡;用栈去模拟排队,顺序全乱了,可代码依旧跑得通;该用优先队列的地方用了普通队列,紧急任务被排在最后,用户只会觉得“这系统怎么这么慢”,排查起来却毫无头绪。这些都不是语法错误,是语义错误,编译器帮不了你。

有个真实的教训:某开发者写了个任务处理器,用列表的pop(0)当出队,一百条数据时毫无感觉,上线后队列里堆到几万条,处理速度断崖式下跌。查了半天内存和网络,最后发现是每次出队都在搬整个数组。换成deque的popleft()之后,问题当场消失。从那以后他养成一个习惯:只要写下“从头部取”这四个字,就先问一句这里是不是该用双端队列。

6. 什么时候用栈、什么时候用队列:三条判断线和一张表

真正要落到选择上,其实不用背定义,问自己三个问题就够了。

第一个问题:我关心的是“最近发生的”,还是“最早发生的”?如果答案是最近,比如撤销、回溯、嵌套结构的闭合、函数返回,那就是栈。如果答案是最早,比如按提交顺序处理、按到达顺序服务,那就是队列。这一条能解决八成的选择困难。

第二个问题:数据是一层一层铺开,还是一条路走到底?逐层扩散——图的广度优先、多轮扩散的影响传播——用队列;一条路走到黑再回头——深度优先、回溯搜索——用栈。这两者在图遍历里可以互相改写,但写出来的代码风格和内存占用完全不同:队列那一版会同时压住一整层的节点,栈那一版只压住当前这条路径。

第三个问题:有没有优先级或两端操作的需求?如果有,普通栈和普通队列都不够用,该上优先队列或双端队列。这一步很多人会跳过,硬拿基础结构凑,最后在业务代码里塞一堆排序,把 O(1) 的操作拖成 O(n log n)。

把这三条整理成一张表,对照着看更清楚:

判断维度选栈选队列都不合适
关心的元素最近一个最早一个需要随机访问任意位置
典型动作撤销、回溯、嵌套匹配排队、缓冲、逐层扩散频繁按值查找
数据进出同一端进出一端进、另一端出两端都要能进能出,应选双端队列
优先级无无(有优先级请用优先队列)需要按优先级出队
性能预期入出均 O(1)入出均 O(1),需避开数组头部删除需要 O(log n) 有序操作,考虑堆

反过来,也有明确不该用的时候。需要按下标随机读取,用数组;需要频繁在中间插入删除,用链表;需要按优先级出队,用堆;需要跨进程传递并且不能丢消息,那不是这两种结构能解决的问题。把它们硬套上去,代码会写得很绕,性能也不会好。

我自己的习惯是:每次写下一个容器之前,先在注释里写半句话——“这里要的是最近的那个”或者“这里要的是最早的那个”。写不出来,就说明我还没想清楚这段逻辑到底在维护什么顺序。这个动作花不了十秒钟,却能挡掉很多后面要花几小时才查得出来的坑。从那以后我每次碰顺序相关的逻辑,都强制先写这半句注释再动手,翻车次数确实少了很多。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/31 7:32:44

技术博文写作规范:如何定义一个可落地的AI项目标题

我无法根据您提供的输入内容生成符合要求的博文。原因如下:输入中项目标题仅包含机构隶属关系描述(“LAMDA隶属于 计算机软件新技术国家重点实验室 和 南京大学计算机科学与技术系…”),但未构成一个可操作、可复现、可延展的具体…

作者头像 李华
网站建设 2026/10/31 7:30:32

华为CE交换机M-LAG配置与优化实战

1. 华为CE系列交换机M-LAG级联实战指南在数据中心网络架构中,M-LAG(Multichassis Link Aggregation Group)技术通过将两台物理交换机虚拟化成一台逻辑设备,实现了跨设备链路聚合。这种部署方式不仅能够提供高可用性,还…

作者头像 李华
网站建设 2026/10/31 7:30:09

PL/0词法分析器手写实现与教学级调试指南

简介:本资源是东北大学秦皇岛分校《编译原理》课程的词法分析实验报告,面向计算机专业本科生及编译技术初学者,聚焦PL/0语言词法分析器的设计与实现,解决从理论到代码落地的关键实践问题。报告含完整C实现代码、详细实验流程说明、…

作者头像 李华
网站建设 2026/10/31 7:29:21

VG3S:视觉几何与高斯溅射融合的3D占用预测新突破

1. 项目概述:VG3S如何重新定义3D占用预测在自动驾驶和机器人感知领域,3D场景理解一直是个硬骨头。传统方法要么依赖昂贵的激光雷达点云,要么需要复杂的多传感器融合,而纯视觉方案往往在几何精度上捉襟见肘。港科大沈劭劼团队最新提…

作者头像 李华
网站建设 2026/10/31 7:29:16

CATIA CAA二次开发环境搭建与mkmk编译排错

简介:一份面向 CATIA 二次开发初学者的环境配置入门文档,聚焦 CAA、RADE 与 Visual Studio 2005 的安装及协同配置,帮助刚接触二次开发的学习者理清软件依赖关系,减少环境搭建阶段的反复试错。压缩包内仅含 1 个 doc 文档&#xf…

作者头像 李华
网站建设 2026/10/31 7:28:25

MySQL ODBC 3.51.24 win32 安装与32位DSN配置避坑

简介:这是一份面向数据库初学者、后端开发人员、系统运维者以及需要将 MySQL 与报表工具、办公软件或其他业务系统连接的用户的操作文档,重点解决在 Windows 环境中接入支持 ODBC 的应用程序时,驱动安装、系统 DSN 添加、连接参数填写与连通性…

作者头像 李华