【Algorithms公开课学习笔记3】 栈与队列

本文详细介绍了栈和队列这两种数据结构,包括它们的接口、链表和可调整数组的实现方式,以及在Java中的泛型应用和迭代器的使用。分析了栈和队列在内存空间和性能上的区别,并探讨了它们在实际编程中的应用,如函数调用和表达式计算。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Stack and Queue 栈和队列

0.前言

本文是对数据类型的学习笔记,将会从接口(interface),实现(implement), 客户(client)等三个角度分模块来组织文章。

接口:对数据类型、基本操作的描述(API)
实现:实现API的基本操作的实际代码
客户:在程序里调用API所定义的基本操作


1.Stack 栈

栈最基本的特点就是后进先出(LIFO, last in first out)

  • 栈的API
//栈的API

public class Stack<Item>{
    //<Item>表示将数据类型泛化,此栈元素可接受任意数据类型
    public Stack();
    public void push(Item item);
    public Item pop();
    public boolean isEmpty();
    public int size;
    //其他
}

  • 栈的实现

栈的实现有两个(基本)方式,分别是链表(linked-list)和可调整数组(resizing array)

链表

入栈和出栈表示

入栈

出栈

代码实现

public class Stack<Item>{

    private Node first = null;
    //因为构造时不做任何操作,所以不写构造方法
    //内部类定义链表节点,注意访问修饰符private
    private class Node{
        Item item;
        Node next;
    }

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

    public void push(Item item){
        Node oldfirst = first;
        first = new Node();
        first.item = item;
        first.next = oldfirst;
    }

    public Item pop(){
        Item item = first.item;
        first = first.next;
        return item;
    }
}

时间性能

出栈和入栈的操作都是顺序语句,常数级(constant)

空间复杂度

在针对String字符串时分析:Node节点对象占用40字节,故所有空间~40N字节,如图

可调整数组

入栈和出栈表示

push: 往数组的的末尾添加item

pop: 在数组的末尾删除item

难处:由于无法提前知道栈的容量,导致在定义数组时无法确定其大小。初始的数组容量过大会浪费空间,容量过小会导致运行过程中溢出。因此,使用可调整的数组来将解决可以难题。

关键:当数组容量满(full)的时候,将数组大小翻倍(double size),当数据容量为大小的四分之一(one-quarter full)的时候,将数组大小减半(halve size)。

代码实现

public class Stack<Item>{

   private Item[] s;
   private int N;

   public Stack(){
       //由于java不支持泛型数组,所以这里需要强转
        s = (Item[])new Object[1];
        N = 0;
    }
    public boolean isEmpty(){
        return N > 0;
    }

    public void push(Item item){
        if (N == s.length) resize(2 * s.length);
        s[N++] = item;
    }

    public Item pop(){
        Item item = s[--N];
        s[N] = null;
        if (N > 0 && N == s.length/4) resize(s.length/2);
        return item;
    }

    private void resize(int capacity){
        Item[] copy = (Item[])new Object[capacity];
        for (int i = 0; i < N; i++)
        copy[i] = s[i];
        s = copy;
    }
}

时间性能

由于在入栈和出栈的过程中需要调整数组大小(此过程需要复制完整数组),若调整过于频繁,必定会耗时严重。因此时间性能分析存在最好、最坏和平摊情况。
*平摊分析:在最坏情况下,每次操作所需的平均时间。

空间复杂度

在针对String字符串时分析:在full的时候需要~8N,在1/4 full的时候需要~32N(暂时未能解释)

对比

  • 链表每一个操作的耗时都是常数级,但比较占用空间(存字符串要~40N)

  • 可调整数组的每一个操作的摊分时间也是常数级,但可能会遇到最坏情况而影响性能,其空间占用较小


2.Queue队列

队列最基本的特点就是先进先出(FIFO, first in first out)

  • 队列的API
//队列的API

public class Queue<Item>{
    //Item表示将数据类型泛化,此队列元素可接受任意数据类型
    public Queue();
    public void enqueue(Item item);
    public Item dequeue();
    public boolean isEmpty();
    public int size;
    //其他
}

  • 队列的实现

栈的实现同样有两个(基本)方式,分别是链表(linked-list)和可调整数组(resizing array)

