前言
又开始学习新的一本书了,这本书《程序员代码面试指南:IT名企算法与数据结构题目最优解》 左程云写的。 讲述的是 一些常见的面试题的最优的解法,我粗略的看了下,我觉得这个不只是面试题。而真的觉得这些解题的思路很有用,能对基础知识有一个更深入的了解。
我也买了一本数据结构,相信大家也买过,无奈里面的各种公式啥的太专业了。看不太懂。还好这本书又是讲数据结构。还是用java来解题的。比较不错。我决定每天学习一道题。
一些基础知识
看了一些文章,说得都很专业。自己也不能太理解,简单的来说:
- 时间复杂度: 循环执行语句的次数(应该是时间长短?)
- 空间复杂度: 占用的空间大小
[前几天写得了,今天才整理笔记思路2015-11-07]
1. 设计一个有getMin功能的栈
题目:实现一个特殊的栈顶,在实现栈的pus基本功能的基础上,再实现返回栈中最小元素的操作,
要求:
- pop、push、getMin 操作时间复杂度都是O(1).
- 设计的栈类型可以使用现成的栈结构
难度:士★☆☆☆
思路:
其实最该题最主要的功能就在怎么来取得最小的值。
首先栈的数据结构是:先进后出。我们要统计栈中最小的值,很容易想到的一个办法就是 取出的时候排序。不过这样就特别的耗费时间,太low了。
书上给出的思路是:使用两个栈,一个栈存储正常的 数据。 一个栈用来存储每一步的最小的值。那么问题来了,怎么存储“每一步的最小的值呢?” 为什么要存储每一步最小的值呢?不能在压入的时候就一直拿着这个最小的值么? 那么就有这么一个问题。栈中的元素是可以压入弹出的,所以你得保证每一步的操作。你手中最小的值是 栈中存在的元素。 所以就得来计算“每一步的最小的值”
压入省空间,弹出费时间
/**
* Created by zhuqiang on 2015/10/30 0030.
*/
public class Client {
public static void main(String[] args) {
MyStack1 myStack = new MyStack1();
myStack.push(3);
myStack.push(4);
myStack.push(5);
myStack.push(1);
myStack.push(2);
myStack.push(1);
System.out.println(myStack.getMin());
myStack.pop();
System.out.println(myStack.getMin());
myStack.pop();
System.out.println(myStack.getMin());
myStack.pop();
System.out.println(myStack.getMin());
}
}
// 时间复杂度为o(1),空间复杂度为o(n)
class MyStack1 {
private Stack<Integer> stackData = new Stack<>();
private Stack<Integer> stackMin = new Stack<>();
/** 压入 **/
public void push(int newNum) { //压入省空间
if (stackMin.empty()) { // 首次压入的时候,由于都为空,所以把记录最小值的stack压入一个初始值
stackMin.push(newNum);
}else if (newNum <= getMin()) { //如果再次压入的值,比 stackMin栈顶的值还要小,或则等于,就压入min中(为什么需要等于呢,是为了 弹出的时候,同步弹出min中与之相等的值)
stackMin.push(newNum);
}
stackData.push(newNum);
}
/** 弹出 **/
public int pop() { //弹出废时间
if (stackData.empty()) {
throw new RuntimeException("stack is empty");
}
int value = stackData.pop();
if (value == getMin()) { // 如果弹出的值 与栈顶的值相等,则移除min中的栈顶值(push的时候判断了 小于等于,就是为了这里确保min中最小的值是stackData中存在的值)
stackMin.pop();
}
return value;
}
/** 获取最小值 **/
public int getMin() {
if (stackMin.empty()) {
throw new RuntimeException("stack is empty");
}
return stackMin.peek();
}
}
运行结果
1
1
1
3
结果说明
- 依次压入:3、4、5、1、2、1。获取栈中最小值:应该是1。
- 弹出栈顶元素,应该弹出的是1,现在栈中元素有3、4、5、1、2。获取栈中最小值:应该还是1
- 弹出栈顶元素,应该弹出的是2,现在栈中元素有3、4、5、1。获取栈中最小值:应该还是1
- 弹出栈顶元素,应该弹出的是1,现在栈中元素有3、4、5。获取栈中最小值:应该是3
上面的步骤,和要求的一致,达到了效果。
压入费空间,弹出省时间
// 压入费空间,弹出省时间
class MyStack2 {
private Stack<Integer> stackData = new Stack<>();
private Stack<Integer> stackMin = new Stack<>();
/** 压入 **/
public void push(int newNum) { //压入废时间
if (stackMin.empty()) {
stackMin.push(newNum);
}else if (newNum <= getMin()) {
stackMin.push(newNum);
}else{
stackMin.push(getMin()); //如果是大于的话,就把当前最小的重复压入,
}
stackData.push(newNum);
}
/** 弹出 **/
public int pop() { //弹出废时间
if (stackData.empty()) {
throw new RuntimeException("stack is empty");
}
stackMin.pop(); //直接弹出,因为在push的时候同步了对应的每一个操作。所以不用判断
return stackData.pop();
}
/** 获取最小值 **/
public int getMin() {
if (stackMin.empty()) {
throw new RuntimeException("stack is empty");
}
return stackMin.peek();
}
}
点评
上面两种实现方式,大体都差不多,区别在于,压入和弹出有区别。
- 第一次压入数据的时候,由于minStack中没有数据,就作为初始值同步压入到两个栈中
- 第二次压入数据的时候,会和minStack中栈顶的值进行判断,如果小于就把这个数压入minStack中
- 在移除的时候,需要同步的判断 移除的值是否和minStack栈顶的值是否相等。是 就同步移除。这样才能保证,你手中最小的元素 是栈中存在的值。
上面是 压入省空间,弹出费时间 的实现思路,对于这个“压入省空间,弹出费时间”的定义,得有比较,然后对比来看:
- 压入省空间:压入minStack的时候,示例1,只判断了小于的最小值时候才压入,而示例2中是每一步都压入了元素
- 弹出费时间:弹出Stack的时候,因为要同步的操作minStack,示例1,要判断是否等于当前弹出的值。 而示例2:不需要判断,直接弹出。
[2015-11-08]
由两个栈组成的队列
题目:编写一个类,用两个栈实现队列,支持队列的基本操作(add、poll、peek)
难度:尉★★☆☆☆
思路:
首先栈的特点是,先进后出,而队列的特点是,先进先出,两个栈,正好把顺序颠倒过来。
难点:颠倒的时机是什么?存储的话怎么存储?取出后又怎么操作,才能保证取出后又压入了数据,再取出的时候顺序是正确的。 这个是我想自己实现这个要求的时候想到的问题。然后就是为了这个顺序。我就彻底没有招了。看了书上的解答才想明白。看下面的图:(把图片拖动在浏览器另外的窗口打开就能看见大图了
)
- 我们依次压入5、4、3
- 当要弹出的时候,会先判断
压入栈
中是否为空,为空的时候,才把压入栈
中的数据弹出并压入弹出栈
中(压入顺序3、4、5)。刚好倒过来了。 - 从
弹出栈
弹出数据,弹出了5. - 如果,这个时候 我再压入一个数字2,那么此时的
压入栈
中,其实已经没有数据了。而弹出栈
中还有4、3的元素。(压入栈
存在的元素:2) - 如果,这个时候弹出一个元素,
弹出栈
中有数据,所以直接弹出了4.
其实到这里,应该就明白了。队列的特点是 先进先出。而栈的特点的先进后出。 最容易迷糊的地方其实就是,压入数据后,弹出数据,再压入好像保存弹出的数据挺困难的。 其实就是这里。 看了上面的分析之后。 因为 5被取走了。剩下的就是4、3,而这两个数据是最早进来的。和后面压入的数据没有半毛钱关系。所以都不需要去理会顺序的问题。要把图上的两个栈看成一个整体。
也就是说要做到顺序不乱,就得保证以下两点,就不会出错了:
- 如果
压入栈
要往弹出栈
压入数据,要一次性把压入栈
的数据弹出完。 - 如果
弹出栈
不为空,是不能往弹出栈压入数据的。
/**
* @author zhuqiang
* @version V1.0
* @date 2015/11/8 21:43
*/
public class Client {
public static void main(String[] args) {
TwoStacksQueue tsq = new TwoStacksQueue();
tsq.add(5);
tsq.add(4);
tsq.add(3);
System.out.println(String.format("预期弹出:%s,实际弹出:%s",5,tsq.poll()));
System.out.println(String.format("预期弹出:%s,实际弹出:%s",4,tsq.peek()));
tsq.add(2);
System.out.println(String.format("预期弹出:%s,实际弹出:%s",4,tsq.peek()));
}
}
class TwoStacksQueue{
private Stack<Integer> pushStack = new Stack<>(); //压入栈
private Stack<Integer> popStack = new Stack<>(); //弹出栈
public void add(int pushInt){
pushStack.push(pushInt);
}
// 弹出栈顶元素,并移除
public int poll(){
if(pushStack.empty() && popStack.empty()){
throw new RuntimeException("Queue is empty!");
}else if(popStack.empty()){ //弹出栈为空的时候,才把压入栈的元素往弹出栈中取出并压入到弹出栈中
while(!pushStack.empty()){
popStack.push(pushStack.pop());
}
}
return popStack.pop();
}
//弹出栈顶元素,不移除
public int peek(){
if(pushStack.empty() && popStack.empty()){
throw new RuntimeException("Queue is empty!");
}else if(popStack.empty()){ //弹出栈为空的时候,才把压入栈的元素往弹出栈中取出并压入到弹出栈中
while(!pushStack.empty()){
popStack.push(pushStack.pop());
}
}
return popStack.peek();
}
}
运行结果
预期弹出:5,实际弹出:5
预期弹出:4,实际弹出:4
预期弹出:4,实际弹出:4
[2015-11-09]
如何只用递归函数和栈操作逆序一个栈
题目:一个栈依次压入1、2、3、4、5,那么栈顶到栈底分别为:5、4、3、2、1。将这个栈转置后,从栈顶到栈底为:1、2、3、4、5,也就是实现栈中元素的逆序,但是只能用递归函数来实现,不能用其他数据结构。
难度:尉★★☆☆☆
思路:
- 只能用站statck操作和递归函数,不能用其他的操作。
- 那么就只能想法把栈中的数据全部弹出,然后再压入。
- 取出的时候,顺序是从栈底取出(递归的取出 放到内存中)
- 再次压入栈中的时候,从内存中递归的压入回去。
上面的思路。光看这几点,肯定是迷糊的。先看一个总体的思路图。再看具体的递归函数运行流程图。
(把图片拖动在浏览器另外的窗口打开就能看见大图了
)
- 入口函数是从
递归函数二
开始 - 在
函数二
中,调用函数一
,获得栈底的值。然后再递归调用获取栈顶底的值。结束递归的条件是:stack中被取空了 - 在
函数二
中递归结束的时候,逐级往上返回的时候,首先要明白的是,在递归结束的时候,我们拿到的值是:3.然后在这个时机压入被取空的stack中。 就造成了上图中的流程。
/**
* @author zhuqiang
* @version V1.0
* @date 2015/11/9 22:23
*/
public class Client {
public static void main(String[] args) {
Stack<Integer> stack = new Stack<>();
stack.push(1);
stack.push(2);
stack.push(3);
stack.push(4);
stack.push(5);
reverse(stack);
System.out.println("*********** 打印反转之后栈中的顺序");
while (!stack.empty()){
System.out.println(stack.pop());
}
}
// 获取并移除栈底的值
public static int getAndRemoveLastElement(Stack<Integer> stack){
Integer result = stack.pop();
if(stack.empty()){ //结束条件,栈为空
return result;
}else{
int last = getAndRemoveLastElement(stack);
stack.push(result); //递归结束的时候,除了栈底的元素都会被再次重新被压入statck中
return last; //返回了 栈底的值
}
}
public static void reverse(Stack<Integer> stack){
if(stack.empty()){
return;
}
int i = getAndRemoveLastElement(stack); //获取一个栈底的值,暂时不做处理(就相当于放在了方法内存中)
reverse(stack); //递归的获取栈底的值
stack.push(i); //在这个递归中,最后一层拿到的值,肯定是之前栈顶的值(也就是说,这里压入的顺序其实就相当于:把之前的栈循环的弹出,然后又压入了另外一个栈)
}
}
运行结果
*********** 打印反转之后栈中的顺序
1
2
3
4
5
可以看到,最先取出的是1,要知道栈的特性是:先进后出。之前栈先出来的应该是最后一个被压入的5,所以这个结果是符合题目要求的。
贴上书上的两张图,画的太好了。理解下两个递归函数的处理流程。
getAndRemoveLastElement 获取并移除栈底的值
reverse 反转
[2015-11-25]
猫狗队列
题目:
宠物、猫、狗类如下:
public class Pet {
private String type; //类型
public Pet(String type) {
this.type = type;
}
public String getType() {
return type;
}
}
public class Cat extends Pet{
public Cat() {
super("cat");
}
}
public class Dog extends Pet {
public Dog() {
super("dog");
}
}
实现一种猫狗队列的结构,要求如下:
- 用户可以调用add方法将cat活dog类的实例放入队列中
- 用户可以调用pollAll方法,将队列中所有的实例按照先进先出的原则弹出。
- 用户可以调用pollDog方法,将队列中dog类的实例按照进队列的顺序弹出
- 用户可以调用pollCat方法,将队列中cat类的实例按照进队列的顺序弹出
- 用户可以调用isEmpty方法,判断队列是否为空
- 用户可以调用isDogEmpty方法,判断队列中的dog是否为空
- 用户可以调用iseCatEmpty方法,判断队列中的cat是否为空
难度:士★☆☆☆☆
思路:
看题目中,有区别对待的,可以使用两个队列来分别存放,要解决的问题就是:先后顺序怎么判定
好了,还有其他的要求,说不能修改给出的类的结构。我们要判断先后顺序,也就是要在取出的时候判断dog先放进去还是cat先放进去,那么就可以使用 一个计数器来解决。
总结思路:使用创建自定义队列,分别用两个队列来存放dog和cat,但是进入队列的不是pet类,而是我们自己包装的pet类,里面有一个计数器。用来判定先后顺序。
/**
* 进入我们的队列的,实体包装类
* @author zhuqiang
* @version V1.0
* @date 2015/11/22 20:55
*/
public class PetEnter {
private long count;
private Pet pet;
public PetEnter(Pet pet, long count) {
this.pet = pet;
this.count = count;
}
public long getCount() {
return count;
}
public Pet getPet() {
return pet;
}
public String getType(){
return pet.getType();
}
}
/**
* @author zhuqiang
* @version V1.0
* @date 2015/11/22 20:58
*/
public class DogCatQueue {
private Queue<PetEnter> dog = new LinkedList<>();
private Queue<PetEnter> cat = new LinkedList<>();
private long count = 0;
public void add(Pet pet){
if(pet == null){
throw new IllegalArgumentException("不能插入空元素");
}else if("dog".equals(pet.getType())){
dog.add(new PetEnter(pet,count++));
}else if("cat".equals(pet.getType())){
cat.add(new PetEnter(pet,count++));
}else {
throw new IllegalArgumentException("不支持的类型");
}
}
//依次弹出所有的元素:看题目我也不知道弹出all是什么意思,看了实现才知道,是在猫和狗的队列中按照先进显出的顺序弹出一个,
public Pet pollAll(){
if(!dog.isEmpty() && !cat.isEmpty()){
if(dog.peek().getCount() > cat.peek().getCount()){
return cat.poll().getPet();
}else{
return dog.poll().getPet();
}
}else if(!dog.isEmpty()){
return dog.poll().getPet();
}else if(!cat.isEmpty()){
return cat.poll().getPet();
}else {
throw new RuntimeException("队列为空");
}
}
public Pet pollDog(){
if(!dog.isEmpty()) {
return dog.poll().getPet();
}else {
throw new RuntimeException("队列为空");
}
}
public Pet pollCat(){
if(!cat.isEmpty()) {
return cat.poll().getPet();
}else {
throw new RuntimeException("队列为空");
}
}
//检测队列是否为空
public boolean isEmpty(){
return dog.isEmpty() && cat.isEmpty();
}
public boolean isDogEmpty(){
return dog.isEmpty();
}
public boolean isCatEmpty(){
return cat.isEmpty();
}
}
public class Client {
public static void main(String[] args) {
DogCatQueue dq = new DogCatQueue();
dq.add(new Dog());
dq.add(new Dog());
System.out.println(String.format("队列是否为空:%s",dq.isEmpty()));
dq.add(new Cat());
System.out.println(String.format("取出一个dog=%s",dq.pollDog().getType()));
dq.add(new Cat());
System.out.println(String.format("pollAll,期望弹出dog,实际弹出:%s",dq.pollAll().getType()));
System.out.println(String.format("isDogEmpty,期望为true,实际=%s",dq.isDogEmpty()));
System.out.println(String.format("pollCat,期望为cat,实际=%s",dq.pollCat().getType()));
}
}
运行结果
队列是否为空:false
取出一个dog=dog
pollAll,期望弹出dog,实际弹出:dog
isDogEmpty,期望为true,实际=true
pollCat,期望为cat,实际=cat
[2015-11-24]
用一个栈实现另一个栈的排序
题目:一个栈中元素为整型,现在想将该栈从栈顶到底按大到小的顺序排序,指许申请一个栈,除此之外,可以申请新的变量,但是不能申请其他的数据结构。
难度:士★☆☆☆☆
我看到这个题的时候,想的是:不能用其他的数据结构,只能在内存中交换,用如何只用递归函数和栈操作逆序一个栈
这个来做,没办法没有任何思路。
思路:
将排序的栈标记为statck,辅助栈为help,在stack上执行pop操作,弹出的元素比标记为cur。
- 如果cur小于或等于help栈顶的元素,则将cur直接压入 help;
- 如果cur大于help栈顶的元素,则将help的元素逐个弹出并压入statc中,再将cur压入help中。
经过上面两个步骤的操作,其实就是说,只要有最大的元素出现,那么help中的元素都将被弹出(并且是按照小到大的顺序弹出),然后压回stack中,也就是说,help中栈底的元素始终是最大的,栈顶始终是最小的。只能这样,在排序完成之后,压回stack的时候,是按照从从小到大的顺序压入,那么栈顶到栈底就是按照大到小的顺序排列的,取出来的时候就是降序排列。
难就难在,栈的先进后出的顺序,被颠倒了好几次。但是最终的目的是,help中是栈顶到底的顺序是:从小到大。
/**
* @author zhuqiang
* @version V1.0
* @date 2015/11/24 21:28
*/
public class Client {
public static void main(String[] args) {
Stack<Integer> stack= new Stack();
stack.push(1);
stack.push(5);
stack.push(3);
stack.push(2);
Stack<Integer> help = new Stack();
while (!stack.isEmpty()){
int cur = stack.pop();
while (!help.isEmpty() && help.peek() <= cur){ //升序 <= 将序。最后的结果
stack.push(help.pop());
}
help.push(cur);
}
while (!help.isEmpty()){
stack.push(help.pop());
}
}
}