news 2026/10/23 23:48:51

数据结构作业银行排队系统:链式队列与事件驱动模拟实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
数据结构作业银行排队系统:链式队列与事件驱动模拟实现

简介:一套以“银行排队系统”为场景的数据结构期末作业完整方案,面向需掌握队列应用与多队列优先级处理的学生,也适合作为课程设计参考。资源围绕队列先进先出(FIFO)特性,实现了VIP用户优先于普通用户的多队列调度,涵盖C++ STL queue容器用法、文件信息解析、循环条件控制等核心环节。压缩包共9个文件,约1.03MB,以C++源码、文本数据文件、Code::Blocks工程配置文件及可执行程序为主要类型,结构清晰,便于直接运行验证与二次修改学习。目前已有3362人学习下载,具备较高参考热度。通过阅读源码和运行调试,可直观看到入队、出队、队头访问及优先级切换的完整逻辑,同时能在现实场景中体会队列结构“先到先服务、VIP插队”的规则,适合期末复习、实验报告撰写及面试前快速回顾队列原理。

1. 数据结构作业里的银行排队系统:它到底在模拟什么

如果你打开一份“银行排队系统”的作业要求,看到的通常是这样的描述:输入客户的到达时间和业务办理时长,输出每个窗口的工作情况、客户的平均等待时间、队列长度变化。看起来像一道应用题,但绝大多数人第一次写它的时候,脑子里想的是“怎么把界面做好看”,而不是“队列到底该怎么组织”。等写到统计指标那一环才发现,前面所有窗口和客户的状态管理,全在跟“谁先谁后”较劲。

这道作业的核心只有一个:用队列模拟离散事件系统。客户是动态到达的,窗口是有限资源,队列是两者之间的缓冲。你能不能在代码里把“时间推进”这件事说清楚,决定了这套系统是能跑通,还是只能在一组固定输入下碰巧跑通。本文就按一条能落地的路径来讲:先建立模型和选型逻辑,再给出一套最小可运行的代码骨架,然后说清测试方法、统计口径和五个高频坑,最后给一个能拿高分的扩展方向。适合正在做这道数据结构作业、并且想让代码真正经得起追问的人。

2. 模型选型与队列设计:单队列还是多队列,以及为什么课程设计都选链式队列

2.1 业务模型拆解:客户、窗口、队列三者怎么映射到数据结构

银行排队系统里,业务角色有三个:客户、窗口、等待队列。客户是一个对象,它至少携带两个字段——到达时间arriveTime和业务时长serviceTime,后续统计时还会用到开始服务时间serveTime。窗口是一个状态机,它要么空闲,要么忙碌,忙碌时要记住正在服务的客户和剩余时长。等待队列则是把“到了但没轮上”的客户组织起来的数据结构,这一层就是数据结构课程要考察的核心。

三者之间的关系是典型的生产者-消费者模型。客户的到达由外部数据驱动,属于生产者;窗口的办理动作由服务逻辑驱动,属于消费者。队列放在中间做解耦:窗口空了,从队头取人;客户来了,先排队。这个模型本身不复杂,但初学者容易犯的一个错误是——把窗口也当成队列里的元素,总想着“窗口也在排队”。窗口不是队列,它是服务台,队列里只有客户。

还有一个极易踩的思维误区是数据驱动方式。常见做法有两种:一种是“按时间片推进”,每过一分钟扫描一次所有窗口;另一种是“事件驱动”,只在“有客户到达”或“有窗口完成任务”这两个时刻推进时间。课程作业用后者往往更清晰,因为统计指标——平均等待时间、窗口利用率——都在事件节点上计算最方便,不用额外处理“一分钟内什么都没发生”的情况。

2.2 顺序队列 vs 链式队列:教科书之外的选型理由

顺序队列用数组实现,操作简单,但有一个致命问题:出队后队头指针不断后移,前面留出的空间没法自动复用,需要取模运算做成循环队列才能解决。链式队列没有这个问题,出队就是摘掉头节点,入队就是尾指针接新节点,逻辑和“排队”这件事完全对应。

