复习-数据结构之栈和队列

草稿箱捉到一篇

介绍

都是照着当初年少无知时候的课堂PPT和一些书,和一些视频,和一些自己的领悟写的。看到了熟人莫见怪。

栈和队列是两种特殊的线性表,是操作受限的线性表,称限定性DS。

定义一组数据,{a1,a2,a3……ai……an}
对于线性表、栈、队列则有:
这里写图片描述

也就是说:
栈和队列是限定插入和删除只能在表的“端点”进行的线性表。

栈就像女朋友,只能从一端进进出出。除了那些有特殊癖好的人。在此多一句嘴,理解女性,正常调和。

再举一个列子,一个杯子(我说的是正常的喝水用的杯子),假设这个杯子只有10cm宽(绝望了吧),长度不限,现在有一些小球,直径9cm。把这些球一个一个的放入杯子中,这时,杯子就像这样:
这里写图片描述
我们此时想用球的话,只能拿球3。也就是说,只能拿最上面的球,如果想要拿1的话,则必须先把3和2倒出来,然后才能拿到1。这就是栈。

栈的定义和特点

  • 定义:限定仅在表尾进行插入或删除操作的线性表。表尾—栈顶,表头—栈底,不含元素的空表称空栈。
  • 特点:先进后出(FILO)或后进先出(LIFO)

这里写图片描述
(这时我上学时的PPT图片,怀念那个时候)

实现一个栈

伪代码:

ADT Stack {
      数据对象:
         D={ ai | ai ∈ElemSet, i=1,2,...,n,  n≥0 }
      数据关系:
         R1={ <ai-1, ai >| ai-1, ai∈D, i=2,...,n }
                   约定an 端为栈顶,a1 端为栈底。  
    一些操作,比如添加:
    push(int addInt){//添加元素的方法,在栈中被我们叫做push,压栈。
        /*只能在一端添加,添加后n+1;如果涉及指针的话,指针上移。
        *举例:
        *假设我们用数组array实现,数据对象是个int。
        *这时添加addInt,只能添加到数组尾部
        *并且定义一个指向栈尾的全局变量Top,
        *当我们添加完这个数addInt后,
        *我们将这个数赋值给Top,因为现在他是最上面的。
        */
        if(array.size()>=n){//判断栈是否还有空间
            进行扩容;
        }
        array.append(addInt);
        n++;
        Top=addInt;
    }
    其他操作;
 } ADT Stack

上面我们写了一个简单的栈中添加元素的操作,我们看看Android中,Java源码是怎么实现栈操作的。

首先随便在Android Studio中写一个Stack,JavaUtil提供的栈。然后点进去看看。

/**
     * Pushes an item onto the top of this stack. This has exactly
     * the same effect as:
     * <blockquote><pre>
     * addElement(item)</pre></blockquote>
     *
     * @param   item   the item to be pushed onto this stack.
     * @return  the <code>item</code> argument.
     * @see     java.util.Vector#addElement
     */
    public E push(E item) {
        addElement(item);

        return item;
    }

再看看addElement,addElement是Vector中的方法,没有错,Stack继承自Vector:

/**
     * Adds the specified component to the end of this vector,
     * increasing its size by one. The capacity of this vector is
     * increased if its size becomes greater than its capacity.
     *
     * <p>This method is identical in functionality to the
     * {@link #add(Object) add(E)}
     * method (which is part of the {@link List} interface).
     *
     * @param   obj   the component to be added
     */
    public synchronized void addElement(E obj) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = obj;
    }

他只做了3件事。首先就是我们所谓的n++;然后是扩容操作ensureCapacityHelper;这个方法里进行判断扩容等操作。最后把我们要添加的东东入栈。
这里并没有把我们添加的元素赋值给指向栈顶的全局变量。因为在Vector中,有一个方法直接返回指定位置的元素,而在Stack中调用了这个方法直接返回最后一个元素。

