栈与队列的基本概念、应用、LetCode真题

目录

栈的基本概念

栈是什么

顺序栈

链栈

栈的案例

符号匹配

浏览器前进后退功能

总结

队列

队列的基本概念

队列是什么

基本操作

顺序队列的数据操作

循环队列

链式队列的数据操作

应用

约瑟夫环

总结

LetCode真题

20. 有效的括号

739. 每日温度

239. 滑动窗口最大值


栈的基本概念

栈是什么

线性表增加和删除操作限制在一端进行,就被称为栈。

那为什么要使用栈?其实,单纯从功能上讲,数组或者链表可以替代栈。然而问题是,数组或者链表的操作过于灵活,这意味着,它们过多暴露了可操作的接口。这些没有意义的接口过多,当数据量很大的时候就会出现一些隐藏的风险。一旦发生代码 bug 或者受到攻击,就会给系统带来不可预知的风险。虽然栈限定降低了操作的灵活性,但这也使得栈在处理只涉及一端新增和删除数据的问题时效率更高。

具体而言,栈的数据结点必须后进先出。后进的意思是,栈的数据新增操作只能在末端进行,不允许在栈的中间某个结点后新增数据。先出的意思是,栈的数据删除操作也只能在末端进行,不允许在栈的中间某个结点后删除数据。

增加和删除操作的一端称为栈顶(top),另一端称为栈底(bottom)。增加数据称为压栈(push),删除数据称为出栈(pop)。栈中没有数据称为空栈。

和线性表类似,栈分为顺序栈和链栈。

顺序栈

空栈:栈顶=栈底 或 top = -1 ;

增加数据:top + 1 ; 

删除数据:top - 1 ;

top < StackSize,有一条数据时 top = 0;

链栈

栈顶放在单链表头部,即不需要头指针,使用top指针代替。增加与删除操作在头部即top指向处进行。

增加数据

在链式栈中进行删除操作时,只能在栈顶进行操作。因此,将栈顶的 top 指针指向栈顶元素的 next 指针即可完成删除。

栈的案例

符号匹配

范例:给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。有效字符串需满足:左括号必须与相同类型的右括号匹配,左括号必须以正确的顺序匹配。例如,{ [ ( ) ( ) ] } 是合法的,而 { ( [ ) ] } 是非法的。

浏览器前进后退功能

范例,浏览器的页面访问都包含了后退和前进功能,利用栈如何实现?

总结

栈继承了线性表的优点与不足,是个限制版的线性表。限制的功能是,只允许数据从栈顶进出,这也就是栈后进先出的性质。不管是顺序栈还是链式栈,它们对于数据的新增操作和删除操作的时间复杂度都是 O(1)。而在查找操作中,栈和线性表一样只能通过全局遍历的方式进行,也就是需要 O(n) 的时间复杂度。

什么时候选择栈?栈具有后进先出的特性,当你面对的问题需要高频使用新增、删除操作,且新增和删除操作的数据执行顺序具备后来居上的相反关系时,栈就是个不错的选择。例如,浏览器的前进和后退,括号匹配等问题。栈在代码的编写中有着很广泛的应用,例如,大多数程序运行环境都有的子程序的调用,函数的递归调用等。这些问题都具有后进先出的特性。

队列

队列的基本概念

队列是什么

线性表限制一端进行删除,则另一端进行增加。即具有先进先出的特点。

与线性表、栈一样,队列也存在这两种存储方式,即顺序队列和链式队列:

  • 顺序队列,依赖数组来实现,其中的数据在内存中也是顺序存储。
  • 而链式队列,则依赖链表来实现,其中的数据依赖每个结点的指针互联,在内存中并不是顺序存储。链式队列,实际上就是只能尾进头出的线性表的单链表。

一个队列依赖头指针(front)尾指针(rear)进行唯一确定。队列从队头(front)删除元素,从队尾(rear)插入元素。

队列为空时:

基本操作

顺序队列的数据操作

为了实现一个有 k 个元素的顺序存储的队列,我们需要建立一个长度比 k 大的数组,以便把所有的队列元素存储在数组中。

入队:

当 A 出队列时,front 指针指向下标为 1 的位置,rear 保持不变。其后 E 加入队列,front 保持不变,rear 则移动到了数组以外,如下图所示:

这就是数组越界的问题。通过循环队列来解决。

循环队列

如果是循环队列,rear 指针就可以重新指向下标为 0 的位置,如下图所示:

如果这时再新增了 F 进入队列,就可以放入在下标为 0 的位置,rear 指针指向下标为 1 的位置。这时的 rear 和 front 指针就会重合,指向下标为 1 的位置,如下图所示:

此时,又会产生新的问题,即当队列为空时,有 front 指针和 rear 指针相等。而现在的队列是满的,同样有 front 指针和 rear 指针相等。那么怎样判断队列到底是空还是满呢?常用的方法是,设置一个标志变量 flag 来区别队列是空还是满。

链式队列的数据操作

链式队列就是一个单链表,同时增加了 front 指针和 rear 指针。链式队列和单链表一样,通常会增加一个头结点,并另 front 指针指向头结点。头结点不存储数据,只是用来辅助标识。

新增操作,链式队列进行新增数据操作时,将拥有数值 X 的新结点 s 赋值给原队尾结点的后继,即 rear.next。然后把当前的 s 设置为队尾结点,指针 rear 指向 s。如下图所示:

删除操作,实际删除的是头结点的后继结点。这是因为头结点仅仅用来标识队列,并不存储数据。因此,出队列的操作,就需要找到头结点的后继,这就是要删除的结点。接着,让头结点指向要删除结点的后继。

头节点干啥用的?为了防止删除最后一个有效数据结点后, front 指针和 rear 指针变成野指针,导致队列没有意义了。有了头结点后,哪怕队列为空,头结点依然存在,能让 front 指针和 rear 指针依然有意义。如图:

应用

约瑟夫环

约瑟夫环是一个数学的应用问题,具体为,已知 n 个人(以编号 1,2,3...n 分别表示)围坐在一张圆桌周围。从编号为 k 的人开始报数,数到 m 的那个人出列;他的下一个人又从 1 开始报数,数到 m 的那个人又出列;依此规律重复下去,直到圆桌周围的人全部出列。这个问题的输入变量就是 n 和 m,即 n 个人和数到 m 的出列的人。输出的结果,就是 n 个人出列的顺序。

这个问题,用队列的方法实现是个不错的选择。它的结果就是出列的顺序,恰好满足队列对处理顺序敏感的前提。因此,求解方式也是基于队列的先进先出原则。解法如下:

  1. 先把所有人都放入循环队列中。注意这个循环队列的长度要大于或者等于 n。
  2. 从第一个人开始依次出队列,出队列一次则计数变量 i 自增。如果 i 比 m 小,则还需要再入队列。
  3. 直到i等于 m 的人出队列时,就不用再让这个人进队列了。而是放入一个用来记录出队列顺序的数组中。
  4. 直到数完 n 个人为止。当队列为空时,则表示队列中的 n 个人都出队列了,这时结束队列循环,输出数组内记录的元素。

至此,我们就通过循环队列解决了约瑟夫环问题。

总结

队列与栈的特性非常相似,队列也继承了线性表的优点与不足,是加了限制的线性表,队列的增和删的操作只能在这个线性表的头和尾进行。

在时间复杂度上,循环队列和链式队列的新增、删除操作都为 O(1)。而在查找操作中,队列和线性表一样只能通过全局遍历的方式进行,也就是需要 O(n) 的时间复杂度。在空间性能方面,循环队列必须有一个固定的长度,因此存在存储元素数量和空间的浪费问题,而链式队列不存在这种问题,所以在空间上,链式队列更为灵活一些。

怎么选择链式队列还是循环队列?在可以确定队列长度最大值时,建议使用循环队列。无法确定队列长度时,应考虑使用链式队列。队列具有先进先出的特点,很像现实中人们排队买票的场景。在面对数据处理顺序非常敏感的问题时,队列一定是个不错的技术选型。

LetCode真题

20. 有效的括号

package cn.ren.demo;
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;
public class Solution {
	public static void main(String[] args) {
		String s = "";
		System.out.println(isValid(s));
	}

	public static boolean isValid(String s) {
		if (s.length() == 0) {
			return true;
		} else if (s.length() % 2 == 1) {
			return false;
		}
		Map<Character, Character> match = new HashMap<Character, Character>();
		match.put('(', ')');
		match.put('[', ']');
		match.put('{', '}');
		Stack<Character> stack = new Stack<Character>();

		for (int i = 0; i < s.length(); i++) {
			char temp = s.charAt(i);
			if (match.containsKey(temp)) { // 左括号,入栈
				stack.add(temp);
			} else if (match.containsValue(temp)) { // 右括号
				if (!stack.isEmpty()) { 
					char popElement = stack.pop(); // 栈顶
					if (match.get(popElement) != temp) { // 不匹配
						return false;
					}
				} else { // 栈空,不匹配:有右括号,无左括号
					return false;
				}
			}
		}
		if (stack.isEmpty()) { 
			return true;
		}
		return false;
	}
}

总结:应用到数据结构:HashMap、Stack、String中charAt(index)

739. 每日温度

class Solution {
	public int[] dailyTemperatures(int[] T) {
		int[] result = new int[T.length];
		Stack<Integer> stack = new Stack<Integer>();
		for (int i = 0; i < T.length; i++) {
			int temp = T[i];
			while ( !stack.isEmpty() && temp > T[stack.peek()]) { // 温度比它大,出栈,直到栈空
				result[stack.peek()] = i - stack.peek();
				stack.pop();
			}
			stack.push(i);
		}
		return result;
	}
}

239. 滑动窗口最大值

class Solution {
	 public static int[] maxSlidingWindow(int[] nums, int k) {
		 int[] result = new int[nums.length - k + 1] ;
		 Deque<Integer> deque = new LinkedList<Integer>() ; // 保存的是索引
		 for(int i = 0; i < nums.length; i++) {
			 // 头:移出头部,保证窗口长度的长度范围
			 if(!deque.isEmpty() && deque.getFirst() < (i - k + 1)) { // 头部没有缩短时,第二个条件会不满足,要去除头部
				 deque.poll() ; 
			 }
			 // 尾:移出尾部元素小于当前值的元素
			 while(!deque.isEmpty() && nums[i] >= nums[deque.getLast()]) {
				 deque.removeLast() ;
			 }
			 // 尾:尾部入队,滑动窗口向右扩充
			 deque.addLast(i);
			 // 头:从头部返回最大值
			 if(i >= k-1) {// 开始时窗口要够大
				 result[i - k + 1] = nums[deque.getFirst()] ;
			 }
		 }
		 
		 return result ;

	    }
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值