数据结构数据结构
LiSir第一章绪论
数据相关概念
数据:所有能输入到计算机中并能被程序识别和处理的符号集合
数据分类:
- 数值数据:整数、实数等。
- 非数值数据:图形、图像、声音、文字等
数据对象:是性质相同的数据元素的集合,是数据的一个子集。比如字符集合C={‘A’,’B’,’C’}
数据元素:数据的基本单位,在程序中作为一个整体进行考虑和处理。
数据项:构成数据元素的最小单位。又被称为属性。
!!!一个数据元素由若干个数据项组成,数据项是数据不可分割的最小单位。
数据结构即数据的组织形式,它是数据元素之间存在的一种或多种特定关系的数据元素集合

数据结构的三要素:
- 数据元素间的逻辑关系 —->数据的逻辑结构
- 数据元素及其关系在计算机存储器中存储的方式 —–>数据的存储结构(或物理结构)
- 施加在该数据上的操作 ——->数据运算。
数据结构三要素
逻辑结构
数据元素之间的关系可以是元素间代表某种含义的自然关系,也可以是为处理问题方便而人为定义的关系,这种人为定义的关系称为数据元素间的逻辑关系,相应的结构称为逻辑结构/概念结构。
数据结构从逻辑上分为四类:
集合、线性结构、树结构、图结构
集合
元素之间的关系:无。
特点:数据元素之间除了”属于同一个集合”的关系外,别无其他逻辑关系。是最松散的,不受任何制约的关系。

线性结构
比如:线性表、栈、队列、串、数组、广义表
元素之间的关系:一对一。
特点:开始元素和终端元素都是唯一的,除此之外,其余元素都有且仅有一个前驱元素和一个后记元素

树结构
比如:树、二叉树
元素之间的关系:一对多。
特点:开始元素唯一,终端元素不为一。除了终端元素外,每个元素有一个或多个后继元素;除开始元素外,每个元素有且仅有一个前驱元素

图结构
比如:有向图和无向图、带权图和无权图
元素之间的关系:多对多。
特点:所有元素都可能有多个前驱元素和多个后继元素