从作业答辩的角度看,链式队列的代码更容易解释“为什么这样设计”。你可以直接指着节点的next指针说:当前队头是第一个要办理的人,新到达的客户从队尾插入。这句话任何老师都能听懂。而循环队列虽然也正确,但讲课时你得先解释(rear+1)%MAXSIZE是什么,解释成本就上去了。

另外,链式队列天然支持动态增长。银行排队系统的一个压力测试场景是短时间内大量客户涌入,顺序队列一旦预分配不足就得扩容,而链式队列只要内存够,来多少接多少。课程作业的输入数据通常不会大到触发内存问题,但用链式队列写出来的代码,在扩展到其他排队场景——比如门诊叫号、食堂打饭——时不需要重构,这个迁移优势在答辩时也能成为加分话术。

2.3 排队策略与公平性:先到先服务为什么是默认解

FCFS(先到先服务)是排队系统最基本的策略,也是这道数据结构作业默认应该实现的策略。原因很简单:FIFO 队列天然就是 FCFS,你不需要额外写任何分支逻辑。如果作业要求里没有明确说“支持 VIP 插队”或“按优先级办理”,不要自己往上加,因为那会把一个数据结构题变成调度策略设计题,偏离了考察点。

但要注意一个边界情况:到达时间相同怎么办?比如输入文件里两条数据都是 09:30 到达,后处理的那条是不是应该排在后面?这取决于你读入数据的顺序是否稳定。我一般会在客户节点里再加一个id编号,入队时按“到达时间升序、id 升序”的次序插入,保证相同到达时间下先读入的先服务。这个细节在答辩时拿出来讲,能体现你对“公平性”的考虑。

另外,如果扩展优先级策略,不要直接往链式队列上叠加排序。优先队列通常用堆实现,强行在一个 FIFO 里做“插队”,会破坏原有队列的结构,统计逻辑也会变得混乱。正确做法是:主队列保持 FIFO,另设一个优先级队列用于 VIP 客户,窗口调度时先看 VIP 队列是否为空。这个扩展方案在最后一张再展开讲。

3. 用 C++ 写一个能跑通的最小银行排队系统:核心队列与窗口调度

3.1 节点、链式队列与客户信息的定义

先把最小骨架定下来:客户节点、链式队列、窗口数组三个部分。语言用 C++,但不用 STL 的queue,因为作业考察的就是手写队列;如果老师允许用 STL,你可以用,但答辩时大概率会被问“底层是什么结构”,所以建议还是自己写一遍。

