List集合类介绍(源码分析)

本文深入剖析了Java集合框架中的ArrayList、LinkedList及Vector的特点与区别,详细解释了它们的底层实现,如数组与链表的运用,以及扩容、增删查改等操作的原理。

前言

基于JDK1.8源码解析List集合类中的ArrayList,LinkedList以及Vector和ArrayList的比较。

先说一下各个List集合类最重要的知识点以及集合类间的区别:

ArrayList:

  • 底层是数组;
  • 线程不安全;
  • 每次扩容后的容量都是原来容量的1.5倍(向下取整),扩容的实现需要调用底层由C/C++编写的native方法;
  • 增删时,需要数组的拷贝复制,调用底层由C/C++编写的native方法;

LinkedList:

  • 底层是链表;
  • 线程不安全;
  • 不需要扩容;
  • LinkedList的增删其实就是链表中添加或者删除结点;

Vector(一般很少使用):

  • 底层是数组;
  • 线程安全;
  • 每次扩容后的容量都是原来容量的1倍;
  • 增删的底层原理与ArrayList相同;

这里主要总结ArrayList(底层是数组,线程不安全)与LinkedList(底层是链表,线程不安全),以及Vector(底层是数组,线程安全)与ArrayList的区别。

1、ArrayList解析

                                                     ArrayList关系图

1.1 ArrayList文件顶部注解

一般源码的精髓都集中在文件最上方的总览注解中,因此,在阅读源码时,阅读总览注解非常重要(原谅我英语渣)。

通过注解我们可以提取出以下几点重要信息:

  • ArrayList是一个可以存储任何对象元素(包括null)的长度可变数组;
  • ArrayList底层是一个Object数组,封装了基本的增删改查等操作,并且利用底层的arrayCopy()实现了动态扩容;
  • 除了add()方法的时间复杂度是O(n)之外,其他方法例如get()、set()、size()等时间复杂度都是常量级。如果不考虑底层内存拷贝的耗时,其实add()方法如果不指定下标,默认插到尾部,时间复杂度依旧是常量级;
  • 如果单次添加的数据过大的话,可以调用ArrayList的ensureCapacity()方法直接给ArrayList开辟一块内存容量,避免中间扩容时多次的内存拷贝造成不必要的时间浪费;
  • ArrayList和Vector类似但是本身线程不安全,如果要对结构做修改,例如增删元素等,在多线程环境下必须加锁,但是也可以通过Collections.synchronizedList()包装成一个线程安全的容器;
  • ArrayList还引入了快速失败机制,关于快速失败机制:https://blog.youkuaiyun.com/weixin_39738307/article/details/106100118;

1.2 ArrayList的成员变量

先来看一下ArrayList的几个成员变量。

    private static final long serialVersionUID = 8683452581122892189L;

    /**
     * Default initial capacity.
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * Shared empty array instance used for empty instances.
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
     * Shared empty array instance used for default sized empty instances. We
     * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
     * first element is added.
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * The array buffer into which the elements of the ArrayList are stored.
     * The capacity of the ArrayList is the length of this array buffer. Any
     * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
     * will be expanded to DEFAULT_CAPACITY when the first element is added.
     */
    transient Object[] elementData; // non-private to simplify nested class access

    /**
     * The size of the ArrayList (the number of elements it contains).
     *
     * @serial
     */
    private int size;

结合注解,可以知道:

  • serialVersionUID:序列化时为了保持版本的兼容性;
  • DEFAULT_CAPACITY:初始化时的默认容量,默认为10;
  • EMPTY_ELEMENTDATA:静态不可变的Object空数组,所有的容量设置为0的list都共享这个空的Object空数组;
  • DEFAULTCAPACITY_EMPTY_ELEMENTDATA:默认的数组,所有没有在初始化时指定容量大小的ArrayList。在第一次插入元素时,如果elementData数组是被赋值为DEFAULTCAPACITY_EMPTY_ELEMENTDATA,那么就会被扩容至默认大小10;如果是被赋值为EMPTY_ELEMENTDATA,那按照原来的扩容方式扩容;
  • elementData:ArrayList中用来存储数据的Object数组,基本上增删扩容等操作都是围绕着这个Object数组展开的。并且ArrayList的size是这个数组的元素个数。如果没有在初始化的时候指定ArrayList的容量,这个数组一开始是空的,只有当插入了第一个元素之后才会扩容至默认的初试容量;
  • size:Object数组中的元素个数。

除此之外还有一个MAX_ARRAY_SIZE,静态final的整型变量,数值是最大整数-8,限定可分配的最大数量,防止ArrayList分配的过大导致OutOfmemoryError。

1.3 构造方法

ArrayList提供了三种方式的构造器,可以构造一个默认初始容量为10的空列表、构造一个指定初始容量的空列表以及构造一个包含指定collection的元素的列表,这些元素按照该collection的迭代器返回的顺序排列的。

其中比较常用的是无参构造器。结合上面的分析,使用无参构造器和使用指定容量的构造器并且指定容量为0的时候,ArrayList会将存储数据的Object设置为一个空的指定数组。

1.4 插入方法

ArrayList的插入方法有两种,可以指定插入位置,或者不指定,插在数组尾。

Add(E e):

这个函数看起来比较简单,先是调用了ensureCapacityInternal方法,然后把元素放在数组的下标为size的位置(数组中最后一个元素的后面一个位置),size再自增。

先来看看ensureCapacityInternal方法做了什么。

    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

这个函数的方法的代码只有一行,接着看看calculateCapacity和ensureExplicitCapacity方法的实现。

calculateCapacity:

    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

结合上面对成员变量的解读,可以知道,如果elementData数组等于DEFAULTCAPACITY_EMPTY_ELEMENTDATA,也就是说elementData现在被赋值为一个指定的空数组,那么就返回DEFAULT_CAPACITY(10)和minCapacity(size+1)中的最大值(对于还没插入过元素的无参构造器构造生成的ArrayList就是返回10,至于为什么会有这样一个比较过程,感兴趣的可以自行扩展阅读)。

ensureExplicitCapacity:

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

如果元素的个数超过了数组的长度,就调用扩容方法。

modCount变量是用于快速失败机制:https://blog.youkuaiyun.com/weixin_39738307/article/details/106100118;

其中grow方法就是ArrayList中核心的扩容方法了。

/**
     * Increases the capacity to ensure that it can hold at least the
     * number of elements specified by the minimum capacity argument.
     *
     * @param minCapacity the desired minimum capacity
     */
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

需要注意的是"int newCapacity = oldCapacity + (oldCapacity >> 1);"。这行代码说明扩容是原来容量的1.5倍向下取整。

其中,Arrays.copyOf()调用的是由C++编写的arraycpoy()本地方法。

到这里可以知道Add(E e)的流程可以概括为:

  • 检查是否需要扩容,如果需要扩容的话底层调用扩容方法进行扩容;
  • modCount+1,用于实现快速失败机制;
  • 在数组尾端添加元素。

add(int index, E element):

再返回开头的add方法源码,并且结合之前分析的Add(E e)调用的方法,可以看出

  • 检查下标是否合法;
  • 检查是否需要扩容;
  • 调用arraycpoy()方法,将要插入的位置上的元素以及之后的元素往后挪;
  • 插入元素到指定坐标;

1.5 get方法

获取指定下标的值。

步骤:

  • 检查坐标
  • 返回对应坐标的元素

1.6 set方法

设置指定下标的值。

步骤:

  • 检查坐标
  • 替换对应坐标上的值
  • 返回旧值

1.7 remove方法 

remove(int index)

删除指定下标的元素。

    /**
     * Removes the element at the specified position in this list.
     * Shifts any subsequent elements to the left (subtracts one from their
     * indices).
     *
     * @param index the index of the element to be removed
     * @return the element that was removed from the list
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E remove(int index) {
        rangeCheck(index);

        modCount++;
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

步骤:

  • 检查下标;
  • 设置快速失败机制的参数;
  • 通过调用底层的arraycopy方法将删除元素后面的所有元素全部左移一位;
  • 将最后一个位置的引用设置为null,配合GC清理;
  • 返回旧值。

remove(Object o)

删除第一个与传入参数相同的元素。

    /**
     * Removes the first occurrence of the specified element from this list,
     * if it is present.  If the list does not contain the element, it is
     * unchanged.  More formally, removes the element with the lowest index
     * <tt>i</tt> such that
     * <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>
     * (if such an element exists).  Returns <tt>true</tt> if this list
     * contained the specified element (or equivalently, if this list
     * changed as a result of the call).
     *
     * @param o element to be removed from this list, if present
     * @return <tt>true</tt> if this list contained the specified element
     */
    public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

通过注解可以知道, 删除第一个与指定对象相同的元素(如果存在的话);如果不存在的话,就不改变这个list。

这里调用了fastRemove方法,看看它的实现。

    /*
     * Private remove method that skips bounds checking and does not
     * return the value removed.
     */
    private void fastRemove(int index) {
        modCount++;
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work
    }

可以看出也是一个配合实现快速失败机制的方法。

步骤:

  • 遍历数组,找到与传入对象相同的第一个元素(ArrayList也可以存储null,如果传入的是null的话,判断用==);
  • 设置快速失败机制的参数;
  • 通过调用底层的arraycopy方法将删除元素后面的所有元素全部左移一位;
  • 将最后一个位置的引用设置为null,配合GC清理;
  • 删除成功则返回true,否则返回false。

2、ArrayList与Vector的区别

Vector是一个比较老的集合类了,现在开发基本不用,在不要求线程安全的情况下一般多用ArrayList,如果要求线程安全可以使用别的方法,例如Collections.synchronizedList()方法包装ArrayList,或者使用JUC包下的CopyOnWriteArrayList来代替。因此对Vector只要稍作了解即可,比如Vector和ArrayList的区别。

  • Vector的底层也是数组,从ArrayList源码文件最上方的总览注释上面可以得知,ArrayList和Vector最大的区别在于Vector是线程安全,而ArrayList是线程不安全的;
  • ArrayList默认扩容是1.5倍,Vector扩容是1倍。

3、LinkedList解析

在平时的开发过程中,虽然一般ArrayList用的比较多,但是LinkedList也是需要学习的,在一些特殊的场景中也会用到。LinkedList的分析相对于ArrayList会简单一些。

LinkedList实现了Deque接口,因此可以像操作队列和栈一样操作LinkedList。

3.1 LinkedList文件顶部注解

老样子,还是先通读一遍类文件顶部的注释。

 补充:关于快速失败机制:https://blog.youkuaiyun.com/weixin_39738307/article/details/106100118;

3.2 LinkedList的成员变量

    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;
  • size:LinkedList中的元素个数;
  • first:链表头结点;
  • last:链表尾结点;

3.3 构造方法

无参构造器:

构造一个包含指定collection的元素的列表,这些元素按照该collection的迭代器返回的顺序排列的:

一般无参构造器用的比较多。

3.4 get方法

    /**
     * Returns the element at the specified position in this list.
     *
     * @param index index of the element to return
     * @return the element at the specified position in this list
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
    }

先看一下"checkElementIndex(index)"做了什么:

    private void checkElementIndex(int index) {
        if (!isElementIndex(index))
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

继续跟踪:

    /**
     * Tells if the argument is the index of an existing element.
     */
    private boolean isElementIndex(int index) {
        return index >= 0 && index < size;
    }

可以知道, "checkElementIndex(index)"对传入的下标做合法性判断。

再看看"node(index)":

    /**
     * Returns the (non-null) Node at the specified element index.
     */
    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;
        }
    }

这个方法根据下标在头结点和尾结点中选取较近的一个结点,然后遍历直到找到对应下标的元素。

可以看到get方法做了两件事:

  • 检验传入的下标的合法性;
  • 根据下标选择头结点或者尾结点,并从选取的结点开始遍历直到找到对应下标的元素并返回。

3.5 add方法

 add(int index, E element):

    /**
     * Inserts the specified element at the specified position in this list.
     * Shifts the element currently at that position (if any) and any
     * subsequent elements to the right (adds one to their indices).
     *
     * @param index index at which the specified element is to be inserted
     * @param element element to be inserted
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public void add(int index, E element) {
        checkPositionIndex(index);

        if (index == size)
            linkLast(element);
        else
            linkBefore(element, node(index));
    }

添加元素到链表的指定位置。

跟踪checkPositionIndex(index):

    private void checkPositionIndex(int index) {
        if (!isPositionIndex(index))
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

继续跟踪 isPositionIndex(index):

    private boolean isPositionIndex(int index) {
        return index >= 0 && index <= size;
    }

可以看到,checkPositionIndex函数是对下标做检查,如果下标不合法就抛出异常(但是,为啥一个同样的函数要写两遍?) 。

其中if-else调用的两个函数:

    /**
     * Links e as last element.
     */
    void linkLast(E e) {
        final Node<E> l = last;
        final Node<E> newNode = new Node<>(l, e, null);
        last = newNode;
        if (l == null)
            first = newNode;
        else
            l.next = newNode;
        size++;
        modCount++;
    }

插入到链表尾。

    /**
     * Inserts element e before non-null Node succ.
     */
    void linkBefore(E e, Node<E> succ) {
        // assert succ != null;
        final Node<E> pred = succ.prev;
        final Node<E> newNode = new Node<>(pred, e, succ);
        succ.prev = newNode;
        if (pred == null)
            first = newNode;
        else
            pred.next = newNode;
        size++;
        modCount++;
    }

插入到指定结点之前。

所以步骤可以概括为:

  • 下标检查;
  • 插入元素到指定位置。

add(E e):

    /**
     * Appends the specified element to the end of this list.
     *
     * <p>This method is equivalent to {@link #addLast}.
     *
     * @param e element to be appended to this list
     * @return {@code true} (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        linkLast(e);
        return true;
    }

添加元素到链表尾。

3.6 set方法

    /**
     * Replaces the element at the specified position in this list with the
     * specified element.
     *
     * @param index index of the element to replace
     * @param element element to be stored at the specified position
     * @return the element previously at the specified position
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E set(int index, E element) {
        checkElementIndex(index);
        Node<E> x = node(index);
        E oldVal = x.item;
        x.item = element;
        return oldVal;
    }

结合之前分析过的,可以概括出步骤为:

  • 检查下标;
  • 替换指定位置为新值;
  • 返回旧值。

3.7 remove方法

remove(int index):

    /**
     * Removes the element at the specified position in this list.  Shifts any
     * subsequent elements to the left (subtracts one from their indices).
     * Returns the element that was removed from the list.
     *
     * @param index the index of the element to be removed
     * @return the element previously at the specified position
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E remove(int index) {
        checkElementIndex(index);
        return unlink(node(index));
    }

删除指定位置上的元素。node方法在之前已经分析过了,先看看unlink方法:

    /**
     * Unlinks non-null node x.
     */
    E unlink(Node<E> x) {
        // 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 {
            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;
    }

可以看出,unlink方法其实是将传入的结点移出链表,同时那个结点的前后两个结点再进行连接。把结点里对元素对象的引用设置为null,如果外部也没有引用指向那个元素对象,那GC就自然会帮我们回收啦~

步骤可以概括为:

  • 检查下标;
  • 删除结点;
  • 返回删除的结点的元素。

remove(Object o):

    /**
     * Removes the first occurrence of the specified element from this list,
     * if it is present.  If this list does not contain the element, it is
     * unchanged.  More formally, removes the element with the lowest index
     * {@code i} such that
     * <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>
     * (if such an element exists).  Returns {@code true} if this list
     * contained the specified element (or equivalently, if this list
     * changed as a result of the call).
     *
     * @param o element to be removed from this list, if present
     * @return {@code true} if this list contained the specified element
     */
    public boolean remove(Object o) {
        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;
    }

删除LinkedList中第一个与传入对象相同的元素 ,如果没有就不做修改。

结合前面已经分析过的,步骤可以概括为:

  • 遍历结点找到对应的元素,如果找不到返回false;
  • 删除找到的结点。

4、总结

要点总结

  • ArrayList底层是数组,初始容量默认为10,每次扩容为原容量的1.5倍(向下取整);
  • ArrayList基于动态数组实现,扩容的底层通过JNI调用JVM里由C/C++编写的native方法实现,可概括为申请一块新的内存存放新数组,再把原数组的数据拷贝到新内存中;
  • ArrayList和LinkedList都不是线程安全,底层存储元素的是一个Object数组,能存放任何元素包括null;
  • ArrayList和LinkedList都实现了快速失败机制;
  • 删除元素是不会减少容量,存储数组最后一个空出来的位置的引用会变成null,减少容量要调用trimToSize();
  • Vector线程安全,增删操作都用synchronized实现同步,但是效率有损失,并且每次扩容都是100%的比率增长,比较消耗内存,目前已经过时。可以用JUC包下的CopyOnWriteArrayList容器代替或者用Collections的方法包装ArrayList(例:List list = Collections.synchronizedList(new Array List<>());)保证线程安全。Vector初始容量为10,每次以一倍的比率扩容,扩容的底层实现与ArrayList相同;
  • LinkedList的底层是一个双向链表,查找和增删时间复杂度均为O(n),增删在大部分情况下优于ArrayList(不是绝对,例如一直在末尾删除,ArrayList不需要移动数组,时间复杂度是常量级),查询在大部分情况下不如ArrayList(不是绝对,例如查询的是首个元素);
  • 增删多的情况一般用LinkedList。

