前言:以下这些题都是高频面试题,工程中不会这么写,但是面试经常面。
原因是这些题非常轻量级,又能够验证你是否有最基本的扣题能力或者对数据结构有最基本的认识,非常适合拿来面试。(知识点大多数来源自左神)
一.用数组结构实现大小固定的队列和栈
1.固定数组结构实现栈:
由于大小固定,假设数组长度为size=5或者随便多少,那么逻辑为:
1.初始index=0;(index永远表示新进来的一个数要放在哪个位置)
2.每次添加一个数,给数组index位置赋值,index++到下一个位置,如果index=size,说明数组已经满了,则越界直接报错。
3.每次取出一个数,--index到上一个位置,取出该数,如果index=0,说明没有上一个数了,则越界直接报错。
如图:
public static class ArrayStack {
private Integer[] arr;
private Integer index;
//initSize 数组的初始大小
public ArrayStack(int initSize) {
if (initSize < 0) {
throw new IllegalArgumentException("The init size is less than 0");
}
arr = new Integer[initSize];
index = 0;
}
//只返回栈顶的数 下次增加不会覆盖掉这个数
public Integer peek() {
if (index == 0) {
return null;
}
return arr[index - 1];
}
//新入栈一个数
public void push(int obj) {
if (index == arr.length) {
throw new ArrayIndexOutOfBoundsException("The queue is full");
}
arr[index++] = obj;
}
//返回栈顶的数 下次增加会覆盖取出的位置的数
public Integer pop() {
if (index == 0) {
throw new ArrayIndexOutOfBoundsException("The queue is empty");
}
return arr[--index];
}
}
2.固定数组结构实现队列:
思路:
设计一个start和一个end变量,初始都指向0的位置。
再单独设计一个size变量,用来约束start和end,初始size=0;
end 代表:新加一个数,加到哪个位置;
start代表:取出一个数,从哪个位置取。
size 代表:当前结构中有几个数。
逻辑为:
假设数组 arr.length = 4;
增加数的时候:只要size < 4,就把数增加到end指向的位置;size++ ; end++ ;
如果end到达最后一个位置,则end回到开头。
取出数的时候:只要size != 0,就把start位置的数取出;size-- ; start++ ;
如果start到达最后一个位置,则start回到开头。
最终表现上去:就像是start一直在追着end走。
设计size的原因:如果单纯视同start和size控制,代码逻辑会很难写。
增加size变量可以很好的解耦start和end的关系,代码也会好些很多。
public static class ArrayQueue {
private Integer[] arr;
private Integer size;
private Integer start;
private Integer end;
public ArrayQueue(int initSize) {
if (initSize < 0) {
throw new IllegalArgumentException("The init size is less than 0");
}
arr = new Integer[initSize];
size = 0;
start = 0;
end = 0;
}
//1.增加一个数
public void push(int obj) {
if (size == arr.length) {
throw new ArrayIndexOutOfBoundsException("The queue is full");
}
size++;
arr[end] = obj;
end = end == arr.length - 1 ? 0 : end + 1;
}
//2.取出一个数
public Integer poll() {
if (size == 0) {
throw new ArrayIndexOutOfBoundsException("The queue is empty");
}
size--;
int tmp = start;
start = start == arr.length - 1 ? 0 : start + 1;
return arr[tmp];
}
}
二、实现一个特殊的栈(能返回栈中最小元素)
在实现栈的基本功能的基础上,再实现返回栈中最小元素的操作。
【要求】
1.pop、push、getMin操作的时间复杂度都是O(1)。
2.设计的栈类型可以使用现成的栈结构
思考:如果只用一个栈,那么getMin的操作时间复杂为O(n),因为获取最小值需要遍历。
设计思路:同时维护两个栈结构(系统提供的栈),一个栈就是正常的栈,另一个栈放当前的最小值,和第一个栈同步操作。
data栈正常存数据,min栈每次存的时候跟当前栈顶的数比较一下;当前数小于栈顶的数,则放入当前数,否则栈顶的数再存一次。
代码很简单,如下:
public static class MyStack {
private Stack<Integer> stackData;
private Stack<Integer> stackMin;
public MyStack() {
this.stackData = new Stack<Integer>();
this.stackMin = new Stack<Integer>();
}
public void push(int newNum) {
if (this.stackMin.isEmpty()) {
this.stackMin.push(newNum);
} else if (newNum < this.getmin()) {
this.stackMin.push(newNum);
} else {
int newMin = this.stackMin.peek();
this.stackMin.push(newMin);
}
this.stackData.push(newNum);
}
public int pop() {
if (this.stackData.isEmpty()) {
throw new RuntimeException("Your stack is empty.");
}
this.stackMin.pop();
return this.stackData.pop();//返回栈顶并弹出
}
public int getmin() {
if (this.stackMin.isEmpty()) {
throw new RuntimeException("Your stack is empty.");
}
return this.stackMin.peek();//peek就是返回栈顶不弹出
}
}
三、如何仅用队列结构实现栈结构?如何仅用栈结构实现队列结构?
1.仅用队列结构实现栈结构
实现思路:
1.准备2个队列,一个data队列,一个help队列。
2.push时往data数列中push,pop时从data数列数据取出放到help,只保留一个,用来完成pop操作。
3.然后data和help互换引用;重复以上操作即可。
public static class TwoQueuesStack {
private Queue<Integer> queue;
private Queue<Integer> help;
public TwoQueuesStack() {
queue = new LinkedList<Integer>();
help = new LinkedList<Integer>();
}
public void push(int pushInt) {
queue.add(pushInt);
}
public int peek() {
if (queue.isEmpty()) {
throw new RuntimeException("Stack is empty!");
}
while (queue.size() != 1) {
help.add(queue.poll());
}
int res = queue.poll();
help.add(res);
swap();
return res;
}
public int pop() {
if (queue.isEmpty()) {
throw new RuntimeException("Stack is empty!");
}
while (queue.size() > 1) {//依次把数据挪到help队列,仅留一个
help.add(queue.poll());
}
int res = queue.poll();
swap();
return res;
}
//交换引用
private void swap() {
Queue<Integer> tmp = help;
help = queue;
queue = tmp;
}
}
2.仅用栈结构实现队列结构
实现思路:
1.准备2个栈,一个stackpush,一个stackpop。
2.push操作只作用在stackpush,pop操作只作用于stackpop。
3.利用栈结构的特点,倒数据,实现队列结构的需求。
public static class TwoStacksQueue {
private Stack<Integer> stackPush;
private Stack<Integer> stackPop;
public TwoStacksQueue() {
stackPush = new Stack<Integer>();
stackPop = new Stack<Integer>();
}
public void push(int pushInt) {
stackPush.push(pushInt);
dao();
}
public int pop() {
if (stackPop.empty() && stackPush.empty()) {
throw new RuntimeException("Queue is empty!");
}
dao();
return stackPop.pop();
}
public int peek() {
if (stackPop.empty() && stackPush.empty()) {
throw new RuntimeException("Queue is empty!");
}
dao();
return stackPop.peek();
}
//到数据的操作
public void dao() {
if (!stackPop.isEmpty()) {
return;
}
while (!stackPush.isEmpty()) {
stackPop.push(stackPush.pop());
}
}
}
四、猫狗队列问题
宠物、狗和猫的类如下:
class Pet{
private String type;
public Pet(String type){
this.type=type;
}
public String getType(){
return this.type;
}
}
class Dog extends Pet{
public Dog(){
super("Dog");
}
}
class Cat extends Pet{
public Cat(){
super("Cat");
}
}
实现一种猫狗队列结构,要求如下:
①用户可以调用add方法将cat类或dog类的实例放进队列中;
②用户可以调用pollAll方法, 将队列中所有的实例按照进队列的先后顺序依次弹出;
③用户可以调用pollDog方法,将队列中所有的Dog类的实例按照进队列的先后顺序依次弹出;
④用户可以调用pollCat方法,将队列中所有的Cat类的实例按照进队列的先后顺序依次弹出;
⑤用户可以调用isEmpty方法,检查队列里,是否还有Dog或Cat类的实例;
⑥用户可以调用isDogEmpty方法,检查队列里,是否还有Dog类的实例;
⑦用户可以调用isCatEmpty方法,检查队列里,是否还有Cat类的示例;
实现思路:把每次进入队列的实例都加上时间戳,但不能改变原有的数据结构,所以只能定义一个新类来封装时间戳,就是PetEnterQueue类;
DogCatQueue类就是我们实现的猫狗队列,队列中存放的就是PetEnterQueue实例
public static class PetEnterQueue {
private Pet pet;
private long count;
public PetEnterQueue(Pet pet, long count) {
this.pet = pet;
this.count = count;
}
public Pet getPet() {
return this.pet;
}
public long getCount() {
return this.count;
}
public String getEnterPetType() {
return this.pet.getPetType();
}
}
public static class DogCatQueue {
private Queue<PetEnterQueue> dogQ;
private Queue<PetEnterQueue> catQ;
private long count;
public DogCatQueue() {
this.dogQ = new LinkedList<PetEnterQueue>();
this.catQ = new LinkedList<PetEnterQueue>();
this.count = 0;
}
public void add(Pet pet) {
if (pet.getPetType().equals("dog")) {
this.dogQ.add(new PetEnterQueue(pet, this.count++));
} else if (pet.getPetType().equals("cat")) {
this.catQ.add(new PetEnterQueue(pet, this.count++));
} else {
throw new RuntimeException("err, not dog or cat");
}
}
public Pet pollAll() {
if (!this.dogQ.isEmpty() && !this.catQ.isEmpty()) {
if (this.dogQ.peek().getCount() < this.catQ.peek().getCount()) {
return this.dogQ.poll().getPet();
} else {
return this.catQ.poll().getPet();
}
} else if (!this.dogQ.isEmpty()) {
return this.dogQ.poll().getPet();
} else if (!this.catQ.isEmpty()) {
return this.catQ.poll().getPet();
} else {
throw new RuntimeException("err, queue is empty!");
}
}
public Dog pollDog() {
if (!this.isDogQueueEmpty()) {
return (Dog) this.dogQ.poll().getPet();
} else {
throw new RuntimeException("Dog queue is empty!");
}
}
public Cat pollCat() {
if (!this.isCatQueueEmpty()) {
return (Cat) this.catQ.poll().getPet();
} else
throw new RuntimeException("Cat queue is empty!");
}
public boolean isEmpty() {
return this.dogQ.isEmpty() && this.catQ.isEmpty();
}
public boolean isDogQueueEmpty() {
return this.dogQ.isEmpty();
}
public boolean isCatQueueEmpty() {
return this.catQ.isEmpty();
}
}
五.有效括号
给定一个字符串所表示的括号序列,包含以下字符: ‘(’, ‘)’, ‘{’, ‘}’, ‘[’ , ‘]’, 判定是否是有效的括号序列。括号必须依照 “()” 顺序表示, “()[]{}” 是有效的括号,但 “([)]” 则是无效的括号。
public boolean isValidParentheses(String s) {
Stack<Character> stack = new Stack<Character>();
for (Character c : s.toCharArray()) {
if ("({[".contains(String.valueOf(c))) {//字符串为({[,push进栈
stack.push(c);
} else {
//字符串不是({[
if (!stack.isEmpty() && isValid(stack.peek(), c)) {//空栈或者不匹配则返回false
stack.pop();//匹配,则栈内的({[弹出,重新恢复空栈
} else {
return false;
}
}
}
return stack.isEmpty();//for结束stack为空在返回tue
}
private boolean isValid(char c1, char c2) {
return (c1 == '(' && c2 == ')') || (c1 == '{' && c2 == '}')
|| (c1 == '[' && c2 == ']');
}
六.逆波兰表达式求值
逆波兰表示法是波兰逻辑学家J・卢卡西维兹(J・ Lukasiewicz)于1929年首先提出的一种表达式的表示方法 [1] 。后来,人们就把用这种表示法写出的表达式称作“逆波兰表达式”。逆波兰表达式把运算量写在前面,把算符写在后面。
逆波兰表达式又叫做后缀表达式。
什么是中缀表达式?
例如a+b,运算符在两个操作数的中间。这是我们从小学开始学习数学就一直使用的表达式形式。
什么是后缀表达式?
例如a b + ,运算符在两个操作数的后面。后缀表达式虽然看起来奇怪,不利于人阅读,但利于计算机处理
那么,逆波兰表达式如何求值?
样例 1:
输入: ["2", "1", "+", "3", "*"]
输出: 9
解释: ["2", "1", "+", "3", "*"] -> (2 + 1) * 3 -> 9
样例 2:
输入: ["4", "13", "5", "/", "+"]
输出: 6
解释: ["4", "13", "5", "/", "+"] -> 4 + 13 / 5 -> 6
【题解】
逆波兰表达式是更利于计算机运算的表达式形式, 需要用到栈(先进后出的数据结构).
遍历表达式:
重点:
1.碰到数字则入栈
2.碰到运算符则连续从栈中取出2个元素, 使用该运算符运算然后将结果入栈
3.最后栈中剩余一个数字, 就是结果.
public int evalRPN(String[] tokens) {
Stack<Integer> s = new Stack<Integer>();
String operators = "+-*/";
for (String token : tokens) {
if (!operators.contains(token)) {
s.push(Integer.valueOf(token));
continue;
}
int a = s.pop();
int b = s.pop();
if (token.equals("+")) {
s.push(b + a);
} else if(token.equals("-")) {
s.push(b - a);
} else if(token.equals("*")) {
s.push(b * a);
} else {
s.push(b / a);
}
}
return s.pop();
}