栈的实现原理

4次阅读

共计 7888 个字符,预计需要花费 20 分钟才能阅读完成。

目录介绍

01. 栈由简单数据实现

1.1 简单数组代码实现
1.2 可能出现问题
1.3 性能和局限性

02. 栈由动态数组实现

2.1 基于简单数组存在问题
2.2 第一种解决办法
2.3 第二种解决办法
2.4 动态数组实现栈代码
2.5 性能和局限性

03. 栈由链表实现

3.1 使用链表的优势
3.2 链表实现栈代码
3.3 性能和局限性

04.Android 栈 Stack 源码分析

4.1 源码展示
4.2 为何选用数组实现栈

05. 创建加强版自定义栈
5.1 扩容和泛型

好消息

博客笔记大汇总【16 年 3 月到至今】,包括 Java 基础及深入知识点,Android 技术博客,Python 学习笔记等等,还包括平时开发中遇到的 bug 汇总,当然也在工作之余收集了大量的面试题,长期更新维护并且修正,持续完善……开源的文件是 markdown 格式的!同时也开源了生活博客,从 12 年起,积累共计 N 篇[近 100 万字,陆续搬到网上],转载请注明出处,谢谢!
链接地址:https://github.com/yangchong2…
如果觉得好,可以 star 一下,谢谢!当然也欢迎提出建议,万事起于忽微,量变引起质变!

栈系列文章

00. 栈基础介绍
什么是栈?栈的使用场景?异常?栈的效率探索?

01. 栈的实现原理
栈由简单数据实现,栈由动态数组实现,栈由链表实现,创建加强版自定义栈,以及几种不同实现栈的方式比较。

02. 栈的常见操作
栈的顺序存储结构及实现,栈的链式存储结构及实现,两种栈形式比较

03. 使用栈判断括号是否匹配
利用栈实现判断字符串中的括号是否都是配对的,注意:“{[()]}”类似的可以匹配,“{(}}”类似的无法匹配。

04. 使用栈实现字符串逆序
将字符串“how are you”反转

05. 用两个栈实现队列
用两个栈来实现一个队列,完成队列的 Push 和 Pop 操作。队列中的元素为 int 类型。

06. 栈的压入、弹出序列
输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。

07. 使用栈解析计算器数学公式
解析一般数学算式,实现简单的带括号的加减乘除运算。

01. 栈由简单数组实现
1.1 简单数组代码实现

首先看一下实现的代码
用数组实现栈,最主要的是要在类的内部定义一个数组,并且这个数组要具有一定的大小,要在定义栈的时候定义好。
public class ArrayStack{
private static final String TAG = “ArrayStack”;
private Object[] contents;
private int top = -1;
private int bottom = -1;
private int SIZE = 10;// 有一个初始值大小

public ArrayStack(){
contents = new Object[SIZE];
top = -1;
}

public int push(Object obj) throws Exception {
if (top > SIZE) throw new Exception(“ 小杨逗比,栈已经满了!”);
top++;
contents[top] = obj;
return top;
}

public Object pop() throws Exception{
if (top == bottom) throw new Exception(“ 小杨逗比,栈已经空了!”);
Object obj = contents[top];
contents[top] = null;
top–;
return obj;
}

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

public int getSize(){
return top + 1;
}

public void display() throws Exception{
if (getSize() == 0) throw new Exception(“ 空栈!”);
for (int i=getSize()-1;i>=0;i–){
System.out.print(contents[i].toString() + “->”);
}
System.out.println(“”);
}
}

public void test{
ArrayStack as = new ArrayStack();
//as.display();
as.push(“ 小杨逗比 ”);
as.push(“ 潇湘剑雨 ”);
as.push(“yc”);
as.push(“ 逗比 ”);
as.push(“aaa”);
as.push(“ertte”);
as.push(“hello”);
as.display();
as.pop();
System.out.println(as.getSize());
as.pop();
as.display();
}

1.2 可能出现问题

可能会出现的问题

当数组栈存满了元素的时候,如果执行插入数据,将会抛出栈满异常。
当数组栈没有元素的时候,如果执行出栈数据,将会抛出栈空异常。

1.3 性能和局限性

性能和局限性分析

栈的最大空间必须提前声明,而且关键是大小还不能改变,这就蛋疼了。所以会出现执行入栈或者出栈操作时会出现异常。那么解决异常就是每次入栈判断栈是否存储满,每次出栈判断栈是否为空。
假设栈中有 m 个元素,基于简单数组实现的栈。栈的出栈,入栈,判空,获取大小等时间复杂度都是 O(1)。

02. 栈由动态数组实现
2.1 基于简单数组存在问题

基于简单数组的栈实现方法中,采用一个下标变量 top,它始终指向栈中最新插入元素的位置。
当插入元素时,会增加 top 值,并且会在数组该下标的位置存储新的元素。
当删除元素时,先获取下标变量 top 位置的元素,然后减小变量 top 的值。
当 top 下标变量为 - 1 时,表示栈是空的。但是存在问题是:在固定大小的数组中,如何处理所有空间都已经保存栈元素这种情况???

