在编程世界中,数据结构是构建高效程序的基石。无论是日常开发中的数据存储,还是算法题中的逻辑实现,掌握核心数据结构及其 C 语言实现都至关重要。本文将从线性表(顺序表、链表)入手,逐步深入栈、队列,最终聚焦二叉树与堆,结合完整代码实现与实战场景,带大家从零到一掌握这些核心数据结构。
一、线性表:数据结构的基础
线性表是 n 个具有相同特性的数据元素的有限序列,逻辑上呈线性结构,物理存储分为连续(数组)和非连续(链式)两种形式,是栈、队列等复杂结构的基础。
1.1 顺序表:数组的封装与优化
1.1.1 核心概念
顺序表是用物理地址连续的存储单元存储数据的线性结构,底层基于数组实现,封装了增删改查等常用接口,解决了原生数组灵活性不足的问题。类比来看,数组就像 "炒西蓝花",而顺序表则是 "绿野仙踪"------ 在基础食材上增加了规范化的处理流程。
1.1.2 分类与缺陷
-
静态顺序表:使用定长数组存储,缺陷明显:空间给少了不够用,给多了造成浪费。
// 静态顺序表
typedef int SLDataType;
#define N 7
typedef struct SeqList {
SLDataType a[N]; // 定长数组
int size; // 有效数据个数
} SL; -
动态顺序表 :按需申请空间,通过
capacity记录容量,size记录有效数据个数,支持动态扩容。// 动态顺序表
typedef int SLDataType;
#define INIT_CAPACITY 4
typedef struct SeqList {
SLDataType* a; // 动态数组指针
int size; // 有效数据个数
int capacity; // 空间容量
} SL;
1.1.3 核心接口实现
动态顺序表的关键在于扩容机制和高效的增删改查,以下是核心接口的完整实现:
// SeqList.h 头文件声明
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
typedef int SLDataType;
#define INIT_CAPACITY 4
typedef struct SeqList {
SLDataType* a;
int size;
int capacity;
} SL;
// 初始化和销毁
void SLInit(SL* ps);
void SLDestroy(SL* ps);
void SLPrint(SL* ps);
// 扩容
void SLCheckCapacity(SL* ps);
// 头部/尾部插入删除
void SLPushBack(SL* ps, SLDataType x);
void SLPopBack(SL* ps);
void SLPushFront(SL* ps, SLDataType x);
void SLPopFront(SL* ps);
// 指定位置插入/删除
void SLInsert(SL* ps, int pos, SLDataType x);
void SLErase(SL* ps, int pos);
int SLFind(SL* ps, SLDataType x);
// SeqList.c 实现
void SLInit(SL* ps) {
assert(ps);
ps->a = (SLDataType*)malloc(sizeof(SLDataType) * INIT_CAPACITY);
if (ps->a == NULL) {
perror("malloc fail");
exit(EXIT_FAILURE);
}
ps->size = 0;
ps->capacity = INIT_CAPACITY;
}
void SLDestroy(SL* ps) {
assert(ps);
free(ps->a);
ps->a = NULL;
ps->size = ps->capacity = 0;
}
void SLPrint(SL* ps) {
assert(ps);
for (int i = 0; i < ps->size; i++) {
printf("%d ", ps->a[i]);
}
printf("\n");
}
void SLCheckCapacity(SL* ps) {
assert(ps);
if (ps->size == ps->capacity) {
// 扩容为原来的2倍
SLDataType* tmp = (SLDataType*)realloc(ps->a, sizeof(SLDataType) * ps->capacity * 2);
if (tmp == NULL) {
perror("realloc fail");
exit(EXIT_FAILURE);
}
ps->a = tmp;
ps->capacity *= 2;
}
}
// 尾插
void SLPushBack(SL* ps, SLDataType x) {
assert(ps);
SLCheckCapacity(ps);
ps->a[ps->size++] = x;
}
// 尾删
void SLPopBack(SL* ps) {
assert(ps && ps->size > 0);
ps->size--;
}
// 头插
void SLPushFront(SL* ps, SLDataType x) {
assert(ps);
SLCheckCapacity(ps);
// 数据后移
for (int i = ps->size; i > 0; i--) {
ps->a[i] = ps->a[i - 1];
}
ps->a[0] = x;
ps->size++;
}
// 头删
void SLPopFront(SL* ps) {
assert(ps && ps->size > 0);
// 数据前移
for (int i = 0; i < ps->size - 1; i++) {
ps->a[i] = ps->a[i + 1];
}
ps->size--;
}
// 按位置插入
void SLInsert(SL* ps, int pos, SLDataType x) {
assert(ps);
assert(pos >= 0 && pos <= ps->size);
SLCheckCapacity(ps);
// 从pos位置开始后移
for (int i = ps->size; i > pos; i--) {
ps->a[i] = ps->a[i - 1];
}
ps->a[pos] = x;
ps->size++;
}
// 按位置删除
void SLErase(SL* ps, int pos) {
assert(ps && ps->size > 0);
assert(pos >= 0 && pos < ps->size);
// 从pos+1位置开始前移
for (int i = pos; i < ps->size - 1; i++) {
ps->a[i] = ps->a[i + 1];
}
ps->size--;
}
// 查找元素
int SLFind(SL* ps, SLDataType x) {
assert(ps);
for (int i = 0; i < ps->size; i++) {
if (ps->a[i] == x) {
return i;
}
}
return -1; // 未找到
}
1.1.4 优缺点分析
- 优点:支持随机访问(O (1) 时间复杂度),尾插尾删效率高。
- 缺点:中间 / 头部插入删除需移动元素(O (N) 时间复杂度);扩容存在空间浪费和拷贝开销。
1.2 链表:非连续存储的灵活选择
1.2.1 核心概念
链表是物理存储非连续、逻辑连续的线性结构,通过指针链接结点实现数据访问。每个结点包含数据域和指针域,类比火车车厢 ------ 每节车厢独立存在,通过挂钩连接。
1.2.2 单链表结构与实现
单链表是最基础的链表结构,每个结点仅存储下一个结点的地址,以下是完整实现:
// SList.h 头文件声明
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
typedef int SLTDataType;
// 单链表结点结构
typedef struct SListNode {
SLTDataType data; // 数据域
struct SListNode* next; // 指针域,指向next结点
} SLTNode;
// 打印链表
void SLTPrint(SLTNode* phead);
// 头部/尾部插入删除
void SLTPushBack(SLTNode** pphead, SLTDataType x);
void SLTPopBack(SLTNode** pphead);
void SLTPushFront(SLTNode** pphead, SLTDataType x);
void SLTPopFront(SLTNode** pphead);
// 查找结点
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
// 指定位置插入/删除
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
void SLTErase(SLTNode** pphead, SLTNode* pos);
// 销毁链表
void SListDestroy(SLTNode** pphead);
// SList.c 实现
// 创建新结点
SLTNode* BuySLTNode(SLTDataType x) {
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL) {
perror("malloc fail");
exit(EXIT_FAILURE);
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
// 打印链表
void SLTPrint(SLTNode* phead) {
SLTNode* pcur = phead;
while (pcur) {
printf("%d ", pcur->data);
pcur = pcur->next;
}
printf("\n");
}
// 尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x) {
assert(pphead);
SLTNode* newnode = BuySLTNode(x);
if (*pphead == NULL) { // 空链表
*pphead = newnode;
return;
}
// 找到尾结点
SLTNode* ptail = *pphead;
while (ptail->next) {
ptail = ptail->next;
}
ptail->next = newnode;
}
// 尾删
void SLTPopBack(SLTNode** pphead) {
assert(pphead && *pphead);
// 只有一个结点
if ((*pphead)->next == NULL) {
free(*pphead);
*pphead = NULL;
return;
}
// 找到倒数第二个结点
SLTNode* prev = NULL;
SLTNode* ptail = *pphead;
while (ptail->next) {
prev = ptail;
ptail = ptail->next;
}
free(ptail);
prev->next = NULL;
}
// 头插
void SLTPushFront(SLTNode** pphead, SLTDataType x) {
assert(pphead);
SLTNode* newnode = BuySLTNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
// 头删
void SLTPopFront(SLTNode** pphead) {
assert(pphead && *pphead);
SLTNode* tmp = *pphead;
*pphead = (*pphead)->next;
free(tmp);
tmp = NULL;
}
// 查找结点
SLTNode* SLTFind(SLTNode* phead, SLTDataType x) {
SLTNode* pcur = phead;
while (pcur) {
if (pcur->data == x) {
return pcur;
}
pcur = pcur->next;
}
return NULL; // 未找到
}
// 销毁链表
void SListDestroy(SLTNode** pphead) {
assert(pphead);
SLTNode* pcur = *pphead;
while (pcur) {
SLTNode* tmp = pcur;
pcur = pcur->next;
free(tmp);
}
*pphead = NULL;
}
1.2.3 链表 vs 顺序表:核心对比
| 对比维度 | 顺序表 | 链表(单链表) |
|---|---|---|
| 存储空间 | 物理连续 | 逻辑连续,物理非连续 |
| 随机访问 | 支持(O (1)) | 不支持(O (N)) |
| 插入删除 | 中间 / 头部 O (N) | 仅需修改指针(O (1)) |
| 空间效率 | 可能存在扩容浪费 | 按需申请,无浪费 |
| 应用场景 | 频繁访问,少量增删 | 频繁增删,少量访问 |
二、栈与队列:受限的线性表
栈和队列是特殊的线性表,其操作受到限制,分别遵循 "后进先出" 和 "先进先出" 原则,广泛应用于算法优化、数据缓冲等场景。
2.1 栈:后进先出(LIFO)
2.1.1 核心概念
栈仅允许在栈顶进行插入(压栈)和删除(出栈)操作,类比叠盘子 ------ 最后放的盘子最先被拿走。栈的实现优先选择数组,因为数组尾插尾删效率高。
2.1.2 完整实现
// stack.h 头文件声明
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
typedef int STDataType;
typedef struct Stack {
STDataType* a; // 数组存储
int top; // 栈顶指针(指向栈顶元素下一个位置)
int capacity; // 容量
} ST;
// 初始化栈
void STInit(ST* ps);
// 销毁栈
void STDestroy(ST* ps);
// 入栈
void STPush(ST* ps, STDataType x);
// 出栈
void STPop(ST* ps);
// 取栈顶元素
STDataType STTop(ST* ps);
// 获取栈大小
int STSize(ST* ps);
// 判断栈是否为空
bool STEmpty(ST* ps);
// stack.c 实现
void STInit(ST* ps) {
assert(ps);
ps->a = (STDataType*)malloc(sizeof(STDataType) * 4);
if (ps->a == NULL) {
perror("malloc fail");
exit(EXIT_FAILURE);
}
ps->top = 0;
ps->capacity = 4;
}
void STDestroy(ST* ps) {
assert(ps);
free(ps->a);
ps->a = NULL;
ps->top = ps->capacity = 0;
}
void STPush(ST* ps, STDataType x) {
assert(ps);
// 扩容
if (ps->top == ps->capacity) {
STDataType* tmp = (STDataType*)realloc(ps->a, sizeof(STDataType) * ps->capacity * 2);
if (tmp == NULL) {
perror("realloc fail");
exit(EXIT_FAILURE);
}
ps->a = tmp;
ps->capacity *= 2;
}
ps->a[ps->top++] = x;
}
void STPop(ST* ps) {
assert(ps && !STEmpty(ps));
ps->top--;
}
STDataType STTop(ST* ps) {
assert(ps && !STEmpty(ps));
return ps->a[ps->top - 1];
}
int STSize(ST* ps) {
assert(ps);
return ps->top;
}
bool STEmpty(ST* ps) {
assert(ps);
return ps->top == 0;
}
2.2 队列:先进先出(FIFO)
2.2.1 核心概念
队列允许在队尾插入(入队)、队头删除(出队),类比排队买票 ------ 先到的人先买票。队列优先选择链表实现,避免数组头删时的数据移动。
2.2.2 完整实现
// Queue.h 头文件声明
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
typedef int QDataType;
// 队列结点
typedef struct QueueNode {
QDataType val;
struct QueueNode* next;
} QNode;
// 队列结构(管理队头和队尾)
typedef struct Queue {
QNode* phead;
QNode* ptail;
int size;
} Queue;
// 初始化队列
void QueueInit(Queue* pq);
// 销毁队列
void QueueDestroy(Queue* pq);
// 入队
void QueuePush(Queue* pq, QDataType x);
// 出队
void QueuePop(Queue* pq);
// 取队头元素
QDataType QueueFront(Queue* pq);
// 取队尾元素
QDataType QueueBack(Queue* pq);
// 判断队列是否为空
bool QueueEmpty(Queue* pq);
// 获取队列大小
int QueueSize(Queue* pq);
// Queue.c 实现
void QueueInit(Queue* pq) {
assert(pq);
pq->phead = pq->ptail = NULL;
pq->size = 0;
}
void QueueDestroy(Queue* pq) {
assert(pq);
QNode* pcur = pq->phead;
while (pcur) {
QNode* tmp = pcur;
pcur = pcur->next;
free(tmp);
}
pq->phead = pq->ptail = NULL;
pq->size = 0;
}
void QueuePush(Queue* pq, QDataType x) {
assert(pq);
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL) {
perror("malloc fail");
exit(EXIT_FAILURE);
}
newnode->val = x;
newnode->next = NULL;
if (pq->ptail == NULL) { // 空队列
pq->phead = pq->ptail = newnode;
} else {
pq->ptail->next = newnode;
pq->ptail = newnode;
}
pq->size++;
}
void QueuePop(Queue* pq) {
assert(pq && !QueueEmpty(pq));
if (pq->phead->next == NULL) { // 只有一个结点
free(pq->phead);
pq->phead = pq->ptail = NULL;
} else {
QNode* tmp = pq->phead;
pq->phead = pq->phead->next;
free(tmp);
}
pq->size--;
}
QDataType QueueFront(Queue* pq) {
assert(pq && !QueueEmpty(pq));
return pq->phead->val;
}
QDataType QueueBack(Queue* pq) {
assert(pq && !QueueEmpty(pq));
return pq->ptail->val;
}
bool QueueEmpty(Queue* pq) {
assert(pq);
return pq->size == 0;
}
int QueueSize(Queue* pq) {
assert(pq);
return pq->size;
}
三、二叉树:非线性数据结构的核心
二叉树是树形结构的核心,每个结点最多有两个子树(左子树、右子树),是非线性数据结构,广泛应用于搜索、排序、文件系统等场景。
3.1 核心概念与结构
-
满二叉树:每一层结点数达到最大值,层数为 k 时结点总数为 2ᵏ-1。
-
完全二叉树:除最后一层外,每一层结点数均满,最后一层结点从左到右连续排列。
-
链式存储:每个结点包含数据域、左指针(指向左子树)、右指针(指向右子树)。
// 二叉树结点结构
typedef int BTDataType;
typedef struct BinaryTreeNode {
BTDataType val;
struct BinaryTreeNode* left; // 左子树指针
struct BinaryTreeNode* right; // 右子树指针
} BTNode;
3.2 二叉树的遍历
遍历是二叉树操作的基础,分为前序、中序、后序(递归实现)和层序(队列实现)四种方式。
3.2.1 递归遍历实现
// 创建新结点
BTNode* BuyBTNode(BTDataType x) {
BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));
if (newnode == NULL) {
perror("malloc fail");
exit(EXIT_FAILURE);
}
newnode->val = x;
newnode->left = newnode->right = NULL;
return newnode;
}
// 前序遍历:根 -> 左 -> 右
void PreOrder(BTNode* root) {
if (root == NULL) {
printf("N "); // 用N表示空结点
return;
}
printf("%d ", root->val);
PreOrder(root->left);
PreOrder(root->right);
}
// 中序遍历:左 -> 根 -> 右
void InOrder(BTNode* root) {
if (root == NULL) {
printf("N ");
return;
}
InOrder(root->left);
printf("%d ", root->val);
InOrder(root->right);
}
// 后序遍历:左 -> 右 -> 根
void PostOrder(BTNode* root) {
if (root == NULL) {
printf("N ");
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%d ", root->val);
}
3.2.2 层序遍历实现(借助队列)
// 层序遍历:自上而下、自左至右
void LevelOrder(BTNode* root) {
if (root == NULL) return;
Queue q;
QueueInit(&q);
QueuePush(&q, root); // 根结点入队
while (!QueueEmpty(&q)) {
BTNode* front = QueueFront(&q);
QueuePop(&q);
printf("%d ", front->val);
// 左孩子入队
if (front->left) {
QueuePush(&q, front->left);
}
// 右孩子入队
if (front->right) {
QueuePush(&q, front->right);
}
}
QueueDestroy(&q);
printf("\n");
}
3.3 堆:特殊的完全二叉树
堆是满足 "双亲结点值大于等于(大堆)或小于等于(小堆)子结点值" 的完全二叉树,常用于堆排序和 Top-K 问题。
3.3.1 堆的实现(大堆)
// Heap.h 头文件声明
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
typedef int HPDataType;
typedef struct Heap {
HPDataType* a;
int size;
int capacity;
} HP;
// 初始化堆
void HPInit(HP* php);
// 销毁堆
void HPDestroy(HP* php);
// 插入元素
void HPPush(HP* php, HPDataType x);
// 删除堆顶元素
void HPPop(HP* php);
// 取堆顶元素
HPDataType HPTop(HP* php);
// 判断堆是否为空
bool HPEmpty(HP* php);
// 获取堆大小
int HPSize(HP* php);
// Heap.c 实现
// 交换两个元素
void Swap(HPDataType* a, HPDataType* b) {
HPDataType tmp = *a;
*a = *b;
*b = tmp;
}
// 向上调整(插入元素后维护堆结构)
void AdjustUp(HPDataType* a, int child) {
int parent = (child - 1) / 2;
while (child > 0) {
if (a[child] > a[parent]) { // 大堆:孩子>父亲则交换
Swap(&a[child], &a[parent]);
child = parent;
parent = (parent - 1) / 2;
} else {
break;
}
}
}
// 向下调整(删除堆顶后维护堆结构)
void AdjustDown(HPDataType* a, int n, int parent) {
int child = parent * 2 + 1; // 左孩子
while (child < n) {
// 选择左右孩子中较大的一个
if (child + 1 < n && a[child + 1] > a[child]) {
child++;
}
// 孩子>父亲则交换
if (a[child] > a[parent]) {
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
} else {
break;
}
}
}
void HPInit(HP* php) {
assert(php);
php->a = NULL;
php->size = php->capacity = 0;
}
void HPDestroy(HP* php) {
assert(php);
free(php->a);
php->a = NULL;
php->size = php->capacity = 0;
}
void HPPush(HP* php, HPDataType x) {
assert(php);
// 扩容
if (php->size == php->capacity) {
int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newCapacity);
if (tmp == NULL) {
perror("realloc fail");
exit(EXIT_FAILURE);
}
php->a = tmp;
php->capacity = newCapacity;
}
// 插入到末尾并向上调整
php->a[php->size++] = x;
AdjustUp(php->a, php->size - 1);
}
void HPPop(HP* php) {
assert(php && !HPEmpty(php));
// 交换堆顶和最后一个元素
Swap(&php->a[0], &php->a[php->size - 1]);
php->size--;
// 向下调整堆顶
AdjustDown(php->a, php->size, 0);
}
HPDataType HPTop(HP* php) {
assert(php && !HPEmpty(php));
return php->a[0];
}
bool HPEmpty(HP* php) {
assert(php);
return php->size == 0;
}
int HPSize(HP* php) {
assert(php);
return php->size;
}
3.3.2 堆的应用:Top-K 问题
Top-K 问题是求海量数据中前 K 个最大 / 最小元素,用堆实现效率极高(时间复杂度 O(nlogk))。
// 求前K个最大元素(用小堆实现)
void TopK(int* a, int n, int k) {
assert(a && n > 0 && k > 0 && k <= n);
HP hp;
HPInit(&hp);
// 用前k个元素建小堆
for (int i = 0; i < k; i++) {
HPPush(&hp, a[i]);
}
// 剩余元素与堆顶比较,大于堆顶则替换并调整
for (int i = k; i < n; i++) {
if (a[i] > HPTop(&hp)) {
hp.a[0] = a[i];
AdjustDown(hp.a, hp.size, 0);
}
}
// 输出结果
printf("前K个最大元素:");
while (!HPEmpty(&hp)) {
printf("%d ", HPTop(&hp));
HPPop(&hp);
}
printf("\n");
HPDestroy(&hp);
}
四、实战演练:经典算法题
4.1 顺序表算法题:移除元素(LeetCode 27)
int removeElement(int* nums, int numsSize, int val) {
int dest = 0;
for (int src = 0; src < numsSize; src++) {
if (nums[src] != val) {
nums[dest++] = nums[src];
}
}
return dest;
}
4.2 链表算法题:反转链表(LeetCode 206)
BTNode* reverseList(BTNode* head) {
BTNode* prev = NULL;
BTNode* cur = head;
while (cur) {
BTNode* next = cur->next;
cur->next = prev;
prev = cur;
cur = next;
}
return prev;
}
4.3 栈算法题:有效的括号(LeetCode 20)
bool isValid(char* s) {
ST st;
STInit(&st);
while (*s != '\0') {
if (*s == '(' || *s == '[' || *s == '{') {
STPush(&st, *s);
} else {
if (STEmpty(&st)) {
STDestroy(&st);
return false;
}
char top = STTop(&st);
STPop(&st);
if ((*s == ')' && top != '(') ||
(*s == ']' && top != '[') ||
(*s == '}' && top != '{')) {
STDestroy(&st);
return false;
}
}
s++;
}
bool ret = STEmpty(&st);
STDestroy(&st);
return ret;
}