/**
     * Looks at the object at the top of this stack without removing it
     * from the stack.
     *
     * @return  the object at the top of this stack (the last item
     *          of the <tt>Vector</tt> object).
     * @throws  EmptyStackException  if this stack is empty.
     */
    public synchronized E peek() {
        int     len = size();

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

而我在上面写的伪代码,是简单的讲述一下入栈的操作,目的是让自己明白栈的原理,并且锻炼自己封装的能力,所以有考虑到比较全面的因素,而我写伪代码的时候并没有对整个系统进行一个整体的封装,所以返回栈顶元素和添加元素入栈的方法自然会有此交集,这也考诉大家,在写代码的时候,要考虑全面一些,瞻前顾后,思量全局。

栈也可以分为链栈和顺序栈。我写的伪代码,举例为用数组实现一个栈,这就是顺序栈,如果我们用链表实现一个栈,那就是链栈。

万变不离其一,都很简单的。

栈的应用举例

例一、数制转换
十进制数N转换为d进制数的原理,此处就应用到栈了,我们利用栈来进行记录。
这里写图片描述
对于N,我们每次把N除以8所得的商入栈变为下一步的N;
对于N除以8的操作,我们每次把结果入栈保存,并把栈顶元素再入栈到N栈当中,直至栈顶为0则结束;
对于N除以8取余的栈,我们每次把余数入栈。最后取出来的时候,途中栈的方向就是按照我们操作步骤的顺序来的。

而真正编写代码的时候,我们只需要实现一个栈就可以了,即余数的栈。此处不详细解,伪代码:

InitStack(s);//初始化栈,s为我们用到的栈,N为我们要转换的数字
    scanf(“%d”,&N);//打印N
    while (N) {//N不为0
         Push(s,N%8);//将N%8取余入栈s
         N=N/8;//你懂
     }
   while (!StackEmpty(s)){
         Pop(s,e);//s栈顶元素出栈
         printf(“%d”,e);//打印栈顶元素
     }

OK昨天我写到这里的时候,CSDN并没有保存草稿成功。所以我又要重写一遍下面。

例二:
括号匹配的检验,如果括号不匹配,请打印错误。
[ ( [ ] [ ] ) ]

伪代码,提供思路:

initStack(s);//初始化栈s

for(int i = 0; i < str.lenght(); i++){
//str是输入字符串,也就是那堆括号
    if(str.charAt(i).equals("{")||"["||"("){
    //如果是左括号,直接入栈
        s.push(str.charAt(i));
    }//if
    else if(str.cahrAt(i).equals("}")||"]"||")"){
    //如果是右括号,则先判断栈中有没有左括号,没有,则说明右括号多了
        if(s.isEmpty()){
            pf("右括号多了");
            break;
        }
        //判断右括号是否和左括号匹配
        if(str.charAt(i).matches(s.peek()){
        //匹配则栈顶元素出栈,即那个匹配的左括号
            s.pop();
        }else{
        //否则,就不匹配啊
            pf("不匹配了啊");
            break;
        }//if else
    }//else if
}//for
//上面循环,把所有字符串压入栈中,并且全部出栈
//如果栈不为空,说明压入的多了,说明左括号多了。
if(!s.isEmpty()){
    pf("左括号多了");
}

例三:迷宫
走迷宫,利用穷举法。用栈来存我们的路径,数据对象是坐标。

思路:
1、将起始坐标入栈。
2、将起始坐标的可达相邻坐标入栈。
3、栈顶元素赋值给当前坐标。即上一步的可达相邻坐标就是下一步的当前坐标。
4、判断当前坐标是否有可达相邻坐标,如果没有,则出栈,新的栈顶元素赋值给当前坐标。也就是相当于退回一步。
5、如果当前坐标等于中点坐标,那就出来了。

将以上思路整理一下,一定需要循环判断当前坐标是否有可达下一坐标,并且一定需要循环判断当前坐标是否等于终点坐标。

伪代码:

initStack(s);//初始化栈s
s.push(入口坐标);//入口坐标入栈
if(入口坐标==出口坐标){
    pf("玩呢?");//迷宫就一个格
}
当前坐标=s.peek();
while(当前坐标!=终点坐标){
    if(当前坐标有可达相邻坐标){
        s.push(此可达相邻坐标);
    }else{
        s.pop();
    }
    当前坐标=此可达相邻坐标;
}
没那么简单,实现还要定义规则,若有多个可达相邻坐标,按哪种优先级进行走。出栈后,还需标记此坐标已经被走过等等。

例n:程序设计语言编译中的表达式求值
要对以下算术表达式求值:
4+2×3 - 10/5

科普一下色泽运算法则,我觉得应该有人不会。
算术四则运算的规则:
(1)先乘除,后加减;
(2)从左算到右;
(3)先括号内,后括号外。

基本思想:
1)首先置操作数栈为空栈,表达式起始符#为运算
符栈的栈底元素;
2)依次读入表达式中每个字符,若是操作数则进
opnd栈,若是运算符,则和optr栈的栈顶运算符
比较优先权后作相应操作,直至整个表达式求值完毕。

