Golang实现数据结构“栈”的三种实现,性能对比及应用示例

栏目: 数据库 · 发布时间: 5年前

内容简介:本文主要讲一讲栈这种非常基础的数据结构,以及其如何用Golang来实现的3种方式,简单用golang bench做一个性能比对,以字符串括号匹配的一个例子来看其一个简单的应用场景。栈是一种FILO类型的数据结构,FILO 即 Fisrt In Last Out,也就是先进后出,也可以说是后进先出。它很像一个只有一个出口的站立的杯子,后边进入杯子的东西,会被最先取出来。首先,实现栈,远非只有这三种方式,这里,只是举例3种相对比较典型的方式。每一种实现方式都很简单,也没什么需要太费周章讲的必要。

本文主要讲一讲栈这种非常基础的数据结构,以及其如何用Golang来实现的3种方式,简单用golang bench做一个性能比对,以字符串括号匹配的一个例子来看其一个简单的应用场景。

栈的特性

栈是一种FILO类型的数据结构,FILO 即 Fisrt In Last Out,也就是先进后出,也可以说是后进先出。它很像一个只有一个出口的站立的杯子,后边进入杯子的东西,会被最先取出来。

栈实现的三种方式

首先,实现栈,远非只有这三种方式,这里,只是举例3种相对比较典型的方式。每一种实现方式都很简单,也没什么需要太费周章讲的必要。

1、利用Golang的slice

package main

import (
	"fmt"
	"sync"
)

// Item the type of the stack
type Item interface{}

// ItemStack the stack of Items
type ItemStack struct {
	items []Item
	lock  sync.RWMutex
}

// New creates a new ItemStack
func NewStack() *ItemStack {
	s := &ItemStack{}
	s.items = []Item{}
	return s
}

// Pirnt prints all the elements
func (s *ItemStack) Print() {
	fmt.Println(s.items)
}

// Push adds an Item to the top of the stack
func (s *ItemStack) Push(t Item) {
	s.lock.Lock()
	s.lock.Unlock()
	s.items = append(s.items, t)
}

// Pop removes an Item from the top of the stack
func (s *ItemStack) Pop() Item {
	s.lock.Lock()
	defer s.lock.Unlock()
	if len(s.items) == 0 {
		return nil
	}
	item := s.items[len(s.items)-1]
	s.items = s.items[0 : len(s.items)-1]
	return item
}

此方式特点:

  1. 利用Golang的Slice,足够简单。
  2. 允许添加任意类型的元素。
  3. Push和Pop有加锁处理,线程安全。
  4. 在一些文章里(Reddit)提到,使用slice作为Stack的底层支持,速度更快。但是,slice最大的问题其实是存在一个共用底层数组的的问题,哪怕你不断的Pop,但可能对于Golang来说,其占用的内存,并不一定减少。

性能测试如下:

package main

import (
	"testing"
)

var stack *ItemStack

func init() {
	stack = NewStack()
}

func Benchmark_Push(b *testing.B) {
	for i := 0; i < b.N; i++ { //use b.N for looping
		stack.Push("test")
	}
}

func Benchmark_Pop(b *testing.B) {
	b.StopTimer()
	for i := 0; i < b.N; i++ { //use b.N for looping
		stack.Push("test")
	}
	b.StartTimer()
	for i := 0; i < b.N; i++ { //use b.N for looping
		stack.Pop()
	}
}
$ go test -test.bench=".*" -benchmem -v
goos: darwin
goarch: amd64
pkg: test/test8
Benchmark_Push-4   	10000000	         222 ns/op	      94 B/op	       0 allocs/op
Benchmark_Pop-4    	20000000	        65.0 ns/op	       0 B/op	       0 allocs/op
PASS
ok  	test/test8	7.644s

2、利用Golang的container/list内置包

package main

import (
	"container/list"
	"sync"
)

type Stack struct {
	list *list.List
	lock *sync.RWMutex
}

func NewStack() *Stack {
	list := list.New()
	l := &sync.RWMutex{}
	return &Stack{list, l}
}

func (stack *Stack) Push(value interface{}) {
	stack.lock.Lock()
	defer stack.lock.Unlock()
	stack.list.PushBack(value)
}

func (stack *Stack) Pop() interface{} {
	stack.lock.Lock()
	defer stack.lock.Unlock()
	e := stack.list.Back()
	if e != nil {
		stack.list.Remove(e)
		return e.Value
	}
	return nil
}

func (stack *Stack) Peak() interface{} {
	e := stack.list.Back()
	if e != nil {
		return e.Value
	}

	return nil
}

func (stack *Stack) Len() int {
	return stack.list.Len()
}

func (stack *Stack) Empty() bool {
	return stack.list.Len() == 0
}

简单来说,container/list 是一个链表。用链表模拟栈,要么都向链表的最后做push和pop,要么都向链表的起点做push和pop,这种方法也非常简单。

性能测试如下:

