数据结构与算法03:栈

news2024/11/26 10:17:24

目录

什么是栈?

栈在函数调用中的应用

栈的应用:如何实现浏览器的前进和后退功能?

每日一练:左右括号匹配


什么是栈?

简单地说,先进后出,后进先出的数据结构就是栈,可以理解为一个纸箱子,往箱子里面放书,一本一本叠上去,取得时候只能从上面取最后放进去的书,最早放进去的最后才会被取出来。栈只允许在一端插入和删除数据,当某个数据集合只涉及在一端插入和删除数据,并且满足后进先出、先进后出的特性,就应该首选“栈”这种数据结构。

与数组或链表相比,栈的操作更为受限,那为什么会出现这种受限的数据结构呢? 从功能上讲,数组或者链表可以替代栈,但问题是数组或者链表的操作过于灵活,对外暴露的接口过多,当数据量很大的时候就会出现一些隐藏的风险。虽然栈限定降低了操作的灵活性,但这使得栈在处理只涉及一端新增和删除数据的问题时效率更高。

栈主要包含两个操作:入栈(在栈顶插入一个数据)和出栈(从栈顶删除一个数据),栈既可以用数组来实现,也可以用链表来实现。下面是在Go语言中分别使用数组和链表实现栈操作的核心代码(完整代码点 这里 查看):

// go-algo-demo/stack/StackArray.go
// 基于数组实现的栈
type StackArray struct {
	data []interface{} //栈里面的数据
	top  int           //栈顶指针
}

// 初始化一个栈
func NewArrayStack() *StackArray {
	return &StackArray{
		data: make([]interface{}, 0, 32),
		top:  -1,
	}
}

// 向栈中插入元素
func (this *StackArray) Push(v interface{}) {
	if this.top < 0 {
		this.top = 0
	} else {
		this.top += 1
	}

	if this.top > len(this.data)-1 {
		this.data = append(this.data, v)
	} else {
		this.data[this.top] = v
	}
}

// 从栈中弹出元素
func (this *StackArray) Pop() interface{} {
	if this.IsEmpty() {
		return nil
	}
	v := this.data[this.top]
	this.top -= 1
	return v
}

// 获取当前栈顶的元素
func (this *StackArray) Top() interface{} {
	if this.IsEmpty() {
		return nil
	}
	return this.data[this.top]
}

// 测试
func main() {
	s := NewArrayStack()
	// 向栈中插入元素
	s.Push(1)
	s.Push(2)
	s.Push(3)
	s.Print() //3 2 1

	// 获取当前栈顶的元素
	fmt.Println(s.Top()) //3

	// 从栈中弹出元素
	fmt.Println(s.Pop()) //3
	fmt.Println(s.Pop()) //2
	fmt.Println(s.Pop()) //1
	fmt.Println(s.Pop()) //<nil>
	s.Print()            //empty statck
}

栈存储数据只需要一个大小为 n 的数组就够了,所以空间复杂度是 O(1);入栈和出栈只涉及栈顶数据的操作,所以时间复杂度也是 O(1)。

如果要实现一个支持动态扩容的栈,可以使用一个可以动态扩容的数组来实现栈,当栈满了之后就申请一个更大的数组,将原来的数据搬移到新数组中。此时,对于出栈操作来说不会涉及内存的重新申请和数据的搬移,所以出栈的时间复杂度仍然是 O(1)。但是对于入栈操作来说,当栈中有空闲空间时,入栈操作的时间复杂度为 O(1);当空间不够时就需要重新申请内存和数据搬移,时间复杂度就变成了 O(n)。

栈在函数调用中的应用

操作系统给每个线程分配了一块独立的内存空间,这块内存就可以理解为“栈”这种结构, 用来存储函数调用时的临时变量。每进入一个函数,就会将临时变量入栈,当函数执行完成之后再将这个临时变量出栈。还记得吗?在 Go语言的函数和defer用法_浮尘笔记的博客-CSDN博客 里面就说过关于栈的逻辑,可以使用defer实现一个函数调用栈,跟踪函数的执行过程。

package main
 
// 使用 defer 跟踪函数的执行过程
func main() {
	defer Trace("main")()
	foo()
}
func Trace(name string) func() {
	println("enter:", name)
	return func() {
		println("exit:", name)
	}
}
 
func foo() {
	defer Trace("foo")()
	bar()
}
func bar() {
	defer Trace("bar")()
}

栈的应用:如何实现浏览器的前进和后退功能?

当打开浏览器依次访问页面 A -> B -> C 之后,然后依次点击浏览器的后退按钮,就可以查看之前浏览过的页面 B 和 A;当后退到页面 A的时候,再点击前进按钮,就可以重新进入页面 B 和 C。但是如果后退到页面 B 之后又点击了新的页面 D,那就无法再通过前进和后退功能查看页面 C 了。

