数据结构与算法

0 阅读13分钟

image.png


数据结构(Go语言版)完全指南

序章:数据结构是什么?—— 程序员的"收纳艺术"

想象你是一个图书管理员。面对成千上万本书,你怎么摆放才能让找书最快?

  • 随便堆地上 → 找一本书要翻遍所有书 → O(n)
  • 按类别放书架 → 先找类别再找书 → O(log n)
  • 每本书编号,知道编号直接定位O(1)

数据结构就是研究"如何高效组织和存储数据"的学科。

graph LR
    A[数据] --> B{如何组织?}
    B -->|无结构| C[堆在一起<br/>查找慢]
    B -->|线性结构| D[排队站好<br/>按顺序找]
    B -->|树形结构| E[分层管理<br/>二分查找]
    B -->|图形结构| F[网状关系<br/>路径搜索]
    B -->|哈希结构| G[直接定位<br/>最快]
    
    style C fill:#FFB6C1
    style G fill:#90EE90

第一章:数据结构基础 —— 万丈高楼平地起

1.1 数据结构是什么?

一句话定义:数据结构是一门研究非数值计算的程序设计问题中,计算机的操作对象以及它们之间的关系操作的学科。

通俗理解:不是研究怎么算1+1,而是研究怎么放数据、怎么找数据、怎么处理数据之间的关系

1.2 逻辑结构 vs 物理结构

维度逻辑结构物理结构
关注点数据之间的逻辑关系数据在内存中的实际存储方式
例子线性表、树、图顺序存储(数组)、链式存储(指针)
独立性独立于计算机依赖于计算机
类比建筑图纸实际施工
graph TD
    A[逻辑结构] --> B[线性结构<br/>一对一]
    A --> C[树形结构<br/>一对多]
    A --> D[图形结构<br/>多对多]
    A --> E[集合结构<br/>无关系]
    
    F[物理结构] --> G[顺序存储<br/>数组<br/>连续内存]
    F --> H[链式存储<br/>链表<br/>分散内存+指针]
    F --> I[索引存储<br/>索引表]
    F --> J[散列存储<br/>哈希表]
    
    A -.->|实现| F

1.3 数据结构与算法的关系

经典公式程序 = 数据结构 + 算法

角色数据结构算法
解决的问题如何存储数据如何处理数据
关系算法的基础数据结构的应用
类比厨房的食材摆放烹饪方法
graph LR
    A[数据结构] -->|提供基础| B[算法]
    B -->|优化需求| A
    
    C[例子] --> D[数组<br/>数据结构]
    D -->|二分查找算法| E[快速查找]
    E -->|发现数组插入慢| F[链表<br/>优化结构]
    F -->|顺序查找慢| G[二叉搜索树<br/>再优化]
    
    style A fill:#E1F5FE
    style B fill:#E8F5E9

1.4 抽象数据类型(ADT)

定义:描述数据的逻辑结构基本操作,不涉及具体实现。

// 线性表的抽象数据类型
type List interface {
    Init()              // 初始化
    Length() int        // 求长度
    Get(i int) any      // 取元素
    Insert(i int, e any) // 插入
    Delete(i int)       // 删除
    // ... 不涉及具体是数组还是链表实现
}

1.5 学习数据结构的基础

  1. 一门编程语言(本文用 Go)
  2. 数学基础(主要是逻辑思维和复杂度分析)

1.6 学习数据结构的好处

好处说明
提升程序运行效率选对结构,速度差100倍
更好解决实际问题复杂系统的基础

第二章:线性表 —— 排队的艺术

线性表:n个数据元素的有限序列,元素之间一对一

graph LR
    A["a₁"] --> B["a₂"]
    B --> C["a₃"]
    C --> D["..."]
    D --> E[aₙ]
    
    style A fill:#90EE90
    style E fill:#90EE90

2.1 顺序表 —— 数组的实现

特点:用连续的内存空间存储数据。

package main

import "fmt"

// 顺序表结构
type SeqList struct {
    data []int  // 存储数据的数组
    length int  // 当前长度
    capacity int // 容量
}

// 初始化
func NewSeqList(cap int) *SeqList {
    return &SeqList{
        data: make([]int, cap),
        length: 0,
        capacity: cap,
    }
}

