阅读 462

动图演示:手撸堆栈的两种实现方法!

正式开始之前,先和各位朋友聊聊后期的一些打算,后面的文章计划写一些关于数据结构和算法的内容,原因很简单底层结构决定上层建筑嘛,对于框架满天飞的今天,我们不止要学习如何使用框架,更要了解它的原理以及底层数据结构,只有这样我们才能更好的应用它。

当然,除了上述原因之外,还有一个重要因素是为了搞定面试。

算法配图-1.gif

随着软件开发行业竞争的日益激烈,面试的难度也在逐渐增加,因为企业要从众多的面试人中选出最优秀的人,只能提高面试的难度,而算法和数据结构比较烧脑的硬核技能之一,自然也就成了面试的首选科目。并且随着时间的推移,算法和数据结构出现的频率和占比也会不断增加,因此为了顺应时代发展的潮流,我们也要做一些调整,所以在后面的一些文章中,我会陆续更新一些关于算法和数据结构的文章,希望大家能够喜欢。

PS:当然随着智能系统的普及(如今日头条和抖音),算法和数据结构在企业中应用也越来越多,因此学习算法和数据结构也是迫在眉睫的事了。

栈(Stack)又叫堆栈(简称栈),它是在同一端进行插入和删除数据的线性表。

栈是最基础也是最常见的数据结构之一,它的数据结构和操作流程如下图所示:

stack-4.gif

其中,允许进行插入和删除的一端叫作栈顶(Top),另一端叫作栈底(Bottom),栈底固定,栈顶浮动。

当栈中的元素为零时,该栈叫作空栈。添加数据时一般叫作入栈或进栈(Push),删除数据叫作出栈或退栈(Pop)。栈是后进先出(Last In First Out,LIFO)的线性表

image.png

物理结构 & 逻辑结构

在手撸算法之前,我们先来认识一下数据结构中的两个重要概念:物理结构和逻辑结构

当谈到“物理”和“逻辑”一词时,我们可以会想到数据库中的逻辑删除和物理删除。

所谓的物理删除是指通过删除命令真实的将数据从物理结构中删除的过程;而逻辑删除是指通过修改命令将数据更改为“已删除”的状态,并非真实的删除数据。

这里的逻辑结构和物理结构和上面的概念类似,所谓的物理结构是指可以将数据存储在物理空间中,比如数组和链表都属于物理数据结构;而逻辑结构则是用于描述数据间的逻辑关系的,比如本文要讲的栈就属于逻辑结构。

af5f4e292b7f10819b018be38865f268.gif

可能有些人看到这里就蒙了,没关系,我这里举一个例子你就明白了。

如果用人来表示物理结构和逻辑结构的话,那么真实存在的有血有肉的人就属于物理结构,而人的思想和信念就属于逻辑结构了

image.png

自定义栈I:数组实现

通过上面的内容,我们知道了栈属于逻辑结构,因此它的实现方式就可以有很多种了,比如数组的实现方式或者是链表的实现方式。那么我们就先用数组实现一下,栈的主要方法有: image.png

① 定义结构

那么我们先来定义它的结构:

public class MyStack<E> {
    private Object[] value = null; // 栈存储容器
    private int top = -1; // 栈顶(的指针)
    private int maxSize = 0; // 栈容量

    // 构造函数(初始化默认容量)
    MyStack() {
        this.maxSize = 10;
    }

    // 有参构造函数
    MyStack(int initSize) throws Exception {
        if (initSize <= 0) {
            throw new Exception("栈容量必须大于 0");
        } else {
            value = new Object[initSize];
            maxSize = initSize;
            top = -1;
        }
    }
}
复制代码

其中栈中数据会存储在 Object[] value 数组中,top 变量代表栈顶的指针,它其实存储的是栈顶元素的下标,会随着入栈不断变化(后进先出),maxSize 表示栈的最大容量。

② 入栈

此方法是给栈添加数据的,实现代码如下:

// 入栈(数据添加)
public boolean push(E e) throws Exception {
    if (maxSize - 1 == top) {
        throw new Exception("入栈失败,栈已满");
    } else {
        value[++top] = e;
        return true;
    }
}
复制代码

每次当有数据插入时,只需在数组中添加一个值,并将栈顶的下标 +1 即可。

入栈操作如下图所示: stack-5.gif

③ 出栈

此方法是删除栈中的数据的,实现代码如下:

// 数据移除(出栈)
public E pop() throws Exception {
    if (top <= -1) {
        throw new Exception("移除失败,栈中已无数据");
    } else {
        return (E) value[top--];
    }
}
复制代码

