数据结构与算法之美之栈、队列和递归

穿精又带淫゛_ 提交于 2019-12-22 00:40:18

栈既可以用数组来实现,也可以用链表来实现。用数组实现的栈,我们叫作顺序栈,用链表实现的栈,我们叫作链式栈

实现方法:

package com.smao.leetcode;

/**
 * 数组式链表
 */
public class ArrayStack {
    private String[] arrayStack;
    private int size;//栈中元素个数
    private int count;//栈的容量

    public ArrayStack(int count) {
        this.arrayStack = new String[count];
        this.count = count;
        this.size = 0;
    }

    public boolean push(String item){
        if(this.count == this.size){
            String[] arrayStackNew = new String[this.count*2];
            for(int i=0;i<arrayStack.length;i++){
                arrayStackNew[i] = arrayStack[i];
                this.count = this.count*2;
            }
            this.arrayStack = arrayStackNew;
        }
        arrayStack[this.size] = item;
        this.size++;
        return true;
    }

    public String pop(){
        if(this.size==0){
            return null;
        }
        String temp = arrayStack[size-1];
        size--;
        return temp;
    }

    public static void main(String[] args) {
        ArrayStack arrayStack = new ArrayStack(10);
        arrayStack.push("1");
        arrayStack.push("2");
        arrayStack.push("3");
        arrayStack.push("4");
        arrayStack.push("5");
        System.out.println(arrayStack.size);
        arrayStack.push("1");
        arrayStack.push("2");
        arrayStack.push("3");
        arrayStack.push("4");
        arrayStack.push("5");
        arrayStack.push("5");
        arrayStack.pop();
        System.out.println(arrayStack.size);
    }
}
package com.smao.leetcode;

import com.cc.common.base.ListNode;

/**
 * 链表实现栈
 */
public class ListNodeStack {
    private ListNode listNode;
    private int size;//栈内元素个数
    private ListNode end;//结尾
    private ListNode pre;//end前面节点

    public ListNodeStack(int root) {
        this.listNode = new ListNode(root);
        this.end = this.listNode;
        this.pre = null;
        this.size++;
    }

    public boolean push(int node){
        this.pre = this.end;
        this.end.next = new ListNode(node);
        this.end = end.next;
        this.size++;
        return true;
    }

    public void pop(){
        this.end = pre;
        this.pre.next = null;
        this.size--;
    }

    public static void main(String[] args) {
        ListNodeStack listNodeStack = new ListNodeStack(1);
        listNodeStack.push(2);
        listNodeStack.push(3);
        listNodeStack.push(4);
        listNodeStack.push(5);
        System.out.println(listNodeStack.size);
        listNodeStack.pop();
        System.out.println(listNodeStack.size);
    }
}

对于出栈操作来说,我们不会涉及内存的重新申请和数据的搬移,所以出栈的时间复杂度仍然是O(1)。但是,对于入栈操作来说,情况就不一样了。当栈中有空闲空间时,入栈操作的时间复杂度为O(1)。但当空间不够时,就需要重新申请内存和数据搬移,所以时间复杂度就变成了O(n)。把耗时多的入栈操作的时间均摊到其他入栈操作上,平均情况下的耗时就接近O(1)。

课后思考
1. 我们在讲栈的应用时,讲到用函数调用栈来保存临时变量,为什么函数调用要用“栈”来保存临时变量呢?用其他数据结构不行吗?

其实,我们不一定非要用栈来保存临时变量,只不过如果这个函数调用符合后进先出的特性,用栈这种数据结构来实现,是最顺理成章的选择。
从调用函数进入被调用函数,对于数据来说,变化的是什么呢?是作用域。所以根本上,只要能保证每进入一个新的函数,都是一个新的作用域就可以。而要实现这个,用栈就非常方便。在进入被调用函数的时候,分配一段栈空间给这个函数的变量,在函数结束的时候,将栈顶复位,正好回到调用函数的作用域内。(仅供参考)