// 插入元素(位置i,元素e)
func (s *SeqList) Insert(i int, e int) bool {
    // 边界检查
    if i < 0 || i > s.length || s.length >= s.capacity {
        return false
    }
    // 第i个位置后的元素后移
    for j := s.length; j > i; j-- {
        s.data[j] = s.data[j-1]
    }
    s.data[i] = e
    s.length++
    return true
}

// 删除元素(位置i)
func (s *SeqList) Delete(i int) (int, bool) {
    if i < 0 || i >= s.length {
        return 0, false
    }
    e := s.data[i]
    // 第i个位置后的元素前移
    for j := i; j < s.length-1; j++ {
        s.data[j] = s.data[j+1]
    }
    s.length--
    return e, true
}

// 查找元素
func (s *SeqList) Find(e int) int {
    for i := 0; i < s.length; i++ {
        if s.data[i] == e {
            return i
        }
    }
    return -1
}

func main() {
    list := NewSeqList(10)
    
    // 插入
    list.Insert(0, 10)  // [10]
    list.Insert(1, 20)  // [10, 20]
    list.Insert(0, 5)   // [5, 10, 20]
    
    fmt.Printf("顺序表: %v, 长度: %d\n", list.data[:list.length], list.length)
    // 输出: 顺序表: [5 10 20], 长度: 3
    
    // 删除
    val, _ := list.Delete(1)
    fmt.Printf("删除位置1的元素: %d\n", val)
    // 输出: 删除位置1的元素: 10
    
    fmt.Printf("删除后: %v\n", list.data[:list.length])
    // 输出: 删除后: [5 20]
    
    // 查找
    idx := list.Find(20)
    fmt.Printf("20的位置: %d\n", idx)
    // 输出: 20的位置: 1
}

插入删除过程图:

graph TD
    subgraph "插入操作(位置2插入99)"
        A1["原: [10, 20, 30, 40]"] --> B1[30后移]
        B1 --> C1[40后移]
        C1 --> D1[位置2放入99]
        D1 --> E1["结果: [10, 20, 99, 30, 40]"]
    end
    
    subgraph "删除操作(删除位置1)"
        A2["原: [10, 20, 30, 40]"] --> B2[20被删除]
        B2 --> C2[30前移]
        C2 --> D2[40前移]
        D2 --> E2["结果: [10, 30, 40]"]
    end
    
    style E1 fill:#90EE90
    style E2 fill:#FFB6C1

时间复杂度分析:

操作最好最坏平均
访问O(1)O(1)O(1)
插入O(1)(尾部)O(n)(头部)O(n)
删除O(1)(尾部)O(n)(头部)O(n)

2.2 单链表 —— 用指针串起来的"火车"

特点:元素可以分散存储,通过指针连接。

package main

import "fmt"

// 节点结构
type ListNode struct {
    data int       // 数据域
    next *ListNode // 指针域
}

// 单链表
type LinkedList struct {
    head *ListNode // 头指针
    length int
}

// 初始化
func NewLinkedList() *LinkedList {
    // 带头结点(方便操作)
    head := &ListNode{}
    return &LinkedList{head: head, length: 0}
}

// 头插法(逆序建立)
func (l *LinkedList) InsertHead(e int) {
    newNode := &ListNode{data: e}
    newNode.next = l.head.next
    l.head.next = newNode
    l.length++
}

// 尾插法(顺序建立)
func (l *LinkedList) InsertTail(e int) {
    newNode := &ListNode{data: e}
    // 找到最后一个节点
    p := l.head
    for p.next != nil {
        p = p.next
    }
    p.next = newNode
    l.length++
}

// 按位置插入
func (l *LinkedList) Insert(i int, e int) bool {
    if i < 0 || i > l.length {
        return false
    }
    p := l.head
    // 找到第i-1个节点
    for j := 0; j < i; j++ {
        p = p.next
    }
    newNode := &ListNode{data: e}
    newNode.next = p.next
    p.next = newNode
    l.length++
    return true
}

// 删除
func (l *LinkedList) Delete(i int) (int, bool) {
    if i < 0 || i >= l.length {
        return 0, false
    }
    p := l.head
    for j := 0; j < i; j++ {
        p = p.next
    }
    e := p.next.data
    p.next = p.next.next  // 跳过被删节点
    l.length--
    return e, true
}

// 打印
func (l *LinkedList) Print() {
    p := l.head.next
    for p != nil {
        fmt.Printf("%d -> ", p.data)
        p = p.next
    }
    fmt.Println("nil")
}

