数据结构和算法-Go泛型实现:第5章栈-1.栈的实现

91 阅读4分钟

第五章:栈

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
}

PopPeek 操作中,当栈为空时,我们需要返回一个类型的零值以表示没有元素。这时候可以使用 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),但需要额外的内存存储节点指针,对于大规模数据,内存占用会相对较高。

选择哪种实现方式应根据具体应用场景进行权衡。如果需要频繁插入和删除且数据规模较小,可以选择节点栈;如果数据规模较大且需要紧凑的内存使用,可以选择切片栈。