java list 底层构建_Java集合系列之三:LinkedList底层原理

底层结构

如果有在LeetCode上刷过题的话,对这种结构一定非常熟悉,这是定义的一个Node节点类,有三个属性,item是任意类型的数值,prev和next则是前置节点和后置节点,构成了整个链条的数据结构。

private static class Node {

E item;

Node next;

Node prev;

Node(Node prev, E element, Node next) {

this.item = element;

this.next = next;

this.prev = prev;

}

}

初始化

可以看到两个构造方法都很简单,其实都是构造方法啥都不做,只有在添加数据的时候才开始构建链表的数据结构。

public LinkedList() {

}

public LinkedList(Collection extends E> c) {

this();

addAll(c);

}

add()方法

尾部追加的逻辑很清晰,获得当前最后一个节点最为当前节点的前置节点,同样把当前节点设置为前置节点的后置节点,然后把当前节点作为最后一个节点,因为只需要创建一个节点与前一个节点建立前后关系即可,时间复杂度是O(1)。

// 第一个节点

transient Node first;

// 最后一个节点

transient Node last;

public boolean add(E e) {

linkLast(e);

return true;

}

void linkLast(E e) {

// 获得当前最后一个节点作为前置节点,可能为空

final Node l = last;

// 初始化当前节点

final Node newNode = new Node<>(l, e, null);

// 把当前节点作为最后的节点

last = newNode;

// 第一次添加设置为第一个节点

if (l == null)

first = newNode;

else

// 把当前节点设置为前置节点的后置节点

l.next = newNode;

size++;

modCount++;

}

add(int index,E e)方法

按索引插入元素,首先判断是不是第一个添加的元素,如果是的话,直接使用add()方法添加就可以了,如果不是则需要根据索引来遍历寻找链表上对应位置,这里用了个小技巧,判断索引是在前半段还是在后半段,从短的那头开始遍历,找到之后,新建一个节点,建立新的前置节点和后置节点的关系。时间复杂度是O(n),n为size/2。

transient int size = 0;

public void add(int index, E element) {

// 检查数组越界

checkPositionIndex(index);

if (index == size)

// 插入索引0的位置时,直接添加即可

linkLast(element);

else

linkBefore(element, node(index));

}

// 获得被插入索引上的元素

