1. 数据结构

1.1. 线性结构

1.1.1. 数组

  1. 结构图

    内存中连续存储,可随机访问。

  2. 常见操作

package datastructures.linear.array;

/**
 * 原始数组结构,处理插入和删除
 */
public class RawArray {

    /**
     * 数组插入元素,本地插入。
     *
     * @param array 数组
     * @param index 插入数据的下标
     * @param value 插入数据的值
     * @param size  当前数组有效元素的个数
     */
    public void insert(int[] array, int index, int value, int size) {
        if (size >= array.length || index < 0 || index > size) {
            throw new IllegalArgumentException("插入失败:索引非法或数组已满");
        }
        for (int i = size - 1; i >= index; i--) {
            array[i + 1] = array[i];
        }
        array[index] = value;
    }

    /**
     * 数组删除目标元素,本地删除。
     *
     * @param a     数组
     * @param index 下表
     * @param size  当前数组有效元素个数
     */
    public void delete(int[] a, int index, int size) {
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("删除失败:索引非法");
        }
        for (int i = index; i < size; i++) {
            a[i] = a[i + 1];
        }

    }

    public static void main(String[] args) {
        int[] array = {0, 1, 2, 3, 4, 5, 6, 0, 0, 0, 0};
        RawArray ra = new RawArray();
        ra.insert(array, 4, 10, 7);
        for (int i = 0; i < array.length; i++) {
            System.out.println(array[i]);
        }

        System.out.println("--------------");

        ra.delete(array, 3, 8);
        for (int i = 0; i < array.length; i++) {
            System.out.println(array[i]);
        }
    }
}

误区纠正:自己写的时候,没有传入size,结果代码始终有问题。我原来的理解是,使用数组的长度 array.length 就可以了。但在移动数组时,会出现越界的情况。经过调查,我明白了,必须在代码中明确的标出当前已经使用了多少个元素,即size。并用size作为循环的边界。在ArrayList中,也是如此,它声明了实例变量 size,是核心属性之一。

package datastructures.linear.array;

import java.util.ArrayList;

public class UseArrayList {
    public static void main(String[] args) {
        ArrayList<Integer> arrayList = new ArrayList<>();
        arrayList.add(0);
        arrayList.add(3, 10);  // 当列表中没有索引为2的数就插入索引为3的数时会报错 IndexOutOfBoundsException
        arrayList.add(1, 5);
        System.out.println("插入后:" + arrayList);

        arrayList.remove(0);
        System.out.println("删除后:" + arrayList);
    }
}
// 源码片段(简化自 java.util.ArrayList):
Object[] elementData;  // 底层数组
int size;              // 实际存储元素的个数

public boolean add(E e) {
    ensureCapacity(size + 1);// 自动扩容逻辑,这里的扩容扩的是已经使用的元素个数,而不是数组大小。
    elementData[size++] = e;
    return true;
}

1.1.2. 链表

1.1.2.1. 单链表

  1. 结构图

  2. 代码表示

class Node {
    int val;        // 数据
    Node next;      // 指向下一个节点

    public Node(int val) {
        this.val = val;
    }
}
  1. 常见操作

package datastructures.linear.linkedlist;

public class MyLinkedList {

    class Node {
        int val;
        Node next;

        public Node(int val) {
            this.val = val;
        }
    }

    private Node head; // 头节点

    public void insert(int val) {
        Node newNode = new Node(val);
        if (head == null) {
            head = newNode;
        }

        Node cur = head;
        while (cur.next != null) {
            cur = cur.next;
        }
        cur.next = newNode;
    }

    public void deleteAt(int index) {
        if (head == null) return;

        if (index == 0) {
            head = head.next;
            return;
        }

        Node cur = head;
        for (int i = 0; i < index - 1 && cur != null; i++) {
            cur = cur.next;
        }

        if (cur == null || cur.next == null) {
            throw new IndexOutOfBoundsException("删除位置不合法");
        }
        cur.next = cur.next.next;
    }

    public void delete(Node node) {
        if (head == node) {
            head = head.next;
            return;
        }

        Node cur = head;
        while (cur.next != null && cur.next != node) {
            cur = cur.next;
        }
        if (cur.next == node) {
            cur.next = node.next;
        }
    }
}