$ go test -test.bench=".*" -benchmem -v  -count=1
goos: darwin
goarch: amd64
pkg: test/test10
Benchmark_Push-4   	 5000000	       222 ns/op	      48 B/op	       1 allocs/op
Benchmark_Pop-4    	20000000	        73.5 ns/op	       0 B/op	       0 allocs/op
PASS
ok  	test/test10	10.837s

3、godoc的实现参考(自定义数据结构实现)

package main

import (
	"sync"
)

type (
	Stack struct {
		top    *node
		length int
		lock   *sync.RWMutex
	}
	node struct {
		value interface{}
		prev  *node
	}
)

// Create a new stack
func NewStack() *Stack {
	return &Stack{nil, 0, &sync.RWMutex{}}
}

// Return the number of items in the stack
func (this *Stack) Len() int {
	return this.length
}

// View the top item on the stack
func (this *Stack) Peek() interface{} {
	if this.length == 0 {
		return nil
	}
	return this.top.value
}

// Pop the top item of the stack and return it
func (this *Stack) Pop() interface{} {
	this.lock.Lock()
	defer this.lock.Unlock()
	if this.length == 0 {
		return nil
	}
	n := this.top
	this.top = n.prev
	this.length--
	return n.value
}

// Push a value onto the top of the stack
func (this *Stack) Push(value interface{}) {
	this.lock.Lock()
	defer this.lock.Unlock()
	n := &node{value, this.top}
	this.top = n
	this.length++
}

此方式特点:

  1. 实现比较精巧,唯一需要理解的地方就是 node 结构体中,prev 的部分,它指向的是前一个node成员。
  2. 允许添加任意类型的元素。
  3. Push和Pop是线程安全的。

性能测试如下:

$ go test -test.bench=".*" -benchmem -v
goos: darwin
goarch: amd64
pkg: test/test9
Benchmark_Push-4   	10000000	         178 ns/op	      32 B/op	       1 allocs/op
Benchmark_Pop-4    	20000000	        75.5 ns/op	       0 B/op	       0 allocs/op
PASS
ok  	test/test9	9.776s

对比

三种方式,总的来看,第三种基于自定义数据结构的实现方式,在push上效率最高,而且实现也比较精巧。个人其实是推荐使用这种方式的。其次,是基于 container/list 实现的方式。

特性对比 push速度 pop速度 push内存分配 pop内存分配
基于slice 222ns/op 65ns/op 94B/op 0B/op
container/list链表 222ns/op 73.5ns/op 48B/op 0B/op
自定义数据结构 178ns/op 75ns/op 32B/op 0B/op

栈数据结构的用途

栈这种数据结构通途很多。典型的应用,比如编辑器的撤销(undo)操作,以及校验字符匹配问题。下面主要举一个校验字符匹配的例子:

题目:给定一个包含了右侧三种字符的字符串, ‘(‘, ‘)’, ‘{‘, ‘}’, ‘[‘ and ‘]’,判断字符串是否合法。合法的判断条件如下:

  1. 必须使用相同类型的括号关闭左括号。
  2. 必须以正确的顺序关闭打开括号。

示例1:

Input: "()[]{}"
Output: true

示例2:

Input: "([)]"
Output: false

参考实现:

func isValid(s string) bool {
    // 括号对字典
    bracketsMap := map[uint8]uint8{'{': '}', '[': ']', '(': ')'}
    // 传入字符串为空则返回false(leetcode认为空字符串应该返回true,这里注意)
    if s == "" {
        return false
    }
    // 初始化栈
    stack := NewStack()
    for i := 0; i < len(s); i++ {
        // 如果栈中有数据,则进行比对,如果栈顶元素和当前元素匹配,则弹出,其他情况向栈中压入元素
        if stack.Len() > 0 {
            if c, ok := bracketsMap[stack.Peek().(uint8)]; ok && c == s[i] {
                stack.Pop()
                continue
            }
        }
        stack.Push(s[i])
    }
    // 到最后如果栈不为空,则说明未完全匹配掉(完全匹配的话,栈只能为空)
    return stack.Len() == 0
}

参考

  1. https://flaviocopes.com/golang-data-structure-stack/
  2. https://github.com/hishboy/gocommons/tree/master/lang
  3. https://www.reddit.com/r/golang/comments/25aeof/building_a_stack_in_go_slices_vs_linked_list/

以上所述就是小编给大家介绍的《Golang实现数据结构“栈”的三种实现,性能对比及应用示例》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Servlet与JSP核心编程

Servlet与JSP核心编程

[美]Marty Hall、Larry Brown、Yaakov Chalkin / 胡书敏 / 2009-6 / 68.00元

《Servlet与JSP核心编程(第2卷 第2版)》在第l卷的基础上,广泛涉及自定义标签库、过滤器、声明式安全、JSTL和Struts等主题,并沿袭深受读者喜爱的写作风格,通过完整、有效、资料丰富的程序来演绎目前最流行的技术和最佳实践。Java EE已经成为电子商务网站、动态网站和Web应用与服务开发的首选,作为这一平台的基础,servlet与JSP的重要性日益突出,并在极短的时间内得以迅速普及。......一起来看看 《Servlet与JSP核心编程》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具