栈
定义
只允许在一端插入和删除数据,后进先出的线性表。
栈既可以用数组实现,也可以用链表来实现。
固定大小的栈
由于操作简单,常规的栈时间复杂度和空间复杂度皆为O(1)。
支持动态扩展的栈
支持动态扩容的栈,其底层其实是用一个支持动态扩容的数组支持的,当栈空间满了以后,我们就申请一个更大的数组,将原来的数据迁移过去。
如果忘了数组是如何支持动态扩展的,可以去下文复习一下。
淤白:02 数组(附ArrayList源码分析)zhuanlan.zhihu.com支持动态扩展的栈,其时间复杂度平时是O(1),但当进行扩容的时候,时间复杂度就变为了O(n),利用摊还分析法,我们可知,其平均情况下的时间复杂度为O(1)。
摊还分析法可以在下文中复习一下。
淤白:01 复杂度分析zhuanlan.zhihu.com平时开发过程中,遇到的栈操作可能不多,但其实我们可能一直在用到它。例如函数调用(遇到过StackOverflowError的化,可能记忆犹新);还有在表达式求值的时候,其实编译器就是通过栈去实现的。面试中,我们可能也会碰到栈的相关问题:括号匹配,用栈实现队列。
队列
定义
允许在一端插入数据,另一端删除数据,先进先出的线性表。
队列和栈类似,也可以使用数组或者链表实现。
顺序队列(数组实现)
相较于栈的一端操作,只需要一个指针,队列的入队出队需要两个指针head和tail的配合。但这里会碰到一个问题,如果每次出队的时候(类比删除数组元素下标为0的元素),都进行数据前移,那时间复杂度就是O(n)了。下图是进行三次出队的情况。
针对这种情况,我们可以用到数组篇聊到的一个方案,延迟前移,只有当入队时发现空间不够了,再进行整体的一个移位,这样均摊下来,时间复杂度也就降下来了。
还有一种解决方案,就是使用循环队列。
循环队列
顾名思义,就是将队尾和队首连起来,即当tail指针指向队尾时,再进行入队操作,会将tail指针指向下标为0的位置。通过循环队列,我们就不需要再考虑数据移位的问题,不过实现起来,需要考虑清楚队空和队满的情况。
在非循环队列中,判断队空用的head == tail,队满用的tail == n。
在循环队列中,判断队空仍然可用head == tail,但是队满不能使用tail == n了,而是要用(tail+1) % n == head,可以画图试一下。
队列在实际开发中,用到的会更多一些,比如支持并发的BlockingQueue阻塞队列,用来实现生产者-消费者模型。还有就是双端队列Deque,支持在队首队尾都可以进行入队出队操作。