1.1.2.2. 双向链表

  1. 结构图

  2. 代码表示

class DNode {
    int val;
    DNode prev, next;

    public DNode(int val) {
        this.val = val;
    }
}
  1. 常见操作

1.1.2.3. 循环链表

1.1.2.4. 对比

特性

单链表(Singly Linked List)

双链表(Doubly Linked List)

循环链表(Circular Linked List)

每个节点包含

值 + next

值 + next + prev

可是单向或双向节点 + 环形连接

是否可逆

是(通过 prev

单向不可逆,双向可逆

最后一个指向

null

null

指向头节点

插入/删除效率

需要遍历找到前驱节点

更高效(可从两端操作)

类似于对应类型

应用场景

简单场景,节省空间

双向遍历、LRU缓存、编辑器等

多人游戏、资源调度、循环播放等

1.1.3. 栈和队列

数据结构

├── Stack(栈)【LIFO】
│ └── 实现方式:
│ ├── 使用 Deque(推荐,如 ArrayDeque)
│ └── 老的 Stack 类(继承 Vector,不推荐)

├── Queue(队列)【FIFO】
│ ├── 普通队列
│ │ └── 实现类:
│ │ ├── LinkedList
│ │ └── ArrayDeque
│ ├── 优先队列(PriorityQueue)【按优先级出队】
│ └── 并发队列(ConcurrentLinkedQueue 等)

└── Deque(双端队列)【双向进出】
├── 可模拟 Stack(使用 push/pop)
├── 可模拟 Queue(使用 offerLast/pollFirst)
└── 实现类:
├── ArrayDeque(推荐)
└── LinkedList(功能全但性能略差)

继承关系

java.util.Collection (接口)
├── List (接口) [有序、可重复]
│ ├── ArrayList
│ ├── LinkedList
│ └── Vector
│ └── Stack(不推荐)

├── Set (接口) [无序、不可重复]
│ ├── HashSet
│ │ └── LinkedHashSet
│ └── SortedSet (接口)
│ └── NavigableSet (接口)
│ └── TreeSet

└── Queue (接口) [FIFO]
├── Deque (接口)
│ ├── LinkedList
│ └── ArrayDeque
├── PriorityQueue
└── BlockingQueue (接口,来自 java.util.concurrent)
└── LinkedBlockingQueue 等

1.1.3.1. 栈

Stack - First In Last Out(FILO)

Java中推荐使用 ArrayDeque

Stack 继承自 Vector,性能和线程安全机制不现代,不推荐使用。官方推荐用 Deque 来实现栈功能(如 ArrayDeque),性能更好。

  1. 结构图

  1. 常见操作

操作名

作用

说明

push(x)

入栈

将元素 x 压入栈顶

pop()

出栈

移除并返回栈顶元素

peek() / top()

查看栈顶

查看但不移除栈顶元素

isEmpty()

是否为空

判断栈是否为空

size()

栈大小

返回栈中元素个数

  1. 代码展示

import java.util.Deque;
import java.util.ArrayDeque;

public class StackExample {
    public static void main(String[] args) {
        Deque<Integer> stack = new ArrayDeque<>();

        stack.push(10); // 入栈
        stack.push(20);
        System.out.println(stack.peek()); // 查看栈顶 -> 20
        System.out.println(stack.pop());  // 出栈 -> 20
        System.out.println(stack.isEmpty()); // 是否为空 -> false
        System.out.println(stack.size());    // 元素数量 -> 1
    }
}

1.1.3.2. 队列

Queue - FIrst In First Out(FIFO)

Array or Doubly Linked List

  1. 结构图

  1. 常见操作

操作名

作用

说明

offer(x) / add(x)

入队

向队尾插入元素 x

poll() / remove()

出队

移除并返回队头元素

peek() / element()

查看队头

查看但不移除队头元素

isEmpty()

判空

判断队列是否为空

size()

大小

返回队列中元素个数

  1. 代码展示

import java.util.Queue;
import java.util.LinkedList;

public class QueueExample {
    public static void main(String[] args) {
        Queue<Integer> queue = new LinkedList<>();

        queue.offer(1); // 入队
        queue.offer(2);
        System.out.println(queue.peek()); // 查看队头 -> 1
        System.out.println(queue.poll()); // 出队 -> 1
        System.out.println(queue.isEmpty()); // 是否为空 -> false
        System.out.println(queue.size());    // 剩余元素个数 -> 1
    }
}

1.1.4. 优先队列

PriorityQueue - 优先队列

正常入、按照优先级出

1.1.4.1. 堆(heap)

  1. 定义和性质

    1. 结构性质:必须是完全二叉树,除了最后一层外,每层节点必须是满的。

    2. 堆序性质:父节点和子节点之间必须满足大小关系

      1. 最小堆(Min-Heap):每个父节点 ≤ 其左右子节点

      2. 最大堆(Max-Heap):每个父节点 ≥ 其左右子节点

      3. 堆不要求兄弟节点之间有大小关系

    3. 局部有序:只能保证堆顶全局最大/最小。

  1. 实现机制

实现方式

插入
(Insert)

取出堆顶
(Extract-Min/Max)

查找最小/最大值
(Peek)

合并两个堆
(Merge)

适用场景 / 特点

Binary Heap
(二叉堆)

O(log n)

O(log n)

O(1)

O(n)

最常用,数组实现,简单高效

Binomial Heap
(二项堆)

O(log n)

O(log n)

O(log n)

O(log n)

可高效合并多个堆

Fibonacci Heap
(斐波那契堆)

O(1) 摊还

O(log n) 摊还

O(1)

O(1) 摊还

理论最优,但实现复杂

Pairing Heap
(配对堆)

O(1) 摊还

O(log n) 摊还

O(1)

O(log n) 摊还

简化版 Fibonacci 堆

Skew Heap
(斜堆)

O(log n) 摊还

O(log n) 摊还

O(1)

O(log n) 摊还

自调整堆,简单

D-ary Heap
(d叉堆)

O(log_d n)

O(d * log_d n)

O(1)

O(n)

减少堆高,提高插入效率

Leftist Heap
(左倾堆)

O(log n)

O(log n)

O(1)

O(log n)

高效合并堆

补充说明:

  • Binary Heap(常用):Java 中 PriorityQueue 默认使用小顶二叉堆。

  • Fibonacci Heap:在某些图算法(如 Dijkstra, Prim)中理论上具有优势。

  • Pairing Heap:性能类似 Fibonacci Heap,且实现简单。

  • D-ary Heap:通常用在需要减少“堆化次数”的系统中,例如网络包处理。

  • Skew / Leftist / Binomial:适合频繁合并场景。

1.1.4.2. 优先队列(PriorityQueue)

实现方式

插入时间复杂度

删除最值时间复杂度

最值查询时间

特点和应用场景

Binary Heap
(二叉堆)

O(log n)

O(log n)

O(1)

最常用,Java PriorityQueue 的实现

Fibonacci Heap
(斐波那契堆)

O(1) 摊还

O(log n) 摊还

O(1)

理论最优,适用于 Dijkstra/Prim,实际较少用

Binomial Heap
(二项堆)

O(log n)

O(log n)

O(log n)

支持堆合并操作较快

Pairing Heap
(配对堆)

O(1) 摊还

O(log n) 摊还

O(1)

实现比斐波那契堆简单,性能接近

Unordered Array
(无序数组)

O(1)

O(n)

O(n)

插入快,取最值慢;适合插入多、删除少

Sorted Array
(有序数组)

O(n)

O(1)

O(1)

取最值快,插入慢;适合查询多、变动少

Unordered Linked List
(无序链表)

O(1)

O(n)

O(n)

插入快,查/删慢,易实现

Sorted Linked List
(有序链表)

O(n)

O(1)

O(1)

插入慢,取最值快

Balanced BST
(如 AVL / 红黑树)

O(log n)

O(log n)

O(log n)

元素有序,支持范围查询、删除中间值

  1. Java中的常见实现

类名

实现结构

特点

PriorityQueue<E>

小顶二叉堆(数组)

最常用的优先队列,非线程安全

PriorityBlockingQueue<E>

小顶堆 + 可重入锁

线程安全,支持并发环境

TreeSet<E> / TreeMap<K,V>

红黑树

有序集合,可实现复杂优先逻辑

  1. PriorityQueue 数据结构

Java 中,实现 PriorityQueue 用的是完全二叉树,而底层是一个数组。

  • 逻辑上:我们认为它是一个“完全二叉树”结构,用来维持堆的顺序关系(父节点 ≥ 或 ≤ 子节点)。

  • 实际实现上:用 数组(Array) 来高效地表示这个完全二叉树。

在数组中,结点与子结点的位置有确定的映射关系。假设根节点索引为 0,那么对于任意节点索引 i

节点

数组索引

数学表达式

左子节点

2i + 1

-

右子节点

2i + 2

-

父节点

(i - 1) / 2

(向下取整)

因此:

  • 不需要使用指针/引用;

  • 插入、删除、访问父子节点非常高效;

  • 内存连续,CPU 缓存友好。

  1. 常见操作

操作方法

含义

复杂度

说明

add(E e) / offer(E e)

插入元素

O(logN)

插入并调整堆结构

peek()

查看堆顶元素

O(1)

但不删除

poll()

取出并删除堆顶元素

O(logN)

删除最小(或最大)元素

remove(Object o)

删除指定元素

O(N)

会遍历查找该元素

contains(Object o)

是否包含某元素

O(N)

线性查找

size()

当前元素数量

O(1)

-

isEmpty()

队列是否为空

O(1)

-

clear()

清空队列

O(N)

-

toArray()

转换为数组

O(N)

-

  1. 代码

PriorityQueue 的常见操作

import java.util.PriorityQueue;

public class PriorityQueueExample {
    public static void main(String[] args) {
        // 默认最小堆
        PriorityQueue<Integer> pq = new PriorityQueue<>();

        // 插入元素
        pq.add(10);
        pq.add(5);
        pq.add(20);

        System.out.println("队顶元素 peek(): " + pq.peek()); // 输出 5

        // 取出并删除最小元素
        System.out.println("弹出元素 poll(): " + pq.poll()); // 输出 5
        System.out.println("再次查看队顶: " + pq.peek()); // 输出 10

        // 删除指定元素
        pq.remove(20);

        // 判空
        System.out.println("是否为空: " + pq.isEmpty());

        // 元素个数
        System.out.println("元素个数: " + pq.size());

        // 清空
        pq.clear();
    }
}

Java 中的 PriorityQueue 默认是最小堆,最大堆需要使用 Comparator 构造。

import java.util.*;

public class MaxHeapExample {
    public static void main(String[] args) {
        PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Comparator.reverseOrder());

        maxHeap.add(3);
        maxHeap.add(1);
        maxHeap.add(5);

        while (!maxHeap.isEmpty()) {
            System.out.println(maxHeap.poll());  // 输出顺序:5, 3, 1
        }
    }
}

