Data structure and algorithm: operation of queue and stack and List and Queue in Java

This article will introduce the method of implementing stacks with arrays, as well as some classic operations of queues and stacks.

Implementing a stack with an array

Since the logical structure of the stack is first-in, last-out , and last-in first-out , the diagram is as follows:

It can be seen from the diagram that it is relatively simple to implement a stack with an array . You only need to maintain the index value to prevent the array from crossing the boundary. The code implementation:

public class MyStack {
    private int[] array;
    private int index;
    
    public MyStack(int size) {
        this.array = new int[size];
    }
    
    //入栈
    public void push(int value) {
        if (index >= array.length) {
            throw new RuntimeException("栈满,不让加了");
        }
        array[index++] = value;
    }
    
    //出栈
    public int pop() {
        if (index <= 0) {
            throw new RuntimeException("栈空,不能取出");
        }
        return array[--index];
    }
}
复制代码

Implementing a Queue with an Array

Let's graphically analyze how to use arrays to implement queues.

Enter the queue, add 1, 2, 3, 4, 5 in sequence:

After the queue reaches the length elements of the given array, let's analyze the process of taking data out of the queue and adding data:

To comply with the first-in-first-out feature of the queue, this array is like a circular array . When the queue is full (meaning that the number of queue elements reaches the specified array length), take out elements and continue to add elements, the index comes to the starting position again. , so back and forth.

Now we assume two pointers, begin and end, and add a variable size to represent the current number of elements in the queue.

When the size is greater than the specified array length, data cannot be inserted into the queue; when size<0, data cannot be fetched from the queue—that is to say, use this size variable to control whether push and pop can be performed.

When inserting data, put the data to be inserted into the position of end, and then use end++. At this time, you need to pay attention to the problem that the subscript is out of bounds. If end is greater than or equal to size, you need to set end to the position of 0. The illustration is as follows :

When taking out data, because of the first-in-first-out feature of the queue, the first data to enter the queue is at the begin position, so the data is fetched from the begin position, and at the same time, let begin++ come to the new earliest data position that entered the queue, and the same is true. Pay attention to whether the subscript of begin is out of bounds. As shown below:

From the above analysis, it can be seen that inserting data and extracting data can be completed with size and begin and end pointers.

The code to implement the queue with an array is as follows:

public static class MyQueue {
    private int[] array;
    private int begin;
    private int end;
    private int size;

    public MyQueue (int limit) {
        this.array = new int[limit];
        this.begin = 0;
        this.end = 0;
        this.size = 0;
    }

    public void push (int value) {
        size++;
        if (size > array.length) {
            throw new RuntimeException("队列满了");
        }
        array[end] = value;
        end++;
        //针对end越界的处理
        if (end >= array.length) {
            end = 0;
        }
    }

    public int pop () {
        size--;
        if (size < 0) {
            throw new RuntimeException("队列已空");
        }
        int result = array[begin];
        begin++;
        //针对begin越界的处理
        if (begin >= array.length) {
            begin = 0;
        }
        return result;
    }
}
复制代码

It is tricky to use an array to implement. It is necessary to control whether it can be inserted or removed according to the size, and then use the auxiliary pointer to move and record the array subscript.

It is still recommended to draw pictures to deepen understanding.

some classic operations

The structures of queues and stacks are very classic, and their variant questions often appear in interviews.

For example, to implement the breadth-first traversal of the graph, it is required to implement it with a stack; to implement the depth-first traversal of the graph, it is required to implement it with a queue.

The dark part of this question is that the breadth-first traversal of graphs is usually implemented using queues, while the depth traversal is implemented using stacks, so we need to do a conversion here:

First use the queue to implement the stack , and then use the stack implemented by this queue to implement breadth-first traversal, so as to achieve the purpose of using the stack to implement the breadth-first traversal of the graph;

For depth-first traversal, first use the stack to implement the queue , and then use the queue implemented by this stack to implement depth-first traversal.

This article does not discuss the structure of the graph , and mainly implements the following two algorithms:

  • Realize queue structure with stack structure
  • Realize stack structure with queue structure

Implementing a queue with a stack

To implement a queue, we need to consider how to achieve first-in first-out data .

The stack is a first-in-last-out structure, so we can use two stacks to implement it: push stack and pop stack.

For the special queue we want to implement, when entering the queue, push the data to the push stack, and observe and judge whether the pop stack is empty.

Insert a 3, add(3), at this time the pop stack is empty, the number in the push stack needs to be popped and pushed into the pop stack until the push stack is empty:

Insert a 2, add(2), the pop stack is not empty at this time, there is no need to pop the push stack and push into the pop stack:

Similarly, add(5) and add(7) in turn:

At this point, I want to take out data from the queue, poll, and pop up the pop stack. At this time, judge whether the pop stack is empty. If it is empty, all the push stack data needs to be poured out and pushed into the pop stack:

Continue to fetch numbers from the queue:

From the process of add and poll above, we can draw a conclusion:

Regardless of the queue add or poll, it is necessary to check whether the pop stack is empty. If the pop stack is empty, the data that needs to be popped from the push stack is pushed into the pop stack until the push stack is empty.

Right now:

  • When the push stack is poured into the pop stack, it must be poured all at once
  • When the pop stack is not empty, there is no need to push the data into the push stack

This ensures first-in, first-out .

