数据结构 线性表 学习 2025/6/12 21点27分

发布于:2025-06-14 ⋅ 阅读:(16) ⋅ 点赞:(0)

数据结构 线性表

具有相同数据类型的n个数据元素的有限序列

  • 元素具有相同数据类型

  • 元素之间具有顺序关系(除首尾元素外,每个元素有且仅有一个直接前驱和一个直接后继)

  • 元素个数有限

1. 顺序表(顺序存储)

顺序表是用一组地址连续的存储单元依次存储线性表中的数据元素。

特点

  • 逻辑上相邻的元素物理位置也相邻

  • 随机访问(通过下标直接访问)

  • 插入/删除需要移动大量元素

#define MAX_SIZE 100  // 最大容量

typedef struct {
    int data[MAX_SIZE];  // 存储数组
    int length;         // 当前长度
} SeqList;

// 初始化
void InitList(SeqList *L) {
    L->length = 0;
}

// 插入操作
int ListInsert(SeqList *L, int pos, int elem) {
    if (pos < 1 || pos > L->length + 1 || L->length >= MAX_SIZE) {
        return 0; // 插入失败
    }
    for (int i = L->length; i >= pos; i--) {
        L->data[i] = L->data[i-1];
    }
    L->data[pos-1] = elem;
    L->length++;
    return 1; // 插入成功
}

// 删除操作
int ListDelete(SeqList *L, int pos, int *elem) {
    if (pos < 1 || pos > L->length) {
        return 0; // 删除失败
    }
    *elem = L->data[pos-1];
    for (int i = pos; i < L->length; i++) {
        L->data[i-1] = L->data[i];
    }
    L->length--;
    return 1; // 删除成功
}
  • 顺序表适用场景
  • 需要频繁访问元素
  • 元素数量变化不大
  • 已知大致元素数量

2. 链表(链式存储)

链表是用一组任意的存储单元存储线性表的数据元素,通过指针连接元素。

特点

  • 逻辑上相邻的元素物理位置不一定相邻

  • 插入/删除不需要移动元素

  • 不支持随机访问

typedef struct LNode {
    int data;           // 数据域
    struct LNode *next; // 指针域
} LNode, *LinkList;

// 头插法建立链表
void CreateListHead(LinkList *L, int n) {
    *L = (LinkList)malloc(sizeof(LNode));
    (*L)->next = NULL;
    for (int i = 0; i < n; i++) {
        LNode *p = (LNode*)malloc(sizeof(LNode));
        p->data = i+1;
        p->next = (*L)->next;
        (*L)->next = p;
    }
}

// 插入操作
int ListInsert(LinkList *L, int pos, int elem) {
    LNode *p = *L;
    int j = 0;
    while (p && j < pos-1) {
        p = p->next;
        j++;
    }
    if (!p || j > pos-1) return 0;
    LNode *s = (LNode*)malloc(sizeof(LNode));
    s->data = elem;
    s->next = p->next;
    p->next = s;
    return 1;
}

// 删除操作
int ListDelete(LinkList *L, int pos, int *elem) {
    LNode *p = *L;
    int j = 0;
    while (p->next && j < pos-1) {
        p = p->next;
        j++;
    }
    if (!(p->next) || j > pos-1) return 0;
    LNode *q = p->next;
    *elem = q->data;
    p->next = q->next;
    free(q);
    return 1;
}

  • 链表适用场景

    • 需要频繁插入/删除

    • 元素数量变化大

    • 无法预估元素数量

线性表的常见操作

操作 顺序表 链表
访问元素 O(1) O(n)
插入/删除 O(n) O(1) [已知位置]
查找元素 O(n) O(n)
空间分配 静态/动态数组 动态分配

线性表的变种

  1. 双向链表:每个节点包含前驱和后继指针

  2. 循环链表:尾节点指向头节点

  3. 静态链表:用数组实现的链表(游标实现)

  4. 栈和队列:受限的线性表(只能在端点操作)

线性表是数据结构的基础,理解它对学习更复杂的数据结构非常重要。

数组

数组实现的线性表适用于:

  • 需要频繁随机访问的场景

  • 元素数量相对固定的情况

// 定义数组
int array[MAX_SIZE];
int length = 0; // 当前长度

// 获取元素
function get(i):
    if i < 0 OR i ≥ length:
        return error
    return array[i]

// 插入元素
function insert(i, e):
    if length == MAX_SIZE:
        return error // 数组已满
    if i < 0 OR i > length:
        return error // 位置不合法
    
    // 将i及之后的元素后移
    for j = length-1 downto i:
        array[j+1] = array[j]
    
    array[i] = e
    length++

// 删除元素
function delete(i):
    if i < 0 OR i ≥ length:
        return error
    
    // 将i之后的元素前移
    for j = i to length-2:
        array[j] = array[j+1]
    
    length--

 

树状数组

(又称二叉索引树)是一种高效实现前缀和查询单点更新的数据结构

