欢迎光临
我们一直在努力

数组模拟链表、栈、队列与优先队列:高效实现与性能对比

数组模拟链表、栈、队列与优先队列(附OJ实战)

一、前言

在算法竞赛中,直接使用Java内置的LinkedList、Stack等类,往往会因为封装和对象创建的开销导致超时。因此,用数组模拟数据结构是蓝桥杯等竞赛中必备的核心技能。它不仅能将时间复杂度优化到极致,还能更深刻地理解数据结构的底层原理。

本文将详细讲解如何用数组模拟单链表、双链表、栈、队列和优先队列(堆)的核心内容,结合蓝桥OJ的经典例题,提供可直接复用的代码模板。


二、链表

链表是一种线性数据结构,它不要求数据在内存中连续存储,而是通过指针将一个个独立的节点串联起来。在数组模拟中,我们用数组下标来代替指针。

2.1 单链表

单链表的每个节点只包含一个指向下一个节点的指针。它的特点是只能从头向尾遍历,无法直接访问前驱节点。

2.1.1 核心思想
  • e[]数组:e[i] 表示地址为 i 的节点的下一个节点的地址(数组下标)。
  • data[]数组:data[i] 表示地址为 i 的节点存储的数据。
  • head 和 tail:分别指向链表的头节点和尾节点。头节点通常是哨兵节点,不存放数据。
  • idx:记录当前已使用的内存空间,用于分配新节点。用 -1 表示空指针。