1.2. 非线性结构

1.2.1. 树

1.2.1.1. 二叉搜索树

public class TreeNode {
    public int val;
    public TreeNode left;
    public TreeNode right;

    public TreeNode (int val) {
        this.val = val;
        this.left = null;
        this.right = null;
    }
}
1.2.1.1.1. 二叉树遍历
  1. 前序(Pre-Order):根-左-右

  2. 中序(In-Order):左-根-右

  3. 后序(Post-Order):左-右-根

class Traversal {

    public void preOrder(TreeNode node) {
        if (node == null) return;
        System.out.print(node.val + " ");
        preOrder(node.left);
        preOrder(node.right);
    }

    public void inOrder(TreeNode node) {
        if (node == null) return;
        inOrder(node.left);
        System.out.print(node.val + " ");
        inOrder(node.right);
    }

    public void postOrder(TreeNode node) {
        if (node == null) return;
        postOrder(node.left);
        postOrder(node.right);
        System.out.print(node.val + " ");
    }
}

1.2.1.2. AVL和红黑树

1.2.1.3. 字典树 Trie

  1. Trie 树的数据结构

    字典树的真实结构如下,它的每一个节点都是一个数组,数组的长度相同:

  2. Trie 树的核心思想
    空间换时间。
    利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。

  3. Trie 树的基本性质

    1. 根节点不包含字符,除根节点外每一个节点都只包含一个字符。

    2. 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。

    3. 每个节点的所有子节点包含的字符都不相同。

  1. 代码