总结
逻辑结构(有时直接称为数据结构)
- 线性结构:线性表、栈、队列、串、广义表
- 非线性结构:集合、树、图。
说明:
- 逻辑结构与数据元素本身的形式、内容无关
- 逻辑结构与数据元素的相对位置无关
- 逻辑结构与所含结点个数无关
- 逻辑结构与计算机无关
数据的存储结构
顺序存储(物理上连续)
- 定义:顺序存储是最直观的一种存储方式,它将数据元素按照一定的顺序存放在一段连续的内存空间中。
- 特点:
- 访问速度快,因为可以直接通过索引进行访问。
- 插入和删除操作较慢,因为需要移动大量元素来保持顺序或腾出空间。
- 存储密度高(存储利用率高),因为不需要额外的空间来存储元素之间的关系。
- 适用场景:适用于频繁访问但很少修改的数据集合。
链式存储
- 定义:链式存储使用指针来链接数据元素,每个元素(节点)包含指向下一个节点的链接。
- 特点:
- 插入和删除操作快,只需改变相邻节点之间的指针即可。
- 访问速度相对较慢,因为需要遍历链表直到找到目标元素。
- 存储密度较低,因为每个节点除了存储自身数据外还需要存储指向其他节点的指针。
- 适用场景:适合频繁修改的数据集合,特别是当数据量不确定或经常变化时
索引存储
- 定义:索引存储是为了解决顺序存储中插入删除效率低下的问题而提出的,它为每个数据元素创建一个索引项,索引项通常包含元素的位置信息。
- 特点:
- 通过索引可以快速定位到具体的数据元素。
- 需要维护额外的索引表,这会增加存储开销。
- 支持高效的随机访问。
- 适用场景:适合大数据量且需要快速定位的情况。
散列存储
- 定义:散列存储使用哈希函数将关键字映射到数组的一个位置上进行存储。
- 特点:
- 基于哈希函数的查找非常快速,理想情况下可以达到O(1)的时间复杂度。
- 可能会出现哈希冲突,需要合适的解决冲突策略。
- 散列表的空间利用效率取决于哈希函数的选择以及处理冲突的方式。
- 适用场景:适合需要快速查找、插入和删除操作的应用场景。
数据的运算
定义:施加在数据上的运算包括运算的定义和实现。
- 运算的定义是针对逻辑结构的,指出运算的功能。
- 运算的实现是针对存储结构的,指出运算的具体操作步骤。
一般的操作都是创建,销毁,增删改查。
数据类型、抽象数据类型
数据类型是一个值的集合和定义在此集合上的一组操作的总称。
举例:
- int:值的范围:-2147483648 ~ 2147483647 ,可进⾏操作:加、减、乘、除、模运算…
- bool:值的范围:true、false,可进⾏操作:与、或、⾮…
1)原子类型。其值不可再分的数据类型。
2)结构类型。其值可以再分解为若干成分(分量)的数据类型。
抽象数据类型(Abstract Data Type,ADT)是抽象数据组织及与之相关的操作。
- ADT 用数学化的语言定义数据的逻辑结构、定义运算。与具体的实现无关。(与存储的结构无关)
算法
算法的概念
程序 = 数据结构 + 算法
算法(Algorithm)是对特定问题求解步骤的一种描述,它是指令的有限序列,其中的每条指令 表示一个或多个操作
算法的特性
- 有穷性。一个算法必须总在执行有穷步之后结束,且每一步都可在有穷时间内完成。
- 确定性。算法中每条指令必须有确切的含义,对于相同的输入只能得出相同的输出。
- 可行性。算法中描述的操作都可以通过已经实现的基本运算执行有限次来实现。
- 输入。一个算法有零个或多个输入,这些输入取自于某个特定的对象的集合。
- 输出。一个算法有一个或多个输出,这些输出是与输入有着某种特定关系的量。
“好”算法的特点
正确性。算法应能够正确地解决求解问题。
可读性。算法应具有良好的可读性,以帮助人们理解。
健壮性。输入非法数据时,算法能适当地做出反应或进行处理,而不会产生莫名其妙的输出结果。
高效率与低存储量需求
时间复杂度
时间复杂度:衡量时间开销和问题规模n的关系。
口诀:常对幂指阶
图示:

上图表示为:**$O(1) < O(\log_2{n}) < O(n) < O(n\log_2{n}) < O(n^2) < O(n^3) < O(2^n) < O(n!) < O(n^n)$**
在计算时间复杂度时,我们可以只考虑阶数高的部分。用大O表示法表示
在利用大O表示法表示时间复杂度时:
- 顺序执行的代码只会影响常数项,可以忽略,
- 只需要挑选循环中的一个基本操作分析它执行的次数x与n的关系即可。
- 多层嵌套循环,只关注最深层次循环执行的次数。
- x的数量级O(x)就是时间复杂度T(n)
比如:
T1(n)=3n+3 —->时间复杂度是O(n)
T2(n)=n^2+3n+1000 ——>时间复杂度是O(n^2)
T3(n)=n^3 + n^2 +9999999 ——–>时间复杂度是:O(n^3)
在比如:

我们在考虑时间复杂度时多考虑:
一般不考虑最好时间复杂度。
空间复杂度
空间复杂度:衡量空间大小与问题规模的关系。
程序代码所占用的空间是不变的,变得是程序中那些需要额外定义的变量所占用的空间。
遇到普通程序时:
①找到所占空间大小与问题规模相关的变量。
②分析所占空间x与问题规模n的关系 x = f(n)
③x的数量级O(x)就是算法的空间复杂度S(n) —->S指的是space空间的意思

当遇到递归函数时:
①找到递归函数调用的次数x与问题规模n的关系x = f(n)
②x的数量级O(x)就是算法的空间复杂度S(n)

第二章线性表
线性表的定义
线性表是计算机科学中最基本的一种数据结构,它是由n(n≥0)个类型相同的元素组成的有限序列。每个元素都有一个前驱和后继的关系,除了第一个元素没有前驱,最后一个元素没有后继。线性表的特点是每个元素都对应一个非负整数的序号,称为该元素的位置或下标,用以表示其在表中的位置。

