数据结构基础(四)堆栈

在这一章我们来了解两个很特殊的数据结构:堆栈 (Stack) 和队列 (Queue)。这两个数据结构类似垃圾桶和队伍,栈是先进后出型,队列是先进先出型。

堆栈(Stack)

概念

堆栈是一种常用的数据结构,这种数据结构的存储方式和垃圾桶一样,后面放进去的元素可以先取出来,而最早放入的元素会被压在最下面,最后才能被拿出来。我们也可以把栈的储存方式简单理解为堆盘子,后面加入的盘子会被堆到最上面,最早堆入的盘子在最下面:

所以栈是一种后进先出(Last In First Out)的数据结构,后入的元素先出,先入的元素后出。

堆栈主要支持以下两种操作:

  • 入栈(Push):将一个元素放入栈,用来加入数据。
  • 出栈(Pop):将一个元素弹出栈,用来删除数据。

还有以下两种辅助操作:

  • Peek:查看最顶部的元素。
  • isEmpty:查看栈是否为空。
Stack Representation

数组栈实现

链表有两种实现方式,一种是数组,另一种是链表。数据的实现方式很简单,以下是数组栈的定义:

public class Stack {
    static final int CAPACITY = 1000;
    int top;
    int stack[];

    public Stack() {
        top = -1;
        stack = new int[CAPACITY];
    }
}

在数组栈中,我们使用数组来储存数据,其中包含一个CAPACITY来限制栈的容量,并使用指针top来记录最顶端元素的位置,top初始化为-1,代表数组栈没有任何元素。以下是push和pop方法的定义:

public boolean push(int val) {
    if (top >= (CAPACITY - 1)) {
        System.out.println("Stack Overflow.");
        return false;
    }
    stack[++top] = val;
    return true;
}

public int pop() {
    if (top < 0) {
        System.out.println("Stack Underflow.");
        return 0;
    }
    int element = stack[top--];
    return element;
}

在push中,我们需要检查顶部元素是否达到容量限制,如果是,输出“溢出栈”错误。否则移动top指针,加入新的元素。在pop中,也要查看栈是否为空,如果是,那么输出“栈下溢”错误。否则将top指针减一。另外还有peek和isEmpty的实现:

public int peek() {
    if (top < 0) {
        System.out.println("Stack Underflow");
        return 0;
    }
    int element = stack[top];
    return element;
}

public boolean isEmpty() {
    return top < 0;
}

peek也要查看栈是否为空,如果不是,直接返回top指向的元素。isEmpty只要查看top是否小于0即可。

链式栈实现

除了数组栈,我们也可以使用链表来实现栈,以下是链式栈的定义:

public class ListStack {

    static class StackNode {
        int val;
        StackNode next;
        StackNode(int val) {
            this.val = val;
        }
    }

    StackNode top;

    public ListStack() {
        top = null;
    }
}

在链式栈中,我们先定义节点StackNode,节点中包含数值和下一个节点的指针。在链式栈中,我们只需要记录top节点,在初始化时定义为null。以下是push和pop的定义:

public void push(int val) {
    StackNode newNode = new StackNode(val);
    if (top == null) {
        top = newNode;
    } else {
        StackNode temp = top;
        top = newNode;
        newNode.next = temp;
    }
    System.out.println(val + " is pushed to stack.");
}

public int pop() {
    if (top == null) {
        System.out.println("Stack is Empty.");
        return Integer.MIN_VALUE;
    }
    int popped = top.val;
    top = top.next;
    return popped;
}

在push中,我们先创建新的节点newNode,如果栈为空,那么直接将newNode赋给top。如果不为空,就将新元素的下一节点指向当前的top,并将newNode更新为top节点。在pop中,也要先检查栈是否为空,不为空的话,记录下top的数据作为返回值,并将top更新为自己的下一个节点。以下是链式栈peek和isEmpty的定义:

public int peek() {
    if (top == null) {
        System.out.println("Stack is empty.");
        return Integer.MIN_VALUE;
    }
    return top.val;
}

public boolean isEmpty() {
    return top == null;
}

在栈不为空的情况下,peek只需查看top的值即可,isEmpty也只要查看top是否是null就可以了。不管是用数组还是链表来实现栈,我们都只要处理头节点top,所以栈的所有操作都为O(1)。

队列(Queue)

概念