要实现这个功能,可以使用栈这种数据结构,使用两个栈来实现。实现逻辑如下:

  • 比如顺序查看了 A,B,C 三个页面,就依次把 A,B,C压入栈X, 当通过浏览器的后退按钮从页面 C 后退到页面 A 之后,就依次把 C 和 B页面从栈 X 中弹出,并且依次放入到栈 Y中。
  • 如果再次进入页面 B,就把 B 再从栈 Y 中出栈并且放入栈 X 中。
  • 如果此时从页面B跳转到新的页面D,那么页面C就无法再通过前进和后退按钮重复查看了,所以需要清空栈 Y。

下面使用Go语言代码实现了这个功能:

// go-algo-demo/stack/Browser.go
package main

import (
	"fmt"
)

type Stack interface {
	Push(v interface{})
	Pop() interface{}
	IsEmpty() bool
	Top() interface{}
	GetTopValue() int
	Flush()
}

type Browser struct {
	forward Stack //前进的栈
	back    Stack //后退的栈
}

// 初始化
func NewBrowser() *Browser {
	return &Browser{
		forward: NewArrayStack(),
		back:    NewArrayStack(),
	}
}

// 打开一个新页面
func (this *Browser) OpenNewPage(addr string) {
	fmt.Printf("打开新页面: %v\n", addr)
	this.back.Push(addr)
	this.forward.Flush()    //清空前进的栈
	this.forward.Push(addr) //把当前最新打开的页面添加到前进的栈
}

// 从已有页面跳转到下一个页面
func (this *Browser) PushNewPage(addr string) {
	fmt.Printf("跳转到: %v\n", addr)
	this.back.Push(addr)
}

// 后退
func (this *Browser) Back() {
	if this.back.GetTopValue() == 0 {
		fmt.Println("已经后退到了第一个页面,无法再次后退")
		return
	}

	this.back.Pop()
	top := this.back.Top()
	this.forward.Push(top)
	fmt.Printf("后退到: %v\n", top)
}

// 前进
func (this *Browser) Forward() {
	if this.forward.GetTopValue() == 0 {
		fmt.Println("已经前进到了最后一个页面,无法再次前进")
		return
	}

	this.forward.Pop()
	top := this.forward.Top()
	this.back.Push(top)
	fmt.Printf("前进到: %v\n", top)
}

// go run Browser.go StackArray.go 
func main() {
	browser := NewBrowser()

	// 依次访问页面 A -> B -> C
	browser.OpenNewPage("www.A.com") //打开新页面: www.A.com
	browser.PushNewPage("www.B.com") //跳转到: www.B.com
	browser.PushNewPage("www.C.com") //跳转到: www.C.com

	// 后退两次, C -> B -> A
	browser.Back() //后退到: www.B.com
	browser.Back() //后退到: www.A.com
	browser.Back() //已经后退到了第一个页面,无法再次后退
	browser.Back() //已经后退到了第一个页面,无法再次后退

	// 前进一次, A -> B
	browser.Forward() //前进到: www.B.com

	//打开一个新页面, A -> B -> D
	browser.OpenNewPage("www.D.com") //打开新页面: www.D.com

	//后退两次, D -> B -> A
	browser.Back() //后退到: www.B.com
	browser.Back() //后退到: www.A.com
	browser.Back() //已经后退到了第一个页面,无法再次后退

	//前进两次, A -> B -> D
	browser.Forward() //前进到: www.B.com
	browser.Forward() //前进到: www.D.com
	browser.Forward() //已经前进到了最后一个页面,无法再次前进
	browser.Forward() //已经前进到了最后一个页面,无法再次前进
}

每日一练:左右括号匹配

力扣20. 有效的括号 

给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。

有效字符串需满足:左括号必须用相同类型的右括号闭合。左括号必须以正确的顺序闭合。每个右括号都有一个对应的相同类型的左括号。

示例:输入:s = "()[]{}",输出:true;输入:s = "(]",输出:false

思路 1:不使用数据结构和算法,直接用内置的 Replace() 方法将字符串中的 () , [] ,{} 全部替换成空字符串,替换之后如果字符串的长度为0就说明是一个“有效的括号” 字符串。时间复杂度: O(N^2),空间复杂度: O(N)

func isValidBrackets1(s string) bool {
	for {
		l := len(s)
		s = strings.Replace(s, "()", "", -1)
		s = strings.Replace(s, "[]", "", -1)
		s = strings.Replace(s, "{}", "", -1)
		//判断s是否没变过,相当于s不存在(),[],{}
		if len(s) == l {
			break
		}
	}
	return len(s) == 0
}

func main() {
	fmt.Println(isValidBrackets1("[]()")) //true
	fmt.Println(isValidBrackets1("[])"))  //false
}

思路2:用 栈 来实现,利用栈的后进先出的特性,如果是左括号就入栈,如果是右括号则查看当前栈顶元素是否与当前元素匹配,如果不匹配直接返回 false;如果全部匹配成功则返回 true。时间复杂度: O(N),空间复杂度: O(N)。注意,如果左右括号的个数是奇数则肯定返回false。

func isValidBrackets2(s string) bool {
    if len(s)%2 != 0 { //如果左右括号的个数是奇数则肯定返回false
		return false
	}
	stack := make([]rune, len(s))
	n := 0
	for _, c := range s {
		switch c {
		case '(', '[', '{':
			stack[n] = c
			n++
		case ')':
			if n == 0 || stack[n-1] != '(' {
				return false
			}
			n--
		case ']':
			if n == 0 || stack[n-1] != '[' {
				return false
			}
			n--
		case '}':
			if n == 0 || stack[n-1] != '{' {
				return false
			}
			n--
		}
	}
	if n == 0 {
		return true
	} else {
		return false
	}
}
func main() {
	fmt.Println(isValidBrackets2("[]()")) //true
	fmt.Println(isValidBrackets2("[])"))  //false
	fmt.Println(isValidBrackets2("[]))")) //false
}

源代码:https://gitee.com/rxbook/go-algo-demo/tree/master/stack

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/573785.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

面试题:什么是 TCP/IP?

目录标题 什么是 TCP/IP?1) 网络接口层:2) 网络层:3) 传输层:4) 应用层: 2.数据包3.网络接口层4.网络层1) IP:2)地址解析协议 ARP3)子网 5 传输层1&#xff09;UDP&#xff1a;2&#xff09;TCP&#xff1a; 6 应用层运行在TCP协议上的协议&#xff1a;运行在UDP协议上的协议&…

大模型即将改变世界,百度先上牌桌

“未来&#xff0c;所有的应用都将基于大模型来开发&#xff0c;每一个行业都应该有属于自己的大模型&#xff0c;大模型会深度融合到实体经济当中去。” 作者|思杭 斗斗 编辑|皮爷 出品|产业家 “大模型即将改变世界。”5月26日&#xff0c;李彦宏在中关村论坛说道。 而…

ESP32CAM开发板记录

忘记过去&#xff0c;超越自己 ❤️ 博客主页 单片机菜鸟哥&#xff0c;一个野生非专业硬件IOT爱好者 ❤️❤️ 本篇创建记录 2023-05-26 ❤️❤️ 本篇更新记录 2023-05-26 ❤️&#x1f389; 欢迎关注 &#x1f50e;点赞 &#x1f44d;收藏 ⭐️留言&#x1f4dd;&#x1f64…

【问题小记】解决Linux下php-fpm进程过多耗尽内存问题

最近一段时间&#xff0c;发现经常性的服务器内存耗尽&#xff0c;导致mysql服务down掉&#xff0c;一开始以为是mysql跑的太久占用较多内存&#xff0c;后来认真排查了一下原来是是PHP-FPM进程过多导致的。 今天一看内存又达到了82%&#xff0c;预计不会太久服务又会挂掉&…

深入探索: 对象构造的隐藏功能与技巧

&#x1f9d1;‍&#x1f4bb;CSDN主页&#xff1a;夏志121的主页 &#x1f4cb;专栏地址&#xff1a;Java基础进阶核心技术专栏 目录 &#x1f383; 一、重载 &#x1f384; 二、默认字段初始化 &#x1f386; 三、无参数的构造器 ✨ 四、显式字段初始化 &#x1f38a; 五…

Vue(路由插件)

一、介绍路由 1. 路由就是一组key-value的对关系&#xff0c;多个路由需要经过路由器进行管理 2. 主要应用在SPA&#xff08;单页面应用&#xff09; 在一个页面展示功能之间的跳转 特点&#xff1a; 当跳转时候不进行页面刷新路径随着变化展示区变化但是不开启新的页签 …

总结丨SGAT单基因关联分析工具,一文上手使用

SGAT是一个免费开源的单基因分析工具&#xff0c;基于Linux系统实现自动化批量处理&#xff0c;能够快速准确的完成单基因和表型的关联分析&#xff0c;只需要输入基因型和表型原始数据&#xff0c;即可计算出显著关联的SNP位点&#xff0c;并自动生成结果报告。 前段时间陆续的…

YOLOv5白皮书-第Y4周:common.py文件解读

目录 0.导入需要的包和基本配置1.基本组件1.1 autopad1.2 Conv1.3 Focus1.4 Bottleneck1.5 BottleneckCSP1.6 C31.7 SPP1.8 Concat1.9 Contract、Expand 2.重要类2.1 非极大值抑制&#xff08;NMS&#xff09;2.2 AutoShape2.3 Detections2.4 Classify &#x1f368; 本文为&am…

【头歌实训】【基于 Logisim 的 RISC-V 处理器设计 · 终】

真的恶心&#xff0c;我哭死 目录 前言 一、说明 1、参考 2、建议 二、处理器设计 三、Control器件设计 1、加速经常性事件&#xff0c;提高效率 2、控制信号设置 1.RegWEn 2.IMMSel 3.BSel 4.ALUSel & WBSel 5.MemWEn 6.PCSel & ASel 7.ALUB 总结…

【C语言】标准库(头文件、静态库、动态库),windows与Linux平台下的常用C语言标准库

一、Introduction1.1 C语言标准库1.2 历代C语言标准1.3 主流C语言编译器 二、C语言标准库2.1 常用标准头文件2.2 常用标准静态库 三、windows平台四、Linux平台五、常用头文件功能速览5.1 通用常用头文件01. stdio.h——标准输入输出02. stdlib.h——内存管理与分配、随机数、字…

Git常用命令reset和revert

Git常用命令reset和revert 1、reset 用于回退版本&#xff0c;可以指定退回某一次提交的版本。 checkout 可以撤销工作区的文件&#xff0c;reset 可以撤销工作区/暂存区的文件。 reset 和 checkout 可以作用于 commit 或者文件&#xff0c;revert 只能作用于 commit。 命…

为什么 String#equals 方法在做比较时没有使用 hashCode

一个疑问的引入 我之前出于优化常数项时间的考虑&#xff0c;想当然的认为 String#equals 会事先使用 hashCode 进行过滤 我想像中的算法是这样的 当两个 hashCode 不等时&#xff0c;直接返回 false&#xff08;对 hash 而言&#xff0c;相同的输入会得到相同的输出&#x…

数据安全复合治理框架和模型解读(0)

数据治理,数据安全治理行业在发展,在实践,所以很多东西是实践出来的,哪有什么神仙理论指导,即使有也是一家之说,但为了提高企业投产比,必要的认知是必须的,当前和未来更需要专业和创新。数据安全治理要充分考虑现实数据场景,强化业务安全与数据安全治理,统一来治理,…

学会了程序替换,我决定手写一个简易版shell玩一玩...

文章目录 &#x1f490;专栏导读&#x1f490;文章导读&#x1f427;程序进程替换&#x1f426;替换原理&#x1f426;替换函数&#x1f414;观察与结论&#x1f414;函数命名理解 &#x1f427;myshell编写&#x1f514;代码展示&#x1f514;效果展示 &#x1f427;myshell_p…

Vue电商项目--分页器制作

分页器静态组件 分页这个组件&#xff0c;不单单是一个页面用到了。多个页面同时用它,因此我们可以封装成一个全局组件 需要将这个分页结构拆分到components 通用的分页组件Pagination <template><div class"pagination"><button>1</butto…

【C语言】函数规则及入门知识

&#x1f6a9;纸上得来终觉浅&#xff0c; 绝知此事要躬行。 &#x1f31f;主页&#xff1a;June-Frost &#x1f680;专栏&#xff1a;C语言 ⚡注&#xff1a;此篇文章的 部分内容 将根据《高质量 C/C 编程指南》 —— 林锐 进行说明。该部分将用橙色表示。 &#x1f525;该篇…

新手建站:使用腾讯云轻量服务器宝塔面板搭建WP博客教程

腾讯云轻量应用服务器怎么搭建网站&#xff1f;太简单了&#xff0c;轻量服务器选择宝塔Linux镜像&#xff0c;然后在宝塔面板上添加站点&#xff0c;以WordPress建站为例&#xff0c;腾讯云服务器网来详细说下腾讯云轻量应用服务器搭建网站全流程&#xff0c;包括轻量服务器配…

html5视频播放器代码实例(含倍速、清晰度切换、续播)

本文将对视频播放相关的功能进行说明&#xff08;基于云平台&#xff09;&#xff0c;包括初始化播放器、播放器尺寸设置、视频切换、倍速切换、视频预览、自定义视频播放的开始/结束时间、禁止拖拽进度、播放器皮肤、控件按钮以及播放控制等。 图 / html5视频播放器调用效果&a…

java web 基础springboot

1.SprintBootj集成mybaits 连接数据库 pom.xml文件添加依赖 <!-- mysql驱动--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.30</version></dependency><!-- …

学习HCIP的day.09

目录 一、BGP&#xff1a;边界网关路由协议 二、BGP特点&#xff1a; 三、BGP数据包 四、BGP的工作过程 五、名词注解 六、BGP的路由黑洞 七、BGP的防环机制—水平分割 八、BGP的基本配置 一、BGP&#xff1a;边界网关路由协议 是一种动态路由协议&#xff0c;且是…