栈 ? 队列 ? 轻轻松松.

一: 栈(Stack)

1.1 概念

栈是一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出的原则。我们来用图形理解一下.

如图所示:
在这里插入图片描述

压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
出栈:栈的删除操作叫做出栈。出数据在栈顶。

如图所示:3最后入栈,但是出栈的时候是3先出.即后进先出
在这里插入图片描述

1.2 栈中的常用方法.

方法 功能作用
Stack() 构造一个空栈
E push(E e) 将e入栈并且返回e
E pop() 将栈顶元素出栈并且返回
E peek() 获取栈顶元素但是不出栈
int size() 获取栈中有效元素个数
boolean empty() 检测栈是否为空

对上述方法来进行简单的使用,我们直接上代码

public static void main(String[] args){
    
    
	//创建一个空的栈,站里面存的数据类型为Integer型.
	Stack<Integer> s=new Stack<>();
	//往栈里面push元素.
	s.push(1);
	s.push(2);
	s.push(3);
	s.push(4);
	//获取栈中元素个数,此时元素个数为4
	System.out.println(s.size());
	//获取栈顶元素,此时的栈顶元素为4,因为4是最后一个入栈的元素.
	System.out.println(s.peek());
	//将栈顶元素出栈,并且返回栈顶元素
	s.pop();
	//再次打印栈中的元素个数,来看4是否已经出栈
	System.out.println(s.size());		//元素个数为3
	System.out.println(s.peek());		//栈顶元素为3
	//判断栈是否为空.
	if(s.empty){
    
    
		//显而易见,此栈不为空.
		System.out.println("s is null");
	}else{
    
    
		System.out.println("s is not null");
	}
}

1.3 栈的模拟实现(主要学思想)

这个部分我们对1.2中的方法来进行一个简单的实现,这里我们会使用泛型,没有接触过的好兄弟可以先去了解一下.这里就不过多解释了.

a: Stack()

//类似于一个线性表
public class Stack<E>{
    
    
	E[] array;
	//size,用来表示栈中总共有多少个元素 ||size-1表示栈顶元素的位置
	int size;
	public Stack(){
    
    
		//将array初始化,并且将Object类型进行强制类型转换转换为E型的.
		array=(E[])new Object[10];
		//元素个数初始化为0
		size=0;
	}

b: push().

	//入栈(相当于尾插)
	public void push(E e){
    
    
		//如果数组容量不足,则自动进行扩容.
		ensureCapacity();
		//将元素e放在size的位置.
		array[size]=e;
		//元素入栈以后,元素个数+1;
		size++;
	}
	//进行扩容
    private void ensureCapacity() {
    
    
        if (size==array.length){
    
    
        	//如果元素个数和数组容量相同时,我们进行扩容.
        	//将容量扩大两倍
            int newCapacity=size*2;
            //用array来接收所申请到的容量.
            array= Arrays.copyOf(array,newCapacity);
        }
    }
}

至于为什么要放在size的位置,我们来看图:
在这里插入图片描述
图中的元素个数为5,我们直接将要插入的元素e直接放在下标为5的位置,再将size++向后走一步即可实现入栈(尾插).

c: peek()

//返回栈顶元素,不用删除
    public E peek(){
    
    
    	//对栈进行判空,为空肯定就不能出栈了.我们在此处抛出栈空的异常
        if (empty()){
    
    
            throw new RuntimeException("peek:栈是空的");
        }
        //返回栈顶元素.
        return array[size-1];
    }

在这里插入图片描述size-1位置的元素刚好是最后一个元素.

d: pop()

//删除栈顶元素并且返回栈顶元素
 public E pop(){
    
    
 		//对栈是否为空进行判断
        if (empty()){
    
    
            throw new RuntimeException("pop:栈是空的");
        }
        //先用e将栈顶元素标记起来,因为最后要返回栈顶元素.
        //peek()方法得到的就是栈顶的元素.
        E e=peek();
        //size-1即可实现出栈
        size--;
        //返回开始标记的栈顶元素e
        return e;
    }

直接上图:
在这里插入图片描述第一个图中,数组中元素个数为5个,经过size - -,让数组中有效元素个数变为了4个,即可实现删除元素,即出栈.刚开始我们用e标记了栈顶元素,所以最后直接返回e就行.

e: size()

