常见的数据结构主要包括以下几种:
一、数组(Array)
-
定义
- 数组是一种线性的数据结构,它是由相同类型的元素(如整数、浮点数、字符等)组成的有序集合。这些元素在内存中是连续存储的,通过索引来访问其中的元素。索引从 0 开始,例如,对于一个包含 n 个元素的数组,其索引范围是 0 到 n - 1。
-
示例
- 例如,定义一个整数数组
int[] numbers = {1, 2, 3, 4, 5};,可以通过numbers[0]访问第一个元素 1,numbers[2]访问第三个元素 3 等。
- 例如,定义一个整数数组
-
优点
- 访问元素速度快。因为数组的元素在内存中是连续存储的,通过计算偏移量可以快速定位到任何一个元素。例如,在一个长度为 n 的数组中,访问第 i 个元素的时间复杂度是 O (1)。
- 简单直观,易于理解和使用。
-
缺点
-
插入和删除元素效率低。在数组中间插入或删除一个元素,需要移动后面的元素,时间复杂度为 O (n)。例如,在一个已经排好序的数组中间插入一个新元素,需要将插入位置后面的所有元素向后移动一位。
-
大小固定。在创建数组时,需要指定数组的大小,一旦创建,其大小通常很难改变。如果要扩展数组,可能需要创建一个新的更大的数组,并将原数组的元素复制到新数组中。
-
二、链表(Linked List)
-
定义
- 链表是一种线性的数据结构,由一系列节点组成。每个节点包含数据部分和指向下一个节点的指针(在单链表中)。如果是双向链表,节点还包含指向前一个节点的指针。链表的节点在内存中不是连续存储的,而是通过指针链接在一起。
-
示例
- 单链表节点的结构可以用如下代码表示(以 C++ 为例):
struct ListNode {
int val;
ListNode* next;
ListNode(int x) : val(x), next(NULL) {}
};
-
可以通过
head指针来访问链表的头节点,然后沿着next指针遍历整个链表。
-
优点
- 插入和删除操作高效。在链表中插入或删除一个节点,只需要修改相关节点的指针,时间复杂度为 O (1)(如果已经知道要插入或删除的位置)。例如,在链表中间插入一个新节点,只需要将新节点插入到合适的位置,更新前后节点的指针即可。
- 可以动态地分配内存。链表的长度可以根据需要动态地增加或减少,不需要预先指定大小。
-
缺点
-
访问元素效率低。因为链表的节点在内存中不是连续存储的,要访问链表中的某个元素,需要从链表头开始遍历,时间复杂度为 O (n)。例如,要访问链表中的第 n 个节点,可能需要遍历 n 个节点才能找到。
-
三、栈(Stack)
-
定义
- 栈是一种特殊的线性数据结构,它遵循后进先出(LIFO - Last In First Out)的原则。可以把栈想象成一个只有一个开口的容器,元素只能从这个开口进出。进入栈的操作称为 “入栈”(push),从栈中取出元素的操作称为 “出栈”(pop)。
-
示例
-
以整数栈为例,在程序中可以用数组或链表来实现栈。如果用数组实现一个简单的栈(以 Python 为例):
-
class Stack:
def __init__(self):
self.stack = []
def push(self, item):
self.stack.append(item)
def pop(self):
if not self.is_empty():
return self.stack.pop()
def is_empty(self):
return len(self.stack) == 0
-
可以使用
push操作将元素压入栈,pop操作将栈顶元素弹出。
-
优点
- 简单易用,对于实现一些具有后进先出特性的功能非常方便。例如,函数调用栈,当一个函数调用另一个函数时,当前函数的状态信息(如局部变量、返回地址等)被压入栈中,当被调用函数返回时,这些信息从栈中弹出,恢复之前函数的执行。
- 栈的操作(入栈和出栈)时间复杂度为 O (1),因为只涉及到对栈顶元素的操作。
-
缺点
-
栈的容量有限(如果是用数组实现的栈)。如果栈满了,就无法再进行入栈操作,除非扩展栈的大小。
-
只能访问栈顶元素。要访问栈中的其他元素,需要先将栈顶元素弹出,这可能会改变栈的状态。
-
四、队列(Queue)
-
定义
- 队列是一种线性数据结构,遵循先进先出(FIFO - First In First Out)的原则。可以把队列想象成一个排队的队伍,元素从队尾进入队列(入队操作,enqueue),从队头离开队列(出队操作,dequeue)。
-
示例
- 以整数队列为例,同样可以用数组或链表来实现队列。用数组实现一个简单的队列(以 Java 为例):
class Queue {
private int[] queue;
private int front;
private int rear;
private int size;
public Queue(int capacity) {
queue = new int[capacity];
front = 0;
rear = -1;
size = 0;
}
public void enqueue(int item) {
if (!isFull()) {
rear = (rear + 1) % queue.length;
queue[rear] = item;
size++;
}
}
public int dequeue() {
if (!isEmpty()) {
int item = queue[front];
front = (front + 1) % queue.length;
size--;
return item;
}
return -1;
}
public boolean isEmpty() {
return size == 0;
}
public boolean isFull() {
return size == queue.length;
}
}
-
这里通过
enqueue操作将元素入队,dequeue操作将队头元素出队。
-
优点
- 对于处理按顺序到达的数据非常有效。例如,在操作系统中,进程调度队列就是按照先进先出的原则,先到达的进程先获得 CPU 资源。
- 入队和出队操作时间复杂度在理想情况下(如用链表实现)为 O (1)。
-
缺点
-
同样存在容量问题(如果是用数组实现的队列)。当队列满时,无法再进行入队操作,除非扩展队列大小。
-
查找特定元素效率低。要在队列中查找一个特定的元素,需要遍历队列中的部分或全部元素,时间复杂度为 O (n)。
-
五、树(Tree)
-
定义
- 树是一种非线性的数据结构,它是由 n(n >= 0)个节点组成的有限集合。当 n = 0 时,称为空树。在非空树中,有一个特殊的节点称为根节点(root),其余节点可以分为 m(m >= 0)个互不相交的有限集合,每个集合本身又是一棵树,称为根节点的子树。
-
示例
- 以二叉树为例,二叉树是一种特殊的树,每个节点最多有两个子节点,分别称为左子节点和右子节点。下面是一个简单二叉树节点的定义(以 Python 为例):
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
-
可以通过根节点来访问整个二叉树,例如,通过递归的方式遍历二叉树的节点。
-
优点
- 树结构可以很好地表示层次关系。例如,文件系统的目录结构就是一个树形结构,根目录是树的根节点,子目录是子节点,文件可以看作是叶子节点。
- 对于一些搜索、排序等操作,树结构可以提供高效的算法。例如,二叉搜索树(BST)可以在平均时间复杂度为 O (log n) 的情况下进行插入、删除和查找操作。
-
缺点
-
实现和理解相对复杂。树的操作(如插入、删除、遍历等)通常需要使用递归等复杂的算法,对于初学者来说可能比较难掌握。
-
树的平衡性问题。在一些树结构(如二叉搜索树)中,如果树不平衡,操作的时间复杂度可能会退化为 O (n)。例如,当二叉搜索树退化成一条链表时,查找操作的效率会变得很低。
-
六、图(Graph)
-
定义
-
图是一种更为复杂的非线性数据结构,由顶点(vertex)和边(edge)组成。顶点表示图中的节点,边表示顶点之间的关系。图可以分为有向图(边有方向)和无向图(边没有方向)。
-
-
优点
- 图可以很好地表示各种复杂的关系。例如,社交网络可以用图来表示,其中人是顶点,人与人之间的朋友关系是边。
- 图算法可以解决很多实际问题,如最短路径问题(如 Dijkstra 算法)、最小生成树问题(如 Prim 算法和 Kruskal 算法)等。
-
缺点
- 图的存储和操作比较复杂。无论是邻接矩阵还是邻接表表示法,在处理大规模图时,都需要占用大量的内存和计算资源。
- 图算法的时间复杂度通常较高。例如,在一个有 n 个顶点的图中,一些算法的时间复杂度可能是 O (n^2) 甚至更高。
七、散列表(哈希表)
根据关键码值(Key)而直接进行访问的数据结构。它通过一个散列函数(Hash Function)将关键码映射到一个特定的位置(槽位,Slot)来存储数据。