28、指针与数据结构:栈、队列、树的指针实现

各位同学,今天我们来聊聊指针在数据结构里的核心作用。说实话,我当年学数据结构时,最头疼的就是指针。但后来做了十几年嵌入式开发,我才真正明白——没有指针,很多数据结构根本玩不转

栈、队列、树,这些听起来抽象的概念,底层全靠指针在撑。你想想看,如果没有指针,你怎么让一个节点“指向”另一个节点?数组?那只能顺序访问,灵活性差太多了。

指针为什么是数据结构的灵魂?

我个人的理解是:指针提供了“连接”的能力。数据结构说白了就是数据之间的关系,而指针就是描述这种关系的工具。

  • 静态数组:数据在内存里排排坐,位置固定
  • 指针链表:数据可以散落在任何地方,指针告诉你怎么找到下一个

我在项目中遇到过一个问题:用数组实现栈,栈大小固定,一旦满了就崩。后来改用指针实现的链式栈,动态分配内存,再也没出过这问题。

核心观点:指针让数据结构从“静态”变成了“动态”。没有指针,你只能做固定大小的数据结构;有了指针,你可以做任意大小、任意形状的数据结构。

栈的指针实现

栈,先进后出。用数组实现很简单,但用指针实现更灵活。我习惯用单链表来实现栈——入栈就是在链表头部插入,出栈就是删除头部节点。

// 链式栈的节点定义
typedef struct StackNode {
    int data;
    struct StackNode* next;
} StackNode;

// 栈结构体(只保存栈顶指针)
typedef struct {
    StackNode* top;
    int size;
} LinkedStack;

// 入栈操作
void push(LinkedStack* stack, int value) {
    StackNode* newNode = (StackNode*)malloc(sizeof(StackNode));
    if (!newNode) return;  // 内存分配失败
    
    newNode->data = value;
    newNode->next = stack->top;  // 新节点指向原来的栈顶
    stack->top = newNode;        // 更新栈顶
    stack->size++;
}

// 出栈操作
int pop(LinkedStack* stack) {
    if (stack->top == NULL) {
        printf("栈已空\n");
        return -1;
    }
    StackNode* temp = stack->top;
    int value = temp->data;
    stack->top = temp->next;  // 栈顶下移
    free(temp);
    stack->size--;
    return value;
}

你看,这里最关键的就是 stack->top = newNode->nextstack->top = temp->next 这两行。说白了,就是通过指针的重新指向,完成了节点的插入和删除。时间复杂度 O(1),非常高效。

我的经验:用指针实现栈时,一定要记得检查 malloc 的返回值。我曾经在资源紧张的嵌入式设备上吃过亏——malloc 返回 NULL,程序直接崩溃。从那以后,我每次 malloc 后面必跟判空。

队列的指针实现

队列,先进先出。用数组实现队列有个问题:出队后前面的空间就浪费了。用指针实现就没有这个烦恼。

队列需要两个指针:front 指向队头,rear 指向队尾。入队操作在 rear 后面加节点,出队操作删除 front 节点。

// 链式队列的节点定义
typedef struct QueueNode {
    int data;
    struct QueueNode* next;
} QueueNode;

// 队列结构体
typedef struct {
    QueueNode* front;  // 队头指针
    QueueNode* rear;   // 队尾指针
    int size;
} LinkedQueue;

// 入队
void enqueue(LinkedQueue* queue, int value) {
    QueueNode* newNode = (QueueNode*)malloc(sizeof(QueueNode));
    if (!newNode) return;
    
    newNode->data = value;
    newNode->next = NULL;
    
    if (queue->rear == NULL) {
        // 队列为空
        queue->front = newNode;
        queue->rear = newNode;
    } else {
        queue->rear->next = newNode;  // 原队尾指向新节点
        queue->rear = newNode;        // 更新队尾
    }
    queue->size++;
}