func main() {
    list := NewLinkedList()
    
    // 尾插建立 [10, 20, 30]
    list.InsertTail(10)
    list.InsertTail(20)
    list.InsertTail(30)
    list.Print()
    // 输出: 10 -> 20 -> 30 -> nil
    
    // 头插 5
    list.InsertHead(5)
    list.Print()
    // 输出: 5 -> 10 -> 20 -> 30 -> nil
    
    // 位置2插入15
    list.Insert(2, 15)
    list.Print()
    // 输出: 5 -> 10 -> 15 -> 20 -> 30 -> nil
    
    // 删除位置2
    val, _ := list.Delete(2)
    fmt.Printf("删除: %d\n", val)
    // 输出: 删除: 15
    list.Print()
    // 输出: 5 -> 10 -> 20 -> 30 -> nil
}

链表插入过程:

graph LR
    subgraph "插入前"
        A1[p] --> B1[...]
        B1 --> C1[q]
    end
    
    subgraph "插入后"
        A2[p] --> D2[newNode]
        D2 --> C2[q]
        A2 -.-> C2
    end
    
    style D2 fill:#90EE90

顺序表 vs 链表对比:

特性顺序表链表
存储空间预先分配,可能浪费动态分配,不浪费
访问速度O(1) 随机访问O(n) 必须顺序访问
插入删除需要移动元素,O(n)只需改指针,O(1)
适用场景查询多、修改少频繁插入删除

2.3 双链表 —— 可以倒着走的"双向通道"

type DListNode struct {
    data int
    prior *DListNode  // 前驱
    next  *DListNode  // 后继
}

// 双链表插入(在p之后插入s)
func InsertAfter(p, s *DListNode) {
    s.next = p.next
    s.prior = p
    p.next.prior = s  // 如果p不是最后一个
    p.next = s
}
graph LR
    A[p.prior] --> B[p]
    B --> C[p.next]
    
    B -.-> D[s]
    D --> C
    D --> B
    
    style D fill:#90EE90

2.4 循环链表 —— 首尾相接的"环形跑道"

image.png

第三章:栈和队列 —— 特殊的线性表

3.1 栈(Stack)— 后进先出(LIFO)

生活类比:叠盘子、弹夹、浏览器后退。

graph TD
    subgraph "入栈 Push"
        A1["栈: [1, 2]"] --> B1["Push 3"]
        B1 --> C1["栈: [1, 2, 3]<br/>↑ 栈顶"]
    end
    
    subgraph "出栈 Pop"
        A2["栈: [1, 2, 3]"] --> B2["Pop"]
        B2 --> C2["返回: 3"]
        C2 --> D2["栈: [1, 2]"]
    end
    
    style C1 fill:#90EE90
    style C2 fill:#FFB6C1
package main

import "fmt"

// 顺序栈
type SeqStack struct {
    data []int
    top  int  // -1表示空栈
}

func NewSeqStack(cap int) *SeqStack {
    return &SeqStack{
        data: make([]int, cap),
        top:  -1,
    }
}

func (s *SeqStack) Push(e int) bool {
    if s.top >= len(s.data)-1 {
        return false // 栈满
    }
    s.top++
    s.data[s.top] = e
    return true
}

func (s *SeqStack) Pop() (int, bool) {
    if s.top == -1 {
        return 0, false // 栈空
    }
    e := s.data[s.top]
    s.top--
    return e, true
}

func (s *SeqStack) Peek() (int, bool) {
    if s.top == -1 {
        return 0, false
    }
    return s.data[s.top], true
}

func main() {
    stack := NewSeqStack(10)
    
    stack.Push(10)
    stack.Push(20)
    stack.Push(30)
    
    top, _ := stack.Peek()
    fmt.Printf("栈顶: %d\n", top)
    // 输出: 栈顶: 30
    
    val, _ := stack.Pop()
    fmt.Printf("弹出: %d\n", val)
    // 输出: 弹出: 30
    
    val, _ = stack.Pop()
    fmt.Printf("弹出: %d\n", val)
    // 输出: 弹出: 20
}

经典应用:括号匹配

func isValid(s string) bool {
    stack := make([]rune, 0)
    pairs := map[rune]rune{')': '(', ']': '[', '}': '{'}
    
    for _, ch := range s {
        if ch == '(' || ch == '[' || ch == '{' {
            stack = append(stack, ch)
        } else {
            if len(stack) == 0 || stack[len(stack)-1] != pairs[ch] {
                return false
            }
            stack = stack[:len(stack)-1]
        }
    }
    return len(stack) == 0
}