package trie;

public class Trie {

    static final int ALPHABET_SIZE = 26;

    private TrieNode root = new TrieNode();

    /**
     * 插入单词
     */
    public void insert(String word) {
        if (word.isEmpty()) {
            return;
        }
        TrieNode node = root;
        for (char c : word.toCharArray()) {
            int i = c - 'a';
            if (node.children == null) {
                node.children = new TrieNode[ALPHABET_SIZE];
            }
            node = node.children[i];
        }
        node.isEndOfWord = true;
    }

    /**
     * 搜索单词是否存在
     */
    public boolean search(String word) {
        TrieNode trieNode = searchNode(word);
        return trieNode != null && trieNode.isEndOfWord;
    }

    private TrieNode searchNode(String str) {
        TrieNode node = root;
        for (char c : str.toCharArray()) {
            int i = c - 'a';
            if (node.children == null) {
                return null;
            }
            node = node.children[i];
        }
        return node;
    }

    class TrieNode {
        TrieNode[] children = new TrieNode[ALPHABET_SIZE];
        boolean isEndOfWord = false;
    }
}

1.2.2. 图

1.3. 其他

1.3.1. 哈希表

1.3.1.1. 哈希原理

  1. 什么是哈希

哈希是一种通过函数(Hash Function)将任意大小的输入映射为固定长度的输出的方式。