2.2 第一种解决办法

可能首先会想到,每次将数组大小增加 1 或者减小 1,将会怎么样?

插入元素,栈的空间大小增加 1
删除元素,栈的空间大小减去 1

这样做存在极大问题

频繁增加数组大小的方法开销很大。为什么这样说呢?
当 n = 3 时,执行 push 插入元素操作,当插入第四条元素时,会新建一个大小为 4 的数组,然后复制原数组中所有的元素到新的数组中,然后在新的数组中的末尾添加插入元素。以此类推,每次插入数据,都会重新创建一个新的数组对象,然后拷贝旧的数组数据到新的数组中来,然后在末尾添加新元素,这样做实在不好。

2.3 第二种解决办法

在第一种解决办法中改造。比如我们经常听到 ArrayList 集合动态扩容,先指定数组的长度,如果数组空间已满,则新建一个比原数据大一倍 [或者 n 倍] 的新数组,再然后复制元素。
采用这种方式,插入元素操作,开销相对来说要小很多。

2.4 动态数组实现栈代码

基于动态数据实现栈的代码如下所示
class DynArrayStack{
private int top;
private int capacity;
private int[] array;

private void doubleStack(){
int[] newArray=new int[capacity*2];
System.arraycopy(array,0,newArray,0,capacity);
capacity=capacity*2;
array=newArray;
}

public DynArrayStack(){
top=-1;
capacity=1;
array=new int[capacity];
}

public boolean isEmpty(){
return (top==-1);
}

public boolean isStackFull(){
return (top==capacity-1);
}

public void push(int date){
if(isStackFull()){
doubleStack();
}
array[++top]=date;
}

public int pop(){
if(isEmpty()){
System.out.println(“Stack Empty”);
return 0;
}else {
return array[top–];
}
}

public void deleteStack(){
top=-1;
}
}

public class Main {

public static void main(String[] args) {
// write your code here
DynArrayStack dynArrayStack=new DynArrayStack();
dynArrayStack.push(1);
dynArrayStack.push(2);
dynArrayStack.push(3);
System.out.println(dynArrayStack.pop());
System.out.println(dynArrayStack.pop());
System.out.println(dynArrayStack.pop());
}
}

2.5 性能和局限性

性能

假设有 n 个元素的栈,基于动态数组的栈实现中,关于栈插入数据,取出数据的时间复杂度都是 O(1)。
可能导致的性能问题:倍增太多可能导致内存溢出。

存在局限性

是用数组实现栈,在定义数组类型的时候,也就规定了存储在栈中的数据类型,那么同一个栈能不能存储不同类型的数据呢?(声明为 Object)?
栈需要初始化容量,而且数组实现的栈元素都是连续存储的,那么能不能不初始化容量呢?(改为由链表实现)?

03. 栈由链表实现
3.1 使用链表的优势
栈规模的增加和减小都很简洁,而且每个操作都是常数时间开销,每个操作都要使用额外的空间和时间开销来处理指针。
3.2 链表实现栈代码

入栈的顺序是:1 2 3 4 5,那么保证出栈的顺序是 5 4 3 2 1,以此类推让 head 节点指向栈顶节点保证让其倒序输出。
public class MyStack<T> {
private T data;
private MyStack<T> next;

public MyStack(T data, MyStack<T> next) {
this.data = data;
this.next = next;
}

public T getData() {
return data;
}

public void setData(T data) {
this.data = data;
}

public MyStack<T> getNext() {
return next;
}

public void setNext(MyStack<T> next) {
this.next = next;
}
}

public class LinkStack<N> {

private MyStack<N> head;
private MyStack<N> tail;
private Integer size=0;

public MyStack<N> getHead() {
return head;
}

public void setHead(MyStack<N> head) {
this.head = head;
}

public MyStack<N> getTail() {
return tail;
}

public void setTail(MyStack<N> tail) {
this.tail = tail;
}

public Integer getSize() {
return size;
}

public void setSize(Integer size) {
this.size = size;
}

public void addStack(N data){
MyStack<N> node = new MyStack<>(data,null);
if(headIsNull()){
head = node;
tail = node;
size++;
}else{
// 新加入的 node 是:(data,null) 让这个新的 node 的 next 指向初始的 head 节点 head 变为(data,head))
node.setNext(head);
head = node;
size++;
}
}

public N outStack(){
if(size>0){
N outData = head.getData();
head = head.getNext();
return outData;
}else{
throw new RuntimeException(“ 栈里无元素!”);
}
}

public boolean headIsNull(){
if(head == null){
return true;
}
return false;
}
}