func main() {
    fmt.Println(isValid("({[]})"))  // true
    fmt.Println(isValid("({[})"))  // false
}

3.2 队列(Queue)— 先进先出(FIFO)

生活类比:排队买票、打印机任务队列。

graph LR
    subgraph "入队 Enqueue(队尾)"
        A1["队列: [1, 2]"] --> B1[Enqueue 3]
        B1 --> C1["队列: [1, 2, 3]<br/>↑队尾"]
    end
    
    subgraph "出队 Dequeue(队头)"
        A2["队列: [1, 2, 3]"] --> B2[Dequeue]
        B2 --> C2[返回: 1]
        C2 --> D2["队列: [2, 3]"]
    end
    
    style C1 fill:#90EE90
    style C2 fill:#FFB6C1
package main

import "fmt"

// 循环队列(解决假溢出)
type CircleQueue struct {
    data []int
    front int  // 队头
    rear  int  // 队尾(下一个插入位置)
    capacity int
}

func NewCircleQueue(cap int) *CircleQueue {
    return &CircleQueue{
        data: make([]int, cap),
        front: 0,
        rear: 0,
        capacity: cap,
    }
}

func (q *CircleQueue) Enqueue(e int) bool {
    if (q.rear+1)%q.capacity == q.front {
        return false // 队满(牺牲一个空间区分空和满)
    }
    q.data[q.rear] = e
    q.rear = (q.rear + 1) % q.capacity
    return true
}

func (q *CircleQueue) Dequeue() (int, bool) {
    if q.front == q.rear {
        return 0, false // 队空
    }
    e := q.data[q.front]
    q.front = (q.front + 1) % q.capacity
    return e, true
}

func main() {
    queue := NewCircleQueue(5) // 实际存4个元素
    
    queue.Enqueue(10)
    queue.Enqueue(20)
    queue.Enqueue(30)
    
    val, _ := queue.Dequeue()
    fmt.Printf("出队: %d\n", val)  // 10
    
    queue.Enqueue(40)
    queue.Enqueue(50)
    
    // 此时队列: [20, 30, 40, 50]
    for !isEmpty(queue) {
        v, _ := queue.Dequeue()
        fmt.Printf("%d ", v)
    }
    // 输出: 20 30 40 50
}

func isEmpty(q *CircleQueue) bool {
    return q.front == q.rear
}

循环队列原理: image.png

第四章:串(String)— 字符的序列

4.1 串的定义与操作

:零个或多个字符组成的有限序列

// Go 中字符串是不可变的字节序列
s := "Hello, 世界"
fmt.Println(len(s))  // 13(中文3字节)
fmt.Println(len([]rune(s)))  // 9(字符数)

// 串的基本操作
fmt.Println(s[0:5])      // "Hello"(子串)
fmt.Println(s + "!")     // "Hello, 世界!"(连接)
fmt.Println(s == "Hello, 世界")  // true(比较)

4.2 模式匹配 —— 找子串的位置

BF算法(暴力匹配):逐个比较,最坏 O(m×n)

image.png KMP算法:利用已匹配信息,避免回溯,O(m+n)

// KMP 核心:next数组(部分匹配表)
func getNext(pattern string) []int {
    next := make([]int, len(pattern))
    next[0] = -1
    i, j := 0, -1
    
    for i < len(pattern)-1 {
        if j == -1 || pattern[i] == pattern[j] {
            i++
            j++
            next[i] = j
        } else {
            j = next[j]
        }
    }
    return next
}

第五章:数组和广义表 —— 多维世界的扩展

5.1 数组 —— 线性表的推广

// 一维数组(向量)
arr1 := [5]int{1, 2, 3, 4, 5}

// 二维数组(矩阵)
arr2 := [3][4]int{
    {1, 2, 3, 4},
    {5, 6, 7, 8},
    {9, 10, 11, 12},
}

// 存储方式:行优先(C/Go)或列优先(Fortran)
// 行优先:a[0][0], a[0][1]...a[0][3], a[1][0]...

特殊矩阵压缩存储:

矩阵类型特点压缩方法
对称矩阵a[i][j] = a[j][i]只存上/下三角
三角矩阵上/下三角为常数只存三角+一个常数
稀疏矩阵大量零元素三元组表 (i, j, value)
// 稀疏矩阵三元组
type Triple struct {
    row, col int
    value    int
}

