Java 数据结构与算法 栈和队列

        在生活中,我们常常遇到这样的情景:1,某人在学校发书的时候,如果要拿出最上面的书来看,那么直接拿出来就可以了,但是如果想要拿出中间某个位置的书来看,必须要挪开它上面压的书,才能看到这本书。这个时候,就是只能最上面的先拿出来,最下面的后拿出来。

 2,某人下课了,去学校食堂吃饭,人多的时候,他就需要排队。假设不存在插队的情况。那么他要吃到饭,必须要等他前面的人排完,到他的时候他才能打饭。这个时候,就是先排队的先走,后排队的后走。

以上者两种情况,在数据结构中分别对应两种数据结构:栈和队列。前者最主要的特点是先进去的最后出,后者最主要的情况是先进去的先出。下面来主要说明两者的特点和区别。

一、栈(stack)

1,概念

        栈是一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。

压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。

出栈:栈的删除操作叫做出栈。出数据在栈顶。

 栈实现的底层原理是顺序表或链表,在这里,我们来使用顺序表来实现栈的一些操作,比如压栈,出栈,查看栈顶的元素,判断栈是否为空。使用顺序表的话,只需要尾插尾删就可以。头插头删也可以,但是这样的话时间复杂度较高,而尾插尾删只需要时间复杂度为o(1)。

2,自己实现栈的一些操作

1)基本属性

这里假设是存放整型的元素,那么先定义一个int类型的数组array,同时由上面的图可知,有一个栈顶,简单来讲,就是一个数组的有效大小。那么我们在定义一个int类型的变量top来存放这个数组的有效大小。这里,我们假设数组的大小为10。

class stack {
    private int[] array;
    private int top;
    //合适的构造方法,将数组初始化,刚开始top没有元素,是0;
    public stack() {
        this.array = new int[10];
    }
}

2)压栈(push)

我们传进去一个值val,首先要判断这个表是不是满的,如果是的话,就要进行扩容。不是的话,放进去元素之后,让top加一次。

    //压栈操作
    public void push(int val) {
        if(this.array.length == this.top) {
            this.array = Arrays.copyOf(this.array, 2 * this.array.length);
        }
        this.array[top] = val;
        top++;
    }

3)是否为空(empty)

判断top是不是0即可。

    //是否为空
    public boolean empty() {
        return this.top == 0;
    }

4)出栈(pop)

出栈,弹出了栈顶的元素,同时,用一个变量来接受。先判断是不是空的,如果是空的,抛出异常,否者,返回栈顶的元素之后,直接让top减一就可以了。 

    //出栈操作
    public int pop() {
        if(empty()) {
            throw new UnsupportedOperationException("栈为空!");
        }
        int ret = this.array[this.top - 1];//下标
        this.top--;//top减一
        return ret;
    }

5)查看栈顶元素(peek)

和4)类似,只是需要查看就可以,但是不需要让top减一。

    //查看栈顶元素但不弹出
    public int peek() {
        if(empty()) {
            throw new UnsupportedOperationException("栈为空!");
        }
        int ret = this.array[this.top - 1];
        return ret;
    }

结果如下:

import java.util.Arrays;

class stack {
    //封装
    private int[] array;
    private int top;
    //合适的构造方法,将数组初始化,top默认是0;
    public stack() {
        this.array = new int[10];
    }
    //压栈操作
    public void push(int val) {
        if(this.array.length == this.top) {
            this.array = Arrays.copyOf(this.array, 2 * this.array.length);
        }
        this.array[top] = val;
        top++;
    }
    //出栈操作
    public int pop() {
        if(empty()) {
            throw new UnsupportedOperationException("栈为空!");
        }
        int ret = this.array[this.top - 1];
        this.top--;
        return ret;
    }
    //是否为空
    public boolean empty() {
        return this.top == 0;
    }
    //查看栈顶元素但不弹出
    public int peek() {
        if(empty()) {
            throw new UnsupportedOperationException("栈为空!");
        }
        int ret = this.array[this.top - 1];
        return ret;
    }
}

public class Main {
    public static void main(String[] args) {
        stack stack = new stack();
        stack.push(1);
        stack.push(2);
        stack.push(3);
        System.out.println(stack.peek());//3
        stack.pop();
        System.out.println(stack.peek());//2
        System.out.println(stack.empty());//false
    }
}

3,官方封装好的栈的一些操作

Java已经对这些数据结构封装好了,直接来导入合适的包来调用就可以了。感兴趣的可以去查看Java的帮助手册,或者查看这些方法的源码。Stack (Java Platform SE 8 ) (oracle.com)https://docs.oracle.com/javase/8/docs/api/java/util/Stack.html#push-E-

