算法学习——栈和队列的操作(队列实现栈,栈实现队列)

栈的特性:先进后出,后进先出

队列的特性: 先进先出,后进后出

图的宽度有限遍历用栈来实现 => 相当于用栈实现队列

先上代码


/**
 * 用于栈实现队列
 * @author hanhuafeng
 * @version V1.0
 * @description
 * @date 2020/11/15
 */
public class Code06_TwoStacksImplementQueue {
    /**
     * 先定义两个栈,模拟出系统给出的栈方法
     */
    private Stack<Integer> stackPush;
    private Stack<Integer> stackPop;


    /**
     * 构造方法构建
     */
    public Code06_TwoStacksImplementQueue(){
        stackPush = new Stack<>();
        stackPop = new Stack<>();
    }

    /**
     * 将stackPush栈中的数据倒入StackPop栈中
     * 实现了把stackPush栈中的数据重新倒序
     * 此方法将会被用在pop、push、peak方法中
     */
    public void importStackPushDataToStackPop(){
        if (stackPop.isEmpty()){
            while (!stackPush.isEmpty()){
                stackPop.push(stackPush.pop());
            }
        }
    }

    /**
     * 实现队列add的方式
     * 每次都会去检查一次stackPop栈是否为空,如果为空则把数据全部吐进去
     * @param i 用户给定的入队列的参数
     */
    public void add(Integer i){
        stackPush.push(i);
        importStackPushDataToStackPop();
    }

    /**
     * 实现队列poll方法
     * 每次都会去检查一次stackPop栈是否为空,如果为空则把数据全部吐进去
     * @return
     */
    public Integer poll(){
        // 两个栈是否都是空的,如果都是空的,则直接提示用户
        if(stackPush.isEmpty() && stackPop.isEmpty()){
            throw new RuntimeException("stack is empty");
        }
        importStackPushDataToStackPop();
        return stackPop.pop();
    }

    /**
     * 实现队列的peak方法
     * 从队列头部删除一个元素
     * @return
     */
    public Integer peek(){
        // 两个栈是否都是空的,如果都是空的,则直接提示用户
        if(stackPush.isEmpty() && stackPop.isEmpty()){
            throw new RuntimeException("stack is empty");
        }
        importStackPushDataToStackPop();
        return stackPop.peek();
    }

    public static void main(String[] args) {
        Code06_TwoStacksImplementQueue test = new Code06_TwoStacksImplementQueue();
        test.add(12);
        test.add(11);
        test.add(10);
        test.add(9);
        test.add(8);
        Queue<Integer> queue = new LinkedList();
        queue.add(12);
        queue.add(11);
        queue.add(10);
        queue.add(9);
        queue.add(8);
        System.out.println(test.poll().equals(queue.poll()));
        System.out.println(test.poll().equals( queue.poll()));
        System.out.println(test.peek().equals(queue.peek()));
    }
}

实现思路:

栈的特性为【先进后出,后进先出】,而队列的特性为【先进先出,后进后出】,这两者相当于反了一下,所以采用两个栈,相互照应,就可以实现出负负得正的效果。
两个栈,一个为push栈,一个为pop栈,push栈用于接收用户输入的数值,pop栈用于输出数据
每一次执行push、pop和week操作的时候,都去判断一次pop栈是否为空,如果为空才能把push栈中的数据全部压入pop栈,具体可看下图。
在这里插入图片描述

如图,把4,7,6压入右边的posStack中,此时会造成的记过便是:理论上下一位需要poll出的是2,而因为popStack没有清空便压入,造成了poll出了最后一位6
当然,pushStack栈需要把所有的数据一次性压入,否则会造成数据错乱,具体看下图
在这里插入图片描述

此时如果用户又新增了一个数字,例如3,导致pushStack变成如下的样子在这里插入图片描述
所以又造成了数据的错乱,理论上要poll出4,但是poll出了3


图的深度有限遍历用队列实现 => 相当于用队列实现栈

先上代码:

/**
 * 题目:给定队列的一切方法,如何实现栈
 * 具体思路:
 * 通过两个队列互相倒入数据达到实现栈的目的
 * @author hanhuafeng
 * @version V1.0
 * @description
 * @date 2020/11/15
 */