举例:

输入字符串:"Tom" → 哈希函数 → 输出:378402

这个输出称为:哈希值(Hash Code)

  1. 哈希的主要用途

  • 快速查找:通过哈希值可以快速定位数据(比如哈希表)

  • 数据校验:如文件完整性校验(如 MD5, SHA-256)

  • 分布式分片:哈希值用于数据分区

  • 去重:将数据转为哈希值快速判断是否重复

  1. 哈希冲突

不同的输入可能产生相同的哈希值,这称为哈希冲突。

例如:

"cat" → 123456
"tac" → 123456

在哈希表结构中,必须通过链表或树结构来解决冲突

1.3.1.2. 哈希表

哈希表是以数组为底层结构 + 哈希函数映射,实现接近 O(1) 的插入/查找效率。

1.3.1.3. Java中的哈希结构与树结构

结构

底层实现

是否有序

是否允许 null

查找效率

排序依据

HashMap

哈希表

key/value 可为 null

平均 O(1)

无顺序

TreeMap

红黑树

key 不可为 null

O(log n)

按 key 排序

HashSet

基于 HashMap

可为 null

平均 O(1)

无顺序

TreeSet

基于 TreeMap

不可为 null

O(log n)

按值排序

1.3.1.4. HashMap 工作机制简述

  1. put
    执行流程:

    1. hashCode():计算键的哈希值

    2. 扰动函数优化分布

    3. 找桶位置:hash % 数组长度

    4. 冲突:

      1. 没冲突:直接插入

      2. 冲突:链表插入(长度>8变红黑树)

    5. 查询时也只需遍历一个桶中的链表或树 → 快速查找

1.3.1.5. TreeMap 工作机制简述

TreeMap 是基于 红黑树 实现的映射容器。

特性:

  • 键是有序的

  • 插入/删除/查询时间复杂度:O(log n)

  • 可以用自定义 Comparator 控制顺序

TreeMap<String, Integer> map = new TreeMap<>();
map.put("apple", 3);
map.put("banana", 1);
map.put("cat", 2);

键自动升序排列(apple, banana, cat)

1.3.1.6. HashSet & TreeSet 是 Map 的简化封装

Set 类型

实现方式

本质结构

HashSet

基于 HashMap

每个元素作为 key,value 是常量

TreeSet

基于 TreeMap

每个元素作为 key,value 是常量

2. 算法

2.1. 基础算法

2.1.1. 排序

2.1.1.1. 归并排序

2.1.1.2. 快速排序

2.1.2. 查找

2.1.2.1. 顺序查找

