一、学习过单链表和单向循环链表之后,对于链表结构特点应该已经相当熟悉。而在链表中还有一种双向链表,来第一步看图说话:
从图中可以看出双链表和单链表相比有如下特点:
1、双链表除了拥有后继结点next外还有一个前驱结点prev;
2、存储同样数据多的数据,双链表要不单链表占用更多的空间(双链表要比单链每个结点要多存储一个前继指针prev);
3、支持双向遍历,比单链表更灵活。
二、具体实现:
1、成员变量及构造方法:
package cn.cast.DoubleLinkedList;
/**
* 双链表具体实现类
* @autor Administrator
* @param <T>
*/
public class MyDoubLinkedList<T> implements DoubleLinkedList<T>{
private DNode<T> head;//不带数据的头结点
private DNode<T> tail;//指向尾部的指针
private int length;//链表的长度
public MyDoubLinkedList(){
this.head=this.tail=new DNode<T>();
}
2、结点类比单链表多了个前驱指针;
/**
* 双链表结点类
* @author Administrator
*
*/
public class DNode<T> {
public T data;//存储数据
public DNode<T> prev,next;//前继指针和后继指针
public DNode(T data,DNode<T> prev,DNode<T> next){
this.data=data;
this.next=next;
this.prev=prev;
}
public DNode(T data){
this(data,null,null);
}
public DNode(){
this(null,null,null);
}
public String toString(){
return this.data.toString();
}
}
3、具体实现方法:双链表和单链表的主要差异也主要体现插入、删除、查找、替换四个操作中,下面就从具体分析下这个四个方法
3.1、添加元素:和单链表一样,双链表在插入时同样是有两种方式的操作:一种插入空链表和尾部插入,另一种是从链表的中间插入。如下图:
具体代码实现如下:
/**指定位置添加元素
* @param index
* @param data
* @return
*/
@Override
public boolean add(int index, T data) {
//下标合法性校验
if(index<0){
throw new IndexOutOfBoundsException();
}
//存储数据校验
if(data==null){
throw new NullPointerException("data==null");
}
int j=0;
DNode<T> front = this.head;
//查找要插入结点位置的前一个结点;
while(front.next!=null && j<index){
j++;
front=front.next;
}
//创建需要插入的结点,并让其前继指针指向front,后继指针指向front.next;
DNode<T> q=new DNode<T>(data,front,front.next);
//空双链表的插入和链表尾部插入,无需此操作
if(front.next!=null){
//更改front.next的前继指针
front.next.prev=q;
}
//更改front的后继指针
front.next=q;
//在尾部插入时需要注意更新tail指向
if(front==this.tail){
this.tail=q;
}
return true;
}
3.2、尾部添加元素:
/**
* 尾部添加元素
* @param data
* @return
*/
@Override
public boolean add(T data) {
//存储数据校验
if(data==null){
throw new NullPointerException("data==null");
}
//创建新结点,并让其前继指针指向tail,后继指针指向tail.next
DNode<T> newDNode =new DNode<T>(data,tail,tail.next);
//tail的后继指针指向新结点
tail.next=newDNode;
//修改tail
tail=newDNode;
return true;
}
3.3 删除元素:双链表删除元素与单链表的场景一致,头结点删除,尾部删除和中间删除,具体操作和分析:头结点和尾部删除一致,需要防止 p.next.prev抛空指针的情况,而对于(c)情况则无需关系 p.next.prev的值。
代码示例如下:
/**
* 根据下标删除结点
* @param index
* @return
*/
@Override
public T remove(int index) {
T temp=null;
//合法性校验
int size = length();
if(index<0 || index>size || isEmpty()){
throw new IndexOutOfBoundsException();
}
DNode<T> p=this.head;
int j=0;
//头结点、尾结点、中间结点删除都需要查找结点的位置,所有i<=index
while(p!=null && i<=index){
p = p.next;
j++;
}
//当双链表只有一个结点时或尾部删除时无需此部
if(p.next!=null){
p.next=p.prev;
}
p.prev.next=p.next;
//如果是尾结点
if(p==this.tail){
this.tail=p.prev;//更新末结点的指向
}
temp=p.data;
return temp;
}
获取元素:与单链表一致,只需要找到当前结点并获取值即可,代码实现如下:
/**
* 获取元素
* @param index
* @return
*/
@Override
public T get(int index) {
T temp=null;//存储数据临时变量
int size = length();
//合法性校验
if(index<0 || index>size || isEmpty()){
throw new IndexOutOfBoundsException();
}
int j=0;
DNode<T> p = this.head;
//获取结点的位置
while(p!=null && j<=index){
j++;
p = p.next;
}
if(p!=null){
temp=p.data;
}
return temp;
}
替换元素:和获取结点类似,首先是获取元素,找到要替换的结点,替换存储元素即可,代码示例如下:
/**
* 替换元素
* @param index
* @param data
* @return
*/
@Override
public T set(int index, T data) {
T oldData=null;//旧存储数据临时变量
int size = length();
//合法性校验
if(index<0 || index>size || isEmpty()){
throw new IndexOutOfBoundsException();
}
int j=0;
DNode<T> p = this.head;
//获取结点的位置
while(p!=null && j<=index){
j++;
p = p.next;
}
//用新存储数据替换旧存储数据
if(p!=null){
oldData=p.data;
p.data=data;
}
return oldData;
}
如上,双链表的主要方法已经完成了,和单链表相比不是也很简单。