// 十字链表(更高效的稀疏矩阵存储)
type OLNode struct {
    row, col int
    value    int
    right    *OLNode  // 行链表
    down     *OLNode  // 列链表
}

5.2 广义表 —— 递归的线性表

广义表:元素可以是原子,也可以是子表

graph TD
    A["广义表 LS = (a, (b, c), d, (e) )"]

    subgraph "结构"
        B1[a] --> B2[(b,c)]
        B2 --> B3[d]
        B3 --> B4[e]
    end
    
    subgraph "操作"
        C1["Head(LS) = a"]  
        C2["Tail(LS) = ((b,c), d, e)"]  
    end

第六章:树 —— 分层管理的艺术

:n个节点的有限集合,有且仅有一个,其余节点分为互不相交的子树

graph TD
    A[根 A] --> B[B]
    A --> C[C]
    B --> D[D]
    B --> E[E]
    C --> F[F]
    C --> G[G]
    
    style A fill:#90EE90

6.1 二叉树 —— 最多两个孩子的树

// 二叉树节点
type BiTreeNode struct {
    data  int
    lchild *BiTreeNode
    rchild *BiTreeNode
}

// 遍历(核心!)
func PreOrder(root *BiTreeNode) {
    if root == nil { return }
    fmt.Print(root.data, " ")  // 访问根
    PreOrder(root.lchild)      // 遍历左子树
    PreOrder(root.rchild)      // 遍历右子树
}

func InOrder(root *BiTreeNode) {
    if root == nil { return }
    InOrder(root.lchild)
    fmt.Print(root.data, " ")  // 中序:左-根-右
    InOrder(root.rchild)
}

func PostOrder(root *BiTreeNode) {
    if root == nil { return }
    PostOrder(root.lchild)
    PostOrder(root.rchild)
    fmt.Print(root.data, " ")  // 后序:左-右-根
}

// 层序遍历(用队列)
func LevelOrder(root *BiTreeNode) {
    if root == nil { return }
    queue := []*BiTreeNode{root}
    
    for len(queue) > 0 {
        node := queue[0]
        queue = queue[1:]
        fmt.Print(node.data, " ")
        
        if node.lchild != nil {
            queue = append(queue, node.lchild)
        }
        if node.rchild != nil {
            queue = append(queue, node.rchild)
        }
    }
}

遍历过程可视化:

graph TD
    A[1] --> B[2]
    A --> C[3]
    B --> D[4]
    B --> E[5]
    C --> F[6]
    
    subgraph "前序遍历: 1 2 4 5 3 6"
        P1[访问根1] --> P2[左子树2]
        P2 --> P3[左子树4]
        P3 --> P4[右子树5]
        P4 --> P5[右子树3]
        P5 --> P6[左子树6]
    end

6.2 线索二叉树 —— 利用空指针

问题:n个节点的二叉树有 n+1 个空指针,浪费!

解决:利用空指针存储前驱/后继(线索),方便遍历。

graph LR
    A[节点] -->|lchild| B[左孩子]
    A -->|rchild| C[右孩子]
    
    A -.->|ltag=1<br/>lchild指向前驱| D[前驱]
    A -.->|rtag=1<br/>rchild指向后继| E[后继]
    
    style D fill:#FFE4B5
    style E fill:#FFE4B5

6.3 二叉搜索树(BST)— 高效的查找结构

性质:左子树 < 根 < 右子树

// 查找
func BSTSearch(root *BiTreeNode, key int) *BiTreeNode {
    for root != nil {
        if key == root.data {
            return root
        } else if key < root.data {
            root = root.lchild
        } else {
            root = root.rchild
        }
    }
    return nil
}

// 插入
func BSTInsert(root **BiTreeNode, key int) bool {
    if *root == nil {
        *root = &BiTreeNode{data: key}
        return true
    }
    if key == (*root).data {
        return false // 已存在
    } else if key < (*root).data {
        return BSTInsert(&(*root).lchild, key)
    } else {
        return BSTInsert(&(*root).rchild, key)
    }
}

BST 查找过程:

graph TD
    A[50] --> B[30]
    A --> C[70]
    B --> D[20]
    B --> E[40]
    C --> F[60]
    C --> G[80]
    
    H[查找 40] --> I[40<50, 去左子树]
    I --> J[40>30, 去右子树]
    J --> K[找到40!]
    
    style K fill:#90EE90

6.4 AVL树 —— 平衡的二叉搜索树

