文章目录
1.线性表的定义
1.1 线性结构
线性结构有以下特点:
- 只有一个 “首元素”;
- 只有一个 “尾元素”;
- 除了首元素外,其余元素只有一个 “直接前驱”;
- 除了尾元素外,其余元素只有一个 “直接后继”;
即结构中的元素存在一个对一个的关系,常见的线性结构有,数组、栈、队列等。
1.2 线性结构的表示
1.2.1 顺序表示
用一组地址连续的存储单元存储线性表的数据元素,假设第一个数据单元的存储地址记作 L O C ( a 1 ) LOC(a_{1}) LOC(a1),每个数据元素占用 l l l 个存储单元,则第 i i i个数据元素的存储位置为 L O C ( a i ) = L O C ( a 1 ) + ( i − 1 ) ∗ l LOC(a_{i}) = LOC(a_{1}) + (i - 1) * l LOC(ai)=LOC(a1)+(i−1)∗l因此只要确定了存储线性表的起始位置,线性表的任一元素的存储位置可由其下标推断出来。即顺序存储结构支持随机存取。但是当插入或删除元素时,元素间的逻辑关系发生改变,所以需要涉及元素的移动。
1.2.2 链式表示
1. 单链表
线性表的链式存储结构即用一组任意的存储单元存储线性表的数据元素(可以是连续的也可以是非连续的)。数据元素间的逻辑关系用一个指针来表示,所以每个数据元素(也称为结点node),包含数据域和指针域。
为了方便,链表中通常设置一个头指针(Head),不存储元素,其指针指向链表的第一个结点。
由于逻辑上相邻的物理上不一定相邻,所以链式结构不支持随机存取,但是插入和删除元素方便,其不涉及元素的移动,仅仅需要修改元素的指针域即可修改元素间的逻辑关系。
2. 循环链表
循环链表(circular linked list)是表尾元素的指针域指向表头结点,整个链表形成一个环。因此,判断空表的条件为 p->next==head
。有时,可设置表的尾指而不设置头指针,简化某些操作,例如表的合并。
3.双向链表
单链表查找后继的复杂度为
O
(
1
)
O(1)
O(1),但是查找前驱的复杂度为
O
(
n
)
O(n)
O(n),有时,为了方便双向查找,可以牺牲一些空间,在结点中同时存储两个指针域,分别指向直接前驱和直接后继。
typedef struct DulNode{
ElemType data;
struct DulNode* prior;
struct DulNode* next;
}DulNode, *DuLinkLists;
2.线性表的实现
2.1 常见的线性表操作
常见的线性表操作包括建表、访问元素、插入、修改和删除元素。
操作 | 结果 |
---|---|
InitList(&L) | 建立一个空表L |
DestoryList(&L) | 销毁线性表L |
ClearList(&L) | 将表L置为空表 |
ListEmpty(L) | 判断是否为空表 |
ListLength(L) | 返回L中元素个数 |
GetElem(L,i,&e) | 返回L中第i个元素e |
PriorElem(L,cur_e,&pre_e) | 返回cur_e的前驱 |
NextElem(L,cur_e,&next_e) | 返回cur_e的后继 |
ListInsert(&L,i,e) | 在第i个位置前插入元素e |
ListDelete(&L,i,&e) | 删除L的第i个元素e |
2.2 线性表实现
顺序结构实现
/* 线性表的顺序存储结构 */
#include<stdio.h>
#include<stdlib.h>
#define INIT_SIZE 10
#define INCREMENT 5
typedef int ElemType;
// --- 线性表的动态分配存储结构 ---
typedef struct list {
ElemType *elem; // 存储空间的基址
int length; // 线性表长度
int listsize; // 线性表当前最大容量
}SeqList;
// 初始化
int InitList(SeqList &L) {
L.elem = (ElemType*)malloc(INIT_SIZE * sizeof(ElemType));
if (L.elem == NULL) exit(-1);
L.listsize = INIT_SIZE;
L.length = 0;
return 0;
}
// 分配新的空间
void IncreatList(SeqList &L) {
L.elem = (ElemType*)realloc(L.elem, (L.listsize + INCREMENT) * sizeof(ElemType));
if (!L.elem) exit(-1);
L.listsize += INCREMENT;
}
// 打印线性表
void ShowList(SeqList L) {
for (int i = 0; i < L.length; i++) printf("%d ", L.elem[i]);
printf("\n");
}
//线性表的插入
int ListInsert_Sq(SeqList &L, int i, ElemType e) {
// 在线性表L的第i个位置前插入元素e
// 1 <= i <= L.length + 1
if (i < 1 || i > L.length + 1) return 0;
if (L.length >= L.listsize) IncreatList(L);
// 将i后面的元素后移动一个位置
for (int j = L.length; j >= i; j--) L.elem[j] = L.elem[j - 1];
L.elem[i - 1] = e;
++L.length;
return 1;
}
// 线性表的删除
int ListDelete_Sq(SeqList &L, int i, ElemType &e) {
// 删除线性表L的第i个位置的元素
// 1 <= i <= L.length
if (i < 1 || i > L.length) return 0;
e = L.elem[i - 1];
// 将i后的元素前移一个位置
for (; i < L.length; i++) L.elem[i-1] = L.elem[i];
--L.length;
return 1;
}
// 线性表的合并
void MergeList_Sq(SeqList& L1, SeqList& L2, SeqList& L) {
// 已知L1和L2元素按非递减排列
// 归并L1和L2元素得到新的非递减排列线性表L
L.listsize = L.length = L1.length + L2.length;
L.elem = (ElemType*)malloc(L.listsize * sizeof(ElemType));
if (!L.elem) exit(-1);
int p1,p2,p;
p = p1 = p2 = 0;
while (p1 < L1.length && p2 < L2.length) {
if (L1.elem[p1] <= L2.elem[p2]) L.elem[p++] = L1.elem[p1++];
else L.elem[p++] = L2.elem[p2++];
}
while(p1 < L1.length) L.elem[p++] = L1.elem[p1++];
while (p2 < L2.length) L.elem[p++] = L2.elem[p2++];
}
int main() {
SeqList L1,L2,L;
ElemType e;
InitList(L1);
InitList(L2);
for (int i = 0; i < 16; i++) ListInsert_Sq(L1, i+1, i*i);
for (int i = 0; i < 8; i++) ListInsert_Sq(L2, i + 1, i);
printf("插入元素后 L1:");
ShowList(L1);
printf("插入元素后 L2:");
ShowList(L2);
printf("删除第5个元素后 L1:");
ListDelete_Sq(L1, 5, e);
ShowList(L1);
printf("\n删除元素为:%d\n", e);
printf("合并L1和L2:");
MergeList_Sq(L1, L2, L);
ShowList(L);
system("pause");
return 0;
}
链式结构实现
/* 线性表的链式存储结构 */
#include<stdio.h>
#include<stdlib.h>
typedef int ElemType;
// --- 链表的定义 ---
typedef struct Lnode {
ElemType data;
struct Lnode* next;
}LNode, *LinkList; // 使用一个头结点LinkList head;即可以表示链表
void InitList_Lin(LinkList &L) {
L = (LinkList)malloc(sizeof(LNode));
L->data = 0; // 头结点的数据域存储链表的长度
L->next = NULL;
}
// 打印链表
void ShowList(LinkList L) {
LNode* p = L->next;
while (p) {
printf("%d ", p->data);
p = p->next;
}
printf("\n");
}
// 链表的插入
int ListInsert_Lin(LinkList &L, int i, ElemType e) {
// 在带头结点的单链表L的第i个元素之前插入元素e
LNode* p = L;
int j = 0;
// 找到插入元素的前驱
while (j < i - 1 && p) { // 寻找第i-1个结点
p = p->next;
++j;
}
if (j > i || !p) return 0; // i小于1 或者 i大于L.length+1
LNode* tmp = (LNode*)malloc(sizeof(LNode));
tmp->data = e;
tmp->next = p->next;
p->next = tmp;
return 1;
}
// 链表的删除
int ListDelete_Lin(LinkList &L, int i, ElemType &e) {
// 删除链表的第i个元素e
int j = 0;
LNode* p = L;
// 找到删除元素的前驱
while (j < i - 1 && p) { // p指向第i个结点的前驱
p = p->next;
++j;
}
if (j > i || !(p->next)) return 0; // i 小于 1 或者 i 大于 L.length+1
LNode* q = p->next; // 删除的结点
e = q->data;
p->next = q->next;
free(q);
return 1;
}
// 头插法建立链表
void CreateList_Lin(LinkList& L, int len) {
InitList_Lin(L);
printf("请逆序输入%d个元素\n", len);
while (len--) {
// 注意将内存分配放在while循环内部
// 为每一个结点分配空间
LNode* tmp = (LNode*)malloc(sizeof(LNode));
scanf_s("%d", &tmp->data);
tmp->next = L->next;
L->next = tmp;
}
}
// 链表的合并
void MergeList_Lin(LinkList& L1, LinkList& L2,LinkList &L) {
// 假设单链表L1和L2按非递减顺序排列
// 将其合并成链表L
// 与顺序表示不一样,合并链表不需要额外新建表空间
// 只需要将原来链表中结点关系重新组织即可
// 合并后L=L1,L2=NULL
LNode* p1 = L1->next, *p2 = L2->next;
LNode* p;
p = L = L1; // p表示当前L的操作指针
while (p1 && p2) {
if (p1->data <= p2->data) {
p->next = p1;
p = p1;
p1 = p1->next;
}
else{
p->next = p2;
p = p2;
p2 = p2->next;
}
}
// 插入剩下的结点
p->next = p1 ? p1 : p2;
free(L2); // 释放表L2
}
int main() {
LinkList L1,L2,L;
// 5 4 3 2 1
// 9 6 3 1
CreateList_Lin(L1, 5);
printf("L1: ");
ShowList(L1);
CreateList_Lin(L2, 4);
printf("L2: ");
ShowList(L2);
MergeList_Lin(L1, L2, L);
printf("L1 U L2: ");
ShowList(L);
system("pause");
return 0;
}
可知,链表在删除和插入时,只需要修改指针域的指向即可,但是索引元素时需重头开始查找,复杂度为 O ( n ) O(n) O(n)。由于不需要移动元素,所以它是线性表的首选存储结构。
3. 线性表的应用——多项式
在数学上,一元多项式形式如下
P
n
(
x
)
=
p
0
+
p
1
x
+
p
2
x
2
+
.
.
.
+
p
n
x
n
P_{n}(x)=p_{0}+p_{1}x+p_{2}x^{2}+...+p_{n}x^{n}
Pn(x)=p0+p1x+p2x2+...+pnxn
由
n
+
1
n+1
n+1个系数确定。所以,在计算机中可以由一个线性表P来表示
P
=
(
p
0
,
p
1
,
p
2
,
.
.
.
,
p
n
)
P=(p_{0},p_{1},p_{2},...,p_{n})
P=(p0,p1,p2,...,pn)
其指数
i
i
i 为对应项
p
i
p_{i}
pi 的下标.
当多项式次数较小且比较连续的时,可采用顺序存储结构来存储,使得多项式的加法十分简洁。但是,多项式的次数往往很高且比较稀疏,顺序表的长度难以确定,所以使用链式存储结构节省空间。此时,我们需要同时存储指数和系数,即
下面用链表存储多项式来实现多项式的加法和乘法。
/* 多项式的表示和操作 */
#include<stdlib.h>
#include<stdio.h>
// 多项式的项
typedef struct {
float coef; // 系数
int expn; // 指数
}term,ElemType;
typedef struct LNode {
ElemType data;
struct LNode* next;
}LNode,*LinkList;
typedef LinkList polynomial;
// 输入m项系数和指数,建立一元多项式P
void InitList(polynomial &P) {
P = (polynomial)malloc(sizeof(LNode));
P->data.expn = -1; // 头结点的指数为-1
P->next = NULL;
if (!P) exit(-1);
}
void CreatPolyn(polynomial &P, int m) {
InitList(P);
printf("请按照指数降序输入多项式的%d项\n", m);
while (m--) {
// 注意将内存分配放在while循环内部
// 为每一个结点分配空间
LNode* tmp = (LNode*)malloc(sizeof(LNode));
scanf_s("%f %d", &tmp->data.coef,&tmp->data.expn);
tmp->next = P->next;
P->next = tmp;
}
}
// 打印多项式
void PrintPolyn(polynomial P) {
LNode* p = P->next;
printf("多项式为:");
while (p) {
printf("%fx^%d+ ", p->data.coef, p->data.expn);
p = p->next;
}
printf("\n");
}
// 多项式的加法
int cmp(term a, term b) {
//按照a的指数< = > b的指数分别返回 -1 0 1
if (a.expn < b.expn) return -1;
else if (a.expn == b.expn) return 0;
else return 1;
}
// 判断是否为空
int ListEmpty(LinkList P) {
return P->next == NULL;
}
void AddPolyn(polynomial& Pa, polynomial& Pb) {
//完成Pa+Pb,并将结果存在Pa,同时销毁Pb
polynomial ha = Pa, hb = Pb; // 头结点,也是当前操作结点的前驱
LNode* qa = ha->next, *qb = hb->next; // 当前操作结点
while (qa && qb) {
term a = qa->data;
term b = qb->data;
switch (cmp(a, b)) {
case -1:
//ha->next = qa;
ha = qa;
qa = qa->next;
break;
case 0:
{
// 指数相等,系数相加
int sum = a.coef + b.coef;
if (sum == 0) {
// 等于0即不存储,删除对应结点
// 删除qa
ha->next = qa->next;
free(qa);
qa = ha->next;
// 删除qb
hb->next = qb->next;
free(qb);
qb = hb->next;
}
else {
// 注意这里需要用指针进行改值
// 使用 a.coef = sum; 将无效
qa->data.coef = sum;
ha = qa;
qa = qa->next;
// 删除qb结点
hb->next = qb->next;
free(qb);
qb = hb->next;
}
// 也可以将相同部分提到括号外
break;
}
case 1:
// 先从hb中删除qb,再插入到ha中
hb->next = qb->next; // 删除
qb->next = ha->next; // 插入
ha->next = qb;
ha = ha->next;
qb = hb->next;
break;
}
}
// 连接qb剩下的结点,此时ha指向Pa的尾指针
if (qb) ha->next = qb;
free(hb);
}
// 多项式相乘
// 转换为一系列的加法
void MultiplyPolyn(polynomial& Pa, polynomial& Pb) {
polynomial ha = Pa, hb = Pb; // 头结点,也是当前操作结点的前驱
LNode* qa = ha->next, *qb = hb->next; // 当前操作结点
int flag = 1; // 表示第一次乘法,不加
while (qb) {
polynomial tmp;
InitList(tmp); // 存储中间的乘法结果
LNode* tc = tmp; // 尾指针,方便使用尾插法插入结点
term b = qb->data;
qa = ha->next;
for (; qa; qa = qa->next) {
LNode* t = (LNode*)malloc(sizeof(LNode));
t->data.coef = qa->data.coef * b.coef;
t->data.expn = qa->data.expn + b.expn;
t->next = tc->next;
tc->next = t;
tc = t; // 尾插法插入中间结果
}
if(flag == 0) AddPolyn(Pa, tmp);
else {
flag = 0;
Pa = tmp;
}
qb = qb->next;
printf("此时的中间结果:");
PrintPolyn(Pa);
}
}
int main() {
polynomial Pa, Pb;
CreatPolyn(Pa, 2);
CreatPolyn(Pb, 2);
/*
逆序
5 17 9 8 3 1 7 0
8 18 -9 8 22 7 8 1
简单测试乘法
2 1 1 0
3 2 1 1
*/
printf("操作前:\n");
PrintPolyn(Pa);
PrintPolyn(Pb);
MultiplyPolyn(Pa, Pb);
//AddPolyn(Pa, Pb);
//printf("操作后:\n");
PrintPolyn(Pa);
system("pause");
return 0;
}
4.链表的反转
4.1 单链表反转
1.问题描述
给定一个单链表的头指针,反转该链表并返回反转后链表的头指针。
2.迭代解法
在遍历节点的时候,我们需要将当前结点curr
的next
指针指向其前驱pre
,由于是单链表,不能直接返回其前驱,所以遍历的时候需要存储其前驱结点,反转之后,其后继结点丢掉了,所以也需要存储其后继结点nextTemp
。所以我们一共需要存储三个结点,称之为三指针反转法
。
LNode* reverseList(List head) {
// 反转链表并返回新的头结点
LNode* pre = NULL;
LNode* curr = head->next; // 头结点是不存储元素的
while (curr != NULL) {
LNode* nextTemp = curr->next;
curr->next = pre;
pre = curr;
curr = nextTemp;
}
LNode* newhead = (LNode*)malloc(sizeof(LNode));
newhead->next = pre;
return newhead;
}
复杂度分析:
时间复杂度:
O
(
n
)
O(n)
O(n), 其中
n
n
n是列表的长度
空间复杂度:
O
(
1
)
O(1)
O(1)
3.递归解法
参考资料
《数据结构 C语言描述》 严蔚敏著