五天手撕数据结构之——线性结构
线性结构概论
线性的数据结构元素之间是一对一的关系。
除了第一个元素,所有的元素都只有唯一前驱;
除了最后一个元素所有的元素都有唯一的后继。
常见的数据结构:
- 数组:像一排带有编号的储物柜,每个柜子都有自己的位置和相应的编号
- 链表:像一列拥堵的车流,每辆车只能知道前后的车辆编号
- 栈:像一叠盘子,只能从顶部取和放
- 队列:像是排队买票,最前面的最先消费
数组:数据的固定存放柜
数组是最简单最直接的线性数据结构
我们可以把他想象成一排储物柜
基本概念和特点:
- 连续存储:数组在内存中占据一块连续的空间,就像每个储物柜都是紧挨着的
- 固定大小:数组在创建时就已经确定了其容量(长度),之后通常不能改变,就像打好的柜子不能再改变大小
- 索引访问:每个元素都有自己的唯一编号,称为索引。在大多数编程语言中,索引从0开始,在查找数据是可以通过索引号快速访问任何一个元素,时间复杂度为O(1)
下面使用java语言的ArrayList(动态数组)演示数组的增删改查
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| public static void main(String[] args) { List<String> cabinet = new ArrayList<String>() {{ add("101-肥皂"); add("102-衣服"); add("103-饭盒"); add("104-空"); add("105-空"); }}; System.out.println("柜子存放情况:"+cabinet);
String string = cabinet.get(1); System.out.println("102柜子中存放的是:"+string);
cabinet.set(3,"104-酱油"); System.out.println("104放入东西后:"+cabinet);
cabinet.add("106-空"); System.out.println("新柜子:"+cabinet);
cabinet.remove(3); System.out.println("砍掉一个格之后:"+cabinet); }
|
优点和缺点:
优点
- 高速随机访问:通过索引可直接访问任何元素
- 内存效率高:连续存储,无需额外存储元素关联关系
缺点
- 大小固定:静态数组一旦创建,容量难以改变
- 插入删除成本高:在中间插入或删除元素需要移动大量后续元素以保持连续性
链表:数据的火车车厢
当数组的固定大小和插入删除的低效成为问题时,链表就登场了。链表像一列火车,每节车厢(节点)都装载着货物(数据),并通过挂钩(指针)连接下一节车厢。

如图所示头指针指向节点A每个节点的Next指针指向下一个节点,最后一个节点的Next指向Null表示结束
链表主要类型:
- 单向链表:节点只指向下一个节点
- 双向链表:节点同时指向前一个和后一个节点,可以双向遍历
- 循环链表:尾节点执行头节点,形成一个环
java实现单向链表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199
| package com.springdemo.demo;
class Node<T> { T data; Node<T> next;
public Node(T data) { this.data = data; this.next = null; } }
public class SinglyLinkedList<T> { private Node<T> head; private int size;
public SinglyLinkedList() { this.head = null; this.size = 0; }
public boolean isEmpty() { return size == 0; }
public int size() { return size; }
public void add(T data) { Node<T> newNode = new Node<>(data); if (head == null) { head = newNode; } else { Node<T> current = head; while (current.next != null) { current = current.next; } current.next = newNode; } size++; }
public void add(int index, T data) { if (index < 0 || index > size) { throw new IndexOutOfBoundsException("索引越界:" + index + ",链表长度:" + size); } Node<T> newNode = new Node<>(data); if (index == 0) { newNode.next = head; head = newNode; } else { Node<T> prev = head; for (int i = 0; i < index - 1; i++) { prev = prev.next; } newNode.next = prev.next; prev.next = newNode; } size++; }
public T remove(int index) { if (index < 0 || index >= size) { throw new IndexOutOfBoundsException("索引越界:" + index + ",链表长度:" + size); } Node<T> removeNode; if (index == 0) { removeNode = head; head = head.next; } else { Node<T> prev = head; for (int i = 0; i < index - 1; i++) { prev = prev.next; } removeNode = prev.next; prev.next = removeNode.next; } size--; return removeNode.data; }
public T get(int index) { if (index < 0 || index >= size) { throw new IndexOutOfBoundsException("索引越界:" + index + ",链表长度:" + size); } Node<T> current = head; for (int i = 0; i < index; i++) { current = current.next; } return current.data; }
public void traverse() { if (isEmpty()) { System.out.println("链表为空"); return; } Node<T> current = head; StringBuilder sb = new StringBuilder(); sb.append("链表内容:"); while (current != null) { sb.append(current.data).append(" -> "); current = current.next; } sb.delete(sb.length() - 4, sb.length()); System.out.println(sb); }
public static void main(String[] args) { SinglyLinkedList<String> list = new SinglyLinkedList<>();
list.add("101"); list.add("102"); list.add("103"); list.traverse();
list.add(1, "104"); list.traverse();
System.out.println("索引2的节点数据:" + list.get(2));
list.remove(1); list.traverse();
System.out.println("链表长度:" + list.size()); System.out.println("链表是否为空:" + list.isEmpty()); } }
|
优点和缺点
优点:
- 动态大小:无需预先分配固定空间,可以灵活的增长和缩小
- 高效的插入/删除:在已知节点位置只需要球盖指针指向,无需移动大量元素
缺点:
- 内存开销大:每个节点需要额外空间存储指针
- 顺序访问:无法像数组一样直接通过索引访问,必须从头开始检索
栈:数据的叠盘子
栈是一种 后进先出 的数据结构,就像餐厅里叠放的盘子,你总是取走最后放上去的
栈只允许在一端(称为 栈顶)进行插入(入栈)和删除(出栈)操作。另一端称为 栈底。
核心操作:
- **push(data)**:将数据压入栈顶。
- **pop()**:弹出并返回栈顶元素。
- **peek() / top()**:查看栈顶元素但不弹出。
- **is_empty()**:检查栈是否为空。