问题:BST 可能退化成链表(最坏 O(n))

解决:AVL树要求左右子树高度差 ≤ 1,不平衡时旋转

graph TD
    subgraph "左旋(右右情况)"
        A1[a] --> B1[>a]
        B1 --> C1[>b]
        C1 --> D1[新节点]
        
        A2[b] --> B2[a]
        A2 --> C2[>b]
    end
    
    subgraph "右旋(左左情况)"
        E1[a] --> F1[<a]
        F1 --> G1[<b]
        G1 --> H1[新节点]
        
        E2[b] --> F2[<b]
        E2 --> G2[a]
    end
    
    style A2 fill:#90EE90
    style E2 fill:#90EE90

6.5 哈夫曼树 —— 带权路径最短的树

应用:数据压缩(哈夫曼编码)

graph TD
    A[带权节点<br/>A:5, B:7, C:2, D:13] --> B[每次选两个最小的合并]
    B --> C[C:2 + A:5 = 7]
    C --> D[7 + B:7 = 14]
    D --> E[14 + D:13 = 27]
    
    F[哈夫曼树] --> G[权大的靠近根<br/>编码短]
    G --> H[D: 0<br/>高频短编码]
    G --> I[C: 110<br/>低频长编码]
    
    style H fill:#90EE90

第七章:图 —— 多对多的复杂关系

:G = (V, E),V是顶点集,E是边集。

image.png

7.1 存储结构

// 邻接矩阵(适合稠密图)
type MGraph struct {
    vexs   []string    // 顶点
    arcs   [][]int     // 边矩阵
    vexNum, arcNum int
}

// 邻接表(适合稀疏图)
type ArcNode struct {
    adjVex int        // 邻接点
    weight int        // 权值
    nextArc *ArcNode  // 下一条边
}

type VNode struct {
    data    string
    firstArc *ArcNode
}

type ALGraph struct {
    vertices []VNode
    vexNum, arcNum int
}

存储对比:

image.png

7.2 遍历

// 深度优先搜索(DFS)— 类似树的前序
func DFS(g *ALGraph, v int, visited []bool) {
    visited[v] = true
    fmt.Print(g.vertices[v].data, " ")
    
    for p := g.vertices[v].firstArc; p != nil; p = p.nextArc {
        if !visited[p.adjVex] {
            DFS(g, p.adjVex, visited)
        }
    }
}

// 广度优先搜索(BFS)— 类似树的层序
func BFS(g *ALGraph, v int) {
    visited := make([]bool, g.vexNum)
    queue := []int{v}
    visited[v] = true
    
    for len(queue) > 0 {
        v = queue[0]
        queue = queue[1:]
        fmt.Print(g.vertices[v].data, " ")
        
        for p := g.vertices[v].firstArc; p != nil; p = p.nextArc {
            if !visited[p.adjVex] {
                visited[p.adjVex] = true
                queue = append(queue, p.adjVex)
            }
        }
    }
}

DFS vs BFS 可视化:

graph TD
    A[1] --> B[2]
    A --> C[3]
    B --> D[4]
    B --> E[5]
    C --> F[6]
    
    subgraph "DFS: 1→2→4→5→3→6<br/>(一路到底,回溯)"
        style A fill:#90EE90
    end
    
    subgraph "BFS: 1→2→3→4→5→6<br/>(层层展开)"
        style A fill:#FFE4B5
    end

7.3 最小生成树 —— 连接所有顶点的最小代价

Prim算法:从一个顶点开始,每次选最小边连接新顶点。

Kruskal算法:按边权排序,每次选最小边,不形成环。

graph TD
    A[A] ---|4| B[B]
    A ---|1| C[C]
    B ---|2| C
    B ---|3| D
    C ---|5| D
    
    subgraph "Prim结果(选边)"
        E1[A-C:1] --> F1[C-B:2]
        F1 --> G1[B-D:3]
        H1[总权值: 6]
    end
    
    style E1 fill:#90EE90
    style F1 fill:#90EE90
    style G1 fill:#90EE90

7.4 最短路径

Dijkstra算法:单源最短路径,贪心策略,不能有负权边

Floyd算法:所有顶点对最短路径,动态规划。

// Floyd 核心代码
for k := 0; k < n; k++ {
    for i := 0; i < n; i++ {
        for j := 0; j < n; j++ {
            if dist[i][k] + dist[k][j] < dist[i][j] {
                dist[i][j] = dist[i][k] + dist[k][j]
                path[i][j] = k  // 记录中间点
            }
        }
    }
}
// 原理:如果经过k点更短,就更新