还有好多例子,不看了,耽误时间。递归、汉诺塔问题等等好多经典的算法。

队列

定义:队列是限定只能在表的一端进行插入,在表的另一端进行删除的线性表
队尾(rear)——允许插入的一端
队头(front)——允许删除的一端

特点:先进先出(FIFO)

同样的,链式和非链式的实现方法,你想怎么实现就怎么实现。所以队列也分链式队列和循环队列。循环队列就是利用数组,是一个顺序的映像。

链式映像的实现就像这样

public class MQueue<E> {
    private int size;
    private mLink<E> head;

    public MQueue() {
        head = new mLink<>(null, null, null);
        head.next = head;
        head.previous = head;
        size = 0;
    }

    public mLink<E> getHead() {
        return head;
    }

    public static final class mLink<ET> {
        public ET data;
        public mLink<ET> previous, next;

        public mLink(ET o, mLink<ET> previous, mLink<ET> next) {
            this.data = o;
            this.previous = previous;
            this.next = next;
        }
    }

    /**
     * 入队
     * 供外部类调用
     *
     * @param e 欲添加的元素
     * @return 添加成功否
     */
    public boolean offer(E e) {
        return addLastImpl(e);
    }


    /**
     * 添加最后一个元素
     * 供内部类调用
     *
     * @param e 欲添加的元素
     * @return 添加成功与否
     */
    private boolean addLastImpl(E e) {
        //添加新链
        mLink<E> oldLastLink = head.previous;//尾元素
        mLink<E> newLink = new mLink<>(e, oldLastLink, head);//新尾元素
        //删掉之前的旧链
        head.previous = newLink;
        oldLastLink.next = newLink;
        size++;//元素个数加一
        return true;
    }

    /**
     * 出队
     *
     * @return 如果有元素,则返回出队的元素,否则返回空;
     */
    public E Poll() {
        return (size == 0) ? null : removeFirstImpl();
    }

    /**
     * 出队实现
     *
     * @return
     */
    private E removeFirstImpl() {
        mLink<E> firstData = head.next;//第一个元素
        if (firstData == head)//判断是否只有两个元素,只有两个元素在做下面的操作无意义,浪费时间
            throw new NoSuchElementException();
        head.next = firstData.next;
        firstData.next.previous = head;
        size--;
        return firstData.data;
    }

    //获取元素个数
    public int getSize() {
        return size;
    }
}

以上是链式的队列,除此之外我们还可以实现一个顺序的队列,利用数组。

假设一个数组的长度n固定,已知我们在队尾添加元素入队列,在队头出队,当我们添加元素添加了n个的时候,此时队列满了,而如果此时我们在队列的队头出队一个元素,这时候理应可以添加一个元素的,但是我们的队尾却已经到头了,如图。
这里写图片描述

下标为0、1、2处的元素已经出队,但是入队时却入不了了,因为队尾已经到头。这时候我们可以用循环队列来解决。类似这样:

这里写图片描述

public class MArrayQueue {
    private int maxsize;//最大元素个数,则元素下标的最大值=maxsize-1;
    private long[] queArray;
    private int front;//头“指针”
    private int rear;//尾“指针”
    private int nItems;//元素个数