线性表的基本操作
创建一个新的空表
向表中添加元素
删除表中的元素
查找特定元素的位置
访问特定位置上的元素
清空整个表
获取表的长度等
顺序表的实现
顺序表是一种常见的线性表实现方式,它是通过一组地址连续的存储单元来存储数据元素的数据结构。在顺序表中,逻辑上相邻的元素,在物理存储位置上也是相邻的。顺序表通常使用数组来实现,因此也可以认为顺序表是数组的一种应用形式。
下面是使用JS代码来实现的顺序表的基本操作。
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
| class SequentialList { constructor(capacity = 10) { this. array = new Array(capacity).fill(null); this.size = 0; }
is_empty() { return this.size === 0; }
is_full() { return this.size === this.array.length; }
append(item) { if (this.is_full()) { console.log("顺序表已满,无法添加更多元素"); return; } this.array[this.size] = item; this.size += 1; }
insert(index, item) { if (index < 0 || index > this.size) { console.log("插入位置无效"); return; } if (this.is_full()) { console.log("顺序表已满,无法插入元素"); return; } for (let i = this.size - 1; i >= index; i--) { this.array[i + 1] = this.array[i]; } this.array[index] = item; this.size += 1; }
remove(index) { if (index < 0 || index >= this.size) { console.log("删除位置无效"); return; } for (let i = index; i < this.size - 1; i++) { this.array[i] = this.array[i + 1]; } this.array[this.size - 1] = null; this.size -= 1; }
display() { console.log("顺序表内容为: " + this.array.slice(0, this.size)); } }
const seq_list = new SequentialList(5); seq_list.append(1); seq_list.append(2); seq_list.append(3); seq_list.display();
seq_list.insert(1, 10); seq_list.display();
seq_list.remove(2); seq_list.display();
|
链表的实现
单链表
定义:单链表是一种数据结构,它由一系列节点组成,每个节点包含两部分:一部分是存储的数据元素,另一部分是一个指向列表中下一个节点的引用(或指针)。单链表中的最后一个节点的下一个节点引用通常为 null,表示链表的结束。单链表的特点是只向一个方向链接,即只能从一个节点访问到其后继节点,而不能反过来从前一个节点访问到后一个节点。
通常我们使用带头结点的单链表 - 如下。
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
| public class Node { int value; Node next;
public Node(int value) { this.value = value; this.next = null; } }
public class LinkedList { private Node head;
public LinkedList() { head = null; }
public void append(int value) { Node newNode = new Node(value);
if (head == null) { head = newNode; } else { Node last = head; while (last.next != null) { last = last.next; } last.next = newNode; } }
public void prepend(int value) { Node newNode = new Node(value); newNode.next = head; head = newNode; }
public void delete(int value) { Node current = head; Node previous = null;
while (current != null) { if (current.value == value) { if (previous == null) { head = current.next; } else { previous.next = current.next; } return; } previous = current; current = current.next; } }
public void printList() { Node current = head; while (current != null) { System.out.print(current.value + " "); current = current.next; } System.out.println(); }
public static void main(String[] args) { LinkedList list = new LinkedList(); list.append(1); list.append(2); list.prepend(0); list.printList(); list.delete(1); list.printList(); } }
|
单链表的插入
实现后插操作:按位序插入
图示:

如何实现给定的节点p进行前插操作?
①手动传入头指针,在循环查找p的前驱节点q,再q之后插入新节点
②在p节点之后插入一个节点s,之后s节点中的数据和p节点进行交换操作。
单链表的删除
实现删除节点:按位序删除
首先要明白,删除一个节点要修改该节点前一个节点的next指针。

如何实现给定的节点p进行删除操作?
首先要明白,删除一个节点要修改该节点前一个节点的next指针。
①传入头指针,循环寻找 p 的前驱结点
②建一个新的指针q指向p节点的后继节点,再把后继节点的数据域和p节点的数据域进行交换,最后另节点p的next指针指向q的next即可。

单链表的查找
按位查找

按值查找