//返回栈中元素的个数.
    public int size(){
    
    
        return size;
    }

f: empty()

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

栈在这就结束了,大家一定要记住栈是后进先出,我们接下来看队列.

二:队列(Queue)

2.1概念

队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出的特点.
入队列:进行插入操作的一端称为队尾
出队列:进行删除操作的一端称为队头

看图理解
在这里插入图片描述如图,1从队尾先进队列,从队头先出队列.

2.2 队列中的常用方法

方法 功能作用
boolean offer(E e) 入队列
E poll() 出队列
E peek() 获取队头元素
int size() 获取队列中有效元素个数
boolean isEmpty() 检测队列是否为空

队列中所有的方法时间复杂度都为1.

对上述的方法来进行简单的使用.
注意:Queue是个接口,在实例化时必须实例化LinkedList的对象,因为LinkedList实现了Queue接口。

public static void main(String[] args) {
    
    
        Queue<Integer> q=new LinkedList<>();
        q.offer(1);
        q.offer(2);
        q.offer(3);
        q.offer(4);
        q.offer(5);
        q.offer(6);
        System.out.println(q);          // [1, 2, 3, 4, 5, 6]
        System.out.println(q.size());   //队列中元素个数       6
        System.out.println(q.peek());   //获取队头元素         1

        q.poll();                       //删除队头元素

        System.out.println(q.size());   //5
        System.out.println(q.peek());   //2
        System.out.println(q);          //[2, 3, 4, 5, 6]
    }

2.3 队列的模拟实现.

队列中既然可以存储元素,那底层肯定要有能够保存元素的空间,我们这里使用双向链表进行模拟实现.

a: 定义属性

