24. 算法与数据结构

目录

点击展开目录

基础数据结构

数据结构是计算机科学中组织和存储数据的方式,它决定了数据的访问效率和操作复杂度。选择合适的数据结构是算法优化的基础,不同的数据结构适用于不同的应用场景。

数组 (Array)

数组是最基础的数据结构,在内存中连续存储相同类型的元素。数组的核心特点是支持随机访问,即可以通过索引在O(1)时间内访问任意元素。

graph TD subgraph "数组内存布局" A["内存地址: 1000"] --> B["arr[0] = 10"] C["内存地址: 1004"] --> D["arr[1] = 20"] E["内存地址: 1008"] --> F["arr[2] = 30"] G["内存地址: 1012"] --> H["arr[3] = 40"] I["内存地址: 1016"] --> J["arr[4] = 50"] end subgraph "随机访问公式" K["访问arr[i]"] --> L["地址 = base_address + i * element_size"] L --> M["时间复杂度: O(1)"] end
graph LR subgraph "数组插入操作流程" N["原数组: [10,20,30,40]"] --> O["在索引1插入15"] O --> P["移动元素: 20→30→40"] P --> Q["插入新元素: 15"] Q --> R["结果: [10,15,20,30,40]"] R --> S["时间复杂度: O(n)"] end

数组的优势:

  • 随机访问快速:通过索引直接计算内存地址
  • 内存连续:缓存友好,局部性原理优势明显
  • 空间效率高:没有额外的指针开销

数组的劣势:

  • 插入删除成本高:需要移动大量元素,时间复杂度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)

链表是一种线性数据结构,通过指针连接各个节点,每个节点包含数据和指向下一个节点的引用。与数组不同,链表的元素在内存中不连续存储。

graph LR subgraph "单向链表结构" A["Head"] --> B["Node1
data: 10
next: →"] B --> C["Node2
data: 20
next: →"] C --> D["Node3
data: 30
next: →"] D --> E["NULL"] end
graph LR subgraph "双向链表结构" F["NULL"] --> G["← prev
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
graph TD subgraph "链表插入操作流程" K["找到插入位置"] --> L["创建新节点"] L --> M["新节点.next = 当前节点.next"] M --> N["当前节点.next = 新节点"] N --> O["时间复杂度: O(1)"] end subgraph "链表删除操作流程" P["找到删除位置"] --> Q["前驱节点.next = 当前节点.next"] Q --> R["释放当前节点内存"] R --> S["时间复杂度: O(1)"] 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) 的数据结构,只能在栈顶进行插入和删除操作。栈在程序执行、表达式求值、括号匹配等场景中应用广泛。

graph TD subgraph "栈的LIFO特性" A["栈底"] --> B["元素1"] B --> C["元素2"] C --> D["元素3"] D --> E["栈顶 ← push/pop"] F["push操作"] --> G["在栈顶添加元素"] H["pop操作"] --> I["移除栈顶元素"] end
graph LR subgraph "栈操作流程演示" J["初始栈: []"] --> K["push(1)"] K --> L["栈: [1]"] L --> M["push(2)"] M --> N["栈: [1,2]"] N --> O["push(3)"] O --> P["栈: [1,2,3]"] P --> Q["pop()"] Q --> R["返回3, 栈: [1,2]"] R --> S["pop()"] S --> T["返回2, 栈: [1]"] end
graph TD subgraph "栈在括号匹配中的应用" U["输入: ((()))"] --> V["遇到'('时push"] V --> W["遇到')'时pop"] W --> X{{"栈是否为空?"}} X -->|是| Y["匹配成功"] X -->|否| Z["匹配失败"] AA["输入: (()"] --> BB["处理完毕"] BB --> CC{{"栈是否为空?"}} CC -->|否| DD["匹配失败(缺少右括号)"] end

栈的核心操作:

  • 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) 的数据结构,在队尾插入元素,在队头删除元素。队列广泛应用于任务调度、缓冲区管理和广度优先搜索等场景。