    public MArrayQueue(int s) {
        maxsize = s;
        queArray = new long[maxsize];
        front = 0;
        rear = -1;
        nItems = 0;
    }

    public void insert(long j) {
        if (isFull()) {
            throw new IndexOutOfBoundsException("out of bound");
        }
        if (rear == maxsize - 1) {//如果之前已经加到了顶,那么这次加之前让rear回到-1
            rear = -1;
        }
        queArray[++rear] = j;//先移动,后插入。简称后入。
        nItems++;
    }

    public long remove() {
        if (isEmpty()) {
            throw new NullPointerException("the element you remove is null,may be the queue is empty");
        }
        /**
         * 这里,我们把指针移动看做此处元素已放弃(出队、删除、移除)。
         * 因为下次我们插入如果插到这里的话,虽然这里的元素没有移除,但会被我们覆盖掉。
         */
        long temp = queArray[front++];
        if (front == maxsize) {
            front = 0;
        }
        nItems--;
        return temp;
    }

    public long peekFront() {
        return queArray[front];
    }

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

    public boolean isFull() {
        return nItems == maxsize;
    }

    public int getSize() {
        return nItems;
    }
}

此时,每次插入都会nItems++,每次移除都会–,影响效率。所以我们需要实现一个没有nItems的队列。此时难点就在于,对于空和满的判断只能通过rear和front。而满队列和空队列的判断情况就一样了。当(++rear)==front时,即是满,也可能是空。

为了解决这一问题,我们可以让maxsize比我们初始化时希望队列有的元素个数大1。这样就好判断了。

注意:我们想实现的队列元素个数只有s个,我们的maxsize为s+1,但是我们只能最多插入s个元素进来。maxsize之所以=s+1是为了很好的分辨满队列和空队列而已。

这时候分两种情况。
1、当rear>front时。
这时候满队列的条件是:front+maxsize-2==rear。
空队列的条件是:front+maxsize-1==rear。
2、当rear< front时。
这时候满队列的条件是:rear+2==front。
空队列的条件是:rear+1==front。

这些条件也可以很好的得出,自己举一个例子,比如队列的实现大小是5,所以我们的maxsize=6。这时候根据满队列和空队列,rear和front可能在的位置,就可以验证以上条件了。

public class MArrayQueueF {
    private int maxsize;//最大元素个数,则元素下标的最大值=maxsize-1;
    private long[] queArray;
    private int front;//头“指针”
    private int rear;//尾“指针”

    public MArrayQueueF(int s) {
        maxsize = s + 1;
        queArray = new long[maxsize];
        front = 0;
        rear = -1;
    }

    public void insert(long j) {
        if (isFull()) {
            throw new IndexOutOfBoundsException("out of bound");
        }
        if (rear == maxsize - 1) {//如果之前已经加到了顶,那么这次加之前让rear回到-1
            rear = -1;
        }
        queArray[++rear] = j;//先移动,后插入。简称后入。
    }

    public long remove() {
        if (isEmpty()) {
            throw new NullPointerException("the element you remove is null,may be the queue is empty");
        }
        /**
         * 这里,我们把指针移动看做此处元素已放弃(出队、删除、移除)。
         * 因为下次我们插入如果插到这里的话,虽然这里的元素没有移除,但会被我们覆盖掉。
         */
        long temp = queArray[front++];
        if (front == maxsize) {
            front = 0;
        }
        return temp;
    }

    public long peekFront() {
        return queArray[front];
    }

    public boolean isEmpty() {
        return (rear + 1 == front || front + (maxsize - 1) == rear);
    }

    public boolean isFull() {
        return (rear + 2 == front || front + (maxsize - 2) == rear);
    }

    public int getSize() {
        if (rear < front) {
            return (maxsize + 1 + rear - front);
        } else {
            return (rear - front + 1);  
        }
    }
}

获取元素个数的,很简单就导出结论了。也是分rear< front和rear>front的两种情况。

这里写图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值