第五章:栈
1. 栈 ADT
栈(Stack)是一种后进先出(LIFO,Last In First Out)的数据结构,其特点是元素的插入和删除都发生在同一端,称为栈顶(Top)。栈在计算机科学中有着广泛的应用,如函数调用、表达式求值等。
栈 ADT 主要包含以下操作:
Push:将元素压入栈顶。Pop:从栈顶弹出元素。Peek:查看栈顶元素但不弹出。IsEmpty:检查栈是否为空。Size:获取栈中元素的数量。
2. 泛型栈的切片实现
使用 Go 泛型可以让我们定义一个通用的栈结构,可以存储任意类型的元素。以下是一个使用切片实现的泛型栈。
泛型栈实现
package stack
type Stack[T any] struct {
elements []T
}
// New 创建一个新的栈
func New[T any]() *Stack[T] {
return &Stack[T]{}
}
// Push 将元素压入栈顶
func (s *Stack[T]) Push(element T) {
s.elements = append(s.elements, element)
}
// Pop 从栈顶弹出元素
func (s *Stack[T]) Pop() (T, bool) {
if len(s.elements) == 0 {
var zero T
return zero, false
}
element := s.elements[len(s.elements)-1]
s.elements = s.elements[:len(s.elements)-1]
return element, true
}
// Peek 查看栈顶元素但不弹出
func (s *Stack[T]) Peek() (T, bool) {
if len(s.elements) == 0 {
var zero T
return zero, false
}
return s.elements[len(s.elements)-1], true
}
// IsEmpty 检查栈是否为空
func (s *Stack[T]) IsEmpty() bool {
return len(s.elements) == 0
}
// Size 获取栈中元素的数量
func (s *Stack[T]) Size() int {
return len(s.elements)
}
GetZero 函数
GetZero 函数用于获取某个类型的零值。在 Go 泛型中,当我们需要返回一个类型的零值时,可以使用以下方法:
package stack
// GetZero 返回类型 T 的零值
func GetZero[T any]() T {
var zero T
return zero
}
在 Pop 和 Peek 操作中,当栈为空时,我们需要返回一个类型的零值以表示没有元素。这时候可以使用 GetZero 函数。
为什么 T 被声明为 Ordered
在某些情况下,我们可能需要栈中的元素具有可比较性,例如实现排序算法时。Go 泛型允许我们使用类型约束来限制泛型类型的范围。通过约束 T 为 constraints.Ordered,我们可以确保栈中的元素是可以比较的。
package stack
import (
"golang.org/x/exp/constraints"
)
type OrderedStack[T constraints.Ordered] struct {
elements []T
}
func NewOrderedStack[T constraints.Ordered]() *OrderedStack[T] {
return &OrderedStack[T]{}
}
// 其余实现与普通泛型栈类似
在上述代码中,我们使用 constraints.Ordered 约束 T,这意味着 T 必须是一个可比较的类型(如整型、浮点型、字符串等)。
通过这种方式,我们可以定义一个功能强大且通用的栈数据结构,并能够在各种场景下使用。这个泛型栈不仅可以存储任意类型的元素,还可以根据需求进行类型约束,确保特定操作的合法性和安全性。
3. 泛型栈的节点实现
除了使用切片实现栈,我们还可以使用节点链表的方式来实现栈。这种实现方式在插入和删除元素时具有 O(1) 的时间复杂度。
节点栈实现
package stack
// Node 是链表节点
type Node[T any] struct {
value T
next *Node[T]
}
// Stack 是链表实现的栈
type Stack[T any] struct {
top *Node[T]
size int
}
// New 创建一个新的链表栈
func New[T any]() *Stack[T] {
return &Stack[T]{}
}
// Push 将元素压入栈顶
func (s *Stack[T]) Push(value T) {
newNode := &Node[T]{value: value, next: s.top}
s.top = newNode
s.size++
}
// Pop 从栈顶弹出元素
func (s *Stack[T]) Pop() (T, bool) {
if s.top == nil {
var zero T
return zero, false
}
value := s.top.value
s.top = s.top.next
s.size--
return value, true
}
// Peek 查看栈顶元素但不弹出
func (s *Stack[T]) Peek() (T, bool) {
if s.top == nil {
var zero T
return zero, false
}
return s.top.value, true
}
// IsEmpty 检查栈是否为空
func (s *Stack[T]) IsEmpty() bool {
return s.top == nil
}
// Size 获取栈中元素的数量
func (s *Stack[T]) Size() int {
return s.size
}
4. 节点栈与切片栈的效率比较
两种实现方式各有优缺点:
-
切片栈:插入和删除操作的平均时间复杂度为 O(1),但在切片扩容时可能需要 O(n) 的时间复杂度。切片栈在大部分情况下非常高效,且内存使用较为紧凑。
-
节点栈:插入和删除操作的时间复杂度始终为 O(1),但需要额外的内存存储节点指针,对于大规模数据,内存占用会相对较高。
选择哪种实现方式应根据具体应用场景进行权衡。如果需要频繁插入和删除且数据规模较小,可以选择节点栈;如果数据规模较大且需要紧凑的内存使用,可以选择切片栈。