在写之前还是聊聊产生的背景吧,首先,在ArrayList某个位置添加数据的时候需要元素有后移的操作,在删除的时候也是需要元素的前移操作。除此之外,还有就是ArrayList这种集合的底层存储数据是一个数组,那么ArrayList的长度就不太好控制,在此时就有两种情况,一种是我并没有用完剩余的空间,还有一种就是我的数据不够长度,那么就需要扩容,这样一来,就还是会出现剩余的空间并没有使用完,造成空间的浪费。当然它的好处就是获取数据很方便。
在我们需要存储数据的时候, 最初的状态就是两种,一种是变量,一种是数组。在之前写了数组这种形式的集合去帮我们动态的扩容。现在解决刚刚说的问题,那么LinkedList就诞生了。
首先分析:
存储数据可以用变量,那么在Java中,变量有两大大类:1、八种计本类型 2、引用类型 那么在这里真正存储数据的时候就应该是使用引用类型。我们就可以得到下面这张图。
怎么才能让这些数据能够不这样游离呢?
使用链表,如下图(其中黑竖线右边的全是new关键字出来的,也就是一个个的对象。左边是用户看得到的引用)
基于此图,就得到了核心的代码如下。
private int size=0;//记录有效个数
private Node<E> first;//记录头结点
private Node<E> last;//记录尾结点
/**
* @author TQY 杨显伍
* @date 2022/10/10 10:57
* @param e 要添加的数据
* @return boolean
* effect: 尾插法
*/
@Override
public boolean add(E e) {
this.linkedLast(e);
return true;
}
@Override
public E get(int index) {
this.checkIndex(index);
return node(index).item;
}
@Override
public E remove(int index) {
this.checkIndex(index);
return unLiked(node(index));
}
@Override
public int size() {
return this.size;
}
@Override
public E[] toArray() {
return this.copyArray();
}
@Override
public boolean addFirst(E addData) {
this.linkedFirst(addData);
return true;
}
@Override
public boolean add(int index, E addData) {
this.checkIndex(index);//校验下标的合法性。
if (index==0){//如果选择的是从头插入
linkedFirst(addData);
return true;
}else if (index==(size-1)){//如果选择的是从尾部插入。
linkedLast(addData);
return true;
}
linkIndex(this.node(index),addData);
return true;
}
/**
* 根据下标替换成想要的元素。
* @param index 下标
* @param replaceData 需要替换的数据
* @return 返回的是替换的数据
*/
@Override
public E replace(int index, E replaceData) {
checkIndex(index);
return linkNew(index,replaceData);
}
/**
* @author TQY 杨显伍
* @date 2022/10/11 11:07
* @return boolean
* effect: 清空链表 思路1、先断next这条链, 就是从前往后循环的让头结点指向next并且然后将next置为空
* 然后断prev这条链, 就是从后往前循环的让尾结点指向prev并且将prev置为空
*/
public boolean clear(){
Node<E> next=first.next;
Node<E> prev=last.prev;
for (int i = 0; i < size; i++) {
first=next;
next=null;
}
for (int i = size-1; i >=0 ; i--) {
last=prev;
prev=null;
}
size=0;
return true;
}
/**
* @author TQY 杨显伍
* @date 2022/10/11 14:22
* @return boolean
* effect: 思路二:从头结点一直向下都置为空。
*/
public boolean clearAll(){
Node<E> remove=first;
while (remove!=null){
Node<E> next =remove.next;
remove.item=null;
remove.next=null;
remove.prev=null;
remove=next;
}
first = last = null;
size=0;
return true;
}
/**
* @author TQY 杨显伍
* @date 2022/10/11 14:48
* @param index
* @param addData
* @return E[]
* effect: 在指定位置添加一个数组。
*/
@Override
public E[] add(int index, E... addData) {
checkIndex(index);
for (int i = 0; i < addData.length; i++) {
linkNew(index,addData[i]);
}
size+=addData.length;
return addData;
}
/**
* @author TQY 杨显伍
* @date 2022/10/11 14:54
* @param addData
* @param method 插入的方式 head---头插法 last----尾插法
* @return E[]
* effect: 让用户选择是进行头插法还是尾插法
*/
public E[] add(String method, E... addData) {
linked(method, addData);
return addData;
}
/**
* @author TQY 杨显伍
* @date 2022/10/10 11:00
* effect: 写私有方法的区域
*/
/**
* @author TQY 杨显伍
* @date 2022/10/10 11:11
* @param e
* effect: 思路:1、首先拿一个来记录最后一个节点
* 2、再次创建一个新的游离的节点,在创建的时候就将新结点挂载在最后一个位置
* 3、需要判断是不是第一个:
* a.如果是第一个,那么记录第一个的first应该记录这个新结点
* b.否则不是第一个,那么事先拿到的这个结点的记录下一个结点位置的就应该指向这个新结点。(此时拿到的这个结点就应该是在插入的时候的最后的那个结点)
* 4、最后别忘了将记录有效长度增加一。
*/
private void linkedLast(E e){
Node<E> l=last; //记录尾结点
Node<E> newNode=new Node(l,e,null);//既然是从尾部开始添加,那么就应该记录上一个的位置
last=newNode;//尾结点指向新的结点
if (l==null){//证明是空的。也就是第一次插入。
first=newNode;
}else {
l.next=newNode;
}
this.size++;
}
/**
* @author TQY 杨显伍
* @date 2022/10/10 11:28
* @param index
* effect: 判断下标是否合法
*/
private void checkIndex(int index){
if (index<0){
throw new RuntimeException("The subscript you entered is illegal ");
}
if (index>=size){
throw new RuntimeException("The index you entered is out of range"+",Data index max is\t"+(size-1));
}
}
/**
* @author TQY 杨显伍
* @date 2022/10/10 14:23
* @param index
* @return com.yxw.list.LinkedListBox.Node<E>
* effect: 根据下标获取元素
* 思路:
* 1、用一个空的结点来记录
* 2、根据下标判断是否处于前半部分(index<(size>>1))
* a.如果下标处于前半部分,那么就将记录的结点指向头结点,然后从头依次的向后遍历,直至遍历至下标位置。然后返回记录的结点
* b.否则下标就处于后半部分,那么就将记录的结点指向尾结点,然后从尾依次的向前遍历,直至遍历至下标位置。然后返回记录的结点
*/
private Node<E> node(int index){
Node<E> result;
if (index<(size>>1)){//说明下标在前半部分
result=first;
for (int i = 0; i < index; i++) {
result=result.next;
}
return result;
}else {
result=last;
for (int i = size-1; i >index ; i--) {
result=result.prev;
}
return result;
}
}
/**
* @author TQY 杨显伍
* @date 2022/10/10 14:52
* @param eNode
* @return E
* effect: 删除元素
* 思路:
* 1、先获取一下删除的元素,以便将删除的元素返回给用户。
* 2、在获取当前结点的下一个结点(next)和上一个结点(prev)
* 3、判断是否为头结点
* a.如果是头结点,那么将头结点置为该结点的下一个结点,即就是在第二步接受的next置为头结点
* b.否则那就是中间的结点,那么就将上一个结点记录下一个结点的引用指向这个结点记录的下一个结点,然后将next置为null
* 4、判断是否为尾结点:
* a.如果是尾结点,那么就将该结点的上一个结点指向尾结点。
* b.否则不是尾结点说明就是中间结点,那么就将该结点的下一个结点记录上一个结点的引用指向这个结点所记录的上一个结点,然后将perv置为空。
*/
private E unLiked(Node<E> eNode){
Node<E> unNode=eNode;
final Node<E> next = eNode.next;//当前节点的下一个节点的引用
final Node<E> prev = eNode.prev;//当前节点的前一个节点的引用
if (prev == null) {//证明现在是头节点
first = next;//让下一个节点为头结点
} else {//中间节点
prev.next = next;//上一个的节点所存储的下一个节点的引用指向当前节点的下一个节点的引用
eNode.prev = null;//当前指向上一个节点的引用置为空。
}
if (next == null) {//证明是尾结点
last = prev;//将当前节点的上一个节点置为尾结点。
} else {//中间节点
next.prev = prev;//下一个节点所存储的上一个节点的信息指向上当前节点的上一个节点
eNode.next = null;//当前节点指向下一个节点置为空。
}
size--;
return unNode.item;
}
/**
* @author TQY 杨显伍
* @date 2022/10/10 16:22
* @return E[]
* effect: 专门为转化为数组的方法所服务的
*/
private E[] copyArray(){
E[] result=(E[]) new Object[size];
for (int i = 0; i < size; i++) {
result[i]= node(i).item;
}
return result;
}
/**
* @author TQY 杨显伍
* @date 2022/10/10 16:37
* effect: 现在已经能够保证插入的结点是在链表的中部进行插入的了
* 那么现在的思路是:1、先获取当前位置所指向的下一个和上一个引用。
* 2、将新的结点的next指向当前结点,即就是newNode.next=node;
* 3、接着将新结点的prev指向当前结点的上一个结点的下一个结点。即就是newNode.prev=prev.next;
* 4、接着将当前结的perv指向新结点。即就是node.prev=node;
* 5、接着将当前的上一个结点的下一个指向结点指向新结点,也就是 node.prev.next=newNode;
*/
private void linkIndex(Node<E> node, E addData) {
Node<E> prev =node.prev;
Node<E> newNode=new Node<E>(null,addData,null);
newNode.next=node;
newNode.prev=prev;
node.prev=newNode;
node.prev.next=newNode;
size++;
}
/**
* @author TQY 杨显伍
* @date 2022/10/10 16:12
* @param addDate
* effect: 头插法,这个和尾插法刚好相反
*/
private void linkedFirst(E addDate){
Node f=first;
Node<E> newNode=new Node<E>(null,addDate,f);
first=newNode;
if (f.next==null){//说明这是一个空的,那么就
last=newNode;
}else {
f.prev=newNode;
}
size++;
}
/**
* @author TQY 杨显伍
* @date 2022/10/11 10:26
* @param index
* @param e
* @return E
* effect: 替换所在下标的元素。
* 思路:
* 1、先接收当前结点的所有的属性
* 2、根据当前结点判断是否为头结点
* a.若是头结点,那么就将新结点置为头结点;然后将新结点的下一个结点置为next,最后将当前结点存储下一个结点的信息置为空
* b.否则则不是头结点,那么就先将新结点的下一个结点置为next,然后将当前结点的next置为空
* 3、根据当前结点判断是否为尾结点
* a.若是尾结点,那么就将新结点置为尾结点,然后将新结点的上一个结点置为新结点的上一个结点,最后降当前结点的上一个结点置为空
* b.否则不是为结点,那么就将新结点的上一个结点置为prev,然后将当前结点的perv置为空
* 4、最后将替换的结点的数据给返回给用户。
*/
private E linkNew(int index,E e){
Node<E> oldNode=node(index);
Node<E> next=oldNode.next;
Node<E> prev=oldNode.prev;
E result=oldNode.item;
Node<E> newNode=new Node<>(null,e,null);
if (prev==null){ //说明是头结点
first=newNode;
newNode.next=next;
oldNode.next=null;
}else {
newNode.next=next;
oldNode.next=null;
}
if (next==null){//说明是为结点
last=newNode;
newNode.prev=prev;
oldNode.prev=null;
}else {
newNode.prev=prev;
oldNode.prev=null;
}
return result;
}
private void linked(String method,E ...addData){
if (method.equals("head")){
for (int i = 0; i < addData.length; i++) {
linkedFirst(addData[i]);
}
} else if (method.equals("last")){
for (int i = 0; i < addData.length; i++) {
linkedLast(addData[i]);
}
}else {
throw new RuntimeException("Your Method has not exist");
}
size+=addData.length;
}
/**
* @author TQY 杨显伍
* @date 2022/10/10 10:54
* effect: 真正的节点类,保存真正的数据和上一个节点的地址,下一个地址
*/
private static class Node<E>{
Node<E> prev;
E item;
Node<E> next;
public Node (Node<E> prev,E item,Node<E> next){
this.prev=prev;
this.next=next;
this.item=item;
}
}
最重要的就是对结点的控制。
在选用集合的时候,要考虑是读数据多还是写数据多,如果写数据多,那推荐使用LinkedList集合,否则推荐使用ArrayList。