草稿箱捉到一篇
介绍
都是照着当初年少无知时候的课堂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昨天我写到这里的时候,优快云并没有保存草稿成功。所以我又要重写一遍下面。
例二:
括号匹配的检验,如果括号不匹配,请打印错误。
[ ( [ ] [ ] ) ]
伪代码,提供思路:
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的两种情况。