24. 算法与数据结构
目录
基础数据结构
数据结构是计算机科学中组织和存储数据的方式,它决定了数据的访问效率和操作复杂度。选择合适的数据结构是算法优化的基础,不同的数据结构适用于不同的应用场景。
数组 (Array)
数组是最基础的数据结构,在内存中连续存储相同类型的元素。数组的核心特点是支持随机访问,即可以通过索引在O(1)时间内访问任意元素。
数组的优势:
- 随机访问快速:通过索引直接计算内存地址
- 内存连续:缓存友好,局部性原理优势明显
- 空间效率高:没有额外的指针开销
数组的劣势:
- 插入删除成本高:需要移动大量元素,时间复杂度O(n)
- 大小固定:静态数组无法动态扩容
- 内存浪费:可能存在未使用的预分配空间
// 数组基础操作示例
public class ArrayOperations {
private int[] arr;
private int size;
private int capacity;
// 动态扩容
private void resize() {
capacity *= 2;
int[] newArr = new int[capacity];
System.arraycopy(arr, 0, newArr, 0, size);
arr = newArr;
}
// 插入元素 - O(n)
public void insert(int index, int value) {
if (size >= capacity) resize();
for (int i = size; i > index; i--) {
arr[i] = arr[i-1];
}
arr[index] = value;
size++;
}
}
链表 (Linked List)
链表是一种线性数据结构,通过指针连接各个节点,每个节点包含数据和指向下一个节点的引用。与数组不同,链表的元素在内存中不连续存储。
data: 10
next: →"] B --> C["Node2
data: 20
next: →"] C --> D["Node3
data: 30
next: →"] D --> E["NULL"] end
Node1
data: 10
next →"] G --> H["← prev
Node2
data: 20
next →"] H --> I["← prev
Node3
data: 30
next →"] I --> J["NULL"] H --> G I --> H end
链表的分类:
- 单向链表:每个节点只有一个指向下一节点的指针
- 双向链表:节点同时包含前驱和后继指针
- 循环链表:尾节点指向头节点,形成环形结构
链表的优势:
- 动态大小:可以在运行时动态分配和释放内存
- 插入删除高效:在已知位置插入/删除只需O(1)
- 内存利用率高:按需分配,不浪费空间
链表的劣势:
- 随机访问慢:必须从头遍历,时间复杂度O(n)
- 额外内存开销:每个节点需要存储指针
- 缓存不友好:节点分散在内存中,局部性差
// 双向链表节点定义
class ListNode {
int val;
ListNode prev, next;
ListNode(int val) { this.val = val; }
}
// 双向链表核心操作
public class DoublyLinkedList {
private ListNode head, tail;
// 在指定节点后插入 - O(1)
public void insertAfter(ListNode node, int val) {
ListNode newNode = new ListNode(val);
newNode.next = node.next;
newNode.prev = node;
if (node.next != null) {
node.next.prev = newNode;
}
node.next = newNode;
}
}
栈 (Stack)
栈是一种后进先出(LIFO) 的数据结构,只能在栈顶进行插入和删除操作。栈在程序执行、表达式求值、括号匹配等场景中应用广泛。
栈的核心操作:
- push():将元素压入栈顶 - O(1)
- pop():弹出栈顶元素 - O(1)
- peek()/top():查看栈顶元素但不删除 - O(1)
- isEmpty():判断栈是否为空 - O(1)
栈的应用场景:
- 函数调用:管理函数调用栈和局部变量
- 表达式计算:中缀转后缀、括号匹配
- 深度优先搜索:DFS算法的核心数据结构
- 回溯算法:保存和恢复状态
// 基于数组的栈实现
public class ArrayStack<T> {
private T[] stack;
private int top = -1;
public void push(T item) {
stack[++top] = item;
}
public T pop() {
return stack[top--];
}
// 括号匹配应用示例
public boolean isValidParentheses(String s) {
Stack<Character> stack = new Stack<>();
for (char c : s.toCharArray()) {
if (c == '(' || c == '[' || c == '{') {
stack.push(c);
} else if (!stack.isEmpty() && matches(stack.peek(), c)) {
stack.pop();
} else {
return false;
}
}
return stack.isEmpty();
}
}
队列 (Queue)
队列是一种先进先出(FIFO) 的数据结构,在队尾插入元素,在队头删除元素。队列广泛应用于任务调度、缓冲区管理和广度优先搜索等场景。
队列的分类:
- 普通队列:基本的FIFO队列
- 循环队列:避免假溢出,提高空间利用率
- 双端队列(Deque):两端都可以插入和删除
- 优先队列:根据优先级而非插入顺序出队
队列的应用场景:
- 任务调度:操作系统进程调度、线程池任务队列
- 缓冲区:生产者-消费者模式的缓冲区
- 广度优先搜索:BFS算法的核心数据结构
- 网络通信:数据包的发送和接收队列
// 循环队列实现
public class CircularQueue {
private int[] queue;
private int front, rear, size, capacity;
public CircularQueue(int capacity) {
this.capacity = capacity;
this.queue = new int[capacity];
}
public boolean enqueue(int value) {
if (isFull()) return false;
queue[rear] = value;
rear = (rear + 1) % capacity;
size++;
return true;
}
public boolean dequeue() {
if (isEmpty()) return false;
front = (front + 1) % capacity;
size--;
return true;
}
}
哈希表 (Hash Table)
哈希表是一种基于哈希函数实现快速查找的数据结构,通过将键映射到数组索引来实现O(1)的平均查找时间。哈希表是现代编程中最重要的数据结构之一。
哈希表的核心概念:
- 哈希函数:将键转换为数组索引的函数
- 冲突处理:处理不同键映射到相同索引的情况
- 负载因子:已使用槽位与总槽位的比值
- 动态扩容:当负载因子过高时扩大数组容量
冲突处理方法:
- 链地址法:每个槽位维护一个链表存储冲突元素
- 开放寻址法:在数组中寻找下一个空槽位
- 线性探测:逐个检查下一个位置
- 二次探测:按平方数序列探测
- 双重哈希:使用第二个哈希函数
// 链地址法哈希表实现
public class HashTable<K, V> {
private class Entry {
K key; V value; Entry next;
Entry(K key, V value) { this.key = key; this.value = value; }
}
private Entry[] table;
private int size, capacity;
private static final double LOAD_FACTOR = 0.75;
private int hash(K key) {
return Math.abs(key.hashCode()) % capacity;
}
public V put(K key, V value) {
if (size >= capacity * LOAD_FACTOR) {
resize();
}
int index = hash(key);
Entry entry = table[index];
// 查找是否已存在
while (entry != null) {
if (entry.key.equals(key)) {
V oldValue = entry.value;
entry.value = value;
return oldValue;
}
entry = entry.next;
}
// 插入新节点
Entry newEntry = new Entry(key, value);
newEntry.next = table[index];
table[index] = newEntry;
size++;
return null;
}
}
堆 (Heap)
堆是一种完全二叉树结构,满足堆性质:父节点的值总是大于等于(最大堆)或小于等于(最小堆)其子节点的值。堆主要用于实现优先队列和堆排序算法。
堆的特点:
- 完全二叉树:除最后一层外,其他层都完全填满
- 堆序性质:父子节点之间的大小关系固定
- 数组存储:可以用数组高效存储,不需要指针
- 索引关系:对于索引i,左子节点为2i+1,右子节点为2i+2
堆的应用场景:
- 优先队列:任务调度、事件处理
- 堆排序:O(n log n)的排序算法
- Top-K问题:找出最大或最小的K个元素
- 图算法:Dijkstra最短路径、Prim最小生成树
// 最小堆实现
public class MinHeap {
private int[] heap;
private int size;
// 上浮操作
private void heapifyUp(int index) {
int parent = (index - 1) / 2;
if (parent >= 0 && heap[parent] > heap[index]) {
swap(parent, index);
heapifyUp(parent);
}
}
// 下沉操作
private void heapifyDown(int index) {
int left = 2 * index + 1;
int right = 2 * index + 2;
int smallest = index;
if (left < size && heap[left] < heap[smallest]) {
smallest = left;
}
if (right < size && heap[right] < heap[smallest]) {
smallest = right;
}
if (smallest != index) {
swap(index, smallest);
heapifyDown(smallest);
}
}
public void insert(int value) {
heap[size] = value;
heapifyUp(size);
size++;
}
public int extractMin() {
int min = heap[0];
heap[0] = heap[--size];
heapifyDown(0);
return min;
}
}
树结构
树是一种分层的数据结构,由节点和边组成,具有天然的递归性质。树结构在计算机科学中应用极其广泛,从文件系统到数据库索引,从编译器的语法分析到人工智能的决策树。
二叉树基础
二叉树是每个节点最多有两个子节点的树结构,分为左子树和右子树。二叉树是最基础的树结构,为理解更复杂的树结构奠定基础。
二叉树的基本概念:
- 根节点:树的顶层节点,没有父节点
- 叶子节点:没有子节点的节点
- 深度:从根节点到某节点的路径长度
- 高度:从某节点到叶子节点的最大路径长度
- 完全二叉树:除最后一层外,其他层都完全填满
- 满二叉树:每个内部节点都有两个子节点
二叉树的遍历方式:
- 前序遍历:根 → 左 → 右(用于复制树结构)
- 中序遍历:左 → 根 → 右(二叉搜索树中序遍历得到有序序列)
- 后序遍历:左 → 右 → 根(用于删除树、计算目录大小)
- 层序遍历:按层从左到右遍历(广度优先)
// 二叉树节点定义
class TreeNode {
int val;
TreeNode left, right;
TreeNode(int val) { this.val = val; }
}
// 二叉搜索树基本操作
public class BinarySearchTree {
private TreeNode root;
// 查找 - O(log n) 平均,O(n) 最坏
public boolean search(int val) {
return searchHelper(root, val);
}
private boolean searchHelper(TreeNode node, int val) {
if (node == null) return false;
if (node.val == val) return true;
return val < node.val ?
searchHelper(node.left, val) : searchHelper(node.right, val);
}
// 插入 - O(log n) 平均
public void insert(int val) {
root = insertHelper(root, val);
}
private TreeNode insertHelper(TreeNode node, int val) {
if (node == null) return new TreeNode(val);
if (val < node.val) {
node.left = insertHelper(node.left, val);
} else if (val > node.val) {
node.right = insertHelper(node.right, val);
}
return node;
}
}
高级树结构
高级树结构通过特殊的平衡策略和存储方式,解决了基础二叉树可能退化为链表的问题,保证了操作的时间复杂度。
AVL树与红黑树
AVL树是严格平衡的二叉搜索树,任意节点的两个子树高度差不超过1。通过旋转操作维护平衡,保证所有操作的时间复杂度为O(log n)。
\\
B(BF=-1)
\\
C"] --> Q["右旋转"] Q --> R[" B
/ \\
C A"] end
红黑树是近似平衡的二叉搜索树,通过颜色标记和旋转操作维护平衡。相比AVL树,红黑树的平衡条件较松,插入删除操作更高效。
祖父节点变红"] X --> Y["祖父节点作为当前节点"] Y --> U W -->|否| Z["需要旋转"] Z --> AA{{"插入节点是父节点的右子?"}} AA -->|是| BB["左旋转父节点"] AA -->|否| CC["右旋转祖父节点"] BB --> CC CC --> DD["调整颜色"] DD --> V end
所有路径包含相同数目的黑节点"] end
特性对比:
| 特性 | AVL树 | 红黑树 |
|---|---|---|
| 平衡严格程度 | 严格平衡(高度差≤1) | 近似平衡(黑高度相等) |
| 查找性能 | 最优O(log n) | 良好O(log n) |
| 插入性能 | 较慢(频繁旋转) | 较快(最多3次旋转) |
| 删除性能 | 较慢(频繁旋转) | 较快(最多3次旋转) |
| 内存开销 | 每节点存储高度 | 每节点存储颜色位 |
| 应用场景 | 查找密集型应用 | 通用平衡树应用 |
// AVL树旋转操作核心
class AVLTree {
class Node {
int val, height;
Node left, right;
}
// 右旋转
private Node rotateRight(Node y) {
Node x = y.left;
y.left = x.right;
x.right = y;
updateHeight(y);
updateHeight(x);
return x;
}
// 获取平衡因子
private int getBalance(Node node) {
return height(node.left) - height(node.right);
}
}
// 红黑树插入后调整
class RedBlackTree {
enum Color { RED, BLACK }
class Node {
int val;
Color color;
Node left, right, parent;
}
// 插入后修复红黑树性质
private void fixInsert(Node node) {
while (node.parent != null && node.parent.color == Color.RED) {
// 根据父节点是祖父的左子还是右子进行不同处理
if (node.parent == node.parent.parent.left) {
Node uncle = node.parent.parent.right;
if (uncle != null && uncle.color == Color.RED) {
// 情况1:叔叔节点是红色,重新着色
node.parent.color = Color.BLACK;
uncle.color = Color.BLACK;
node.parent.parent.color = Color.RED;
node = node.parent.parent;
} else {
// 情况2/3:叔叔节点是黑色,需要旋转
if (node == node.parent.right) {
node = node.parent;
rotateLeft(node);
}
node.parent.color = Color.BLACK;
node.parent.parent.color = Color.RED;
rotateRight(node.parent.parent);
}
}
// 对称处理右侧情况...
}
root.color = Color.BLACK; // 根节点始终为黑色
}
}
B+树
B+树是一种多路搜索树,专门为磁盘存储系统设计。与二叉树不同,B+树的每个节点可以包含多个键值,所有数据都存储在叶子节点中,内部节点只存储索引信息。
[50, 100]"] --> B["内部节点
[20, 30, 40]"] A --> C["内部节点
[70, 80, 90]"] A --> D["内部节点
[120, 140]"] B --> E["叶子节点
[10, 15, 18]"] B --> F["叶子节点
[25, 28]"] B --> G["叶子节点
[35, 38]"] B --> H["叶子节点
[42, 45]"] E --> F F --> G G --> H end
B+树的核心特性:
- 多路结构:每个节点包含多个键值,减少树的高度
- 数据在叶子:所有实际数据都在叶子节点,内部节点只做索引
- 叶子链表:叶子节点通过指针连接,支持高效范围查询
- 磁盘友好:节点大小与磁盘页面大小匹配,减少I/O次数
B+树的优势:
- 范围查询高效:叶子节点链表结构支持快速范围扫描
- 磁盘I/O少:高扇出比减少树的层数,降低磁盘访问次数
- 查询稳定:所有查询都到达叶子节点,性能一致
- 并发友好:叶子节点分离,便于实现锁机制
// B+树简化实现
class BPlusTree {
private int order; // B+树的阶数
abstract class Node {
int[] keys;
int keyCount;
Node parent;
boolean isLeaf;
}
class InternalNode extends Node {
Node[] children;
// 查找子节点
Node findChild(int key) {
int i = 0;
while (i < keyCount && key >= keys[i]) i++;
return children[i];
}
}
class LeafNode extends Node {
String[] values; // 实际数据
LeafNode next; // 链接到下一个叶子节点
// 范围查询
List<String> rangeQuery(int start, int end) {
List<String> result = new ArrayList<>();
LeafNode current = this;
while (current != null) {
for (int i = 0; i < current.keyCount; i++) {
if (current.keys[i] >= start && current.keys[i] <= end) {
result.add(current.values[i]);
}
if (current.keys[i] > end) return result;
}
current = current.next;
}
return result;
}
}
// 搜索操作
public String search(int key) {
Node current = root;
// 从根节点向下查找到叶子节点
while (!current.isLeaf) {
current = ((InternalNode) current).findChild(key);
}
// 在叶子节点中查找具体值
LeafNode leaf = (LeafNode) current;
for (int i = 0; i < leaf.keyCount; i++) {
if (leaf.keys[i] == key) {
return leaf.values[i];
}
}
return null;
}
}
LSM-Tree
LSM-Tree(Log-Structured Merge Tree)是一种针对写密集型工作负载优化的数据结构。通过将随机写转换为顺序写,LSM-Tree在写入性能上远超传统的B+树,广泛应用于现代NoSQL数据库。
(内存中的跳表)"] B --> C{{"MemTable满?"}} C -->|否| D["继续写入MemTable"] C -->|是| E["转为Immutable MemTable"] E --> F["后台刷写到L0层"] F --> G["L0 SSTable
(磁盘文件)"] G --> H["L1 SSTable"] H --> I["L2 SSTable"] I --> J["L3 SSTable..."] end
批量刷写磁盘"] XX["读取性能"] --> YY["可能需要查找多层
Bloom Filter优化"] ZZ["空间放大"] --> AAA["多版本数据存在
定期压缩清理"] BBB["写放大"] --> CCC["数据多次重写
压缩过程中"] end
LSM-Tree的设计思想:
- 写入优化:所有写入都先进入内存,批量刷写到磁盘
- 分层存储:数据分布在多个层次,上层小而新,下层大而旧
- 合并压缩:定期将小文件合并成大文件,保持读取效率
- 顺序I/O:利用磁盘顺序读写的高性能特性
LSM-Tree的核心组件详解:
MemTable(内存表)
MemTable是LSM-Tree中的核心内存数据结构,通常使用跳表(Skip List)实现,负责接收所有的写入操作。
跳表(Skip List)特点:
- 多层索引结构:通过多层链表提供O(log n)的查找性能
- 概率平衡:通过随机化避免复杂的平衡操作
- 并发友好:相比红黑树更容易实现无锁并发
- 有序存储:天然维护键值的有序性
MemTable的关键特性:
- 写入性能:O(log n)的插入时间复杂度
- 查找性能:O(log n)的查找时间复杂度
- 内存限制:通常限制大小为64MB-256MB
- 线程安全:支持多线程并发读写
- WAL保护:写入前先记录WAL日志保证持久性
Immutable MemTable(不可变内存表)
当MemTable达到大小限制时,会转换为Immutable MemTable,这是一个只读的内存结构。
可读写"] --> DD["达到大小限制
(如64MB)"] DD --> EE["转为Immutable MemTable
只读"] EE --> FF["创建新的Active MemTable"] EE --> GG["后台刷写到磁盘"] GG --> HH["生成L0 SSTable"] HH --> II["删除Immutable MemTable"] end
Immutable MemTable特点:
- 只读性质:不再接受新的写入操作
- 内存驻留:在刷写完成前仍在内存中
- 并发读取:支持与新MemTable并发操作
- 原子切换:从Active到Immutable的切换是原子操作
- WAL清理:刷写完成后可以清理对应的WAL日志
L0 SSTable(第0层有序字符串表)
L0 SSTable是从Immutable MemTable刷写到磁盘的第一层存储文件。
(不同SSTable间)"] JJ --> LL["单个文件内部有序"] JJ --> MM["文件间可能无序"] JJ --> NN["直接从MemTable生成"] OO["查找过程"] --> PP["需要检查所有L0文件"] OO --> QQ["使用Bloom Filter优化"] OO --> RR["按时间戳排序检查"] end
按键排序"] UU --> UU1["存储数据块索引
快速定位"] VV --> VV1["Bloom Filter
快速判断键存在性"] WW --> WW1["统计信息
压缩信息等"] XX --> XX1["文件格式版本
校验和等"] end
L0 SSTable的关键特征:
- 键范围重叠:不同L0文件的键范围可能重叠
- 时间有序:文件按生成时间排序
- 压缩触发:L0文件数量达到阈值时触发压缩
- 读取复杂:查找时需要检查多个文件
Bloom Filter(布隆过滤器)
Bloom Filter是LSM-Tree中的重要优化组件,用于快速判断键是否可能存在。
需进一步检查"] end
Bloom Filter在LSM-Tree中的应用:
- 快速过滤:避免不必要的磁盘I/O操作
- 假阳性:可能误判存在,但不会误判不存在
- 内存高效:使用很少的内存空间
- 多层应用:每个SSTable都有对应的Bloom Filter
层级结构(Leveled Structure)
LSM-Tree采用分层存储策略,数据从L0层逐步向下层合并,每一层都有特定的大小限制和特性。
4个文件
键范围重叠"] --> L1["L1层
10个文件
键范围不重叠
总大小10MB"] L1 --> L2["L2层
100个文件
键范围不重叠
总大小100MB"] L2 --> L3["L3层
1000个文件
键范围不重叠
总大小1GB"] L3 --> L4["L4层及更多
按倍数增长"] L0_Detail["L0特点:
• 直接从MemTable生成
• 文件间键范围可重叠
• 数量限制(如4个)
• 触发压缩阈值"] L1_Detail["L1+特点:
• 键范围严格不重叠
• 文件大小固定
• 层间大小比例10:1
• 有序存储"] end
层级特性对比:
| 层级 | 文件数量 | 键范围重叠 | 总大小限制 | 压缩策略 | 查找复杂度 |
|---|---|---|---|---|---|
| L0 | 4-8个 | 允许重叠 | 64MB-256MB | Size-tiered | O(文件数) |
| L1+ | 变化 | 严格不重叠 | 按倍数增长 | Leveled | O(1) |
压缩策略(Compaction Strategies)
压缩是LSM-Tree维持性能的关键机制,通过合并小文件为大文件,清理过期数据,优化存储结构。
MemTable → SSTable"] --> Minor1["• 将内存数据刷写到L0
• 不涉及数据合并
• 频率高,开销小
• 异步后台执行"] Major["Major Compaction
SSTable间合并"] --> Major1["• L0 → L1 合并
• Li → Li+1 合并
• 涉及数据去重
• 开销大,影响性能"] Full["Full Compaction
全层压缩"] --> Full1["• 整个层级重写
• 彻底清理垃圾数据
• 恢复最优存储
• 通常离线执行"] end
(如4个文件)"] --> B["选择L0所有文件"] B --> C["在L1中找重叠文件"] C --> D["多路归并排序"] D --> E["生成新的L1文件"] E --> F["删除旧文件"] F --> G["更新元数据"] H["Li层达到大小限制"] --> I["选择一个Li文件"] I --> J["在Li+1中找重叠文件"] J --> K["合并排序"] K --> L["生成Li+1文件"] L --> M["删除旧文件"] end
压缩触发条件:
- L0文件数量:超过阈值(如4个)触发L0→L1压缩
- 层级大小:Li层总大小超过限制触发Li→Li+1压缩
- 读放大:查找性能下降时主动触发压缩
- 定期维护:定时触发清理过期数据
WAL(Write-Ahead Log)
WAL日志确保LSM-Tree的数据持久性,在MemTable写入前先记录日志。
WAL的关键特性:
- 顺序写入:WAL采用追加写入,性能优异
- 持久性保证:写入WAL后数据不会丢失
- 故障恢复:系统重启时通过WAL恢复数据
- 定期清理:MemTable刷写后清理对应WAL
- 压缩优化:定期压缩WAL文件减少空间占用
读写路径优化
性能优化技术:
- Block Cache:缓存热点数据块,减少磁盘I/O
- 批量写入:合并多个写入操作,提高吞吐量
- 异步刷写:后台异步执行刷写操作,不阻塞写入
- 压缩调度:智能调度压缩任务,平衡读写性能
- 分区策略:按键范围分区,支持并行操作
(内存)"] A2 --> A3["Immutable MemTable"] A3 --> A4["L0 SSTable"] A4 --> A5["L1 SSTable"] A5 --> A6["L2 SSTable"] A6 --> A7["...更多层级"] end subgraph "合并压缩" B1["Minor Compaction"] --> B2["MemTable → SSTable"] B3["Major Compaction"] --> B4["多个SSTable合并"] end subgraph "读取路径" C1["读取请求"] --> C2["检查MemTable"] C2 --> C3["检查Immutable MemTable"] C3 --> C4["按层级检查SSTable"] C4 --> C5["Bloom Filter过滤"] end
// LSM-Tree核心操作实现
class LSMTree {
private SkipListMap<String, String> memTable;
private List<SSTable> sstables;
private int memTableThreshold = 1000;
// 写入操作 - O(log n)
public void put(String key, String value) {
memTable.put(key, value);
// 检查是否需要刷写
if (memTable.size() >= memTableThreshold) {
flushMemTable();
}
}
// 读取操作 - O(log n * 层数)
public String get(String key) {
// 1. 检查MemTable
if (memTable.containsKey(key)) {
return memTable.get(key);
}
// 2. 检查各层SSTable
for (SSTable table : sstables) {
if (table.mightContain(key)) { // Bloom Filter检查
String value = table.get(key);
if (value != null) return value;
}
}
return null; // 未找到
}
// 刷写MemTable到磁盘
private void flushMemTable() {
SSTable newTable = new SSTable(memTable);
sstables.add(newTable);
memTable.clear();
// 触发压缩检查
checkCompaction();
}
// 压缩操作
private void checkCompaction() {
if (sstables.size() > 4) {
// 选择需要压缩的SSTable
List<SSTable> toCompact = selectTablesForCompaction();
SSTable merged = mergeTables(toCompact);
// 替换旧表
sstables.removeAll(toCompact);
sstables.add(merged);
}
}
}
// SSTable简化实现
class SSTable {
private Map<String, String> data;
private BloomFilter<String> bloomFilter;
public SSTable(Map<String, String> memTable) {
this.data = new TreeMap<>(memTable);
this.bloomFilter = BloomFilter.create(
Funnels.stringFunnel(Charset.defaultCharset()),
data.size()
);
// 构建Bloom Filter
for (String key : data.keySet()) {
bloomFilter.put(key);
}
}
public boolean mightContain(String key) {
return bloomFilter.mightContain(key);
}
public String get(String key) {
return data.get(key);
}
}
树结构性能对比与应用场景
不同的树结构适用于不同的应用场景,选择合适的树结构对系统性能至关重要。
| 树结构 | 查找 | 插入 | 删除 | 范围查询 | 内存使用 | 主要应用场景 |
|---|---|---|---|---|---|---|
| 二叉搜索树 | O(n) | O(n) | O(n) | O(n) | 低 | 教学、简单应用 |
| AVL树 | O(log n) | O(log n) | O(log n) | O(log n + k) | 中 | 查找密集型应用 |
| 红黑树 | O(log n) | O(log n) | O(log n) | O(log n + k) | 中 | 通用平衡树、STL |
| B+树 | O(log n) | O(log n) | O(log n) | O(log n + k) | 高 | 数据库索引、文件系统 |
| LSM-Tree | O(log n * L) | O(log n) | O(log n) | O(log n + k) | 中 | 写密集型数据库 |
应用场景分析:
// 场景1:内存数据库 - 选择红黑树
class InMemoryDatabase {
private TreeMap<String, Object> index = new TreeMap<>(); // 基于红黑树
public void insert(String key, Object value) {
index.put(key, value); // O(log n),插入删除平衡
}
public List<Object> rangeQuery(String start, String end) {
return index.subMap(start, true, end, true)
.values().stream().collect(Collectors.toList());
}
}
// 场景2:数据库索引 - 选择B+树
class DatabaseIndex {
private BPlusTree index = new BPlusTree(100); // 高扇出比
// 范围查询高效,磁盘I/O少
public List<Record> rangeQuery(int start, int end) {
return index.rangeQuery(start, end); // 叶子节点链表扫描
}
}
// 场景3:高吞吐写入系统 - 选择LSM-Tree
class HighThroughputWriteSystem {
private LSMTree storage = new LSMTree();
public void batchWrite(List<KeyValue> data) {
for (KeyValue kv : data) {
storage.put(kv.key, kv.value); // 写入MemTable,批量刷写
}
}
}
选择决策树:
红黑:通用场景"] G --> K["数据库索引
文件系统"] H --> L["STL容器
内存数据库"] I --> M["关系型数据库
分布式存储"] D --> N["NoSQL数据库
日志系统"]
高级树结构操作对比流程:
最优查找"] T --> V["红黑树: O(log n)
良好查找"] T --> W["B+树: O(log n)
磁盘I/O优化"] T --> X["LSM-Tree: O(log n × L)
多层查找"] U --> U1["树高度最小"] V --> V1["平衡性适中"] W --> W1["范围查询高效"] X --> X1["Bloom Filter加速"] end
查找密集型"] Z -->|否| BB{"需要频繁插入删除?"} BB -->|是| CC{"主要是写操作?"} CC -->|是| DD["LSM-Tree
写密集型"] CC -->|否| EE["红黑树
通用场景"] BB -->|否| FF{"需要范围查询?"} FF -->|是| GG["B+树
数据库索引"] FF -->|否| HH["根据其他因素选择"] end
图结构
图是一种复杂的非线性数据结构,由**顶点(Vertex)和边(Edge)**组成,用于表示对象之间的复杂关系。图结构在社交网络、路径规划、网络拓扑、依赖分析等领域有广泛应用。
图的表示方法
图的存储方式直接影响算法的时间复杂度和空间复杂度,主要有两种表示方法:邻接矩阵和邻接表。
邻接矩阵
邻接矩阵使用二维数组表示图,matrix[i][j]表示顶点i到顶点j是否有边。适用于稠密图和需要快速判断两点间是否有边的场景。
邻接矩阵的特点:
- 空间复杂度:O(V²),V为顶点数
- 边查询:O(1),快速判断两点间是否有边
- 遍历邻居:O(V),需要扫描整行
- 适用场景:稠密图、频繁边查询
邻接表
邻接表为每个顶点维护一个邻居列表,只存储实际存在的边。适用于稀疏图和需要频繁遍历邻居的场景。
邻接表的特点:
- 空间复杂度:O(V + E),E为边数
- 边查询:O(degree),需要遍历邻居列表
- 遍历邻居:O(degree),直接访问邻居
- 适用场景:稀疏图、频繁邻居遍历
// 邻接矩阵实现
class GraphMatrix {
private int[][] matrix;
private int vertices;
public GraphMatrix(int vertices) {
this.vertices = vertices;
this.matrix = new int[vertices][vertices];
}
// 添加边 - O(1)
public void addEdge(int from, int to, int weight) {
matrix[from][to] = weight;
// 无向图需要双向添加
// matrix[to][from] = weight;
}
// 检查是否有边 - O(1)
public boolean hasEdge(int from, int to) {
return matrix[from][to] != 0;
}
// 获取所有邻居 - O(V)
public List<Integer> getNeighbors(int vertex) {
List<Integer> neighbors = new ArrayList<>();
for (int i = 0; i < vertices; i++) {
if (matrix[vertex][i] != 0) {
neighbors.add(i);
}
}
return neighbors;
}
}
// 邻接表实现
class GraphList {
private List<List<Edge>> adjacencyList;
private int vertices;
class Edge {
int to, weight;
Edge(int to, int weight) { this.to = to; this.weight = weight; }
}
public GraphList(int vertices) {
this.vertices = vertices;
this.adjacencyList = new ArrayList<>(vertices);
for (int i = 0; i < vertices; i++) {
adjacencyList.add(new ArrayList<>());
}
}
// 添加边 - O(1)
public void addEdge(int from, int to, int weight) {
adjacencyList.get(from).add(new Edge(to, weight));
// 无向图需要双向添加
// adjacencyList.get(to).add(new Edge(from, weight));
}
// 检查是否有边 - O(degree)
public boolean hasEdge(int from, int to) {
return adjacencyList.get(from).stream()
.anyMatch(edge -> edge.to == to);
}
// 获取所有邻居 - O(degree)
public List<Edge> getNeighbors(int vertex) {
return adjacencyList.get(vertex);
}
}
图的遍历算法
图的遍历是图算法的基础,主要有**深度优先搜索(DFS)和广度优先搜索(BFS)**两种方法。
深度优先搜索(DFS)
DFS使用栈的思想,沿着一条路径深入到底,然后回溯到上一个节点继续探索其他路径。适用于路径搜索、连通性检测、拓扑排序等场景。
DFS的应用场景:
- 路径搜索:寻找从起点到终点的路径
- 连通性检测:判断图是否连通
- 环检测:检测图中是否存在环
- 拓扑排序:有向无环图的线性排序
广度优先搜索(BFS)
BFS使用队列的思想,按层次逐步扩展,先访问距离起点近的节点。适用于最短路径、最小生成树、层次遍历等场景。
BFS的应用场景:
- 最短路径:无权图的最短路径搜索
- 层次遍历:按距离分层访问节点
- 连通分量:寻找图的连通分量
- 最小生成树:Prim算法的基础
// 图遍历算法实现
class GraphTraversal {
private GraphList graph;
// 深度优先搜索 - 递归实现
public void dfsRecursive(int start, boolean[] visited) {
visited[start] = true;
System.out.print(start + " ");
for (GraphList.Edge edge : graph.getNeighbors(start)) {
if (!visited[edge.to]) {
dfsRecursive(edge.to, visited);
}
}
}
// 深度优先搜索 - 栈实现
public void dfsIterative(int start) {
boolean[] visited = new boolean[graph.vertices];
Stack<Integer> stack = new Stack<>();
stack.push(start);
while (!stack.isEmpty()) {
int current = stack.pop();
if (!visited[current]) {
visited[current] = true;
System.out.print(current + " ");
// 将邻居加入栈(逆序以保持顺序)
List<GraphList.Edge> neighbors = graph.getNeighbors(current);
for (int i = neighbors.size() - 1; i >= 0; i--) {
if (!visited[neighbors.get(i).to]) {
stack.push(neighbors.get(i).to);
}
}
}
}
}
// 广度优先搜索
public void bfs(int start) {
boolean[] visited = new boolean[graph.vertices];
Queue<Integer> queue = new LinkedList<>();
visited[start] = true;
queue.offer(start);
while (!queue.isEmpty()) {
int current = queue.poll();
System.out.print(current + " ");
for (GraphList.Edge edge : graph.getNeighbors(current)) {
if (!visited[edge.to]) {
visited[edge.to] = true;
queue.offer(edge.to);
}
}
}
}
// BFS最短路径
public List<Integer> shortestPath(int start, int end) {
boolean[] visited = new boolean[graph.vertices];
int[] parent = new int[graph.vertices];
Queue<Integer> queue = new LinkedList<>();
Arrays.fill(parent, -1);
visited[start] = true;
queue.offer(start);
while (!queue.isEmpty()) {
int current = queue.poll();
if (current == end) {
// 重构路径
List<Integer> path = new ArrayList<>();
int node = end;
while (node != -1) {
path.add(0, node);
node = parent[node];
}
return path;
}
for (GraphList.Edge edge : graph.getNeighbors(current)) {
if (!visited[edge.to]) {
visited[edge.to] = true;
parent[edge.to] = current;
queue.offer(edge.to);
}
}
}
return new ArrayList<>(); // 无路径
}
}
图遍历算法对比:
| 算法 | 数据结构 | 时间复杂度 | 空间复杂度 | 主要应用 |
|---|---|---|---|---|
| DFS递归 | 系统栈 | O(V + E) | O(V) | 路径搜索、环检测 |
| DFS迭代 | 显式栈 | O(V + E) | O(V) | 避免栈溢出 |
| BFS | 队列 | O(V + E) | O(V) | 最短路径、层次遍历 |
排序算法
排序是将一组数据按照特定顺序重新排列的过程,是计算机科学中最基础和重要的算法之一。不同的排序算法有不同的时间复杂度、空间复杂度和稳定性特征,选择合适的排序算法对程序性能至关重要。
基础排序算法
基础排序算法虽然时间复杂度较高,但实现简单,适用于小规模数据或作为其他算法的组成部分。
冒泡排序 (Bubble Sort)
冒泡排序通过重复交换相邻的逆序元素,让大元素像气泡一样逐渐"浮"到数组末尾。虽然效率低,但概念简单,是理解排序算法的入门选择。
算法特点:
- 时间复杂度:O(n²) 平均和最坏,O(n) 最好(已排序)
- 空间复杂度:O(1)
- 稳定性:稳定
- 适用场景:教学演示、极小数据集
选择排序 (Selection Sort)
选择排序每次选择未排序部分的最小元素,与未排序部分的第一个元素交换。交换次数最少,但比较次数固定。
算法特点:
- 时间复杂度:O(n²) 所有情况
- 空间复杂度:O(1)
- 稳定性:不稳定
- 适用场景:内存写入代价高的场景
插入排序 (Insertion Sort)
插入排序将数组分为已排序和未排序两部分,逐个将未排序元素插入到已排序部分的正确位置。对于小规模或近似有序的数据效率很高。
算法特点:
- 时间复杂度:O(n²) 平均和最坏,O(n) 最好
- 空间复杂度:O(1)
- 稳定性:稳定
- 适用场景:小数据集、近似有序数据、递归算法的基础情况
// 基础排序算法实现
public class BasicSorting {
// 冒泡排序
public static void bubbleSort(int[] arr) {
int n = arr.length;
boolean swapped;
for (int i = 0; i < n - 1; i++) {
swapped = false;
for (int j = 0; j < n - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
swap(arr, j, j + 1);
swapped = true;
}
}
// 优化:如果没有交换,说明已排序
if (!swapped) break;
}
}
// 选择排序
public static void selectionSort(int[] arr) {
int n = arr.length;
for (int i = 0; i < n - 1; i++) {
int minIdx = i;
// 找到最小元素的索引
for (int j = i + 1; j < n; j++) {
if (arr[j] < arr[minIdx]) {
minIdx = j;
}
}
swap(arr, i, minIdx);
}
}
// 插入排序
public static void insertionSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
int key = arr[i];
int j = i - 1;
// 将大于key的元素向右移动
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
}
}
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
高级排序算法
高级排序算法通过分治、堆等策略优化时间复杂度,适用于大规模数据排序。
快速排序 (Quick Sort)
快速排序采用分治策略,选择一个基准元素,将数组分为小于和大于基准的两部分,然后递归排序。平均性能优秀,是最常用的排序算法之一。
算法特点:
- 时间复杂度:O(n log n) 平均,O(n²) 最坏
- 空间复杂度:O(log n) 平均(递归栈)
- 稳定性:不稳定
- 优化策略:随机选择基准、三数取中、尾递归优化
归并排序 (Merge Sort)
归并排序采用分治策略,将数组分为两半,分别排序后合并。性能稳定,适用于外排序和链表排序。
算法特点:
- 时间复杂度:O(n log n) 所有情况
- 空间复杂度:O(n)
- 稳定性:稳定
- 适用场景:要求稳定排序、外排序、链表排序
堆排序 (Heap Sort)
堆排序利用堆的性质,先构建最大堆,然后重复提取最大元素。性能稳定,原地排序。
算法特点:
- 时间复杂度:O(n log n) 所有情况
- 空间复杂度:O(1)
- 稳定性:不稳定
- 适用场景:内存受限、需要最坏情况保证
排序算法性能对比:
| 算法 | 最好时间 | 平均时间 | 最坏时间 | 空间复杂度 | 稳定性 | 适用场景 |
|---|---|---|---|---|---|---|
| 冒泡排序 | O(n) | O(n²) | O(n²) | O(1) | 稳定 | 教学、极小数据 |
| 选择排序 | O(n²) | O(n²) | O(n²) | O(1) | 不稳定 | 内存写入昂贵 |
| 插入排序 | O(n) | O(n²) | O(n²) | O(1) | 稳定 | 小数据、近似有序 |
| 快速排序 | O(n log n) | O(n log n) | O(n²) | O(log n) | 不稳定 | 通用、高性能 |
| 归并排序 | O(n log n) | O(n log n) | O(n log n) | O(n) | 稳定 | 稳定排序、外排序 |
| 堆排序 | O(n log n) | O(n log n) | O(n log n) | O(1) | 不稳定 | 内存受限 |
算法设计思想
算法设计思想是解决复杂问题的基本策略和方法论。掌握这些思想能够帮助我们将复杂问题分解为简单的子问题,从而设计出高效的算法解决方案。
动态规划
动态规划(Dynamic Programming, DP)是一种通过将复杂问题分解为子问题来求解的算法思想。它适用于具有重叠子问题和最优子结构性质的问题。
动态规划的核心概念:
- 重叠子问题:子问题会被重复计算,可以通过存储结果避免重复计算
- 最优子结构:问题的最优解包含子问题的最优解
- 状态转移方程:描述问题状态之间关系的数学表达式
- 边界条件:递归的终止条件
动态规划的解题步骤:
- 定义状态:确定用什么变量表示子问题
- 找出状态转移方程:子问题之间的关系
- 确定边界条件:最简单子问题的解
- 确定计算顺序:避免使用未计算的状态
经典动态规划问题
1. 斐波那契数列
// 斐波那契数列 - 自底向上
public int fibonacci(int n) {
if (n <= 1) return n;
int[] dp = new int[n + 1];
dp[0] = 0;
dp[1] = 1;
for (int i = 2; i <= n; i++) {
dp[i] = dp[i-1] + dp[i-2];
}
return dp[n];
}
// 空间优化版本
public int fibonacciOptimized(int n) {
if (n <= 1) return n;
int prev2 = 0, prev1 = 1;
for (int i = 2; i <= n; i++) {
int current = prev1 + prev2;
prev2 = prev1;
prev1 = current;
}
return prev1;
}
2. 最长递增子序列(LIS)
// 最长递增子序列
public int lengthOfLIS(int[] nums) {
if (nums.length == 0) return 0;
int[] dp = new int[nums.length];
Arrays.fill(dp, 1);
int maxLength = 1;
for (int i = 1; i < nums.length; i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
maxLength = Math.max(maxLength, dp[i]);
}
return maxLength;
}
3. 0-1背包问题
// 0-1背包问题
public int knapsack(int[] weights, int[] values, int capacity) {
int n = weights.length;
int[][] dp = new int[n + 1][capacity + 1];
for (int i = 1; i <= n; i++) {
for (int w = 1; w <= capacity; w++) {
if (weights[i-1] <= w) {
// 可以选择第i个物品
dp[i][w] = Math.max(
dp[i-1][w], // 不选
dp[i-1][w-weights[i-1]] + values[i-1] // 选择
);
} else {
// 不能选择第i个物品
dp[i][w] = dp[i-1][w];
}
}
}
return dp[n][capacity];
}
// 空间优化版本
public int knapsackOptimized(int[] weights, int[] values, int capacity) {
int[] dp = new int[capacity + 1];
for (int i = 0; i < weights.length; i++) {
// 从右到左更新,避免重复使用
for (int w = capacity; w >= weights[i]; w--) {
dp[w] = Math.max(dp[w], dp[w - weights[i]] + values[i]);
}
}
return dp[capacity];
}
4. 最长公共子序列(LCS)
// 最长公共子序列
public int longestCommonSubsequence(String text1, String text2) {
int m = text1.length(), n = text2.length();
int[][] dp = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (text1.charAt(i-1) == text2.charAt(j-1)) {
dp[i][j] = dp[i-1][j-1] + 1;
} else {
dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
}
}
}
return dp[m][n];
}
动态规划问题分类:
| 问题类型 | 特征 | 经典例题 | 状态定义 |
|---|---|---|---|
| 线性DP | 状态呈线性排列 | 斐波那契、爬楼梯、最大子数组 | dp[i] |
| 区间DP | 在区间上进行决策 | 矩阵链乘法、回文串 | dp[i][j] |
| 背包DP | 选择物品的问题 | 0-1背包、完全背包 | dp[i][w] |
| 树形DP | 在树上进行状态转移 | 树的直径、打家劫舍III | dp[node][state] |
| 状态机DP | 有限状态转移 | 买卖股票、打家劫舍 | dp[i][state] |
贪心算法
贪心算法在每一步都选择当前看起来最优的选择,希望通过一系列局部最优选择达到全局最优。贪心算法的关键是证明贪心选择的正确性。
贪心算法的特点:
- 贪心选择性质:局部最优选择能导致全局最优解
- 最优子结构:问题的最优解包含子问题的最优解
- 无后效性:一旦做出选择,不会回头修改
经典贪心问题:
// 活动选择问题
public int activitySelection(int[] start, int[] end) {
int n = start.length;
// 按结束时间排序
Integer[] indices = new Integer[n];
for (int i = 0; i < n; i++) indices[i] = i;
Arrays.sort(indices, (a, b) -> end[a] - end[b]);
int count = 1;
int lastEnd = end[indices[0]];
for (int i = 1; i < n; i++) {
if (start[indices[i]] >= lastEnd) {
count++;
lastEnd = end[indices[i]];
}
}
return count;
}
// 分糖果问题
public int candy(int[] ratings) {
int n = ratings.length;
int[] candies = new int[n];
Arrays.fill(candies, 1);
// 从左到右,保证右边比左边高分的孩子糖果更多
for (int i = 1; i < n; i++) {
if (ratings[i] > ratings[i-1]) {
candies[i] = candies[i-1] + 1;
}
}
// 从右到左,保证左边比右边高分的孩子糖果更多
for (int i = n-2; i >= 0; i--) {
if (ratings[i] > ratings[i+1]) {
candies[i] = Math.max(candies[i], candies[i+1] + 1);
}
}
return Arrays.stream(candies).sum();
}
分治算法
分治算法将问题分解为若干个规模较小的子问题,递归解决子问题,然后合并子问题的解得到原问题的解。
分治算法的步骤:
- 分解:将原问题分解为若干子问题
- 解决:递归解决子问题
- 合并:将子问题的解合并为原问题的解
// 归并排序(分治经典例子)
public void mergeSort(int[] arr, int left, int right) {
if (left < right) {
int mid = left + (right - left) / 2;
// 分解
mergeSort(arr, left, mid);
mergeSort(arr, mid + 1, right);
// 合并
merge(arr, left, mid, right);
}
}
// 最大子数组问题(分治解法)
public int maxSubArrayDivide(int[] nums, int left, int right) {
if (left == right) return nums[left];
int mid = left + (right - left) / 2;
// 分别计算左半部分和右半部分的最大子数组
int leftMax = maxSubArrayDivide(nums, left, mid);
int rightMax = maxSubArrayDivide(nums, mid + 1, right);
// 计算跨越中点的最大子数组
int leftSum = Integer.MIN_VALUE;
int sum = 0;
for (int i = mid; i >= left; i--) {
sum += nums[i];
leftSum = Math.max(leftSum, sum);
}
int rightSum = Integer.MIN_VALUE;
sum = 0;
for (int i = mid + 1; i <= right; i++) {
sum += nums[i];
rightSum = Math.max(rightSum, sum);
}
int crossMax = leftSum + rightSum;
// 返回三者中的最大值
return Math.max(Math.max(leftMax, rightMax), crossMax);
}
算法设计思想对比:
| 设计思想 | 核心策略 | 适用问题 | 时间复杂度 | 典型例子 |
|---|---|---|---|---|
| 动态规划 | 存储子问题解,避免重复计算 | 重叠子问题+最优子结构 | 通常O(n²)或O(n³) | 背包、LIS、LCS |
| 贪心算法 | 每步选择局部最优 | 贪心选择+最优子结构 | 通常O(n log n) | 活动选择、Huffman编码 |
| 分治算法 | 分解问题,递归求解,合并结果 | 可分解的独立子问题 | 通常O(n log n) | 归并排序、快速排序 |
| 回溯算法 | 试探性搜索,回退撤销选择 | 搜索所有可能解 | 通常指数级 | N皇后、数独 |
查找算法
查找算法用于在数据集合中寻找特定元素,是程序设计中的基础操作。选择合适的查找算法能够显著提升程序性能,不同的数据组织方式需要采用不同的查找策略。
基础查找
线性查找(顺序查找)
线性查找是最简单直观的查找方法,从数据集合的第一个元素开始,逐个比较直到找到目标元素或遍历完所有元素。
算法特点:
- 时间复杂度:O(n)
- 空间复杂度:O(1)
- 适用场景:无序数据、小规模数据集
- 优势:实现简单,对数据组织无要求
- 劣势:效率低,不适合大规模数据
// 线性查找实现
public class LinearSearch {
// 基本线性查找
public static int linearSearch(int[] arr, int target) {
for (int i = 0; i < arr.length; i++) {
if (arr[i] == target) {
return i;
}
}
return -1; // 未找到
}
// 改进的线性查找 - 哨兵技术
public static int sentinelLinearSearch(int[] arr, int target) {
int n = arr.length;
int last = arr[n - 1];
arr[n - 1] = target; // 设置哨兵
int i = 0;
while (arr[i] != target) {
i++;
}
arr[n - 1] = last; // 恢复原值
if (i < n - 1 || arr[n - 1] == target) {
return i;
}
return -1;
}
// 查找所有匹配元素
public static List<Integer> findAll(int[] arr, int target) {
List<Integer> indices = new ArrayList<>();
for (int i = 0; i < arr.length; i++) {
if (arr[i] == target) {
indices.add(i);
}
}
return indices;
}
}
二分查找
二分查找是一种高效的查找算法,利用数据的有序性,每次将查找范围缩小一半。它是分治思想的经典应用。
算法特点:
- 时间复杂度:O(log n)
- 空间复杂度:O(1) 迭代版本,O(log n) 递归版本
- 前提条件:数据必须有序
- 适用场景:大规模有序数据集
二分查找的变种:
- 查找第一个等于目标值的位置
- 查找最后一个等于目标值的位置
- 查找第一个大于目标值的位置
- 查找最后一个小于目标值的位置
// 二分查找及其变种
public class BinarySearch {
// 标准二分查找
public static int binarySearch(int[] arr, int target) {
int left = 0, right = arr.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] == target) {
return mid;
} else if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1;
}
// 递归版本二分查找
public static int binarySearchRecursive(int[] arr, int target, int left, int right) {
if (left > right) return -1;
int mid = left + (right - left) / 2;
if (arr[mid] == target) {
return mid;
} else if (arr[mid] < target) {
return binarySearchRecursive(arr, target, mid + 1, right);
} else {
return binarySearchRecursive(arr, target, left, mid - 1);
}
}
// 查找第一个等于target的位置
public static int findFirst(int[] arr, int target) {
int left = 0, right = arr.length - 1;
int result = -1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] == target) {
result = mid;
right = mid - 1; // 继续向左查找
} else if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return result;
}
// 查找最后一个等于target的位置
public static int findLast(int[] arr, int target) {
int left = 0, right = arr.length - 1;
int result = -1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] == target) {
result = mid;
left = mid + 1; // 继续向右查找
} else if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return result;
}
// 在旋转排序数组中查找
public static int searchInRotatedArray(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
}
// 判断哪一半是有序的
if (nums[left] <= nums[mid]) {
// 左半部分有序
if (nums[left] <= target && target < nums[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
} else {
// 右半部分有序
if (nums[mid] < target && target <= nums[right]) {
left = mid + 1;
} else {
right = mid - 1;
}
}
}
return -1;
}
}
高级查找
哈希查找
哈希查找通过哈希函数将键映射到存储位置,实现快速的数据检索。在理想情况下可以达到O(1)的查找时间。
哈希查找的核心要素:
- 哈希函数:将键转换为数组索引
- 冲突处理:处理不同键映射到相同位置的情况
- 负载因子控制:维持哈希表的性能
- 动态扩容:根据负载情况调整表大小
// 哈希查找实现
public class HashSearch {
// 简单哈希表实现
static class SimpleHashTable {
private static class Entry {
String key;
Integer value;
Entry next;
Entry(String key, Integer value) {
this.key = key;
this.value = value;
}
}
private Entry[] table;
private int size;
private int capacity;
private static final double LOAD_FACTOR = 0.75;
public SimpleHashTable(int initialCapacity) {
this.capacity = initialCapacity;
this.table = new Entry[capacity];
this.size = 0;
}
private int hash(String key) {
return Math.abs(key.hashCode()) % capacity;
}
// 查找操作 - O(1) 平均时间复杂度
public Integer search(String key) {
int index = hash(key);
Entry current = table[index];
while (current != null) {
if (current.key.equals(key)) {
return current.value;
}
current = current.next;
}
return null;
}
// 插入操作
public void put(String key, Integer value) {
if (size >= capacity * LOAD_FACTOR) {
resize();
}
int index = hash(key);
Entry current = table[index];
// 检查是否已存在
while (current != null) {
if (current.key.equals(key)) {
current.value = value;
return;
}
current = current.next;
}
// 插入新节点
Entry newEntry = new Entry(key, value);
newEntry.next = table[index];
table[index] = newEntry;
size++;
}
private void resize() {
Entry[] oldTable = table;
capacity *= 2;
table = new Entry[capacity];
size = 0;
for (Entry head : oldTable) {
while (head != null) {
put(head.key, head.value);
head = head.next;
}
}
}
}
}
树查找
树查找在有序的树结构中进行查找,利用树的性质快速定位目标元素。不同的树结构有不同的查找性能。
// 树查找实现
public class TreeSearch {
// 二叉搜索树查找
static class BSTSearch {
static class TreeNode {
int val;
TreeNode left, right;
TreeNode(int val) { this.val = val; }
}
// 递归查找
public static TreeNode search(TreeNode root, int target) {
if (root == null || root.val == target) {
return root;
}
if (target < root.val) {
return search(root.left, target);
} else {
return search(root.right, target);
}
}
// 迭代查找
public static TreeNode searchIterative(TreeNode root, int target) {
while (root != null && root.val != target) {
if (target < root.val) {
root = root.left;
} else {
root = root.right;
}
}
return root;
}
// 查找最小值
public static TreeNode findMin(TreeNode root) {
while (root != null && root.left != null) {
root = root.left;
}
return root;
}
// 查找最大值
public static TreeNode findMax(TreeNode root) {
while (root != null && root.right != null) {
root = root.right;
}
return root;
}
// 查找前驱节点
public static TreeNode findPredecessor(TreeNode root, int target) {
TreeNode predecessor = null;
while (root != null) {
if (target > root.val) {
predecessor = root;
root = root.right;
} else {
root = root.left;
}
}
return predecessor;
}
}
// Trie树查找(字典树)
static class TrieSearch {
static class TrieNode {
TrieNode[] children = new TrieNode[26];
boolean isEnd = false;
}
private TrieNode root;
public TrieSearch() {
root = new TrieNode();
}
// 插入单词
public void insert(String word) {
TrieNode current = root;
for (char c : word.toCharArray()) {
int index = c - 'a';
if (current.children[index] == null) {
current.children[index] = new TrieNode();
}
current = current.children[index];
}
current.isEnd = true;
}
// 查找完整单词
public boolean search(String word) {
TrieNode node = searchNode(word);
return node != null && node.isEnd;
}
// 查找前缀
public boolean startsWith(String prefix) {
return searchNode(prefix) != null;
}
private TrieNode searchNode(String word) {
TrieNode current = root;
for (char c : word.toCharArray()) {
int index = c - 'a';
if (current.children[index] == null) {
return null;
}
current = current.children[index];
}
return current;
}
// 获取所有以prefix开头的单词
public List<String> getWordsWithPrefix(String prefix) {
List<String> result = new ArrayList<>();
TrieNode prefixNode = searchNode(prefix);
if (prefixNode != null) {
dfs(prefixNode, prefix, result);
}
return result;
}
private void dfs(TrieNode node, String current, List<String> result) {
if (node.isEnd) {
result.add(current);
}
for (int i = 0; i < 26; i++) {
if (node.children[i] != null) {
dfs(node.children[i], current + (char)('a' + i), result);
}
}
}
}
}
高级查找技术
布隆过滤器(Bloom Filter):空间高效的概率性数据结构,用于快速判断元素是否可能存在。
跳表(Skip List):基于链表的数据结构,通过多层索引实现O(log n)的查找性能。
一致性哈希:解决分布式系统中数据分布和负载均衡问题。
// 布隆过滤器实现
public class BloomFilter {
private BitSet bitSet;
private int size;
private int hashFunctions;
public BloomFilter(int expectedElements, double falsePositiveRate) {
this.size = calculateOptimalSize(expectedElements, falsePositiveRate);
this.hashFunctions = calculateOptimalHashFunctions(expectedElements, size);
this.bitSet = new BitSet(size);
}
// 添加元素
public void add(String element) {
for (int i = 0; i < hashFunctions; i++) {
int hash = hash(element, i);
bitSet.set(Math.abs(hash % size));
}
}
// 查找元素(可能存在)
public boolean mightContain(String element) {
for (int i = 0; i < hashFunctions; i++) {
int hash = hash(element, i);
if (!bitSet.get(Math.abs(hash % size))) {
return false; // 确定不存在
}
}
return true; // 可能存在
}
private int hash(String element, int seed) {
return element.hashCode() * seed;
}
private int calculateOptimalSize(int n, double p) {
return (int) (-n * Math.log(p) / (Math.log(2) * Math.log(2)));
}
private int calculateOptimalHashFunctions(int n, int m) {
return (int) (m / n * Math.log(2));
}
}
查找算法性能对比:
| 查找算法 | 时间复杂度 | 空间复杂度 | 数据要求 | 适用场景 |
|---|---|---|---|---|
| 线性查找 | O(n) | O(1) | 无要求 | 小规模、无序数据 |
| 二分查找 | O(log n) | O(1) | 有序数组 | 大规模有序数据 |
| 哈希查找 | O(1) 平均 | O(n) | 无要求 | 快速查找、缓存 |
| BST查找 | O(log n) 平均 | O(n) | 无要求 | 动态数据、范围查询 |
| Trie查找 | O(m) | O(ALPHABET_SIZE * N * M) | 字符串 | 前缀匹配、自动补全 |
| 布隆过滤器 | O(k) | O(m) | 无要求 | 快速过滤、大数据去重 |
字符串处理
字符串处理是程序设计中的重要内容,涉及模式匹配、编辑距离、压缩等多个方面。字符串算法在文本处理、生物信息学、数据挖掘等领域有广泛应用。
基础字符串操作
基础字符串操作是所有高级字符串算法的基础,包括字符串的基本变换和统计操作。
字符串基础操作
// 基础字符串操作
public class BasicStringOperations {
// 字符串反转
public static String reverse(String s) {
char[] chars = s.toCharArray();
int left = 0, right = chars.length - 1;
while (left < right) {
char temp = chars[left];
chars[left] = chars[right];
chars[right] = temp;
left++;
right--;
}
return new String(chars);
}
// 回文判断
public static boolean isPalindrome(String s) {
// 忽略大小写和非字母数字字符
String cleaned = s.replaceAll("[^a-zA-Z0-9]", "").toLowerCase();
int left = 0, right = cleaned.length() - 1;
while (left < right) {
if (cleaned.charAt(left) != cleaned.charAt(right)) {
return false;
}
left++;
right--;
}
return true;
}
// 字符频率统计
public static Map<Character, Integer> countCharacters(String s) {
Map<Character, Integer> frequency = new HashMap<>();
for (char c : s.toCharArray()) {
frequency.put(c, frequency.getOrDefault(c, 0) + 1);
}
return frequency;
}
// 最长回文子串
public static String longestPalindrome(String s) {
if (s == null || s.length() < 2) return s;
int start = 0, maxLen = 1;
for (int i = 0; i < s.length(); i++) {
// 奇数长度回文
int len1 = expandAroundCenter(s, i, i);
// 偶数长度回文
int len2 = expandAroundCenter(s, i, i + 1);
int len = Math.max(len1, len2);
if (len > maxLen) {
maxLen = len;
start = i - (len - 1) / 2;
}
}
return s.substring(start, start + maxLen);
}
private static int expandAroundCenter(String s, int left, int right) {
while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
left--;
right++;
}
return right - left - 1;
}
// 字符串压缩
public static String compress(String s) {
if (s.length() <= 2) return s;
StringBuilder compressed = new StringBuilder();
int count = 1;
for (int i = 1; i < s.length(); i++) {
if (s.charAt(i) == s.charAt(i - 1)) {
count++;
} else {
compressed.append(s.charAt(i - 1));
if (count > 1) {
compressed.append(count);
}
count = 1;
}
}
// 添加最后一组
compressed.append(s.charAt(s.length() - 1));
if (count > 1) {
compressed.append(count);
}
return compressed.length() < s.length() ? compressed.toString() : s;
}
}
字符串匹配基础
暴力匹配算法是最直观的字符串匹配方法,虽然效率不高,但易于理解和实现。
// 暴力字符串匹配
public class BruteForceStringMatch {
// 基本暴力匹配
public static int bruteForceSearch(String text, String pattern) {
int n = text.length();
int m = pattern.length();
for (int i = 0; i <= n - m; i++) {
int j = 0;
while (j < m && text.charAt(i + j) == pattern.charAt(j)) {
j++;
}
if (j == m) {
return i; // 找到匹配
}
}
return -1; // 未找到
}
// 查找所有匹配位置
public static List<Integer> findAllMatches(String text, String pattern) {
List<Integer> matches = new ArrayList<>();
int n = text.length();
int m = pattern.length();
for (int i = 0; i <= n - m; i++) {
if (text.substring(i, i + m).equals(pattern)) {
matches.add(i);
}
}
return matches;
}
}
高级字符串算法
高级字符串算法通过巧妙的预处理和优化策略,大幅提升字符串处理的效率。
KMP算法
**KMP算法(Knuth-Morris-Pratt)**是一种高效的字符串模式匹配算法,通过预处理模式串构建失效函数,避免不必要的字符比较。
KMP算法的核心思想:
- 失效函数:记录模式串中前缀和后缀的最长相等长度
- 跳跃策略:匹配失败时,根据失效函数跳跃到合适位置
- 时间复杂度:O(n + m),其中n是文本长度,m是模式串长度
// KMP算法实现
public class KMPAlgorithm {
// 构建失效函数(部分匹配表)
private static int[] buildFailureFunction(String pattern) {
int m = pattern.length();
int[] failure = new int[m];
int j = 0;
for (int i = 1; i < m; i++) {
while (j > 0 && pattern.charAt(i) != pattern.charAt(j)) {
j = failure[j - 1];
}
if (pattern.charAt(i) == pattern.charAt(j)) {
j++;
}
failure[i] = j;
}
return failure;
}
// KMP搜索
public static int kmpSearch(String text, String pattern) {
int n = text.length();
int m = pattern.length();
if (m == 0) return 0;
if (m > n) return -1;
int[] failure = buildFailureFunction(pattern);
int j = 0; // pattern的索引
for (int i = 0; i < n; i++) { // text的索引
while (j > 0 && text.charAt(i) != pattern.charAt(j)) {
j = failure[j - 1]; // 跳跃
}
if (text.charAt(i) == pattern.charAt(j)) {
j++;
}
if (j == m) {
return i - m + 1; // 找到匹配
}
}
return -1; // 未找到
}
// 查找所有匹配位置
public static List<Integer> kmpSearchAll(String text, String pattern) {
List<Integer> matches = new ArrayList<>();
int n = text.length();
int m = pattern.length();
if (m == 0 || m > n) return matches;
int[] failure = buildFailureFunction(pattern);
int j = 0;
for (int i = 0; i < n; i++) {
while (j > 0 && text.charAt(i) != pattern.charAt(j)) {
j = failure[j - 1];
}
if (text.charAt(i) == pattern.charAt(j)) {
j++;
}
if (j == m) {
matches.add(i - m + 1);
j = failure[j - 1]; // 继续查找下一个匹配
}
}
return matches;
}
}
Rabin-Karp算法
Rabin-Karp算法使用滚动哈希技术进行字符串匹配,特别适用于多模式匹配。
// Rabin-Karp算法实现
public class RabinKarpAlgorithm {
private static final int PRIME = 101; // 用于哈希计算的质数
public static int rabinKarpSearch(String text, String pattern) {
int n = text.length();
int m = pattern.length();
if (m > n) return -1;
long patternHash = calculateHash(pattern, m);
long textHash = calculateHash(text, m);
for (int i = 0; i <= n - m; i++) {
// 如果哈希值相等,进行字符比较
if (patternHash == textHash && checkEqual(text, i, pattern)) {
return i;
}
// 计算下一个窗口的哈希值
if (i < n - m) {
textHash = recalculateHash(text, i, i + m, textHash, m);
}
}
return -1;
}
private static long calculateHash(String str, int length) {
long hash = 0;
for (int i = 0; i < length; i++) {
hash += str.charAt(i) * Math.pow(PRIME, i);
}
return hash;
}
private static long recalculateHash(String str, int oldIndex, int newIndex,
long oldHash, int patternLength) {
long newHash = oldHash - str.charAt(oldIndex);
newHash = newHash / PRIME;
newHash += str.charAt(newIndex) * Math.pow(PRIME, patternLength - 1);
return newHash;
}
private static boolean checkEqual(String text, int start, String pattern) {
for (int i = 0; i < pattern.length(); i++) {
if (text.charAt(start + i) != pattern.charAt(i)) {
return false;
}
}
return true;
}
}
编辑距离
**编辑距离(Levenshtein Distance)**衡量两个字符串之间的相似度,表示将一个字符串转换为另一个字符串所需的最少编辑操作次数。
编辑操作包括:
- 插入一个字符
- 删除一个字符
- 替换一个字符
// 编辑距离算法
public class EditDistance {
// 计算编辑距离
public static int minDistance(String word1, String word2) {
int m = word1.length();
int n = word2.length();
// dp[i][j] 表示 word1[0...i-1] 转换为 word2[0...j-1] 的最小操作数
int[][] dp = new int[m + 1][n + 1];
// 初始化边界条件
for (int i = 0; i <= m; i++) {
dp[i][0] = i; // 删除所有字符
}
for (int j = 0; j <= n; j++) {
dp[0][j] = j; // 插入所有字符
}
// 填充DP表
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1]; // 字符相同,不需要操作
} else {
dp[i][j] = 1 + Math.min(
Math.min(dp[i - 1][j], // 删除
dp[i][j - 1]), // 插入
dp[i - 1][j - 1] // 替换
);
}
}
}
return dp[m][n];
}
// 空间优化版本
public static int minDistanceOptimized(String word1, String word2) {
int m = word1.length();
int n = word2.length();
// 只需要两行
int[] prev = new int[n + 1];
int[] curr = new int[n + 1];
// 初始化第一行
for (int j = 0; j <= n; j++) {
prev[j] = j;
}
for (int i = 1; i <= m; i++) {
curr[0] = i;
for (int j = 1; j <= n; j++) {
if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
curr[j] = prev[j - 1];
} else {
curr[j] = 1 + Math.min(Math.min(prev[j], curr[j - 1]), prev[j - 1]);
}
}
// 交换数组
int[] temp = prev;
prev = curr;
curr = temp;
}
return prev[n];
}
// 获取编辑操作序列
public static List<String> getEditOperations(String word1, String word2) {
int m = word1.length();
int n = word2.length();
int[][] dp = new int[m + 1][n + 1];
// 构建DP表(同上)
for (int i = 0; i <= m; i++) dp[i][0] = i;
for (int j = 0; j <= n; j++) dp[0][j] = j;
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = 1 + Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]);
}
}
}
// 回溯操作序列
List<String> operations = new ArrayList<>();
int i = m, j = n;
while (i > 0 || j > 0) {
if (i > 0 && j > 0 && word1.charAt(i - 1) == word2.charAt(j - 1)) {
i--; j--;
} else if (i > 0 && j > 0 && dp[i][j] == dp[i - 1][j - 1] + 1) {
operations.add(0, "Replace " + word1.charAt(i - 1) + " with " + word2.charAt(j - 1));
i--; j--;
} else if (i > 0 && dp[i][j] == dp[i - 1][j] + 1) {
operations.add(0, "Delete " + word1.charAt(i - 1));
i--;
} else {
operations.add(0, "Insert " + word2.charAt(j - 1));
j--;
}
}
return operations;
}
}
最长公共子序列
**最长公共子序列(LCS)**是两个序列中按相对顺序出现的最长子序列。
// 最长公共子序列
public class LongestCommonSubsequence {
// 计算LCS长度
public static int longestCommonSubsequence(String text1, String text2) {
int m = text1.length();
int n = text2.length();
int[][] dp = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[m][n];
}
// 获取LCS字符串
public static String getLCS(String text1, String text2) {
int m = text1.length();
int n = text2.length();
int[][] dp = new int[m + 1][n + 1];
// 构建DP表
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
// 回溯构建LCS
StringBuilder lcs = new StringBuilder();
int i = m, j = n;
while (i > 0 && j > 0) {
if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
lcs.insert(0, text1.charAt(i - 1));
i--; j--;
} else if (dp[i - 1][j] > dp[i][j - 1]) {
i--;
} else {
j--;
}
}
return lcs.toString();
}
}
字符串算法性能对比:
| 算法 | 时间复杂度 | 空间复杂度 | 适用场景 | 特点 |
|---|---|---|---|---|
| 暴力匹配 | O(nm) | O(1) | 简单匹配 | 实现简单,效率低 |
| KMP算法 | O(n + m) | O(m) | 单模式匹配 | 预处理模式串,跳跃匹配 |
| Rabin-Karp | O(n + m) 平均 | O(1) | 多模式匹配 | 滚动哈希,适合多模式 |
| 编辑距离 | O(nm) | O(nm) | 字符串相似度 | 动态规划,衡量差异 |
| LCS | O(nm) | O(nm) | 序列比较 | 找出公共部分 |
大数据算法
大数据算法专门处理无法完全加载到内存中的海量数据,需要特殊的设计思想和实现技术。这些算法在搜索引擎、推荐系统、数据分析等领域发挥着重要作用。
外排序
当数据量超过内存容量时,需要使用外排序算法,将大文件分割成可以放入内存的小块,分别排序后再合并。
多路归并排序
外排序的核心是多路归并,通过维护多个已排序的小文件,逐步合并成最终的有序结果。外排序分为两个阶段:
- 分割排序阶段:将大文件分成多个小文件,每个小文件在内存中排序
- 多路归并阶段:使用优先队列同时读取多个已排序文件,合并输出
外排序的应用场景:
- 大文件排序:处理几GB到几TB的数据文件
- 数据库排序:数据库管理系统的外部排序
- 日志处理:对大量日志文件进行排序分析
- 数据预处理:为后续算法准备有序数据
海量数据处理
海量数据处理需要特殊的算法和数据结构,以在有限的资源下处理大规模数据。
Top-K问题
Top-K问题是找出海量数据中最大(或最小)的K个元素,使用小顶堆可以高效解决此问题。
Top-K问题的变种:
- 静态Top-K:一次性处理所有数据
- 流式Top-K:持续处理数据流
- 分布式Top-K:在多台机器上并行处理
// Top-K问题核心实现
public class TopKSolution {
// 基础Top-K实现
public static List<Integer> findTopK(int[] nums, int k) {
PriorityQueue<Integer> minHeap = new PriorityQueue<>(k);
for (int num : nums) {
if (minHeap.size() < k) {
minHeap.offer(num);
} else if (num > minHeap.peek()) {
minHeap.poll();
minHeap.offer(num);
}
}
return new ArrayList<>(minHeap);
}
// 流式Top-K处理器
static class StreamTopK {
private PriorityQueue<Integer> heap;
private int k;
public StreamTopK(int k) {
this.k = k;
this.heap = new PriorityQueue<>(k);
}
public void add(int value) {
if (heap.size() < k) {
heap.offer(value);
} else if (value > heap.peek()) {
heap.poll();
heap.offer(value);
}
}
public List<Integer> getTopK() {
return new ArrayList<>(heap);
}
}
}
布隆过滤器详解
布隆过滤器是一种空间高效的概率性数据结构,用于快速判断元素是否可能存在于集合中。它的核心特点是:
- 无假阴性:如果布隆过滤器说元素不存在,那么元素一定不存在
- 可能有假阳性:如果说元素存在,元素可能实际不存在
- 空间效率高:使用位数组存储,空间占用远小于哈希表
布隆过滤器的应用:
- 缓存系统:避免缓存穿透,快速判断数据是否可能在缓存中
- 数据库查询优化:预先过滤不存在的查询
- 网络爬虫:判断URL是否已被访问过
- 分布式系统:减少不必要的网络请求
一致性哈希
一致性哈希解决分布式系统中的数据分布问题,在节点增减时最小化数据迁移。
一致性哈希的优势:
- 最小化数据迁移:添加/删除节点时,只需迁移少量数据
- 负载均衡:通过虚拟节点技术实现负载均衡
- 容错性好:单个节点故障不影响整个系统
- 扩展性强:支持动态添加和删除节点
一致性哈希的应用:
- 分布式缓存:Redis Cluster、Memcached集群
- 分布式存储:Cassandra、DynamoDB
- 负载均衡:一致性哈希负载均衡算法
- CDN系统:内容分发网络的节点选择
其他重要的大数据技术
HyperLogLog:用于基数估计的概率性数据结构,可以在使用很少内存的情况下估算大数据集的唯一元素个数。
Count-Min Sketch:用于频率估计的概率性数据结构,可以估算数据流中元素的出现频率。
MapReduce模式:分而治之的计算模式,通过Map和Reduce两个阶段处理大规模数据集。
分布式哈希表(DHT):在分布式环境中实现哈希表功能,支持数据的分布式存储和查找。
大数据算法性能对比:
| 算法/技术 | 主要用途 | 空间复杂度 | 时间复杂度 | 准确性 | 适用场景 |
|---|---|---|---|---|---|
| 外排序 | 大文件排序 | O(n) | O(n log n) | 精确 | 超大文件排序 |
| Top-K堆 | 找最大/小K个元素 | O(k) | O(n log k) | 精确 | 排行榜、推荐系统 |
| 布隆过滤器 | 成员查询 | O(m) | O(k) | 概率性 | 缓存穿透防护 |
| 一致性哈希 | 分布式数据分布 | O(n) | O(log n) | 精确 | 分布式缓存 |
| HyperLogLog | 基数估计 | O(1) | O(1) | 概率性 | UV统计、去重计数 |
| Count-Min Sketch | 频率估计 | O(ε⁻¹ log δ⁻¹) | O(log δ⁻¹) | 概率性 | 热点数据统计 |
| MapReduce | 分布式计算 | 取决于具体实现 | 取决于具体实现 | 精确 | 大数据批处理 |
总结
算法与数据结构是计算机科学的核心基础,掌握这些知识对于编写高效程序至关重要。本文档涵盖了从基础数据结构到高级算法设计思想的完整知识体系:
核心要点:
- 数据结构选择:根据应用场景选择合适的数据结构是性能优化的基础
- 算法复杂度:理解时间和空间复杂度,在效率和资源之间做出权衡
- 设计思想:掌握动态规划、贪心、分治等思想,能够解决复杂问题
- 实践应用:将理论知识应用到实际项目中,解决真实的工程问题
学习建议:
- 循序渐进:从基础数据结构开始,逐步学习高级算法
- 理论结合实践:不仅要理解算法原理,更要动手实现和优化
- 多做练习:通过大量练习巩固知识,提高编程能力
- 关注应用:了解各种算法的实际应用场景和优化技巧
通过系统学习本文档内容,能够建立完整的算法与数据结构知识体系,为后续的软件开发和技术面试打下坚实基础。