So I can abstract a method of popping the push stack data into the pop stack:

public void pushToPop() {
    if (popStack.isEmpty()) {
        while (!pushStack.isEmpty()) {
            popStack.push(pushStack.pop());
        }
    }
}
复制代码

The complete code for implementing a queue with a stack:

public class MyQueueWithStack {
    private Stack<Integer> pushStack;
    private Stack<Integer> popStack;
    
    public MyQueueWithStack() {
        this.pushStack = new Stack<>();
        this.popStack = new Stack<>();
    }
    
    public void pushToPop() {
        if (popStack.isEmpty()) {
            while (!pushStack.isEmpty()) {
                popStack.push(pushStack.pop());
            }
        }
    }
    
    public void add(int value) {
        pushStack.push(value);
        pushToPop();
    }
    
    public int poll() {
        if (popStack.isEmpty() && pushStack.isEmpty()) {
            throw new RuntimeException("队列空了!");
        }
        pushToPop();
        return popStack.pop();
    }
}
复制代码

Implementing a stack with a queue

With the experience of implementing a queue with two stacks, we can try again how to implement a stack with two queues.

The ultimate goal is to realize the first-in-first-out of data, that is, the data that is added first is taken out first when fetching.

Let's first use the diagram to demonstrate the process again:

The data stacking and popping process demonstrated in the above figure:

Push: 1, 2, Pop: 2

Push: 3, 4, 5, Pop: 5

Push: None, Pop: 4

...

Therefore, this process realizes the last-in-first-out of the stack , and achieves the purpose of using the queue to realize the stack (using two queues to rewind data).

Key points: define two queues, and implement this kind of stack to insert data into the non-empty queue (if they are all empty, select one of them) when pushing, and take out the non-empty queue data and insert them into the original empty queue in turn when popping In the queue, only the last element is left, and this element is taken out and returned , so that the original non-empty queue becomes an empty queue. ---Each operation, regardless of push or pop, has a queue that is empty.

Translating the ideas I analyzed above into code is like this:

public class MyStackWithQueue<T> {
    private Queue<T> queue;
    private Queue<T> help;
    
    public MyStackWithQueue() {
        this.queue = new LinkedList<>();
        this.help = new LinkedList<>();
    }
    
    public void push(T value) {
        if (queue.isEmpty() && help.isEmpty()) {
            queue.add(value);
        }
        if (!queue.isEmpty()) {
            queue.add(value);
        }
        if (!help.isEmpty()) {
            help.add(value);
        }
    }

    public T pop() {
        Queue<T> temp = new LinkedList<>();
        if (!queue.isEmpty()) {
            temp = queue;
            while (queue.size() > 1) {
                help.add(queue.poll());
            }
        } else if (!help.isEmpty()) {
            temp = help;
            while (help.size() > 1) {
                queue.add(help.poll());
            }
        }
        return temp.poll();
    }
}
复制代码

Extension: List and Queue in Java

List collection

List collection elements have clear previous and next elements, and there are also clear first and last elements.

The most commonly used list collections are ArrayList and LinkedList two collection classes.

ArrayList

The capacity of ArrayList can be changed, non-thread-safe collection. Its internal implementation uses arrays for storage. When the collection is expanded, a larger array control will be created and the original data will be copied to the new array.

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    private static final long serialVersionUID = 8683452581122892189L;

    /**
     * Default initial capacity.
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * Shared empty array instance used for empty instances.
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
     * Shared empty array instance used for default sized empty instances. We
     * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
     * first element is added.
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * The array buffer into which the elements of the ArrayList are stored.
     * The capacity of the ArrayList is the length of this array buffer. Any
     * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
     * will be expanded to DEFAULT_CAPACITY when the first element is added.
     */
    transient Object[] elementData; // non-private to simplify nested class access
    
    //......
}
复制代码

ArrayList supports fast random access to elements , but insertion and deletion are slower because the process of insertion and deletion requires moving elements.

LinkedList

LinkedList is essentially a doubly linked list.

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
    transient int size = 0;

    /**
     * Pointer to first node.
     * Invariant: (first == null && last == null) ||
     *            (first.prev == null && first.item != null)
     */
    transient Node<E> first;

    /**
     * Pointer to last node.
     * Invariant: (first == null && last == null) ||
     *            (last.next == null && last.item != null)
     */
    transient Node<E> last;
    
    //...
}
复制代码

The obvious difference between it and ArrayList is that the insertion and deletion speed of LinkedList is fast, while the random access speed is very slow.

In addition, LinkedList also implements the Deque interface (double-ended queue, double-ended queue). Deque has the properties of both a queue and a stack, because it can be first-in, first-out, or first-in, last-out.

LinkedList associates scattered memory units through additional references (which internally define first and last pointers to the previous and next elements), forming a linear structure searched in link order, with high memory utilization.

Queue collection

The previous articles have been discussing the data structures of queues and stacks. The **first-in-first-out (FIFO)** of the queue should go deep into our minds --- the queue only allows data to be fetched from one end, and data to be inserted at the other end .

Since the emergence of BlockingQueue under the juc package , queues have been used in various high-concurrency scenarios. In view of its first-in, first-out characteristics and memory blocking operations, it is often used as a data buffer .

Guess you like

Origin blog.csdn.net/m0_48922996/article/details/125872186