队列是很好理解的一种数据结构,顾名思义,队列数据结构就和我们平时排队一样,先进入的元素先出,后进入的元素后出。队列的两端都是开的,一段负责插入新元素,另一端负责删除元素。

Queue Example

队列主要支持以下两种操作:

  • 入队(enqueue):增加一个新的元素
  • 出队(dequeue):删除一个元素

还支持其他辅助操作:

  • peek – 查看队列最前端的元素
  • isFull – 查看队列是否满了
  • isEmpty – 查看队列是否为空

数组队列实现

队列和堆栈一样,也可以使用两种实现方式,一种是使用数组,叫做顺序队列,另一种是使用链表实现,叫做链式队列。以下我们会先实现一种更常见的数组队列,叫做循环队列,它和基础的顺序队列相比较,更能有效地利用数组空间。以下是循环队列的定义:

public class ArrayQueue {
    int front, rear, size;
    int capacity;
    int array[];

    public ArrayQueue(int capacity) {
        this.capacity = capacity;
        front = rear = size = 0;
        array = new int[capacity];
    }
}

在循环队列中,我们需要capacity来限制队列的长度,并创建两个指针front和rear,front用来指向队列的头部,而rear指向队列的尾部。队列总是从头部取出元素,从尾部插入新元素,在操作队列时,我们只需要移动front和rear两个指针即可。我们还需要一个额外的size变量来记录元素的数量,front,rear和size都初始化为0 。

以下是enqueue和dequeue的定义:

public void enqueue(int item) {
    if (isFull()) return;
    array[rear] = item;
    rear = (rear + 1) % capacity;
    size++;
    System.out.println(item + " is enqueued.");
}

public int dequeue() {
    if (isEmpty()) return Integer.MIN_VALUE;
    int item = array[front];
    front = (front + 1) % capacity;
    size --;
    return item;
}

在新元素入队的时候,我们需要先判断队列是否已满(isFull的代码在下一段)。如果未满,那么就把元素插入rear的位置,并将rear加1,并与capacity取模,然后增加size。在出队的时候,先要检查队列是否为空(isEmpty的代码在下一段),记录下删除元素的值后,我们将front指针增加1,与capacity取模,然后将size减少1。以下是辅助操作的定义:

public int peek() {
    if (isEmpty()) return Integer.MIN_VALUE;
    return array[front];
}

public boolean isFull() {
    return size == capacity;
}

public boolean isEmpty() {
    return size == 0;
}

peek只要查看front指针指向的值即可,isFull要检查size是否和容量capacity相同,isEmpty直接查看size是否等于0。

链式队列实现

用链表实现队列也很简单,和数组实现相似,也需要两个指针(front和rear)来实现。以下是ListQueue和QueueNode的定义:

public class ListQueue {

    QueueNode front;
    QueueNode rear;

    static class QueueNode {
        int value;
        QueueNode next;
        public QueueNode(int value) {
            this.value = value;
        }
    }
}

在链式队列中,我们需要定义节点QueueNode,QueueNode中含有两个值:一个是节点的数值value,另一个是指向下一个节点的next指针。在链式队列ListQueue中,我们只需要两个节点front和rear,front用来指向队列最前端的节点,而rear用来指向尾节点。以下是两个重要操作enqueue和deuque的实现:

public void enqueue(int value) {
    QueueNode newNode = new QueueNode(value);
    if (this.rear == null) { // Queue is empty
        this.front = this.rear = newNode;
        return;
    }
    this.rear.next = newNode;
    this.rear = newNode;
}

public int dequeue() {
    if (this.front == null) {
        System.out.println("The queue is empty.");
        return Integer.MIN_VALUE;
    }
    QueueNode frontNode = this.front;
    this.front = this.front.next;
    if (this.front == null) {
        this.rear = null;
    }
    return frontNode.value;
}

在enqueue中,我们先创建一个新的节点,如果队列为空,那么将头节点和尾节点同时指向新节点,结束操作。如果队列不为空,只要尾节点的next指针指向新节点,然后将尾节点指向新节点。在dequeue中,如果头节点front为空,直接返回默认数值。队列不为空的情况下,记录下front的数值作为返回值,并将头节点更新为下一节点。

完整代码

GitHub完整代码

Leetcode相关练习

Stack相关:

Queue相关:

 

posted @ 2021-06-13 14:12  秋华  阅读(1518)  评论(0编辑  收藏  举报