什么是Linked List?
概述
- LinkedList和前面讲到的ArrayList都实现了List接口,但是各自实现的方式不同,ArrayList是基于动态数组随机访问实现的,访问效率高,直接通过下标即可获得值。而LinkedList是基于链表实现的一种数据结构,每一个数据都是一个节点Node,他们通过指针连在一起,插入和删除效率极高,但是查询效率却不如Array List。
- 在严老师的数据结果里面,单链表是这样定义的:用一组任意的存储单元存储线性表的数据元素。数据是通过结点连接起来的。这个结点包括两部分,一部分是数据,另一部分是后继元素的指针。
- 其实很好理解,将链表看出车链子的结点,结点这么连起来就构成了链表。然后就是在上面增加一个结点,替换节点,查询节点,删除节点等操作了。LinkedList是使用的双链表来实现的,直接看源码不是那么容易理解。我们先来看看自己用单链表实现的LinkeList,搞清楚 单链表的 节点的增删改查之后 ,再来看看双链表。
1.1自己的LinkedList
- 先看看节点Node的结构
private class Node{
public E e;
public Node next;//指向下一个元素,即指针
/**
*构造一个结点
*/
public Node(E e ,Node next){
this.e=e;
this.next=next;
}
public Node(E e){
this(e,null);
}
public Node(){
this(null,null);
}
@Override
public String toString() {
return e.toString();
}
}
- LinkedList这里只有两个属性 一个是Node一个是size
//设置为虚拟头节点 所谓虚拟头节点其实就是链表存有数据的第一个结点(头节点)的前一个结点(有点绕)
private Node dummyHead;
private int size;
- 构造函数
public LinkList(){
//这个虚拟头节点 初始化就是个空节点
dummyHead=new Node(null,null);
size=0;
}
//获取链表个数
public int getSize(){
return size;
}
//链表是否为空
public boolean isEmpty(){
return size==0;
}
1.2常规增删改查
add(int index,E e)
:根据索引添加元素e
public void add(int index,E e){
//判断索引是否合理, 一般来说 这个判断封装成一个方面比较好,因为可以实现代码复用。
if (index<0||index>size)
throw new IllegalArgumentException("Add Failed,Illegal index");
//prev先指向头节点
Node prev=dummyHead;
//找到要插入的位置的前一个节点
for (int i=0;i<index;i++)
//prev指向了下一个节点
prev=prev.next;
//1
// Node node =new Node(e);
node.next=prev.next;
// prev.next=node;
//2
prev.next=new Node(e,prev.next);
//1和2 是等价的
size++;
}
- 这里有个地方是比较有意思和值得思考的:
Node prev=dummyHead;
:这句话其实是prev拿到了dummyHead的引用。有思考过为什么我们对prev进行操作却给dummyHead增加了节点呢?- 看看这个图
- 插入添加操作就是这样的。实在不清楚的可以去看一下这个java四种引用方式
- 后续的增加操作都是继续节点的插入操作
addLast()和addFrist()
:在头部或者尾部插入节点
//在链表头添加新的元素e
public void addFirst(E e){
// Node node=new Node(e);
// node.next=head;
// head=node;
add(0,e);
}
//在链表末尾添加新的元素
public void addLast(E e ){
add(size,e);
}
- 查询操作,非常简单之间看代码
//获得链表的第index个位置的元素
public E get(int index){
if (index<0||index>=size)
throw new IllegalArgumentException("index is Illegal");
//当前节点
Node cur=dummyHead.next;
for (int i=0;i<index;i++)
cur=cur.next;
return cur.e;
}
//获得链表第一个元素
public E getFirst(){
return get(0);
}
//获得最后一个元素
public E getLast(){
return get(size-1);
}
- 删除操作
//删除一个节点
public E remove(int index){
if (index<0||index>=size)
throw new IllegalArgumentException(" set failed ;index is Illegal");
Node pre=dummyHead;
for (int i=0;i<index;i++)
pre=pre.next;
//pre表示待删除结点的前一个结点.
//待删除结点记为retNode
Node retNode=pre.next;
//前一个结点与待删除结点的一个结点相连,这样就将 待删除结点retNode与链表断开联系
pre.next=retNode.next;
//retNode.next不再指向链表中的结点而是为空 等待被GC回收
retNode.next=null;
//链表数量减一
size--;
return retNode.e;
}
//删除第一个元素
public E removeFirst(){
return remove(0);
}
//删除最后一个元素
public E removeLast(){
return remove(size-1);
}
- 修改操作
set(int index,E e)
//修改链表的第index 个的元素为e
public void set(int index,E e){
if (index<0||index>=size)
throw new IllegalArgumentException(" set failed ;index is Illegal");
Node cur=dummyHead.next;
for(int i=0;i<index;i++)
cur=cur.next;
//找到待修改结点之后 重新赋值即可
cur.e =e;
}
到这里我们自己简化版的LinkeList就差不多完成了,还是附上完整代码 供参考
public class LinkList<E> {
//节点 私有内部类
private class Node{
public E e;
public Node next;//下一个节点
public Node(E e ,Node next){
this.e=e;
this.next=next;
}
public Node(E e){
this(e,null);
}
public Node(){
this(null,null);
}
@Override
public String toString() {
return e.toString();
}
}
//设置为虚拟头节点
private Node dummyHead;
private int size;
public LinkList(){
dummyHead=new Node(null,null);
size=0;
}
//获取链表个数
public int getSize(){
return size;
}
//链表是否为空
public boolean isEmpty(){
return size==0;
}
/**
* 在链表index位置添加元素e
*/
public void add(int index,E e){
if (index<0||index>size)
throw new IllegalArgumentException("Add Failed,Illegal index");
//prev先指向头节点
Node prev=dummyHead;
//找到要插入的位置的前一个节点
for (int i=0;i<index;i++)
//prev指向了下一个节点
prev=prev.next;
//1
// Node node =new Node(e);
node.next=prev.next;
// prev.next=node;
//2
prev.next=new Node(e,prev.next);
//1和2 是等价的
size++;
}
//在链表头添加新的元素e
public void addFirst(E e){
// Node node=new Node(e);
// node.next=head;
// head=node;
add(0,e);
}
//在链表末尾添加新的元素
public void addLast(E e ){
add(size,e);
}
//获得链表的第index个位置的元素
public E get(int index){
if (index<0||index>=size)
throw new IllegalArgumentException("index is Illegal");
//当前节点
Node cur=dummyHead.next;
for (int i=0;i<index;i++)
cur=cur.next;
return cur.e;
}
//获得链表第一个元素
public E getFirst(){
return get(0);
}
//获得最后一个元素
public E getLast(){
return get(size-1);
}
//修改链表的第index 个的元素为e
public void set(int index,E e){
if (index<0||index>=size)
throw new IllegalArgumentException(" set failed ;index is Illegal");
Node cur=dummyHead.next;
for(int i=0;i<index;i++)
cur=cur.next;
cur.e =e;
}
//查找链表中是否有元素e
public boolean contains(E e){
Node cur=dummyHead.next;
while (cur!=null) {
if (cur.e.equals(e))
return true;
cur = cur.next;
}
return false;
}
//删除一个节点
public E remove(int index){
if (index<0||index>=size)
throw new IllegalArgumentException(" set failed ;index is Illegal");
Node pre=dummyHead;
for (int i=0;i<index;i++)
pre=pre.next;
Node retNode=pre.next;
pre.next=retNode.next;
retNode.next=null;
size--;
return retNode.e;
}
//删除第一个元素
public E removeFirst(){
return remove(0);
}
//删除最后一个元素
public E removeLast(){
return remove(size-1);
}
@Override
public String toString() {
StringBuilder res=new StringBuilder();
Node cur=dummyHead.next;
while (cur!=null){
res.append(cur+"->");
cur=cur.next;
}
res.append("NULL");
return res.toString();
}
}
开始吃荤菜了 上源码!
2.1 LinkedListd 继承与实现
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
-实现了List ,Deque
,克隆和序列化。继承了 AbstractSequentialList<E>
:提供了 List 接口的骨干实现,从而最大限度地减少了实现受“连续访问”数据存储(如链接列表)支持的此接口所需的工作,从而以减少实现List接口的复杂度。
Deque
一个线性 collection,支持在两端插入和移除元素,定义了双端队列的操作。
Serializable
:序列化接口,这个接口什么东西都没有,就是用于标识实现改接口的类是可以序列化的。
public interface Serializable {
}
-
这里有个面试常问的问题 :什么是序列化和反序列化呢?
- 回答:Java 序列化就是指将对象转换为字节序列的过程,而反序列化则是只将字节序列转换成目标对象的过程。(类似于你在网上买了一个女朋友,为了方便女朋友的运输肯定要将起打包处理,这就是序列化,你把包裹拆开 取出女朋友(反序列化))
- 追问:为什么我们要序列化呢?
-
回答: 把的内存中的对象状态保存到一个文件中或者数据库中时候;
用套接字在网络上传送对象的时候;
通过RMI传输对象的时候;seriallization 序列化 : 将对象转化为便于传输的格式, 常见的序列化格式:二进制格式,字节数组 json字符串(现在前后端交互就是用json来传输数据的) xml字符串。
de seriallization 反序列化:将序列化的数据恢复为对象的过程。
-
2.2 属性
- 属性有 链表数量size,第一个节点first和最后一个节点last
transient int size = 0; /** * Pointer to first node. * Invariant: (first == null && last == null) || * (first.prev == null && first.item != null) */ transient Node<E> first; /** * Pointer to last node. * Invariant: (first == null && last == null) || * (last.next == null && last.item != null) */ transient Node<E> last;
- 来看看Node这个东西
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; } }
- LinkedList在java里面是实现的双链表,相对于我们刚刚的单链表,也只是复杂了一点点。
2.3构造函数
public LinkedList() {
}
/**
构造一个包含指定集合中的元素,他们的顺序是由集合的迭代器返回一个列表。.
*
* @param c 其元素将被放置在此列表中的集合
* @throws NullPointerException if the specified collection is null
*/
public LinkedList(Collection<? extends E> c) {
this();
//调用了封装了的方法。 我们每次阅读源码的时候总能够看到许多 封装 看的多了,我们自然慢慢就明白了 什么时候要封装一下, 提升代码的质量
addAll(c);
}
2.4 依旧 增删改查
- 增加
add(E e)
:向尾部插入一个元素
public boolean add(E e) {
//你看 是不是又封装了 ,到就可以在哪里需要 就在哪里调用即可
linkLast(e);
return true;
}
//这个方法对外是不可见的
//链接Ë作为最后一个元素
void linkLast(E e) {
//l拿到最后一个节点的引用
final Node<E> l = last;
//新的节点直接接到l后面,
final Node<E> newNode = new Node<>(l, e, null);
//last移动到newNode的位置
last = newNode;
/******这一步并不是特别好理解 我将其拆开看看
//生成一个 普通节点
Node node=new Node(null,e,null);
//将这个节点插入到last这个尾巴节点
//node先和前一个结点接上
node.pre=l.pre;
//前一个的结点不再指向l 而是指向了node
l.pre.next=node;
//node的下一个结点指向了l
node.next=l;
//l的前一个结点再指向node
l.pre=node;
//*****/上面这四步其实就是等于 源代码两句代码的结果。
/*构造函数
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}*/
//双端队列 首位相接
if (l == null)
first = newNode;
else
l.next = newNode;
//链表节点数量加一
size++;
modCount++;
}
- 同学再看这个的时候 最好拿笔把这个过程画一遍,加深影响。其实我们在做链表或者数相关的算法题时,都是基于结点的操作,把单链表和双向链表的结点的插入和删除操作搞明白之后,做那些操作结点的题目也是手到擒来的事情了😀。
addFirst(E e)
: 在头部插入
//在插入此列表的开头指定的元素
public void addFirst(E e) {
linkFirst(e);
}
/**
* Links e as first element.
*/
private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
//操作和linkLast一样的 只是接入的是头节点而已
if (f == null)
last = newNode;
else
//在头节点的前一个就是被插入的节点
f.prev = newNode;
size++;
modCount++;
}
- 再来最后一个
addAll(int index, Collection<? extends E> c)
:插入所有指定集合中的元素插入此列表,开始在指定的位置。 目前移动的元件在该位置(如果有的话)和任何后续元素向右(增加其索引)。 新元素将出现在它们被指定collection的迭代器返回的顺序列表
public boolean addAll(int index, Collection<? extends E> c) {
//检测是否越界
checkPositionIndex(index);
//转换为数组
Object[] a = c.toArray();
int numNew = a.length;
if (numNew == 0)
return false;
//前一个结点和当前节点
Node<E> pred, succ;
//如果索引的大小和size相等 则直接从末尾开始插入
if (index == size) {
succ = null;
pred = last;
} else {
//否则 通过node(index)这个函数 计算出插入位置的节点
succ = node(index);
pred = succ.prev;
}
for (Object o : a) {
@SuppressWarnings("unchecked")
//强制转换 叫做向上转型, 因为所有的类都是Object的子类
E e = (E) o;
//和前面的插入操作一样的
//把node 插到pred的后面
Node<E> newNode = new Node<>(pred, e, null);
//其实我这里也不是特别理解 搞得来的 大佬来指点一下 哈哈
if (pred == null)
first = newNode;
else
pred.next = newNode;
//修改插入位置的前一个节点,这样做的目的是将插入位置右移一位,保证后续的元素是插在该元素的后面,确保这些元素的顺序
pred = newNode;
}
if (succ == null) {
last = pred;
} else {
pred.next = succ;
succ.prev = pred;
}
size += numNew;
modCount++;
return true;
}
//找到下标所对应的节点并返回
Node<E> node(int index) {
// assert isElementIndex(index);
//判断遍历的方向
//从前往后遍历
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
//从后往前遍历
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
- 删除:
remove()
删除第一个元素
public E remove() {
return removeFirst();
}
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
//其实最终就是调用的这个函数
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item;
//头节点的下一个节点
final Node<E> next = f.next;
//另头节点的元素值为null
f.item = null;
//不再指向链表的下一个节点 等待被GC回收
f.next = null; // help GC
//头节点右移动一个
first = next;
//如果next==null的话 那么这个链表也为空
if (next == null)
last = null;
else //不为的空话 头节点的前一个节点就应该为空了
next.prev = null;
size--;
modCount++;
return element;
}
- 总结 熟练的操作节点的增删改 将会是突破算法的关键点之一,同学们加油!。