1. 线性表:数据结构中的基石
线性表是数据结构中最基础、最常用的结构之一,它就像一条直线上的珍珠项链,数据元素一个接一个有序排列。在C语言中,我们可以用多种方式实现线性表,每种方式都有其独特的应用场景和性能特点。
线性表的核心特征在于数据元素之间存在"一对一"的关系。除了第一个和最后一个数据元素外,其他元素都有且仅有一个直接前驱和一个直接后继。这种简单的结构却构成了许多复杂算法的基础。
新手常见误区:很多人认为数组就是线性表,实际上数组只是实现线性表的一种方式。线性表是逻辑结构,而数组是物理存储结构。
1.1 线性表的基本操作
无论采用哪种实现方式,线性表通常都支持以下基本操作:
- InitList(&L): 初始化一个空线性表
- ListInsert(&L,i,e): 在位置i插入元素e
- ListDelete(&L,i,&e): 删除位置i的元素并用e返回
- LocateElem(L,e): 查找元素e的位置
- GetElem(L,i): 获取位置i的元素
- Length(L): 获取线性表长度
- PrintList(L): 输出线性表所有元素
在C语言中,我们可以用结构体来封装线性表的数据和操作:
c复制typedef struct {
ElemType *data; // 存储空间基址
int length; // 当前长度
int capacity; // 最大容量
} SqList;
1.2 线性表的两种主要实现方式
线性表主要有两种实现方式:顺序表和链表。它们各有优缺点:
| 特性 | 顺序表 | 链表 |
|---|---|---|
| 存储方式 | 连续内存空间 | 非连续内存空间 |
| 随机访问 | O(1) | O(n) |
| 插入/删除 | O(n) | O(1) |
| 空间利用率 | 高(无额外开销) | 低(需要指针空间) |
| 适用场景 | 查询多、修改少 | 频繁插入删除 |
在实际项目中,选择哪种实现取决于具体的应用场景。例如,如果程序需要频繁按索引访问元素但很少修改结构,顺序表是更好的选择;如果需要频繁插入删除元素,链表则更合适。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 顺序表的C语言实现
顺序表是用一段地址连续的存储单元依次存储线性表的数据元素。在C语言中,我们通常用数组来实现顺序表。
2.1 顺序表的结构定义
c复制#define INIT_SIZE 10 // 初始容量
#define INC_SIZE 5 // 增量大小
typedef int ElemType; // 元素类型可根据需要修改
typedef struct {
ElemType *data; // 存储空间基址
int length; // 当前长度
int capacity; // 当前分配的存储容量
} SqList;
2.2 顺序表的基本操作实现
2.2.1 初始化顺序表
c复制Status InitList(SqList *L) {
L->data = (ElemType *)malloc(INIT_SIZE * sizeof(ElemType));
if (!L->data) {
return ERROR; // 内存分配失败
}
L->length = 0;
L->capacity = INIT_SIZE;
return OK;
}
2.2.2 插入元素
c复制Status ListInsert(SqList *L, int i, ElemType e) {
// 判断插入位置是否合法
if (i < 1 || i > L->length + 1) {
return ERROR;
}
// 如果空间已满,则扩容
if (L->length >= L->capacity) {
ElemType *newbase = (ElemType *)realloc(L->data,
(L->capacity + INC_SIZE) * sizeof(ElemType));
if (!newbase) return ERROR;
L->data = newbase;
L->capacity += INC_SIZE;
}
// 将第i个位置及之后的元素后移
for (int j = L->length; j >= i; j--) {
L->data[j] = L->data[j-1];
}
// 插入新元素
L->data[i-1] = e;
L->length++;
return OK;
}
实际开发中的经验:在插入操作中,我们采用了动态扩容的策略。这是一种常见的做法,但扩容因子(这里是INC_SIZE)的选择很重要。太小会导致频繁扩容,太大会浪费内存。Java的ArrayList扩容因子是1.5,这是一个经过实践检验的值。
2.2.3 删除元素
c复制Status ListDelete(SqList *L, int i, ElemType *e) {
// 判断删除位置是否合法
if (i < 1 || i > L->length) {
return ERROR;
}
// 获取被删除元素
*e = L->data[i-1];
// 将第i个位置之后的元素前移
for (int j = i; j < L->length; j++) {
L->data[j-1] = L->data[j];
}
L->length--;
return OK;
}
2.3 顺序表的优缺点分析
顺序表的最大优势是支持随机访问,任何位置的元素都可以在O(1)时间内访问到。这使得它在需要频繁按索引访问元素的场景中表现优异。
然而,顺序表的插入和删除操作平均需要移动一半的元素,时间复杂度为O(n)。当数据量很大时,这会成为性能瓶颈。此外,顺序表需要预先分配连续的内存空间,当数据量不确定时,要么浪费内存,要么需要频繁扩容。
3. 链表的C语言实现
链表通过指针将一组零散的内存块串联起来,每个节点除了存储数据外,还存储了下一个节点的地址。链表不需要连续的内存空间,可以充分利用零散的内存。
3.1 单链表的结构定义
c复制typedef struct LNode {
ElemType data; // 数据域
struct LNode *next; // 指针域
} LNode, *LinkList;
3.2 单链表的基本操作
3.2.1 头插法创建链表
c复制void CreateList_Head(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 = rand() % 100 + 1; // 随机生成数据
p->next = (*L)->next;
(*L)->next = p;
}
}
3.2.2 尾插法创建链表
c复制void CreateList_Tail(LinkList *L, int n) {
*L = (LinkList)malloc(sizeof(LNode));
(*L)->next = NULL;
LNode *r = *L; // r指向尾结点
for (int i = 0; i < n; i++) {
LNode *p = (LNode *)malloc(sizeof(LNode));
p->data = rand() % 100 + 1;
p->next = NULL;
r->next = p;
r = p;
}
}
开发经验:头插法创建的链表元素顺序与输入顺序相反,而尾插法则相同。在实际应用中,尾插法更为常用,因为它保持了数据的原始顺序。头插法在某些特定场景下有用,比如实现栈结构。
3.2.3 链表插入操作
c复制Status ListInsert(LinkList *L, int i, ElemType e) {
LNode *p = *L;
int j = 0;
// 寻找第i-1个结点
while (p && j < i-1) {
p = p->next;
j++;
}
if (!p || j > i-1) {
return ERROR; // 插入位置不合法
}
LNode *s = (LNode *)malloc(sizeof(LNode));
s->data = e;
s->next = p->next;
p->next = s;
return OK;
}
3.2.4 链表删除操作
c复制Status ListDelete(LinkList *L, int i, ElemType *e) {
LNode *p = *L;
int j = 0;
// 寻找第i-1个结点
while (p->next && j < i-1) {
p = p->next;
j++;
}
if (!(p->next) || j > i-1) {
return ERROR; // 删除位置不合法
}
LNode *q = p->next;
*e = q->data;
p->next = q->next;
free(q);
return OK;
}
3.3 链表的变体
除了基本的单链表外,链表还有几种常见的变体:
3.3.1 双向链表
双向链表每个节点有两个指针,分别指向前驱和后继节点:
c复制typedef struct DuLNode {
ElemType data;
struct DuLNode *prior;
struct DuLNode *next;
} DuLNode, *DuLinkList;
双向链表的优势是可以双向遍历,但每个节点需要额外的空间存储前驱指针。
3.3.2 循环链表
循环链表的尾节点指向头节点,形成一个环:
c复制// 初始化循环链表
void InitList_Circular(LinkList *L) {
*L = (LinkList)malloc(sizeof(LNode));
(*L)->next = *L; // 指向自己
}
循环链表适合需要周期性访问数据的场景,比如操作系统中的进程调度。
3.3.3 静态链表
静态链表用数组实现链表,通过游标代替指针:
c复制#define MAXSIZE 1000
typedef struct {
ElemType data;
int cur; // 游标,相当于指针
} Component, StaticLinkList[MAXSIZE];
静态链表在一些不支持指针的语言中很有用,或者在内存受限的嵌入式系统中。
4. 线性表的典型应用
线性表在实际开发中有广泛的应用,下面介绍几个典型场景。
4.1 栈的实现与应用
栈是一种特殊的线性表,只允许在一端进行插入和删除操作,遵循后进先出(LIFO)原则。
4.1.1 顺序栈的实现
c复制#define STACK_INIT_SIZE 100
#define STACK_INCREMENT 10
typedef struct {
ElemType *base;
ElemType *top;
int stacksize;
} SqStack;
Status InitStack(SqStack *S) {
S->base = (ElemType *)malloc(STACK_INIT_SIZE * sizeof(ElemType));
if (!S->base) return ERROR;
S->top = S->base;
S->stacksize = STACK_INIT_SIZE;
return OK;
}
Status Push(SqStack *S, ElemType e) {
if (S->top - S->base >= S->stacksize) {
// 栈满,扩容
S->base = (ElemType *)realloc(S->base,
(S->stacksize + STACK_INCREMENT) * sizeof(ElemType));
if (!S->base) return ERROR;
S->top = S->base + S->stacksize;
S->stacksize += STACK_INCREMENT;
}
*S->top++ = e;
return OK;
}
Status Pop(SqStack *S, ElemType *e) {
if (S->top == S->base) return ERROR;
*e = *--S->top;
return OK;
}
4.1.2 栈的应用:括号匹配
c复制Status BracketMatch(char *str) {
SqStack S;
InitStack(&S);
for (int i = 0; str[i] != '\0'; i++) {
switch (str[i]) {
case '(': case '[': case '{':
Push(&S, str[i]);
break;
case ')':
if (StackEmpty(S)) return ERROR;
char e;
Pop(&S, &e);
if (e != '(') return ERROR;
break;
case ']':
if (StackEmpty(S)) return ERROR;
Pop(&S, &e);
if (e != '[') return ERROR;
break;
case '}':
if (StackEmpty(S)) return ERROR;
Pop(&S, &e);
if (e != '{') return ERROR;
break;
}
}
return StackEmpty(S) ? OK : ERROR;
}
4.2 队列的实现与应用
队列是另一种特殊的线性表,只允许在一端插入,另一端删除,遵循先进先出(FIFO)原则。
4.2.1 循环队列的实现
c复制#define MAXQSIZE 100
typedef struct {
ElemType *base;
int front;
int rear;
} SqQueue;
Status InitQueue(SqQueue *Q) {
Q->base = (ElemType *)malloc(MAXQSIZE * sizeof(ElemType));
if (!Q->base) return ERROR;
Q->front = Q->rear = 0;
return OK;
}
Status EnQueue(SqQueue *Q, ElemType e) {
if ((Q->rear + 1) % MAXQSIZE == Q->front) {
return ERROR; // 队满
}
Q->base[Q->rear] = e;
Q->rear = (Q->rear + 1) % MAXQSIZE;
return OK;
}
Status DeQueue(SqQueue *Q, ElemType *e) {
if (Q->front == Q->rear) {
return ERROR; // 队空
}
*e = Q->base[Q->front];
Q->front = (Q->front + 1) % MAXQSIZE;
return OK;
}
4.2.2 队列的应用:广度优先搜索(BFS)
c复制void BFS(Graph G, int v) {
SqQueue Q;
InitQueue(&Q);
visited[v] = true;
EnQueue(&Q, v);
while (!QueueEmpty(Q)) {
int u;
DeQueue(&Q, &u);
for (int w = FirstAdjVex(G, u); w >= 0; w = NextAdjVex(G, u, w)) {
if (!visited[w]) {
visited[w] = true;
EnQueue(&Q, w);
}
}
}
}
4.3 线性表在算法中的应用
4.3.1 多项式相加
c复制typedef struct {
float coef; // 系数
int expn; // 指数
} Term;
typedef LinkList Polynomial;
void CreatePolyn(Polynomial *P, int n) {
InitList(P);
for (int i = 1; i <= n; i++) {
Term e;
printf("输入第%d项的系数和指数:", i);
scanf("%f %d", &e.coef, &e.expn);
ListInsert(P, i, e);
}
}
void AddPolyn(Polynomial *Pa, Polynomial *Pb) {
LNode *qa = (*Pa)->next, *qb = (*Pb)->next;
LNode *pa = *Pa, *pb = *Pb;
while (qa && qb) {
Term a = qa->data, b = qb->data;
if (a.expn < b.expn) {
pa = qa;
qa = qa->next;
} else if (a.expn > b.expn) {
pb = qb->next;
qb->next = qa;
pa->next = qb;
pa = qb;
qb = pb;
} else {
float sum = a.coef + b.coef;
if (sum != 0.0) {
qa->data.coef = sum;
pa = qa;
} else {
pa->next = qa->next;
free(qa);
}
qa = pa->next;
pb->next = qb->next;
free(qb);
qb = pb->next;
}
}
if (qb) {
pa->next = qb;
pb->next = NULL;
}
free(*Pb);
*Pb = NULL;
}
4.3.2 大整数运算
使用线性表可以方便地实现大整数的存储和运算:
c复制typedef struct {
int *digits; // 存储各位数字,低位在前
int length; // 数字位数
int sign; // 符号:1表示正,-1表示负
} BigInt;
Status InitBigInt(BigInt *N, char *str) {
int len = strlen(str);
N->length = len;
N->digits = (int *)malloc(len * sizeof(int));
if (!N->digits) return ERROR;
for (int i = 0; i < len; i++) {
N->digits[i] = str[len-1-i] - '0';
}
return OK;
}
Status AddBigInt(BigInt *A, BigInt *B, BigInt *C) {
int maxlen = A->length > B->length ? A->length : B->length;
C->digits = (int *)malloc((maxlen + 1) * sizeof(int));
if (!C->digits) return ERROR;
int carry = 0;
int i;
for (i = 0; i < maxlen; i++) {
int a = i < A->length ? A->digits[i] : 0;
int b = i < B->length ? B->digits[i] : 0;
int sum = a + b + carry;
C->digits[i] = sum % 10;
carry = sum / 10;
}
if (carry > 0) {
C->digits[i] = carry;
C->length = maxlen + 1;
} else {
C->length = maxlen;
}
return OK;
}
性能优化提示:在实际项目中,我们可以将多个十进制位压缩存储在一个数组元素中(如每9位存储为一个int),这样可以减少内存访问次数,提高运算效率。但需要注意处理进位和边界条件。