2.1.2.2. 二分查找

  1. 条件

    1. 单调递增或者递减(Sorted)

    2. 存在上下界(Bounded)

    3. 能够通过索引访问(Accessible by index)

  1. 代码

public class BinarySearch {

    /**
     * 二分查找目标索引
     * 假定 a 是已经排序的
     */
    public int binarySearch(int[] a, int target) {
        int lo = 0, hi = a.length - 1;
        while (lo <= hi) {
            int mid = (hi - lo) / 2 + lo;
            if (a[mid] == target) {
                return mid;
            } else if (a[mid] < target) {
                lo = mid + 1;
            } else {
                hi = mid - 1;
            }
        }
        return -1;
    }
}

2.1.2.3. 哈希查找

2.1.2.4. 二叉搜索树

2.1.2.5. 平衡搜索树

2.1.2.6. 广度优先搜索(Breadth-First-Search)

import java.util.*;

public class Solution {
    public void bfs(int start, Map<Integer, List<Integer>> graph) {
        Queue<Integer> queue = new LinkedList<>();
        Set<Integer> visited = new HashSet<>();

        queue.offer(start);
        visited.add(start);

        while (!queue.isEmpty()) {
            int node = queue.poll();
            System.out.println("访问节点: " + node);

            for (int neighbor : graph.getOrDefault(node, new ArrayList<>())) {
                if (!visited.contains(neighbor)) {
                    queue.offer(neighbor);
                    visited.add(neighbor);
                }
            }
        }
    }
}
Queue<Integer> queue = new LinkedList<>();
Set<Integer> visited = new HashSet<>();

queue.offer(start);
visited.add(start);

while (!queue.isEmpty()) {
    int size = queue.size();  // 当前这一层的节点数量

    for (int i = 0; i < size; i++) {
        int node = queue.poll(); // 访问当前层的节点
        for (int neighbor : graph.getOrDefault(node, new ArrayList<>())) {
            if (!visited.contains(neighbor)) {
                queue.offer(neighbor);     // 下一层
                visited.add(neighbor);
            }
        }
    }

    // 当前层访问完毕,queue 只剩下一层的所有节点
}

2.1.2.7. 深度优先搜索(Depth-First-Search)

import java.util.*;

public class Solution {
    public void dfs(int node, Map<Integer, List<Integer>> graph, Set<Integer> visited) {
        if (visited.contains(node)) return;

        visited.add(node);
        System.out.println("访问节点:" + node);

        for (int neighbor : graph.getOrDefault(node, new ArrayList<>())) {
            dfs(neighbor, graph, visited);
        }
    }

    public void runDFS(int start, Map<Integer, List<Integer>> graph) {
        Set<Integer> visited = new HashSet<>();
        dfs(start, graph, visited);
    }
}

2.2. 算法范式

范式

特点

暴力枚举

穷举所有可能

分治

大问题拆成小问题,递归解决后合并

递归

问题用自身定义(可结合分治)

贪心

每一步做局部最优

回溯

尝试+撤回,解决组合/排列

动态规划(DP)

记住子问题结果避免重复计算

并查集/图论技巧

处理集合、连通性等问题

位运算

对数据位级操作,效率高

模拟/技巧

直接模拟问题流程

2.2.1. 递归(Recursion)

void recursion(参数) {
    // 1. 终止条件(Base Case)
    if (满足终止条件) {
        return;
    }

    // 2. 当前层处理逻辑(当前状态的操作)
    // ...

    // 3. 进入下一层(递归调用)
    recursion(变化后的参数);

    // 4. 如果需要,恢复状态(回溯时使用)
    // ...
}

递归三要素:

要素

说明

终止条件

避免死递归

当前操作

本层要处理的数据或决策

递归调用

对问题的更小子问题递归处理

顺序

题目

链接

1

斐波那契数

509

2

二叉树最大深度

104

3

全排列

46

4

子集

78

5

汉诺塔问题

自定义(也可在力扣Discuss找)

2.2.2. 分治(Divide and Conquer)