graph LR subgraph "队列的FIFO特性" A["队头(front)"] --> B["元素1"] B --> C["元素2"] C --> D["元素3"] D --> E["队尾(rear)"] F["dequeue ←"] --> A E --> G["← enqueue"] end
graph LR subgraph "队列操作流程演示" H["初始队列: []"] --> I["enqueue(A)"] I --> J["队列: [A]"] J --> K["enqueue(B)"] K --> L["队列: [A,B]"] L --> M["enqueue(C)"] M --> N["队列: [A,B,C]"] N --> O["dequeue()"] O --> P["返回A, 队列: [B,C]"] P --> Q["dequeue()"] Q --> R["返回B, 队列: [C]"] end
graph TD subgraph "循环队列结构" S["数组索引0"] --> T["数组索引1"] T --> U["数组索引2"] U --> V["数组索引3"] V --> W["数组索引4"] W --> S X["front指针"] --> Y["指向队头元素"] Z["rear指针"] --> AA["指向队尾下一位置"] BB["队列满条件"] --> CC["(rear + 1) % capacity == front"] DD["队列空条件"] --> EE["front == rear"] end

队列的分类:

  • 普通队列:基本的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)的平均查找时间。哈希表是现代编程中最重要的数据结构之一。

graph TD subgraph "哈希表基本结构" A["Key: 'apple'"] --> B["Hash Function"] B --> C["Index: 3"] C --> D["Array[3] = 'apple'"] E["Key: 'banana'"] --> F["Hash Function"] F --> G["Index: 1"] G --> H["Array[1] = 'banana'"] end
graph LR subgraph "链地址法处理冲突" I["Array[0]"] --> J["NULL"] K["Array[1]"] --> L["Node1: key1"] L --> M["Node2: key4"] M --> N["NULL"] O["Array[2]"] --> P["Node3: key2"] P --> Q["NULL"] R["Array[3]"] --> S["Node4: key3"] S --> T["Node5: key6"] T --> U["NULL"] end
graph TD subgraph "开放寻址法处理冲突" V["插入key,hash(key)=2"] --> W["检查Array[2]"] W --> X{{"Array[2]是否为空?"}} X -->|是| Y["直接插入Array[2]"] X -->|否| Z["线性探测:检查Array[3]"] Z --> AA{{"Array[3]是否为空?"}} AA -->|是| BB["插入Array[3]"] AA -->|否| CC["继续探测Array[4]..."] end
graph LR subgraph "哈希表动态扩容流程" DD["负载因子 > 0.75"] --> EE["创建新数组(2倍大小)"] EE --> FF["重新计算所有元素的哈希值"] FF --> GG["将元素迁移到新数组"] GG --> HH["释放旧数组"] HH --> II["扩容完成"] end

哈希表的核心概念:

  • 哈希函数:将键转换为数组索引的函数
  • 冲突处理:处理不同键映射到相同索引的情况
  • 负载因子:已使用槽位与总槽位的比值
  • 动态扩容:当负载因子过高时扩大数组容量

冲突处理方法:

  1. 链地址法:每个槽位维护一个链表存储冲突元素
  2. 开放寻址法:在数组中寻找下一个空槽位
    • 线性探测:逐个检查下一个位置
    • 二次探测:按平方数序列探测
    • 双重哈希:使用第二个哈希函数
// 链地址法哈希表实现
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)

堆是一种完全二叉树结构,满足堆性质:父节点的值总是大于等于(最大堆)或小于等于(最小堆)其子节点的值。堆主要用于实现优先队列和堆排序算法。

graph TD subgraph "最小堆结构示例" A["1"] --> B["3"] A --> C["2"] B --> D["7"] B --> E["8"] C --> F["4"] C --> G["5"] H["数组表示: [1,3,2,7,8,4,5]"] I["索引关系:"] J["父节点: (i-1)/2"] K["左子节点: 2*i+1"] L["右子节点: 2*i+2"] end
graph TD subgraph "堆的插入操作(上浮)" M["在数组末尾插入新元素"] --> N["比较新元素与父节点"] N --> O{{"新元素 < 父节点?"}} O -->|是| P["交换新元素与父节点"] P --> Q["继续向上比较"] Q --> N O -->|否| R["插入完成"] end
graph TD subgraph "堆的删除操作(下沉)" S["移除堆顶元素"] --> T["将最后一个元素移到堆顶"] T --> U["比较当前节点与子节点"] U --> V{{"存在更小的子节点?"}} V -->|是| W["与最小子节点交换"] W --> X["继续向下比较"] X --> U V -->|否| Y["删除完成"] end
graph LR subgraph "堆化过程演示" Z["无序数组: [4,1,3,2,16,9,10,14,8,7]"] --> AA["从最后一个非叶子节点开始"] AA --> BB["对每个节点执行下沉操作"] BB --> CC["最终得到最小堆: [1,2,3,4,7,9,10,14,8,16]"] end

