算法day9
- 栈与队列基础
- 232用栈实现队列
- 225用队列实现栈
栈与队列理论基础
言简意赅:栈的原理就是后进先出。队列就是先进先出。
相关操作:
栈:入栈,出栈,判栈空,取栈顶元素。
队列:出队,入队,判队空等。
这些操作都可以用数组来模拟。
golang和c++不一样,这些类型都需要自己去自定义数据类型模拟来实现。
c++我当时很多时候都习惯用现成的。
下面是一些简单的模拟:
golang实现栈和栈的操作
package mainimport "fmt"type Stack []int//入栈操作
func (s *Stack) Push(value int) {*s = append(*s, value)
}//出栈操作,返回值是删除的这个元素的值和是否删除成功。
func (s *Stack) Pop() (int, bool) {if len(*s) == 0 {return 0, false}index := len(*s) - 1 // 先计算栈顶元素的下标索引。element := (*s)[index] //然后取出这个元素*s = (*s)[:index] // 然后就不要后面这个元素了。return element, true //返回值
}// 判栈空就是判数组长度是否为0。
func (s *Stack) IsEmpty() bool {return len(*s) == 0
}func main() {var stack Stack //初始化stack.Push(1) //调用push函数stack.Push(2)stack.Push(3)fmt.Println(stack) // Output: [1 2 3]if value, ok := stack.Pop(); ok {//出栈fmt.Println("Popped:", value) // Output: Popped: 3}fmt.Println(stack) // Output: [1 2] 输出
}
总结就是控制数组的一端变化就完事了。
golang实现队列和队列的相关操作
还是数组模拟
package mainimport "fmt"type Queue []int// 入队操作,直接操作切片append目标元素就完事了,因为在队尾插入。
func (q *Queue) Enqueue(value int) {*q = append(*q, value)
}// 删除对头元素
func (q *Queue) Dequeue() (int, bool) {if len(*q) == 0 {return 0, false}element := (*q)[0] // Get the first element. //直接访问队头元素就是0号*q = (*q)[1:] // 进行切片操作,最前面的元素不要了return element, true
}// 判空
func (q *Queue) IsEmpty() bool { return len(*q) == 0
}func main() {var q Queueq.Enqueue(1)q.Enqueue(2)q.Enqueue(3)fmt.Println(q) // Output: [1 2 3]if value, ok := q.Dequeue(); ok {fmt.Println("Dequeued:", value) // Output: Dequeued: 1}fmt.Println(q) // Output: [2 3]
}
总结:
个人感觉golang模拟比c++用数组模拟好多了。因为切片操作真的很方便。
用栈实现队列
就是使用栈实现队列的下列操作:
push(x):将一个元素放入队列的尾部
pop():从队列首部移除元素
peek():返回队列首部的元素
empty():返回队列是否为空
题目也说了,栈里面的pop,push,size,isempty是合法操作。
核心:
首先这个过程要想清楚:就是用栈是怎么去实现队列的操作的。
实现的方法是用双栈,我想出栈和队列得到统一,那我就要用到下面这个outstack来模拟。进栈操作就是用instack来模拟。
但是有一些细节需要去注意:这个过程结合模拟来比较好,在push数据的时候,只要数据放进输入栈就好,但在pop的时候,操作就复杂一些,输出栈如果为空,就把进栈数据全部导入进来(注意是全部导入),再从出栈弹出数据,如果输出栈不为空,则直接从出栈弹出数据就可以了。
最后如何判断队列为空?如果进栈和出栈都为空的话,就说明模拟的队列为空了。
技巧,实现一些功能的时候其实不用再写一次逻辑,可以直接调函数。
ps:里面的错误处理是返回-1
type MyQueue struct {stackIn []intstackout []int
}func Constructor() MyQueue {return MyQueue{stackIn:make([]int,0),stackout:make([]int,0),}
}func (this *MyQueue) Push(x int) {this.stackIn = append(this.stackIn, x)
}func (this *MyQueue) Pop() int {inlen,outlen:=len(this.stackIn),len(this.stackout)if outlen == 0{if inlen==0{return -1}for i:=inlen-1;i>=0;i--{this.stackout=append(this.stackout, this.stackIn[i])}this.stackIn=[]int{}outlen = len(this.stackout)}val:=this.stackout[outlen-1]this.stackout=this.stackout[:outlen-1]return val
}func (this *MyQueue) Peek() int {val:= this.Pop()if val == -1{return -1}this.stackout=append(this.stackout, val)return val
}func (this *MyQueue) Empty() bool {return len(this.stackIn)==0 && len(this.stackout)==0
}/*** Your MyQueue object will be instantiated and called as such:* obj := Constructor();* obj.Push(x);* param_2 := obj.Pop();* param_3 := obj.Peek();* param_4 := obj.Empty();*/
每个函数实现的思路解读
我还是把这个图放在这里:
结构体:
type MyQueue struct {stackIn []int //输入栈stackOut []int //输出栈
}
为什么这么写,这里就是代表两个栈代表了这个队列。
构造函数,就是把这个队列的组成,两个模拟栈初始化。
func Constructor() MyQueue {
return MyQueue{
stackIn:make([]int,0),
stackout:make([]int,0),
}
}
队列push操作,就是直接往入栈里面加元素,就完成了入队操作
func (this *MyQueue) Push(x int) {
this.stackIn = append(this.stackIn, x)
}
出队操作
func (this *MyQueue) Pop() int {
inLen, outLen := len(this.stackIn), len(this.stackOut)
if outLen == 0 {
if inLen == 0 {
return -1
}
for i := inLen - 1; i >= 0; i-- {
this.stackOut = append(this.stackOut, this.stackIn[i])
}
this.stackIn = []int{} //导出后清空
outLen = len(this.stackOut) //更新长度值
}
val := this.stackOut[outLen-1]
this.stackOut = this.stackOut[:outLen-1]
return val
}
这个稍微难了一点点,这里的关键是要把逻辑想清楚。这个要靠自己模拟得出来为什么这么写。
出队:出栈不为空,就直接进行出栈,如果出栈为空,那就把入栈元素全部进入出栈,然后再进行出栈。(这个过程要自己去模拟看看为什么),你可以去看看你不这么做会发生什么结果(顺序会混乱)。
然后正确的逻辑我建议按照上面这个图想了写。
思考:
因为你模拟队列,入第一个栈的时候你这个时候进行弹栈输出你得到的序列肯定是反的,所以再来一个栈,去接弹栈的结果,此时第二个栈出栈的效果就很你出队的效果是一样的,顺序都是正的,所以对这第二个栈出栈就达到了队列出队的目的。
获取对头的元素值
func (this *MyQueue) Peek() int {
val:= this.Pop()
if val == -1{
return -1
}
this.stackout=append(this.stackout, val)
return val
}
这里是前面实现的方法的复用,我可以先使用队列的出队操作,将对头出队获得这个元素,然后我再对这个出栈再把这个出队的元素进行入栈即可。
判空操作就是,两个栈都空,那就代表没有元素,队列为空。
func (this *MyQueue) Empty() bool {
return len(this.stackIn)==0 && len(this.stackout)==0
}
225用队列实现栈
使用队列来完成栈的操作
由于go语言没有像c++一样的stack和queue,所以题目中队列的那些合法操作可以说鸟用没有。都需要自己来模拟,所以自己有那个思想,自己来模拟每一个需要的操作就行了。而且go语言的切片操作非常的强大,完成这些操作我觉得都是一步到位的。
本题主要用队列实现栈的一下操作:
push(x) – 元素 x 入栈
pop() – 移除栈顶元素
top() – 获取栈顶元素
empty() – 返回栈是否为空
解法:用一个队列就可以,我个人认为两个队列反而还难理解了。
两个队列的版本:
核心思想就是往栈的性质去靠:
一个队列为主队列,一个为辅助队列,当入栈操作时,我们先将主队列内容导入辅助队列,然后将入栈元素放入主队列队头位置,再将辅助队列内容,依次添加进主队列即可。
这里我不写这种,这种我个人认为是十分的麻烦,理解起来也不好理解。
一个队列的版本:
使用一个队列时,为了满足栈的特性,即最后入栈的元素最先出栈,同样需要满足队列前端的元素是最后入栈的元素。
入栈操作时,首先获得入栈前的元素的个数,然后将元素入队到队列,再将队列中的前n个元素(即除了心如站的元素之外的全部元素)依次出队并入队到队列,此时队列的前端的元素即为新入栈的元素,且队列的前端和后端分别对应栈顶和栈底。
由于每次入栈操作都确保队列的前端元素为栈顶元素,因此出栈操作和获得栈顶元素操作都可以简单实现。出栈操作只需要移除队列的前端元素并返回即可。获得栈顶元素操作只需要获得队列的前端元素并返回即可(不移除元素)。
我总结一下:
就是入栈的时候我只管先加进来,然后前面的元素依次出队然后往后面依次入队,这样保证了我加进来的这个元素一定是对头元素,这就满足了后进先出,此时我队列出队时出的这个元素就达到了这种效果。
举个例子
栈 1-2-3-4-5 ,123先入栈
队列入1, 1
入2, 2-1
入3-2-1,
出3,3直接删除 现在队列2-1
4入队 4-2-1
5入队5-4-2-1,显然可以发现,这简直和栈完全是一个状态,我删队头就是弹栈顶。
这样这个题大致的思路已经理清楚了
type MyStack struct {queue []int
}func Constructor() MyStack {return MyStack{queue:make([]int,0),}
}func (this *MyStack) Push(x int) {n:=len(this.queue)this.queue=append(this.queue,x)for ;n>0;n--{this.queue=append(this.queue,this.queue[0])this.queue=this.queue[1:]}
}func (this *MyStack) Pop() int {val := this.queue[0]this.queue=this.queue[1:]return val
}func (this *MyStack) Top() int {return this.queue[0]
}func (this *MyStack) Empty() bool {return len(this.queue)==0
}/*** Your MyStack object will be instantiated and called as such:* obj := Constructor();* obj.Push(x);* param_2 := obj.Pop();* param_3 := obj.Top();* param_4 := obj.Empty();*/
我再来解释一下代码逻辑
type MyStack struct {queue []int
}
现在我的队列就是我要拿来模拟栈,所以此时我的栈就是队列。
func Constructor() MyStack {return MyStack{queue:make([]int,0),}
}
这就是个初始化函数。
入栈操作:
func (this *MyStack) Push(x int) {n:=len(this.queue)this.queue=append(this.queue,x)for ;n>0;n--{this.queue=append(this.queue,this.queue[0])this.queue=this.queue[1:]}
}
为什么这样的操作能行我在上面已经做了相关的模拟:
1.在入栈之前我先进行队列元素统计,因为我统计的这些元素都要进行依次的后移操作。
2.然后把这个元素加进来。
3.开始把这个元素前面的元素依次后移,这个就是把队头元素依次的append到队尾,这就达到了队头元素后移的效果,然后再用个切片操作把队头不要了,这就达到了队头出队的效果。
出队操作
func (this *MyStack) Pop() int {val := this.queue[0]this.queue=this.queue[1:]return val
}
前面我已经证明过了,出队操作就是直接出,因为我push函数操作的原因,此时队头就是栈顶,出队和出栈一个效果。
获取栈顶元素
func (this *MyStack) Top() int {
return this.queue[0]
}
那更是简单,就是访问队头.
判空,现在队列和这个栈可以说就是一回事,因为底层就一个切片结构。所以直接用长度判
func (this *MyStack) Empty() bool {return len(this.queue)==0
}