本次要实现的链表如图所示,first和last分别指向首部和尾部.该链表首尾不循环.
首先建一个interface定义一些链表应该具有的方法.
package List;
/**
* @InterfaceName SingalLinkedList
* @Description TODO
* @Author lixiang
* @Date 2020/2/26 11:24
* @Version 1.0
*/
public interface SingalLinkedList<E> {
//---------增------------
boolean add(E data);//尾插数据
boolean add(int index,E data);//指定位置插入
void addFirst(E data);//头插
void addLast(E data);//尾插
//---------删------------
E removeFirst();//删除头节点
E removeLast();//删除尾节点
boolean remove(Object o);//删除指定元素
E remove(int index);//删除指定位置元素
void clear();//清空链表
//---------改------------
E set(int index,E element);//修改指定位置数据,并返回旧数据
//---------查------------
int size();//获取链表长度
E getFirst();//获取头数据
E getLast();//获取尾数据
E get(int index);//获取指定位置数据
int indexOf(Object o);//获取指定数据下标
boolean isEmpty();//判断链表是否为空
}
然后定义节点 每一个节点都包括 ,element (存数据) ,prev(指向前面的节点地址) ,next(指向后面的节点地址)
//--------------------------------节点---------------------------------------
private static class Node<E>{
//数据
E item;
//next域
Node<E> next;
//prev域
Node<E> prev;
//构造方法
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
然后定义链表的全局变量,和构造方法
size 容量 ,每次添加或者删除一个节点,size相应的加一或者减一 ,默认是0 即链表没有节点.
first 指向头节点地址,初始化时为null
last 指向尾节点地址,初始化时为null
first和last指针本身也是Node节点类型.注意每次增删节点,first和last都会相应变化,以保证first和last永远指向链表的首尾.
//容量
transient int size = 0;
//头指针
transient Node<E> first;
//尾指针
transient Node<E> last;
//构造一个空链表
public MyLinkedList(){
}
接下来,先写一个添加节点的方法,默认是向尾部添加.
注意添加的时候需要考虑两种情况.
1.该链表刚刚初始化,没有任何节点.此时size是0,first是null,last也是null.
所以,首次添加节点后,first和last应该都指向这个节点.此时这个节点既是头节点也是尾节点.如下图
该节点的prev和next不指向任何节点,所以应该都为null.
2.该链表已经存在至少一个节点,这时向尾部添加需要将新建的节点跟前一个节点链接上,然后将last指向这个新节点.如下图
代码如下
//---------------------------------添加----------------------------------------
/**
* 添加节点
* @param e
* @return
*/
public boolean add(E e) {
//默认向尾部添加
linkLast(e);
return true;
}
public void addLast(E e) {
linkLast(e);
}
/**
* 尾插法
* @param e
*/
private void linkLast(E e) {
//1.将指针指向尾节点
final Node<E> l = last;
//2.新建一个节点(prev指向当前的尾节点l,next指向null)
Node<E> newNode = new Node<>(l,e,null);
//3.把新节点的地址给last
last = newNode;
//4.判断l指针是否为空,
// 即之前的尾节点是否为空(初次初始化链表时,调用无参构造,这时给链表 MyLinkedList 分配内存地址(例如0X4192)
// 但是first和last是null这里需要想明白)
if (l == null){
//该链表里没有任何节点,那么就把last和first都指向第一次新建的节点,
//此时链表里仅有一个节点,它既是头节点也是尾节点
first = newNode;
}else {
//说明链表里已经有节点 last此时已经指向它(看第一步),那就把这个节点的next域赋值 新节点的地址
l.next = newNode;
}
//5.节点连接完成,容量加一
size++;
}
头插法的原理类似,代码如下
/**
* 头插法
* @param e
*/
public void addFirst(E e) {
linkFirst(e);
}
private void linkFirst(E e) {
//1.把f指针指向first节点
final Node<E> f = first;
//2.新建一个节点 prev指向null,next指向当前这个first节点
Node<E> newNode = new Node<>(null, e, f);
//3.更新first指针,将新建的这个节点的地址赋给first指针.
first = newNode;
//4.判断新建的这个节点是不是链表里的第一个节点,
//如果是那么就把last指针也指向这个节点
if (f == null){
last = newNode;
}else {
//在创建这个节点之前链表已经有节点了
//那就让之前的first节点的prev域指向现在的新节点
f.prev = newNode;
}
//5.容量加一
size++;
}
向指定位置插入
/**
* 向指定位置插入
* @param index
* @param element
* @return
*/
public boolean add(int index, E element) {
checkPositionIndex(index);
if (index == size){
//如果向最后一个节点后面添加直接调用尾插法
linkLast(element);
}else {
//向index前面插入
linkBefore(element,node(index));
}
return true;
}
/**
* 向指定位置的前面插入
* @param element
* @param succ
*/
private void linkBefore(E element, Node<E> succ) {
//1.得到插入位置的前一个节点地址
final Node<E> pred = succ.prev;
//2.新建节点
Node<E> newNode = new Node<>(pred, element, succ);
//3.插入位置节点的prev 指向新节点
succ.prev = newNode;
//4.判断插入位置的前一个节点是否存在
if (pred == null){
//如果不存在说明这个新节点就是头节点
first = newNode;
}else {
//否则让这个节点的next指向新节点
pred.next = newNode;
}
//5.容量加一
size++;
}
/**
* 返回要index位置的节点的地址
* @param index
* @return
*/
Node<E> node(int index) {
//如果index在左半边从头开始查,否则从尾部开始查 使用右移运算
if ( index < (size >> 1) ){
//把指针指向头节点
Node<E> x = first;
for (int i = 0; i < index; i++) {
//向后找
x = x.next;
}
//返回index处节点的地址
return x;
}else {
//把指针指向尾节点
Node<E> x = last;
for (int i = size - 1; i > index; i--) {
//向前找
x = x.prev;
}
return x;
}
}
private void checkPositionIndex(int index) {
if (index > size || index < 0){
throw new IndexOutOfBoundsException("下标越界"+index);
}
}
/**
* 将一个集合加入list
* @param c
* @return
*/
public boolean addAll(Collection<? extends E> c) {
return addAll(size,c);
}
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;
//如果通过只有一个参数的addAll添加,默认index就是size,
// 用户也可以直接调用含有2个参数的方法 指定index 和 集合
if (index == size){
//在原链表后面插入集合数据
succ = null;
pred = last;
}else {
//向index位置前插入,所以调用node()方法拿到index位置节点的地址
//将succ指针指向index位置的节点
succ = node(index);
//将pred指针指向index位置前一个位置的节点
pred = succ.prev;
}
for (Object o : a){
//遍历集合,依次向链表插入
@SuppressWarnings("unchecked") E e = (E)o;
Node<E> newNode = new Node<>(pred, e, null);
if (pred == null){
//如果pred是空说明之前的链表index位置上的节点是头节点
//那么这就相当于头插法 新加入的节点就变成头节点
first = newNode;
}else {
//否则pred指向的这个节点的next指向新节点
pred.next = newNode;
}
//把pred指向这个新加入的节点
pred = newNode;
}
//此时已经把集合里的数据都依次加到了index所指节点的前一个节点的后面
//接下把index所指节点跟加入后的节点连接到一块
//可以想象成一列火车,要在这列火车中部插入几节车厢
if (succ == null){
last = pred;
}else {
pred.next = succ;
succ.prev = pred;
}
size += numNew;
return true;
}
修改指定位置的节点
//--------------------------------修改----------------------------------
/**
* 修改指定位置的节点
* @param index
* @param element
* @return
*/
public E set(int index, E element) {
//检查下标合法性
checkElementIndex(index);
//得到index位置节点的地址
Node<E> x = node(index);
//保存旧数据
E oldValue = x.item;
//更新数据
x.item = element;
//返回旧数据
return oldValue;
}
private void checkElementIndex(int index) {
if(index < 0 || index > size-1 ){
throw new IndexOutOfBoundsException("下标 "+ index +" 不合法");
}
}
查找方法
//-------------------------------------查找--------------------------------------
/**
* 查询链表容量
* @return
*/
public int size() {
return size;
}
/**
* 查找头节点
* @return
*/
public E getFirst() {
//将头指针赋值给f
final Node<E> f = first;
//2.判断头节点是否为空
if (f == null){
throw new NoSuchElementException("头节点为空");
}
return f.item;
}
/**
* 查找尾节点数据
* @return
*/
public E getLast() {
//1.将尾指针赋值给l
final Node<E> l =last;
if (l ==null){
throw new NoSuchElementException("尾节点为空");
}
return l.item;
}
/**
* 查找指定位置节点
* @param index
* @return
*/
public E get(int index) {
//1.下标合法性校验
checkPositionIndex(index);
//2.返回index位置对应节点的地址
Node<E> adress = node(index);
//3.将这个节点里的数据返回
return adress.item;
}
/**
* 查询数据在哪一个节点里
* @param o
* @return
*/
public int indexOf(Object o) {
int index = 0;
//1.链表允许存储null 所以先判断要查的是不是null
if (o == null){
for (Node<E> x = first; x != null; x = x.next){
//x指的是地址
if (x.item == null){
return index;
}else {
index++;
}
}
}else {
for (Node<E> x = first; x != null; x = x.next){
//x指的是地址
if (o.equals(x.item)){
return index;
}else {
index++;
}
}
}
//数据不存在返回-1
return -1;
}
/**
* 查看链表是否为空
* @return
*/
public boolean isEmpty() {
return size() == 0;
}
删除方法
//--------------------------------------删除--------------------------------------------
/**
* 默认删除头节点
* @return
*/
public E remove() {
return removeFirst();
}
/**
* 删除头节点
* @return
*/
public E removeFirst() {
//1.将f指向头节点
final Node<E> f = first;
//2.检查链表是否为空
// 当只有一个节点的时候,first和last都指向这个节点,当没有节点的时候first和last都是空
if (f == null){
throw new NoSuchElementException("没有头节点");
}
//3.保存头节点后面节点的地址和数据,因为一会要清除头节点
Node<E> temp = f.next;
E oldValue = f.item;
//4.删除头节点数据
f.item = null;
//5.断链,将头节点与后面的节点断开 帮助jvm GC
f.next = null;
//6.链表的第二个节点将变成头节点
// 因此将头指针first指向当前的头节点(即放在temp里的地址)
first = temp;
//7.这时需要注意,如果链表只有一个节点,那么这个节点既是头节点也是尾节点
//当执行完删除后链表就空了,这时last里面的地址还是之前那一个节点的地址,
// 并没有因为删除节点而更新,因此需要把last也置空
if (temp == null){
//temp记录的是头节点后面节点的地址,如果他是null
//说明链表只有一个节点,这时删除完后,链表为空,所以last也要置空
last = null;
}else {
//否则链表节点个数大于1 那么就可以将当前头节点的prev
//如果不判断那么当链表只有一个节点时,删除该节点后temp指向null
//那么执行temp.prev = null;时就会报错
temp.prev = null;
}
size--;
//7.返回被删的头节点里的数据
return oldValue;
}
public E removeLast() {
//1.把指针指向尾节点
Node<E> l = last;
//2.检查链表是否为空
if (l == null){
throw new NoSuchElementException("链表为空");
}
//3.链表可能只有一个节点,也可能有多个 所以需要分别处理
//先记录这个尾节点的数据,以及它前面一个节点的地址,以便于断链
final E oldValue = l.item;
final Node<E> temp = l.prev;
//4.清理掉数据
l.item = null;
//5.把尾节点的prev置为null 帮助GC
l.prev = null;
//6.把last前移指向新的尾节点,注意当链表只有一个节点时此时temp就是null,last也是null
last = temp;
//7.判断该链表是不是只有一个节点,
// 如果只有一个那么first和last都指向它,删除它之后要把first也置为null
if (temp == null){
first = null;
}else {
//如果链表超过一个节点,删除完尾节点,
// 把它前一个节点的next域置为null,然后把last指向它
temp.next = null;
}
size--;
return oldValue;
}
/**
* 删除指定数据
* @param o
* @return
*/
public boolean remove(Object o) {
//1.链表可以存null值 先判断o是不是null
if (o == null){
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null){
//找到这个节点断链
unlink(x);
return true;
}
}
}else {
for (Node<E> x = first; x != null ; x = x.next) {
if (o.equals(x.item)){
unlink(x);
return true;
}
}
}
return false;
}
E unlink(Node<E> x) {
//记录被删节点信息
final E element = x.item;
final Node<E> prev = x.prev;
final Node<E> next = x.next;
//判断被删节点是不是首节点
if (prev == null){
//如果是头节点,那么要把first指针后移
first = next;
}else {
//如果不是则直接断链,这一步只跟它前面的节点断链
prev.next = next;
x.prev = null;
}
//判断被删节点是不是尾节点
if (next == null){
//如果是尾节点,要把last指针前移
last = prev;
}else {
//如果不是则直接断链,这一步只跟它后面的节点断链
next.prev = prev;
x.next = null;
}
//清除原节点的data域
x.item = null;
size--;
return element;
}
/**
* 删除指定位置节点
* @param index
* @return
*/
public E remove(int index) {
//检查index合法性
checkElementIndex(index);
//返回index位置节点的地址
Node<E> node = node(index);
//断链
E oldValue = unlink(node);
return oldValue;
}
/**
* 清空链表
*/
public void clear() {
//记录下一个节点的地址
//清空当前节点
if (size == 0){
throw new NoSuchElementException("链表为空");
}
//我采用的双指针的方法 从节点的首尾向中间删.jdk8的方法是从头向尾删
int left = 0;
int right = size - 1;
//初始化首尾指针,分别指向首尾节点
Node<E> leftNode = first;
Node<E> rightNode = last;
//只要左右指针没重合就一直删
while (left <= right){
//先记录被删节点后面节点的地址,要不然把这个节点删了就找不到它后面的节点了
Node<E> tempL = leftNode.next;
leftNode.item = null;
leftNode.prev = null;
leftNode.next = null;
//把之前记录的地址给到leftNode,下次就可以直接删它了
leftNode = tempL;
//从后面向中间删的原理相同
Node<E> tempR = rightNode.prev;
rightNode.item = null;
rightNode.next = null;
rightNode.prev = null;
rightNode = tempR;
//双指针向中间移动 直到重合
left++;
right--;
}
//所有的节点都删除后,把首尾节点的指针置为null
first = null;
last = null;
size = 0;
}
其他方法
//------------------------------------其他方法------------------------------------
/**
* 找到并返回头节点的数据
* @return
*/
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
/**
*
* 使用链表做栈 首部插入 首部删除
* @param e
*/
public void push(E e) {
addFirst(e);
}
public E pop() {
return removeFirst();
}
/**
* 返回链表里的所有数据
* @return
*/
public Object[] toArray() {
Object[] result = new Object[size];
int i = 0;
for (Node<E> x = first; x != null; x=x.next){
result[i++] = x.item;
}
return result;
}
public E peekFirst() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
public E peekLast() {
final Node<E> l = last;
return (l == null) ? null : l.item;
}
方法上都有注释,理解起来应该不难,需要源码的可以留言