public class Code_TwoQueuesImplementStack {
    /**
     * 先定义两个队列,模拟出系统给出的队列方法
     */
    private Queue<Integer> pushQueue;
    private Queue<Integer> popQueue;


    /**
     * 构造方法构建
     */
    public Code_TwoQueuesImplementStack(){
        pushQueue = new LinkedList<>();
        popQueue = new LinkedList<>();
    }

    /**
     * 先把数据压入pushQueue中
     * 再把popQueue中的全部数据压入pushQueue
     * 再把pushQueue重新压回popQueue
     * 此方法将会被用在pop、push、peak方法中
     */
    public void importStackPushDataToStackPop(){
        if (!popQueue.isEmpty()){
            while (!popQueue.isEmpty()){
                pushQueue.add(popQueue.poll());
            }
        }
        while (!pushQueue.isEmpty()){
            popQueue.add(pushQueue.poll());
        }
    }

    /**
     * 实现队列add的方式
     * 每次都会去检查一次stackPop栈是否为空,如果为空则把数据全部吐进去
     * @param i 用户给定的入队列的参数
     */
    public void push(Integer i){
        pushQueue.add(i);
        importStackPushDataToStackPop();
    }

    /**
     * 实现栈的出栈pop方法
     * 如果有数据,则直接吐就行了
     * @return
     */
    public Integer pop(){
        // 检测pop队列是否是空的,如果是空的,则直接提示用户
        if(popQueue.isEmpty()){
            throw new RuntimeException("stack is empty");
        }
        return popQueue.poll();
    }

    /**
     * 实现栈的peak方法
     * 从队列头部弹出一个元素,不删除
     * @return
     */
    public Integer peek(){
        // 两个栈是否都是空的,如果都是空的,则直接提示用户
        if(popQueue.isEmpty()){
            throw new RuntimeException("stack is empty");
        }
        return popQueue.peek();
    }

    public static void main(String[] args) {
        Code_TwoQueuesImplementStack test = new Code_TwoQueuesImplementStack();
        test.push(12);
        test.push(11);
        test.push(10);
        test.push(9);
        test.push(8);
        Stack<Integer> stack = new Stack<>();
        stack.push(12);
        stack.push(11);
        stack.push(10);
        stack.push(9);
        stack.push(8);
        System.out.println(test.pop().equals(stack.pop()));
        System.out.println(test.pop().equals( stack.pop()));
        System.out.println(test.pop().equals( stack.pop()));
        System.out.println(test.pop().equals( stack.pop()));
        System.out.println(test.peek().equals(stack.peek()));
    }
}

具体的实现思路请看下面的解释图

 数据只会从popQueue中取出,  
 pushQueue只负责了用户数据一开始的压入
 以及数据从popQueue重新导回来再把全部数据导出只popQueue。

单链表的实现,实现单链表的反转和删除

先上代码:

/**
 * 实现单向链表
 * 链表的特性为当前节点存有下一节点的地址信息
 * 本代码主要是实现单链表的反转和删除
 * @author hanhuafeng
 * @version V1.0
 * @date 2020/11/19
 */
public class Code_SingleLinkedList {
    public static class Node {
        private int value;
        private Node next;
        private Node(int data) {
            value = data;
        }
        private int getValue() {
            return value;
        }
        private Node getNext() {
            return next;
        }
        private void setNext(Node next) {
            this.next = next;
        }
        @Override
        public String toString() {
            return "Node{" +
                    "value=" + value +
                    ", next=" + next +
                    '}';
        }
    }

    /**
     * 反转的方法
     * 用户给出头部的Node节点,通过指针替换的方式,进行反转
     * @author hanhuafeng
     * @date : 22:10 2020/11/19
     * @param head 头部指针
     * @return 返回已经反转好的链表
     */
    public static Node reverse(Node head){
        Node pre = null;
        Node next;
        while (head !=null){
            next = head.getNext();
            head.setNext(pre);
            pre = head;
            head = next;
        }
        return pre;
    }

    /**
     * 删除链表中指定的node节点
     * 需要考虑头节点被删除的情况,所以需要先遍历出头节点
     * @author: hanhuafeng
     * @date : 22:45 2020/11/19
     * @param  value 待删除的值
     * @return 返回已经删除完毕的链表
     */
    private static Node deleteNode(Node head, int value){
        while (head !=null){
            if(head.getValue() != value){
                break;
            }
            head = head.getNext();
        }
        Node now = head;
        Node pre = head;
        while (now != null){
            if(now.getValue() == value){
                pre.setNext(now.getNext());
            }
            pre = now;
            now = now.getNext();
        }
        return head;
    }

    //对数器验证

    /**
     * 生成一个随机的单链表
     * @return
     */
    public static Node randomNode(int maxSize){
        // 生成一个链表长度
        int size = (int)(Math.random()*maxSize)+1;
        // 设置链表中最大的数据为10000
        int maxValue = 10000;
        Node[] nodes = new Node[size];
        for(int i = 0;i<size;i++){
            nodes[i] = new Node((int)(Math.random()*maxValue)+1);
        }
        for (int i =0;i<nodes.length-1;i++){
            nodes[i].setNext(nodes[i+1]);
        }
        return nodes[0];
    }

    /**
     * 使用java原生的方法做一个倒序比较器
     * @param node 需要倒序的node
     * @return
     */
    private static List<Integer> testReverse(Node node){
        // 直接使用java自带的LinkedList
        List<Integer> list = new LinkedList<>();
        while (node !=null){
            list.add(node.getValue());
            node = node.getNext();
        }
        // 1.8自带的倒序方法
        Collections.reverse(list);
        return list;
    }

    /**
     * 使用java原生的方法做一个删除比较器
     * @param node 需要删除的
     * @return
     */
    private static List<Integer> testDelete(Node node,int value){
        // 直接使用java自带的LinkedList
        List<Integer> list = new LinkedList<>();
        while (node !=null){
            list.add(node.getValue());
            node = node.getNext();
        }
        // 1.8自带的删除方法
        list.removeIf(integer -> integer == value);
        return list;
    }
    public static void main(String[] args) throws CloneNotSupportedException {
        // 最大测试次数
        int maxTestSize = 10000;
        int maxSize = 1000;
        for(int i =0;i<maxTestSize;i++){
            Node node1 = randomNode((int)(Math.random()*maxSize)+1);
            // 用系统方式实现的倒序
            List<Integer> integers = testReverse(node1);
            // 自己写的倒序方式
            node1 = reverse(node1);
            int index = 0;
            while (node1.getNext() != null){
                if(node1.getValue() != integers.get(index++)){
                    System.out.println("出错了");
                    break;
                }
                node1 = node1.getNext();
            }
        }
        for(int i =0;i<maxTestSize;i++){
            Node node1 = randomNode((int)(Math.random()*maxSize)+1);
            int deleteNum = (int)(Math.random()*maxSize)+1;
            // 用系统方式实现的倒序
            List<Integer> integers = testDelete(node1,deleteNum);
            // 自己写的倒序方式
            node1 = deleteNode(node1,deleteNum);
            int index = 0;
            while (node1.getNext() != null){
                if(node1.getValue() != integers.get(index++)){
                    System.out.println("出错了");
                    break;
                }
                node1 = node1.getNext();
            }
        }
    }
}

