《hello-algo》栈与队列 —— 小记随笔
栈
我们把堆叠元素的顶部称为“栈顶”,底部称为“栈底”。将把元素添加到栈顶的操作叫作“入栈”,删除栈顶元素的操作叫作“出栈”。
栈的常用操作
/* 初始化栈 */ // 在 Go 中,推荐将 Slice 当作栈来使用 var stack []int /* 元素入栈 */ stack = append(stack, 1) stack = append(stack, 3) stack = append(stack, 2) stack = append(stack, 5) stack = append(stack, 4) /* 访问栈顶元素 */ peek := stack[len(stack)-1] /* 元素出栈 */ pop := stack[len(stack)-1] stack = stack[:len(stack)-1] /* 获取栈的长度 */ size := len(stack) /* 判断是否为空 */ isEmpty := len(stack) == 0
栈的实现
为了深入了解栈的运行机制,我们来尝试自己实现一个栈类。
栈遵循先入后出的原则,因此我们只能在栈顶添加或删除元素。然而,数组和链表都可以在任意位置添加和删除元素,因此栈可以视为一种受限制的数组或链表。换句话说,我们可以“屏蔽”数组或链表的部分无关操作,使其对外表现的逻辑符合栈的特性。
基于链表的实现
/* 基于链表实现的栈 */ type linkedListStack struct { // 使用内置包 list 来实现栈 data *list.List } /* 初始化栈 */ func newLinkedListStack() *linkedListStack { return &linkedListStack{ data: list.New(), } } /* 入栈 */ func (s *linkedListStack) push(value int) { s.data.PushBack(value) } /* 出栈 */ func (s *linkedListStack) pop() any { if s.isEmpty() { return nil } e := s.data.Back() s.data.Remove(e) return e.Value } /* 访问栈顶元素 */ func (s *linkedListStack) peek() any { if s.isEmpty() { return nil } e := s.data.Back() return e.Value } /* 获取栈的长度 */ func (s *linkedListStack) size() int { return s.data.Len() } /* 判断栈是否为空 */ func (s *linkedListStack) isEmpty() bool { return s.data.Len() == 0 } /* 获取 List 用于打印 */ func (s *linkedListStack) toList() *list.List { return s.data }
基于数组的实现
/* 基于数组实现的栈 */ type arrayStack struct { data []int // 数据 } /* 初始化栈 */ func newArrayStack() *arrayStack { return &arrayStack{ // 设置栈的长度为 0,容量为 16 data: make([]int, 0, 16), } } /* 栈的长度 */ func (s *arrayStack) size() int { return len(s.data) } /* 栈是否为空 */ func (s *arrayStack) isEmpty() bool { return s.size() == 0 } /* 入栈 */ func (s *arrayStack) push(v int) { // 切片会自动扩容 s.data = append(s.data, v) } /* 出栈 */ func (s *arrayStack) pop() any { val := s.peek() s.data = s.data[:len(s.data)-1] return val } /* 获取栈顶元素 */ func (s *arrayStack) peek() any { if s.isEmpty() { return nil } val := s.data[len(s.data)-1] return val } /* 获取 Slice 用于打印 */ func (s *arrayStack) toSlice() []int { return s.data }
两种实现对比
时间效率
- 基于数组实现的栈在触发扩容时效率会降低,但由于扩容是低频操作,因此平均效率更高。
- 基于链表实现的栈可以提供更加稳定的效率表现。
空间效率
在初始化列表时,系统会为列表分配“初始容量”,该容量可能超出实际需求;并且,扩容机制通常是按照特定倍率(例如 2 倍)进行扩容的,扩容后的容量也可能超出实际需求。因此,基于数组实现的栈可能造成一定的空间浪费。
然而,由于链表节点需要额外存储指针,因此链表节点占用的空间相对较大。
队列
「队列 queue」是一种遵循先入先出规则的线性数据结构。顾名思义,队列模拟了排队现象,即新来的人不断加入队列尾部,而位于队列头部的人逐个离开。
队列常用操作
/* 初始化队列 */ // 在 Go 中,将 list 作为队列来使用 queue := list.New() /* 元素入队 */ queue.PushBack(1) queue.PushBack(3) queue.PushBack(2) queue.PushBack(5) queue.PushBack(4) /* 访问队首元素 */ peek := queue.Front() /* 元素出队 */ pop := queue.Front() queue.Remove(pop) /* 获取队列的长度 */ size := queue.Len() /* 判断队列是否为空 */ isEmpty := queue.Len() == 0
队列实现
基于链表的实现
/* 基于链表实现的队列 */ type linkedListQueue struct { // 使用内置包 list 来实现队列 data *list.List } /* 初始化队列 */ func newLinkedListQueue() *linkedListQueue { return &linkedListQueue{ data: list.New(), } } /* 入队 */ func (s *linkedListQueue) push(value any) { s.data.PushBack(value) } /* 出队 */ func (s *linkedListQueue) pop() any { if s.isEmpty() { return nil } e := s.data.Front() s.data.Remove(e) return e.Value } /* 访问队首元素 */ func (s *linkedListQueue) peek() any { if s.isEmpty() { return nil } e := s.data.Front() return e.Value } /* 获取队列的长度 */ func (s *linkedListQueue) size() int { return s.data.Len() } /* 判断队列是否为空 */ func (s *linkedListQueue) isEmpty() bool { return s.data.Len() == 0 } /* 获取 List 用于打印 */ func (s *linkedListQueue) toList() *list.List { return s.data }
基于数组的实现
我们可以使用一个变量 front 指向队首元素的索引,并维护一个变量 size 用于记录队列长度。定义 rear = front + size ,这个公式计算出的 rear 指向队尾元素之后的下一个位置。
基于此设计,数组中包含元素的有效区间为 [front, rear - 1],各种操作的实现方法如图 5-6 所示。
入队操作:将输入元素赋值给 rear 索引处,并将 size 增加 1 。
出队操作:只需将 front 增加 1 ,并将 size 减少 1 。
可以看到,入队和出队操作都只需进行一次操作,时间复杂度均为 (O(1)) 。
你可能会发现一个问题:在不断进行入队和出队的过程中,front 和 rear 都在向右移动,当它们到达数组尾部时就无法继续移动了。为了解决此问题,我们可以将数组视为首尾相接的“环形数组”。
对于环形数组,我们需要让 front 或 rear 在越过数组尾部时,直接回到数组头部继续遍历。这种周期性规律可以通过“取余操作”来实现,代码如下所示:
/* 基于环形数组实现的队列 */ type arrayQueue struct { nums []int // 用于存储队列元素的数组 front int // 队首指针,指向队首元素 queSize int // 队列长度 queCapacity int // 队列容量(即最大容纳元素数量) } /* 初始化队列 */ func newArrayQueue(queCapacity int) *arrayQueue { return &arrayQueue{ nums: make([]int, queCapacity), queCapacity: queCapacity, front: 0, queSize: 0, } } /* 获取队列的长度 */ func (q *arrayQueue) size() int { return q.queSize } /* 判断队列是否为空 */ func (q *arrayQueue) isEmpty() bool { return q.queSize == 0 } /* 入队 */ func (q *arrayQueue) push(num int) { // 当 rear == queCapacity 表示队列已满 if q.queSize == q.queCapacity { return } // 计算队尾指针,指向队尾索引 + 1 // 通过取余操作实现 rear 越过数组尾部后回到头部 rear := (q.front + q.queSize) % q.queCapacity // 将 num 添加至队尾 q.nums[rear] = num q.queSize++ } /* 出队 */ func (q *arrayQueue) pop() any { num := q.peek() // 队首指针向后移动一位,若越过尾部,则返回到数组头部 q.front = (q.front + 1) % q.queCapacity q.queSize-- return num } /* 访问队首元素 */ func (q *arrayQueue) peek() any { if q.isEmpty() { return nil } return q.nums[q.front] } /* 获取 Slice 用于打印 */ func (q *arrayQueue) toSlice() []int { rear := (q.front + q.queSize) if rear >= q.queCapacity { rear %= q.queCapacity return append(q.nums[q.front:], q.nums[:rear]...) } return q.nums[q.front:rear] }
以上实现的队列仍然具有局限性:其长度不可变。然而,这个问题不难解决,我们可以将数组替换为动态数组,从而引入扩容机制。
双向队列
双向队列常用操作
/* 初始化双向队列 */ // 在 Go 中,将 list 作为双向队列使用 deque := list.New() /* 元素入队 */ deque.PushBack(2) // 添加至队尾 deque.PushBack(5) deque.PushBack(4) deque.PushFront(3) // 添加至队首 deque.PushFront(1) /* 访问元素 */ front := deque.Front() // 队首元素 rear := deque.Back() // 队尾元素 /* 元素出队 */ deque.Remove(front) // 队首元素出队 deque.Remove(rear) // 队尾元素出队 /* 获取双向队列的长度 */ size := deque.Len() /* 判断双向队列是否为空 */ isEmpty := deque.Len() == 0
双向队列实现
基于双向链表的实现
/* 基于双向链表实现的双向队列 */ type linkedListDeque struct { // 使用内置包 list data *list.List } /* 初始化双端队列 */ func newLinkedListDeque() *linkedListDeque { return &linkedListDeque{ data: list.New(), } } /* 队首元素入队 */ func (s *linkedListDeque) pushFirst(value any) { s.data.PushFront(value) } /* 队尾元素入队 */ func (s *linkedListDeque) pushLast(value any) { s.data.PushBack(value) } /* 队首元素出队 */ func (s *linkedListDeque) popFirst() any { if s.isEmpty() { return nil } e := s.data.Front() s.data.Remove(e) return e.Value } /* 队尾元素出队 */ func (s *linkedListDeque) popLast() any { if s.isEmpty() { return nil } e := s.data.Back() s.data.Remove(e) return e.Value } /* 访问队首元素 */ func (s *linkedListDeque) peekFirst() any { if s.isEmpty() { return nil } e := s.data.Front() return e.Value } /* 访问队尾元素 */ func (s *linkedListDeque) peekLast() any { if s.isEmpty() { return nil } e := s.data.Back() return e.Value } /* 获取队列的长度 */ func (s *linkedListDeque) size() int { return s.data.Len() } /* 判断队列是否为空 */ func (s *linkedListDeque) isEmpty() bool { return s.data.Len() == 0 } /* 获取 List 用于打印 */ func (s *linkedListDeque) toList() *list.List { return s.data }
基于数组的实现
与基于数组实现队列类似,我们也可以使用环形数组来实现双向队列。
/* 基于环形数组实现的双向队列 */ type arrayDeque struct { nums []int // 用于存储双向队列元素的数组 front int // 队首指针,指向队首元素 queSize int // 双向队列长度 queCapacity int // 队列容量(即最大容纳元素数量) } /* 初始化队列 */ func newArrayDeque(queCapacity int) *arrayDeque { return &arrayDeque{ nums: make([]int, queCapacity), queCapacity: queCapacity, front: 0, queSize: 0, } } /* 获取双向队列的长度 */ func (q *arrayDeque) size() int { return q.queSize } /* 判断双向队列是否为空 */ func (q *arrayDeque) isEmpty() bool { return q.queSize == 0 } /* 计算环形数组索引 */ func (q *arrayDeque) index(i int) int { // 通过取余操作实现数组首尾相连 // 当 i 越过数组尾部后,回到头部 // 当 i 越过数组头部后,回到尾部 return (i + q.queCapacity) % q.queCapacity } /* 队首入队 */ func (q *arrayDeque) pushFirst(num int) { if q.queSize == q.queCapacity { fmt.Println("双向队列已满") return } // 队首指针向左移动一位 // 通过取余操作实现 front 越过数组头部后回到尾部 q.front = q.index(q.front - 1) // 将 num 添加至队首 q.nums[q.front] = num q.queSize++ } /* 队尾入队 */ func (q *arrayDeque) pushLast(num int) { if q.queSize == q.queCapacity { fmt.Println("双向队列已满") return } // 计算队尾指针,指向队尾索引 + 1 rear := q.index(q.front + q.queSize) // 将 num 添加至队首 q.nums[rear] = num q.queSize++ } /* 队首出队 */ func (q *arrayDeque) popFirst() any { num := q.peekFirst() // 队首指针向后移动一位 q.front = q.index(q.front + 1) q.queSize-- return num } /* 队尾出队 */ func (q *arrayDeque) popLast() any { num := q.peekLast() q.queSize-- return num } /* 访问队首元素 */ func (q *arrayDeque) peekFirst() any { if q.isEmpty() { return nil } return q.nums[q.front] } /* 访问队尾元素 */ func (q *arrayDeque) peekLast() any { if q.isEmpty() { return nil } // 计算尾元素索引 last := q.index(q.front + q.queSize - 1) return q.nums[last] } /* 获取 Slice 用于打印 */ func (q *arrayDeque) toSlice() []int { // 仅转换有效长度范围内的列表元素 res := make([]int, q.queSize) for i, j := 0, q.front; i < q.queSize; i++ { res[i] = q.nums[q.index(j)] j++ } return res }
双向队列应用
双向队列兼具栈与队列的逻辑,因此它可以实现这两者的所有应用场景,同时提供更高的自由度。
我们知道,软件的“撤销”功能通常使用栈来实现:系统将每次更改操作 push 到栈中,然后通过 pop 实现撤销。然而,考虑到系统资源的限制,软件通常会限制撤销的步数(例如仅允许保存 (50) 步)。当栈的长度超过 (50) 时,软件需要在栈底(队首)执行删除操作。但栈无法实现该功能,此时就需要使用双向队列来替代栈。请注意,“撤销”的核心逻辑仍然遵循栈的先入后出原则,只是双向队列能够更加灵活地实现一些额外逻辑。
本文作者:Blue Mountain
本文链接:https://www.cnblogs.com/BlueMountain-HaggenDazs/p/18023297
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具