2. 我们都知道, JVM内存管理中有个“堆栈”的概念。栈内存用来存储局部变量和方法调用,堆内存用来存储Java中的对象。那JVM里面的“栈”跟我们这里说的“栈”是不是一回事呢?如果不是,那它为什么又叫作“栈”呢?
内存中的堆栈和数据结构堆栈不是一个概念,可以说内存中的堆栈是真实存在的物理区,数据结构中的堆栈是抽象的数据存储结构。
内存空间在逻辑上分为三部分:代码区、静态数据区和动态数据区,动态数据区又分为栈区和堆区。
代码区:存储方法体的二进制代码。高级调度(作业调度)、中级调度(内存调度)、低级调度(进程调度)控制代码区执行代码的切换。
静态数据区:存储全局变量、静态变量、常量,常量包括final修饰的常量和String常量。系统自动分配和回收。
栈区:存储运行方法的形参、局部变量、返回值。由系统自动分配和回收。
堆区: new一个对象的引用或地址存储在栈区,指向该对象存储在堆区中的真实数据。(仅供参考)

队列

先进先出

使用数组实现队列

package com.smao.leetcode.queue;

/**
 * 用数组实现一个队列
 */
public class ArrayQueue {
    private String[] queue;
    private int head;
    private int tail;
    private int n;

    public ArrayQueue(int count) {
        this.queue = new String[count];
        n = count;
    }
    public boolean push(String num){
        if(n==tail){
            if(head==0){
                return false;
            }
            for(int i=head;i<tail;++i){
                queue[i-head] = queue[i];
            }
            head = 0;
            tail-=head;
        }
        queue[tail] = num;
        ++tail;
        return true;
    }
    public String pop(){
        if(head==tail){
            return null;
        }
        String item = queue[head];
        ++head;
        return item;
    }

    public static void main(String[] args) {
        ArrayQueue arrayQueue = new ArrayQueue(10);
        arrayQueue.push("1");
        System.out.println(arrayQueue.pop());
        arrayQueue.push("2");
        System.out.println(arrayQueue.pop());
        arrayQueue.push("3");
        System.out.println(arrayQueue.pop());
        arrayQueue.push("4");
        System.out.println(arrayQueue.pop());
        arrayQueue.push("5");
        arrayQueue.push("1");
        arrayQueue.push("2");
        arrayQueue.push("3");
        arrayQueue.push("4");
        arrayQueue.push("5");
        arrayQueue.push("5");
    }
}

使用链表实现队列

package com.smao.leetcode.queue;

import com.cc.common.base.ListNode;

/**
 * 用链表实现一个队列
 */
public class ListNodeQueue {
    private ListNode listNode;
    private ListNode tail;
    private ListNode head;

    public ListNodeQueue(int root) {
        listNode = new ListNode(root);
        tail = listNode;
        head = listNode;
    }

    public void push(int num){
        tail.next = new ListNode(num);
        tail = tail.next;
    }

    public Integer pop(){
        int temp = head.val;
        head = head.next;
        return temp;
    }
    public static void main(String[] args) {
        ListNodeQueue listNodeQueue = new ListNodeQueue(0);
        listNodeQueue.push(1);
        listNodeQueue.push(2);
        listNodeQueue.push(3);
        listNodeQueue.push(4);
        listNodeQueue.push(5);
        listNodeQueue.push(6);
        listNodeQueue.push(7);
        listNodeQueue.push(8);
        listNodeQueue.push(9);
        listNodeQueue.push(10);
        listNodeQueue.push(11);
        listNodeQueue.pop();
    }
}

实现循环队列(当队满时, (tail+1)%n=head)

package com.smao.leetcode.queue;

/**
 * 循环队列
 */
public class CircularQueue {
    public String[] item;
    public int head;
    public int tail;
    public int n;

    public CircularQueue(int count) {
        this.n = count;
        item = new String[count];
    }

    public boolean push(String str){
        if((tail+1)%n == head){
            return false;
        }
        item[tail] = str;
        tail = (tail+1)%n;
        return true;
    }

    public String pop(){
        if(head == tail){
            return null;
        }
        String str = item[head];
        head = (head+1)%n;
        return str;
    }
}

阻塞队列