先解释一下什么是链表,上图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RdzLmNTm-1606654569491)(https://raw.githubusercontent.com/hanhuafeng/pic_factory/master/20201120233859.png)]
图中的Value就是这个节点中的存的数据,Address就是记录了下一个节点的地址
例如,地址为A的Node中的Address记录了地址为B的地址

反转代码具体的实现思路请看下面的解释:

首先是第一步


接下来把pre,head,next往下移动
在这里插入图片描述

一直需要移动到以下情况才能结束,最后把图中的pre返回出去,当成新的头部即可

在这里插入图片描述

对应代码如下:
Node pre = null;
Node next;
while (head !=null){
    next = head.getNext();
    head.setNext(pre);
    pre = head;
    head = next;
}
return pre;

删除链表中指定的node节点解释:

遍历到可以使用的头部代码:
while (head !=null){
    if(head.getValue() != value){
        break;
    }
    head = head.getNext();
}
/**
为什么要上这块代码?
首先需要保证能找到一个可用的头,例如以下的情况:
链表中的值如下情况,需要删除1最后的返回情况是怎么样的?
1,2,3,4,1,5,6
最后的返回:2,3,4,5,6
可以看出需要把2作为新的头部作为返回结果,所以开头就需要一个while去找到可用的头部
接下来就是正常的流程了,代码对应的具体流程请看下图:
*/
if(now.getValue() == value){
    pre.setNext(now.getNext());
}
pre = now;
now = now.getNext();

在这里插入图片描述


双链表的实现,实现双向链表的反转以及双向链表节点的删除

先上代码:

/**
 * 双链表的实现
 * 实现双向链表的反转以及双向链表节点的删除
 * @author hanhuafeng
 * @date 2020/11/19
 */
public class Code_DoubleLinkedList {
    public static class DoubleNode {
        private int value;
        private DoubleNode pre;
        private DoubleNode next;

        public DoubleNode(int data) {
            value = data;
        }
        public int getValue() {
            return value;
        }
        public void setValue(int value) {
            this.value = value;
        }
        public DoubleNode getPre() {
            return pre;
        }
        public void setPre(DoubleNode pre) {
            this.pre = pre;
        }
        public DoubleNode getNext() {
            return next;
        }
        public void setNext(DoubleNode next) {
            this.next = next;
        }
    }
    /**
     * 反转的方法
     * 用户给出头部的Node节点,通过指针替换的方式,进行反转
     * @author hanhuafeng
     * @date : 22:10 2020/11/19
     * @param head 头部指针
     * @return 返回已经反转好的链表
     */
    public static DoubleNode reverse(DoubleNode head){
        DoubleNode pre = null;
        DoubleNode next;
        DoubleNode preIndex=null;
        while (head !=null){
            pre = head.getPre();
            next = head.getNext();
            head.setPre(next);
            head.setNext(pre);
            // preIndex因为最后会遍历到null为止,所以要记录一下null之前的节点用于返回
            preIndex= head;
            head = next;
        }
        return preIndex;
    }
    /**
     * 删除链表中指定的node节点
     * 需要考虑头节点被删除的情况,所以需要先遍历出头节点
     * @author: hanhuafeng
     * @date : 22:45 2020/11/19
     * @param  value 待删除的值
     * @return 返回已经删除完毕的链表
     */
    private static DoubleNode deleteNode(DoubleNode head, int value){
        while (head !=null){
            if(head.getValue() != value){
                break;
            }
            head = head.getNext();
            head.setPre(null);
        }
        DoubleNode now = head;
        DoubleNode pre;
        DoubleNode next;
        while (now != null){
            pre = head.getPre();
            next = head.getNext();
            if(now.getValue() == value){
                if(pre!=null){
                    pre.setNext(next);
                }
                if(next!=null){
                    next.setPre(pre);
                }
            }
            now = now.getNext();
        }
        return head;
    }
    public static void main(String[] args) {
        DoubleNode doubleNode1 = new DoubleNode(1);
        DoubleNode doubleNode2 = new DoubleNode(2);
        DoubleNode doubleNode3 = new DoubleNode(3);
        doubleNode1.setNext(doubleNode2);
        doubleNode2.setPre(doubleNode1);
        doubleNode2.setNext(doubleNode3);
        doubleNode3.setPre(doubleNode2);
        doubleNode1 = reverse(doubleNode1);
        doubleNode1 = deleteNode(doubleNode1,3);
    }
}

双向链表的实现比单向链表简单,双向链表的翻转只是将pre节点和next交换位置即可实现,
具体的的实现逻辑请看下图:
在这里插入图片描述


实现从一个栈中取最小值GetMin,要求时间复杂度O(1)

先上代码:

/**
 * 实现从一个栈中取最小值GetMin,和实现栈的pop和push方法,要求时间复杂度O(1)
 * 解析:
 * 要求了时间复杂度为1,即说明了不能使用遍历的方式取出最小值
 * 所以这边可以采用两个栈,一个用于正常push数据,一个用于每次把最小值压入,并且pop出最小值
 * 每次有数据进来都检测一次pop栈的栈顶数据是否是最小的,如果是最小的pop栈依旧压入栈顶的数据,否则压入刚进来的数据
 * @author hanhuafeng
 * @version V1.0
 * @description
 * @date 2020/11/18
 */
public class Code_StackGetMin {
    private Stack<Integer> pushStack;
    private Stack<Integer> minStack;

    public Code_StackGetMin(){
        pushStack = new Stack<>();
        minStack = new Stack<>();
    }

    public void push(int val){
        if(minStack.empty()){
            minStack.push(val);
        }else{
            minStack.push(val<minStack.peek()?val:minStack.peek());
        }
        pushStack.push(val);
    }

    public int pop(){
        if (pushStack.empty()){
            throw new RuntimeException("栈还是空的!");
        }
        this.minStack.pop();
        return pushStack.pop();
    }

    public int getMin(){
        if(pushStack.empty()){
            throw new RuntimeException("栈还是空的!");
        }
        return this.minStack.peek();
    }

    public static void main(String[] args) {
        Code_StackGetMin stackGetMin = new Code_StackGetMin();
        stackGetMin.push(1);
        stackGetMin.push(2);
        stackGetMin.push(3);
        stackGetMin.push(4);
        stackGetMin.push(0);
        System.out.println(stackGetMin.getMin());
        System.out.println(stackGetMin.pop());
        System.out.println(stackGetMin.getMin());
    }
}

最后的数据结构如下图:


递归

PS:任何递归的代码一定都可以改成非递归!

递归在系统中的简单实现

/**
 * 用递归的方式在arr中求出最大值
 *
 * @author hanhuafeng
 * @date 2020/11/21
 */
public class Code_RecursionImplentGetMax {
    public static int getMax(int[]arr) {
        return getMax(arr,0,arr.length-1);
    }
    public static int getMax(int[] arr, int l, int r) {
        if (l == r) {
            // 说明数组中只存在了一个数字,返回任意一个都行
            return arr[l];
        }
        // 求中点
        int mid = l + ((r - l) >> 1);
        int leftMax = getMax(arr,l,mid);
        int rightMax = getMax(arr,mid+1,r);
        return leftMax>rightMax?leftMax:rightMax;
    }
}

递归最需要考虑的是baseCase,即何时结束


Master 公式(用于估算递归行为的时间复杂度)

形如:T(N) = a*T(N/b) + O(N^d)(其中的 a、b、d 都是常数)的递归函数,可以直接通过 Master 公式来确定时间复杂度
如果 log(b,a) < d,复杂度为 O(N^d)
如果 log(b,a) > d,复杂度为 O(N^log(b,a))
如果 log(b,a) == d,复杂度为 O(N^d * logN)
a(子过程调用了几次)
T(N/b) 子过程数据规模

Master公式运用的条件:

PS:master针对于递归函数并且子规模一致的情况下适用

哈希表

  • 哈希表的增删改查操作的时间复杂度都是常数时间,只是这个常数会比较大,这类操作包括了containsKey、get、put等操作
  • 如果Hashmap<String,String>,hash表内部会存储字符串,而不是这个字符串的地址,但如果定义的是Hashmap<Node,Node>,hash表内部会存储node的内存地址,只占用8字节;
  • HashMap和HashSet性能都是一样的;
  • java中的hash表有个特别的地方,Integer这一类,在hash表内部也都会理解为基本类型存入表内,如果是非原生类型,则按引用类型;
/**
例如如下例子:
*/
public static class Zuo{
    private int value;
    public Zuo(int a){
        this.value = a;
    }
}
public static void main(String[] args){
    HashMap<Integer,String> test = new HashMap<>();
    Integer a = 190000;
    Integer b = 190000;
    // 下面代码会返回false
    System.out.println(a == b);
    test.put(a,"我是3");
    // 下面代码回返回true
    System.out.println(test.contrailKey(b));
    Zuo zuo1 = new Zuo(1);
    Zuo zuo2 = new Zuo(2);
    test.put(zuo1,"我是z1");
    // 下面代码会返回false
    System.out.println(test.contrailKey(zuo2));
}

有序表

  • java中TreeMap就是有序表
  • TreeMap 是一个接口名
  • 红黑树、avl树、sb树(size balance tree)、跳表都可以实现有序表
  • 增删改查的时间复杂度都是O(logN),功能比hash表强大,但是时间复杂度比hash表弱
  • java中的TreeMap就是红黑树实现的
  • 有序表中的key是按序组织的,所以拥有firstKey(最小的key),lastKey(最大的key),floor(离某个值最近的key,<=value),ceilingKey(离某个值最近的key,>=value);
  • 不是基础类型需要实现比较器,用于两者间的比较,否则会报错
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值