import java.util.Arrays;
import java.util.Stack;

public class Main {
    public static void main(String[] args) {
        Stack<Integer> stack = new Stack<>();//要导入包
        stack.push(1);
        stack.push(2);
        stack.push(3);
        System.out.println(stack.peek());//3
        stack.pop();
        System.out.println(stack.peek());//2
        System.out.println(stack.empty());//false
    }
}

二、队列

(一)、队列(Queue)

1,概念

队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out)。入队列:进行插入操作的一端称为队尾(Tail/Rear)。出队列:进行删除操作的一端称为队头。(Head/Front)

2,自己实现队列的一些操作

队列也可以数组和链表的结构实现。使用链表的结构实现更优一些,因为如果使用数组的结构,出队列在数组头上出数据,效率会比较低。和栈有着类似的操作。我们使用单链表来实现。 

1)基本属性

这里假设是存放整型的元素,设类名称为Node,定义一个int类型的元素val,在定义一个存储下一个元素地址的引用类型Node类型元素next.。

class Node {
    //私有权限,设置成公有的也行,更直接
    private int val;
    private Node next;
    
    public Node(int val) {
        this.val = val;
    }

    public int getVal() {
        return val;
    }

    public void setVal(int val) {
        this.val = val;
    }

    public Node getNext() {
        return next;
    }

    public void setNext(Node next) {
        this.next = next;
    }
}

2)入队(offer)

我们传进去一个值val,设置两个指针first 和 last分别用来存储头和尾的地址,首先要判断这个结点是不是第一次插入,如果是的话,让first 和last同时指向这个结点,不是的话,放进去元素之后,让上一个的last指向插入的元素的地址。

class queue {
    private Node first;
    private Node last;
    //入队
    public void offer(int val) {
        Node node = new Node(val);
        if(this.first == null) {
            this.first = node;
            this.last = node;
        } else {
            this.last.setNext(node);
            this.last = node;
        }
    }
}

3)是否为空(isEmpty)

判断first指向是否为空即可。

    //是否为空
    public boolean isEmpty() {
        return this.first == null;
    }

4)出队(poll)

先判断是否为空,是的话,抛出异常,否者获取元素后,让first直接指向下一个元素就可以了。

    //出队
    public int poll() {
        if(isEmpty()) {
            throw new UnsupportedOperationException("队为空!");
        } 
        int ret = this.first.getVal();
        this.first = this.first.getNext();
        return ret;
    }

5)得到队头元素但不删除

和4)类似,但不需要移动first。

    //得到队头元素但不删除
    public int peek() {
        if(isEmpty()) {
            throw new UnsupportedOperationException("队为空!");
        }
        int ret = this.first.getVal();
        return ret;
    }

结果如下:

class Node {
    //私有权限
    private int val;
    private Node next;

    public Node(int val) {
        this.val = val;
    }

    public int getVal() {
        return val;
    }

    public void setVal(int val) {
        this.val = val;
    }

    public Node getNext() {
        return next;
    }

    public void setNext(Node next) {
        this.next = next;
    }
}

class queue {
    private Node first;
    private Node last;
    //入队
    public void offer(int val) {
        Node node = new Node(val);
        if(this.first == null) {
            this.first = node;
            this.last = node;
        } else {
            this.last.setNext(node);
            this.last = node;
        }
    }
    //出队
    public int poll() {
        if(isEmpty()) {
            throw new UnsupportedOperationException("队为空!");
        }
        int ret = this.first.getVal();
        this.first = this.first.getNext();
        return ret;
    }
    //得到队头元素但不删除
    public int peek() {
        if(isEmpty()) {
            throw new UnsupportedOperationException("队为空!");
        }
        int ret = this.first.getVal();
        return ret;
    }
    //是否为空
    public boolean isEmpty() {
        return this.first == null;
    }
}

public class Main {
    public static void main(String[] args) {
        queue queue = new queue();
        queue.offer(1);
        queue.offer(2);
        queue.offer(3);
        System.out.println(queue.peek());//1
        queue.poll();
        System.out.println(queue.peek());//2
        System.out.println(queue.isEmpty());//false
    }
}

3,官方封装好的队列的一些操作

Queue (Java Platform SE 8 ) (oracle.com)https://docs.oracle.com/javase/8/docs/api/java/util/Queue.html

