链表在物理上/逻辑上
链表是一种物理存储单元上非连续、非顺序的存储结构。
数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
链表每个结点的组成
链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。
每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。
链表使用过程中的时间复杂度
链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。
简单说,查询慢,增删快。
因为需要加入一个指针域,所以空间开销比较大。
单向链表
它存储的数据分散在内存中,每个结点只能也只有它能知道下一个结点的存储位置。由N各节点(Node)组成单向链表,每一个Node记录本Node的数据及下一个Node。向外暴露的只有一个头节点(Head),我们对链表的所有操作,都是直接或者间接地通过其头节点来进行的。
尝试自己写一个简单的
首先准备好结点(class Node)实现一个单向的,所以只需要准备当前结点的数据,和next地址就可以了。
具体的增删改查方法就是new Node并且处理next 和data就可以了。
public class MyLink<E> {
/**
* 头结点,声明链表的时候,实例化一个空的头结点
*/
Node head = null;
int size = 0;
/**
* 链表插入数据 插入的数据指向的对象,是否存在下一个结点,
* @param d
*/
private synchronized void addLastNode(E d){
final Node<E> newNode = new Node(d);
if(head ==null){
head = newNode;
size++;
return;
}
//定义一个临时的结点 判断头结点下面的值,且不改变实际头结点的值
Node<E> tmp = head;
//当前结点的下一个结点是否有值 如果有,当前结点变为next结点
while(tmp.next!=null){
tmp = tmp.next;
}
//直到last Node 给当前结点的下一个结点赋为需要添加的值
tmp.next = newNode;
size++;
}
/**
* 给头结点添加元素
* @param d
*/
private synchronized void addHeadNode(E d){
//声明一个头结点 next为null,需要将原来的头结点 变成newNode的next
final Node<E> newNode = new Node(d);
newNode.next = head;
head = newNode;
size++;
}
/**
* 给中间结点添加新结点
* @param d
*/
private synchronized void addSizeNode(int index,E d){
Node<E> newNode = null;
//插入中间元素
for (int i = 0; i < index-1; i++){
//next index次
newNode = head.next;
}
//得到需要插入的结点的位置,往该节点下插入新的
Node<E> addNode = new Node<>(d);
addNode.next = newNode.next;
newNode.next = addNode;
size++;
}
/**
* 实例一个删除头部元素
* @param index
*/
public synchronized void deleteHeadNode(){
//现有头部元素作为新的头部
Node newHead = head.next;
//原有头部data设置为null;
head.data = null;
head.next = null;
head = newHead;
size--;
}
public static void main(String[] args) {
MyLink<SystemA> myLink = new MyLink<>();
myLink.addHeadNode(new SystemA("001","张三","true"));
myLink.addLastNode(new SystemA("002","李四","true"));
myLink.addLastNode(new SystemA("004","王五","true"));
myLink.addSizeNode(2,new SystemA("003","李四","true"));
myLink.deleteHeadNode();
System.out.println("长度是:"+myLink.size);
System.out.println("头元素是:"+myLink.head.data.toString());
System.out.println("头.next元素是:"+myLink.head.next.data.toString());
}
}
class Node<E>{
Node next = null;//结点的引用,指向下一个结点
E data;//结点的对象,内容
public Node(E data){
this.data = data;
}
}
自己实现的过程中,发现删除元素,新增元素很容易实现,但是需要先找到元素之后再做这些操作才简单。数据的二分法就比较的好。
边界值问题需要考虑很多,比如当前链表的长度,新增的时候是否越界、删除的时候是否越界、长度为0的时候等等等。
那些问题大佬们早就考虑到了
一般实现好的链表会有限定好的值,而且有跳跃表来简化链表的查找数据。
单向链表反转
单向链表反转的方式有比较多的,很粗暴的拆了重组。
从头结点一直next,直到node.next==null,取当前结点为新的头结点【head】并且切断该结点。
继续切已经被切过尾巴的链表,得到的数据添加到新的链表中…直到老的链表为null,返回新的链表表头就可以了。时间复杂度O(n)。
双向链表
双向链表也是同样的准备Node
public class MyLinkDou<E> {
int size = 0;
Node head = null;
Node last = null;
class Node {
//实际存储的数据
public E e;
//下一个结点
public Node next;
//上一个结点
public Node pre;
/**
*
* @param e
*/
public Node(E e){
this.e = e;
next = null;
pre = null;
}
}
}
其他的增删改查与单向的挺像的。
/**
* 根据下标找到当前结点的上一个结点
* @param index
* @return
*/
public Node findpreNode(int index){
Node presend = head;
int nowindex = -1;
while(presend.next != null){
if( nowindex== index - 1){
return presend;
}
presend = presend.next;
nowindex++;
}
return null;
}
/**
* 根据下标插入一个数据
* @param index
* @param e
*/
public void addIndex(int index,E e){
if(index <0||index>=size)
return;
Node node = new Node(e);
Node preNode = this.findpreNode(index);
node.next = preNode.next;
preNode.next.pre = node;
preNode.next = node;
node.pre = preNode;
size++;
}
阅读LinkedList源码文档注释 哇哦,看着好复杂,但是能看到官方的链表写的有多么的强大。
首先多线程下不安全
*<p><strong>Note that this implementation is not synchronized.</strong>
* If multiple threads access a linked list concurrently, and at least
* one of the threads modifies the list structurally, it <i>must</i> be
* synchronized externally. (A structural modification is any operation
* that adds or deletes one or more elements; merely setting the value of
* an element is not a structural modification.) This is typically
* accomplished by synchronizing on some object that naturally
* encapsulates the list.
其次是双线链表
* Doubly-linked list implementation of the {@code List} and {@code Deque}
* interfaces. Implements all optional list operations, and permits all
* elements (including {@code null}).
源码里写到的结点类,只有一个全参构造器
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
add方法有六个、remove方法有9个、返回迭代器、clone、等方法。原来学习Java数据结构的时候对链表不是很了解,自己手写后,对这种数据结构也有了自己的认识。
跳跃表
上面说到,链表寻找数据很麻烦,比如ABCDEFG,这种顺序如果要得到F,需要遍历ABCDE结点,才可以到达F结点,如果可以ACEF的跳跃式的访问结点,那么链表查找某个值就快很多了;如果可以AEF的,甚至直接AF,那么取F的值,也是比较简单的。