堆的特点:

  • 完全二叉树:除最后一层外,其他层都完全填满
  • 堆序性质:父子节点之间的大小关系固定
  • 数组存储:可以用数组高效存储,不需要指针
  • 索引关系:对于索引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)。

graph TD subgraph "AVL树插入流程" A["插入新节点"] --> B["按BST规则插入"] B --> C["更新节点高度"] C --> D["检查平衡因子"] D --> E{"|BF| > 1?"} E -->|是| F["确定旋转类型"] E -->|否| G["插入完成"] F --> H["LL型: 右旋转"] F --> I["RR型: 左旋转"] F --> J["LR型: 左右旋转"] F --> K["RL型: 右左旋转"] H --> L["重新计算高度"] I --> L J --> L K --> L L --> G end
graph LR subgraph "AVL树旋转操作示例" M["不平衡节点A"] --> N["右旋转"] N --> O["新根节点B"] P["A(BF=-2)
\\
B(BF=-1)
\\
C"] --> Q["右旋转"] Q --> R[" B
/ \\
C A"] end

红黑树是近似平衡的二叉搜索树,通过颜色标记和旋转操作维护平衡。相比AVL树,红黑树的平衡条件较松,插入删除操作更高效。

graph TD subgraph "红黑树插入流程" S["插入新节点(红色)"] --> T["按BST规则插入"] T --> U{{"父节点是黑色?"}} U -->|是| V["插入完成"] U -->|否| W{{"叔叔节点是红色?"}} W -->|是| X["父节点和叔叔节点变黑
祖父节点变红"] X --> Y["祖父节点作为当前节点"] Y --> U W -->|否| Z["需要旋转"] Z --> AA{{"插入节点是父节点的右子?"}} AA -->|是| BB["左旋转父节点"] AA -->|否| CC["右旋转祖父节点"] BB --> CC CC --> DD["调整颜色"] DD --> V end
graph TD subgraph "红黑树性质" EE["红黑树五大性质"] --> FF["1. 节点是红色或黑色"] EE --> GG["2. 根节点是黑色"] EE --> HH["3. 叶子节点(NIL)是黑色"] EE --> II["4. 红色节点的子节点必须是黑色"] EE --> JJ["5. 从任一节点到叶子节点的
所有路径包含相同数目的黑节点"] end

特性对比:

graph TD subgraph "AVL树特性" A1["严格平衡"] --> A2["高度差 ≤ 1"] A2 --> A3["查找最优 O(log n)"] A3 --> A4["插入删除成本高"] end subgraph "红黑树特性" B1["近似平衡"] --> B2["黑高度相等"] B2 --> B3["查找良好 O(log n)"] B3 --> B4["插入删除高效"] end subgraph "应用场景" C1["AVL: 查找密集型"] --> C2["数据库索引"] C3["红黑树: 通用型"] --> C4["STL map/set"] C4 --> C5["Java TreeMap"] 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+树的每个节点可以包含多个键值,所有数据都存储在叶子节点中,内部节点只存储索引信息。

graph TD subgraph "B+树结构示例" A["根节点
[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
graph TD subgraph "B+树插入流程" I["插入键值对"] --> J["定位到叶子节点"] J --> K{{"叶子节点有空间?"}} K -->|是| L["直接插入"] K -->|否| M["分裂叶子节点"] M --> N["创建新叶子节点"] N --> O["分配键值对"] O --> P["更新链表指针"] P --> Q["向父节点插入分隔键"] Q --> R{{"父节点有空间?"}} R -->|是| S["插入完成"] R -->|否| T["递归分裂父节点"] T --> Q L --> S end
graph TD subgraph "B+树查询流程" U["开始查询"] --> V["从根节点开始"] V --> W["比较键值"] W --> X{{"是叶子节点?"}} X -->|否| Y["选择子节点"] Y --> V X -->|是| Z["在叶子节点中查找"] Z --> AA{{"找到键值?"}} AA -->|是| BB["返回数据"] AA -->|否| CC["返回未找到"] end
graph LR subgraph "B+树范围查询" DD["范围查询[20,80]"] --> EE["定位起始叶子节点"] EE --> FF["从20开始遍历"] FF --> GG["沿叶子链表扫描"] GG --> HH["直到80结束"] HH --> II["返回结果集"] end

B+树的核心特性:

  • 多路结构:每个节点包含多个键值,减少树的高度
  • 数据在叶子:所有实际数据都在叶子节点,内部节点只做索引
  • 叶子链表:叶子节点通过指针连接,支持高效范围查询
  • 磁盘友好:节点大小与磁盘页面大小匹配,减少I/O次数

B+树的优势:

  • 范围查询高效:叶子节点链表结构支持快速范围扫描
  • 磁盘I/O少:高扇出比减少树的层数,降低磁盘访问次数
  • 查询稳定:所有查询都到达叶子节点,性能一致
  • 并发友好:叶子节点分离,便于实现锁机制
graph TD subgraph "B+树结构特点" B1["多路搜索树"] --> B2["内部节点存索引"] B2 --> B3["叶子节点存数据"] B3 --> B4["叶子节点链接"] B4 --> B5["支持范围查询"] end subgraph "磁盘优化" D1["节点大小=页面大小"] --> D2["减少I/O次数"] D2 --> D3["高扇出比"] --> D4["降低树高度"] end subgraph "应用场景" A1["数据库索引"] --> A2["InnoDB主键索引"] A3["文件系统"] --> A4["B+树目录结构"] end
// 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数据库。

graph TD subgraph "LSM-Tree整体架构" A["写入请求"] --> B["MemTable
(内存中的跳表)"] 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
graph TD subgraph "LSM-Tree写入流程" K["写入操作"] --> L["检查MemTable空间"] L --> M{{"空间足够?"}} M -->|是| N["直接写入MemTable"] M -->|否| O["触发Minor Compaction"] O --> P["MemTable → Immutable"] P --> Q["创建新的MemTable"] Q --> R["后台线程刷写SSTable"] R --> S["写入L0层"] N --> T["写入完成"] S --> U["检查是否需要Major Compaction"] U --> T end
graph TD subgraph "LSM-Tree读取流程" V["读取请求"] --> W["查找MemTable"] W --> X{{"找到数据?"}} X -->|是| Y["返回结果"] X -->|否| Z["查找Immutable MemTable"] Z --> AA{{"找到数据?"}} AA -->|是| Y AA -->|否| BB["按层级查找SSTable"] BB --> CC["检查Bloom Filter"] CC --> DD{{"可能存在?"}} DD -->|否| EE["查找下一层"] DD -->|是| FF["读取SSTable"] FF --> GG{{"找到数据?"}} GG -->|是| Y GG -->|否| EE EE --> HH{{"还有更多层?"}} HH -->|是| CC HH -->|否| II["返回未找到"] end
graph TD subgraph "LSM-Tree压缩流程" JJ["触发压缩条件"] --> KK["选择压缩层级"] KK --> LL["选择SSTable文件"] LL --> MM["多路归并排序"] MM --> NN["生成新的SSTable"] NN --> OO["更新层级信息"] OO --> PP["删除旧文件"] PP --> QQ["压缩完成"] RR["Minor Compaction"] --> SS["MemTable → SSTable"] TT["Major Compaction"] --> UU["多个SSTable合并"] end
graph LR subgraph "LSM-Tree性能特点" VV["写入性能"] --> WW["O(1) 写入MemTable
批量刷写磁盘"] XX["读取性能"] --> YY["可能需要查找多层
Bloom Filter优化"] ZZ["空间放大"] --> AAA["多版本数据存在
定期压缩清理"] BBB["写放大"] --> CCC["数据多次重写
压缩过程中"] end

LSM-Tree的设计思想:

  • 写入优化:所有写入都先进入内存,批量刷写到磁盘
  • 分层存储:数据分布在多个层次,上层小而新,下层大而旧
  • 合并压缩:定期将小文件合并成大文件,保持读取效率
  • 顺序I/O:利用磁盘顺序读写的高性能特性

LSM-Tree的核心组件详解:

MemTable(内存表)

MemTable是LSM-Tree中的核心内存数据结构,通常使用跳表(Skip List)实现,负责接收所有的写入操作。

graph TD subgraph "MemTable内部结构(跳表)" A["Level 3"] --> B["NULL"] C["Level 2"] --> D["Node(10)"] --> E["Node(30)"] --> F["NULL"] G["Level 1"] --> H["Node(10)"] --> I["Node(20)"] --> J["Node(30)"] --> K["NULL"] L["Level 0"] --> M["Node(10)"] --> N["Node(15)"] --> O["Node(20)"] --> P["Node(25)"] --> Q["Node(30)"] --> R["NULL"] end

跳表(Skip List)特点:

  • 多层索引结构:通过多层链表提供O(log n)的查找性能
  • 概率平衡:通过随机化避免复杂的平衡操作
  • 并发友好:相比红黑树更容易实现无锁并发
  • 有序存储:天然维护键值的有序性
graph TD subgraph "MemTable操作流程" S["写入请求"] --> T["计算键的哈希值"] T --> U["在跳表中定位位置"] U --> V{{"键已存在?"}} V -->|是| W["更新值(覆盖写)"] V -->|否| X["插入新节点"] X --> Y["随机决定层数"] Y --> Z["更新各层索引"] W --> AA["写入WAL日志"] Z --> AA AA --> BB["写入完成"] end

MemTable的关键特性:

  • 写入性能:O(log n)的插入时间复杂度
  • 查找性能:O(log n)的查找时间复杂度
  • 内存限制:通常限制大小为64MB-256MB
  • 线程安全:支持多线程并发读写
  • WAL保护:写入前先记录WAL日志保证持久性

Immutable MemTable(不可变内存表)

当MemTable达到大小限制时,会转换为Immutable MemTable,这是一个只读的内存结构。

graph TD subgraph "MemTable生命周期" CC["Active 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刷写到磁盘的第一层存储文件。

graph TD subgraph "L0 SSTable特殊性质" JJ["L0层特点"] --> KK["键范围可能重叠
(不同SSTable间)"] JJ --> LL["单个文件内部有序"] JJ --> MM["文件间可能无序"] JJ --> NN["直接从MemTable生成"] OO["查找过程"] --> PP["需要检查所有L0文件"] OO --> QQ["使用Bloom Filter优化"] OO --> RR["按时间戳排序检查"] end
graph TD subgraph "SSTable文件结构" SS["SSTable文件"] --> TT["数据块(Data Block)"] SS --> UU["索引块(Index Block)"] SS --> VV["过滤器块(Filter Block)"] SS --> WW["元数据块(Meta Block)"] SS --> XX["文件尾(Footer)"] TT --> TT1["存储实际键值对
按键排序"] UU --> UU1["存储数据块索引
快速定位"] VV --> VV1["Bloom Filter
快速判断键存在性"] WW --> WW1["统计信息
压缩信息等"] XX --> XX1["文件格式版本
校验和等"] end

L0 SSTable的关键特征:

  • 键范围重叠:不同L0文件的键范围可能重叠
  • 时间有序:文件按生成时间排序
  • 压缩触发:L0文件数量达到阈值时触发压缩
  • 读取复杂:查找时需要检查多个文件

Bloom Filter(布隆过滤器)

Bloom Filter是LSM-Tree中的重要优化组件,用于快速判断键是否可能存在。

graph TD subgraph "Bloom Filter工作原理" YY["键值Key"] --> ZZ["Hash函数1"] YY --> AAA["Hash函数2"] YY --> BBB["Hash函数3"] ZZ --> CCC["位置3"] AAA --> DDD["位置7"] BBB --> EEE["位置12"] CCC --> FFF["设置bit=1"] DDD --> FFF EEE --> FFF GGG["查询Key"] --> HHH["计算哈希位置"] HHH --> III{{"所有位置都是1?"}} III -->|否| JJJ["确定不存在"] III -->|是| KKK["可能存在
需进一步检查"] end

Bloom Filter在LSM-Tree中的应用:

  • 快速过滤:避免不必要的磁盘I/O操作
  • 假阳性:可能误判存在,但不会误判不存在
  • 内存高效:使用很少的内存空间
  • 多层应用:每个SSTable都有对应的Bloom Filter

层级结构(Leveled Structure)

LSM-Tree采用分层存储策略,数据从L0层逐步向下层合并,每一层都有特定的大小限制和特性。

graph TD subgraph "LSM-Tree层级结构详解" L0["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

层级特性对比:

层级文件数量键范围重叠总大小限制压缩策略查找复杂度
L04-8个允许重叠64MB-256MBSize-tieredO(文件数)
L1+变化严格不重叠按倍数增长LeveledO(1)

压缩策略(Compaction Strategies)

压缩是LSM-Tree维持性能的关键机制,通过合并小文件为大文件,清理过期数据,优化存储结构。

graph TD subgraph "压缩类型详解" Minor["Minor Compaction
MemTable → SSTable"] --> Minor1["• 将内存数据刷写到L0
• 不涉及数据合并
• 频率高,开销小
• 异步后台执行"] Major["Major Compaction
SSTable间合并"] --> Major1["• L0 → L1 合并
• Li → Li+1 合并
• 涉及数据去重
• 开销大,影响性能"] Full["Full Compaction
全层压缩"] --> Full1["• 整个层级重写
• 彻底清理垃圾数据
• 恢复最优存储
• 通常离线执行"] end
graph TD subgraph "Leveled Compaction详细流程" A["L0达到阈值
(如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写入前先记录日志。

graph TD subgraph "WAL工作机制" Write["写入请求"] --> WAL_Log["写入WAL日志"] WAL_Log --> WAL_Sync["同步到磁盘"] WAL_Sync --> MemTable_Write["写入MemTable"] MemTable_Write --> Response["返回成功"] Recovery["系统恢复"] --> WAL_Read["读取WAL日志"] WAL_Read --> Replay["重放操作"] Replay --> Rebuild["重建MemTable"] Rebuild --> WAL_Clean["清理WAL"] end

WAL的关键特性:

  • 顺序写入:WAL采用追加写入,性能优异
  • 持久性保证:写入WAL后数据不会丢失
  • 故障恢复:系统重启时通过WAL恢复数据
  • 定期清理:MemTable刷写后清理对应WAL
  • 压缩优化:定期压缩WAL文件减少空间占用

读写路径优化

graph TD subgraph "LSM-Tree读取优化策略" Read_Start["读取请求"] --> Cache_Check["检查Block Cache"] Cache_Check --> Cache_Hit{{"缓存命中?"}} Cache_Hit -->|是| Return_Cache["返回缓存数据"] Cache_Hit -->|否| Bloom_Check["检查Bloom Filter"] Bloom_Check --> Bloom_Result{{"可能存在?"}} Bloom_Result -->|否| Return_NotFound["返回未找到"] Bloom_Result -->|是| Index_Check["检查索引块"] Index_Check --> Data_Read["读取数据块"] Data_Read --> Cache_Update["更新缓存"] Cache_Update --> Return_Data["返回数据"] end
graph TD subgraph "LSM-Tree写入优化策略" Write_Start["写入请求"] --> Batch_Check["检查批量写入"] Batch_Check --> WAL_Write["批量写入WAL"] WAL_Write --> MemTable_Batch["批量写入MemTable"] MemTable_Batch --> Size_Check{{"MemTable满?"}} Size_Check -->|否| Write_Done["写入完成"] Size_Check -->|是| Async_Flush["异步刷写"] Async_Flush --> Write_Done end

性能优化技术:

  • Block Cache:缓存热点数据块,减少磁盘I/O
  • 批量写入:合并多个写入操作,提高吞吐量
  • 异步刷写:后台异步执行刷写操作,不阻塞写入
  • 压缩调度:智能调度压缩任务,平衡读写性能
  • 分区策略:按键范围分区,支持并行操作
graph TD subgraph "LSM-Tree架构" A1["写入请求"] --> A2["MemTable
(内存)"] 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-TreeO(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,批量刷写
        }
    }
}

选择决策树:

graph TD A["选择树结构"] --> B{"主要操作类型?"} B -->|读多写少| C{"数据规模?"} B -->|写多读少| D["LSM-Tree"] B -->|读写平衡| E{"存储介质?"} C -->|内存级别| F["AVL树/红黑树"] C -->|磁盘级别| G["B+树"] E -->|内存| H["红黑树"] E -->|磁盘| I["B+树"] F --> J["AVL:查找密集
红黑:通用场景"] G --> K["数据库索引
文件系统"] H --> L["STL容器
内存数据库"] I --> M["关系型数据库
分布式存储"] D --> N["NoSQL数据库
日志系统"]

高级树结构操作对比流程:

graph TD subgraph "插入操作对比" O["插入新节点"] --> P["AVL树: 严格平衡检查"] O --> Q["红黑树: 颜色调整"] O --> R["B+树: 节点分裂"] O --> S["LSM-Tree: 内存写入"] P --> P1["可能需要多次旋转"] Q --> Q1["最多3次旋转"] R --> R1["可能触发级联分裂"] S --> S1["批量刷写磁盘"] end
graph TD subgraph "查询操作对比" T["查询操作"] --> U["AVL树: O(log n)
最优查找"] 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
graph TD subgraph "应用场景决策" Y["选择合适的树结构"] --> Z{"需要严格平衡?"} Z -->|是| AA["AVL树
查找密集型"] 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)最短路径、层次遍历
graph TD subgraph "DFS特点" A1["深度优先"] --> A2["使用栈结构"] A2 --> A3["递归或迭代"] A3 --> A4["路径搜索"] A4 --> A5["环检测"] end subgraph "BFS特点" B1["广度优先"] --> B2["使用队列结构"] B2 --> B3["层次遍历"] B3 --> B4["最短路径"] B4 --> B5["连通分量"] end subgraph "选择策略" C1["需要最短路径"] --> C2["选择BFS"] C3["需要遍历所有路径"] --> C4["选择DFS"] C5["内存受限"] --> C6["选择DFS迭代"] end

排序算法

排序是将一组数据按照特定顺序重新排列的过程,是计算机科学中最基础和重要的算法之一。不同的排序算法有不同的时间复杂度、空间复杂度和稳定性特征,选择合适的排序算法对程序性能至关重要。

基础排序算法

基础排序算法虽然时间复杂度较高,但实现简单,适用于小规模数据或作为其他算法的组成部分。

冒泡排序 (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. 定义状态:确定用什么变量表示子问题
  2. 找出状态转移方程:子问题之间的关系
  3. 确定边界条件:最简单子问题的解
  4. 确定计算顺序:避免使用未计算的状态

经典动态规划问题

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在树上进行状态转移树的直径、打家劫舍IIIdp[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();
}

分治算法

分治算法将问题分解为若干个规模较小的子问题,递归解决子问题,然后合并子问题的解得到原问题的解。

分治算法的步骤:

  1. 分解:将原问题分解为若干子问题
  2. 解决:递归解决子问题
  3. 合并:将子问题的解合并为原问题的解
// 归并排序(分治经典例子)
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-KarpO(n + m) 平均O(1)多模式匹配滚动哈希,适合多模式
编辑距离O(nm)O(nm)字符串相似度动态规划,衡量差异
LCSO(nm)O(nm)序列比较找出公共部分

大数据算法

大数据算法专门处理无法完全加载到内存中的海量数据,需要特殊的设计思想和实现技术。这些算法在搜索引擎、推荐系统、数据分析等领域发挥着重要作用

外排序

当数据量超过内存容量时,需要使用外排序算法,将大文件分割成可以放入内存的小块,分别排序后再合并

多路归并排序

外排序的核心是多路归并,通过维护多个已排序的小文件,逐步合并成最终的有序结果。外排序分为两个阶段:

  1. 分割排序阶段:将大文件分成多个小文件,每个小文件在内存中排序
  2. 多路归并阶段:使用优先队列同时读取多个已排序文件,合并输出

外排序的应用场景:

  • 大文件排序:处理几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分布式计算取决于具体实现取决于具体实现精确大数据批处理

总结

算法与数据结构是计算机科学的核心基础,掌握这些知识对于编写高效程序至关重要。本文档涵盖了从基础数据结构到高级算法设计思想的完整知识体系:

核心要点:

  1. 数据结构选择:根据应用场景选择合适的数据结构是性能优化的基础
  2. 算法复杂度:理解时间和空间复杂度,在效率和资源之间做出权衡
  3. 设计思想:掌握动态规划、贪心、分治等思想,能够解决复杂问题
  4. 实践应用:将理论知识应用到实际项目中,解决真实的工程问题

学习建议:

  • 循序渐进:从基础数据结构开始,逐步学习高级算法
  • 理论结合实践:不仅要理解算法原理,更要动手实现和优化
  • 多做练习:通过大量练习巩固知识,提高编程能力
  • 关注应用:了解各种算法的实际应用场景和优化技巧

通过系统学习本文档内容,能够建立完整的算法与数据结构知识体系,为后续的软件开发和技术面试打下坚实基础。