数据结构与算法


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

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

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

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

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

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

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

1.1 数据结构是什么?

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

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

1.2 逻辑结构 vs 物理结构

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

1.3 数据结构与算法的关系

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

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

1.4 抽象数据类型(ADT)

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

go 复制代码
// 线性表的抽象数据类型
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 顺序表 ------ 数组的实现

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

go 复制代码
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 单链表 ------ 用指针串起来的"火车"

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

go 复制代码
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 双链表 ------ 可以倒着走的"双向通道"

go 复制代码
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 循环链表 ------ 首尾相接的"环形跑道"

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

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

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

graph TD subgraph "入栈 Push" A1["栈: [1, 2]"] --> B1["Push 3"] B1 --> C1["栈: [1, 2, 3]
↑ 栈顶"] 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
go 复制代码
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
}

经典应用:括号匹配

go 复制代码
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]
↑队尾"] 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
go 复制代码
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
}

循环队列原理:

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

4.1 串的定义与操作

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

go 复制代码
// 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)

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

go 复制代码
// 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 数组 ------ 线性表的推广

go 复制代码
// 一维数组(向量)
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)
go 复制代码
// 稀疏矩阵三元组
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 二叉树 ------ 最多两个孩子的树

go 复制代码
// 二叉树节点
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
lchild指向前驱| D[前驱] A -.->|rtag=1
rchild指向后继| E[后继] style D fill:#FFE4B5 style E fill:#FFE4B5

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

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

go 复制代码
// 查找
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[ G1[ H1[新节点] E2[b] --> F2[ G2[a] end style A2 fill:#90EE90 style E2 fill:#90EE90

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

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

graph TD A[带权节点
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[权大的靠近根
编码短] G --> H[D: 0
高频短编码] G --> I[C: 110
低频长编码] style H fill:#90EE90

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

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

7.1 存储结构

go 复制代码
// 邻接矩阵(适合稠密图)
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
}

存储对比:

7.2 遍历

go 复制代码
// 深度优先搜索(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
(一路到底,回溯)" style A fill:#90EE90 end subgraph "BFS: 1→2→3→4→5→6
(层层展开)" 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算法:所有顶点对最短路径,动态规划。

go 复制代码
// 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)
go 复制代码
// 二分查找(非递归)
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
[5, 13, 19, 21, 37, 56, 64, 75, 80, 88, 92]"] --> B["low=0, high=10
mid=5, arr[5]=56"] B --> C[37<56, high=4] C --> D["low=0, high=4
mid=2, arr[2]=19"] D --> E[37>19, low=3] E --> F["low=3, high=4
mid=3, arr[3]=21"] F --> G[37>21, low=4] G --> H["low=4, high=4
mid=4, arr[4]=37"] H --> I[找到! 位置4] style I fill:#90EE90

8.2 动态查找表

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

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

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

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

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

9.1 排序算法分类与对比

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

go 复制代码
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
右半: 76 97 65 49] I --> J[递归排序...] style H fill:#90EE90

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

go 复制代码
// 建大顶堆,然后每次把堆顶(最大)放到末尾
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[栈和队列
受限的线性表] F --> G[串
特殊的线性表] end subgraph "扩展结构" H[数组和广义表
线性表的推广] end subgraph "非线性结构" I[树
一对多] --> J[二叉树] J --> K[搜索树
BST/AVL/B] K --> L[哈夫曼树
应用] M[图
多对多] --> N[存储/遍历] N --> O[生成树/最短路径] end subgraph "算法" P[查找] --> Q[顺序/二分/哈希] R[排序] --> S[插入/交换/选择
归并/基数] 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 实现一遍,才能真正掌握!

相关推荐
Betelgeuse762 小时前
打通 Django 认证:原生 Auth 组件实战与 API 改造
后端·python·django
ltl2 小时前
一致性哈希:不要相信教科书版本
后端
亦暖筑序2 小时前
让 AI 客服真能用的 3 个模块:情绪感知 + 意图识别 + Agent 工具链
java·人工智能·后端
ltl2 小时前
康威定律与逆康威定律:组织架构决定系统架构
后端
fliter2 小时前
Go 泛型切片函数:你可能忽略的内存陷阱
后端
SimonKing2 小时前
别让你的代码裸奔!Spring Boot混淆全攻略(附配置)
java·后端·程序员
Mintopia2 小时前
系统复杂度失控的根源:不是业务,而是边界
后端
穗余2 小时前
Rust——impl是什么意思
开发语言·后端·rust
代码羊羊2 小时前
Rust模式匹配
开发语言·后端·rust