算法面试题
栈和队列
栈
栈的基本性质
栈是一种操作受限的线性表,只允许在一端插入和删除数据。
先进后出,后进先出,这就是典型的栈的特点。
栈既可以用数组来实现,又可以用链表来实现,用数组实现的栈,叫做顺序栈,用链表实现的栈叫做链式栈
栈操作(出栈、入栈)的时间复杂度是O(1)
如何实现一个支持动态扩容的栈,只需要底层依赖一个支持动态扩容的数组就可以了,当栈满了以后,就申请一个更大的数组,将原来的数组搬移到新的数组中。
栈的应用
- 栈在函数调用中的应用
操作系统给每个线程分配了一块独立的内存空间,这块内存被组织成“栈”这种数据结构,用来存储函数调用时的临时变量。每进入一个函数,就会将临时变量作为一个栈帧入栈,当被调用函数执行完成,返回之后,将这个函数对应的栈帧出栈。 - 栈在表达式求值中的应用
编译器通过两个栈来实现,其中一个保存操作数的栈,另一个保存运算符的栈。我们从左向右遍历表达式,当遇到数字,我们就直接压入操作数栈,当遇到运算符,就与运算符栈的站定元素比较。
如果闭运算符栈顶元素的优先级高,就将当前运算符压入栈;如果比运算符栈顶元素的优先级低或者相同,从运算符栈中取栈顶元运算符,从操作数栈的栈顶取2个操作数,然后进行计算,在把计算完的结果压入操作数栈,继续比较。 - 栈在括号匹配中的应用
当扫描到左括号时,则将其压入栈中,当扫描到右括号时,从栈顶取出一个左括号,如果能够匹配,则继续扫描剩下的字符串。如果扫描的过程中,遇到不能配对的右括号,或者栈中没有数据,则说明为非法格式。
当所有括号都扫描完成之后,如果栈为空,则说明字符串为合法格式,如果说明有未匹配的左括号,为非法格式。
小问题
jvm里面的栈跟我们这里说的栈是不是一回事儿呢?
如果不是,那它为什么又叫做栈呢?
内存中的堆栈和数据结构堆栈不是一个概念,可以说内存中的堆栈是真实存在的物理区,数据结构中的堆栈是抽象的数据存储结构。
内存空间在逻辑上分为三部分:代码区、静态数据区和动态数据区,动态数据区又分为栈区和堆区。
代码区:存储方法体的二进制代码。高级调度(作业调度)、中级调度(内存调度)、低级调度(进程调度)控制代码区执行代码的切换。
静态数据区:存储全局变量、静态变量、常量,常量包括final修饰的常量和String常量。系统自动分配和回收。
栈区:存储运行方法的形参、局部变量、返回值。由系统自动分配和回收。
堆区:new一个对象的引用或地址存储在栈区,指向该对象存储在堆区中的真实数据。
几个练习题
有效的括号
给定一个只包括 ‘(’,’)’,’{’,’}’,’[’,’]’ 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
思路:栈最基本的操作,遇到右括号出栈判断,遇到左括号进行入栈,最主要的是基础知识点不扎实
public boolean isValid(String s) {
boolean flag = true;
if (s == null || s.length() ==0){
return flag;
}
Stack<String> stack = new Stack<String>();
for(int i =0;i<s.length();i++){
String string = s.charAt(i)+"";
String ss = "";
if(string.equals("(")||string.equals("{")||string.equals("[")){
stack.push(string);
}else if(string.equals(")")){
if(stack.isEmpty()||!stack.pop().equals("(")){
flag = false;
break;
}
}else if(string.equals("}")){
if(stack.isEmpty()||!stack.pop().equals("{")){
flag = false;
break;
}
}else if(string.equals("]")){
if(stack.isEmpty()||!stack.pop().equals("[")){
flag = false;
break;
}
}else{
flag = false;
break;
}
}
if(!stack.isEmpty()){
flag = false;
}
return flag;
}
优化后的代码
public boolean isValid(String s) {
boolean flag = true;
if (s == null || s.length() ==0){
return flag;
}
if(s.length()%2 == 1){
flag = false;
return flag;
}
HashMap<Character,Character> map = new HashMap<Character,Character>(){{
put(')','(');
put('}','{');
put(']','[');
}};
Stack<Character> stack = new Stack<Character>();
for(int i = 0;i<s.length();i++){
char ss = s.charAt(i);
if(map.containsKey(ss)){
if(stack.isEmpty() || !map.get(ss).equals(stack.peek())){
flag = false;
break;
}
stack.pop();
}else{
stack.push(ss);
}
}
if(!stack.isEmpty()){
flag = false;
}
return flag;
}
棒球比赛
你现在是一场采用特殊赛制棒球比赛的记录员。这场比赛由若干回合组成,过去几回合的得分可能会影响以后几回合的得分。
比赛开始时,记录是空白的。你会得到一个记录操作的字符串列表 ops,其中 ops[i] 是你需要记录的第 i 项操作,ops 遵循下述规则:
整数 x - 表示本回合新获得分数 x
“+” - 表示本回合新获得的得分是前两次得分的总和。题目数据保证记录此操作时前面总是存在两个有效的分数。
“D” - 表示本回合新获得的得分是前一次得分的两倍。题目数据保证记录此操作时前面总是存在一个有效的分数。
“C” - 表示前一次得分无效,将其从记录中移除。题目数据保证记录此操作时前面总是存在一个有效的分数。
请你返回记录中所有得分的总和。
public int calPoints(String[] ops) {
Deque<Integer> stack = new LinkedList<Integer>();
for(int i = 0;i < ops.length;i++){
String s = ops[i];
if(s.equals("+")){
int a = stack.pop();
int b = a + stack.peek();
stack.push(a);
stack.push(b);
}else if(s.equals("D")){
int a = stack.peek();
stack.push(2*a);
}else if(s.equals("C")){
stack.pop();
}else{
stack.push(Integer.parseInt(s));
}
}
int sum = 0;
while(!stack.isEmpty()){
sum += stack.pop();
}
return sum;
}
比较含退格的字符串
给定 S 和 T 两个字符串,当它们分别被输入到空白的文本编辑器后,判断二者是否相等,并返回结果。 # 代表退格字符。
注意:如果对空文本输入退格字符,文本继续为空。
此题也可以用stringBuffer来解答
public boolean backspaceCompare(String s, String t) {
Deque<String> stack_s = new LinkedList<String>();
Deque<String> stack_t = new LinkedList<String>();
String s1 = rev(s,stack_s);
String s2 = rev(t,stack_t);
return s1.equals(s2)? true:false;
}
public String rev (String string,Deque<String> stack){
for(int i =0;i<string.length();i++){
String s = string.charAt(i)+"";
if(s.equals("#")){
if(!stack.isEmpty()){
stack.pop();
}
}else{
stack.push(s);
}
}
String result = "";
while(!stack.isEmpty()){
result+=stack.pop();
}
return result;
}
滑动窗口的平均值
给定一个整数数据流和一个窗口大小,根据该滑动窗口的大小,计算滑动窗口里所有数字的平均值。
实现 MovingAverage 类:
MovingAverage(int size) 用窗口大小 size 初始化对象。
double next(int val) 成员函数 next 每次调用的时候都会往滑动窗口增加一个整数,请计算并返回数据流中最后 size 个值的移动平均值,即滑动窗口里所有数字的平均值。
class MovingAverage {
Queue<Integer> queue;
int n = 0;
double sum = 0.0;
public MovingAverage(int size) {
queue = new LinkedList<Integer>();
n = size;
}
public double next(int val) {
if(queue.size()<n){
sum += val;
queue.offer(val);
}else{
sum +=val;
sum -=queue.poll();
queue.offer(val);
}
return sum/queue.size();
}
}
队列的最大值
请定义一个队列并实现函数 max_value 得到队列里的最大值,要求函数max_value、push_back 和 pop_front 的均摊时间复杂度都是O(1)。
若队列为空,pop_front 和 max_value 需要返回 -1
示例1
输入:
["MaxQueue","push_back","push_back","max_value","pop_front","max_value"]
[[],[1],[2],[],[],[]]
输出: [null,null,null,2,1,2]
思路:需要定义两个队列,一个普通队列存储数据queue,一个为双向队列deque,存储最大值,需要在插入和删除的时候将其进行修改;如果添加时,添加的数据大于deque中的getlast,则将小于val的值都删掉,添加val;如果删除的时候,queue.peek==deque.peek则queue和deque的peek都需要删除
在这里插入代码片class MaxQueue {
Queue<Integer> queue;//用于添加入队出队信息
Deque<Integer> deque;//用于存放最大值信息
public MaxQueue() {
queue = new LinkedList<Integer>();
deque = new LinkedList<Integer>();
}
public int max_value() {//获得最大的数
int max = -1;
if(queue.isEmpty()){
return max;
}
return deque.getFirst();
}
public void push_back(int value) {
queue.offer(value);
while(!deque.isEmpty() && deque.getLast()<value){
deque.removeLast();
}
deque.offer(value);
}
public int pop_front() {
if(queue.isEmpty()){
return -1;
}
int result = queue.peek();
if(deque.peek()==result){
deque.poll();
}
return queue.poll();
}
}
单调栈1 2
单调栈是一种特殊的栈,栈本身就是一种受限的数据结构,单调栈在此基础上又受限一次,单调栈要求栈中的元素是单调递减或单调递增的(按照出栈顺序是递增还是递减分为单调递增栈和单调递减栈)
单调栈适合的应用场景就是求下一个大于*或者下一个小于的题目
单调栈模板
vector<int> nextGreaterElement(vector<int>& nums) {
vector<int> res(nums.size()); // 存放答案的数组
stack<int> s;
// 倒着往栈里放
for (int i = nums.size() - 1; i >= 0; i--) {
// 判定个子高矮
while (!s.empty() && s.top() <= nums[i]) {
// 矮个起开,反正也被挡着了。。。
s.pop();
}
// nums[i] 身后的 next great number
res[i] = s.empty() ? -1 : s.top();
//
s.push(nums[i]);
}
return res;
}
//java 中顶端元素用的是peek()而不是此处的top
下一个更大元素
给你两个 没有重复元素 的数组 nums1 和 nums2 ,其中nums1 是 nums2 的子集。
请你找出 nums1 中每个元素在 nums2 中的下一个比其大的值。
nums1 中数字 x 的下一个更大元素是指 x 在 nums2 中对应位置的右边的第一个比 x 大的元素。如果不存在,对应位置输出 -1 。
思路一:暴力搜索两层遍历
public int[] nextGreaterElement(int[] nums1, int[] nums2) {
//第一种方法:暴力搜索法
int[] result = new int[nums1.length];
for(int i =0;i<nums1.length;i++){
int j = 0;
while(nums1[i]!=nums2[j]){
j++;
}
int max_num = -1;
for(;j<nums2.length;j++){
if(nums2[j]>nums1[i]){
max_num = nums2[j];
break;
}
}
result[i] = max_num;
}
return result;
//第二种方法:利用单调栈,stack、nums[],result[],同时设置nums和result两者map对应关系,然后在遍历nums1,找到对应的值即可
}
思路二:利用单调栈,stack、nums[],result[],同时设置nums和result两者map对应关系,然后在遍历nums1,找到对应的值即可
- 先对 nums2 中的每一个元素,求出它的右边第一个更大的元素;
- 将上一步的对应关系放入哈希表(HashMap)中;
- 再遍历数组 nums1,根据哈希表找出答案。
public int[] nextGreaterElement(int[] nums1, int[] nums2) {
//第二种方法:利用单调栈,stack、nums[],result[],同时设置nums和result两者map对应关系,然后在遍历nums1,找到对应的值即可
int [] finalresult = new int[nums1.length];
int [] result = monoStack(nums2);
HashMap<Integer,Integer> map = new HashMap<Integer,Integer>();
for(int i =0; i<nums2.length;i++){
map.put(nums2[i],result[i]);
}
for(int i = 0;i<nums1.length;i++){
finalresult[i]=map.get(nums1[i]);
}
return finalresult;
}
public int[] monoStack(int[] nums){
int[] result = new int[nums.length];
Stack<Integer> stack = new Stack<Integer>();
for(int i = nums.length-1; i >= 0; i--){
while(!stack.isEmpty() && nums[i]>=stack.peek()){
stack.pop();
}
result[i] = stack.isEmpty()? -1:stack.peek();
stack.push(nums[i]);
}
return result;
}
每日温度
请根据每日 气温 列表 temperatures ,请计算在每一天需要等几天才会有更高的温度。如果气温在这之后都不会升高,请在该位置用 0 来代替。
示例 1:
输入: temperatures = [73,74,75,71,69,72,76,73]
输出: [1,1,4,2,1,1,0,0]
思路:还是单调栈的用法,只不过stack和result数组中存的是下标,而不是具体的值
public int[] dailyTemperatures(int[] temperatures) {
//单调栈问题,特殊点同时需要保存最高温所在的时间,即pos【用两个栈,一个存temp,一个存pos同时出栈入栈??】
//变通
int[] result = new int[temperatures.length];
Stack<Integer> stack = new Stack<Integer>();
for(int i = temperatures.length-1;i>=0;i--){
while(!stack.isEmpty()&& temperatures[i]>=temperatures[stack.peek()]){
stack.pop();
}
result[i] = stack.isEmpty()? 0 : stack.peek()-i;
stack.push(i);
}
return result;
}
下一个更大的元素II
给定一个循环数组(最后一个元素的下一个元素是数组的第一个元素),输出每个元素的下一个更大元素。数字 x 的下一个更大的元素是按数组遍历顺序,这个数字之后的第一个比它更大的数,这意味着你应该循环地搜索它的下一个更大的数。如果不存在,则输出 -1。
示例 1:
输入: [1,2,1]
输出: [2,-1,2]
解释: 第一个 1 的下一个更大的数是 2;
数字 2 找不到下一个更大的数;
第二个 1 的下一个最大的数需要循环搜索,结果也是 2。
思路:主要区别是这是一个循环数组,所以我们可以凑出2倍的数组,就可以解决这个问题了。
比较经典的就是可以通过%运算符求模取余数,来获得环形特效
int[] arr = {1,2,3,4,5};
int n = arr.length, index = 0;
while (true) {
print(arr[index % n]);
index++;
}
本题题解为:
public int[] nextGreaterElements(int[] nums) {
int n = nums.length;
int[]res = new int[n];
Stack<Integer> stack = new Stack<Integer>();
for(int i = 2*n-1;i>=0;i--){
while(!stack.isEmpty()&&nums[i%n]>=stack.peek()){
stack.pop();
}
res[i%n] = stack.isEmpty()? -1 : stack.peek();
stack.push(nums[i%n]);
}
return res;
}
队列
队列和栈一样也是一种操作受限的线性表数据结构,队列可以由数组实现也可以由链表实现,数组实现的叫顺序队列,链表实现的叫链式队列。
队列需要两个指针:一个是head指针指向头,一个是tail指针,指向队尾
出队dequeue;入队enqueue;都需要判断一下是否满了或者是否为空;
队列满的条件是tail == n,n是数组的大小;
队列空的条件是head == tail;
循环队列
队空的条件:head == tail
队满的条件:(tail+1)%n ==head
阻塞队列和并发队列
阻塞队列其实就是在队列基础上增加了阻塞操作。简单来说,就是在队列为空的时候,从队头取数据会被阻塞。因为此时还没有数据可取,直到队列中有了数据才能返回;如果队列已经满了,那么插入数据的操作就会被阻塞,直到队列中有空闲位置后再插入数据,然后再返回。典型的例子就是生产者消费者模型。
线程安全的队列我们叫作并发队列。最简单直接的实现方式是直接在 enqueue()、dequeue() 方法上加锁,但是锁粒度大并发度会比较低,同一时刻仅允许一个存或者取操作。实际上,基于数组的循环队列,利用 CAS 原子操作,可以实现非常高效的并发队列。这也是循环队列比链式队列应用更加广泛的原因。
几个小的例子
队列常用的函数offer、poll、peek、add、remove等
队列里常用的有两套表示方式:
- 第一种
String[] items;//记录队里中的内容
int n =0;//数组大小,即申请的队列的大小
int head = 0;//队头下标
int tail = 0;//队尾下标
- 第二种
String[] items;//记录队里中的内容
int n =0;//数组大小,即申请的队列的大小
int head = 0;//队头下标
int size = 0;//队列中数据的个数
tail可以通过head和size 的计算来获得,所以两者都是可以的,只不过在循环队列中,如果使用tail,tail始终指向下一个待插入的位置,在循环队列中需要多申请一个空间,例如需要k个,则需要申请k+1才可以;笔者更喜欢用第一种方式来实现,只要逻辑正确任何一种都可以
循环队列中,判断为空的条件为head == tail,判断满的条件是**(tail+1)%n == head** 实际上当队列满时,tail指向的位置实际上是没有存储数据的,所以循环队列会浪费一个数组的存储空间。
用队列实现栈
请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty)。
实现 MyStack 类:
void push(int x) 将元素 x 压入栈顶。
int pop() 移除并返回栈顶元素。
int top() 返回栈顶元素。
boolean empty() 如果栈是空的,返回 true ;否则,返回 false 。
注意:
你只能使用队列的基本操作 —— 也就是 push to back、peek/pop from front、size 和 is empty 这些操作。
你所使用的语言也许不支持队列。 你可以使用 list (列表)或者 deque(双端队列)来模拟一个队列 , 只要是标准的队列操作即可。
思路:对于push函数,需要特别注意,考虑到栈是先进后出,而队列是先进先出,所以可以用两个队列来相互交换实现
class MyStack {
/** Initialize your data structure here. */
Queue<Integer> queue1;//主
Queue<Integer> queue2;//辅助
public MyStack() {
queue1 = new LinkedList<Integer>();
queue2 = new LinkedList<Integer>();
}
/** Push element x onto stack. */
public void push(int x) {
//先放入到queue2中,然后将queue1的也放入到queue2中,然后在都导入到queue1中
queue2.offer(x);
while(!queue1.isEmpty()){
queue2.offer(queue1.poll());
}
Queue<Integer> temp = queue1;
queue1 = queue2;
queue2 = temp;
}
/** Removes the element on top of the stack and returns that element. */
public int pop() {
return queue1.poll();
}
/** Get the top element. */
public int top() {
return queue1.peek();
}
/** Returns whether the stack is empty. */
public boolean empty() {
return queue1.isEmpty();
}
}
设计循环队列
设计你的循环队列实现。 循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。
循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。
你的实现应该支持如下操作:
MyCircularQueue(k): 构造器,设置队列长度为 k 。
Front: 从队首获取元素。如果队列为空,返回 -1 。
Rear: 获取队尾元素。如果队列为空,返回 -1 。
enQueue(value): 向循环队列插入一个元素。如果成功插入则返回真。
deQueue(): 从循环队列中删除一个元素。如果成功删除则返回真。
isEmpty(): 检查循环队列是否为空。
isFull(): 检查循环队列是否已满。
思路:用第一种来实现,注意申请空间的时候需要多申请一个
class MyCircularQueue {
int[]iterms;
int count =0;//记录容量
int head = 0,tail =0;//头和尾
public MyCircularQueue(int k) {
iterms = new int[k+1];
count=k+1;
}
public boolean enQueue(int value) {
if((tail+1)%count == head){
return false;
}
iterms[tail % count] = value;
tail = (tail+1) % count;
return true;
}
public boolean deQueue() {
if(head == tail){
return false;
}
head = (head+1) % count;
return true;
}
public int Front() {
if(head == tail){
return -1;
}
return iterms[head];
}
public int Rear() {
if(head == tail){
return -1;
}
return iterms[(tail+count-1)%count];
}
public boolean isEmpty() {
return head == tail;
}
public boolean isFull() {
return (tail+1)%count == head;
}
}
第二种方法实现
class MyCircularQueue {
private int[] queue;
private int headIndex;
private int count;
private int capacity;
/** Initialize your data structure here. Set the size of the queue to be k. */
public MyCircularQueue(int k) {
this.capacity = k;
this.queue = new int[k];
this.headIndex = 0;
this.count = 0;
}
/** Insert an element into the circular queue. Return true if the operation is successful. */
public boolean enQueue(int value) {
if (this.count == this.capacity)
return false;
this.queue[(this.headIndex + this.count) % this.capacity] = value;
this.count += 1;
return true;
}
/** Delete an element from the circular queue. Return true if the operation is successful. */
public boolean deQueue() {
if (this.count == 0)
return false;
this.headIndex = (this.headIndex + 1) % this.capacity;
this.count -= 1;
return true;
}
/** Get the front item from the queue. */
public int Front() {
if (this.count == 0)
return -1;
return this.queue[this.headIndex];
}
/** Get the last item from the queue. */
public int Rear() {
if (this.count == 0)
return -1;
int tailIndex = (this.headIndex + this.count - 1) % this.capacity;
return this.queue[tailIndex];
}
/** Checks whether the circular queue is empty or not. */
public boolean isEmpty() {
return (this.count == 0);
}
/** Checks whether the circular queue is full or not. */
public boolean isFull() {
return (this.count == this.capacity);
}
}
其他参考文献[^3] 和[4]和[5]