当前位置: 首页 > news >正文

JDK源码系列(二)

一、ArrayList类

ArrayList类结构

ArrayList是一个用数组实现的集合,支持随机访问,元素有序且可以重复


public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable{

  }
  •  List:表明它是一个列表,支持添加、删除、查找等操作,并且可以通过下标进行访问。
  • RandomAccess:这是一个标志接口,表明实现这个接口的List集合是支持快速访问的。在ArrayList中,我们可以通过元素的序号快速获取元素对象,这就是快速随机访问
  • Cloneable:表明它具有拷贝能力,可以进行深拷贝或浅拷贝。
  • Serializable:表明它可以进行序列化操作,也就是可以将对象转换为字节流进行持久化存储或网络传输,非常方便。

 ArrayList核心源码解读

字段属性

//默认初始容量大小
private static final int DEFAULT_CAPACITY = 10;
 
//空的数组实例
private static final Object[] EMPTY_ELEMENTDATA = {};
 
//这也是一个空的数组实例,和EMPTY_ELEMENTDATA空数组相比是用于了解添加第一个元素时数组膨胀多少
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
 
//存储 ArrayList集合的元素,集合的长度即这个数组的长度
//1、当 elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA 时将会清空 ArrayList
//2、当添加第一个元素时,elementData 长度会扩展为 DEFAULT_CAPACITY=10
transient Object[] elementData;
 
//表示集合中所包含的元素个数
private int size;

类构造器 

无参构造
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

此无参构造函数将创建一个DEFAULTCAPACITY_EMPTY_ELEMENTDATA声明的数组,注意此时初始容量为0,而不是10。

注意:根据默认构造函数创建的集合,ArrayList list = new ArrayList(); 此时集合长度是 0。 

重载:有参构造ArrayList(int initialCapacity)
public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}

初始化集合大小创建ArrayList集合。当大于0时,给定多少那就创建多大的数组。当等于0时,创建一个空数组。当小于0时,抛出异常。

重载:ArrayList(Collection<? extends E> c)
public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // replace with empty array.
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

将已有的集合复制到ArrayList集合中。

添加元素

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  //添加元素之前,首先要确定集合的大小(是否需要扩容)
    elementData[size++] = e;
    return true;
}

如上述代码所示,在通过调用add方法添加元素之前,我们要首先调用ensureCapacityInternal方法来确定集合的大小,如果集合满了,则要进行扩容操作:

private void ensureCapacityInternal(int minCapacity) {//这里的minCapacity 是集合当前大小+1
    //elementData 是实际用来存储元素的数组,注意数组的大小和集合的大小不是相等的,前面的size是指集合大小
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
 
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {//如果数组为空,则从size+1的值和默认值10中取最大的
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;//不为空,则返回size+1
}
 
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
 
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

在ensureExplicitCapacity方法中,首先对修改次数modCount加一,这里的modCount给ArrayList的迭代器使用的,在并发操作被修改时,提供快速失败行为(保证modCount在迭代期间不变,否则抛出ConcurrentModificationException异常,源码867行),接着判断minCapacity是否大于当前ArrayList内部数组长度,大于的话调用下面grow方法对内部elementData扩容。

private void grow(int minCapacity) {
    int oldCapacity = elementData.length;//得到原始数组的长度
    int newCapacity = oldCapacity + (oldCapacity >> 1);//新数组的长度等于原数组长度的1.5倍
    if (newCapacity - minCapacity < 0)//当新数组长度仍然比minCapacity小,则为保证最小长度,新数组等于minCapacity
        newCapacity = minCapacity;
    //MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8 = 2147483639
    if (newCapacity - MAX_ARRAY_SIZE > 0)//当得到的新数组长度比 MAX_ARRAY_SIZE 大时,调用 hugeCapacity 处理大数组(将数组容量设置为Inteyger.MAX_VALUE)
        newCapacity = hugeCapacity(minCapacity);
    //调用 Arrays.copyOf 将原数组拷贝到一个大小为newCapacity的新数组(注意是拷贝引用)
    elementData = Arrays.copyOf(elementData, newCapacity);
}
 
private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) //
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ? //minCapacity > MAX_ARRAY_SIZE,则新数组大小为Integer.MAX_VALUE
        Integer.MAX_VALUE :
    MAX_ARRAY_SIZE;
}

扩容的核心方法就是调用前面的Arrays.copyOf方法,创建一个更大的数组,然后将原数组元素拷贝过去。

对ArrayList集合添加元素的总结:

  • 当通过ArrayList()构造一个空集合时,初始长度是0;第一次添加元素,会创建一个长度为10的数组,并将该元素赋值到数组的第一个位置。
  • 第二次添加元素时,集合不为空,而且由于集合的长度size + 1是小于数组的长度10,所以直接添加元素到数组的第二个位置,不用扩容。
  • 第十一次添加元素,此时size + 1 = 11,而数组长度是10,这时候创建一个长度为10 + 10 * 0.5 = 15的数组(扩容1.5倍),然后将原数组元素引用拷贝到新数组。并将第十一次添加的元素赋值到新数组下标为10的位置。
  • 第 Integer.MAX_VALUE - 7次添加元素时,创建一个大小为Integer.MAX_VALUE的数组,再进行元素添加。
  • 第Integer.MAX_VALUE + 1 次添加元素时,抛出 OutOfMemoryError 异常。

注意:可以向集合中添加 null ,因为数组可以有 null 值存在。

删除元素

public E remove(int index) {
    rangeCheck(index);  //判断给定索引的范围,超过集合大小则抛出异常
 
    modCount++;
    E oldValue = elementData(index);  //得到索引处的删除元素
 
    int numMoved = size - index - 1;
    if (numMoved > 0)  //size-index-1 > 0 表示 0<= index < (size-1),即索引不是最后一个元素
        //通过 System.arraycopy()将数组elementData 的下标index+1之后长度为 numMoved 的元素拷贝到从index开始的位置
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; //更新 size 的同时将数组最后一个元素置为 null,便于垃圾回收
 
    return oldValue;
}

remove(int index)方法表示删除索引index处的元素,首先通过rangeCheck(int index)方法判断给定索引的范围,超过集合大小则抛出异常。接着通过System.arraycopy方法对数组进行自身拷贝。

/*
* src:源数组
  srcPos:源数组要复制的起始位置
  dest:目的数组
  destPos:目的数组放置的起始位置
  length:复制的长度
  注意:src 和 dest都必须是同类型或者可以进行转换类型的数组。
*/
public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);

修改元素

public E set(int index, E element) {
    rangeCheck(index);//判断索引合法性
 
    E oldValue = elementData(index);//获得原数组指定索引的元素
    elementData[index] = element;//将指定所引处的元素替换为 element
    return oldValue;//返回原数组索引元素
}

通过调用set(int index,E element)方法在指定索引index处的元素替换为element,并返回原数组的元素。

通过调用rangeCheck(index)来检查索引合法性

private void rangeCheck(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

当索引为负数时,会抛出 java.lang.ArrayIndexOutOfBoundsException 异常。当索引大于集合长度时,会抛出 IndexOutOfBoundsException 异常。

查找元素

public E get(int index) {
    rangeCheck(index);
 
    return elementData(index);
}

同理,首先还是判断给定索引的合理性,然后直接返回处于该下标位置的数组元素。

二、LinkedList类

LinkedList类定义

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
  //...
}

  • 和ArrayList集合一样,LinkedList集合也实现了Cloneable接口和Serializable接口,分别用来支持克隆以及序列化。
  • List:表明它是一个列表,支持添加、删除、查找等操作,并且可以通过下标进行访问。
  •  Deque :继承自 Queue 接口,具有双端队列的特性,支持从两端插入和删除元素,方便实现栈和队列等数据结构。

注意:由于LinkedList底层数据结构是链表,内存地址不连续,只能通过指针来定位,不支持随机快速访问,所以不能实现RandomAccess接口。

LinkedList源码分析

LinkedList中的元素是通过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;
    }
}

初始化 

// 创建一个空的链表对象
public LinkedList() {
}

// 接收一个集合类型作为参数,会创建一个与传入集合相同元素的链表对象
public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}

LinkedList有两个构造函数,第一个是默认的空的构造函数,第二个是将已有元素的集合Collection的实例添加到LinkedList中,调用的是addAll()方法。

注意 :LinkedList是没有初始化链表大小的构造函数,因为链表不像数组,一个定义好的数组是必须要有确定的大小,然后去分配内存空间,而链表不同,它没有确定的大小,通过指针的移动来指向下一个内存地址的分配。

添加元素

addFirst(E e)

将指定元素添加到链表头

//将指定的元素附加到链表头节点
public void addFirst(E e) {
    linkFirst(e);
}
 
private void linkFirst(E e) {
    final Node<E> f = first;  //将头节点赋值给 f
    final Node<E> newNode = new Node<>(null, e, f);  //将指定元素构造成一个新节点,此节点的指向下一个节点的引用为头节点
    first = newNode;     //将新节点设为头节点,那么原先的头节点 f 变为第二个节点
    if (f == null)       //如果第二个节点为空,也就是原先链表是空
        last = newNode;  //将这个新节点也设为尾节点(前面已经设为头节点了)
    else
        f.prev = newNode;//将原先的头节点的上一个节点指向新节点
    size++;  //节点数加1
    modCount++;  //和ArrayList中一样,iterator和listIterator方法返回的迭代器和列表迭代器实现使用。
}
addLast(E e)和add(E e)

将指定元素添加到链表尾

//将元素添加到链表末尾
public void addLast(E e) {
    linkLast(e);
}
//将元素添加到链表末尾
public boolean add(E e) {
    linkLast(e);
    return true;
}
void linkLast(E e) {
    final Node<E> l = last;  //将l设为尾节点
    final Node<E> newNode = new Node<>(l, e, null);  //构造一个新节点,节点上一个节点引用指向尾节点l
    last = newNode;       //将尾节点设为创建的新节点
    if (l == null)        //如果尾节点为空,表示原先链表为空
        first = newNode;  //将头节点设为新创建的节点(尾节点也是新创建的节点)
    else
        l.next = newNode; //将原来尾节点下一个节点的引用指向新节点
    size++;  //节点数加1
    modCount++;  //和ArrayList中一样,iterator和listIterator方法返回的迭代器和列表迭代器实现使用。
}
add(int index, E element)

将指定的元素插入此列表中的指定位置

//将指定的元素插入此列表中的指定位置
public void add(int index, E element) {
    //判断索引 index >= 0 && index <= size中时抛出IndexOutOfBoundsException异常
    checkPositionIndex(index);
 
    if (index == size)//如果索引值等于链表大小
        linkLast(element);//将节点插入到尾节点
    else
        linkBefore(element, node(index));
}
 
void linkLast(E e) {
    final Node<E> l = last;//将l设为尾节点
    final Node<E> newNode = new Node<>(l, e, null);//构造一个新节点,节点上一个节点引用指向尾节点l
    last = newNode;//将尾节点设为创建的新节点
    if (l == null)//如果尾节点为空,表示原先链表为空
        first = newNode;//将头节点设为新创建的节点(尾节点也是新创建的节点)
    else
        l.next = newNode;//将原来尾节点下一个节点的引用指向新节点
    size++;//节点数加1
    modCount++;//和ArrayList中一样,iterator和listIterator方法返回的迭代器和列表迭代器实现使用。
}
 
Node<E> node(int index) {
    if (index < (size >> 1)) {//如果插入的索引在前半部分
        Node<E> x = first;//设x为头节点
        for (int i = 0; i < index; i++)//从开始节点到插入节点索引之间的所有节点向后移动一位
            x = x.next;
        return x;
    } else {//如果插入节点位置在后半部分
        Node<E> x = last;//将x设为最后一个节点
        for (int i = size - 1; i > index; i--)//从最后节点到插入节点的索引位置之间的所有节点向前移动一位
            x = x.prev;
        return x;
    }
}
 
void linkBefore(E e, Node<E> succ) {
    final Node<E> pred = succ.prev;//将pred设为插入节点的上一个节点
    final Node<E> newNode = new Node<>(pred, e, succ);//将新节点的上引用设为pred,下引用设为succ
    succ.prev = newNode;//succ的上一个节点的引用设为新节点
    if (pred == null)//如果插入节点的上一个节点引用为空
        first = newNode;//新节点就是头节点
    else
        pred.next = newNode;//插入节点的下一个节点引用设为新节点
    size++;
    modCount++;
}
addAll(Collection<? extends E> c)

按照指定集合的迭代器返回的顺序,将指定集合中的所有元素追加到此列表的末尾

addAll有两个重载函数:

  • addAll(Collection<? extends E>)型
  • addAll(int, Collection<? extends E>)型

我们平时习惯调用的 addAll(Collection<? extends E>) 型会转化为 addAll(int, Collection<? extends E>) 型。addAll(c):

//按照指定集合的••迭代器返回的顺序,将指定集合中的所有元素追加到此列表的末尾。
public boolean addAll(Collection<? extends E> c) {
    return addAll(size, c);
}
//真正核心的地方就是这里了,记得我们传过来的是size,c
public boolean addAll(int index, Collection<? extends E> c) {
    //检查index这个是否为合理。这个很简单,自己点进去看下就明白了。
    checkPositionIndex(index);
    //将集合c转换为Object数组 a
    Object[] a = c.toArray();
    //数组a的长度numNew,也就是由多少个元素
    int numNew = a.length;
    if (numNew == 0)
        //集合c是个空的,直接返回false,什么也不做。
        return false;
    //集合c是非空的,定义两个节点(内部类),每个节点都有三个属性,item、next、prev。
    Node<E> pred, succ;
    //构造方法中传过来的就是index==size
    if (index == size) {
        //linkedList中三个属性:size、first、last。 size:链表中的元素个数。 first:头节点  last:尾节点,就两种情况能进来这里
 
        //情况一、:构造方法创建的一个空的链表,那么size=0,last、和first都为null。linkedList中是空的。什么节点都没有。succ=null、pred=last=null
 
        //情况二、:链表中有节点,size就不是为0,first和last都分别指向第一个节点,和最后一个节点,在最后一个节点之后追加元素,就得记录一下最后一个节点是什么,所以把last保存到pred临时节点中。
        succ = null;
        pred = last;
    } else {
        //情况三、index!=size,说明不是前面两种情况,而是在链表中间插入元素,那么就得知道index上的节点是谁,保存到succ临时节点中,然后将succ的前一个节点保存到pred中,这样保存了这两个节点,就能够准确的插入节点了
        //举个简单的例子,有2个位置,1、2、如果想插数据到第二个位置,双向链表中,就需要知道第一个位置是谁,原位置也就是第二个位置上是谁,然后才能将自己插到第二个位置上。如果这里还不明白,先看一下开头对于各种链表的删除,add操作是怎么实现的。
        succ = node(index);
        pred = succ.prev;
    }
    //前面的准备工作做完了,将遍历数组a中的元素,封装为一个个节点。
    for (Object o : a) {
        @SuppressWarnings("unchecked") E e = (E) o;
        //pred就是之前所构建好的,可能为null、也可能不为null,为null的话就是属于情况一、不为null则可能是情况二、或者情况三
        Node<E> newNode = new Node<>(pred, e, null);
        //如果pred==null,说明是情况一,构造方法,是刚创建的一个空链表,此时的newNode就当作第一个节点,所以把newNode给first头节点
        if (pred == null)
            first = newNode;
        else
            //如果pred!=null,说明可能是情况2或者情况3,如果是情况2,pred就是last,那么在最后一个节点之后追加到newNode,如果是情况3,在中间插入,pred为原index节点之前的一个节点,将它的next指向插入的节点,也是对的
            pred.next = newNode;
        //然后将pred换成newNode,注意,这个不在else之中,请看清楚了。
        pred = newNode;
    }
    if (succ == null) {
        //如果succ==null,说明是情况一或者情况二,
        //情况一、构造方法,也就是刚创建的一个空链表,pred已经是newNode了,last=newNode,所以linkedList的first、last都指向第一个节点。
        //情况二、在最后节后之后追加节点,那么原先的last就应该指向现在的最后一个节点了,就是newNode。
        last = pred;
    } else {
        //如果succ!=null,说明可能是情况三、在中间插入节点,举例说明这几个参数的意义,有1、2两个节点,现在想在第二个位置插入节点newNode,根据前面的代码,pred=newNode,succ=2,并且1.next=newNode,1已经构建好了,pred.next=succ,相当于在newNode.next = 2; succ.prev = pred,相当于 2.prev = newNode, 这样一来,这种指向关系就完成了。first和last不用变,因为头节点和尾节点没变
        pred.next = succ;
        //。。
        succ.prev = pred;
    }
    //增加了几个元素,就把 size = size +numNew 就可以了
    size += numNew;
    modCount++;
    return true;
}

 说明:参数中的index表示在索引下标为index的结点(实际上是第index + 1个结点)的前面插入。     

