为了更好地理解双向链表,先说几个相关的概念
List:特性:必须按照插入的顺序来保存元素
第一种实现:最常用的arrylist:结构类似于数组,所以访问元素的时候可以直接通过索引来访问任何位置的数据,但是当插入元素的时候默认是追加到数组的最后,那么数组当中原有的元素的位置不变 只是申请开辟了一块内存空间和新增加了一个索引,其他都没有变化,但是当向其他位置插入元素的时候,会先申请开辟一块内存空间和一个新索引 但是这个索引不是给新插入元素使用的,而是给数组当中最后一个元素使用的,新元素会插入到指定索引位置 代替原索引处的元素 并将该元素以及其后面的所有元素全部向后移动,所以这个是浪费时间的,而删除呢就是一样的原理,队尾删除很简单,其他位置删除。位于被删除元素后面的元素的位置全部向前移动。所以一样很浪费时间。所以该实现只是适用于随机访问元素或者遍历元素,因为他的底层是由数组来实现的。
第二种常用的实现是:linkedlist,它是基于链表实现的,链表有很多种
链表是一系列的节点组成的,这些点不需要在内存中相连
单链表是由一个头结点开始。然后依次插入新的节点。每个节点包含两个部分一个是数据的引用或者基本类型的数据 和下一个节点的存储地址。这样一个链表向外暴露的只是第一个头结点。所以只要知道头结点就可以直接找到剩下其余的节点。
单链表的内存结构如下图:
头结点不存储数据 其他节点存储的结构看下图 是数据加上下一个节点的地址
双向链表:双向链表的每一个结点都有一条指向其后继结点的next链和一条指向其前结点的pre链。双向链表既可以从第一项开始遍历也可以从最后一项开始往前遍历,双向链表可以用下图表示:
java的linkedlist实现了双向链表 看下linkedlist的用法
import java.util.ArrayList;
import java.util.LinkedList;
import org.junit.Before;
public class Test {
public static LinkedList<String> dl;
public static ArrayList<String> l;
@Before
public void init() {
dl = new LinkedList<String>();
dl.add("N1");
dl.add("N2");
dl.add("N3");
dl.add("N4");
dl.add("N5");
l = new ArrayList<String>();
l.add("N1");
l.add("N2");
l.add("N3");
l.add("N4");
l.add("N5");
}
@org.junit.Test
public void test() {
for (String str : dl) {
System.out.println("双向链表:" + str);
}
for (String str : l) {
System.out.println("普通arrylist:" + str);
}
}
@org.junit.Test
public void add() {
// 双向链表独有的向链表头部添加元素
dl.addFirst("N6");
dl.addLast("N7");
}
@org.junit.Test
public void del() {
dl.remove("N1");
for (String str : dl) {
System.out.println("双向链表:" + str);
}
l.remove("N1");
for (String str : l) {
System.out.println("普通arrylist:" + str);
}
}
}
最后 是java实现双向链表的代码
public class DoubleLink<T> {
private class Node<T>{
//节点值
private T value;
//前一个节点
private Node<T> prev;
//后一个节点
private Node<T> prex;
public Node(T value, Node<T> prev, Node<T> prex) {
this.value = value;
this.prev = prev;
this.prex = prex;
}
}
//链表长度
private int size;
//头结点
private Node<T> head;
public DoubleLink() {
/**
* 头结点不存储值 并且头结点初始化时 就一个头结点。
* 所以头结点的前后节点都是自己
* 并且这个链表的长度为0;
*/
head = new Node<>(null, null, null);
head.prev = head.prex;
head = head.prex;
size = 0;
}
public int getSize() {
return this.size;
}
/**
* 判断链表的长度是否为空
*/
public boolean isEmplty(){
return size == 0;
}
/**
* 判断索引是否超出范围
*/
public void checkIndex(int index){
if(index<0||index>=size){
throw new IndexOutOfBoundsException();
}
return;
}
/**
* 通过索引获取链表当中的节点
*
*/
public Node<T> getNode(int index){
/**
* 检查该索引是否超出范围
*/
checkIndex(index);
/**
* 当索引的值小于该链表长度的一半时,那么从链表的头结点开始向后找是最快的
*/
if(index<size/2){
Node<T> cur = head.prex;
for(int i=0;i<index;i++){
cur = cur.prex;
}
return cur;
}
/**
* 当索引值位于链表的后半段时,则从链表的另端开始找是最快的
*/
/**
* 此
*/
Node<T> cur = head.prev;
int newIndex = size - (index+1);
for(int i=0;i<newIndex;i++){
cur = cur.prev;
}
return cur;
}
/**
* 获取节点当中的值
*/
public T getValue(Node<T> cur){
return cur.value;
}
/**
* 获取第一个节点的值
*/
public T getFirst(){
return getValue(getNode(0));
}
/**
* 获取最后一个节点的值
*/
public T getLast(){
return getValue(getNode(size-1));
}
/**
* 插入节点
*/
public void inesert(int index,T value){
//如果这次插入时 链表是空的
if(index==0){
//这个节点的
Node<T> cur = new Node<T>(value, head, head.prex);
head.prex.prev = cur;
head.prex = cur;
size++;
return;
}
/**
* 先根据给出的插入位置 找到该链表原来在此位置的节点
*/
Node<T> node = getNode(index);
/**
*放置的位置的前一个节点就是原节点的前置节点 而后节点就是原节点
*/
Node<T> cur = new Node<T>(value,node.prev,node);
/**
* 现将该位置也就是 原节点的前节点的后节点 赋值成为新节点
* 然后将新节点的后置节点的值赋值成为原节点
*/
node.prev.prex = cur;
node.prev = cur;
size++;
}
/**
* 向表头插入数据
*/
public void insertTo(T Value)
{
inesert(0,Value);
}
/**
* 将元素插入到链表的尾部
*/
public void insertTotatil(T vlaue){
Node<T> cur = new Node<>(vlaue,head.prev, head);
//head.prev 代表原来的尾部节点
//遵循两个原则 一 新插入节点的前一个节点的后一个节点为新节点。新节点的后一个节点的前一个节点是新节点
head.prev.prex = cur;
head.prev = cur;
size++;
}
/**
* 删除节点的方法
*/
public void del(int index){
checkIndex(index);
Node<T> cur = getNode(index);
//记住此时的指针还没断开 赋值以后才相当于断开
cur.prev.prex = cur.prex;
cur.prex.prev = cur.prev;
size--;
cur = null;
return;
}
/**
* 删除第一个节点
*/
public void delFirst(){
del(0);
}
/**
* 删除最后一个节点
*/
public void delLast(){
del(size-1);
}
}