ReturnType divideAndConquer(ProblemType problem) {
    // 1. 终止条件(不能再分)
    if (problem规模足够小) {
        return 直接解决;
    }

    // 2. 拆分为子问题
    ProblemType subProblem1 = 拆分方式1(problem);
    ProblemType subProblem2 = 拆分方式2(problem);
    // 可以有多个子问题

    // 3. 递归解决子问题
    ReturnType result1 = divideAndConquer(subProblem1);
    ReturnType result2 = divideAndConquer(subProblem2);

    // 4. 合并子问题结果
    ReturnType result = merge(result1, result2);

    return result;
}

顺序

题目

说明

1

归并排序(手写)

基础且必须掌握

2

快速排序(手写)

理解分区和递归

3

最大子数组和(分治法写法)

理解“合并”含义

4

数组中第K大元素(快速选择)

分治快速定位

5

二叉树最大路径和

分治在树结构中的典型应用

2.2.3. 动态规划(Dynamic Programming)

2.2.3.1. 基本知识

  1. 动态规划是将复杂问题分解成子问题,利用子问题最优解构造整体最优解。

  2. 核心要素:最优子结构 + 重叠子问题

  3. 状态:问题的某一阶段的“局部情况”描述
    状态的定义:

    1. opt[n]:常用来表示“规模为 n 的问题的最优解”(opt = optimal)。

    2. dp[n]:一般表示“规模为 n 的子问题的解”(Dynamic Programming)。

  1. 状态转移方程:由子状态计算当前状态的方法
    当前状态的最优解,可以通过之前已计算的状态的最优解,经过某种“选择”操作,推导得到。
    状态转移方程:opt[n] = best_of(opt[n-1], opt[n-2], ...)

  2. 边界条件(Base Case):DP 初始值,最小子问题解

  3. 记忆化(Memoization)vs 迭代(Tabulation):递归 + 记忆化 -> 递推

2.2.3.2. 能否用DP

满足以下三个特征,就可以考虑用 DP:

  1. 问题可以分解成“子问题”

  • 比如整体问题的答案依赖于子问题的解。

  • 例:爬楼梯问题(爬第n阶 = 爬n-1 + 爬n-2)

  1. 子问题有“重叠”

  • 同一个子问题在递归/搜索中被多次计算。

  • 例:斐波那契数列,f(n) = f(n-1) + f(n-2),很多重复计算。

  1. 问题有“最优子结构”

  • 当前状态的最优解依赖于之前某个状态的最优解。

  • 例:背包问题,当前容量的最优选择依赖于更小容量的选择。

2.2.3.3. 解题思路

  1. 明确问题的“最优子结构”

  • 复杂问题能分解成规模更小的子问题。

  • 子问题的最优解能组合成原问题的最优解。

  1. 找出“状态”

  • 定义“状态”:用一个或多个变量表示当前子问题的规模或特征。

  • 每个状态代表一个子问题。

  1. 确定“状态转移方程”

  • 找出从子状态到当前状态的关系,即“dp方程”。

  • 这通常是表达当前状态的最优值是如何通过之前状态的最优值计算得来的。

  1. 确定“边界条件”或“初始状态”

  • 明确最小子问题的答案(base case),作为状态转移的起点。

  1. 选择计算顺序(自顶向下还是自底向上)

  • 自顶向下:带备忘录的递归(记忆化递归)。

  • 自底向上:循环计算所有状态,填表。

  1. 根据需要,设计状态压缩或空间优化

  • 例如,一维数组代替二维数组,减少空间复杂度。

2.2.3.4. 常见模板

  1. 一维线性DP(例如最大子序和、斐波那契数列)

int n = nums.length;
int[] dp = new int[n];
dp[0] = nums[0];
for (int i = 1; i < n; i++) {
    dp[i] = Math.max(nums[i], dp[i-1] + nums[i]);
}
return dp[n-1];
  1. 二维DP(例如最长公共子序列、矩阵路径)

int m = s1.length(), n = s2.length();
int[][] dp = new int[m+1][n+1];
for (int i = 1; i <= m; i++) {
    for (int j = 1; j <= n; j++) {
        if (s1.charAt(i-1) == s2.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];
  1. 背包类DP

for (int i = 1; i <= n; i++) {
    for (int j = 0; j <= capacity; j++) {
        if (weight[i] > j) {
            dp[i][j] = dp[i-1][j];
        } else {
            dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i]);
        }
    }
}
  1. 状态压缩(一维滚动数组优化)