单链表的建立
- 尾插法:适用于需要在链表末尾添加元素的情况。每次插入都需要遍历整个链表。
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
| public class Node { int value; Node next;
public Node(int value) { this.value = value; this.next = null; } }
public class LinkedList { private Node head;
public LinkedList() { head = null; }
public void append(int value) { Node newNode = new Node(value);
if (head == null) { head = newNode; } else { Node current = head; while (current.next != null) { current = current.next; } current.next = newNode; } }
public void printList() { Node current = head; while (current != null) { System.out.print(current.value + " "); current = current.next; } System.out.println(); }
public static void main(String[] args) { LinkedList list = new LinkedList(); list.append(1); list.append(2); list.printList(); } }
|
- 头插法:适用于需要快速在链表头部添加元素的情况。插入操作非常快,因为不需要遍历链表。
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
| public class LinkedList { private Node head;
public LinkedList() { head = null; }
public void prepend(int value) { Node newNode = new Node(value); newNode.next = head; head = newNode; }
public void printList() { Node current = head; while (current != null) { System.out.print(current.value + " "); current = current.next; } System.out.println(); }
public static void main(String[] args) { LinkedList list = new LinkedList(); list.prepend(2); list.prepend(1); list.printList(); } }
|
双链表
双链表(Doubly Linked List)是一种更加灵活的数据结构,它在单链表的基础上增加了对前一个节点的引用。这意味着,在双链表中,每个节点不仅包含一个指向下一个节点的指针,还有一个指向前一个节点的指针。这样的设计使得双链表支持双向遍历,并且在插入和删除节点时更为方便。
以下是一份关于双链表的学习笔记,包括基本概念、优缺点以及实现示例。
双链表的基本概念
- 节点结构:每个节点包含三个部分:存储的数据元素、指向前一个节点的指针(prev)和指向下一个节点的指针(next)。
- 头节点(Head):链表的第一个节点。
- 尾节点(Tail):链表的最后一个节点。
双链表的优缺点
优点:
- 双向遍历:可以从前向后或从后向前遍历链表。
- 插入和删除操作:相比于单链表,在插入和删除节点时,双链表只需要修改前后的指针,不需要额外的遍历来查找前一个节点。
缺点:
- 空间开销较大:每个节点需要额外的指针来存储前一个节点的信息。
- 实现相对复杂:相对于单链表,双链表的实现需要处理更多的指针操作。
双链表的基本操作
- 插入节点:在指定位置插入新节点。
- 删除节点:删除指定位置的节点。
- 遍历链表:从前向后或从后向前遍历链表。
- 反转链表:将链表的方向反转。
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
| public class DoublyLinkedList { private DoublyNode head;
private DoublyNode tail;
public DoublyLinkedList() { head = null; tail = null; }
public void append(int value) { DoublyNode newNode = new DoublyNode(value);
if (head == null) { head = newNode; tail = newNode; } else { tail.next = newNode; newNode.prev = tail; tail = newNode; } }
public void prepend(int value) { DoublyNode newNode = new DoublyNode(value);
if (head == null) { head = newNode; tail = newNode; } else { newNode.next = head; head.prev = newNode; head = newNode; } }
public void delete(int value) { DoublyNode current = head;
while (current != null) { if (current.value == value) { if (current.prev != null) { current.prev.next = current.next; } else { head = current.next; }
if (current.next != null) { current.next.prev = current.prev; } else { tail = current.prev; } return; } current = current.next; } }
public void printList() { DoublyNode current = head; while (current != null) { System.out.print(current.value + " "); current = current.next; } System.out.println(); }
public static void main(String[] args) { DoublyLinkedList list = new DoublyLinkedList(); list.append(1); list.append(2); list.prepend(0); list.printList(); list.delete(1); list.printList(); } }
|
双链表的其他应用场景
双链表还可以用于实现更高级的数据结构,如双向队列(Deque)、LRU 缓存等。由于双链表支持双向遍历和快速插入删除,因此在实际应用中非常有用。
希望这份学习笔记对你理解和掌握双链表有所帮助!如果有任何进一步的问题,请随时提问。
循环链表
把最后一个节点的next指针指向头结点即可。
循环链表(Circular Linked List)是一种特殊的链表结构,其中最后一个节点的下一个节点指针指向链表的第一个节点(头节点),形成了一个闭环。这种结构使得从任何一个节点出发都可以遍历整个链表。
循环链表的一个主要特点是,没有明确的“尾节点”,因为最后一个节点的 next 指针指向的是头节点。此外,循环链表也没有明确的“末尾”,因为链表形成了一个环形结构。
循环链表的基本操作
- 插入节点:可以在链表的任意位置插入节点。
- 删除节点:可以从链表中删除指定位置的节点。
- 遍历链表:可以从任意节点开始遍历整个链表。
循环链表的实现
下面是一个简单的 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
| public class Node { int value; Node next;
public Node(int value) { this.value = value; this.next = null; } }
public class CircularLinkedList { private Node head;
public CircularLinkedList() { head = null; }
public void append(int value) { Node newNode = new Node(value);
if (head == null) { head = newNode; head.next = head; } else { Node last = head; while (last.next != head) { last = last.next; } last.next = newNode; newNode.next = head; } }
public void prepend(int value) { Node newNode = new Node(value);
if (head == null) { head = newNode; head.next = head; } else { newNode.next = head; Node last = head; while (last.next != head) { last = last.next; } last.next = newNode; head = newNode; } }
public void delete(int value) { if (head == null) { return; }
Node current = head; Node prev = null;
do { if (current.value == value) { if (prev == null) { head = current.next; } else { prev.next = current.next; } break; } prev = current; current = current.next; } while (current != head); }
public void printList() { if (head == null) { System.out.println("链表为空"); return; }
Node current = head; do { System.out.print(current.value + " "); current = current.next; } while (current != head); System.out.println(); }
public static void main(String[] args) { CircularLinkedList list = new CircularLinkedList(); list.append(1); list.append(2); list.prepend(0); list.printList(); list.delete(1); list.printList(); } }
|
代码解析
**节点类 Node**:
- 包含一个整数值
value 和一个指向下一个节点的引用 next。
**循环链表类 CircularLinkedList**:
- 包含一个头节点引用
head。
- 构造函数初始化
head 为 null。
append 方法用于在链表末尾添加一个新节点。
prepend 方法用于在链表头部添加一个新节点。
delete 方法用于删除链表中的某个节点。
printList 方法用于打印链表中的所有节点。
特点
- 闭环结构:最后一个节点的
next 指向头节点。
- 遍历:从任意节点开始,通过
next 指针可以遍历整个链表,直到回到起点。
- 删除操作:需要注意处理头节点的特殊情况。