栈常被用于释放内存,撤销更改,浏览器前进后退,括号匹配等
java实现数组版本栈
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
|
public class ArrayStack<T> { private Object[] stack; private int top; private static final int DEFAULT_CAPACITY = 10;
public ArrayStack() { stack = new Object[DEFAULT_CAPACITY]; top = -1; }
public ArrayStack(int initialCapacity) { if (initialCapacity <= 0) { throw new IllegalArgumentException("初始容量必须大于0"); } stack = new Object[initialCapacity]; top = -1; }
public void push(T element) { if (top == stack.length - 1) { resize(stack.length * 2); } stack[++top] = element; }
@SuppressWarnings("unchecked") public T pop() { if (isEmpty()) { throw new RuntimeException("栈为空,无法执行出栈操作"); } T element = (T) stack[top]; stack[top--] = null; return element; }
@SuppressWarnings("unchecked") public T peek() { if (isEmpty()) { throw new RuntimeException("栈为空,无栈顶元素"); } return (T) stack[top]; }
public boolean isEmpty() { return top == -1; }
public int size() { return top + 1; }
private void resize(int newCapacity) { Object[] newStack = new Object[newCapacity]; System.arraycopy(stack, 0, newStack, 0, top + 1); stack = newStack; }
public static void main(String[] args) { ArrayStack<String> stack = new ArrayStack<>();
stack.push("101"); stack.push("102"); stack.push("103"); System.out.println("栈大小:" + stack.size()); System.out.println("栈顶元素:" + stack.peek());
String popElement = stack.pop(); System.out.println("出栈元素:" + popElement); System.out.println("出栈后栈大小:" + stack.size());
System.out.println("栈是否为空:" + stack.isEmpty());
stack.pop(); stack.pop(); System.out.println("清空后栈是否为空:" + stack.isEmpty()); } }
|
栈的核心优势:O (1) 的操作效率 + 天然贴合 “后进先出” 场景 + 安全可控的封装性
队列:数据的排队通道
队列是一种 先进先出 的数据结构,就像生活中任何需要排队的地方(如超市收银台),先来的人先接受服务。
队列允许在一端(称为 队尾)进行插入(入队)操作,在另一端(称为 队头)进行删除(出队)操作。
核心操作:
- **enqueue(data)**:将数据加入队尾。
- **dequeue()**:从队头移除并返回数据。
- **front() / peek()**:查看队头数据但不移除。
- **is_empty()**:检查队列是否为空。
队列常用于需要按顺序处理任务的场景,如:打印任务队列、消息队列、广度优先搜索等。
java循环数组实现队列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138
|
public class ArrayQueue<T> { private Object[] queue; private int front; private int rear; private int size; private static final int DEFAULT_CAPACITY = 10;
public ArrayQueue() { queue = new Object[DEFAULT_CAPACITY]; front = 0; rear = 0; size = 0; }
public ArrayQueue(int initialCapacity) { if (initialCapacity <= 0) { throw new IllegalArgumentException("初始容量必须大于0"); } queue = new Object[initialCapacity]; front = 0; rear = 0; size = 0; }
public void enqueue(T element) { if (size == queue.length) { resize(queue.length * 2); } queue[rear] = element; rear = (rear + 1) % queue.length; size++; }
@SuppressWarnings("unchecked") public T dequeue() { if (isEmpty()) { throw new RuntimeException("队列为空,无法执行出队操作"); } T element = (T) queue[front]; queue[front] = null; front = (front + 1) % queue.length; size--; return element; }
@SuppressWarnings("unchecked") public T peek() { if (isEmpty()) { throw new RuntimeException("队列为空,无队首元素"); } return (T) queue[front]; }
public boolean isEmpty() { return size == 0; }
public int size() { return size; }
private void resize(int newCapacity) { Object[] newQueue = new Object[newCapacity]; for (int i = 0; i < size; i++) { newQueue[i] = queue[(front + i) % queue.length]; } queue = newQueue; front = 0; rear = size; }
public static void main(String[] args) { ArrayQueue<String> queue = new ArrayQueue<>();
queue.enqueue("101"); queue.enqueue("102"); queue.enqueue("103"); System.out.println("队列大小:" + queue.size()); System.out.println("队首元素:" + queue.peek());
String deqElement = queue.dequeue(); System.out.println("出队元素:" + deqElement); System.out.println("出队后队列大小:" + queue.size());
System.out.println("队列是否为空:" + queue.isEmpty());
queue.dequeue(); queue.dequeue(); System.out.println("清空后队列是否为空:" + queue.isEmpty()); } }
|
如何选择?
- 需要 快速随机访问 和已知数据量时,选择 数组。
- 需要 频繁在任意位置插入或删除 元素,且数据量变化大时,选择 链表。
- 需要实现 “撤销” 功能或 反向处理 顺序时,考虑 栈。
- 需要按 “先来后到” 的顺序处理任务时,使用 队列。