Node node(int index) {

// assert isElementIndex(index);

// 如果索引是在链表的前半段

if (index < (size >> 1)) {

// 获得第一个节点

Node x = first;

// 往后找到插入索引位置上的节点

for (int i = 0; i < index; i++)

x = x.next;

return x;

}

// 如果索引是在链表的前半段

else {

// 获得最后个节点

Node x = last;

// 往前找到插入索引位置上的节点

for (int i = size - 1; i > index; i--)

x = x.prev;

return x;

}

void linkBefore(E e, Node succ) {

// assert succ != null;

// 获得要插入索引上的节点的前置节点

final Node pred = succ.prev;

// 创建一个节点,前置节点是索引位置上节点的前置节点,后置节点则是索引位置上的节点

final Node newNode = new Node<>(pred, e, succ);

// 将这个节点设置成索引位置上节点的前置节点

succ.prev = newNode;

// 第一次插入,将该节点设置为第一个节点

if (pred == null)

first = newNode;

// 不是第一次插入,将前置节点的后置节点设置成插入的节点

else

pred.next = newNode;

size++;

modCount++;

}

addAll(Collection extends E> c)/addAll(int index, Collection extends E> c)方法

这两个方法是直接把集合添加进来,而addAll(Collection extends E> c)是直接调用addAll(int index, Collection extends E> c)方法,首先会把集合转换成数组,然后找到插入位置的节点,索引位置上的节点就是后置节点,它的前置节点也就是插入的节点的前置节点,然后循环给集合中的数值创建节点,建立前后节点的关系,最后根据最开始找到的前置节点和后置节点把新创建的链表和以前的链表合成一条链表,时间复杂度是O(n),n为插入集合的长度。

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 pred, succ;

// 在尾部插入,前置节点就是最后一个节点,没有后置节点

if (index == size) {

succ = null;

pred = last;

}

// 调用node()方法找到后置节点,前置节点就是后置节点的前置节点

else {

succ = node(index);

pred = succ.prev;

}

// 循环创建节点建立关系

for (Object o : a) {

@SuppressWarnings("unchecked") E e = (E) o;

Node 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;

}

addFirst(E e)/addLast(E e)方法

addFirst()方法是直接获取第一个节点,然后创建一个新节点作为新的第一个节点,而原来的第一个节点则是这个节点的后置节点,addLast()方法和add()方法没区别,都是直接在链表尾部追加,时间复杂度是O(1)。

public void addFirst(E e) {

linkFirst(e);

}

private void linkFirst(E e) {

final Node f = first;

final Node newNode = new Node<>(null, e, f);

first = newNode;

if (f == null)

last = newNode;

else

f.prev = newNode;

size++;

modCount++;

}

public void addLast(E e) {

linkLast(e);

}

get(int index)方法

get()方法是用的上面介绍过的node()方法,时间复杂度是O(n),n为size/2。

public E get(int index) {

// 判断数组越界

checkElementIndex(index);

// 遍历寻找节点

return node(index).item;

}

getFirst()/getLast()方法

这两个方法都是直接读取first和last两个节点变量,时间复杂度是O(1)。

public E getFirst() {

final Node f = first;

if (f == null)

throw new NoSuchElementException();

return f.item;

}

public E getLast() {

final Node l = last;

if (l == null)

throw new NoSuchElementException();

return l.item;

}

indexOf(Object o)/lastIndexOf(Object o)/contains(Object o)方法

indexOf()是从头查找集合中一个值的索引,如果是正常的数值则是直接遍历,如果是找null值,则只会返回最前面的null的索引,因为是可以存入多个null值的。lastIndexOf()是从尾部查找,所以null值是返回最后面一个,contains()方法是查找集合中是否包含目标值,直接调用的indexOf()方法时间复杂度都是是O(n)。

public int indexOf(Object o) {

int index = 0;

// 查找为null的节点,遍历到的第一个就返回

if (o == null) {

for (Node x = first; x != null; x = x.next) {

if (x.item == null)

return index;

index++;

}

}

// 查找有值的节点,遍历到匹配的就返回

else {

for (Node x = first; x != null; x = x.next) {

if (o.equals(x.item))

return index;

index++;

}

}

return -1;

}

public int lastIndexOf(Object o) {

int index = size;

if (o == null) {

for (Node x = last; x != null; x = x.prev) {

index--;

if (x.item == null)

return index;

}

} else {

for (Node x = last; x != null; x = x.prev) {

index--;

if (o.equals(x.item))

return index;

}

}

return -1;

}

public boolean contains(Object o) {

return indexOf(o) != -1;

}

remove()/removeFirst()/removeLast()方法

remove()方法其实就是调用的removeFirst()方法,移除头部结点,removeLast()方法是移除尾部节点,时间复杂度都是O(1)。

public E remove() {

return removeFirst();

}

public E removeFirst() {

final Node f = first;

if (f == null)

throw new NoSuchElementException();

return unlinkFirst(f);

}

public E removeLast() {

final Node l = last;

if (l == null)

throw new NoSuchElementException();

return unlinkLast(l);

}

remove(Object o)/removeFirstOccurrence(Object o)/removeLastOccurrence(Object o)/remove(int index)方法

前三个方法都是移除目标值,因为值可以重复的缘故,每次只会移除一个,removeFirstOccurrence()其实就是调用的remove(Object o)方法,从头遍历,removeLastOccurrence()则是从尾开始遍历,remove(int index)则是移除指定索引上的节点,遍历到之后都会调用一个unlink()方法,这个方法是用来解除移除节点跟前置节点和后置节点的关系,然后重新建立关系,因为需要先遍历查找,时间复杂度是O(n)。

public boolean remove(Object o) {

if (o == null) {

for (Node x = first; x != null; x = x.next) {

if (x.item == null) {

unlink(x);

return true;

}

}

} else {

for (Node x = first; x != null; x = x.next) {

if (o.equals(x.item)) {

unlink(x);

return true;

}

}

}

return false;

}

public boolean removeFirstOccurrence(Object o) {

return remove(o);

}

public boolean removeLastOccurrence(Object o) {

if (o == null) {

for (Node x = last; x != null; x = x.prev) {

if (x.item == null) {

unlink(x);

return true;

}

}

} else {

for (Node x = last; x != null; x = x.prev) {

if (o.equals(x.item)) {

unlink(x);

return true;

}

}

}

return false;

}

public E remove(int index) {

checkElementIndex(index);

return unlink(node(index));

}

E unlink(Node x) {

// assert x != null;

// 获得节点的前置节点和后置节点

final E element = x.item;

final Node next = x.next;

final Node prev = x.prev;

//前置节点为空,第一个节点就是后置节点

if (prev == null) {

first = next;

}

//否则前置节点的后置节点就是被移除节点的后置节点

else {

prev.next = next;

x.prev = null;

}

// 后置节点为空,最后一个节点就是前置节点

if (next == null) {

last = prev;

}

// 否则后置节点的前置节点就是被移除节点的前置节点

else {

next.prev = prev;

x.next = null;

}

x.item = null;

size--;

modCount++;

return element;

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值