出栈只需删除数组中栈顶数据(最后加入的数据),并修改栈顶下标 -1 即可。

出栈操作如下图所示: stack-6.gif

④ 数据查询

除了以上操作方法之外,我们还需要添加一个查询栈顶数据的方法:

// 数据查询
public E peep() throws Exception {
    if (top <= -1) {
        throw new Exception("移除失败,栈中已无数据");
    } else {
        return (E) value[top];
    }
}
复制代码

⑤ 代码测试

到此为止栈的数据结构就已经实现完了,接下来我们来测试一下:

// 代码测试
public static void main(String[] args) throws Exception {
    MyStack stack = new MyStack(10);
    stack.push("Hello");
    stack.push("Java");
    System.out.println(stack.peep());
    stack.pop();
    System.out.println(stack.pop());
}
复制代码

以上程序的执行结果为:

Java

Hello

从上述代码可以看出,我们添加栈的顺序是 HelloJava 而输出的顺序是 JavaHello 符合栈的定义(后进先出)。

自定义栈II:链表实现

除了数组之外,我们可以还可使用链表来实现栈结构,它的实现稍微复杂一些,我们先来看链表本身的数据结构: image.png 使用链表实现栈的流程如下:

stack-7.gif 也就是说,入栈时我们将数据存储在链表的头部,出栈时我们从头部进行移除,并将栈顶指针指向原头部元素的下一个元素,实现代码如下。

我们先来定义一个链表节点:

public class Node {
    Object value; // 每个节点的数据
    Node next; // 下一个节点

    public Node(Object value) {
        this(value, null);
    }

    /**
     * 创建新节点
     * @param value 当前节点数据
     * @param next  指向下一个节点(头插法)
     */
    public Node(Object value, Node next) {
        this.value = value;
        this.next = next;
    }
}
复制代码

接下来我们使用链表来实现一个完整的栈:

public class StackByLinked {

    private Node top = null; // 栈顶数据
    private int maxSize = 0; // 栈最大容量
    private int leng = 0; // 栈实际容量

    public StackByLinked(int initSize) throws Exception {
        if (initSize <= 0) {
            throw new Exception("栈容量不能小于等于0");
        }
        top = null;
        maxSize = initSize;
        leng = 0;
    }

    /**
     * 容量是否已满
     * @return
     */
    public boolean isFull() {
        return leng >= maxSize;
    }

    /**
     * 是否为空
     * @return
     */
    public boolean isEmpty() {
        return leng <= 0;
    }

    /**
     * 入栈
     * @param val
     * @return
     * @throws Exception
     */
    public boolean push(Object val) throws Exception {
        if (this.isFull()) {
            // 容量已满
            throw new Exception("容量已满");
        }
        top = new Node(val, top); // 存入信息,并将当前节点设置为头节点
        leng++;
        return true;
    }

    /**
     * 出栈(移除)
     * @return
     * @throws Exception
     */
    public Node pop() throws Exception {
        if (this.isEmpty()) {
            throw new Exception("栈为空,无法进行移除操作");
        }
        Node item = top; // 返回当前元素
        top = top.next;
        leng--;
        return item;
    }

    /**
     * 查询栈顶信息
     * @return
     */
    public Node peek() throws Exception {
        if (isEmpty()) {
            throw new Exception("你操作的是一个空栈");
        }
        return top;
    }

    // 代码测试
    public static void main(String[] args) throws Exception {
        StackByLinked stack = new StackByLinked(10);
        stack.push("Hello");
        stack.push("Java");
        System.out.println(stack.peek().value);
        stack.pop();
        System.out.println(stack.pop().value);
    }
}
复制代码

以上程序的执行结果是:

Java

Hello

总结

本文我们使用了数组和链表等物理结构来实现了栈,当然我们也可以使用其他容器来实现,比如 Java 中的 List,我们只需要保证在操作栈时是后进先出的执行顺序,并且至少包含 3 个重要方法:入栈、出栈和查询栈顶元素就可以了。

最后

算法和数据结构的学习是 3 分学 7 分练,只看不练是没办法学好算法的,而且学习算法和数据结构是一个循序渐进的过程,短时间内不会有明显的收效。因为这些算法经过了几百年的发展和积累才得以流传下来的,所以想要“玩得转”还需要一点耐心。

这里给你讲一个学习算法的“秘诀”:**看不懂的知识要反复看,如果反复看还是看不懂,那么别着急,休息一下再继续看!相信我,对于学习算法这件事,所有人的过程都是一样的。

关注公众号「Java中文社群」发送“面试”,领取最新的面试资料。