一、数据结构
- 数组(Array)
- 定义:数组是一种最简单、最基本的数据结构,它是由相同类型的元素组成的有序集合。这些元素在内存中是连续存储的,通过索引(从 0 开始)来访问每个元素。例如,在一个整数数组
int[] arr = {1, 2, 3, 4, 5};
中,arr[0]
表示第一个元素 1,arr[1]
表示第二个元素 2,以此类推。 - 操作:
- 访问元素:通过索引访问元素的时间复杂度为,这是数组的一个重要优势。因为数组在内存中的存储是连续的,计算机可以根据起始地址和索引直接计算出元素的内存地址。例如,对于一个长度为 n 的数组,要访问第 i 个元素,计算其内存地址的公式为
起始地址 + i * 每个元素占用的字节数
。 - 插入元素:在数组中间插入元素比较复杂。如果要在数组的第 i 个位置插入一个元素,需要将第 i 个位置及以后的元素向后移动一位,为新元素腾出空间。平均时间复杂度为,因为在最坏情况下,需要移动数组中一半的元素。例如,在一个已经有 10 个元素的数组中插入一个新元素到第 3 个位置,需要将第 3 个位置及以后的 9 个元素都向后移动一位。
- 删除元素:类似地,从数组中间删除一个元素也需要移动元素。如果要删除第 i 个元素,需要将第 i + 1 个位置及以后的元素向前移动一位。平均时间复杂度也为。例如,从一个有 8 个元素的数组中删除第 4 个元素,需要将第 5 个元素及以后的 4 个元素向前移动一位。
- 应用场景:
- 数组适用于存储和访问一组固定大小、类型相同的数据。在很多编程语言中,数组是最基本的数据类型,用于实现其他更复杂的数据结构。例如,在实现栈和队列时,可以使用数组作为底层存储结构。在存储和处理图像像素数据、矩阵运算等场景中,数组也发挥着重要作用。
- 链表(Linked List)
- 定义:链表是一种线性的数据结构,由一系列节点组成。每个节点包含数据部分和指向下一个节点的指针(在单链表中)。例如,一个简单的单链表节点结构可以定义为:
class ListNode {
int val;
ListNode next;
ListNode(int val) {
this.val = val;
this.next = null;
}
}
- 操作:
- 访问元素:要访问链表中的某个元素,需要从链表头开始逐个遍历节点。如果要访问第 n 个元素,在最坏情况下需要遍历 n 个节点,时间复杂度为。例如,在一个有 10 个节点的链表中查找第 7 个节点,需要从第一个节点开始,依次经过 6 个节点才能找到。
- 插入元素:在链表中插入元素相对灵活。如果要在节点 p 之后插入一个新节点 q,只需要将 q 的 next 指针指向 p 的 next,然后将 p 的 next 指针指向 q,时间复杂度为。例如,在一个链表中,要在节点值为 3 的节点之后插入一个新节点,只需要修改两个指针即可。
- 删除元素:删除链表中的一个元素也比较简单。如果要删除节点 p 之后的节点 q,只需要将 p 的 next 指针指向 q 的 next 即可,时间复杂度为。例如,在一个链表中删除某个指定值的节点,首先找到该节点的前驱节点,然后按照上述方法进行删除。
- 应用场景:
- 链表适用于需要频繁插入和删除元素的场景。例如,在实现一个动态的数据集合,如操作系统中的进程调度队列,链表可以方便地添加和移除进程节点。在多项式运算、图形处理等领域,链表也有广泛的应用。
- 栈(Stack)
- 定义:栈是一种特殊的线性数据结构,它遵循后进先出(LIFO - Last In First Out)的原则。可以把栈想象成一个只有一端开口的容器,元素只能从这个开口进出。例如,在一个栈中,先放入元素 A,再放入元素 B,那么取出元素时,先取出的是 B,然后是 A。
- 操作:
- 入栈(Push):将一个元素添加到栈顶,时间复杂度为。例如,在一个栈中,使用
push
操作将新元素压入栈顶,就像在一摞盘子上再放一个盘子一样。 - 出栈(Pop):从栈顶取出一个元素,时间复杂度为。例如,在栈顶元素被取出后,栈的高度就会减少一个单位。
- 查看栈顶元素(Peek):获取栈顶元素的值,但不将其从栈中取出,时间复杂度为。
- 应用场景:
- 栈在表达式求值、函数调用(用于保存函数调用的上下文信息,如局部变量、返回地址等)、括号匹配等场景中有广泛应用。例如,在计算一个包含括号的算术表达式时,栈可以用来检查括号是否匹配。
- 队列(Queue)
- 定义:队列是另一种特殊的线性数据结构,它遵循先进先出(FIFO - First In First Out)的原则。可以把队列想象成一个排队的队伍,先进入队列的元素先离开。例如,在一个超市的收银队列中,先排队的顾客先结账。
- 操作:
- 入队(Enqueue):将一个元素添加到队尾,时间复杂度为。例如,在一个队列中,新的顾客在队尾排队等待服务。
- 出队(Dequeue):从队头取出一个元素,时间复杂度为。例如,在队头的顾客完成服务后离开队列。
- 查看队头元素(Peek):获取队头元素的值,但不将其从队列中取出,时间复杂度为。
- 应用场景:
- 队列在广度优先搜索(BFS)算法、任务调度、消息队列等场景中有广泛应用。例如,在计算机网络中,消息队列可以用来暂存等待处理的消息,按照先入先出的顺序进行处理。
- 树(Tree)
- 定义:树是一种非线性的数据结构,它由节点(Node)和边(Edge)组成。树有一个根节点(Root),每个节点可以有零个或多个子节点。例如,二叉树是一种特殊的树,每个节点最多有两个子节点。以下是一个简单二叉树节点的定义:
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int val) {
this.val = val;
this.left = null;
this.right = null;
}
}
- 操作:
- 遍历(Traversal):
- 前序遍历(Pre - order Traversal):先访问根节点,然后递归地访问左子树和右子树。例如,对于二叉树,前序遍历的顺序是根 - 左 - 右。这种遍历方式在复制树结构、表达式求值等场景中有应用。
- 中序遍历(In - order Traversal):先访问左子树,然后访问根节点,最后访问右子树。对于二叉树,中序遍历的顺序是左 - 根 - 右。在对二叉搜索树(BST - Binary Search Tree)进行中序遍历时,可以得到一个有序的节点序列,这在排序和查找等场景中有应用。
- 后序遍历(Post - order Traversal):先访问左子树,然后访问右子树,最后访问根节点。对于二叉树,后序遍历的顺序是左 - 右 - 根。后序遍历在计算树的高度、释放树节点内存等场景中有应用。
- 插入节点(Insertion):在二叉搜索树中插入一个节点,需要根据节点的值与当前节点的值比较,决定向左子树还是右子树插入。时间复杂度平均为,在最坏情况下(树退化为链表)为。例如,在一个二叉搜索树中插入一个新节点,需要从根节点开始比较,直到找到合适的插入位置。
- 删除节点(Deletion):在二叉搜索树中删除一个节点比较复杂。如果要删除的节点是叶子节点,直接删除即可;如果是有一个子节点的节点,将其子节点替换它;如果是有两个子节点的节点,通常用其右子树中的最小节点(或左子树中的最大节点)来替换它。时间复杂度平均为,最坏情况为。
- 应用场景:
- 树在文件系统、数据库索引(如 B + Tree)、语法树(在编译器设计中)、决策树(在机器学习和数据挖掘中)等领域有广泛应用。例如,在数据库索引中,B + Tree 结构可以高效地存储和检索数据,减少磁盘 I/O 操作。
- 图(Graph)
- 定义:图是一种更为复杂的非线性数据结构,它由顶点(Vertex)和边(Edge)组成。边可以表示顶点之间的关系,边可以是有向的(Directed)或无向的(Undirected)。例如,在一个社交网络中,人可以看作顶点,人与人之间的朋友关系可以看作边。
- 操作:
- 遍历(Traversal):
- 深度优先搜索(DFS - Depth - First Search):从一个起始顶点开始,沿着一条路径尽可能深地探索,直到不能再前进,然后回溯到上一个未完全探索的顶点,继续探索其他路径。例如,在一个迷宫中,使用深度优先搜索可以找到从入口到出口的一条路径。
- 广度优先搜索(BFS - Breadth - First Search):从一个起始顶点开始,先访问它的所有邻居顶点,然后再访问邻居顶点的邻居顶点,以此类推。例如,在一个社交网络中,使用广度优先搜索可以找到与一个人距离为 1、2、3 等的朋友。
- 最短路径问题(Shortest Path Problem):在有向图或无向图中,寻找两个顶点之间的最短路径。例如,在地图导航中,寻找两个地点之间的最短行驶路线。解决最短路径问题的算法有迪杰斯特拉算法(Dijkstra's Algorithm)、弗洛伊德算法(Floyd's Algorithm)等。
- 应用场景:
- 图在社交网络分析、交通网络规划、电路设计、计算机网络拓扑等众多领域有广泛应用。例如,在互联网的路由算法中,图可以用来表示网络节点之间的连接关系,通过最短路径算法来优化数据传输路径。
二、算法
- 排序算法
- 冒泡排序(Bubble Sort):
- 原理:它是一种简单的排序算法,通过反复比较相邻的元素并交换位置,将最大(或最小)的元素逐步 “冒泡” 到数组的一端。例如,对于一个整数数组
int[] arr = {5, 4, 3, 2, 1}
,第一轮比较会将 5 交换到最后,第二轮将 4 交换到倒数第二的位置,以此类推。 - 时间复杂度:最坏情况和平均情况都是,最好情况(数组已经有序)是。这是因为在最坏情况下,每一对相邻元素都需要交换,总共需要进行次比较。
- 空间复杂度:,因为它只需要几个额外的变量来进行元素交换,不需要额外的存储空间来存储数据。
- 插入排序(Insertion Sort):
- 原理:将一个元素插入到已经有序的子数组中的合适位置。例如,对于数组
int[] arr = {3, 1, 4, 2, 5}
,开始时认为第一个元素 3 是有序的,然后将 1 插入到合适位置,得到{1, 3, 4, 2, 5}
,接着将 4 插入,以此类推。 - 时间复杂度:最坏情况是,平均情况也是,最好情况(数组已经有序)是。这是因为在最坏情况下,每次插入一个元素都需要移动前面的所有元素。
- 空间复杂度:,和冒泡排序一样,它只需要少量的额外变量。
- 选择排序(Selection Sort):
- 原理:每次从未排序的元素中选择最小(或最大)的元素,将其与未排序部分的第一个元素交换。例如,对于数组
int[] arr = {4, 3, 2, 1, 5}
,第一轮会选择 1 并与 4 交换,得到{1, 3, 2, 4, 5}
,然后在剩余的{3, 2, 4, 5}
中选择 2 交换,以此类推。 - 时间复杂度:无论最好、最坏还是平均情况,都是。因为每次选择最小元素都需要遍历未排序部分的所有元素。
- 空间复杂度:。
- 快速排序(Quick Sort):
- 原理:通过选择一个基准元素(Pivot),将数组分为两部分,左边部分的元素都小于等于基准元素,右边部分的元素都大于等于基准元素。然后对左右两部分递归地进行排序。例如,对于数组
int[] arr = {5, 2, 7, 1, 9}
,选择 5 作为基准元素,经过一次划分后得到{1, 2} 5 {7, 9}
,然后对{1, 2}
和{7, 9}
分别进行排序。 - 时间复杂度:平均情况是,最坏情况(每次选择的基准元素都是最大或最小元素)是。在平均情况下,每次划分可以将数组分为大致相等的两部分,递归的深度为,每层需要的时间来划分。
- 空间复杂度:最坏情况是,平均情况是。这是因为在最坏情况下,需要个栈空间来存储递归调用的信息,而在平均情况下,递归深度为。
- 归并排序(Merge Sort):
- 原理:将数组分为两部分,对每一部分递归地进行排序,然后将排序好的两部分合并。例如,对于数组
int[] arr = {3, 1, 4, 2}
,先分为{3, 1}
和{4, 2}
,分别排序得到{1, 3}
和{2, 4}
,然后合并得到{1, 2, 3, 4}
。 - 时间复杂度:无论最好、最坏还是平均情况,都是。因为每次划分需要的时间,总共需要划分次,每次合并需要的时间。
- 空间复杂度:,因为在合并过程中需要一个临时数组来存储合并后的元素。
- 搜索算法
- 线性搜索(Linear Search):
- 原理:从数组的第一个元素开始,逐个比较元素与目标元素,直到找到目标元素或者遍历完整个数组。例如,在数组
int[] arr = {1, 3, 5, 7, 9}
中寻找 7,需要从第一个元素开始比较,直到找到 7 或者遍历完所有元素。 - 时间复杂度:最好情况是(目标元素是第一个元素),最坏情况是(目标元素是最后一个元素或者不存在),平均情况是。
- 空间复杂度:,因为只需要几个额外的变量来进行比较。
- 二分搜索(Binary Search):
- 原理:适用于已排序的数组。每次比较中间元素与目标元素,如果目标元素小于中间元素,就在左半部分继续搜索;如果大于中间元素,就在右半部分继续搜索;如果等于中间元素,就找到了目标元素。例如,在已排序数组
int[] arr = {1, 3, 5, 7, 9}
中寻找 7,首先比较中间元素 5,因为 7 大于 5,所以在右半部分{7, 9}
中继续搜索。 - 时间复杂度:最好情况是(第一次比较就找到目标元素),最坏和平均情况都是。因为每次比较都可以将搜索范围缩小一半。
- 空间复杂度:。
- 图算法
- 迪杰斯特拉算法(Dijkstra's Algorithm):
- 原理:用于在带权重的有向图(也可以用于无向图)中寻找一个顶点到其他顶点的最短路径。它维护一个顶点集合,每次选择距离起始顶点最近的未包含在集合中的顶点,将其加入集合,并更新与它相邻顶点的距离。例如,在一个城市交通图中,每个城市是一个顶点,城市之间的道路长度是权重,迪杰斯特拉算法可以用来计算