在addAll函数中,addAll函数中还会调用到node函数,get函数也会调用到node函数,此函数是根据索引下标找到该结点并返回,具体代码如下:

Node<E> node(int 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; // 返回该结点
    }
}

 说明:在根据索引查找节点时,有一个小优化,结点在前半段则从头开始遍历;在后半段则从尾开始遍历,这样就保证了只需要遍历最多一半节点就可以找到指定索引的节点。

修改元素

通过调用 set(int index, E element) 方法,用指定的元素替换此列表中指定位置的元素

public E set(int index, E element) {
    //判断索引 index >= 0 && index <= size中时抛出IndexOutOfBoundsException异常
    checkElementIndex(index);
    Node<E> x = node(index);//获取指定索引处的元素
    E oldVal = x.item;
    x.item = element;//将指定位置的元素替换成要修改的元素
    return oldVal;//返回指定索引位置原来的元素
}

这里主要是通过 node(index) 方法获取指定索引位置的节点,然后修改此节点位置的元素即可。 

获取元素

LinkedList获取元素相关的方法一共有 3 个:

  1. getFirst():获取链表的第一个元素。
  2. getLast():获取链表的最后一个元素。
  3. get(int index):获取链表指定位置的元素。
// 获取链表的第一个元素
public E getFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return f.item;
}

// 获取链表的最后一个元素
public E getLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return l.item;
}

// 获取链表指定位置的元素
public E get(int index) {
  // 下标越界检查,如果越界就抛异常
  checkElementIndex(index);
  // 返回链表中对应下标的元素
  return node(index).item;
}

删除元素

LinkedList删除元素相关的方法一共有 5 个:

  1. removeFirst():删除并返回链表的第一个元素。
  2. removeLast():删除并返回链表的最后一个元素。
  3. remove(E e):删除链表中首次出现的指定元素,如果不存在该元素则返回 false。
  4. remove(int index):删除指定索引处的元素,并返回该元素的值。
  5. void clear():移除此链表中的所有元素。
// 删除并返回链表的第一个元素
public E removeFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
}

// 删除并返回链表的最后一个元素
public E removeLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return unlinkLast(l);
}

// 删除链表中首次出现的指定元素,如果不存在该元素则返回 false
public boolean remove(Object o) {
    // 如果指定元素为 null,遍历链表找到第一个为 null 的元素进行删除
    if (o == null) {
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null) {
                unlink(x);
                return true;
            }
        }
    } else {
        // 如果不为 null ,遍历链表找到要删除的节点
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item)) {
                unlink(x);
                return true;
            }
        }
    }
    return false;
}