// 出队
int dequeue(LinkedQueue* queue) {
    if (queue->front == NULL) {
        printf("队列已空\n");
        return -1;
    }
    QueueNode* temp = queue->front;
    int value = temp->data;
    queue->front = temp->next;  // 队头后移
    
    if (queue->front == NULL) {
        queue->rear = NULL;  // 队列变空,rear也要置空
    }
    free(temp);
    queue->size--;
    return value;
}

注意:出队时如果队列变空,一定要把 rear 也置为 NULL。我见过有人只更新 front,结果 rear 还指着已经释放的内存,下次入队时直接野指针崩溃。

树的指针实现

树,这是指针发挥到极致的数据结构。每个节点可以有多个子节点,全靠指针来维系。

二叉树是最常见的树结构。每个节点有三个指针:左孩子、右孩子,以及(可选)父节点。

// 二叉树节点定义
typedef struct TreeNode {
    int data;
    struct TreeNode* left;   // 左孩子指针
    struct TreeNode* right;  // 右孩子指针
    struct TreeNode* parent; // 父节点指针(可选)
} TreeNode;

// 创建新节点
TreeNode* createNode(int value) {
    TreeNode* node = (TreeNode*)malloc(sizeof(TreeNode));
    if (!node) return NULL;
    
    node->data = value;
    node->left = NULL;
    node->right = NULL;
    node->parent = NULL;
    return node;
}

// 插入左孩子
void insertLeft(TreeNode* parent, int value) {
    if (parent->left != NULL) {
        printf("左孩子已存在\n");
        return;
    }
    TreeNode* child = createNode(value);
    if (child) {
        parent->left = child;
        child->parent = parent;
    }
}

// 前序遍历(递归)
void preorder(TreeNode* root) {
    if (root == NULL) return;
    
    printf("%d ", root->data);   // 访问根节点
    preorder(root->left);        // 遍历左子树
    preorder(root->right);       // 遍历右子树
}

你看,树的遍历本质上就是沿着指针走。递归也好,非递归也好,核心都是通过指针从一个节点跳到另一个节点。

我做过一个项目,需要快速查找数据。用数组?查找 O(n)。用二叉搜索树?查找 O(log n)。为什么?因为指针让树可以二分查找——每次比较后,通过 left 或 right 指针直接跳到下一层,不用遍历所有数据。

指针在数据结构中的核心作用

总结一下,指针在数据结构里到底扮演了什么角色?我归纳了三点:

作用 说明 举例
连接 把分散的内存块串起来 链表中的 next 指针
定位 快速找到目标节点 树的 left/right 指针
动态扩展 运行时分配新节点,改变结构 栈的 push 操作

说白了,没有指针,数据结构就是一堆死数据。有了指针,数据才有了“关系”,才有了“结构”。

一句话总结:指针是数据结构的“粘合剂”。它把零散的数据块粘合成你想要的形状——栈、队列、树,甚至图。

知识体系图

下面这张图展示了指针与三种数据结构的关系:

指针 栈 (Stack) top 指针 队列 (Queue) front/rear 指针 树 (Tree) left/right 指针 连接数据 快速定位 动态扩展

这张图想表达的是:指针是中心,栈、队列、树都依赖指针来实现各自的结构特性。没有指针,这些数据结构就失去了“动态”的灵魂。

避坑指南

最后,分享几个我踩过的坑:

  • 野指针:节点释放后,记得把指针置 NULL。我早期写代码时经常忘了,结果程序跑着跑着就崩了。
  • 内存泄漏:用 malloc 分配了节点,用完一定要 free。嵌入式设备内存小,泄漏几次就挂了。
  • 空指针检查:每次访问指针前,先判断是不是 NULL。这个习惯救了我很多次。

我的习惯:写数据结构代码时,我会先画图。把指针的指向画清楚,再写代码。这样逻辑清晰,不容易出错。你也不妨试试。

好了,指针与数据结构就讲到这里。记住一句话:指针是数据结构的骨架,没有它,数据就是一盘散沙


公众号:蓝海资料掘金营,微信deep3321