链表

入队和出队表示

入队

出队

代码实现

public class Queue<Item>{

    private Node first = null;
    private Node last = null;
    //因为构造时不做任何操作,所以不写构造方法
    //内部类定义链表节点,注意访问修饰符private
    private class Node{
        Item item;
        Node next;
    }

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

    public void enqueue(Item item){
        Node oldlast = last;
        last = new Node();
        first.item = item;
        first.next = null;
        if (isEmpty()) first = last;
        else oldlast.next = last;
    }

    public Item pop(){
        Item item = first.item;
        first = first.next;
        if (isEmpty()) last = null;
        return item;
    }
}

时间性能

出队和入队的操作都是顺序语句,常数级(constant)

空间复杂度

在针对String字符串时分析:Node节点对象占用40字节,故所有空间~40N字节

可调整数组

入队和出队表示

push: 往数组的的末尾(tail)添加item

pop: 在数组的首部(head)删除item

关键:调整首部和末尾的指针,以及调整数组的容量

代码实现

public class Queue<Item>{

    //这里使用循环队列来实现,必须固定长度。
    //这里暂时没有实现调整数组容量的操作
   private final static int N =10 ;

   private Item[] s;
   private int tail;
   private int head;

   public Stack(){
       //由于java不支持泛型数组,所以这里需要强转
        s = (Item[])new Object[1];
        tail = 0;
        head = 0;
    }
    public boolean isEmpty(){
        return head == tail;
    }

    public void enqueue(Item item){
        if ((tail + 1) % N == head ){
            //溢出,不允许入队
             return ;
        }
        s[tail] = item;
        tail = (tail + 1) % N;
    }

    public Item dequeue(){
        if(isEmpty()){
            //空队列,不允许出队
            return;
        }
        Item item = s[head];
        head = (head + 1) % N;
        return item;
    }

}

时间性能

循环队列由于不涉及调整数组容量的操作,所以入队和出队操作都是顺序语句,常数级(constant)

空间复杂度

空间占用就是数组的容量,拿String字符串来分析,就是~8N

3.泛型

前面的代码已经涉及泛型,使用泛型的目的就是为了能够适配所有的数据类型。

在使用可调整数组实现栈的时候提到:由于java不支持定义泛型数组(其他程序语言也不支持),所以在定义泛型数组的时候使用强制转型

//强转
 Item[] copy = (Item[])new Object[capacity];

虽然使用强转是一种糟糕的编程习惯,但是此场景下只能通过该方法解决。

另外,学习泛型还需要学习java数据类型的包装类(wrapper type)和自动装箱子机制(autoboxing)。


//int类型的包装类是Integer
//push操作中会将int自动转成Integer,称为自动装箱

Stack<Integer> s = new Stack<Integer>();
s.push(17);

4.迭代器

使用迭代器的好处就是可以方便地遍历栈(或者队列)的元素,例如使用foreach方法来遍历。

类实现迭代器的基本方法如下:


public class Stack<Item> implements Iterable<Item>{

    //省略了其他属性和方法
    //重写iterator()方法,返回自定义的iterator对象
    public Iterator<Item> iterator() {
        return new ListIterator();
        }

    //自定义iterator类
    private class ListIterator implements Iterator<Item>{
        private Node current = first;
        //必须重写next()he hasnext()方法
        public boolean hasNext() {
            return current != null;
        }
        public void remove() { /* not supported */ }
        public Item next(){
            Item item = current.item;
            current = current.next;
            return item;
        }
    }
}

5.应用

  • java本身就提供了非常丰富的集合类(如List、Stack等),但是其提供的操作都是非常冗余且低性能的。如果对性能有追求的最好自己封装集合类。

  • 函数的调用就是对栈的而一种应用。由于函数的调用,程序流程进入函数内部,其当前环境和变量将全部推入栈,直到调用完毕返回结果时,才出栈。注意多级调用和递归调用。

  • 对于表达式的运算(如下图),使用双栈来完成:1)遇到左括号不操;2)遇到数字push到value satck;3)遇到操作符push到operator stack;4)遇到有括号从value satck pop两个数字,再从operator stack pop一个操作符,最后将计算结果push 到value stack。

第2周作业的答案

Queues

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值