测试一下
public void test() {
LinkStack<Integer> linkStack = new LinkStack<>();
linkStack.addStack(1);
linkStack.addStack(2);
linkStack.addStack(3);
linkStack.addStack(4);
linkStack.addStack(5);

for(int i=0;i<linkStack.getSize();i++){
System.out.println(linkStack.outStack());
}
}

3.3 性能和局限性

假设栈中有 n 个元素,基于链表的栈实现中,关于栈插入元素和取出元素的时间复杂度是 O(1)
数据入栈和出栈的时间复杂度都为 O(1),也就是说栈操作所耗的时间不依赖栈中数据项的个数,因此操作时间很短。而且需要注意的是栈不需要比较和移动操作。

04. 栈 Stack 源码分析

在 Android 中,对于 activity 使用栈 stack 进行管理的,下面看看栈源代码。
可以看到栈 stack 是实现 vector,其实底层也是用数组来实现的。
public class Stack<E> extends Vector<E> {
/**
* 创建一个空的栈对象
*/
public Stack() {
}

/**
* 将对象推送到此堆栈的顶部。
*/
public E push(E item) {
addElement(item);

return item;
}

/**
* 移除此堆栈顶部的对象,并将该对象作为此函数的值返回。
*/
public synchronized E pop() {
E obj;
int len = size();
obj = peek();
removeElementAt(len – 1);

return obj;
}

/**
* 查看此堆栈顶部的对象,而不将其从堆栈中移除。
*/
public synchronized E peek() {
int len = size();

if (len == 0)
throw new EmptyStackException();
return elementAt(len – 1);
}

/**
* 判断是否是空栈
*/
public boolean empty() {
return size() == 0;
}

/**
* 返回对象位于此堆栈上的基于 1 的位置。
*/
public synchronized int search(Object o) {
int i = lastIndexOf(o);

if (i >= 0) {
return size() – i;
}
return -1;
}

private static final long serialVersionUID = 1224463164541339165L;
}

05. 创建加强版自定义栈

一个能自动扩容,第二个能存储各种不同类型的数据,解决办法如下:
public class ArrayStack {
// 存储元素的数组, 声明为 Object 类型能存储任意类型的数据
private Object[] elementData;
// 指向栈顶的指针
private int top;
// 栈的总容量
private int size;

// 默认构造一个容量为 10 的栈
public ArrayStack(){
this.elementData = new Object[10];
this.top = -1;
this.size = 10;
}

public ArrayStack(int initialCapacity){
if(initialCapacity < 0){
throw new IllegalArgumentException(“ 栈初始容量不能小于 0: “+initialCapacity);
}
this.elementData = new Object[initialCapacity];
this.top = -1;
this.size = initialCapacity;
}

// 压入元素
public Object push(Object item){
// 是否需要扩容
isGrow(top+1);
elementData[++top] = item;
return item;
}

// 弹出栈顶元素
public Object pop(){
Object obj = peek();
remove(top);
return obj;
}

// 获取栈顶元素
public Object peek(){
if(top == -1){
throw new EmptyStackException();
}
return elementData[top];
}
// 判断栈是否为空
public boolean isEmpty(){
return (top == -1);
}

// 删除栈顶元素
public void remove(int top){
// 栈顶元素置为 null
elementData[top] = null;
this.top–;
}

/**
* 是否需要扩容,如果需要,则扩大一倍并返回 true,不需要则返回 false
* @param minCapacity

*/
public boolean isGrow(int minCapacity){
int oldCapacity = size;
// 如果当前元素压入栈之后总容量大于前面定义的容量,则需要扩容
if(minCapacity >= oldCapacity){
// 定义扩大之后栈的总容量
int newCapacity = 0;
// 栈容量扩大两倍 (左移一位) 看是否超过 int 类型所表示的最大范围
if((oldCapacity<<1) – Integer.MAX_VALUE >0){
newCapacity = Integer.MAX_VALUE;
}else{
newCapacity = (oldCapacity<<1);// 左移一位,相当于 *2
}
this.size = newCapacity;
int[] newArray = new int[size];
elementData = Arrays.copyOf(elementData, size);
return true;
}else{
return false;
}
}
}
“`

其他介绍
01. 关于博客汇总链接

1. 技术博客汇总

2. 开源项目汇总

3. 生活博客汇总

4. 喜马拉雅音频汇总

5. 其他汇总

02. 关于我的博客

github:https://github.com/yangchong211

知乎:https://www.zhihu.com/people/…

简书:http://www.jianshu.com/u/b7b2…

csdn:http://my.csdn.net/m0_37700275

喜马拉雅听书:http://www.ximalaya.com/zhubo…

开源中国:https://my.oschina.net/zbj161…

泡在网上的日子:http://www.jcodecraeer.com/me…

邮箱:yangchong211@163.com
阿里云博客:https://yq.aliyun.com/users/a… 239.headeruserinfo.3.dT4bcV
segmentfault 头条:https://segmentfault.com/u/xi…

掘金:https://juejin.im/user/593943…

正文完
 0