public class Queue<E> {
    
    
	//内部类,用来定义节点的属性.
    public static class Node<E>{
    
    
 	 		E value;
 	 		//指向下一个节点
  	 		Node<E> next;
  	 		//标记前一个节点.
  	 		Node<E> prev;
  	 		//构造方法
  	 		public Node(E val){
    
    
  	 			this.value = val;
  	 		}
   	 }
 	 Node<E> first;		//队头
	 Node<E> last;		//队尾
	 int size;			//队列中有效元素个数.

b: offer()

插入元素的时候要考虑一下,看队列是否为空.
如果队列为空的话可以直接插入.
如果队列不为空时要往最后一个节点的后面进行插入.
注意:在成功插入元素后要让last后移,保证last始终在最后一个位置.

用图来演示一下了可能更加直观:
在这里插入图片描述last.next=newNode;让last的下一个指向新插入的节点.
newNode.prev=last;newNode的前一个节点指向last;

然后last=newNode;即让last往后移动到newNode的位置,也就是此时的最后一个位置.
这样就可以实现插入一个元素.
在这里插入图片描述

//插入元素(尾插)
    public void offer(E val){
    
    
    	//创建值为val的节点
        ListNode<E> newNode=new ListNode<>(val);
        //队列为空时,直接插入.
        if (first==null){
    
    
            first=newNode;
        }else{
    
    
            //不为空时,往最后一个节点后面的位置插入.
            last.next=newNode;
            newNode.prev=last;
        }
        //插入成功后,让last指向最后一个位置.
        last=newNode;
        //元素个数+1;
        size++;
    }

c: poll()

删除队头元素时,我们要考虑三种情况.
a.队列是否为空: 队列为空的时候肯定是不能删除的.
b.队列不为空且队列中只有一个元素.
c.队列中有多个元素.

第一种情况就不需要看了,直接return null就行.
第二种情况:因为只有一个节点,所以令first和last都为空就行.
first=null;
last=null;

主要来看第三种情况:我们要删除对头元素,也就是节点1.
因为最后要返回对头元素的值,所以我们要先把first.val值保存一下.
然后让first向右移动一位.
在这里插入图片描述

上述操作之后.我们直接让first的前一个节点指向null,然后让first的前指向指向null,就可以删除1号节点,所以元素个数size-1,最后直接返回value的值就行.
在这里插入图片描述

d: peek()

返回栈顶元素的值.
这个直接给出代码就行,相信大家可以看懂.

public E peek(){
    
    
	   //为空时返回空
       if (first==null){
    
    
           return null;
       }
       //不为空时,直接返回first.val.
       return first.val;
    }

e: size()

返回队列中有效元素的个数

public int size(){
    
    
    return size;
    }

f: isEmpty

判断队列是否为空.

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

2.4循环队列

2.4.1 产生的背景

其实除了用双向链表可以模拟实现队列外,还可以使用连续空间实现,但是在使用连续空间的时候,会出现一些问题,循环队列就是用来解决这些问题的而出现的.
假设我们现在使用连续空间来模拟实现队列

用front标记对头元素,用rear表示队尾位置.
假设我们插入的是 1 2 3 4 5 6 六个元素.队列容量为10.
在这里插入图片描述

入队列:

入队列的时候比较简单,我们只需要将元素放到rear的位置,然后rear++就行.
时间复杂度和标准库中一样为O(1).
在这里插入图片描述

出队列:
出队列的时候有两种方式:

方式一:保持front不动,将对头后的元素整体往前搬移一个位置,最后将rear- -一下.
比如删除元素1.时间复杂度为O(N)
在这里插入图片描述

方式二:保持rear不动,front++.如图,相当于将front之前的元素全部出队列了.
时间复杂度为O(1).
在这里插入图片描述

在方式二中,假设继续给数组中添加元素,直到加满.此时的效果图如下:

在这里插入图片描述

此时数组就不能往进添加元素了,front前面的位置没有有效元素,即当前数组的有效元素没有存满,所以还有三个地方空着,这种情况我们称之为队列的假溢出.
为了解决上述使用连续空间实现队列时的假溢出问题,就引入了循环队列.

2.4.2 循环队列的实现

我们这里用

直接上图:
循环队列基础图
在这里插入图片描述

里面的数字是数组元素值,外围的是数组下标.
入队列: 将元素放在rear队尾位置,然后rear往后移动一步.
出队列: front往后走一步.

假设经过入队列后现在环形队列如下图,数组的容量为7,里面现在有7个元素:
当70入完队列后rear++重新走到队头位置.
在这里插入图片描述

当前10和20两个元素出队列后,空出两个位置,有效元素个数此时为5个.
front走到2号位置
在这里插入图片描述

此时如果想要入队列,直接放在rear的位置,rear++就行,解决了上述入队列方式2的队列假溢出的问题.
但是此时又出现了新的问题:如何判断循环队列的空满状态呢?

2.4.3 循环队列的空满判断

我们从上面的图中可以看出,在循环为空的时候,front和rear在同一个位置,但是当元素入队列后,队列满了后,rear又和front相遇了,在同一个位置.此时就不知道怎么判断了.别急,我们慢慢往下看.
方式一:使用count来进行计数.
队列空时:count == 0;
队列满时:count ==array.length;
方式二:少存储一个元素
队列空时:front=rear;
队列满时:(rear+1) % array.length == front

取模的原因,如图:
当rear处于当前位置时,我们看出来rear+1与front处于同一个位置,但是真实情况是
rear+1=7,而front=0,所以此时我们给(rear+1) % array.length,结果为0,就和front相遇了.
在这里插入图片描述

方式三:设置标志位.

flag=false;
入队列时:rear需要向下一个位置移动,同时让flag=true;
出队列时:front需要向下一个位置移动,同时让flag=false;

队列空时:front==rear && flag==false;
当满足上面条件时,flag==false,证明是在出队列之后,front与rear相遇了,
肯定表示队列为空.

队列满时:front==rear && flag==true;
当满足上面条件时,flag==true,证明是在入队列之后,front与rear相遇了,
肯定表示队列满.

2.4.4 队尾元素的获取

情况一:队列未满时,直接获取(rear-1)位置的元素
rear-1=5
array[5]=60;

在这里插入图片描述

情况二:队列满时,获取(rear-1+array.length)位置元素.
为了方便,我们让N=array.length;
rear-1+N=0-1+7=6.
array[6]=70;

在这里插入图片描述

这样看是不是挺麻烦的,还要分两种情况,那么有没有办法用一个式子来表示两种情况呢?答案是有的.
公式就是 array[(rear-1+N)%N].
我们来对上面两种情况进行验证:
a.情况一:
rear=6,N=7;
(rear-1+N)%N=12%7=5;
array[5]=60;
b.情况二:
rear=0,N=7;
(rear-1+N)%N=6%7=6;
array[6]=70;

猜你喜欢

转载自blog.csdn.net/weixin_47278183/article/details/121339937