第八章:查找表 —— 快速定位的艺术

8.1 静态查找表

方法条件时间复杂度
顺序查找无序O(n)
二分查找有序O(log n)
分块查找分块有序O(√n)
// 二分查找(非递归)
func BinarySearch(arr []int, key int) int {
    low, high := 0, len(arr)-1
    
    for low <= high {
        mid := (low + high) / 2
        if arr[mid] == key {
            return mid
        } else if arr[mid] < key {
            low = mid + 1
        } else {
            high = mid - 1
        }
    }
    return -1
}

二分查找过程:

graph TD
    A["查找 37<br/>[5, 13, 19, 21, 37, 56, 64, 75, 80, 88, 92]"] --> B["low=0, high=10<br/>mid=5, arr[5]=56"]
    B --> C[37<56, high=4]
    C --> D["low=0, high=4<br/>mid=2, arr[2]=19"]
    D --> E[37>19, low=3]
    E --> F["low=3, high=4<br/>mid=3, arr[3]=21"]
    F --> G[37>21, low=4]
    G --> H["low=4, high=4<br/>mid=4, arr[4]=37"]
    H --> I[找到! 位置4]
    
    style I fill:#90EE90

8.2 动态查找表

  • 二叉排序树:插入删除方便,但可能不平衡
  • 平衡二叉树(AVL):自动平衡,查找稳定 O(log n)
  • B树/B+树:多路平衡树,数据库索引的核心

8.3 哈希表 —— O(1) 的查找神话

核心思想:通过哈希函数直接计算存储位置。

package main

import "fmt"

// 简单哈希表(拉链法解决冲突)
type HashTable struct {
    buckets []*HashNode  // 桶数组
    size    int
}

type HashNode struct {
    key   string
    value int
    next  *HashNode  // 冲突时用链表
}

func NewHashTable(size int) *HashTable {
    return &HashTable{
        buckets: make([]*HashNode, size),
        size:    size,
    }
}

// 哈希函数(简单取模)
func (h *HashTable) hash(key string) int {
    sum := 0
    for _, ch := range key {
        sum += int(ch)
    }
    return sum % h.size
}

// 插入
func (h *HashTable) Put(key string, value int) {
    index := h.hash(key)
    newNode := &HashNode{key: key, value: value}
    
    // 头插法
    newNode.next = h.buckets[index]
    h.buckets[index] = newNode
}

// 查找
func (h *HashTable) Get(key string) (int, bool) {
    index := h.hash(key)
    p := h.buckets[index]
    
    for p != nil {
        if p.key == key {
            return p.value, true
        }
        p = p.next
    }
    return 0, false
}

func main() {
    ht := NewHashTable(10)
    
    ht.Put("apple", 5)
    ht.Put("banana", 3)
    ht.Put("cherry", 8)
    
    val, _ := ht.Get("banana")
    fmt.Printf("banana: %d\n", val)  // 3
    
    // 查看哈希分布
    for i, bucket := range ht.buckets {
        count := 0
        for p := bucket; p != nil; p = p.next {
            count++
        }
        if count > 0 {
            fmt.Printf("桶%d: %d个元素\n", i, count)
        }
    }
}

哈希冲突解决:

graph TD
    A[哈希冲突<br/>不同key映射到同一位置] --> B[开放定址法]
    A --> C[链地址法<br/>拉链法]
    
    B --> D["线性探测<br/>hi = (h(key)+i)%m"]
    B --> E["二次探测<br/>hi = (h(key)+i²)%m"]
    
    C --> F[每个桶是一个链表<br/>Go map的实现方式]
    
    subgraph zip_example["拉链法示例"]
        G[桶0] --> H[key1]
        H --> I[key2]
        H --> J[key3<br/>冲突]
        K[桶1] --> L[key4]
    end
    
    style F fill:#90EE90

第九章:排序算法 —— 让数据井然有序

9.1 排序算法分类与对比

image.png

9.2 快速排序 —— 分治的经典

package main

import "fmt"

func QuickSort(arr []int, low, high int) {
    if low < high {
        // 划分:pivot左边小,右边大
        pivot := Partition(arr, low, high)
        
        // 递归排序左右两部分
        QuickSort(arr, low, pivot-1)
        QuickSort(arr, pivot+1, high)
    }
}