int[] dp = new int[capacity+1];
for (int i = 1; i <= n; i++) {
    for (int j = capacity; j >= weight[i]; j--) {
        dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
    }
}
  1. 记忆化递归(自顶向下)

Map<Integer, Integer> memo = new HashMap<>();
public int dfs(int state) {
    if (memo.containsKey(state)) return memo.get(state);
    // 计算状态结果
    memo.put(state, result);
    return result;
}

2.2.3.5. 分类

2.2.4. 贪心算法(Greedy Algorithm)

贪心算法:在对问题求解时,总是做出在当前看来最好的选择。

使用场景:

简单地说,问题能够分解成子问题解决,子问题的最优解能地推到最终问题的最优解。这种子问题最优解成为最优子结构。

贪心算法与动态规划的不同在于它对每个子问题的解决方案都做出选择,不能回退。动态规划则会保存以前的运算结果,并根据以前的结果对当前进行选择,有回退功能。

2.2.5. 回溯算法(Backtracking)

3. 优化技巧

3.1. 位运算

3.1.1. 位运算介绍

程序中的所有数在计算机内存中都是以二进制的形式存储的。位运算说穿了,就是直接对整数在内存中的二进制位进行操作。比如,and 运算本来是一个逻辑运算符,但整数与整数之间也可以进行and运算。举个例子,6 的二进制是 110,11 的二进制是 1011,那么 6 and 11 的结果就是 2,它是二进制对应位进行逻辑运算的结果(0表示False,1表示True,空位都当0处理):

110 AND 1011 -->  0010(b) --> 2(d)

由于位运算是直接对内存数据进行操作,不需要转成十进制,因此处理速度非常快。

3.1.2. 位运算常用操作

符号

描述

运算规则

&

两个位都为1时,结果才为1

|

两个位都为0时,结果才为0

^

异或

两个位相同为0,相异为1

~

取反

0变1,1变0

<<

左移

各二进位全部左移若干位,高位丢弃,低位补0

>>

右移

各二进位全部右移若干位,对无符号数,高位补0,有符号数,各编译器处理方法不一样,有的补符号位(算术右移),有的补0(逻辑右移)

>>>

无符号右移

高位补0(Java特有)

XOR - 异或

异或:相同为0,不同位1。也可用【不进位加法】来理解。

异或操作的一些特点:

x ^ 0 = x

x ^ 1s = ~x // 1s = ~0

x ^ (~x) = 0

a ^ b = c => a ^ c = b, b ^ c = a // 交换

a ^ b ^ c = a ^ ( b ^ c) = ( a ^ b) ^ c

3.1.3. 位运算的应用

3.1.3.1. 经典应用

  1. 判断奇偶

if ((n & 1) == 1) // 奇数
if ((n & 1) == 0) // 偶数
  1. 快速乘除以 2 的幂

n << 1  // 等价于 n * 2
n >> 1  // 等价于 n / 2(取整)
  1. 交换两个数(不使用临时变量)

a = a ^ b;
b = a ^ b;
a = a ^ b;
  1. 只出现一次的数字

int res = 0;
for (int num : nums) {
    res ^= num;
}
  1. x & 1 == 1 or == 0判断奇偶

  2. x = x & ( x - 1)=> 清零最低位的 1

  3. x & -x=> 得到最低位的 1

3.1.3.2. 更为复杂的位运算操作

  1. x最右边的 n位清零 - x & (~0 << n)

  2. 获取 x的第 n 位值(0或者1) - ( x >> n ) & 1

  3. 获取 x 的第 n 位的幂值 - x & ( 1 << ( n - 1 ) )

  4. 仅将第 n 位置为 1 - x | ( 1 << n )

  5. 仅将第 n 位置为 0 - x & ( ~ ( 1 << n ) )

  6. 将 x 最高位至第 n 位(含)清零 - x & ( ( 1 << n ) - 1 )

  7. 将第 n 位至第 0 位(含)清零 - x & ( ~ ( ( 1 << ( n + 1 ) ) - 1