以下完整代码均可从这里获取
栈
栈的基本概念
后进先出、先进后出就是典型的栈结构。栈可以理解成一种受了限制的线性表,插入和删除都只能从一端进行
当某个数据集合只涉及在一端插入和删除数据,并且满足后进先出、先进后出的特性,就应该首选“栈”这种数据结构(浏览器的前进、后退功能)
栈的实现
栈主要有两种操作,入栈和出栈,这里通过数组(顺序栈)和链表(链式栈)两种方式实现栈
顺序栈
package arrayStack
import "fmt"
type Item interface {}
type ItemStack struct {
Items []Item
N int
}
//init stack
func (stack *ItemStack) Init() *ItemStack {
stack.Items = []Item{}
return stack
}
//push stack Item
func (stack *ItemStack) Push(item Item) {
if len(stack.Items) > stack.N {
fmt.Println("栈已满")
return
}
stack.Items = append(stack.Items, item)
}
//pop Item from stack
func (stack *ItemStack) Pop() Item {
if len(stack.Items) == 0 {
fmt.Println("栈已空")
return nil
}
item := stack.Items[len(stack.Items) - 1]
stack.Items = stack.Items[0:len(stack.Items) - 1]
return item
}
链式栈
package linkListStack
import "fmt"
type Item interface {}
type Node struct {
Data Item
Next *Node
}
type Stack struct {
headNode *Node
}
//push Stack item
func (stack *Stack) Push(item Item) {
newNode := &Node{Data: item}
newNode.Next = stack.headNode
stack.headNode = newNode
}
//pop Item from stack
func (stack *Stack) Pop() Item {
if stack.headNode == nil {
fmt.Println("栈已空")
return nil
}
item := stack.headNode.Data
stack.headNode = stack.headNode.Next
return item
}
func (stack *Stack) Traverse() {
if stack.headNode == nil {
fmt.Println("栈已空")
return
}
currentNode := stack.headNode
for currentNode != nil {
fmt.Printf("%v\t", currentNode.Data)
currentNode = currentNode.Next
}
}
栈的应用场景
函数调用栈
操作系统给每个线程分配了一块独立的内存空间,这块内存被组织成“栈”这种结构, 用来存储函数调用时的临时变量。每进入一个函数,就会将临时变量作为一个栈帧入栈,当被调用函数执行完成,返回之后,将这个函数对应的栈帧出栈来源:数据结构与算法之美
从这段代码的执行过程中了解函数调用栈
int main() {
int a = 1;
int ret = 0;
int res = 0;
ret = add(3, 5);
res = a + ret;
printf("%d", res);
reuturn 0;
}
int add(int x, int y) {
int sum = 0;
sum = x + y;
return sum;
}
main()函数调用了 add() 函数,获取计算结果,并且与临时变量 a 相加,最后打印 res 的值。程序在执行过程中,main函数中的变量会先后入栈,当执行到add()函数时,add()函数中的临时变量也会先后入栈,结果如下:
说明:内存中的堆栈和数据结构堆栈不是一个概念,内存中的堆栈是真实存在的物理区,数据结构中的堆栈是抽象的数据存储结构
栈在表达式求值中的应用
一个表达式包含两个部分,数字和运算符。我们用两个栈来实现表达式求值,一个栈用来存储数字,一个栈用来存储运算符
假设有这么一个表达式
1000+5*6-6
从左向右遍历表达式,当遇到数字时,将数字放入到存储数字的栈;如果遇到运算符,将存储运算符栈的栈顶元素取出,进行优先级比较
如果比运算符栈顶元素优先级高,则将当前运算符压入到存储运算符的栈中;如果比运算符栈顶元素低或优先级一样,则从存储数字的栈中取出两个元素,然后进行计算,将计算的结果放入到存储数字的栈中。重复上边的操作。过程如图:
栈在括号匹配中的应用
这也是一个比较经典的题,就是给定一个括号串,验证它是否完全匹配,如:
{{} 不匹配
[[{()}]] 匹配
([{}] 不匹配
这个也可以用栈来解决。从左到右遍历括号串,遇到未匹配的左括号则将其压入栈中,遇到右括号时,从栈顶取出一个左括号,如果能匹配,则继续遍历后边的括号,当遍历完之后,栈为空了,说明这个括号串是匹配的,否则是不匹配的。具体实现如下:
package bracketMatch
func BracketsMatch(str string) bool {
brackets := map[rune]rune{')':'(', ']':'[', '}':'['}
var stack []rune
for _, char := range str {
if char == '(' || char == '[' || char == '{' {
stack = append(stack, char)
} else if len(stack) > 0 && brackets[char] == stack[len(stack) - 1] {
stack = stack[:len(stack) - 1]
} else {
return false
}
}
return len(stack) == 0
}
队列
队列的基本概念
先进先出就是典型的队列结构,队列也可以理解成一种受了限制的线性表,插入只能从队列尾部进行,删除只能从队列尾部进行。类比排队取票
队列的基本操作也只有两个,入队和出队。队列的应用确实是十分的广泛,如消息队列、阻塞队列、循环队列等
队列的实现
还是通过两种方式实现队列,通过数组实现顺序队列,通过链表实现链式队列
实现队列需要两个指针,一个指向队列头部,一个指向队列尾部
顺序队列
package arrayQueue
import "fmt"
type Item interface {}
type Queue struct {
Queue []Item
Length int
}
func (queue *Queue) Init() {
queue.Queue = []Item{}
}
func (queue *Queue) Enqueue(data Item) {
if len(queue.Queue) > queue.Length {
fmt.Println("队列满了")
return
}
queue.Queue = append(queue.Queue, data)
}
func (queue *Queue) Dequeue() Item {
if len(queue.Queue) == 0 {
fmt.Println("队列空了")
return nil
}
item := queue.Queue[0]
queue.Queue = queue.Queue[1:]
return item
}
链式队列
package linkListQueue
import "fmt"
type Item interface {}
type Node struct {
Data Item
Next *Node
}
type Queue struct {
headNode *Node
}
func (queue *Queue) Enqueue(data Item) {
node := &Node{Data: data}
if queue.headNode == nil {
queue.headNode = node
} else {
currentNode := queue.headNode
for currentNode.Next != nil {
currentNode = currentNode.Next
}
currentNode.Next = node
}
}
func (queue *Queue) Dequeue() Item {
if queue.headNode == nil {
fmt.Println("队列空了")
return nil
}
item := queue.headNode.Data
queue.headNode = queue.headNode.Next
return item
}
func (queue *Queue) Traverse() {
if queue.headNode == nil {
fmt.Println("队列空的")
return
}
currentNode := queue.headNode
for currentNode.Next != nil {
fmt.Printf("%v\t", currentNode.Data)
currentNode = currentNode.Next
}
fmt.Printf("%v\t", currentNode.Data)
}
循环队列
为什么会出现循环队列?
看下边这种情况,我有有一个长度是5的队列,目前队列是满的。假设现在我从队头取出3个元素之后,想再往队列中放入数据,其实是放不进去的,此时就出现一个问题,队列有空闲空间,但是却无法向队列中放入数据了
其中一个解决办法就是,数据搬移。但是这样的话,每次在出队的时候就等于说删除数组下标为0的元素,而且要将后边所有的数据向前搬移,这样就导致出队的时间复杂度由原来的O(1)变成O(n),这种方法显然是不可取的
第二个办法就是使用一个循环队列,很明显就是一个环,这简直和单向循环链表一模一样。具体什么样,大家应该都十分的清楚,它的难点就在于判空和判满
队列为空时:tail == head
队列为满时:(tail+1)%n == head
哎,找规律问题,不行硬记住就可以了,直接看下边如何实现
循环队列的实现
package loopQueue
import "fmt"
type Item interface {}
const QueueSize = 5
type LoopQueue struct {
Items [QueueSize]Item
Head int
Tail int
}
//init
func (queue *LoopQueue) Init() {
queue.Head = 0
queue.Tail = 0
}
//enqueue
func (queue *LoopQueue) Enqueue(data Item) {
if ((queue.Tail + 1) % QueueSize) == queue.Head {
fmt.Println("队列满了")
}
queue.Items[queue.Tail] = data
queue.Tail = (queue.Tail+1) % QueueSize
}
//dequeue
func (queue *LoopQueue) Dequeue() Item {
if queue.Head == queue.Tail {
fmt.Println("队列空了")
return nil
}
item := queue.Items[queue.Head]
queue.Head = (queue.Head + 1) % QueueSize
return item
}
队列的应用场景
阻塞队列和并发队列
阻塞队列
在实际应用中,队列不会是无限长的,队列一旦有长度限制,就会有满的时候,当队列满了的时候,入队操作就会被阻塞,因为没有数据可取。而当队列为空时,出队是阻塞的,直到队列中有数据可取
没错,平时经常用到的生产者-消费者模型就是这样,通过队列可以轻松实现一个生产者-消费者模型
基于阻塞队列,还可以通过调整“生产者”和“消费者”的个数,来提高数据的处理效率
并发队列
对于上边的阻塞队列,在多线程的情况下,就会存在多个线程同时操作队列,这个时候就会存在线程安全问题(如果想了解底层原因,可以看我的这篇文章:进程管理之进程同步)
保证线程安全的队列就称之为并发队列,最简单的方式就是在入队和出队的时候加锁。关于锁,也可以看我这里的系列文章:线程锁系列
参考资料:
- 数据结构与算法之美
- 从零学习数据结构