java栈和队列的实际应用_极客算法训练笔记(四),栈和队列,从实际应用看数据结构...

没有最好的数据结构,只有最合适的数据结构。❞

栈和队列都是操作受限的数据结构,那么为什么不直接用数组和链表呢?事实上,从功能上来说,数组或链表确实可以替代栈,因为栈其实就是通过数组和链表来实现的,但是,特定的数据结构是对特定场景的抽象,而且,数组或链表暴露了太多的操作接口,操作上的确灵活自由,但使用时就比较不可控,自然也就更容易出错,所谓能力越大责任越大就是这个道理。

顺序栈和链栈

栈只允许在一端进行插入和删除数据,满足先进后出,后进先出的特点,有数组实现的顺序栈和链表实现的链栈两种。

44897b6d1f3c485198db9319d2313b47.png顺序栈

a4ce6f8ee8962df2492d80f153e48dfe.png链栈

栈应用:

1. 栈应用之函数调用栈

操作系统给每个线程分配了一块独立的内存空间,这块内存被组织成“栈”这种结构,用来存储函数调用时的临时变量。每进入一个函数,就会将临时变量作为一个栈帧入栈,当被调用函数执行完成,返回之后,将这个函数对应的栈帧出栈。

这个应用是最广泛的,因为实际开发过程中,我们到处都在写函数,函数的调用过程其实就是不断的入栈出栈的过程。

如下的例子,两个函数对应两个栈帧,main函数先入栈,然后调用了add函数将其入栈。

两个栈帧

2. 栈应用之表达式求值

给一个运算 34+13*9+44-12/3,用脚都能算出来这个结果是多少,但是要让编译器给我们算还得想办法,我们写一套规则让编译器实现,因为运算符是有优先级的。

实际上,编译器就是通过两个栈来实现的,编译器实现步骤:一个保存操作数的栈,另一个是保存运算符的栈;

从左向右遍历表达式,当遇到数字,我们就直接压入操作数栈;当遇到运算符,就与运算符栈的栈顶元素进行比较。

如果比运算符栈顶元素的优先级高,就将当前运算符压入栈;

如果比运算符栈顶元素的优先级低或者相同,从运算符栈中取栈顶运算符,从操作数栈的栈顶取2个操作数,然后进行计算,再把计算完的结果压入操作数栈,继续比较。

7ae31c06fdc13f728ab94562767fb0b6.png表达式实现过程分解

这个应用也是比较广泛的吧,算数喽~

3. 栈应用之括号匹配

具体的场景,我拿力扣的括号题来举例,这道题就是对栈典型的应用,实际开发中括号也是用的很多的场景。

9c526566045e9249165d5efe88d34ddb.pngleetcode栈典型应用

4. 栈应用之浏览器前进后退功能

这个功能,想必大家经常用吧,现在就来看看怎么用栈实现吧。我们使用两个栈,X和Y,我们把首次浏览的页面依次压入栈X;

当点击后退按钮时,再依次从栈X中出栈,并将出栈的数据依次放入栈Y。

当我们点击前进按钮时,我们依次从栈Y中取出数据,放入栈X中。

当栈X中没有数据时,那就说明没有页面可以继续后退浏览了。当栈Y中没有数据,那就说明没有页面可以点击前进按钮浏览了。

e0c8ee0426da48be9fd7f8eba7e26c32.png浏览器前进后退跳新页面示意图

队列

队列也是一种操作受限的结构,仅允许在表的一端进行插,而在表的另一端进行删除,插入的一端称做队尾(rear),进行删除的一端称做队首(front),满足先进先出原则。同样分为顺序队列和链式队列两种

顺序队列和链式队列

顺序队列入队出队:

a1出队,front指向a1

链式队列入队出队:

链队列链式队列入队出队

队列应用之生产者消费者模型

阻塞队列

阻塞队列其实就是在队列基础上增加了阻塞操作。简单来说,就是在队列为空的时候,从队头取数据会被阻塞。因为此时还没有数据可取,直到队列中有了数据才能返回;

如果队列已经满了,那么插入数据的操作就会被阻塞,直到队列中有空闲位置后再插入数据,然后再返回。

这种场景,就是典型的生产者消费者模型。

生产者消费者模型

基于阻塞队列,我们还可以通过协调“生产者”和“消费者”的个数,来提高数据的处理效率。比如前面的例子,我们可以多配置几个“消费者”,来应 对一个“生产者”。

039ae6995b587fb715e8565ac401310c.png多个消费者应对一个生产者

用过中间件rabbitmq的朋友,想必对这些东西很熟悉。

队列本身其实就是个排队的过程,实际中很多场景都会进行排队,前几天吃了顿牛蛙也是取票排队来着,因此这种场景用队列这种数据结构就特别的合适。

算法 链表反转

上篇链表的算法题一:

a7cba91c68d0764aff4d850caff3292f.png反转链表

首先最开始想到的还是暴力递归,使用递归函数,一直递归到链表的最后一个结点,该结点就是反转后的头结点,记作 ret

此后,每次函数在返回的过程中,让当前结点的下一个结点的 nextnext 指针指向当前节点。

同时让当前结点的 next 指针指向 NULL,从而实现从链表尾部开始的局部反转

当递归函数全部出栈后,链表反转完成。class Solution {

public:

ListNode* reverseList(ListNode* head) {

if (head == NULL || head->next == NULL) {

return head;

}

ListNode* ret = reverseList(head->next);

head->next->next = head;

head->next = NULL;

return ret;

}

};

算法 链表环检测

上篇链表的算法题二:

1ad2be6b5cc847b04e51e6dca089788b.png链表环检测哈希表实现

利用 hashSet 中,每个元素都不重复的特点,我们遍历所有结点并在哈希表中存储每个结点的引用(或内存地址)。如果当前结点为空结点 null(即已检测到链表尾部的下一个结点),那么我们已经遍历完整个链表,并且该链表不是环形链表。如果当前结点的引用已经存在于哈希表中,那么返回 true(即该链表为环形链表)。public boolean hasCycle(ListNode head) {

Set nodesSeen = new HashSet<>();

while (head != null) {

if (nodesSeen.contains(head)) {

return true;

} else {

nodesSeen.add(head);

}

head = head.next;

}

return false;

}快慢指针

通过使用具有不同速度的快、慢两个指针遍历链表,空间复杂度可以被降低至 O(1)O(1)。慢指针每次移动一步,而快指针每次移动两步。

如果列表中不存在环,最终快指针将会最先到达尾部,此时我们可以返回 false。public boolean hasCycle(ListNode head) {

if (head == null || head.next == null) {

return false;

}

ListNode slow = head;

ListNode fast = head.next;

while (slow != fast) {

if (fast == null || fast.next == null) {

return false;

}

slow = slow.next;

fast = fast.next.next;

}

return true;

}

算法 接雨水

超哥给的这道题看着就‘好看’啊,给hard题一点面子,下一篇就只写这一题吧,万一写不出来还很尴尬~

a6b34ab7f20f204e0be69953e27b2a5c.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值