上一期我们手撕了一个基于双向链表的容器,这一期就手撕最常用的栈和队列吧。
这一期的栈和队列我们还是用不需要扩容的链表实现,以面向手撕编程为纲,一切从简,不考虑泛型,只针对int数据进行设计,也只实现无界栈和队列,省事嘛~
别忘了点个Star哦~
学了数据结构的同学都知道栈是一个LIFO,也就是后进先出的结构,用链表来设计的话,入栈和出栈都只需要对链表尾进行操作,非常的简单。
使用过Java中java.util.Stack的栈容器的同学应该能很快想到,最常用的有以下几种方法:
入栈方法:public int push(int item);
出栈方法:public int pop();
获取栈顶元素:public int peek();
检验栈是否为空:public boolean isEmpty();
public class LinkedStack{
//定义尾节点指针,指向链表尾
private Node tail=null;
//记录栈中的元素个数
private int size=0;
}
//双链表节点类
private class Node{
//前后指针
Node next,pre;
int val;
//构造方法
Node(int val){
this.val=val;
}
}
基于链表的入栈就两个字,尾插,如果不熟悉的话还是要多复习一下链表呀
public int push(int val){
//若栈中没有元素
if(size++==0){
//此时栈中只有一个元素,直接为栈尾赋值
tail=new Node(val);
}else{
//若栈中存在元素,则对栈内链表进行尾插
tail.next=new Node(val);
tail.next.pre=tail;//将新栈尾的pre指针指向旧栈尾
tail=tail.next;
}
return tail.val;//返回栈尾元素值(也就是刚插入的元素)
}
出栈就是返回链尾元素并进行链表尾删
public int pop(){
//若栈为空,返回-1
if(size==0) return-1;
//若栈非空:
--size; //栈的长度自减
Node res=tail;
tail=tail.pre; //尾指针指向原链表尾的前一个元素
return res.val; //返回原栈顶元素
}
这个比pop更简单,不需要尾删,只需要返回链尾元素
//返回栈顶元素
public int peek(){
return tail.val;
}
我们之前实现的方法都维护了size值,只需要看size值是否为0即可
//判断栈是否为空
public boolean isEmpty(){
return size==0;
}
其实可以不用维护这个size值,直接检测tail是否为空即可,但是size值的存在也有它的作用,比如说可以实现一个size方法返回栈的当前长度
public int size(){
return size;
}
LinkedStack ls=new LinkedStack();
ls.push(3);
ls.push(6);
ls.push(9);
System.out.println(ls.isEmpty()); //false
System.out.println(ls.size()); //3
System.out.println(ls.peek()); //9
while(!ls.isEmpty()){
System.out.println(ls.pop()); //顺序输出9,6,3
}
System.out.println(ls.isEmpty()); //true
结果达到预期
队列是一个FIFO,也就是先进先出的数据结构,就和我们平时排队买奶茶一样,谁先排队谁就可以先下单,对于链表来说,入队就是尾插,出队就是获取头元素之后头删(也可以反过来),和栈有略微不同。
由于入队是尾插,出队是头删,只需要用到链表结构的next指针,所以这个队列我们可以使用单链表实现。
我们来看看Java中的Queue接口定义了哪些实例方法:
有入队方法:
出队方法:
获取队头元素方法:
有关于这些方法的区别看这里:java Queue中 remove/poll, add/offer, element/peek区别
方便起见,我们来实现offer,poll和peek方法:
public int offer(int val); //这里的返回值改为int,用来返回刚刚入队的值
public int poll();
public int peek();
以及判定队列元素是否为空的public boolean isEmpty();
//基于单链表的队列
public class LinkedQueue {
//头尾指针,指向链表的头和尾
private Node head,tail;
//记录队列长度
private int size=0;
}
//单链表节点类
private class Node{
Node next;
int val;
Node(int val){
this.val=val;
}
}
public int offer(int val){
//按队列是否为空来决定操作
if(size++==0){
head=new Node(val);
tail=head;
}else{
//单链表的尾插
tail.next=new Node(val);
tail=tail.next;
}
return tail.val;
}
public int poll(){
//按队列是否为空来决定操作
if(size==0) return -1;//队列为空返回-1
//队列不为空时
--size; //队列长度自减
Node res=head; //获取当前链头
head=head.next; //单链表头删
return res.val;
}
//获取队头元素
public int peek(){
return head.val; //直接返回链头元素即可
}
和上面栈的一样
//是否为空
public boolean isEmpty(){
return size==0;
}
LinkedQueue lq=new LinkedQueue();
lq.offer(3);
lq.offer(6);
lq.offer(9);
System.out.println(lq.peek()); //3
while(!lq.isEmpty())
System.out.println(lq.poll()); //依次输出3,6,9
以上对无界的栈和队列用基于链表的形式进行了一个面向手撕简单实现,首先一定要搞懂链表的头插,尾插,头删,尾删这样的基本操作,然后想想这些操作需要依赖节点的哪种指针,是前指针还是后指针?以此来决定在一个结构中用单链表还是双链表。
使用到前指针(pre)的操作:尾删,头插
如:
//尾删
tail=tail.pre;
//头插
head.pre=new Node(val);
head=head.pre;
使用到后指针(next)的操作:尾插,头删
如
//尾插
tail.next=new Node(val);
tail=tail.next;
//头删
head=head.next;
像基于链表实现的栈就需要尾插和尾删这两种操作,需要前指针和后指针,所以需要维护一个双链表,而像队列就只有头删和尾插,只需要使用到后指针,因此只需要使用单链表就可以实现一个队列。
当然,用单链表也可以实现栈,只不过单链表实现的栈,在尾插或者尾删操作之间必须要选一个来承受时间复杂度为O(N)所带来的性能损耗:
假设使用只有尾指针,没有头指针的单链表,在尾删时需要如下操作
--size;
int tmp=size;
tail=head;
while(--tmp>0)
tail=tail.next;
虽然减少了维护头指针带来的一点空间损耗,但却要用O(N)的时间复杂度来进行尾删,是不是得不偿失了呢?