参考资料

  • JDK1.8源码
<think>好的,我现在需要回答用户关于静态链表是否使用指针进行操作的问题。首先,我要理解静态链表的基本概念。根据用户提供的引用资料,静态链表是用结构体数组实现的,每个节点包含数据域和游标。游标代替了指针,存储下一个节点的数组下标。 接下来,我需要确认C语言中指针的作用。指针通常用于动态内存分配和访问内存地址,但静态链表使用预先分配的数组。因此,静态链表的操作不依赖指针,而是通过数组索引(游标)来管理节点间的逻辑关系。 然后,查看用户提供的引用内容。引用[1]提到静态链表的存储是结构体数组,游标存放下一个节点的位置下标。引用[4]指出静态链表的游标是相对地址,即数组下标,不需要指针。这些资料都支持静态链表不使用指针的观点。 另外,需要区分静态链表与动态链表的区别。动态链表每个节点用指针指向下一个节点的内存地址,而静态链表用数组索引。因此,在C语言中,静态链表的操作通过修改游标值,而不是指针操作。 可能存在的疑问是,结构体数组本身是否涉及指针。但数组名在C语言中虽然是地址,但静态链表的操作并不使用指针变量来动态分配或链接节点,而是通过下标访问数组元素。因此,用户不需要显式使用指针来操作静态链表。 最后总结:静态链表在C语言中不需要使用指针,而是通过数组下标(游标)实现节点间的链接,适用于不支持动态内存的环境。</think>在C语言中,**静态链表不需要使用传统意义上的指针进行操作**,而是通过数组下标(游标)实现节点间的逻辑关系。以下是具体分析: --- ### 一、静态链表的实现原理 1. **存储结构** 静态链表通过**预分配的结构体数组**实现,每个节点包含两部分: - **数据域**:存储有效数据(如`int data`) - **游标(cur)**:存储下一个节点的**数组下标**(而非内存地址)[^1][^4] ```c typedef struct { int data; // 数据域 int cur; // 游标(数组下标) } StaticNode; ``` 2. **与动态链表的区别** - 动态链表:通过指针(`Node* next`)直接操作内存地址。 - 静态链表:通过游标(`int cur`)间接表示逻辑关系,**无需动态内存分配**。 --- ### 二、静态链表的操作特点 1. **无需指针操作** - **插入/删除**:通过修改游标值调整逻辑顺序,例如将节点A的游标指向节点B的数组下标[^4]。 - **遍历**:从数组的某个下标开始,依次读取游标跳转至下一个节点。 - **示例代码片段**: ```c // 在静态链表中插入节点(伪代码) void insert(StaticNode array[], int pos, int new_data) { int new_index = get_free_node(array); // 获取空闲节点下标 array[new_index].data = new_data; array[new_index].cur = array[pos].cur; // 修改游标 array[pos].cur = new_index; // 不涉及指针操作 } ``` 2. **优势与限制** - **优势**: - 避免动态内存管理的复杂性(如内存泄漏)[^4]。 - 适用于不支持动态内存分配的环境(如嵌入式系统)[^4]。 - **限制**: - 需要预先分配固定大小的数组,灵活性较低。 - 不能随机存取,时间复杂度与链表相同(如查找需$O(n)$)。 --- ### 三、对比动态链表的指针操作 | **操作** | **动态链表** | **静态链表** | |----------------|-----------------------------|-----------------------------| | 节点定义 | `struct Node { int data; Node* next; }` | `struct Node { int data; int cur; }` | | 内存管理 | 动态分配(`malloc`/`free`) | 预分配数组 | | 逻辑关系实现 | 直接指针跳转 | 游标(数组下标)跳转 | --- ### 四、总结 静态链表通过**数组下标(游标)替代指针**,在C语言中实现了链式逻辑结构。其核心操作依赖**数组索引的修改**,而非传统指针操作,因此特别适合**内存受限或需要固定存储空间的场景**。 --- 相关问题
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值