简介:本专栏致力于提供最易理解的数据结构与算法知识,帮助学习者掌握计算机科学基础,助力面试成功。专栏内容涵盖:数据结构详解(数组、链表、栈、队列、树、哈希表、图等)、LeetCode题目图解解析、剑指Offer题目深入分析。通过图解、代码示例和实战任务,读者可以全面掌握数据结构与算法的应用,提升编程思维和问题解决能力,为技术面试和实际工作做好充分准备。
1. 数据结构基础
数据结构是计算机科学中组织和存储数据的方法,它决定了数据的表示方式以及对数据的操作方式。数据结构的选择对于程序的性能和效率至关重要。
数据结构可以分为两大类:线性数据结构和非线性数据结构。线性数据结构中的元素按顺序排列,而非线性数据结构中的元素之间具有更复杂的关系。
2. 线性数据结构
线性数据结构是一种数据结构,其中元素按顺序排列,并且只能通过其前一个或后一个元素来访问。线性数据结构的典型示例包括数组、链表、栈和队列。
2.1 数组
2.1.1 数组的基本概念和操作
数组是一种数据结构,它存储固定大小的相同类型元素的集合。数组中的元素按索引访问,索引从 0 开始。数组的基本操作包括:
- 创建数组: 使用以下语法创建数组:
int arr[size];
其中 size
是数组的大小。
- 访问元素: 使用索引访问数组中的元素:
arr[index] = value; // 赋值
cout << arr[index]; // 输出
- 遍历数组: 使用循环遍历数组中的所有元素:
for (int i = 0; i < size; i++) {
cout << arr[i] << " ";
}
2.1.2 数组的动态分配和释放
在 C++ 中,可以使用 new
和 delete
运算符动态分配和释放数组。例如:
int* arr = new int[size]; // 分配数组
delete[] arr; // 释放数组
2.1.3 数组的常见应用场景
数组广泛用于存储大量同类型数据,例如:
- 存储学生成绩
- 存储图像像素
- 存储哈希表中的键
2.2 链表
2.2.1 链表的基本概念和操作
链表是一种线性数据结构,其中元素存储在称为节点的动态分配的结构中。每个节点包含数据和指向下一个节点的指针。链表的基本操作包括:
- 创建链表: 使用以下语法创建链表:
struct Node {
int data;
Node* next;
};
- 插入节点: 在链表中插入节点:
Node* newNode = new Node();
newNode->data = value;
newNode->next = head;
head = newNode;
- 删除节点: 从链表中删除节点:
Node* temp = head;
while (temp->next != nodeToDelete) {
temp = temp->next;
}
temp->next = nodeToDelete->next;
delete nodeToDelete;
- 遍历链表: 使用循环遍历链表中的所有节点:
Node* temp = head;
while (temp != nullptr) {
cout << temp->data << " ";
temp = temp->next;
}
2.2.2 单链表和双链表的实现
单链表只包含指向下一个节点的指针,而双链表还包含指向前一个节点的指针。双链表的实现如下:
struct Node {
int data;
Node* prev;
Node* next;
};
2.2.3 链表的常见应用场景
链表广泛用于存储不规则长度的数据,例如:
- 存储字符串
- 存储图中的顶点
- 存储哈希表中的冲突链表
2.3 栈
2.3.1 栈的基本概念和操作
栈是一种后进先出 (LIFO) 数据结构。栈的基本操作包括:
- 压栈: 将元素压入栈顶:
stack.push(value);
- 弹栈: 从栈顶弹出元素:
stack.pop();
- 取栈顶元素: 获取栈顶元素:
stack.top();
2.3.2 栈的实现和应用场景
栈通常使用数组或链表实现。栈的常见应用场景包括:
- 存储函数调用时的局部变量
- 实现递归算法
- 括号匹配检查
2.4 队列
2.4.1 队列的基本概念和操作
队列是一种先进先出 (FIFO) 数据结构。队列的基本操作包括:
- 入队: 将元素入队到队尾:
queue.push(value);
- 出队: 从队首出队元素:
queue.pop();
- 取队首元素: 获取队首元素:
queue.front();
2.4.2 队列的实现和应用场景
队列通常使用数组或链表实现。队列的常见应用场景包括:
- 存储打印任务
- 实现生产者-消费者模型
- 消息传递系统
3. 非线性数据结构
非线性数据结构是一种数据组织方式,其中数据元素之间的关系不是线性的,而是具有层次结构或网状结构。非线性数据结构具有较强的灵活性,可以高效地处理复杂的数据关系。本章将介绍三种重要的非线性数据结构:树、哈希表和图。
3.1 树
3.1.1 树的基本概念和术语
树是一种层次化的数据结构,它由称为节点的元素组成。每个节点可以包含一个或多个子节点,而根节点是没有父节点的节点。树中的节点之间的关系可以用以下术语来描述:
- 根节点: 树中没有父节点的节点。
- 父节点: 拥有子节点的节点。
- 子节点: 拥有父节点的节点。
- 叶节点: 没有子节点的节点。
- 深度: 从根节点到给定节点的最长路径上的边数。
- 高度: 树中从根节点到最深叶节点的最长路径上的边数。
- 度: 一个节点拥有的子节点的数量。
3.1.2 二叉树和二叉搜索树的实现
二叉树是一种特殊的树,其中每个节点最多有两个子节点。二叉搜索树(BST)是一种特殊的二叉树,其中每个节点的值都大于其左子树中的所有值,而小于其右子树中的所有值。
二叉树的实现:
class Node:
def __init__(self, value):
self.value = value
self.left = None
self.right = None
class BinaryTree:
def __init__(self):
self.root = None
def insert(self, value):
new_node = Node(value)
if self.root is None:
self.root = new_node
else:
self._insert(new_node, self.root)
def _insert(self, new_node, current_node):
if new_node.value < current_node.value:
if current_node.left is None:
current_node.left = new_node
else:
self._insert(new_node, current_node.left)
else:
if current_node.right is None:
current_node.right = new_node
else:
self._insert(new_node, current_node.right)
二叉搜索树的实现:
class BinarySearchTree(BinaryTree):
def insert(self, value):
new_node = Node(value)
if self.root is None:
self.root = new_node
else:
self._insert(new_node, self.root)
def _insert(self, new_node, current_node):
if new_node.value < current_node.value:
if current_node.left is None:
current_node.left = new_node
else:
self._insert(new_node, current_node.left)
elif new_node.value > current_node.value:
if current_node.right is None:
current_node.right = new_node
else:
self._insert(new_node, current_node.right)
else:
raise ValueError("Duplicate values are not allowed in a binary search tree.")
3.1.3 树的遍历和应用场景
树的遍历是指按照某种顺序访问树中的所有节点。常用的遍历方法有:
- 先序遍历: 根节点、左子树、右子树
- 中序遍历: 左子树、根节点、右子树
- 后序遍历: 左子树、右子树、根节点
树在实际应用中非常广泛,例如:
- 文件系统: 文件系统中的目录和文件可以组织成一个树状结构。
- XML文档: XML文档中的元素和属性可以组织成一个树状结构。
- 数据库索引: 数据库索引可以组织成一个树状结构,以提高查询效率。
- 决策树: 决策树是一种机器学习算法,它将数据组织成一个树状结构,以根据输入特征预测输出值。
3.2 哈希表
3.2.1 哈希表的基本概念和实现
哈希表是一种数据结构,它使用哈希函数将键映射到值。哈希函数将键转换为一个哈希值,该哈希值用于确定键在哈希表中的位置。哈希表通常使用数组来存储键值对,其中数组的索引由哈希值确定。
哈希表的实现:
class HashTable:
def __init__(self, size):
self.size = size
self.table = [[] for _ in range(size)]
def put(self, key, value):
hash_value = self._hash(key)
self.table[hash_value].append((key, value))
def get(self, key):
hash_value = self._hash(key)
for k, v in self.table[hash_value]:
if k == key:
return v
return None
def _hash(self, key):
return key % self.size
3.2.2 哈希函数和冲突处理
哈希函数是将键映射到哈希值的一种算法。常用的哈希函数有:
- 模运算: 将键对一个常数取模。
- 除留余数法: 将键除以一个常数,并取余数。
- 平方取中法: 将键平方,然后取中间几位数字。
冲突是指多个键映射到同一个哈希值的情况。冲突处理方法有:
- 开放寻址法: 在发生冲突时,在哈希表中查找下一个空位置。
- 链地址法: 在发生冲突时,将冲突的键值对存储在一个链表中。
- 再散列法: 使用不同的哈希函数重新计算哈希值。
3.2.3 哈希表的应用场景
哈希表在实际应用中非常广泛,例如:
- 字典: 字典是一种数据结构,它使用键来快速查找值。哈希表可以用来实现字典,以提高查找效率。
- 缓存: 缓存是一种数据存储机制,它将最近访问的数据存储在内存中。哈希表可以用来实现缓存,以提高数据访问速度。
- 集合: 集合是一种数据结构,它存储不重复的元素。哈希表可以用来实现集合,以提高元素查找和插入效率。
- 数据库索引: 数据库索引可以组织成一个哈希表,以提高查询效率。
3.3 图
3.3.1 图的基本概念和术语
图是一种数据结构,它由称为顶点(或节点)的元素和连接顶点的边组成。图可以表示各种各样的关系,例如:
- 社交网络: 顶点代表用户,边代表用户之间的关系。
- 交通网络: 顶点代表城市,边代表道路。
- 流程图: 顶点代表步骤,边代表步骤之间的流向。
图的基本术语:
- 顶点: 图中的元素。
- 边: 连接顶点的线段。
- 权重: 边的权重表示边的长度或成本。
- 度: 一个顶点的度表示与该顶点相连的边的数量。
- 路径: 从一个顶点到另一个顶点的顶点序列。
- 回路: 从一个顶点出发,经过若干边后又回到该顶点的路径。
- 连通图: 图中任意两个顶点之间都存在路径。
3.3.2 图的表示和遍历
图可以用邻接矩阵或邻接表来表示。
邻接矩阵: 一个二维数组,其中元素表示顶点之间的权重。
邻接表: 一个数组,其中每个元素是一个链表,该链表存储与该顶点相连的边的信息。
图的遍历方法有:
- 深度优先搜索(DFS): 从一个顶点出发,沿着一条边一直向下遍历,直到无法再向下遍历为止,然后回溯到上一个顶点,继续遍历。
- 广度优先搜索(BFS): 从一个顶点出发,将与该顶点相连的所有顶点放入队列中,然后依次从队列中取出顶点进行遍历。
3.3.3 图的应用场景
图在实际应用中非常广泛,例如:
- 社交网络: 社交网络可以表示为一个图,其中顶点代表用户,边代表用户之间的关系。
- 交通网络: 交通网络可以表示为一个图,其中顶点代表城市,边代表道路。
- 流程图: 流程
4. 算法基础
算法是计算机科学的基础,它为解决问题提供了系统化的方法。本章将介绍算法基础,包括排序算法、搜索算法和动态规划。
4.1 排序算法
排序算法用于将一组数据按特定顺序排列。常见的排序算法包括:
4.1.1 冒泡排序和选择排序
冒泡排序 通过不断比较相邻元素并交换顺序,将最大元素逐个移动到数组末尾。其时间复杂度为 O(n^2)。
def bubble_sort(arr):
for i in range(len(arr) - 1):
for j in range(len(arr) - i - 1):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
选择排序 通过不断找到最小元素并将其交换到数组开头,将最小元素逐个移动到数组开头。其时间复杂度也为 O(n^2)。
def selection_sort(arr):
for i in range(len(arr)):
min_idx = i
for j in range(i + 1, len(arr)):
if arr[j] < arr[min_idx]:
min_idx = j
arr[i], arr[min_idx] = arr[min_idx], arr[i]
4.1.2 快速排序和归并排序
快速排序 使用分治法,通过选择一个基准元素将数组划分为两个子数组,然后递归地对子数组进行排序。其平均时间复杂度为 O(n log n),最坏时间复杂度为 O(n^2)。
def quick_sort(arr, low, high):
if low < high:
partition_index = partition(arr, low, high)
quick_sort(arr, low, partition_index - 1)
quick_sort(arr, partition_index + 1, high)
def partition(arr, low, high):
pivot = arr[high]
i = low - 1
for j in range(low, high):
if arr[j] < pivot:
i += 1
arr[i], arr[j] = arr[j], arr[i]
arr[i + 1], arr[high] = arr[high], arr[i + 1]
return i + 1
归并排序 也使用分治法,通过将数组划分为两个子数组,递归地对子数组进行排序,然后合并排序后的子数组。其时间复杂度始终为 O(n log n)。
def merge_sort(arr):
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left_half = merge_sort(arr[:mid])
right_half = merge_sort(arr[mid:])
return merge(left_half, right_half)
def merge(left, right):
merged = []
left_index = 0
right_index = 0
while left_index < len(left) and right_index < len(right):
if left[left_index] <= right[right_index]:
merged.append(left[left_index])
left_index += 1
else:
merged.append(right[right_index])
right_index += 1
merged.extend(left[left_index:])
merged.extend(right[right_index:])
return merged
4.1.3 排序算法的复杂度分析
下表总结了不同排序算法的时间复杂度:
| 算法 | 平均时间复杂度 | 最坏时间复杂度 | |---|---|---| | 冒泡排序 | O(n^2) | O(n^2) | | 选择排序 | O(n^2) | O(n^2) | | 快速排序 | O(n log n) | O(n^2) | | 归并排序 | O(n log n) | O(n log n) |
4.2 搜索算法
搜索算法用于在数据结构中查找特定元素。常见的搜索算法包括:
4.2.1 线性搜索和二分搜索
线性搜索 逐个比较数据结构中的元素,直到找到目标元素或遍历完整个数据结构。其时间复杂度为 O(n)。
def linear_search(arr, target):
for i in range(len(arr)):
if arr[i] == target:
return i
return -1
二分搜索 仅适用于已排序的数据结构。它通过不断将搜索范围缩小到一半,快速找到目标元素。其时间复杂度为 O(log n)。
def binary_search(arr, target):
low = 0
high = len(arr) - 1
while low <= high:
mid = (low + high) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
low = mid + 1
else:
high = mid - 1
return -1
4.2.2 哈希表中的搜索
哈希表 是一种数据结构,它使用哈希函数将键映射到值。哈希表的搜索操作通过计算键的哈希值并直接访问相应的值来完成。其时间复杂度为 O(1)。
def hash_search(hash_table, key):
index = hash(key) % len(hash_table)
return hash_table[index]
4.2.3 搜索算法的复杂度分析
下表总结了不同搜索算法的时间复杂度:
| 算法 | 时间复杂度 | |---|---| | 线性搜索 | O(n) | | 二分搜索 | O(log n) | | 哈希表中的搜索 | O(1) |
4.3 动态规划
动态规划是一种解决优化问题的算法范式。它通过将问题分解成子问题,并存储子问题的最优解,逐步构建问题的最优解。
4.3.1 动态规划的基本概念和原理
动态规划算法遵循以下基本步骤:
- 分解问题: 将问题分解成一系列重叠的子问题。
- 存储子问题: 使用数据结构存储子问题的最优解。
- 逐步求解: 从最小的子问题开始,逐步求解更大的子问题。
4.3.2 动态规划的经典问题和应用
动态规划算法广泛应用于各种问题,包括:
- 最长公共子序列
- 背包问题
- 最短路径问题
- 编辑距离
4.3.3 动态规划的复杂度分析
动态规划算法的复杂度取决于具体问题。一般来说,其时间复杂度为 O(n^k),其中 n 是问题的大小,k 是子问题的数量。
5. 算法实战应用
5.1 LeetCode经典题目图解解析
5.1.1 数组和链表相关题目
题目:两数之和
题目描述: 给定一个整数数组 nums
和一个目标值 target
,找出数组中两个数的索引,使得它们的和等于 target
。
图解解析:
graph LR
subgraph 暴力解法
A[暴力解法] --> B[双层循环]
end
subgraph 哈希表解法
A[哈希表解法] --> B[哈希表存储] --> C[哈希表查找]
end
暴力解法:
def twoSum_brute_force(nums, target):
for i in range(len(nums)):
for j in range(i + 1, len(nums)):
if nums[i] + nums[j] == target:
return [i, j]
哈希表解法:
def twoSum_hash_table(nums, target):
hash_table = {}
for i, num in enumerate(nums):
complement = target - num
if complement in hash_table:
return [hash_table[complement], i]
hash_table[num] = i
5.1.2 树和图相关题目
题目:二叉树的最大深度
题目描述: 给定一棵二叉树,求它的最大深度。
图解解析:
graph LR
subgraph 递归解法
A[递归解法] --> B[左子树深度] --> C[右子树深度]
end
subgraph 迭代解法
A[迭代解法] --> B[队列存储] --> C[队列出队]
end
递归解法:
def maxDepth_recursive(root):
if not root:
return 0
left_depth = maxDepth_recursive(root.left)
right_depth = maxDepth_recursive(root.right)
return max(left_depth, right_depth) + 1
迭代解法:
def maxDepth_iterative(root):
if not root:
return 0
queue = [root]
depth = 0
while queue:
level_size = len(queue)
for _ in range(level_size):
node = queue.pop(0)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
depth += 1
return depth
简介:本专栏致力于提供最易理解的数据结构与算法知识,帮助学习者掌握计算机科学基础,助力面试成功。专栏内容涵盖:数据结构详解(数组、链表、栈、队列、树、哈希表、图等)、LeetCode题目图解解析、剑指Offer题目深入分析。通过图解、代码示例和实战任务,读者可以全面掌握数据结构与算法的应用,提升编程思维和问题解决能力,为技术面试和实际工作做好充分准备。