数据结构(一)数组和链表

目录

[1 数组](#1 数组)

[1.1 静态数组](#1.1 静态数组)

[1.1.1 增](#1.1.1 增)

[1.1.2 删](#1.1.2 删)

[1.1.3 改](#1.1.3 改)

[1.1.4 查](#1.1.4 查)

[1.2 动态数组](#1.2 动态数组)

[1.2.1 关键点1:自动扩缩容](#1.2.1 关键点1:自动扩缩容)

[1.2.2 关键点2:索引越界的检查](#1.2.2 关键点2:索引越界的检查)

[1.2.3 关键点3:删除元素谨防内存泄漏](#1.2.3 关键点3:删除元素谨防内存泄漏)

[1.2.4 手搓动态数组](#1.2.4 手搓动态数组)

[2 链表](#2 链表)

[2.1 单链表的基本操作](#2.1 单链表的基本操作)

[2.1.1 查/改](#2.1.1 查/改)

[2.1.2 增](#2.1.2 增)

[2.1.3 删](#2.1.3 删)

[2.2 双链表的基本操作](#2.2 双链表的基本操作)

[2.2.1 查/改](#2.2.1 查/改)

[2.2.2 增](#2.2.2 增)

[2.2.3 删](#2.2.3 删)

[2.3 链表代码实现](#2.3 链表代码实现)

[2.3.1 关键点1:同时持有头尾节点的引用](#2.3.1 关键点1:同时持有头尾节点的引用)

[2.3.2 关键点2:虚拟头尾节点](#2.3.2 关键点2:虚拟头尾节点)

[2.3.3 关键点3:内存泄露?](#2.3.3 关键点3:内存泄露?)

[2.3.4 手搓双链表](#2.3.4 手搓双链表)

[2.3.5 手搓单链表](#2.3.5 手搓单链表)

[3 环形数组](#3 环形数组)

[3.1 原理](#3.1 原理)

[3.2 举例](#3.2 举例)

[3.3 手搓环形数组](#3.3 手搓环形数组)

[3.4 优势和缺点](#3.4 优势和缺点)


1 数组

1.1 静态数组

连续内存必须一次性分配,分配完了之后就不能随意增减了

java 复制代码
// 定义一个大小为 10 的静态数组
int[] arr = new int[10];

// 使用索引赋值
arr[0] = 1;
arr[1] = 2;

// 使用索引取值
int a = arr[0];

1.1.1 增

情况一:数组末尾追加(append)元素。只是对索引赋值,所以在数组末尾追加元素的时间复杂度是O(1)

情况二:数组中间插入(insert)元素。因为涉及到数据搬移,给新元素腾地方,在数组中间插入元素的时间复杂度是 O(N)

情况三:数组空间已满。数组的扩容操作会涉及到新数组的开辟和数据的复制,时间复杂度是 O(N)

1.1.2 删

情况一:删除末尾元素。删除数组尾部元素的本质就是进行一次随机访问,时间复杂度是O(1)

情况二:删除中间元素。因为涉及到数据搬移,在数组中间删除元素的时间复杂度是O(N)

1.1.3 改

给定指定索引,修改索引对应的元素的值,时间复杂度 O(1)

1.1.4 查

给定指定索引,查询索引对应的元素的值,时间复杂度 O(1)

1.2 动态数组

动态数组底层还是静态数组,只是自动帮我们进行数组空间的扩缩容,并把增删查改操作进行了封装,让我们使用起来更方便而已

java 复制代码
// 创建动态数组
// 不用显式指定数组大小,它会根据实际存储的元素数量自动扩缩容
ArrayList<Integer> arr = new ArrayList<>();

for (int i = 0; i < 10; i++) {
    // 在末尾追加元素,时间复杂度 O(1)
    arr.add(i);
}

// 在中间插入元素,时间复杂度 O(N)
// 在索引 2 的位置插入元素 666
arr.add(2, 666);

// 在头部插入元素,时间复杂度 O(N)
arr.add(0, -1);

// 删除末尾元素,时间复杂度 O(1)
arr.remove(arr.size() - 1);

// 删除中间元素,时间复杂度 O(N)
// 删除索引 2 的元素
arr.remove(2);

// 根据索引查询元素,时间复杂度 O(1)
int a = arr.get(0);

// 根据索引修改元素,时间复杂度 O(1)
arr.set(0, 100);

// 根据元素值查找索引,时间复杂度 O(N)
int index = arr.indexOf(666);

1.2.1 关键点1:自动扩缩容

简单的扩缩容的策略:

  • 当数组元素个数达到底层静态数组的容量上限时,扩容为原来的 2 倍;
  • 当数组元素个数缩减到底层静态数组的容量的 1/4 时,缩容为原来的 1/2。

1.2.2 关键点2:索引越界的检查

有两个检查越界的方法,分别是 checkElementIndex 和 checkPositionIndex,你可以看到它俩的区别仅仅在于 index < size 和 index <= size。

合法的索引一定是 index < size,但如果是要在数组中插入新元素,那么新元素可能的插入位置并不是元素的索引,而是索引之间的空隙:

nums = [ | 5 | 6 | 7 | 8 | ],

这些空隙都是合法的插入位置,所以说 index == size 也是合法的,这就是 checkPositionIndex 和 checkElementIndex 的区别。

1.2.3 关键点3:删除元素谨防内存泄漏

删除元素时,我都会把被删除的元素置为 null。

如果一个对象再也无法被访问到,那么这个对象占用的内存才会被释放;否则,垃圾回收器会认为这个对象还在使用中,就不会释放这个对象占用的内存。

如果你不执行 data[size - 1] = null 这行代码,那么 data[size - 1] 这个引用就会一直存在,你可以通过 data[size - 1] 访问这个对象,所以这个对象被认为是可达的,它的内存就一直不会被释放,进而造成内存泄漏。

内存泄漏:程序已经不再使用的对象,却仍然被引用占用,无法被垃圾回收释放。

1.2.4 手搓动态数组

java 复制代码
package Theoretical_foundation;

import java.util.Arrays;
import java.util.NoSuchElementException;

public class MyArrayList<E> {
    private E[] data; // 真正存储数据的底层数组
    private int size; // 实际存放元素个数
    private static final int INIT_CAP = 1; // 默认初始容量

    public MyArrayList() {
        this(INIT_CAP);
    }

    public MyArrayList(int initCap) {
        data = (E[]) new Object[initCap];
        size = 0;
    }

    public void addLast(E e) {
        int cap = data.length;
        if (size == cap) {
            resize(cap * 2);
        }
        data[size] = e;
        size++;
    }

    public void add(int index, E e) {
        checkPositionIndex(index);

        int cap = data.length;
        if (cap == size) {
            resize(cap * 2);
        }

        for (int i = size; i > index; i--) {
            data[i] = data[i - 1];
        }
        data[index] = e;
        size++;
    }

    public void addFirst(E e) {
        add(0, e);
    }

    public E removeLast() {
        if (size == 0) {
            throw new NoSuchElementException();
        }
        int cap = data.length;
        if (size == cap / 4) {
            resize(cap / 2);
        }
        E deleteElement = data[size - 1];
        // 必须给最后一个元素置为 null,否则会内存泄漏
        data[size - 1] = null;
        size--;
        return deleteElement;
    }

    public E remove(int index) {
        checkElementIndex(index);
        int cap = data.length;
        if (size == cap / 4) {
            resize(cap / 2);
        }
        E deleteElement = data[index];
        for (int i = index; i < size - 1; i++) {
            data[i] = data[i + 1];
        }
        data[size - 1] = null;
        size--;
        return deleteElement;
    }

    public E removeFirst() {
        return remove(0);
    }

    public E get(int index) {
        checkElementIndex(index);
        return data[index];
    }

    public E set(int index, E e) {
        checkElementIndex(index);
        E oldElement = data[index];
        data[index] = e;
        return oldElement;
    }

    //工具类
    public int size() {
        return size;
    }

    public boolean isEmpty() {
        return size == 0;
    }

    private void resize(int newCap) {
        E[] temp = (E[]) new Object[newCap];
        for (int i = 0; i < size; i++) {
            temp[i] = data[i];
        }
        data = temp;
    }

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

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

    // 检查 index 索引位置是否可以添加元素
    private void checkPositionIndex(int index) {
        if (!isPositionIndex(index)) {
            throw new IndexOutOfBoundsException("Index:" + index + ", Size:" + size);
        }
    }

    // 检查 index 索引位置是否可以存在元素
    private void checkElementIndex(int index) {
        if (!isElementIndex(index)) {
            throw new IndexOutOfBoundsException("Index:" + index + ", Size:" + size);
        }
    }

    private void display() {
        System.out.println("size=" + size + ", cap=" + data.length);
        //在 Java 里,直接打印数组 → 输出的是【内存地址】,不是内容!
        //必须用 Arrays.toString(数组) → 才能看到里面的元素!
        System.out.println(Arrays.toString(data));
    }

    public static void main(String[] args) {
        MyArrayList<Integer> arr = new MyArrayList<>(3);
        arr.display();

        for (int i = 0; i <= 5; i++) {
            arr.addLast(i);
        }
        arr.display();

        arr.add(1, 9);
        arr.display();

        arr.addFirst(100);
        arr.display();
        int val = arr.removeLast();

        for (int i = 0; i < arr.size(); i++) {
            System.out.println(arr.get(i));
        }
    }
}

扩容判断在添加元素前,满了(size=cap)就扩容,避免放不下。

缩容判断在删除元素前/后都可以,元素太少(size=cap/4)才缩容。

为什么是 1/4 缩容? 防止频繁扩容缩容(抖动),给中间留缓冲空间,保证性能稳定。

1/2 扩容、1/4 缩容是业界标准设计,JDK 的 ArrayList 就是这么写的!

2 链表

java 复制代码
class Node<E> {
    E val;
    Node<E> next;
    Node<E> prev;

    Node(Node<E> prev, E element, Node<E> next) {
        this.val = element;
        this.next = next;
        this.prev = prev;
    }
}

2.1 单链表的基本操作

java 复制代码
class ListNode {
    int val;
    ListNode next;
    ListNode(int x) { val = x; }
}

// 输入一个数组,转换为一条单链表
ListNode createLinkedList(int[] arr) {
    if (arr == null || arr.length == 0) {
        return null;
    }
    ListNode head = new ListNode(arr[0]);
    ListNode cur = head;
    for (int i = 1; i < arr.length; i++) {
        cur.next = new ListNode(arr[i]);
        cur = cur.next;
    }
    return head;
}

2.1.1 查/改

单链表的遍历/查找/修改,时间复杂度是 O(N),其中 n 是链表的长度。

2.1.2 增

情况一:在单链表头部插入新元素。我们会持有单链表的头结点,所以只需要将插入的节点接到头结点之前,并将新插入的节点作为头结点即可,时间复杂度是O(1)

情况二:在单链表尾部插入新元素。遍历到链表尾部,时间复杂度是O(N)

情况三:在单链表中间插入新元素。先找到插入位置的前驱节点,时间复杂度是O(N)

2.1.3 删

情况一:在单链表中删除一个节点。删除一个节点,首先要找到要被删除节点的前驱节点,然后把这个前驱节点的 next 指针指向被删除节点的下一个节点,时间复杂度是O(N)

情况二:在单链表尾部删除元素。找到倒数第二个节点,然后把它的 next 指针置为 null,时间复杂度是O(N)

情况三:在单链表头部删除元素。时间复杂度是O(1)

2.2 双链表的基本操作

java 复制代码
class DoublyListNode {
    int val;
    DoublyListNode next, prev;
    DoublyListNode(int x) { val = x; }
}

DoublyListNode createDoublyLinkedList(int[] arr) {
    if (arr == null || arr.length == 0) {
        return null;
    }
    DoublyListNode head = new DoublyListNode(arr[0]);
    DoublyListNode cur = head;
    // for 循环迭代创建双链表
    for (int i = 1; i < arr.length; i++) {
        DoublyListNode newNode = new DoublyListNode(arr[i]);
        cur.next = newNode;
        newNode.prev = cur;
        cur = cur.next;
    }
    return head;
}

2.2.1 查/改

对于双链表的遍历和查找,我们可以从头节点或尾节点开始,根据需要向前或向后遍历。最坏时间复杂度是O(N)。访问或修改节点时,可以根据索引是靠近头部还是尾部,选择合适的方向遍历,这样可以一定程度上提高效率。

2.2.2 增

情况一:在双链表头部插入新元素。我们会持有双链表的头结点,时间复杂度是O(1)

情况二:在双链表尾部插入新元素。遍历到链表尾部,时间复杂度是O(N)

情况三:在双链表中间插入新元素。先找到插入位置的前驱节点,时间复杂度是O(N)

2.2.3 删

情况一:在双链表中删除一个节点。时间复杂度是O(N)

情况二:在双链表尾部删除元素。时间复杂度是O(N)

情况三:在双链表头部删除元素。时间复杂度是O(1)

2.3 链表代码实现

2.3.1 关键点1:同时持有头尾节点的引用

有尾部节点的引用,就可以在 O(1) 的时间复杂度内完成尾部添加元素的操作。

删除单链表尾结点的时候,遍历到倒数第二个节点(尾结点的前驱),才能通过指针操作把尾结点删掉。此时,顺便把尾结点的引用更新。

2.3.2 关键点2:虚拟头尾节点

在创建双链表时就创建一个虚拟头节点和一个虚拟尾节点,无论双链表是否为空,这两个节点都存在。

假设虚拟头尾节点分别是 dummyHead 和 dummyTail,那么一条空的双链表长这样:

java 复制代码
dummyHead <-> dummyTail

添加几个元素,那么链表长这样:

复制代码
dummyHead <-> 1 <-> 2 <-> 3 <-> dummyTail

有了头尾虚拟节点,无论链表是否为空,都只需要考虑在中间插入元素的情况就可以了,这样代码会简洁很多。虚拟节点是内部实现,对外不可见。

2.3.3 关键点3:内存泄露?

被删除节点的指针未置为 null,不会引起内存泄露的问题,因为 Java 的垃圾回收的判断机制是看这个对象是否被别人引用。

不过删除节点时,最好还是把被删除节点的指针都置为 null,这是个好习惯。

2.3.4 手搓双链表

java 复制代码
package Theoretical_foundation;

import java.util.NoSuchElementException;

public class MyDoublyLinkedList<E> {
    // 虚拟头尾节点
    final private Node<E> head, tail;
    private int size;

    public MyDoublyLinkedList() {
        this.head = new Node<>(null);
        this.tail = new Node<>(null);
        head.next = tail;
        tail.prev = head;
        this.size = 0;
    }

    // Node 是【静态内部类】, Node 只给链表用,是私有工具,封装在内部更安全。
    // static 作用:属于【类】,不属于【对象】
    // static 使用场景:①内部类。②常量(共用)。③工具方法、不依赖对象的方法。
    private static class Node<E> {
        E val;
        Node<E> next;
        Node<E> prev;

        Node(E val) {
            this.val = val;
        }
    }

    public void addLast(E e) {
        Node<E> newNode = new Node<>(e);
        Node<E> temp = tail.prev;

        newNode.prev = temp;
        newNode.next = tail;

        temp.next = newNode;
        tail.prev = newNode;

        size++;
    }

    public void addFirst(E e) {
        Node<E> newNode = new Node<>(e);
        Node<E> temp = head.next;

        newNode.prev = head;
        newNode.next = temp;

        head.next = newNode;
        temp.prev = newNode;

        size++;
    }

    public void add(int index, E e) {
        checkPositionIndex(index);
        if (index == size) {
            addLast(e);
            return;
        }

        Node<E> newNode = new Node<>(e);
        Node<E> p = getNode(index);
        Node<E> temp = p.prev;

        newNode.prev = temp;
        newNode.next = p;

        temp.next = newNode;
        p.prev = newNode;

        size++;
    }

    public E removeLast() {
        if (size < 1) {
            throw new NoSuchElementException();
        }
        Node<E> deleteNode = tail.prev;
        Node<E> temp = deleteNode.prev;

        temp.next = tail;
        tail.prev = temp;

        deleteNode.prev = null;
        deleteNode.next = null;

        size--;
        return deleteNode.val;
    }

    public E removeFirst() {
        if (size < 1) {
            throw new NoSuchElementException();
        }
        Node<E> deleteNode = head.next;
        Node<E> temp = deleteNode.next;

        head.next = temp;
        temp.prev = head;
        deleteNode.prev = null;
        deleteNode.next = null;

        size--;
        return deleteNode.val;
    }

    public E remove(int index) {
        checkElementIndex(index);
        Node<E> deleteNode = getNode(index);
        Node<E> temp = deleteNode.prev;

        temp.next = deleteNode.next;
        deleteNode.next.prev = temp;

        deleteNode.prev = null;
        deleteNode.next = null;

        size--;
        return deleteNode.val;
    }

    public E get(int index) {
        checkElementIndex(index);
        Node<E> p = getNode(index);
        return p.val;
    }

    public E getFirst() {
        if (size < 1) {
            throw new NoSuchElementException();
        }
        return head.next.val;
    }

    public E getLast() {
        if (size < 1) {
            throw new NoSuchElementException();
        }
        return tail.prev.val;
    }

    public E set(int index, E e) {
        checkElementIndex(index);
        Node<E> p = getNode(index);
        E oldValue = p.val;
        p.val = e;
        return oldValue;
    }

    // 工具函数
    public int size() {
        return size;
    }

    public boolean isEmpty() {
        return size == 0;
    }

    private Node<E> getNode(int index) {
        checkElementIndex(index);
        Node<E> temp = head.next;
        while (index != 0) {
            temp = temp.next;
            index--;
        }
        return temp;
    }

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

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

    private void checkPositionIndex(int index) {
        if (!isPositionIndex(index)) {
            throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
        }
    }

    private void checkElementIndex(int index) {
        if (!isElementIndex(index)) {
            throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
        }
    }

    private void display(){
        System.out.println("size = "+size);
        for (Node<E> p=head.next;p!=tail;p=p.next){
            System.out.print(p.val +" <-> ");
        }
        System.out.println("null");
        System.out.println();
    }

    public static void main(String[] args) {
        MyDoublyLinkedList<Integer> list = new MyDoublyLinkedList<>();
        list.addLast(1);
        list.display();
        list.addLast(2);
        list.display();
        list.addLast(3);
        list.display();
        list.addFirst(0);
        list.display();
        list.add(2, 100);

        list.display();
        // size = 5
        // 0 <-> 1 <-> 100 <-> 2 -> 3 -> null
    }
}

2.3.5 手搓单链表

java 复制代码
package Theoretical_foundation;

import java.util.NoSuchElementException;


public class MySinglyLinkedList<E> {

    // head 是虚拟头节点,tail 是真实尾节点
    private final Node<E> head;
    private Node<E> tail;
    private int size;

    public MySinglyLinkedList() {
        this.head = new Node<>(null);
        this.tail = head;
        this.size = 0;
    }

    private static class Node<E> {
        E val;
        Node<E> next;

        Node(E val) {
            this.val = val;
            this.next = null;
        }
    }

    public void addLast(E e) {
        Node<E> newNode = new Node<>(e);
        tail.next = newNode;
        tail = newNode;
        size++;
    }

    public void addFirst(E e) {
        Node<E> newNode = new Node<>(e);
        newNode.next = head.next;
        head.next = newNode;
        if (size == 0) {
            tail = newNode;
        }
        size++;
    }

    public void add(int index, E e) {
        checkPositionIndex(index);
        if (index == 0) {
            addFirst(e);
            return;
        }
        if (index == size) {
            addLast(e);
            return;
        }
        Node<E> newNode = new Node<>(e);
        Node<E> p = getNode(index - 1);
        newNode.next = p.next;
        p.next = newNode;
        size++;
    }

    public E removeLast() {
        if (isEmpty()) {
            throw new NoSuchElementException();
        }
        if (size == 1) {
            E deleteVal = tail.val;
            tail = head;
            head.next = null;
            size--;
            return deleteVal;
        } else {
            Node<E> temp = getNode(size - 2);
            E deleteVal = tail.val;
            temp.next = null;
            tail = temp;

            size--;
            return deleteVal;
        }
    }

    public E removeFirst() {
        if (isEmpty()) {
            throw new NoSuchElementException();
        }
        Node<E> temp = head.next;
        E deleteVal = temp.val;
        head.next = temp.next;
        temp.next = null;
        if (size == 1) {
            tail = head;
        }
        size--;
        return deleteVal;
    }

    public E remove(int index) {
        checkElementIndex(index);
        if (index == size - 1) {
            return removeLast();
        }
        Node<E> temp = getNode(index - 1);
        Node<E> deleteNode = temp.next;
        temp.next = deleteNode.next;
        deleteNode.next = null;

        size--;
        return deleteNode.val;
    }

    public E getFirst() {
        if (isEmpty()) {
            throw new NoSuchElementException();
        }
        return head.next.val;
    }

    public E getLast() {
        if (isEmpty()) {
            throw new NoSuchElementException();
        }
        return tail.val;
    }

    public E get(int index) {
        checkElementIndex(index);
        return getNode(index).val;
    }

    public E set(int index, E e) {
        checkElementIndex(index);
        Node<E> temp = getNode(index);
        E oldVal = temp.val;
        temp.val = e;
        return oldVal;
    }

    //工具函数
    public int size() {
        return size;
    }

    public boolean isEmpty() {
        return size == 0;
    }

    private Node<E> getNode(int index) {
        checkElementIndex(index);
        Node<E> temp = head.next;
        while (index != 0) {
            temp = temp.next;
            index--;
        }
        return temp;
    }


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

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

    private void checkPositionIndex(int index) {
        if (!isPositionIndex(index)) {
            throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
        }
    }

    private void checkElementIndex(int index) {
        if (!isElementIndex(index)) {
            throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
        }
    }

    public static void main(String[] args) {
        MySinglyLinkedList<Integer> list = new MySinglyLinkedList<>();
        list.addFirst(1);
        list.addFirst(2);
        list.addLast(3);
        list.addLast(4);
        list.add(2, 5);

        System.out.println(list.removeFirst()); // 2
        System.out.println(list.removeLast()); // 4
        System.out.println(list.remove(1)); // 5

        System.out.println(list.getFirst()); // 1
        System.out.println(list.getLast()); // 3
        System.out.println(list.get(1)); // 3
    }
}

3 环形数组

环形数组技巧利用求模(余数)运算,将普通数组变成【逻辑上】的环形数组,可以让我们用**O(1)**的时间在数组头部增删元素。

3.1 原理

维护了两个指针 start 和 end,start 指向第一个有效元素的索引,end 指向最后一个有效元素的下一个位置索引。

这样,当我们在数组头部添加或删除元素时,只需要移动 start 索引,而在数组尾部添加或删除元素时,只需要移动 end 索引。

当 start, end 移动超出数组边界(< 0 或 >= arr.length)时,我们可以通过求模运算 % 让它们转一圈到数组头部或尾部继续工作,这样就实现了环形数组的效果。

3.2 举例

现在有一个长度为 6 的数组,现在其中只装了 3 个元素,如下(未装元素的位置用 _ 标识):

复制代码
[1, 2, 3, _, _, _]

现在我们要在数组头部删除元素 1,那么我们可以把数组变成这样:

复制代码
[_, 2, 3, _, _, _]

即,仅仅把元素 1 的位置标记为空,不做数据搬移

此时,如果我们要在数组头部增加元素 4 和元素 5,我们可以把数组变成这样:

复制代码
[4, 2, 3, _, _, 5]

你可以看到,当头部没有位置添加新元素时,它转了一圈,把新元素加到尾部了。

3.3 手搓环形数组

环形数组的区间被定义为左闭右开的,即区间[start, end)包含数组元素。

java 复制代码
package Theoretical_foundation;

public class CycleArray<T> {
    private T[] arr;
    private int start;
    private int end;
    private int count;
    private int size;

    public CycleArray() {
        this(1);
    }

    @SuppressWarnings("unchecked")
    public CycleArray(int size) {
        this.size = size;
        this.arr = (T[]) new Object[size];
        // start 指向第一个有效元素的索引,闭区间
        this.start = 0;
        // end 指向最后一个有效元素的下一个位置索引,开区间
        this.end = 0;
        this.count = 0;
    }

    @SuppressWarnings("unchecked")
    private void resize(int newSize) {
        T[] newArr = (T[]) new Object[newSize];
        for (int i = 0; i < count; i++) {
            newArr[i] = arr[(start + i) % size];
        }
        arr = newArr;
        // 重置 start 和 end 指针
        start = 0;
        end = count;
        size = newSize;
    }


    public void addFirst(T val) {
        if (isFull()) {
            resize(size * 2);
        }
        start = (start - 1 + size) % size;
        arr[start] = val;
        count++;
    }

    public void removeFirst() {
        if (isEmpty()) {
            throw new IllegalStateException("Array is empty");
        }
        arr[start] = null;
        start = (start + 1) % size;
        count--;
        if (count > 0 && count == size / 4) {
            resize(size / 2);
        }
    }


    public void addLast(T val) {
        if (isFull()) {
            resize(size * 2);
        }
        arr[end] = val;
        end = (end + 1) % size;
        count++;
    }


    public void removeLast() {
        if (isEmpty()) {
            throw new IllegalStateException("Array is empty");
        }
        end = (end - 1 + size) % size;
        arr[end] = null;
        count--;
        if (count > 0 && count == size / 4) {
            resize(size / 2);
        }
    }

    public T getFirst() {
        if (isEmpty()) {
            throw new IllegalStateException("Array is empty");
        }
        return arr[start];
    }

    public T getLast() {
        if (isEmpty()) {
            throw new IllegalStateException("Array is empty");
        }
        return arr[(end - 1 + size) % size];
    }

    public boolean isFull() {
        return count == size;
    }

    public int size() {
        return count;
    }

    public boolean isEmpty() {
        return count == 0;
    }
}

3.4 优势和缺点

在数组增删头部元素的时间复杂度是 O(N),因为需要搬移元素。但是使用环形数组可以实现在 O(1) 的时间复杂度内增删头部元素的。

环形数组实现其他方法,时间复杂度相比普通数组,有退化吗?没有。

  • 删除指定索引的元素,也要做数据搬移,复杂度 O(N);
  • 获取指定索引的元素(随机访问),通过 start 计算出真实索引,复杂度 O(1);
  • 在指定索引插入元素,也要做数据搬移,复杂度是 O(N)。

环形数组可以实现头部增删(addFirst、removeFirst)的时间复杂度为O(1),其他操作的时间复杂度与普通动态数组基本一致,而普通动态数组头部增删复杂度是O(N),但为什么编程语言标准库中的动态数组容器,底层没有使用环形数组实现?

  1. 设计目标不一致:动态数组的核心设计目标是「优先优化高频操作」------ 尾部增删和随机访问,这两个操作在业务中占比99%;而头部增删属于低频操作,没必要为了低频操作,牺牲高频操作的性能。

  2. 环形数组会削弱核心优势:动态数组最核心的优势是「随机访问快」,普通数组直接通过index访问(O(1),1条CPU指令);而环形数组需要先计算真实索引(realIndex = (start + index) % size),多了加法+取模操作,随机访问变慢;同时环形数组数据不连续,遍历速度也会降低。

  3. 实现复杂、扩容成本高:普通数组扩容只需简单调用内存拷贝,高效且简单;而环形数组扩容需要重新计算start、end指针,还要拼接不连续的数据,代码复杂、扩容速度更慢,维护成本更高。

数据结构的选择,核心是「匹配业务场景」------ 没有最优的数据结构,只有最适合的,标准库放弃环形数组,正是遵循了这个设计原则。

补充:环形数组并非无用,它的最佳应用场景是「队列(Queue)」,因为队列只需要尾部添加、头部删除,刚好匹配环形数组O(1)的优势,且不需要频繁随机访问,完美规避其缺点。

相关推荐
代码改善世界2 小时前
【数据结构与算法】二叉树题解
数据结构
像污秽一样2 小时前
算法设计与分析-算法效率分析基础-蛮力法
数据结构·算法·排序算法
仰泳的熊猫2 小时前
题目1834:蓝桥杯2016年第七届真题-路径之谜
数据结构·c++·算法·蓝桥杯·深度优先·图论
Darkwanderor2 小时前
数据结构——单调栈和单调队列
数据结构·c++·单调栈·单调队列
自信150413057592 小时前
数据结构之队列的实现
c语言·数据结构·算法·链表
宵时待雨2 小时前
C++笔记归纳8:stack & queue
开发语言·数据结构·c++·笔记·算法
24白菜头2 小时前
第十六届蓝桥杯C&C++大学B组
数据结构·c++·笔记·算法·职场和发展·蓝桥杯
小年糕是糕手2 小时前
【35天从0开始备战蓝桥杯 -- Day1】
jvm·数据结构·c++·程序人生·算法·职场和发展·蓝桥杯
xlp666hub6 小时前
C++ 链表修炼指南
数据结构·c++