线性表
线性表的定义
- 线性表示具有相同数据类型的 n(n >= 0)个数据元素的有限序列,其中 n 为表长,当 n = 0时线性表是一个空表。若用L命名线性表,则其一般表示为 L = (a1, a2, ai, ai+1, an)。每个数据元素所占空间一样大。
- 有次序。
- 有限(所有整数按递增次序排列不是一个线性表)。
- ai 是线性表中的“第 i 个”元素线性表的位序。
- a1 是表头元素;an 是表尾元素
- 除第一个元素外,每个元素有且仅有一个直接前驱;除最后一个元素外,每个元素有且仅有一个直接后继。
线性表的基本操作
InitList(&L)
:初始化表。构造一个空的线性表 L,分配内存空间。Destroy(&L)
:销毁操作。销毁线性表,并释放线性表 L 所占用的内存空间。LinstInsert(&L, i, e)
:插入操作。在表L中的第 i 个位置上插入指定元素e。ListDelete(&L, i, &e)
:删除操作。删除表 L 中的第 i 个位置的元素,并用 e 返回删除元素的值。LocateElem(L, i)
:按值查找操作。在表 L 中查找具有给定关键字值的元素。GetElem(L, i)
:按位查找操作。获取表 L 中第 i 个位置的元素的值。Length(L)
:求表长。返回线性表 L 的长度,即 L 中数据元素的个数。PrintList(L)
:输出操作。按前后顺序输出线性表 L 的所有元素值。Empty(L)
:判空操作。若 L 为空表,则返回 true,否则返回 false。
顺序表
顺序表的定义
- 用顺序存储的方式实现线性表顺序存储。把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。
顺序表的实现
-
静态分配
#define MaxSize 10 typedef struct { ElemType data[MaxSize]; int length; }SqList;
-
动态分配
#define InitSize 10 typedef struct { ElemType* data; int MaxSize; int lengeh; }SeqList; //增加动态数组的长度 void IncreaseSize(SeqList& L, int len) { int* p = L.data; L.data = (int*)malloc((L.MaxSize + len) * sizeof(int)); for(int i = 0; i < L.length; i++) { L.data[i] = p[i]; } L.MaxSize = L.MaxSize + len; free(p); }
-
顺序表的特点
- 随机访问,即可以在 O(1) 时间内找到第i个元素。
- 存储密度高,每个结点只存储数据元素。
- 拓展容量不方便(即便采用动态分配的方式实现,拓展长度的时间复杂度也比较高)。
- 插入、删除操作不方便,需要移动大量元素。
顺序表的插入和删除
-
插入操作。在表 L 中的第 i 个位置上插入指定元素e。
bool LinstInsert(SqList& L, int i, int e) { if(i < 1 || i > L.length + 1) return false; if(L.length >= MaxSize) return false; for(int j = L.length; j >= i; j--) { L.data[j] = L.data[j - 1]; } L.data[i - 1] = e; L.length++; return true; }
最好时间复杂度:O(1)
最坏时间复杂度:O(n)
平均时间复杂度:O(n)
-
删除操作。删除表 L 第 i 个位置的元素,并用 e 返回删除元素的值。
bool ListDelete(SqList& L, int i, int& e) { if(i < 1 || i > L.length) return false; e = L.data[i - 1]; for(int j = i; j < L.length; j++) { L.data[j - 1] = L.data[j]; L.length--; return true; } }
最好时间复杂度:O(1)
最坏时间复杂度:O(n)
平均时间复杂度:O(n)
顺序表的查找
-
按位查找操作。获取表 L 中第 i 个位置的元素的值。
ElemType GetElem(SqList L, int i) { return L.data[i - 1]; }
时间复杂度:O(1)
-
按值查找操作。在表 L 中查找具有给定关键字值的元素。
int LocateElem(SeqList L, ElemType e) { for(int i = 0; i < L.length; i++) if(L.data[i] == e) return i + 1; return 0; }
最好时间复杂度:O(1)
最坏时间复杂度:O(n)
平均时间复杂度:O(n)
单链表
单链表的定义
-
顺序表:每个结点中只存放数据元素。
- 优点:可随机存取,存储密度高。
- 缺点:要求大片连续空间,改变容量不方便。
-
单链表:每个结点除了存放数据元素外,还要存储指向下一个结点的指针。
- 优点:不要求大片连续空间,改变容量方便。
- 缺点:不可随机存取,要耗费一定空间存放指针。
-
结构体定义
typedef struct LNode { ElemType data; struct LNode* next; }LNode, * LinkList;
强调这是一个单链表:使用
LinkList
强调这是一个结点:使用
LNode*
-
不带头结点的单链表
bool InitList(LinkList& L) { L = NULL;// 空表,暂时还没有任何结点(防止脏数据) return true; }
-
带头结点的单链表
bool InitList(LinkList& L) { L = (LNode*)malloc(sizeof(LNode)); if(L == NULL) return false; L->next = NULL; return true; }
头结点不存储数据。
单链表的插入
-
插入操作。在表L中的第i个位置上插入指定元素e。
- 带头结点:
bool ListInsert(LinkList& L, int i, ElemType e) { if(i < 1) return false; LNode* p; int j = 0; p = L; while(p != NULL && j < i-1) { p = p->next; j++; } if(p == NULL) return false; LNode* s = (LNode*)malloc(sizeof(LNode)); s->data = e; s->next = p->next;//此句和下一句不能颠倒 p->next = s; return true; }
最好时间复杂度:O(1)
最坏时间复杂度:O(n)
平均时间复杂度:O(n)
- 不带头结点:
bool ListInsert(LinkList& L, int i, ElemType e) { if(i < 1) return false; if(i == 1) { LNode* s = (LNode*)malloc(sizeof(LNode)); s->data = e; s->next = L; L = s; return true; } LNode* p; int j = 1; p = L; while(p != NULL && j < i-1) { p = p->next; j++; } if(p == NULL) return false; LNode* s = (LNode*)malloc(sizeof(LNode)); s->data = e; s->next = p->next; p->next = s; return true; }
-
后插操作。在 p 结点之后插入元素。
bool InsertNextNode(LNode* p, ElemType e) { if(p == NULL) return false; LNode* s = (LNode*)malloc(sizeof(LNode)); if(s == NULL)// 某些情况下内存分配失败(如内存不足) return false; s->data = e; s->next = p->next; p->next = s; return true; }
-
前插操作。在 p 结点之前插入元素 e。
bool InsertPriorNode(LNode* p, ElemType e) { if(p == NULL) return false; LNode* s = (LNode*)malloc(sizeof(LNode)); if(s == NULL) return false; s->next = p->next; p->next = s;//新节点s连到p之后 s->data = p->data;//将p中元素复制到s中 p->data = e;//p中元素覆盖为e return true; }
时间复杂度:O(1)
单链表的删除
-
删除操作。删除表中第 i 个位置的元素,并用 e 返回删除元素的值。
bool ListDelete(LinkList& L, int i, ElemType& e) { if(i < 1) return false; LNode* p; int j = 0; p = L; while(p != NULL && j < i-1) { p = p->next; j++; } if(p == NULL) return false; LNode* q = p->next; e = q->data; p->next = q->next; free(q); return true; }
最好时间复杂度:O(1)
最坏时间复杂度:O(n)
平均时间复杂度:O(n)
-
删除指定结点p。
bool DeleteNode(LNode* p) { if(p == NULL) return false; LNode* q = p->next; p->data = p->next->data; p->next = q->next; free(q); return true; }
注意:不满足p是最后一个结点的情况。若需要删除最后一个结点,只能从表头依次寻找p的前驱。
单链表的查找
-
按位查找,返回第i个元素(带头结点)
LNode* GetElem(LinkList L, int i) { if(i < 0) return NULL; LNode* p; int j = 0; p = L; while(p != NULL && j < i) { p = p->next; j++; } return p; }
平均时间复杂度:O(n)
-
封装(基本操作)的好处:避免重复代码,简洁、易维护。
-
按值查找,找到
数据域 == e
的结点。LNode* LocateElem(LinkList L, ElemType e) { LNode* p = L->next; while(p != NULL && p->data != e) p = p->next; return p; }
-
求表的长度。
int Length(LinkList L) { int len = 0; while(p->next != NULL) { p = p->next; len++; } }
时间复杂度:O(1)
单链表的建立
-
尾插法建立单链表
LinkList List_TailInsert(LinkList& L) { int x; L = (LinkList)malloc(sizeof(LNode)); LNode* s,* r = L; scanf("%d", &x); while(x != 9999) {//输入9999表示结束 s = (LNode*)malloc(sizeof(LNode)); s->data = x; r->next = s; r = s; scanf("%d", &x); } r->next = NULL; return L; }
注意:设置一个指向表尾结点的指针。
-
头插法建立单链表
LinkList List_HeadInsert(LinkList& L) { LNode *s; int x; L = (LinkList)malloc(sizeof(LNode)); L->next = NULL; scanf("%d", &x); while(x != 9999) {//输入9999表示结束 s = (LNode *)malloc(sizeof(LNode)); s->data = x; s->next = L->next; L->next = s; scanf("%d", &x); } return L; }
双链表
-
单链表:无法逆向检索,有时候不太方便。
-
双链表:可双向检索,存储密度略低于单链表。
-
typedef struct DNode { ElemType data; struct DNode* prior, * next; }DNode, * DLinkList;
-
双链表的初始化(带头结点)
bool InitDLinkList(DLinklist& L) { L = (DNode*)malloc(sizeof(DNode)); if(L == NULL)//内存不足,分配失败 return false; L->prior = NULL; L->next = NULL; return true; }
-
双链表的插入
bool InsertNextDNode(DNode* p, DNode* s) { if(p == NULL || s == NULL) return false; s->next = p->next;//将s结点插入到p结点之后 if(p->next != NULL) p->next->prior = s; s->prior = p; p->next = s; }
-
双链表的删除
bool DeleteNextDNode(DNode* p) {//删除p结点的后继结点q if(p == NULL) return false; DNode* q = p->next; if(q == NULL) return false; p->next = q->next; if(q->next != NULL) q->next->prior = p; free(q); return true; }
-
双链表的遍历
- 后向遍历
while(p != NULL) { //对结点p做相应处理 p = p->next; }
- 前向遍历
while(p != NULL) { //对结点p做相应处理 p = p->prior; }
- 前向遍历(跳过头结点)
while(p->prior != NULL) { //对结点p做相应处理 p = p->prior; }
-
双链表不可随机存取,按位查找、按值查找操作都只能用遍历方法实现。时间复杂度O(n)
循环链表
-
单链表:从一个结点出发只能找到后续的各个结点。
-
循环单链表:从一个结点出发可以找到其他任何一个结点。
-
初始化循环单链表
bool InitList(LinkList& L) { L = (LNode*)malloc(sizeof(LNode)); if(L == NULL)//内存不足,分配失败 return false; L->next = L;//头结点next指向头结点 return true; }
-
在循环单链表中如果需要经常对表头/表尾结点操作,可将表头/表尾设为头结点。
-
初始化循环双链表
bool InitDLinkList(DLinklist& L) { L = (DNode*)malloc(sizeof(DNode)); if(L == NULL) return false; L->prior = L; L->next = L; return true; }
-
循环双链表的插入
bool InsertNextDNode(DNode* p, DNode* s) { s->next = p->next;//将s结点插入到p结点之后 p->next->prior = s; s->prior = p; p->next = s; }
-
循环双链表的删除
bool DeleteNextDNode(DNode* p) {//删除p结点的后继结点q DNode* q = p->next; p->next = q->next; q->next->prior = p; free(q); return true; }
静态链表
-
定义:分配一整片连续的内存空间,各个结点集中安置(用数组方式实现的链表)。
- 与数组不同之处:数组各个元素是按照数据下标0至n依次排序的。而在每个结点包含数据元素和下一个结点的数组下标(游标)两个信息,因而静态链表不一定依次排序。
- 每个数据元素4B,每个游标4B(每个结点8B),设起始地址为addr(不是e0地址),则e1的存放地址为addr + 8*2。
-
结构体定义
#define MaxSize 10 struct Node { ElemType data; int next;//下一个数据下标 }
#define MaxSize 10 typedef struct { ElemType data; int next; }SLinkList[MaxSize]
SLinkList a
可用 SLinkList 定义“一个长度为 MaxSize 的 Node 型数组”
-
初始化静态链表:
- 把 a[0] 的 next 设为-1。
- 把其他结点的 next 设为一个特殊的值来表示空闲,如-2。
-
查找:从头结点出发依次往后遍历结点。
-
插入位序为i的结点:
- 找到一个空的结点,存入数据元素。
- 从头结点出发找到位序为 i-1 的结点。
- 修改新结点的 next。
- 修改 i-1 号结点的 next。
-
删除某个结点:
- 从头结点出发找到前驱结点。
- 修改前驱结点的游标。
- 被删除结点的 next 设为-2。
-
静态链表的优点:增删操作不需要大量移动元素
-
静态链表的缺点:
- 不能随机存取,只能从头结点开始依次往后查找。
- 容量固定不可变。
顺序表和链表的比较
逻辑结构
- 都属于线性表,都是线性结构。
存储结构
- 顺序表:
- 优点:支持随机存取、存储密度高。
- 缺点:大片连续空间分配不方便,改变容量不方便。
- 链表:
- 优点:离散的小空间分配方便,改变容量方便。
- 缺点:不可随机存取,存储密度低。
基本操作
- 初始化:
- 顺序表:需要预分配大片连续空间。若分配内存过小,则之后不方便拓展容量;若分配空间过大,则浪费内存资源。
- 静态分配:静态数组:容量不可改变。
- 动态分配:动态数组(malloc、free):容量可以改变,但需要移动大量元素,时间代价高。
- 链表:只需分配一个头结点(也可以不要头结点,只声明一个头指针),以后方便拓展。
- 顺序表:需要预分配大片连续空间。若分配内存过小,则之后不方便拓展容量;若分配空间过大,则浪费内存资源。
- 销毁:
- 顺序表:修改
length
=0。- 静态分配(静态数组):系统自动回收空间。
- 动态分配(动态数组:malloc、free):需要手动 free。
- 链表:依次删除各个结点(free)。
- 顺序表:修改
- 插入删除:
- 顺序表:需要将后续元素后移/前移。
- 时间复杂度:O(n)。时间开销主要来自于移动元素。若数据元素很大,则移动的时间代价很高。
- 链表:只需要修改指针。
- 时间复杂度:O(n)。时间开销主要来自于查找元素。查找元素的时间代价更低。
- 顺序表:需要将后续元素后移/前移。
- 查找:
- 顺序表:
- 按位查找:O(1)
- 按值查找:O(n)。若表内元素有序,则可在O(log2n)时间内找到。
- 链表:
- 按位查找:O(n)
- 按值查找:O(n)
- 顺序表:
- 表长难以预估、经常要增加/删除元素:链表。
- 表长可预估、查询(搜索)操作较多:顺序表。