栈的链式存储结构
上一次我们讲解了链式存储结构里面的基础链表,今天我们先来讲讲栈的链式存储结构,我们将它命名为LinkedStack,当然我们也要实现之前写的Stack接口里面的方法。跟我们之前线性存储结构一样,我们之前写了LinkedList,我们是不是就可以用它里面的共同特性来写栈(相当于对LinkedList封装起来)。
强调一点,Java中的LinkedList本身就是双向的。
#进栈
我们需要知道栈的方式,我们通常这样说栈的特点,先进后出,那么此时问题来了,我们需要将头当作栈顶还是将尾当作栈顶,我们将头当作栈顶,它的进栈和出栈时间复杂度都是O(1),而我们将尾当作栈顶时候,进栈时间复杂度是O(1),而出栈时间复杂度是O(n),两者相比,我们的结果肯定是选择头当栈顶,那么代码就好写了。
代码实现:
/**
* 进栈一个元素
*/
@Override
public void push(E e) {
list.addFirst(e);
}
出栈
通过刚才我们分析,出栈也是从头出,因为此时的时间复杂度为O(1),也就是相当LinkedList用头删法,出栈一个元素。
代码实现:
/**
* 出栈一个元素用链表的头删法
* */
@Override
public E pop() {
return list.removeFirst();
}
获取当前栈顶的元素
通过分析,因为栈的链式存储结构,就相当于我们给链表规定了那边为栈顶那边为栈尾,所以因为每次我们都是从头插入的元素,那么这就好写多了。
代码实现:
/**
* 获取当前栈顶的元素,也就是获取链表的头元素
* */
@Override
public E peek() {
return list.getFirst();
}
清空栈
当然这也就是相当于把链表清空,所以直接调用就可以。
代码实现:
/**
* 清空栈
* */
@Override
public void clear() {
list.clear();
}
总体来说,当我们将LinkedList掌握,LinkedStack就好写,也比较方便。
全局代码实现:
package com.study.动态链表;
import com.study.zhan.Stack;
public class LinkedStack<E> implements Stack<E> {
private LinkedList<E> list;
public LinkedStack() {
list = new LinkedList<E>();
}
@Override
public int getSize() {
return list.getSize();
}
@Override
public boolean isEmpty() {
return list.isEmpty();
}
/**
* 进栈一个元素用链表的头插法
*/
@Override
public void push(E e) {
list.addFirst(e);
}
/**
* 出栈一个元素用链表的头删法
* */
@Override
public E pop() {
return list.removeFirst();
}
/**
* 获取当前栈顶的元素,也就是获取链表的头元素
* */
@Override
public E peek() {
return list.getFirst();
}
/**
* 清空栈
* */
@Override
public void clear() {
list.clear();
}
/*
* 重写toString方法(non-Javadoc)
* 当然这里我们用StringBuilder,为何不用String
* 原因是前者能动态存储我们的数据,可以避免出现一切意外情况
*/
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("LinkedStack: size="+getSize()+"\n");
if(isEmpty()) {
sb.append("[]");
} else {
sb.append('[');
for(int i=0;i<getSize();i++) { //时间复杂度o(n)
sb.append(list.get(i));
if(i!=getSize()-1) {
sb.append(',');
} else {
sb.append(']');
}
}
}
return sb.toString();
}
/**
* 重写equals方法
* 这里因为我们对LinkedList进行封装,所以我们是不是可以调用List的equals方法,将两对象相比较
* */
@Override
public boolean equals(Object obj) {
if(obj == null) {
return false;
}
if(obj == this) {
return true;
}
if(obj instanceof LinkedStack) {
LinkedStack<E> ls = (LinkedStack<E>) obj;
return list.equals(ls.list);
}
return false;
}
}
队列的链式存储结构
队列特点:先进先出,后进后出。
这里我们同样命名一个LinkedQueue类,同样这个要实现我们的Queue接口,让后实现里面的方法,那么问题来了,我们队列是不是也需要确定队头和队尾,那我们该如何确定队头队尾呢?
这里先告诉大家答案,肯定是尾进头出。
那么为什么?我们分析一波,你不管在头删还是头增,此时的时间复杂度都是为O(1),而在尾增时间复杂度是O(1),尾删是O(n),因为需要找到尾的前一个元素才能删尾。进给尾我们只需要移动尾往后一位,出用头删法,此时的进出时间复杂度都为O(1)。
那么此时代码就可以很流畅的往下写,下面就是各个方法的具体代码实现。
清空队列
清空队列直接将里面所有的元素移除。
代码实现:
/**
* 清空队列
* */
@Override
public void clear() {
list.clear();
}
进队
进通过之前的分析,我们将链表的尾当作队列的队尾,相当于将元素直接给链表进行尾插法。
代码实现:
/**
* 进队一个元素,相当于运用链表的尾插法
* */
@Override
public void enqueue(E e) {
list.addLast(e);
}
出队
之前的分析应该可以明白,就相当于用链表的头删法。
代码实现:
/**
* 出队一个元素
* */
@Override
public E dequeue() {
return list.removeFirst();
}
获取队头元素
获取队列头元素,相当于获取链表的头指针的下一个元素。
代码实现:
/**
* 获取队头元素
* */
@Override
public E getFront() {
return list.getFirst();
}
获取队尾元素
获取队尾元素,也就是相当于获取链表的尾指针元素。
代码实现:
/**
* 获取队尾元素
* */
@Override
public E getRear() {
return list.getLast();
}
总结一下,如果我们将链表掌握通透,像我们的栈和队列写起来就很轻松,当然如果你链表写的很差,那么栈和队列实现起来就会有许多莫名起名的错误,当然在对列中我们也需要重写toString和equals方法,这两个方法在具体实现代码里面有解析。
具体实现代码:
package com.study.动态链表;
import com.study.duilie.Queue;
public class LinkedQueue<E> implements Queue<E> {
// 头出尾进
private LinkedList<E> list;
public LinkedQueue() {
list = new LinkedList<E>();
}
@Override
public int getSize() {
return list.getSize();
}
@Override
public boolean isEmpty() {
return list.isEmpty();
}
/**
* 清空队列
* */
@Override
public void clear() {
list.clear();
}
/**
* 进队一个元素,相当于运用链表的尾插法
* */
@Override
public void enqueue(E e) {
list.addLast(e);
}
/**
* 出队一个元素
* */
@Override
public E dequeue() {
return list.removeFirst();
}
/**
* 获取队头元素
* */
@Override
public E getFront() {
return list.getFirst();
}
/**
* 获取队尾元素
* */
@Override
public E getRear() {
return list.getLast();
}
/**
* 重写toString方法
* 跟链表和栈的一样,这里就不作过多的解释
* */
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("LinkedQueue: size="+getSize()+"\n");
if(isEmpty()) {
sb.append("[]");
} else {
sb.append('[');
for(int i=0;i<getSize();i++) {
sb.append(list.get(i));
if(i!=getSize()-1) {
sb.append(',');
} else {
sb.append(']');
}
}
}
return sb.toString();
}
/**
* 重写equals方法
* 首先当你新创建的对象为空时,肯定返回false
* 如果你的队列与自己本身相比较,那当然是返回true
* 当你重新创建一个队列对象,也可以调用LinkedList的equals方法,他们的实现是一样的
* */
@Override
public boolean equals(Object obj) {
if(obj == null) {
return false;
}
if(obj == this) {
return true;
}
if(obj instanceof LinkedQueue) {
LinkedQueue<E> queue = (LinkedQueue<E>) obj;
return list.equals(queue.list);
}
return false;
}
}
单向循环链表
将单链表中尾结点的指针用空改为头结点(或第一个元素结点),就使整个单链表形成一个环,这种头尾相接的单链表成为单向循环链表。
这里也就是说,我们之前不是说,我们之前的链表都是通过虚拟头结点来操作的,而单向循环链表这里我们用的是真实头结点。如下图
如果我们再来几个元素,形成下面图所示
接下来我们进行分析,如果此时我们想要循环链表为空时条件应该是什么,因为是真实头结点,那么是不是意味着我们只要将头尾结点同时指向空,那么此时的单循环链表就为空,如图所示。
当我们新来一个元素是不是意味着我们头和尾都要移动,那么此时相当于A的下一跳指向A本身,如图所示。
那么问题又来了,当我们再进入一个元素时候,该怎么办,那么对照上图,这时候我们是不是想到也应该有头插法和尾插法。此时head指向元素A,头插法之前因为是虚拟头结点,直接给虚拟头结点之后插元素就可以,那么此时呢,我们是不是应该给head也就是A元素之前插入一个新元素,当然尾插很好理解,直接给尾部来一个新元素。
那么我们来说一下头插,如下如图,我们想要给A元素之前插入一个元素C,我们怎么操作。
我们可以这样做,先将当前尾的下一跳给新元素,再将新元素的下一跳给头,然后头移动到新元素处,如下图。
那么头删呢,这里我们直接拿上图说i,将C删除,操作就是将当前头给头的下一跳,然后断开C再将尾的下一跳指向头,是不是就将C元素删除。
再来看看删尾,那我们就只能先确定尾的前驱,然后再删除尾,操作就是将B的下一跳给A的下一跳,然后尾再移动回来,当删除的只剩一个元素时,我们是不是就可以将头和尾赋值为空就行。
下面就分别来代码实现各个操作,我们创建一个单循环链表类,命名为LoopSingle,此时我们是不是用LinkedList不能实现它,但是它的底层还因该是一个链表吧,所以我们这里就实现List接口,进行方法重写。
判空
根据之前的分析,因为使用的真实头结点,我们将它的头结点和尾结点赋值为空,当然为了保险起见我们也可以让它的元素为空。
代码实现:
@Override
public boolean isEmpty() {
return size==0&head==null&&rear==null;
}
通过角标添加元素
我们先判断角标范围,当角标小于0或者大于元素个数,抛出异常插入角标非法,当角标合法时,在进行判断如果链表为空时,相当于空链表插入第一个元素,直接插入,角标为0相当于头插,当角标为元素个数相当于尾插,下来就是一般情况。
代码实现:
/**
* 通过角标添加元素
* */
@Override
public void add(int index, E e) {
if(index<0||index>size) {
throw new IllegalArgumentException("插入角标非法");
}
Node n = new Node(e,null);
if(isEmpty()) { //特殊情况
head=n;
rear=n;
rear.next=head;
}else if(index==0) { //头插
n.next=head;
head=n;
rear.next=head;
}else if(index==size) { //尾插
n.next=head;
rear.next=n;
rear=n;
}else { //一般情况
Node p = head;
for(int i=0;i<index-1;i++) {
p=p.next;
}
n.next=p.next;
p.next=n;
}
size++;
}
头插
相当于0角标元素插入元素。
代码实现:
@Override
public void addFirst(E e) {
add(0,e);
}
尾插
相当于元素个数后一位插入元素。
代码实现:
@Override
public void addLast(E e) {
add(size,e);
}
获取指定位置的元素
先判断输入的该位置是否合法,有三种情况,当角标为0,第一个元素。当角标等于size-1,尾元素,一般情况要进行一个临时指针P然后变量,然后一个一个找。
代码实现:
@Override
public E get(int index) {
if(index<0||index>=size) {
throw new IllegalArgumentException("查找角标非法");
}
if(index==0) {
return head.data;
}else if(index==size) {
return rear.data;
} else {
Node p = head;
for(int i=0;i<index;i++) {
p=p.next;
}
return p.data;
}
}
通过角标删除元素
我们先判断角标范围,当角标小于0或者大于元素个数,抛出异常删除角标非法,当角标合法时,在进行判断如果链表size=1时,相当于链表只有一个元素,头和尾给一个空就可以,size=0。角标为0相当于删头,当角标为元素个数相当于尾删,下来就是一般情况。
代码实现:
@Override
public E remove(int index) {
if(index<0||index>=size) {
throw new IllegalArgumentException("删除角标非法");
}
E res =null;
if(size==1) {//特殊情况只有一个元素
res=head.data;
head=null;
rear=null;
size=0;
} else if(index==0) {
res=head.data;
head=head.next;
rear.next=head;
} else if(index==size-1) {
res = rear.data;
Node p = head;
while(p.next!=rear) {
p=p.next;
}
p.next=rear.next;
rear=p;
} else {
Node p = head;
for(int i=0;i<index-1;i++) {
p=p.next;
}
Node del = p.next;
res = del.data;
p.next = del.next;
del.next = null;
}
size--;
return res;
}
删除头元素
相当于删除指定index=0的元素
代码实现:
@Override
public E removeFirst() {
return remove(0);
}
删除尾元素
相当于删除指定的index=size-1的元素
代码实现:
@Override
public E removeLast() {
return remove(size-1);
}
清空链表元素
相当于给头和尾赋值为空,当然元素个数也赋值0;
代码实现:
@Override
public void clear() {
head=null;
rear=null;
size=0;
}
总结一下,对于LoopSingle与LinkedList一个用了虚拟头结点,一个是真实头结点,对于删除和增加都要分析特殊情况。同样也需要进行重写toString方法。
具体代码实现:
package com.study.动态链表;
import com.study.shuzu.List;
import sun.management.counter.StringCounter;
@SuppressWarnings("unused")
public class LoopSingle<E> implements List<E> {
/**
* 单向链表的结点类
* */
private class Node{
E data;//数据域
Node next;//指针域
public Node() {
this(null,null);
}
public Node(E data,Node next) {
this.data = data;
this.next = next;
}
@Override
public String toString() {
return data.toString();
}
}
private Node head;
private Node rear;
private int size;
public LoopSingle() {
head = null;
rear = null;
size = 0;
}
public LoopSingle(E[] arr) {
}
@Override
public int getSize() {
return size;
}
@Override
public boolean isEmpty() {
return size==0&head==null&&rear==null;
}
/**
* 通过角标添加元素
* */
@Override
public void add(int index, E e) {
if(index<0||index>size) {
throw new IllegalArgumentException("插入角标非法");
}
Node n = new Node(e,null);
if(isEmpty()) { //特殊情况
head=n;
rear=n;
rear.next=head;
}else if(index==0) { //头插
n.next=head;
head=n;
rear.next=head;
}else if(index==size) { //尾插
n.next=head;
rear.next=n;
rear=n;
}else { //一般情况
Node p = head;
for(int i=0;i<index-1;i++) {
p=p.next;
}
n.next=p.next;
p.next=n;
}
size++;
}
@Override
public void addFirst(E e) {
add(0,e);
}
@Override
public void addLast(E e) {
add(size,e);
}
@Override
public E get(int index) {
if(index<0||index>=size) {
throw new IllegalArgumentException("查找角标非法");
}
if(index==0) {
return head.data;
}else if(index==size) {
return rear.data;
} else {
Node p = head;
for(int i=0;i<index;i++) {
p=p.next;
}
return p.data;
}
}
@Override
public E getFirst() {
return get(0);
}
@Override
public E getLast() {
return get(size-1);
}
@Override
public void set(int index, E e) {
if(index<0||index>size) {
throw new IllegalArgumentException("修改角标非法");
}
if(index==0) {
head.data=e;
}else if(index==size) {
rear.data=e;
} else {
Node p = head;
for(int i=0;i<index;i++) {
p=p.next;
}
p.data=e;
}
}
@Override
public boolean contains(E e) {
return find(e)!=-1;
}
@Override
public int find(E e) {
if(isEmpty()) {
return -1;
}
Node p =head;
int index=0;
while(p.data!=e) {
p=p.next;
index++;
if(p==head) {
return -1;
}
}
return index;
}
@Override
public E remove(int index) {
if(index<0||index>=size) {
throw new IllegalArgumentException("删除角标非法");
}
E res =null;
if(size==1) {//特殊情况只有一个元素
res=head.data;
head=null;
rear=null;
size=0;
} else if(index==0) {
res=head.data;
head=head.next;
rear.next=head;
} else if(index==size-1) {
res = rear.data;
Node p = head;
while(p.next!=rear) {
p=p.next;
}
p.next=rear.next;
rear=p;
} else {
Node p = head;
for(int i=0;i<index-1;i++) {
p=p.next;
}
Node del = p.next;
res = del.data;
p.next = del.next;
del.next = null;
}
size--;
return res;
}
@Override
public E removeFirst() {
return remove(0);
}
@Override
public E removeLast() {
return remove(size-1);
}
@Override
public void removeElement(E e) {
remove(find(e));
}
@Override
public void clear() {
head=null;
rear=null;
size=0;
}
/**
* 重写toString方法
* */
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("LoopSingle: size="+getSize()+"\n");
if(isEmpty()) {
sb.append("[]");
} else {
sb.append('[');
Node p = head; //创建临时节点P
while(true) {
sb.append(p.data);
if(p.next==head) { //当P的下一跳为头,说明元素遍历完此时加上]符号,否则加入,号继续遍历
sb.append(']');
break;
} else {
sb.append(',');
}
p=p.next;
}
}
return sb.toString();
}
单向循环链表的应用
约瑟夫问题
这就是一个典型的用单向循环链表解决的问题,通过凡是环形的都可以用单向循环链表来解决。这里我们先来分析一下,这里我们先那20个数据来分析问题。
此时头指向1,尾指向20,每当到第三个人时就自杀也就相当于删除这个数据,这里要考虑的一点是当你删除的这个数据为头元素或者尾元素时就要进行头结点和尾结点的移动,最后就剩20和13这两个数据,并且打印出死亡顺序,这里我们创建一个临时结点P,先将P节点指向head,当P指到2是删除3,然后此时P相当于移动一次,当然详细的操作见代码解释。
实现代码:
public LinkedList<Integer> josephusLoop(int count,int step){
if(count<=0||step<=0) { //当人数小于0或者指定位死的数位0,异常。
throw new IllegalArgumentException("参数不合法");
}
clear(); //先将链表清空
for(int i=1;i<=count;i++) { //将41个人添加进去
addLast((E)new Integer(i));
}
LinkedList<Integer> list = new LinkedList<Integer>(); //创建一个链表存储死掉的人
Node p =head; //创建一个临时结点P从head开始
while(!isEmpty()) { //判断当前链表是否为空
for(int i=0;i<step-2;i++) { //删3此时P移动一次
p=p.next; //找到删除元素的前驱
}
Node del = p.next; //在创建一个结点del,此时del指向要删除的结点
list.addLast((Integer)del.data); //将del的元素添加到链表
if(size==1) { //当此时只有一个元素,直接跳出,相当于不满足三个人,没有意义
size--;
break;
}
if(del==head) { //当del指向head结点,相当于删除头元素,那么此时将头给它的下一跳
//再将尾的下一跳重新指向head,此时P指向head
head=head.next;
rear.next=head;
size--;
p=head;
} else if(del==rear) { //当del指向rear结点,相当于删除尾元素,那么此时将P的下一跳给rear的下一跳
//rear指向P,再将P指向head
p.next=rear.next;
rear=p;
size--;
p=head;
} else { //除上面特殊情况下,直接将P的下一跳给del的下一跳,del的下一跳为空,del指向空,
//P指向P的下一跳
p.next=del.next;
del.next=null;
del=null;
size--;
p=p.next;
}
}
return list; //返回死亡的元素添加的链表
}
魔术师发牌
问题来了,就是这些牌的原先顺序是什么?来个图更直观的说明问题
再来一张原先牌的顺序,当然这里是我们梳理出来的,来说明问题,现实中是需要我们进行排序才能达到这种操作。
那么这个顺序到底是怎么通过算法实现的来的?其实我们想想是不是就是意思,当我们说一时,拿走A,说二的时候重新往后走两次,这张牌肯定就是2,然后拿走2,说三的时候重新往后走三次,这张牌肯定是3,再拿走这张牌,然后重复操作,相当于每次拿走说的牌重新组成一个链表然后继续走下去,那么详细解释见代码解释。
代码实现:
public void magicPoker() {
head=null;
rear=null;
size=0;
for(int i=0;i<13;i++) { //记录元素个数
addLast((E)new Integer(0));
}
Integer pokerNumber=1; //定义第一个牌
Node p=rear; //创建一个临时结点P指向rear
while(true) {
for(int i=0;i<pokerNumber;) { //循环判断走的步数
p=p.next;
if(p.data.equals(0)) { //如果此时P的值为0,当前没有牌继续走
i++;
}
}
p.data=(E) pokerNumber; //将牌放入
pokerNumber++; //牌面往上加1
if(pokerNumber.equals(14)) { //因为只有13张牌,当牌面=14时,跳出
break;
}
}
System.out.println(this); //输出链表
}