目录
[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),但为什么编程语言标准库中的动态数组容器,底层没有使用环形数组实现?
-
设计目标不一致:动态数组的核心设计目标是「优先优化高频操作」------ 尾部增删和随机访问,这两个操作在业务中占比99%;而头部增删属于低频操作,没必要为了低频操作,牺牲高频操作的性能。
-
环形数组会削弱核心优势:动态数组最核心的优势是「随机访问快」,普通数组直接通过index访问(O(1),1条CPU指令);而环形数组需要先计算真实索引(realIndex = (start + index) % size),多了加法+取模操作,随机访问变慢;同时环形数组数据不连续,遍历速度也会降低。
-
实现复杂、扩容成本高:普通数组扩容只需简单调用内存拷贝,高效且简单;而环形数组扩容需要重新计算start、end指针,还要拼接不连续的数据,代码复杂、扩容速度更慢,维护成本更高。
数据结构的选择,核心是「匹配业务场景」------ 没有最优的数据结构,只有最适合的,标准库放弃环形数组,正是遵循了这个设计原则。
补充:环形数组并非无用,它的最佳应用场景是「队列(Queue)」,因为队列只需要尾部添加、头部删除,刚好匹配环形数组O(1)的优势,且不需要频繁随机访问,完美规避其缺点。