public class Main {
    public static void main(String[] args) {
        Queue<Integer> queue = new LinkedList<>();
        queue.offer(1);
        queue.offer(2);
        queue.offer(3);
        System.out.println(queue.peek());//1
        queue.poll();
        System.out.println(queue.peek());//2
        System.out.println(queue.isEmpty());//false
    }
}

(二)、双端队列(Deque)

        双端队列是允许两端都可以进行入队和出队操作的队列。元素可以从队头出队和入队,也可以从队尾出队和入队。我们直接看Java对这些操作的封装。如果感兴趣的,可以去阅读它的源代码。


Deque (Java Platform SE 8 ) (oracle.com)https://docs.oracle.com/javase/8/docs/api/java/util/Deque.html

(三)、循环队列

在实际当中,会遇到一种队列叫做循环队列。我们以静态队列来说明。

有的时候,我们在弹出队头的元素,然后要新增元素,在队尾直接新增就可以了。如果一个数组满了,首先想到的是扩容。但是我们再一看,发现前面弹出的位置是空的。

 这样就能大大节省空间。下面,我们使用数组来自己实现一个循环队列。

1)基本属性

由上面的图可知,一个循环队列,最起码要包含数组(elem),队头(front),队尾(rear)。我们将其封装在 MyCircularQueue 类当中。提供合适的构造方法(MyCircularQueue),在这个构造方法里面,定义数组的大小,队头的下标,队尾的下标。

class MyCircularQueue {   
    private int[] elem;//数组
    private int front;//头下标
    private int rear;//尾下标

    public MyCircularQueue(int k) {
       
        this.elem = new int[k];
        this.rear = 0;
        this.front = 0;
    }
}

2)入队列(enterQueue)

每一步的入队列操作,都要进行队列是否满(isFull)的判断,如果是满的,就报错,如果不是,就把元素放进去尾下标的的空间,同时,让尾下标加1。但是这里不能直接加,从上面的情况就可以知道,将8放在0下标,这里rear本来是下标7,在+1显然是越界的。需要进行微微的改动。改成:

this.rear = (this.rear + 1) % this.elem.length;(数组的长度)

    public boolean enterQueue(int val) {
        if(isFull()) {
            return false;
        }
        this.elem[this.rear] = val;
        this.rear = (this.rear + 1) % this.elem.length;
        return true;
    }
    public boolean isFull() {

        return (this.rear + 1) % this.elem.length == this.front;
    }

3)出队列(deleteQueue)

判断是不是空,如果不是,让头下标+1。

    public boolean deleteQueue() {
        if(isEmpty()) {
            return false;
        }
        this.front = (this.front + 1) % this.elem.length;
        return true;
    }
    public boolean isEmpty() {
        //相遇了,就是空的队列,不理解可以去画图
        return this.front == this.rear;
    }

4)得到队头元素(Front)

    public int Front() {
        if(isEmpty()) {
            return -1;
        }
        return this.elem[this.front];
    }

5)得到队尾元素(Rear)

要注意的是,如果rear到最大容量的时候,在+1,就到了0下标,这个需要考虑。

    public int Rear() {
        if(isEmpty()) {
            return -1;
        }
        //当rear到0下标的时候
        int index = (this.rear == 0) ? this.elem.length - 1 : this.rear - 1;
        return this.elem[index];
    }

全部代码如下:

class MyCircularQueue {

    private int[] elem;//数组
    private int front;//头下标
    private int rear;//尾下标

    public MyCircularQueue(int k) {
        
        this.elem = new int[k];
        this.rear = 0;
        this.front = 0;
    }

    public boolean enterQueue(int val) {
        if(isFull()) {
            return false;
        }
        this.elem[this.rear] = val;
        this.rear = (this.rear + 1) % this.elem.length;
        return true;
    }

    public boolean deleteQueue() {
        if(isEmpty()) {
            return false;
        }
        this.front = (this.front + 1) % this.elem.length;
        return true;
    }

    //得到队头的元素
    public int Front() {
        if(isEmpty()) {
            return -1;
        }
        return this.elem[this.front];
    }

    //得到队尾的元素
    public int Rear() {
        if(isEmpty()) {
            return -1;
        }
        //当rear到0下标的时候
        int index = (this.rear == 0) ? this.elem.length - 1 : this.rear - 1;
        return this.elem[index];
    }

    public boolean isEmpty() {
        //相遇了,就是空的队列
        return this.front == this.rear;
    }

    public boolean isFull() {

        return (this.rear + 1) % this.elem.length == this.front;
    }
}

猜你喜欢

转载自blog.csdn.net/Naion/article/details/122428859