// 删除链表指定位置的元素
public E remove(int index) {
    // 下标越界检查,如果越界就抛异常
    checkElementIndex(index);
    return unlink(node(index));
}

这里的核心在于 unlink(Node<E> x) 这个方法:

E unlink(Node<E> x) {
    // 断言 x 不为 null
    // assert x != null;
    // 获取当前节点(也就是待删除节点)的元素
    final E element = x.item;
    // 获取当前节点的下一个节点
    final Node<E> next = x.next;
    // 获取当前节点的前一个节点
    final Node<E> prev = x.prev;

    // 如果前一个节点为空,则说明当前节点是头节点
    if (prev == null) {
        // 直接让链表头指向当前节点的下一个节点
        first = next;
    } else { // 如果前一个节点不为空
        // 将前一个节点的 next 指针指向当前节点的下一个节点
        prev.next = next;
        // 将当前节点的 prev 指针置为 null,,方便 GC 回收
        x.prev = null;
    }

    // 如果下一个节点为空,则说明当前节点是尾节点
    if (next == null) {
        // 直接让链表尾指向当前节点的前一个节点
        last = prev;
    } else { // 如果下一个节点不为空
        // 将下一个节点的 prev 指针指向当前节点的前一个节点
        next.prev = prev;
        // 将当前节点的 next 指针置为 null,方便 GC 回收
        x.next = null;
    }

    // 将当前节点元素置为 null,方便 GC 回收
    x.item = null;
    size--;
    modCount++;
    return element;
}
  1. 首先获取待删除节点 x 的前驱和后继节点;
  2. 判断待删除节点是否为头节点或尾节点:
    • 如果 x 是头节点,则将 first 指向 x 的后继节点 next
    • 如果 x 是尾节点,则将 last 指向 x 的前驱节点 prev
    • 如果 x 不是头节点也不是尾节点,执行下一步操作
  3. 将待删除节点 x 的前驱的后继指向待删除节点的后继 next,断开 x 和 x.prev 之间的链接;
  4. 将待删除节点 x 的后继的前驱指向待删除节点的前驱 prev,断开 x 和 x.next 之间的链接;
  5. 将待删除节点 x 的元素置空,修改链表长度。

相关文章:

  • 第44天:Web开发-JavaEE应用反射机制类加载器利用链成员变量构造方法抽象方法
  • 代码随想录刷题day28|(栈与队列篇:栈)232.用栈实现队列
  • pycharm中配置PyQt6详细教程
  • Turborepo 使用配置
  • 深入探讨Web应用开发:从前端到后端的全栈实践
  • LLaMA-Factory|微调大语言模型初探索(4),64G显存微调13b模型
  • 苹果确认iOS 18.4四月初推出:Apple Intelligence将迎来中文支持
  • MFC开发:如何创建第一个MFC应用程序
  • 将 Vue 项目打包后部署到 Spring Boot 项目中的全面指南
  • Python在实际工作中的运用-基础操作
  • 数据库面试知识点总结
  • 口腔应用AI模型推荐
  • 论文略读:Uncovering Hidden Representations in Language Models
  • 使用IDEA提交SpringBoot项目到Gitee上
  • 算法日记25:01背包(DFS->记忆化搜索->倒叙DP->顺序DP->空间优化)
  • 组合优化问题的机器学习研究——以图匹配问题为例
  • 二叉树(中等题)
  • AI赋能传统系统:Spring AI Alibaba如何用大模型重构机票预订系统?
  • 3.3.2 交易体系构建——缠论操作思路
  • Ubuntu 下 nginx-1.24.0 源码分析 - ngx_array_push
  • 日本广岛大学一处拆迁工地发现疑似未爆弹
  • 国务院新闻办公室发布《新时代的中国国家安全》白皮书
  • 2025年上海好护士揭晓,上海护士五年增近两成达12.31万人
  • 中国工程院院士、国医大师石学敏逝世
  • 宜昌全域高质量发展:机制创新与产业重构的双向突围
  • 瑞士联邦主席凯勒-祖特尔、联邦副主席帕姆兰会见何立峰