静态链表
静态链表(Static Linked List)是一种在编译时就确定了大小和结构的链表实现方式。它利用数组来存储链表的节点,并使用索引来模拟链表中的指针关系。这种方式结合了数组的固定大小特性和链表的动态访问特性,可以在某些情况下提供更好的性能和管理。
静态链表的基本概念
在静态链表中,每个数组元素代表一个节点,数组元素中包含数据和一个指向“下一个”节点的索引(即“指针”)。这种实现方式允许在不改变数组大小的情况下,通过索引来模拟链表的插入和删除操作。
图示:
第三章栈、队列和数组
栈
基本概念
定义:
- 栈是一种线性数据结构,是一种线性表,它遵循特定的访问模式:后进先出(LIFO)。这意味着最后进入栈的元素将是最先被移除的。
用途:
- 栈用于解决很多问题,如括号匹配、后缀表达式计算(逆波兰表达式)、撤销功能实现等。
操作:栈的操作全部在栈顶进行。这是由于它的访问模型决定的
栈的操作
压栈(Push):
- 向栈中添加元素。
- 如果栈满了,则不能再进行压栈操作,此时会发生“栈溢出”。
弹栈(Pop):
- 移除栈顶元素。
- 如果栈是空的,则不能进行弹栈操作,此时会发生“栈下溢”。
查看栈顶(Peek/Top):
清空栈(Clear):
检查是否为空(IsEmpty):
获取栈大小(Size):
实现方式
- **数组实现(顺序存储方式)**:
- 使用固定大小的数组来实现栈,需要管理栈顶指针。
- 缺点是当栈满时需要处理栈溢出的情况。
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
| public class ArrayStack { private int maxSize; private int top; private int[] stackArray;
public ArrayStack(int size) { maxSize = size; stackArray = new int[maxSize]; top = -1; }
public void push(int value) { if (top < maxSize - 1) { stackArray[++top] = value; } else { System.out.println("Stack is full. Cannot push " + value); } }
public int pop() { if (top > -1) { return stackArray[top--]; } else { System.out.println("Stack is empty."); return -1; } }
public int peek() { if (top > -1) { return stackArray[top]; } else { System.out.println("Stack is empty."); return -1; } }
public boolean isEmpty() { return (top == -1); }
public int size() { return top + 1; }
public static void main(String[] args) { ArrayStack stack = new ArrayStack(5);
stack.push(10); stack.push(20); stack.push(30); stack.push(40); stack.push(50);
stack.push(60);
System.out.println(stack.pop()); System.out.println(stack.pop());
System.out.println(stack.peek());
System.out.println(stack.isEmpty());
System.out.println(stack.size()); } }
|
- 链表实现(链式存储方式):
- 使用链表来实现栈,可以动态调整栈的大小。
- 需要维护指向栈顶节点的指针。
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
| class Node { int data; Node next;
public Node(int data) { this.data = data; this.next = null; } }
public class LinkedListStack { private Node top;
public LinkedListStack() { top = null; }
public void push(int data) { Node newNode = new Node(data); newNode.next = top; top = newNode; }
public int pop() { if (top == null) { System.out.println("Stack is empty. Cannot perform pop."); return Integer.MIN_VALUE; } int poppedData = top.data; top = top.next; return poppedData; }
public int peek() { if (top == null) { System.out.println("Stack is empty."); return Integer.MIN_VALUE; } return top.data; }
public boolean isEmpty() { return top == null; }
public int size() { int count = 0; Node current = top; while (current != null) { count++; current = current.next; } return count; }
public static void main(String[] args) { LinkedListStack stack = new LinkedListStack(); stack.push(10); stack.push(20); stack.push(30); stack.push(40); stack.push(50); System.out.println("Popped: " + stack.pop()); System.out.println("Popped: " + stack.pop()); System.out.println("Peek: " + stack.peek()); System.out.println("Empty: " + stack.isEmpty()); System.out.println("Size: " + stack.size()); } }
|
应用实例
括号匹配:
- 利用栈可以检查字符串中的括号是否正确配对。
- 左括号入栈,扫描到右括号时,就弹出一个左括号与其匹配
- 匹配失败情况:左括号未匹配到,右括号未匹配到,左右括号未匹配到
表达式求值:
- 表达式分类:
- 前缀表达式:运算符位于两个操作数之间。
- 中缀表达式:所有运算符都放在它们的操作数之前。
- 后缀表达式:所有运算符都放在它们的操作数之后。
- 中缀转后缀:1.确定运算顺序 2.找到下一个运算符按照[左操作数 右操作数 运算符] 组合成一个新的操作数。
- 中缀转前缀:
注意事项
性能考量:
- 栈的插入和删除操作通常非常快,因为它们只涉及修改几个指针。
内存管理:
- 动态分配内存的栈需要注意释放不再使用的空间,以防止内存泄漏。
异常处理:
队列
队列是只允许在一端进行插入,在另一端删除的线性表
当然可以帮助你整理关于队列的数据结构的学习笔记。下面是一个简洁的总结,包括队列的基本概念、类型以及一些常见的操作等:
队列定义
- 定义:队列是一种特殊的线性表,它只允许在表的一端进行插入操作,在另一端进行删除操作。允许插入的一端称为队尾(rear),允许删除的一端称为队头(front)。
- 特性:遵循先进先出(FIFO, First In First Out)原则,即最先加入队列的元素总是最先被移除。
常见操作
- 入队(Enqueue):在队尾添加一个新元素。
- 出队(Dequeue):从队头移除一个元素。
- 查看队头(Front):返回队头元素,但不移除。
- 查看队尾(Rear):返回队尾元素,但不移除。
- 是否为空(IsEmpty):检查队列是否为空。
- 队列大小(Size):返回队列中元素的数量。
常见操作的实现
队列的顺序存储实现
让我们来看一下如何使用顺序存储(即数组)来实现队列的基本操作。顺序存储的队列通常被称为数组队列或循环队列。下面是一个基于数组实现的顺序队列的基本操作的 Java 代码示例。
队列中元素个数的计算(取余数):(rear + capacity - front) % capacity
!!!!!!!当我们的队列的队尾指针在队尾元素的下一个位置时:
下面是几种判断队列空满的方法:
方法①:
队空的条件是:队列中队头索引和队尾索引相等。
队满条件是:队尾指针的在下一个位置是队头指针
方法②:
引入变量size:队列中当前元素的个数。初始化为0
添加一个元素size++,删除一个元素size–
队空条件:size == 0
队满条件:size == capacity
方法③:
引入标志变量比如tag,tag的值只有0/1。值为0时进行了删除操作,值为1时进行了添加操作。
一次操作中,进行删除操作可能让队列为空
一次操作中,进行添加操作可能让队列为满
队空条件:队头指针等于队尾指针且tag的值等于0
队满条件:队头指针等于队尾指针且tag的值等于1
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
| public class ArrayQueue<T> { private T[] elements; private int front; private int rear; private final int capacity;
@SuppressWarnings("unchecked") public ArrayQueue(int capacity) { this.capacity = capacity; elements = (T[]) new Object[capacity]; front = 0; rear = 0; }
public void enqueue(T element) { if ((rear + 1) % capacity == front) { throw new IllegalStateException("Queue is full."); } elements[rear] = element; rear = (rear + 1) % capacity; }
public T dequeue() { if (isEmpty()) { throw new NoSuchElementException("Queue is empty."); } T element = elements[front]; elements[front] = null; front = (front + 1) % capacity; return element; }
public T front() { if (isEmpty()) { throw new NoSuchElementException("Queue is empty."); } return elements[front]; }
public T rearElement() { if (isEmpty()) { throw new NoSuchElementException("Queue is empty."); } return elements[(rear - 1 + capacity) % capacity]; }
public boolean isEmpty() { return front == rear; }
public int size() { if (front <= rear) { return rear - front; } else { return capacity - (front - rear); } }
public static void main(String[] args) { ArrayQueue<Integer> queue = new ArrayQueue<>(5);
queue.enqueue(1); queue.enqueue(2); queue.enqueue(3);
System.out.println("Front element: " + queue.front());
System.out.println("Rear element: " + queue.rearElement());
System.out.println("Dequeued element: " + queue.dequeue());
System.out.println("Queue size: " + queue.size()); } }
|
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
| public class ArrayQueue<T> { private T[] elements; private int front; private int rear; private int size; private final int capacity;
@SuppressWarnings("unchecked") public ArrayQueue(int capacity) { this.capacity = capacity; elements = (T[]) new Object[capacity]; front = 0; rear = -1; size = 0; }
public void enqueue(T element) { if (size == capacity) { throw new IllegalStateException("Queue is full."); } if (rear == capacity - 1) { rear = 0; } else { rear++; } elements[rear] = element; if (front == -1) { front = 0; } size++; }
public T dequeue() { if (isEmpty()) { throw new NoSuchElementException("Queue is empty."); } T element = elements[front]; elements[front] = null; if (front == rear) { front = 0; rear = -1; } else if (front == capacity - 1) { front = 0; } else { front++; } size--; return element; }
public T front() { if (isEmpty()) { throw new NoSuchElementException("Queue is empty."); } return elements[front]; }
public T rear() { if (isEmpty()) { throw new NoSuchElementException("Queue is empty."); } return elements[rear]; }
public boolean isEmpty() { return size == 0; }
public int size() { return size; }
public static void main(String[] args) { ArrayQueue<Integer> queue = new ArrayQueue<>(5);
queue.enqueue(1); queue.enqueue(2); queue.enqueue(3);
System.out.println("Front element: " + queue.front());
System.out.println("Rear element: " + queue.rear());
System.out.println("Dequeued element: " + queue.dequeue());
System.out.println("Queue size: " + queue.size()); } }
|
队列的链式存储实现
图示:有带头节点和不带头节点两种情况

带头节点情况
初始化:

队尾入队

出队操作:

3.
不带头节点情况
初始化:

队尾入队:

队头出队

数组
数组理论基础

结论: