数据结构-栈

数据结构 栈

前言

久违的博客。保持学习,回顾以前曾经学过的数据结构。工作了一年多基本全是数组/哈希表解决一切,基本没想过其他实现,也没想过底层或者优化。

结果前几天做写一个地方,写了一个O(n^2)的代码,在思考能不能降到O(n)甚至O(1)的时候,突然发现自己对于这块的知识已经模糊了,于是重新开始学习数据结构与算法。

这可能会是一个系列,我希望我能做到一周一篇。

定义

栈,最常见的定义就是一个遵守先进后出,并且只在一端进行存取操作的线性表。

如一打碟子,我们总是会先拿最上面的,而最底下的那个往往是最先放到桌子上的。这便是一个形象的栈结构。

它的常见方法有:

type Stacks interface {
	// 判断栈是否为空
	Empty() bool
	// 输出栈大小
	Size() int
	// 返回栈顶元素,但不推出
	Top() (val interface{}, ok bool)
	// 推出栈顶元素
	Pop() (val interface{}, ok bool)
	// 向栈顶压入元素
	Push(val interface{}) (ok bool)
}

我不是狠喜欢给值定义为interface,不过为了表示普适性,还是这样写了。

实现

基于上面的定义,我们就可以很轻松地做写栈的实现了。我们用两个基础线性表:数组、链表 分别实现

个人愚见,所谓的用链表或者数组实现,其实只是将链表或者数组再次抽象

数组实现


type ArrayStack struct {
	Array []interface{}
}

func (s *ArrayStack) Empty() bool {
	return len(s.Array) == 0
}

func (s *ArrayStack) Size() int {
	return len(s.Array)
}

func (s *ArrayStack) Top() (val interface{}, ok bool) {
	if s.Empty() {
		return nil, false
	}
	val = s.Array[s.Size()-1]
	return val, true
}

func (s *ArrayStack) Pop() (val interface{}, ok bool) {
	if s.Empty() {
		return nil, false
	}
	val = s.Array[s.Size()-1]
	s.Array = s.Array[:s.Size()-1]
	return val, true
}

func (s *ArrayStack) Push(val interface{}) (ok bool) {
	s.Array = append(s.Array, val)
	return true
}

链表实现

链表实现有一个问题需要思考,那就是栈顶选在头节点还是末尾

如果选在末尾的话,我们使用Top、Pop、Push 需要调用的链表方法对应 Get(Size()-1),insert(Size(),val),Remove(Size()-1) 这样的话,时间复杂度是 O(n)

而如果选在头节点的话,就都是Get(0),insert(0,val),Remove(0) 时间复杂度为O(1)

因而选择用头节点做栈顶

type LinkedStack struct {
	ChainNode *chainNode
	StackSize int
}

type chainNode struct {
	Val  interface{}
	Next *chainNode
}

func (s *LinkedStack) Empty() bool {
	return s.StackSize == 0
}

func (s *LinkedStack) Size() int {
	return s.StackSize
}

func (s *LinkedStack) Top() (val interface{}, ok bool) {
	if s.Empty() {
		return nil, false
	}
	return s.ChainNode.Val, true
}

func (s *LinkedStack) Pop() (val interface{}, ok bool) {
	if s.Empty() {
		return nil, false
	}
	val = s.ChainNode.Val
	s.ChainNode = s.ChainNode.Next
	s.StackSize--
	return val, true
}

func (s *LinkedStack) Push(val interface{}) (ok bool) {
	s.ChainNode = &chainNode{
		Val:  val,
		Next: s.ChainNode,
	}
	s.StackSize++
	return true
}

应用

来到应用这一步了,使用leetcode题目作为演示

一道经典的栈应用题目: 括号匹配

括号匹配

(leetcode)[https://leetcode.cn/problems/valid-parentheses/]

简单题,题目要求就是给定一个字符串,仅由’()[]{}‘组成,判断字符串能否闭合

思路简单,string进来后入栈

[{()}]

那么先入一半 [{(

后面的每一个元素必定与栈顶元素抵消

那如果是 [](){}

那就每入第一个元素,之后的元素可以与栈顶元素抵消

同时可以在一开始下先判断一次奇偶,为奇必错,直接返回

代码:

func isValid(s string) bool {
	n := len(s)
	if n%2 == 1 {
		return false
	}
	// 使用Map快速配对
	pairs := map[byte]byte{
		')': '(',
		']': '[',
		'}': '{',
	}
	stack := []byte{}
	for i := 0; i < n; i++ {
		v, ok := pairs[s[i]]
		if ok {
			// 拿到了右括号,但是没有与其抵消的左括号在栈顶,说明无配对括号,错误
			if len(stack) == 0 || stack[len(stack)-1] != v {
				return false
			}
			// 抵消
			stack = stack[:len(stack)-1]
		} else {
			stack = append(stack, s[i])
		}
	}
	return len(stack) == 0
}

结语

本文使用go语言实现了栈的两种实现方式,对应的代码与单元测试可以详见 个人github

日后会尽力保证每周更新博客,后续数据结构计划为:

  • 队列
  • 跳表与散列
  • 二叉树与其他树
  • 优先级队列
  • 竞赛树
  • 搜索树
  • 平衡搜索树