struct Customer { int id; // 客户编号,用于稳定排序与结果核对 int arriveTime; // 到达时刻(相对第0分钟) int serviceTime; // 办理业务所需分钟数 int startTime; // 实际开始服务时刻,初始为 -1 int finishTime; // 服务完成时刻,初始为 -1 }; struct QueueNode { Customer data; QueueNode* next; }; struct LinkedQueue { QueueNode* front; // 队头指针,出队时从这里摘节点 QueueNode* rear; // 队尾指针,入队时从这里挂新节点 int count; // 当前队列长度,便于直接输出队长 };

结构体的关键字段是startTime和finishTime,统计等待时间全靠这两个值。arriveTime和serviceTime来自输入文件,startTime在窗口分配时写入,finishTime在服务完成时写入。很多同学只记录“完成时间”,回头算等待时间的时候发现还得倒推,倒推出来的数还容易对不上,所以一开始就把时间字段存全。

LinkedQueue里的count字段非常实用。输出最大队长时,如果每次去遍历链表数节点,代码既啰嗦又容易出错,维护一个计数器即可。入队count++,出队count--,查询长度直接返回count。

3.2 队列操作:入队、出队、判空

void initQueue(LinkedQueue& q) { q.front = q.rear = nullptr; q.count = 0; } bool isQueueEmpty(LinkedQueue& q) { return q.front == nullptr; } void enqueue(LinkedQueue& q, Customer c) { QueueNode* node = new QueueNode; node->data = c; node->next = nullptr; if (q.rear == nullptr) { q.front = q.rear = node; // 空队列入队,队头队尾都指向新节点 } else { q.rear->next = node; // 原队尾的 next 指向新节点 q.rear = node; // 队尾指针后移 } q.count++; } bool dequeue(LinkedQueue& q, Customer& c) { if (isQueueEmpty(q)) return false; QueueNode* temp = q.front; c = temp->data; q.front = temp->next; delete temp; // 从堆上摘除节点,防内存泄漏 if (q.front == nullptr) { q.rear = nullptr; // 队列空了,队尾也要置空,否则悬空 } q.count--; return true; }

这段代码里最容易写错的是enqueue时空队列分支。如果队列为空,front和rear都是nullptr,如果只改rear不写front,第一个节点就“飘”在队列外面,后面dequeue时直接解引用空指针。另一个坑是dequeue出最后一个节点后,rear还指着已经被delete的节点,这就是悬空指针。所以每次出队后都要检查front是否为空,是的话把rear一起置空。

3.3 时间推进与窗口调度:事件驱动的三种情形

接下来是核心逻辑:如何推进时间、如何分配窗口。先定义窗口状态:

struct Window { int id; // 窗口编号 bool busy; // 是否正在办理业务 Customer current; // 正在服务的客户 int remainTime; // 当前客户剩余服务时间 int serveCount; // 累计服务客户数 int idleTime; // 累计空闲时间 };

调度主循环用“事件驱动”方式:维护当前时刻currentTime,每次循环检查“下一个到达事件”和“下一个完成事件”哪个先发生,然后把时钟跳到那个时刻。这样做的好处是:不会为了空跑一分钟而浪费循环次数,统计结果也精确到事件发生的那一瞬间。

int currentTime = 0; while (有未到达的客户 || 有忙碌窗口 || 队列非空) { // 1. 找出所有在 currentTime 时刻完成的窗口,办理完释放 for (int i = 0; i < windowCount; i++) { if (windows[i].busy && windows[i].remainTime == 0) { windows[i].current.finishTime = currentTime; windows[i].busy = false; windows[i].serveCount++; } } // 2. 把所有到达时间等于 currentTime 的客户入队 while (nextArriveIndex < totalCustomers && customers[nextArriveIndex].arriveTime == currentTime) { enqueue(queue, customers[nextArriveIndex]); nextArriveIndex++; } // 3. 让空闲窗口从队头取客户 for (int i = 0; i < windowCount; i++) { if (!windows[i].busy && !isQueueEmpty(queue)) { dequeue(queue, windows[i].current); windows[i].current.startTime = currentTime; windows[i].remainTime = windows[i].current.serviceTime; windows[i].busy = true; } } // 4. 推进时间到下一个事件 int nextEventTime = INF; // 下一个到达事件 if (nextArriveIndex < totalCustomers) nextEventTime = min(nextEventTime, customers[nextArriveIndex].arriveTime); // 下一个完成事件:取所有忙碌窗口中 remainTime 的最小值 for (int i = 0; i < windowCount; i++) { if (windows[i].busy) nextEventTime = min(nextEventTime, currentTime + windows[i].remainTime); } // 5. 推进前先统一减少所有忙碌窗口的剩余时间 int delta = nextEventTime - currentTime; for (int i = 0; i < windowCount; i++) { if (windows[i].busy) { windows[i].remainTime -= delta; } else { windows[i].idleTime += delta; } } currentTime = nextEventTime; }

这段代码有几个关键点需要解释。第 1 步“先释放完成的窗口”必须在“入队新客户”之前,否则新客户可能会被一个还没释放的窗口接走,造成逻辑冲突。第 3 步分配窗口时,多个窗口同时空闲的情况下,按窗口编号从小到大分配即可,这符合“叫号后空闲窗口接单”的直观逻辑。

第 4 步找“下一个完成事件”时,不能直接取剩余remainTime最小的窗口,而要取“currentTime + remainTime最小”的窗口。因为每个窗口的remainTime是在各自的“最后推进时刻”之后计算的,直接用会算错。我见过有人在这里取成了全局最小remainTime,结果窗口释放时刻全部错位,统计的等待时间偏小,而且数据量越大偏得越离谱。

3.4 统计指标:平均等待时间、窗口利用率、最大队长

主循环跑完后,统计三项指标并打印:

double totalWaitTime = 0; int maxQueueLength = 0; // 可以在每次 enqueue 后更新 for (int i = 0; i < totalCustomers; i++) { totalWaitTime += customers[i].startTime - customers[i].arriveTime; } double avgWait = totalWaitTime / totalCustomers; for (int i = 0; i < windowCount; i++) { double usage = 1.0 - (double)windows[i].idleTime / currentTime; // 打印每个窗口的客户数与利用率 } // 最大队长:在 enqueue 和 dequeue 时记录队列的 count 峰值

计算avgWait时,customers[i].startTime一定已经被赋值了。如果某个客户在模拟结束时还在队列里没被服务,说明窗口数太少或输入数据不合理——但课程作业一般不会给你这种“跑不完”的数据,万一遇到,输出前要判断startTime != -1,否则算出来的等待时间是负数。maxQueueLength我习惯放在enqueue后更新,因为队列长度只会在入队时变大。

4. 用测试数据验证正确性:手算可达的边界用例与统计口径

4.1 手算可达的用例:单窗口、无等待、峰值冲击

代码写完第一件事不是直接上随机数据,而是构造一组你用手能算出结果的小数据,逐行核对输出。第一组建议:单窗口,3 个客户,到达时间分别为 0、2、4,业务时长分别为 5、3、2。手算结果应为——客户 1 从 0 等到 0,服务到 5;客户 2 从 2 等到 5,服务到 8;客户 3 从 4 等到 8,服务到 10。总等待时间 = 0 + 3 + 4 = 7,平均等待时间约 2.33,窗口利用率 10/10 = 100%。

这组数据能把“时间推进”的每一步都暴露在外。你用断点跟一遍,能看到当前时间依次跳到 0、2、4、5、8、10,每个事件节点的窗口状态都符合预期。

第二组测无等待场景:窗口数 3,客户到达时间 0、0、0,业务时长各 2。三个客户同一时刻到达,三个窗口全空闲,理论上没有等待,平均等待时间必须是 0。如果代码在“同一时刻多客户入队”和“多窗口同时取人”之间存在顺序问题,这一组就会出错。

第三组测峰值冲击:单窗口,5 个客户,到达时间 0、1、2、3、4,业务时长各 10。窗口忙的时候后来的人全部要排队,最大队长应该是 4(第二到第五个客户全在队列里)。做完这组你能验证maxQueueLength的统计位置是否正确。

4.2 随机到达间隔与业务时长:均值参数怎么设才合理

手算用例通过后,可以上随机数据测试。到达时间用指数分布间隔模拟,业务时长在 [3, 10] 分钟内均匀分布。指数分布的“无记忆性”比较贴近真实排队场景,但作为作业,你不需要真的写一个指数分布生成器,用固定间隔 + 随机偏移也够用。

srand(42); // 固定随机种子,保证结果可复现 int timeNow = 0; for (int i = 0; i < 20; i++) { timeNow += rand() % 6 + 1; // 下一个客户在 1~6 分钟后到达 int service = rand() % 8 + 3; // 业务时长 3~10 分钟 // 写入 customers[i] }

固定随机种子是测试阶段最重要的习惯。你调试时换一个随机种子,数据全变了,之前发现的 bug 可能被掩盖。用srand(42)或任何定值,能让同一份数据反复复现,维护效率高很多。

窗口数设多少合适?给你一条经验:窗口数 × 平均业务时长 ÷ 平均到达间隔 ≥ 0.8 且 ≤ 1.5,这个区间内队列既不会很快空也不会无限堆积。比如平均到达间隔 4 分钟、平均业务时长 6 分钟,单窗口的负荷就是 1.5,偏高了;开两个窗口,负荷降到 0.75,站在值班经理的角度这个配置就合理了。

4.3 验收视角:老师最常追问的三个追问点

答辩时老师通常不问“你用了什么数据结构”——这个太明显了,而是问“你认为这个模型的局限在哪”。我建议你准备三个回答方向。第一个:FCFS 不一定是最优策略,比如 VIP 客户、老年客户、耗时短但来得多的小额存取款业务,用单一队列并不能反映真实银行的差异化服务。第二个:模型只考虑了“到达”“排队”“服务”三个状态,没有考虑“客户等不及走了”的弃队行为,现实中等待时间超过某个阈值,客户会离开,队列长度会受影响。第三个:窗口效率被假设为固定值,但现实中柜员处理速度因人而异,而且业务之间还有交接时间。

这三个追问方向有两个共同点:一是你的回答能展示“我没有把代码当黑匣子”,二是方向都指向扩展设计——这刚好给最后一章的进阶方案留下接口。

5. 避坑:数据结构作业里排队系统最容易翻车的五个点

5.1 悬空指针与节点释放:统计对不上的第一元凶

现象是:程序跑完,平均等待时间比手算结果明显偏小,甚至出现负数。查来查去逻辑没问题,最后发现dequeue函数里front移动后没有把空队列的rear置空,enqueue时把新节点接在了已经被释放的旧rear上,链表结构乱了,队列长度也错了。统计基于错误的队列长度和出队顺序,结果自然全崩。

原因就是:链式队列出队后,队尾指针归零这一步容易被遗漏,而数据量小的时候——比如只有 3 个客户——最后的rear不一定会被再次使用,问题被掩盖了。数据量一大,立刻翻车。

解决:dequeue函数里,front移动后立刻检查是否为空,为空就把rear置空。并且每次入队出队,都打印queue.count和队头队尾地址做临时校验,跑通后再去掉打印。

5.2 时间推进模型混乱:按分钟扫描和时间跳跃混着写

现象:写了事件驱动逻辑,但内层还残留一个for (t = 0; t < totalTime; t++)的外壳,两个时间模型互相打架。结果要么窗口明明在处理业务,下一秒又被重新分配;要么所有客户都在同一分钟内到达,输入数据波峰被抹平。

原因:拿到题目第一反应是“模拟银行嘛,一分钟一分钟跑”,写了一版按分钟扫描的,后来觉得效率低改成了事件驱动,但外层的分钟循环没删干净。

解决:明确告诉自己,一个系统只用一个时间模型。事件驱动模型的关键是“没有事件发生的时间段直接跳过”,如果采用这个模型,代码里不应该出现t++这种逐分钟遍历。我在写调度主循环前,会在注释里先写清“唯一推动时间的语句是currentTime = nextEventTime”,避免自己又顺手写个循环。

5.3 窗口空闲判断与分配顺序:break 用错导致空忙

现象:窗口还忙着,新客户来了却直接被分配给这个窗口,或者窗口明明空闲却一直没人被分配。查下来发现是分配窗口时内层用了break,导致一根空闲窗口处理完第一个客户后直接跳出循环,后面还有空闲窗口没被用上;又或者忘了在服务中窗口的if判断条件里加busy标志。

原因:窗口状态只有busy和remainTime两个字段,新手容易用remainTime > 0来判定“是否忙碌”,但刚分配完窗口还没推进时间时,remainTime等于业务时长,此时判断没问题;可业务做完了remainTime == 0,如果你同时用remainTime == 0表示“空闲”,这个窗口就可能被重复分配。

解决:用独立的busy布尔字段作为窗口状态的唯一依据,remainTime只负责存剩余时长。分配时用continue而不是break遍历所有窗口,确保一个窗口接完客户后继续看后面的窗口。

5.4 统计口径不一致:平均等待时间到底含不含正在办理的人

现象:两组数据,一个客户都没排队,平均等待时间却算出来 3 分钟。原因是计算总等待时间时,把正在办理的客户的“等待时间”也算进去了。定义上,等待时间是指从到达时刻到开始服务时刻的时间差,一个人正在窗口被服务,不代表他已开始服务;但如果代码用的是startTime - arriveTime,这个时间段只有服务开始时才确定。

原因:统计口径混了。有些代码用finishTime - arriveTime - serviceTime算等待时间,这在单个客户逐条处理时是对的,但在同时存在多个客户、且服务可能重叠时,这个公式和startTime - arriveTime是等价的——前提是finishTime和startTime都正确记录了。

解决:交付前统一口径,在注释里写清楚“等待时间 = startTime - arriveTime,serviceTime 只用于计算完成时刻”。如果老师给的样例输出里等待时间是另一种口径——比如“客户到达后到窗口叫号的时间”——那就按样例为准,代码里做相应调整。关键是口径一致,不然后期改统计比改逻辑还痛苦。

5.5 随机种子没固定:代码对、文档对、演示出洋相

现象:答辩现场演示时,随机生成的输入数据每次都不同,第一次跑出平均等待 5 分钟,第二次变成 8 分钟,老师眉头一皱。你心里知道代码大概率没问题,但现场根本没法解释“为什么两次结果不一样”。

原因:没有固定随机种子,每次启动程序都是一次新的实验。

解决:程序支持“手动输入模式”和“随机生成模式”两个开关。答辩前用手动模式展示一组精心设计的数据,随机模式用于展示系统能处理不同负载,但固定srand(42),确保每次运行出来的数据序列一致。这样无论老师问“换一组数据会不会崩”,你都能给出可复现的回复。

6. 进阶:从“能交差”到“能加分”——优先级队列与动态窗口

这一章给认真做完基础版、还想多拿点分的同学。两个扩展方向,二选一即可,但做之前建议先确认基础版完全稳定,因为扩展部分的 bug 排查难度更高,别给答辩挖坑。

第一个方向是优先级队列。真实银行有 VIP 窗口、老弱病残窗口,在模型里做成独立队列:

struct PriorityQueueNode { int priority; // 0 普通,1 VIP Customer customer; PriorityQueueNode* next; };

VIP 队列用优先队列或简单维护的按优先级有序链表,窗口调度时先检查 VIP 队列,再检查普通队列。这里注意优先级不要做得太复杂,一个字段就够了。答辩时重点解释“为什么 VIP 队列和普通队列分开,而不是放在一个队列里排序”——前者是公平性和效率的权衡,后者是数据结构选型问题。

第二个方向是动态窗口启停。窗口空闲超过一定时间就关闭,队列长度超过阈值就新开窗口。这个方向能体现你对“系统负载”的理解,但代码量会明显增加。我建议如果平时时间紧,做优先级队列就好;时间充裕再做动态窗口,并且把“窗口何时关闭何时开启”的判定逻辑写成一个独立函数,方便单独测试。

最后说一个我自己的习惯:每次跑完模拟,把输入数据、输出指标、运行参数一起存成一个文本,哪怕只是随手粘贴在记事本里。答辩时老师问“这个结果怎么来的”,你当场翻出来,比沉默半天重新跑一遍更有说服力。这道作业做到最后,拼的不只是队列代码对不对,而是整个事件流程你心里是不是门清——数据结构里最抽象的那层“状态变化”,拿手工队列和事件表推过一遍,以后换任何场景都能做到心里有底。希望这篇能帮到你。

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

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

Unity射击游戏开发:从对象池到碰撞检测的完整实现路径

简介&#xff1a;这是一份基于 Unity 3D 实现的太空飞机射击游戏期末作业项目&#xff0c;面向正在学习 Unity 或需要完成同类课程设计的学生。工程覆盖了游戏开发的主要环节&#xff1a;太空场景搭建、飞机移动控制、子弹射击与对象池、碰撞检测、分数与游戏状态管理&#xff…

作者头像 李华
网站建设 2026/10/14 12:31:12

CCNA中版PDF实验指南:从模拟器选型到VLAN/OSPF/ACL排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/23 7:15:04

AI Agent 面试题 133:如何设计Agent的异步任务处理架构?

&#x1f525; AI Agent 面试题 133&#xff1a;如何设计Agent的异步任务处理架构&#xff1f;摘要&#xff1a;本文深入解析了「如何设计Agent的异步任务处理架构&#xff1f;」这一 AI Agent 领域的核心面试题。文章从 混合架构模式 的基本概念出发&#xff0c;系统性地剖析了…

作者头像 李华
网站建设 2026/10/14 8:29:03

Ubuntu常用指令

1、系统类 查看内存占用 df -h sudo du -sh *2、文件相关 查看目录下文件数量 ls -1 /path/to/folder | grep -iE \.(jpg|jpeg)$ | wc -l ls -1 . | grep -iE \.(jpg|jpeg)$ | wc -l # 查当前目录或者 find . -maxdepth 1 -type f \( -iname *.jpg -o -iname *.jpeg \) | wc -…

作者头像 李华