func Partition(arr []int, low, high int) int {
    pivot := arr[low]  // 选第一个为基准
    
    for low < high {
        // 从右找小的
        for low < high && arr[high] >= pivot {
            high--
        }
        arr[low] = arr[high]
        
        // 从左找大的
        for low < high && arr[low] <= pivot {
            low++
        }
        arr[high] = arr[low]
    }
    
    arr[low] = pivot
    return low
}

func main() {
    arr := []int{49, 38, 65, 97, 76, 13, 27, 49}
    fmt.Printf("排序前: %v\n", arr)
    
    QuickSort(arr, 0, len(arr)-1)
    
    fmt.Printf("排序后: %v\n", arr)
    // 输出: 排序后: [13 27 38 49 49 65 76 97]
}

快排过程可视化:

graph TD
    A[49 38 65 97 76 13 27 49] --> B[pivot=49]
    B --> C[右找小: 27]
    C --> D[左找大: 65]
    D --> E[交换后: 27 38 49 97 76 13 65 49]
    E --> F[继续...]
    F --> G[最终: 27 38 13 49 76 97 65 49]
    G --> H[49归位]
    H --> I[左半: 27 38 13<br/>右半: 76 97 65 49]
    I --> J[递归排序...]
    
    style H fill:#90EE90

9.3 堆排序 —— 选择排序的优化

// 建大顶堆,然后每次把堆顶(最大)放到末尾
func HeapSort(arr []int) {
    n := len(arr)
    
    // 从最后一个非叶子节点开始建堆
    for i := n/2 - 1; i >= 0; i-- {
        heapify(arr, n, i)
    }
    
    // 逐个取出最大值
    for i := n - 1; i > 0; i-- {
        arr[0], arr[i] = arr[i], arr[0]  // 堆顶放到末尾
        heapify(arr, i, 0)               // 重新调整堆
    }
}

func heapify(arr []int, n, i int) {
    largest := i
    left := 2*i + 1
    right := 2*i + 2
    
    if left < n && arr[left] > arr[largest] {
        largest = left
    }
    if right < n && arr[right] > arr[largest] {
        largest = right
    }
    
    if largest != i {
        arr[i], arr[largest] = arr[largest], arr[i]
        heapify(arr, n, largest)  // 递归调整
    }
}

堆排序过程:

graph TD
    subgraph "初始数组"
        A1[4 10 3 5 1]
    end
    
    subgraph "建大顶堆"
        B1[    10] --> B2[  5   3]
        B2 --> B3[4 1]
    end
    
    subgraph "排序过程"
        C1[交换10和1] --> C2[1 5 3 4 10]
        C2 --> C3[调整堆: 5 4 3 1 10]
        C3 --> C4[交换5和1] --> C5[1 4 3 5 10]
        C5 --> C6[调整堆: 4 1 3 5 10]
        C6 --> C7[继续...] --> C8[1 3 4 5 10]
    end
    
    style B1 fill:#90EE90
    style C8 fill:#90EE90

知识总串联:从线性到非线性,从简单到复杂

graph TD
    subgraph "基础"
        A[数据结构基础] --> B[逻辑/物理结构]
        B --> C[复杂度分析]
    end
    
    subgraph "线性结构"
        D[线性表] --> E[顺序表/链表]
        E --> F[栈和队列<br/>受限的线性表]
        F --> G[串<br/>特殊的线性表]
    end
    
    subgraph "扩展结构"
        H[数组和广义表<br/>线性表的推广]
    end
    
    subgraph "非线性结构"
        I[树<br/>一对多] --> J[二叉树]
        J --> K[搜索树<br/>BST/AVL/B]
        K --> L[哈夫曼树<br/>应用]
        
        M[图<br/>多对多] --> N[存储/遍历]
        N --> O[生成树/最短路径]
    end
    
    subgraph "算法"
        P[查找] --> Q[顺序/二分/哈希]
        R[排序] --> S[插入/交换/选择<br/>归并/基数]
    end
    
    A --> D
    D --> H
    H --> I
    H --> M
    I --> P
    M --> P
    D --> R
    
    style A fill:#E1F5FE
    style D fill:#E8F5E9
    style I fill:#FFF3E0
    style M fill:#F3E5F5
    style P fill:#FFEBEE
    style R fill:#FFF8E1

核心:数据结构不是背出来的,是画出来的、写出来的、调出来的。每学一个结构,一定要亲手用 Go 实现一遍,才能真正掌握!