0基础入门Go线性结构:栈和队列
文章目录
- 0基础入门Go线性结构:栈和队列
- 一、栈(Stack):像叠衣服一样,“后进先出”
- 1\. 栈的核心操作(0基础也能懂)
- 2\. Go实现极简栈(无复杂语法,复制就能跑)
- 3\. 栈的实际用途(知道用在哪,才好理解)
- 二、队列(Queue):像排队买奶茶一样,“先进先出”
- 1\. 队列的核心操作(和栈对比着记,更简单)
- 2\. Go实现极简线性队列(同样无复杂语法)
- 3\. 队列的实际用途(贴近生活,好记)
- 三、栈和队列的核心区别(一张表看懂,0基础不混淆)
- 四、0基础总结(重点记这3点,不迷路)
0基础入门Go线性结构:栈和队列
【引言】
在上一篇博客中,我们详细讲解了Go中切片的核心操作——copy方法,了解了切片作为顺序表的基础特性,而今天我们要在此基础上,延伸学习线性结构中最常用的两个“工具”——栈和队列。它们既是切片应用的重要场景,也是后续我们学习哈希表(下一篇内容)的基础,相当于“承上启下”的关键知识点:切片的特性决定了栈和队列的常见实现方式,而栈和队列的线性逻辑,又能帮助我们更好地理解后续哈希表的非线性存储逻辑,跟着节奏一步步来,0基础也能轻松吃透。
大家好,我是刚入门Go的编程小白,今天想和大家分享两个最基础、最常用的线性结构——栈和队列。很多0基础的朋友一听到“数据结构”就头大,觉得是高深莫测的东西,但其实它们就藏在我们的日常生活里,用类比的方式一讲就懂。
先跟大家说个小前提:什么是「线性结构」?简单说就是“数据像排队一样,排成一条直线,每个数据只有一个前一个和一个后一个(除了开头和结尾)”,就像我们排队买奶茶、叠衣服,都是线性的,没有分支。栈和队列,就是线性结构里最典型的两个“小伙伴”,只是它们的“使用规则”不一样。
一、栈(Stack):像叠衣服一样,“后进先出”
先想一个生活场景:我们叠衣服的时候,总是把先洗好的衣服放在最下面,后洗好的衣服叠在上面。等要穿的时候,只能从最上面开始拿,先拿最后叠的那件,才能拿到下面的。这就是栈的核心逻辑——后进先出(LIFO),也可以叫“先进后出”。
用更直白的话讲:栈就像一个“密封的圆筒”,只有一个开口,数据只能从这个开口“放进去”(入栈),也只能从这个开口“拿出来”(出栈),不能从中间插着放、也不能从中间抽出来。
为了更直观理解,我们用一个简单的示意图展示栈的结构和核心操作(类比叠衣服),示意图放在代码块里,清晰醒目:
【栈的文字示意图(类比叠衣服)】 ┌─────────┐ │ 3 │ ← 栈顶(最后入栈,最先出栈,类比最上面的衣服) │ 2 │ ← 中间元素 │ 1 │ ← 栈底(最先入栈,最后出栈,类比最下面的衣服) └─────────┘ 操作流程说明: 1. 入栈:1 → 2 → 3(依次叠在上面,和叠衣服一致,先叠1,再叠2,最后叠3); 2. 出栈:3 → 2 → 1(先拿最上面的3,再拿2,最后拿最下面的1); 3. 全程只有栈顶一个操作口,符合“后进先出”逻辑。1. 栈的核心操作(0基础也能懂)
入栈(Push):把数据“叠”到栈的最上面(就像叠衣服时,新衣服放在最顶层);
出栈(Pop):把栈最上面的数据“拿”走(就像拿衣服时,先拿最顶层的);
查看栈顶(Peek):看看最上面是什么数据,但不拿走(就像看看叠在最上面的是哪件衣服);
判断空栈(IsEmpty):看看栈里有没有数据(就像看看衣服叠完了没,或者拿完了没)。
2. Go实现极简栈(无复杂语法,复制就能跑)
Go里没有自带的栈结构,但我们用「切片」就能轻松实现一个,切片的append(添加)和len(长度)方法,刚好对应栈的入栈和判断空栈,非常简单。
补充:栈的实现有两种常见方式——顺序表(如切片、数组)和链表,两者各有优劣,并非顺序表绝对最优,需结合数据量和场景选择:
1. 顺序表(切片/数组):日常开发中小数据量场景最常见。栈的核心操作是“末尾入栈、末尾出栈”,顺序表的末尾操作(append、删除末尾元素)时间复杂度是O(1),效率极高,且代码简单;但需注意扩容和内存浪费问题:切片扩容时会申请更大的内存空间(通常是原容量的2倍),若数据量极大后又大幅减少,扩容后的空闲内存无法释放,会造成内存浪费,且大数据量下频繁扩容也会有性能损耗。
2. 链表:大数据量、需频繁增减数据的场景更优。链表实现栈无需担心扩容问题,增减元素时只需修改指针,无需移动大量数据,也不会造成内存浪费;且链表的内存是按需分配,数据减少时内存可及时释放,适合大数据处理场景。但链表需要额外维护指针,代码比顺序表复杂,日常小数据量场景下几乎不用。
栈的两种实现方式对比示意图(放在代码块里,直观区分):
【栈的两种实现方式对比】 ┌─────────────────────────────────────────────────┐ │ 顺序表(切片)实现栈 │ ├─────────────────────────────────────────────────┤ │ 初始化:空切片(var stack []int) │ │ 入栈:append(stack, num) (末尾添加,O(1)) │ │ 出栈:stack = stack[:len(stack)-1](O(1)) │ │ 优点:代码简单、操作高效 │ │ 缺点:大数据量扩容后易造成内存浪费 │ └─────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────┐ │ 链表实现栈 │ ├─────────────────────────────────────────────────┤ │ 初始化:定义链表节点,维护头指针(栈顶) │ │ 入栈:修改头指针,添加新节点(O(1)) │ │ 出栈:修改头指针,删除栈顶节点(O(1)) │ │ 优点:无扩容问题、不浪费内存 │ │ 缺点:代码复杂、需维护指针 │ └─────────────────────────────────────────────────┘packagemainimport"fmt"// 定义栈结构,用切片存储数据(切片本身就是线性的,完美适配)typeStackstruct{data[]int// 存储栈里的数据,这里用int举例,其他类型也可以}// 入栈:往栈顶添加数据(类比叠衣服)func(s*Stack)Push(numint){s.data=append(s.data,num)// 切片末尾添加,就是栈顶}// 出栈:从栈顶删除并返回数据(类比拿衣服)func(s*Stack)Pop()(int,error){// 先判断栈是否为空,空栈不能出栈ifs.IsEmpty(){return0,fmt.Errorf("栈为空,无法出栈")}// 切片最后一个元素就是栈顶,先取出,再删除top:=s.data[len(s.data)-1]s.data=s.data[:len(s.data)-1]returntop,nil}// 查看栈顶:只看不删func(s*Stack)Peek()(int,error){ifs.IsEmpty(){return0,fmt.Errorf("栈为空,无栈顶元素")}returns.data[len(s.data)-1],nil}// 判断栈是否为空func(s*Stack)IsEmpty()bool{returnlen(s.data)==0}// 测试一下,看看效果funcmain(){// 初始化一个空栈stack:=&Stack{}// 入栈:1->2->3(类比叠衣服:1在最下,3在最上)stack.Push(1)stack.Push(2)stack.Push(3)fmt.Println("入栈后,栈顶元素:",stack.Peek())// 输出3// 出栈:先出3,再出2(类比拿衣服:先拿最上面的3)pop1,_:=stack.Pop()fmt.Println("第一次出栈:",pop1)// 输出3pop2,_:=stack.Pop()fmt.Println("第二次出栈:",pop2)// 输出2fmt.Println("出栈两次后,栈是否为空?",stack.IsEmpty())// 输出false(还有1)}3. 栈的实际用途(知道用在哪,才好理解)
不用记复杂的场景,记住两个最常见的:
浏览器的“后退”功能:你浏览页面1→页面2→页面3,后退时先退页面3,再退页面2,就是栈的逻辑;
括号匹配:比如判断 (())、()() 是否合法,就是用栈来存储左括号,遇到右括号就出栈匹配,0基础暂时不用深究,知道就行。
二、队列(Queue):像排队买奶茶一样,“先进先出”
讲完栈,再看队列,它的主流逻辑和栈刚好相反,但要注意:队列并非只有一种逻辑,也存在先进后出的特殊队列(如双端队列的特殊使用场景),不过我们今天重点讲「线性结构的常规队列」。还是用生活场景类比常规队列:排队买奶茶,先到的人排在前面,先拿到奶茶;后到的人排在后面,只能等前面的人都拿到了,才能轮到自己。这就是线性结构常规队列的核心逻辑——先进先出(FIFO)。
队列就像一个“两头开口的管道”,数据只能从“一端进”(队尾),从“另一端出”(队头),不能插队、也不能从中间进出,完美贴合线性结构的特点。
用生活场景类比+文字示意图(放在代码块里),理解队列的核心操作:
【队列的文字示意图(类比排队买奶茶)】 ┌─────────┬─────────┬─────────┐ │ 1(队头) │ 2(中间) │ 3(队尾) │ ← 排队的顾客(数据) └─────────┴─────────┴─────────┘ ↓ ↓ ↓ 出队口 中间位置 入队口 操作流程说明: 1. 入队:4 → 排在3后面(队尾),队列变成 [1,2,3,4]; 2. 出队:1先离开(队头),队列变成 [2,3,4]; 3. 全程只能从队尾入、队头出,符合“先进先出”逻辑。示意图说明:排队买奶茶时,顾客依次排到队尾(入队),先到的顾客先买完离开(出队),和队列“先进先出”的逻辑完全一致。
补充说明:队列有很多类型(比如循环队列、链式队列、双端队列等),其中双端队列可实现先进后出的效果(类似栈),但我们今天只聚焦「线性结构的常规队列」(也就是顺序队列),其核心逻辑是先进先出,不用管其他复杂类型和特殊场景,0基础先掌握这个核心就够了。
1. 队列的核心操作(和栈对比着记,更简单)
入队(Enqueue):把数据放到队列的末尾(就像排队时,站到队尾);
出队(Dequeue):把队列最前面的数据拿走(就像排队时,最前面的人买完奶茶离开);
查看队头(Front):看看最前面是什么数据,但不拿走(就像看看队伍最前面的人是谁);
判断空队列(IsEmpty):看看队列里有没有数据(就像看看队伍有没有人)。
2. Go实现极简线性队列(同样无复杂语法)
和栈一样,Go也没有自带的队列,我们还是用切片实现,重点区分“队头”和“队尾”——切片的开头是队头,切片的末尾是队尾,入队就是append,出队就是删除切片的第一个元素。
补充:线性队列的实现同样有顺序表(切片、数组)和链表两种方式,两者各有适用场景,日常开发中顺序表(切片)更常见,但需注意其局限性:顺序表实现队列时,出队(删除第一个元素)需要移动所有元素,时间复杂度是O(n)(元素越多,移动越慢)。如果队列数据量小、操作不频繁,顺序表(切片)足够用;若数据量大、出队频繁,链表实现更优——链表出队时只需修改头指针,时间复杂度O(1),无需移动元素,效率更高(但链表代码稍复杂,0基础可先掌握顺序表实现)。
队列两种实现方式对比示意图(放在代码块里,清晰易读):
【队列的两种实现方式对比】 ┌─────────────────────────────────────────────────┐ │ 顺序表(切片)实现队列 │ ├─────────────────────────────────────────────────┤ │ 初始化:空切片(var queue []int) │ │ 入队:append(queue, num) (队尾,O(1)) │ │ 出队:queue = queue[1:] (移动元素,O(n)) │ │ 优点:代码简单、小数据量适用 │ │ 缺点:大数据量出队慢、可能浪费内存 │ └─────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────┐ │ 链表实现队列 │ ├─────────────────────────────────────────────────┤ │ 初始化:定义链表节点,维护头、尾指针 │ │ 入队:修改尾指针,添加新节点(O(1)) │ │ 出队:修改头指针,删除队头节点(O(1)) │ │ 优点:大数据量高效、无扩容浪费 │ │ 缺点:代码复杂、需维护指针 │ └─────────────────────────────────────────────────┘packagemainimport"fmt"// 定义队列结构,用切片存储数据(线性结构)typeQueuestruct{data[]int// 存储队列数据,int类型举例}// 入队:往队尾添加数据(类比排队站到队尾)func(q*Queue)Enqueue(numint){q.data=append(q.data,num)// 切片末尾添加,就是队尾}// 出队:从队头删除并返回数据(类比排队最前面的人离开)func(q*Queue)Dequeue()(int,error){// 先判断队列是否为空,空队列不能出队ifq.IsEmpty(){return0,fmt.Errorf("队列为空,无法出队")}// 切片第一个元素是队头,先取出,再删除front:=q.data[0]q.data=q.data[1:]// 切片从索引1开始,相当于删除第一个元素returnfront,nil}// 查看队头:只看不删func(q*Queue)Front()(int,error){ifq.IsEmpty(){return0,fmt.Errorf("队列为空,无队头元素")}returnq.data[0],nil}// 判断队列是否为空func(q*Queue)IsEmpty()bool{returnlen(q.data)==0}// 测试一下funcmain(){// 初始化一个空队列queue:=&Queue{}// 入队:1->2->3(类比排队:1在最前,3在队尾)queue.Enqueue(1)queue.Enqueue(2)queue.Enqueue(3)fmt.Println("入队后,队头元素:",queue.Front())// 输出1// 出队:先出1,再出2(类比排队:1先买完离开,然后是2)deq1,_:=queue.Dequeue()fmt.Println("第一次出队:",deq1)// 输出1deq2,_:=queue.Dequeue()fmt.Println("第二次出队:",deq2)// 输出2fmt.Println("出队两次后,队列是否为空?",queue.IsEmpty())// 输出false(还有3)}3. 队列的实际用途(贴近生活,好记)
消息通知:我们手机收到的消息,按时间顺序排列,先收到的先显示、先处理,就是队列;
打印队列:多个人同时打印文件,打印机按顺序打印,先提交的先打印,后提交的排队,也是队列的逻辑。
三、栈和队列的核心区别(一张表看懂,0基础不混淆)
| 对比项 | 栈(Stack) | 线性队列(Queue) |
|---|---|---|
| 核心逻辑 | 后进先出(LIFO),像叠衣服 | 先进先出(FIFO),像排队买奶茶 |
| 操作端 | 只有一个开口(栈顶),入栈、出栈都在这 | 两个开口(队头出、队尾进) |
| Go实现 | 切片append(入栈)、删除末尾(出栈) | 切片append(入队)、删除开头(出队) |
| 最优/常见实现 | 小数据量:顺序表(切片);大数据量:链表 | 小数据量:顺序表(切片);大数据量:链表 |
| 生活类比 | 叠衣服、浏览器后退、装快递箱子 | 排队买奶茶、打印文件、消息通知 |
补充:表格中两种结构的实现选择,可结合上面的示意图理解,小数据量优先选顺序表(切片)(简单高效),大数据量优先选链表(无扩容、不浪费内存)。
四、0基础总结(重点记这3点,不迷路)
栈和队列都是「线性结构」,数据排成一条直线,没有分支;
栈:后进先出(先放的后拿),只有一个操作口,Go用切片就能实现;实现上,小数据量用顺序表(切片)最常见,大数据量、需频繁增减数据时,链表更优(无扩容烦恼、不浪费内存),纠正“顺序表绝对最优”的误区,兼顾扩容和内存浪费问题。
线性队列:主流逻辑是先进先出(先放的先拿),有两个操作口(队头出、队尾进),同样用切片实现;也存在先进后出的特殊线性队列(如双端队列的特殊使用),但常规场景下以先进先出为主;实现上,小数据量用顺序表(切片)最常见,大数据量用链表更优。
其实数据结构没有那么可怕,只要把抽象的逻辑和生活中的场景结合起来,再动手跑一跑简单的代码,0基础也能轻松掌握。今天的栈和队列就讲到这里,后续可以再深入学习更复杂的队列类型,但先把这个基础打牢就够啦~
如果大家有不懂的地方,或者想补充其他知识点,欢迎在评论区留言哦!
【结语】
到这里,Go中栈和队列的核心知识点就全部讲解完毕了。我们不难发现,栈和队列的实现始终围绕着上一篇提到的切片(顺序表)展开,同时也了解了链表在大数据场景下的优势,这既是对切片copy知识点的延伸,也为我们下一篇要学习的哈希表做好了铺垫。下一篇,我们将跳出线性结构的范畴,学习非线性结构中的哈希表,看看它如何解决栈和队列在查找效率上的局限,继续跟着节奏,一步步夯实Go数据结构的基础~
关注我,点赞👍、收藏⭐本篇内容
(注:文档部分内容可能由 AI 生成)