2.1.2 核心操作
  • 初始化:int head = 0, tail = 0, idx = 0;
    int[] e = new int[N], data = new int[N];
    Arrays.fill(e, 1); // 初始化所有指针为空

  • 在尾部插入节点:// 将值val插入到链表尾部
    void insert_to_tail(int val) {
    e[tail] = ++idx; // 原尾节点的next指向新节点
    e[idx] = 1; // 新节点的next为空
    data[idx] = val; // 新节点存储数据
    tail = idx; // 更新尾节点
    }

  • 在指定节点后插入节点:// 将值val插入到地址为adr的节点之后
    void insert_after(int adr, int val) {
    ++idx; // 分配新节点地址
    e[idx] = e[adr]; // 新节点的next指向adr节点原来的next
    e[adr] = idx; // adr节点的next指向新节点
    data[idx] = val; // 新节点存储数据
    }

  • 删除指定节点后的节点:// 删除地址为adr的节点的下一个节点(adr不能是尾节点)
    void delete_next(int adr) {
    e[adr] = e[e[adr]]; // 让adr节点直接指向它下下个节点
    }


  • 2.2 双链表

    双链表在单链表的基础上,为每个节点增加了一个指向前驱节点的指针,因此可以双向遍历,插入和删除操作也更加灵活。

    2.2.1 核心思想
    • next[]数组:next[i] 表示地址为 i 的节点的下一个节点地址。
    • pre[]数组:pre[i] 表示地址为 i 的节点的上一个节点地址。
    • 哨兵节点:通常用 0 作为哨兵节点,将链表首尾相连,方便操作。
    2.2.2 经典例题:蓝桥OJ 3255 – 重新排队

    题目大意:初始有 1~n 按顺序排列,进行 m 次操作,每次将数字 x 移动到数字 y 的前面或后面,求最终顺序。

    解题思路:移动节点的通用操作是“先删除,再插入”。

  • 删除节点 x:找到 x 的前驱 a 和后继 b,让 a 和 b 直接相连。
  • 插入节点 x:将 x 插入到目标位置 y 的后面。如果要求插入到前面,则先找到 y 的前驱 pre[y],再插入到 pre[y] 后面。
  • Java代码实现:

    import java.util.Scanner;

    public class Main {
    static int N = (int)1e4 + 10;
    static int[] pre = new int[N];
    static int[] next = new int[N];

    // 删除节点x
    static void remove(int x) {
    int a = pre[x];
    int b = next[x];
    next[a] = b;
    pre[b] = a;
    }

    // 将x插入到y的后面
    static void add(int x, int y) {
    int z = next[y];
    pre[z] = x;
    pre[x] = y;
    next[y] = x;
    next[x] = z;
    }

    public static void main(String[] args) {
    Scanner scan = new Scanner(System.in);
    int n = scan.nextInt();
    int m = scan.nextInt();

    // 初始化双链表 1<->2<->…<->n
    for (int i = 1; i < n; i++) {
    pre[i] = i 1;
    next[i] = i + 1;
    }
    next[0] = 1; pre[0] = n; // 哨兵0
    next[n] = 0; pre[n] = n 1;

    while (m > 0) {
    int x = scan.nextInt();
    int y = scan.nextInt();
    int z = scan.nextInt(); // z=1表示插到y前面,z=0表示插到y后面

    if (z == 1) {
    y = pre[y]; // 插到y前面等价于插到pre[y]后面
    }
    // z=0表示x插到y后面(默认)
    remove(x);
    add(x, y);
    }

    // 遍历输出
    for (int i = next[0]; i > 0; i = next[i]) {
    System.out.print(i + " ");
    }
    scan.close();
    }
    }


    三、栈

    栈是一种“后进先出”(LIFO)的数据结构,只能在栈顶进行插入和删除操作。数组模拟栈非常高效。

    3.1 核心思想

    • stk[]数组:用于存储栈内元素。
    • top变量:指向栈顶元素。初始化时 top = 0 表示栈空。

    3.2 核心操作

  • 入栈(push):stk[++top] = x;
  • 出栈(pop):top–;
  • 取栈顶(peek):stk[top];
  • 判空:top == 0;
  • 3.3 经典例题:蓝桥OJ 2490 – 小蓝的括号串1

    题目大意:判断一个由 ( 和 ) 组成的括号串是否合法。

    解题思路:经典的栈应用。

  • 遍历括号串,遇到 ( 就入栈(用top++模拟)。
  • 遇到 ) 时,如果栈不为空(top > 0),则出栈(top–);如果栈为空,说明括号不合法。
  • 遍历结束后,如果栈为空(top == 0),则所有括号都匹配,合法;否则不合法。
  • Java代码实现:

    import java.util.Scanner;

    public class Main {
    public static void main(String[] args) {
    Scanner scan = new Scanner(System.in);
    int n = scan.nextInt();
    char[] c = scan.next().toCharArray();

    int top = 0;
    boolean valid = true;

    for (char x : c) {
    if (x == '(') {
    top++;
    } else {
    top;
    if (top < 0) { // 遇到多余的')'
    valid = false;
    break;
    }
    }
    }
    if (top != 0) { // 有多余的'('未匹配
    valid = false;
    }

    System.out.println(valid ? "Yes" : "No");
    scan.close();
    }
    }


    四、队列

    队列是一种“先进先出”(FIFO)的数据结构,元素从队尾入队,从队头出队。

    4.1 核心思想

    • q[]数组:用于存储队列元素。
    • h(head):队头指针,指向队头元素。
    • t(tail):队尾指针,指向队尾元素。
    • 初始化:h = 1, t = 0。此时 h > t,表示队列为空。有效元素区间为 [h, t]。

    4.2 核心操作

  • 入队(enqueue):q[++t] = x;
  • 出队(dequeue):h++;
  • 取队头:q[h];
  • 判空:t – h + 1 == 0 或 h > t。
  • 4.3 经典例题:蓝桥OJ 511 – 机器翻译

    题目大意:模拟一个内存容量为 M 的翻译软件。文章有 N 个单词。如果单词在内存中,直接翻译;否则查词典(计数+1),并将单词加入内存。如果内存满了,就删除最早进入内存的单词(FIFO)。

    解题思路:用队列模拟内存。

  • 遍历每个单词 x。
  • 扫描队列,检查 x 是否在内存中。如果在,跳过。
  • 如果不在,查词典次数 res++。
  • 如果队列长度(t-h+1)大于 M,则队头元素出队(h++)。
  • 将 x 入队。
  • Java代码实现(与图片一致):

    import java.util.Scanner;

    public class Main {
    public static void main(String[] args) {
    Scanner scan = new Scanner(System.in);
    int m = scan.nextInt(); // 内存容量
    int n = scan.nextInt(); // 单词数
    int[] queue = new int[n + 1];
    int h = 1, t = 0, res = 0;

    for (int i = 0; i < n; i++) {
    int x = scan.nextInt();
    boolean f = true;
    // 扫描队列,检查x是否在内存中
    for (int j = h; j <= t; j++) {
    if (queue[j] == x) {
    f = false;
    break;
    }
    }
    if (f) {
    // 不在内存中,需要查词典
    res++;
    queue[++t] = x;
    // 如果内存满了,先出队
    if (t h + 1 > m) {
    ++h;
    }
    }
    }
    System.out.println(res);
    scan.close();
    }
    }


    五、优先队列(堆)

    优先队列是一种队列数据结构实现,其中元素根据优先级处理,而非先进先出。在优先队列中,添加的对象根据其优先级排序,默认情况下,优先级由对象的自然顺序决定,队列构建时提供的比较器可以覆盖默认优先级。

    优先队列的底层实现就是一个堆,它可以维护一个集合的最大值(大顶堆)或最小值(小顶堆),并能在优秀的对数时间复杂度(O(log n))内完成查询、插入(push)和删除(pop)操作。

    5.1 核心思想

    • 堆(Heap):优先队列的核心是堆,它是一个完全二叉树。
      • 大顶堆:父节点的值大于等于子节点的值,堆顶为最大值。
      • 小顶堆:父节点的值小于等于子节点的值,堆顶为最小值。
    • 数组模拟:在竞赛中,常用数组来模拟堆。数组下标从1开始,对于节点i,其左孩子为2*i,右孩子为2*i+1,父节点为i/2。

    5.2 核心操作

  • 上浮(siftUp):当新元素插入堆底时,通过与父节点比较并交换,将其移动到正确位置,以维护堆的性质。
  • 下沉(siftDown):当堆顶元素被移除后,将堆底元素移到堆顶,然后通过与子节点比较并交换,将其移动到正确位置,以维护堆的性质。
  • 5.3 经典例题:蓝桥OJ 3343 – 简单的取模问题

    题目大意:给定一个数组 a,共有 q 次操作,每次操作给定一个值 x,对数组的每个元素对 x 进行一次取模,输出每次操作后数组所有元素的和。

    解题思路:

  • 直接对每个元素取模会导致O(nq)的时间复杂度,对于大数据量会超时。
  • 我们可以使用一个大顶堆来维护数组中的元素。每次操作时,只对堆顶大于等于x的元素进行取模,然后将取模后的值重新插入堆中。
  • 这样可以避免对所有元素进行不必要的操作,将时间复杂度优化到O((n+q)log n)。
  • Java代码实现:

    import java.util.PriorityQueue;
    import java.util.Scanner;

    public class Main {
    public static void main(String[] args) {
    Scanner scan = new Scanner(System.in);
    int n = scan.nextInt();
    int q = scan.nextInt();
    PriorityQueue<Integer> queue = new PriorityQueue<>((o1, o2) -> o2 o1);
    long sum = 0;
    for (int i = 0; i < n; i++) {
    int x = scan.nextInt();
    queue.add(x);
    sum += x;
    }
    while (q > 0) {
    int k = scan.nextInt();
    while (!queue.isEmpty() && queue.peek() >= k) {
    int x = queue.poll();
    sum -= x;
    sum += x % k;
    queue.add(x % k);
    }
    System.out.println(sum);
    }
    scan.close();
    }
    }

    六、总结

    数据结构核心数组/变量核心操作典型应用
    单链表 e[], data[], head, tail, idx 头/尾插、指定位置插删 邻接表、静态链表
    双链表 pre[], next[] 任意位置插删(O(1)) 重新排队、LRU缓存
    stk[], top push, pop 括号匹配、表达式求值、DFS
    队列 q[], h, t enqueue, dequeue BFS、缓冲区、FIFO替换策略
    优先队列(堆) heap[], size push, pop, peek TopK问题、贪心算法、事件调度
    赞(0)
    未经允许不得转载:171主机测评 » 数组模拟链表、栈、队列与优先队列:高效实现与性能对比
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址