核心思想

利用二进制表示中最低位的1来构建层次结构,每个节点存储特定区间的和。

基本操作原理
  1. lowbit计算x & -x,获取x的二进制表示中最低位的1

    • 例如:6 (110) 的lowbit是 2 (10)

  2. 更新操作

    • 修改一个元素时,需要更新所有包含该元素的区间

    • 通过不断加上lowbit值向上更新

  3. 查询操作

    • 查询前缀和时,通过不断减去lowbit值向下累加

class FenwickTree {
private:
    vector<int> tree;
    
    int lowbit(int x) {
        return x & -x;
    }
    
public:
    FenwickTree(int size) : tree(size + 1, 0) {}
    
    // 单点更新:位置i增加val
    void update(int i, int val) {
        while (i < tree.size()) {
            tree[i] += val;
            i += lowbit(i);
        }
    }
    
    // 查询前缀和:[1..i]的和
    int query(int i) {
        int sum = 0;
        while (i > 0) {
            sum += tree[i];
            i -= lowbit(i);
        }
        return sum;
    }
    
    // 区间查询:[i..j]的和
    int rangeQuery(int i, int j) {
        return query(j) - query(i - 1);
    }
};

树状数组常用于

  1. 计算逆序对数量

  2. 实时统计前缀和

  3. 解决某些计数问题

  4. 配合离散化处理大数据集

 

字符串:(KMP 字典树 AC自动机 BM 后缀数组)

KMP: 高效的字符串匹配算法 (在主字符串 中 找 模式串 出现的 位置)

KMP算法特别适用于:

  • 在长文本中反复搜索同一模式串

  • 需要高效字符串匹配的场景(如文本编辑器、IDE的查找功能)

  • 作为其他算法的基础组件(如字符串周期性问题)

主串(text): "ABABDABACDABABCABAB"
模式串(pattern): "ABABCABAB"

构建next数组:[0, 0, 1, 2, 0, 1, 2, 3, 4]

匹配过程:

在text[10]处找到完整匹配

字典树:用于高效地存储和检索字符串集合。它也被称为前缀树或单词查找树。

基本概念

字典树的核心特点:

  • 每个节点代表一个字符串的字符

  • 从根节点到某一节点的路径上的字符连接起来,构成该节点对应的字符串

  • 根节点对应空字符串

  • 通常在节点中用标志位标记字符串的结束

字典树的节点通常包含:

  • 子节点指针(通常用数组或哈希表实现)

  • 是否是单词结尾的标志

  • 可能的附加数据(如词频等)

应用场景
  1. 自动补全系统:如搜索引擎的搜索建议

  2. 拼写检查:快速判断单词是否存在

  3. IP路由:最长前缀匹配

  4. 单词游戏:如Boggle、Scrabble等

  5. 输入法:中文输入法的候选词推荐

 AC自动机:

AC自动机是一种多模式串匹配算法,可以同时查找多个模式串在文本中的所有出现位置。结合了字典树(Trie)KMP算法的思想,是字符串匹配领域的重要算法。

 

核心思想
  1. 构建字典树:将所有模式串构建成一棵字典树

  2. 添加失败指针:类似于KMP的next数组,实现匹配失败时的跳转

  3. 多模式匹配:在文本串上进行高效扫描,同时匹配所有模式串

AC自动机包含三个核心部分:

  1. Trie树结构:存储所有模式串

  2. 失败指针(fail指针):匹配失败时的跳转指针

  3. 输出表:记录每个节点代表的完整模式串

BM BM算法是一种高效的字符串匹配算法 实际应用中通常比KMP算法表现更好

核心思想

BM算法采用两种启发式规则来跳过不必要的比较:

  1. 坏字符规则(Bad Character Rule):从右向左比较,遇到不匹配字符时根据预处理的坏字符表滑动模式串

  2. 好后缀规则(Good Suffix Rule):当发现部分匹配时,根据已匹配的后缀信息滑动模式串

应用场景
  1. 文本编辑器的查找功能

  2. 病毒扫描中的特征码匹配

  3. 生物信息学中的DNA序列匹配

  4. 大文本中的关键词搜索

 后缀数组

后缀数组是一种强大的字符串处理数据结构,能够高效解决多种字符串问题。它是后缀树的空间优化替代方案,在实际应用中更为常用

基本概念

后缀数组是一个包含字符串所有后缀的按字典序排序的数组,存储的是这些后缀的起始索引。

对于长度为n的字符串S,其后缀数组SA是一个[0..n-1]的排列,满足:
S[SA[0]..] < S[SA[1]..] < ... < S[SA[n-1]..]

 

示例
字符串S = "banana"(长度n=6)

索引	后缀
0	banana
1	anana
2	nana
3	ana
4	na
5	a
排序后得到后缀数组SA = [5, 3, 1, 0, 4, 2]

2025年6月13日 23点21分


网站公告

今日签到

点亮在社区的每一天
去签到