就是在队列为空的时候,从队头取数据会被阻塞。因为此时还没有数据可取,直到队列中有了数据
才能返回;如果队列已经满了,那么插入数据的操作就会被阻塞,直到队列中有空闲位置后再插入数据,然后再返回。

递归

只要同时满足以下三个条件,就可以用递归来解决。
1.一个问题的解可以分解为几个子问题的解
2.这个问题与分解之后的子问题,除了数据规模不同,求解思路完全一样
3.存在递归终止条件
递归需要注意的问题:
1、递归代码要警惕堆栈溢出
函数调用会使用栈来保存临时变量。每调用一个函数,都会将临时变量封装为栈帧压入内存栈,等函数执行完成返回时,才出栈。系统栈或者虚拟机栈空间一般都不大。如果递归求解的数据规模很大,调用层次很深,一直压入栈,就会有堆栈溢出的风险。
2、递归代码要警惕重复计算
可以通过公共变量存储已经计算过的结果
3、怎么将递归代码改写为非递归代码?
递归有利有弊,利是递归代码的表达力很强,写起来非常简洁;而弊就是空间复杂度高、有堆栈溢出的风险、存在重复计算、过多的函数调用会耗时较多等问题。所以,在开发过程中,我们要根据实际情况来选择是否需要用递归的方式来实现。

练习题

package com.smao.leetcode;

import java.util.HashMap;
import java.util.Map;
import java.util.Stack;

/**
 * 给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。
 *
 * 有效字符串需满足:
 *
 * 左括号必须用相同类型的右括号闭合。
 * 左括号必须以正确的顺序闭合。
 * 注意空字符串可被认为是有效字符串。
 *
 * 示例 1:
 *
 * 输入: "()"
 * 输出: true
 * 示例 2:
 *
 * 输入: "()[]{}"
 * 输出: true
 * 示例 3:
 *
 * 输入: "(]"
 * 输出: false
 * 示例 4:
 *
 * 输入: "([)]"
 * 输出: false
 * 示例 5:
 *
 * 输入: "{[]}"
 * 输出: true
 *
 * 链接:https://leetcode-cn.com/problems/valid-parentheses
 */
public class ValidParentheses {
    public static boolean isValid(String s) {
        Stack stack = new Stack();
        Map<Character, Character> map = new HashMap<>();
        map.put(')', '(');
        map.put('}', '{');
        map.put(']', '[');
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            if (stack == null || stack.size()==0) {
                if (map.keySet().contains(c)) {
                    return false;
                } else {
                    stack.push(c);
                    continue;
                }
            }
            if (map.keySet().contains(c)) {
                if(map.get(c)!=stack.pop()){
                    return false;
                }
            }else{
                stack.push(c);
            }
        }
        return stack.size() == 0;
    }

    public static void main(String[] args) {
        System.out.print(isValid("()"));
    }
}
package com.smao.leetcode;

import java.util.HashMap;
import java.util.Map;

/**
 * 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
 *
 * 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
 *
 * 注意:给定 n 是一个正整数。
 *
 * 示例 1:
 *
 * 输入: 2
 * 输出: 2
 * 解释: 有两种方法可以爬到楼顶。
 * 1.  1 阶 + 1 阶
 * 2.  2 阶
 * 示例 2:
 *
 * 输入: 3
 * 输出: 3
 * 解释: 有三种方法可以爬到楼顶。
 * 1.  1 阶 + 1 阶 + 1 阶
 * 2.  1 阶 + 2 阶
 * 3.  2 阶 + 1 阶
 *
 * 来源:力扣(LeetCode)
 * 链接:https://leetcode-cn.com/problems/climbing-stairs
 * 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
 */
public class ClimbingStairs {
    public static int climbStairs(int n) {
        int[] array = new int[n+1];
        return clambStairsNew(n,array);
    }

    private static int clambStairsNew(int n,int[] array) {
        if(n==1){
            return 1;
        }
        if(n==2){
            return 2;
        }
        if(array[n]!=0){
            return array[n];
        }
        array[n] = clambStairsNew(n-1,array)+clambStairsNew(n-2,array);
        return array[n];
    }

    public static void main(String[] args) {
        System.out.println(climbStairs(44));
    }
}

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!