内容来源于自己的刷题笔记,对一些题目进行方法总结,用 java 语言实现。
第一章 栈和队列
1. 设计一个有 getMin 功能的栈:
-
题目描述:
实现一个特殊的栈,在实现栈的基础上,再实现返回栈中最小元素的操作。
-
解题思路:
在设计时,我们使用两个栈,一个栈用来保存当前栈中的元素,其功能和一个正常的栈没有区别,这个栈记为 stackData;另一个栈记为 stackMin。具体的实现方式有两种:(一般情况下)
- 数据压栈时,先对 stackMin 进行判断,如果小于等于栈顶元素,压入 stackMin 和 stackData 中,反之则只压入 stackMin 中;数据出栈时,要比较此时两个栈顶元素是否相等,如果相等,stackMin 也得出栈。这样设计能保证 stackMin 内的元素自上而下非严格递增,并且栈的大小小于 stackData,因此在判断是否为空时只需要判断其中之一就可。
- 数据压栈时,先对 stackMin 进行判断,如果小于等于栈顶元素,压入 stackMin 和 stackData 中,反之将当前 stackMin 的栈顶元素再次压入一次;数据出栈时两个栈同时出栈。
这两个方法都是用 stackMin 栈保存着 stackData 每一步的最小值。共同点是所有操作的时间复杂度都是 O(1),空间复杂度都是 O(n)。区别是:第一个方法中 stackMin 压入栈时稍省空间,但是弹出操作费时间;第二个方法中 stackMin 压入栈时费空间,但是弹出操作稍省时间。
-
代码实现:
public class MyStack1 { Stack<Integer> stackData; Stack<Integer> stackMin; public MyStack1(){ stackData = new Stack<>(); stackMin = new Stack<>(); } public void push(int newNum){ if (stackMin.isEmpty()){ stackMin.push(newNum); }else if (stackMin.peek() >= newNum){ stackMin.push(newNum); } stackData.push(newNum); } public int pop(){ if (stackData.isEmpty()){ throw new RuntimeException("Your stack is empty"); } int value = stackData.pop(); if (stackMin.peek() == value){ stackMin.pop(); } return value; } public int getMin(){ if (stackData.isEmpty()){ throw new RuntimeException("Your stack is empty"); } return stackMin.peek(); } } public class MyStack2 { Stack<Integer> stackData; Stack<Integer> stackMin; public MyStack2(){ stackData = new Stack<>(); stackMin = new Stack<>(); } public void push(int newNum){ if (stackMin.isEmpty()){ stackMin.push(newNum); }else if (stackMin.peek() >= newNum){ stackMin.push(newNum); }else { stackMin.push(stackMin.peek()); } stackData.push(newNum); } public int pop(){ if (stackData.isEmpty()){ throw new RuntimeException("Your stack is empty"); } stackMin.pop(); return stackData.pop(); } public int getMin(){ if (stackData.isEmpty()){ throw new RuntimeException("Your stack is empty"); } return stackMin.peek(); } }
2.如何仅用递归函数和栈操作逆序一个栈:
-
题目描述:
一个栈依次压入 1、2、3、4、5,那么从栈顶到栈底分别为 5、4、3、2、1。将这个栈转置后,从栈顶到栈底为 1、2、3、4、5,也就是实现栈中元素的逆序,但是只能用递归函数来实现,不能用其他数据结构。
-
解题思路:
我们需要设计两个递归函数,一个用于获取栈底元素并且将其删除,另外一个就是转置的主函数。在转置的递归调用过程中,不断得将栈底元素删除,再将其压入栈顶。
-
代码实现:
/** * 将栈 stack 的栈底元素返回并移除 * @param stack * @return */ private static int getAndRemoveLastElement(Stack<Integer> stack){ int result = stack.pop(); if (stack.isEmpty()){ return result; }else { int last = getAndRemoveLastElement(stack); stack.push(result); return last; } } /** * 逆序一个栈 * @param stack */ public static void reverse(Stack<Integer> stack){ if (stack.isEmpty()){ return; } int i = getAndRemoveLastElement(stack); reverse(stack); stack.push(i); }
3. 猫狗队列:
-
题目描述:
宠物,狗和猫的类如下:
public class Pet { private String type; public Pet(String type){ this.type = type; } public String getType(){ return type; } } public class Dog extends Pet{ public Dog(String type) { super("Dog"); } } public class Cat extends Pet{ public Cat(String type) { super("Cat"); } }
实现一种猫狗队列的结构,要求如下:
- 用户可以调用 add 方法将 cat 类或 dog 类的实例放入队列中
- 用户可以调用 pollAll 方法,将队列中所有的实例按照队列的先后顺序依次弹出。
- 用户可以调用 pollDog 方法,将队列中 dog 类的实例按照队列的先后顺序依次弹出。
- 用户可以调用 pollCat 方法,将队列中 cat 类的实例按照队列的先后顺序依次弹出。
- 用户可以调用 isEmpty 方法,检查队列中是否还有 dog 或 cat 的实例。
- 用户可以调用 isDogEmpty 方法,检查队列中是否还有 dog 的实例。
- 用户可以调用 isCatEmpty 方法,检查队列中是否还有 cat 的实例。
-
解题思路:
本题实现将不同的实例盖上时间戳的方法,但是又不能改变用户本身的类,所以定义一个新的类 PetEnterQueue,属性包括实例和一个计数值(也就是插入队列的序号值)。
具体的猫狗队列实现逻辑为:
- 在加入实例时,如果实例是 dog,就盖上时间戳,生成对应的 PetEnterQueue 类的实例,然后放入 dogQ;如果实例是 cat,就盖上时间戳,生成对应的 PetEnterQueue 类的实例,然后放入 catQ 中。
- 弹出方法只需要先比较两个队列的队列头元素序号,小的先弹出(如果此时两个队列都不为空)。
- 判断是否有实例方法实现较为简单,本身队列就有类似的实现方法,只需要调用即可。
-
代码实现:
/** * @Author: milkteazzz * @Data: 2020-12-19 10:12 * @Version: 1.0 * * 宠物类中间队列,属性包含实例以及数量(这里是序号的意思,用来标明顺序用) */ public class PetEnterQueue { private Pet pet; private long count; public PetEnterQueue(Pet pet, long count){ this.pet = pet; this.count = count; } public Pet getPet() { return pet; } public long getCount() { return count; } public String getEnterType(){ return pet.getType(); } } /** * @Author: milkteazzz * @Data: 2020-12-19 10:24 * @Version: 1.0 * * 猫狗队列的具体实现 */ public class CatDogQueue { private Queue<PetEnterQueue> dogQ; private Queue<PetEnterQueue> catQ; private long count; public CatDogQueue() { dogQ = new LinkedList<>(); catQ = new LinkedList<>(); count = 0; } public void add(Pet pet){ if (pet.getType().equals("Cat")){ catQ.add(new PetEnterQueue(pet,count++)); }else if (pet.getType().equals("Dog")){ dogQ.add(new PetEnterQueue(pet,count++)); }else { throw new RuntimeException("error,not cat or dog"); } } public Pet pollAll(){ if (!catQ.isEmpty() && !dogQ.isEmpty()){ if (catQ.peek().getCount() < dogQ.peek().getCount()){ return catQ.poll().getPet(); }else { return dogQ.poll().getPet(); } }else if (!catQ.isEmpty()){ return catQ.poll().getPet(); }else if (!dogQ.isEmpty()){ return dogQ.poll().getPet(); }else { throw new RuntimeException("error,queue is empty"); } } public Dog pollDog(){ if (!dogQ.isEmpty()){ return (Dog) dogQ.poll().getPet(); } throw new RuntimeException("error,dog queue is empty"); } public Cat pollCat(){ if (!catQ.isEmpty()){ return(Cat) catQ.poll().getPet(); } throw new RuntimeException("error,cat queue is empty"); } public boolean isEmpty(){ return catQ.isEmpty() || dogQ.isEmpty(); } public boolean isCatEmpty(){ return catQ.isEmpty(); } public boolean isDogEmpty(){ return dogQ.isEmpty(); } }
4. 用一个栈实现对另外一个栈的排序:
-
题目描述:
一个栈中元素的类型为整型,现在想将该栈从顶到底按从大到小的顺序排序,只允许申请一个栈。除此之外,可以申请新的变量,但不能申请额外的数据结构。
-
解题思路:
首先有两个栈,需要排序的栈 stack 和辅助栈 help。
主要思路是:将 stack 中的元素压入 help 的过程中,进行排序压入 help 中,最终再循环压回 stack。
stack 的要求是从顶部到底部是从大到小,而 help 是从小到大。
首先先将 stack 的元素弹出,但是不一定它会立刻压入栈中,于是用一个变量将其存储起来,命名为 cur。于是将 cur 与 help 的栈顶元素进行比较,如果是大于栈顶元素,必须将其压到栈顶以下的位置。这个时候,需要将 help 中小于 cur 的元素压回 stack 中,然后将 cur 压入栈,再将元素压回来,这个时候 stack 是 help 的辅助栈。
-
代码实现:
public static void sortStackByStack(Stack<Integer> stack){ Stack<Integer> help = new Stack<>(); while (!stack.isEmpty()){ int cur = stack.pop(); while (!help.isEmpty() && cur > help.peek()){ stack.push(help.pop()); } help.push(cur); } while (!help.isEmpty()){ stack.push(help.pop()); } }
5. 用栈来实现汉诺塔问题:
-
问题描述:
修改一下规则:现在限制不能从最左侧的塔直接移动到右侧,也不能从最右侧直接移动到最左侧,而是必须经过中间。求当塔有 N 层的时候,打印移动过程和最优移动总步数。
-
解题思路:
塔不能从 “左” 移动到 “右”,也不能从 “右” 移动到 “左”,而是要经过中间过程。也就是说,实际动作只有4个:“左” 到 “中”,“中” 到 “左”,“中” 到 “右”,“右” 到 “中”。
现在我们把左,中,右三个地点抽象成栈,依次记为 LS、MS、RS。最初所有的塔都在 LS 上。那么如上 4 个动作就能看作是:某一个栈(from)把栈顶元素弹出,然后压入到另一个栈里(to),作为这一个栈(to)的栈顶。
这里需要注意有两个原则,这也是需要注意的逻辑:
- 小压大原则:如果将 from 的栈顶元素移动到 to 中,必须保证该元素小于 to 的栈顶元素。
- 不可逆原则:由于小于找到最短的移动过程,即不能出现从 L 到 M,然后下一步又从 M 移动到 L。
- 不可重复原则:由于本身汉诺塔的结构就是 “头轻脚重”,一旦我们将 L 的栈顶移动到 M 中,此时 M 的栈顶元素小于 L 的栈顶元素,M 不可能再一次将元素压到 M 中。
-
代码实现:
public class StackHanoiProblem { public enum Action{ No,LToM,MToL,MToR,RToM; } public int hanoiProblem(int num,String left,String mid,String right){ Stack<Integer> lS = new Stack<>(); Stack<Integer> mS = new Stack<>(); Stack<Integer> rS = new Stack<>(); lS.push(Integer.MAX_VALUE); mS.push(Integer.MAX_VALUE); rS.push(Integer.MAX_VALUE); for(int i = num;i > 0;i--){ lS.push(i); } Action[] record = {Action.No}; int step = 0; while (rS.size() != num + 1){ //由于不可重复和不可逆原则,一个动作的下一步动作只可能对应两个 step += fStackForStack(record,Action.MToL,Action.LToM,lS,mS,left,mid); step += fStackForStack(record,Action.LToM,Action.MToL,mS,lS,mid,left); step += fStackForStack(record,Action.RToM,Action.MToR,mS,rS,mid,right); step += fStackForStack(record,Action.MToR,Action.RToM,rS,mS,right,mid); } return step; } public static int fStackForStack(Action[] record,Action preNoAct,Action nowAck, Stack<Integer> fStack, Stack<Integer> tStack,String from,String to){ //上一步与这一步不能重复,并且不能大压小 if (record[0] != preNoAct && fStack.peek() < tStack.peek()){ tStack.push(fStack.pop()); System.out.println("Move "+tStack.peek()+" from "+from+" to "+to); record[0] = nowAck; return 1; } return 0; } }