02 线性表
2.1 线性表的逻辑结构
线性表的特点
在数据元素的非空有限集中:
-
存在唯一一个被称为“第一个”的数据元素;
-
存在唯一一个被称为“最后一个”的数据结构;
-
除第一个外,每个数据元素只有一个前驱;
-
除最后一个外,每个数据元素只有一个后继。
2.2 线性表的顺序存储结构
用一组 地址连续 的存储单元存放的线性表称为顺序表。可采用一维数组或动态分配顺序存储结构的方式实现,对任一数据元素均 可随机存取。
(1)顺序表初始化
#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
#define INFEASIBLE -1
#define OVERFLOW -2
typedef int Status;
//*******************************
// 定义顺序表
#define LIST_INIT_SIZE 100 // 线性表初始容量
#define LISTINCREMENT 10 // 分配增量
typedef int ElemType; // 元素类型
typedef struct
{
ElemType *elem; // 线性表首地址
int length; // 当前线性表的长度
int listSize; // 最大容量
} SqList;
// *******************************
// 线性表初始化
Status InitList_Sq(SqList &L)
{
L.elem = (ElemType*) malloc(LIST_INIT_SIZE * sizeof(ElemType));
if (! L.elem)
exit(OVERFLOW); // 判断是否申请成功
L.length = 0; // 初始长度
int listSize = LIST_INIT_SIZE; // 初始容量
}
(2)动态分配函数
- 申请内存函数
void* malloc(unsigned size)
申请长度为 size
个字节的空间,返回内存空间的首地址,申请失败则返回 NULL
,注意需要将返回值进行强制转换。
- 释放内存函数
void free(void *p);
释放以 p
为首地址的内存空间,但释放的空间必须是 malloc()
申请的。
例如
// 申请长度为 n 的整型数组空间
int *p, n;
scanf("%d", &n);
p = (int*) malloc(n * sizeof(int)); 强制转换为 int 型指针
...
free(p);
(3)查找:
在线性表 L 中查找是否存在数据元素 e,返回第 1 个值与 e 相同的元素的位序,若不存在则返回 0。
int LocationElem(Sqlist L, ElemType e)
{
int i = 1; // 位序,初始为 1
ElemType *p = L.elem; // 从第一个元素位置开始查找
while(*p != e && i<=L.length)
{
p++;
i++
}
if(i<=L.length) // 判断是否存在
return i;
else
return 0;
}
(4)插入:
将 e 插入到 $\mathrm{a_i}$
,将 $\mathrm{a_i}$
~$\mathrm{a_n}$
的元素一次后移。若空间不足,则修改已分配内存区的大小。
void* realloc(void* p, unsigned size)
将 p
指向的内存空间大小改为 size
,可以比原来空间大或小。若原存储空间不足,分配另一个足够大的存储空间,将原内容复制进去。申请成功,返回首地址;否则返回 NULL。
Status ListInsert(SqList &L, int i, ElemType e)
{
ElemType *p, *q, *newbase;
if(i<1 || i>L.length+1) // 判断插入位置 i 是否合法
return (ERROR);
if(L.length>=L.listSize) // 容量不足。扩大容量
{
newbase = (ElemType*) realloc(L.elem, (L.listSize+LISTINCREMENT) * sizeof(ElemType));
if(!newbase)
exit(OVERFLOW);
L.elem = newbase;
L.listSize += LISTINCREMENT;
}
p = &(L.elem[L.length-1]); // 指向末尾元素
q = &(L.elem[i-1]); // 指向插入位置
for(; p>=q; p--) // 元素后移
*(p+1) = *p;
*q = e; // 插入
++L.length; // 表长加 1
return OK;
}
(5)删除:
删除
a
i
\mathrm{a_i}
ai,将
a
i
+
1
\mathrm{a_{i+1}}
ai+1~
a
n
\mathrm{a_n}
an 依次前移。
Status ListDelete(SqList &L, int i, ElemType &e)
{
ElemType *p, *q;
if(i < 1 || i > L.length)
return ERROR;
p = &(L.elem[i-1]); // 记录删除位置
e = *p; // 记录删除元素
q = L.elem + L.length - 1; // 记录表尾位置
for(++p; p<=q; ++p)
*(p-1) = *p;
--L.length; // 表长度减一
return OK;
}
2.3 线性表的链式存储结构
每个节点包括数据域和指针域,数据域中存储元素本身信息,指针域存储结点直接后继的地址。
2.3.1 单链表
(1)为便于操作,在第一个结点之前附设一个头结点。链表节点的定义
typedef struct node
{
ElemType data;
struct node* next;
}Lnode, *LinkList;
(2)查找:
查找单链表中是否存在数据域为 e 的结点,若有则返回该结点的指针;否则返回 NULL。
LiskList LocationElem_L(LinkList L, ElemType e)
{
LinkList p = L->next; // 指向第一个结点的地址
while(p!=NULL && p->data != e)
p = p->next;
return p;
}
复杂度:T(n)=O(n)
(3)取元素值
取单链表中第 i 个元素的值,若存在该元素,用 e 返回其值;否则返回 ERROR。
Status GetElem_L(LinkList L, int i, ElemType &e)
{
LinkList p = L->next;
int j = 1; // 从第一个结点开始查找
while(p && j<i)
{
p = p-next;
j ++;
}
if(p==NULl || i<j) // 判断跳出循环原因
return ERROR;
e = p->data;
return OK;
}
复杂度:T(n)=O(n)
(4)插入
在单链表 L 的第 i 个元素之前插入新元素 e。
先找到第 i-1 个元素,然后用结点指针连接。
Status ListInsert_L(LinkList &L, int i, ElemType e)
{
LinkList p = L, s;
int j = 0;
while(p && j<i-1) // 查找 i-1 元素
{
p = p->next;
j++;
}
if(p==NULL || j>i-1)
return ERROR;
// 找到 i-1 后插入结点
s = (LinkList)malloc(sizeof(Lnode); // 申请新结点空间
s->data = e;
s->next = p->next;
p->next = s;
return OK;
}
复杂度:T(n)=O(n)
(5)删除:
删除单链表 L 的第 i 个结点并记录。还是先找到第 i-1 个结点,将第 i 个结点释放。
Status ListDelete_L(LinkList &L, int i, ElemType &e)
{
LinkList p = L, q;
int j = 0;
while(p && j<i-1)
{
p = p->next;
j++
}
if(p==NULL || j>i-1)
return ERROR;
q = p->next; // p 指向第 i 个结点
p->next = q->next;
e = q->data;
free(q);
return OK;
}
复杂度:T(n)=O(n)
(6)动态建立单链表
输入 n 个数据,建立单链表 L。
① 头插法:在头结点和第一个结点之间插入新结点,逆序 输入 n 个元素。
void Create_L1(LinkList &L, int n)
{
LinkList p;
int i;
// 创建头结点
L = (LinkList)malloc(sizeof(Lnode);
L->data = 0;
L->next = NULL;
for(i=0; i<n; i++)
{
p = (LinkList)malloc(sizeof(Lnode); // 创建新结点
scanf("%d", &p->data);
p->next = L->next; // 将 p 插入为头结点的后继
L->next = p;
}
}
复杂度:T(n)=O(n)
② 尾插法:在单链表尾插入新结点,顺序输入 n 个元素。
void Create_L2(LinkList &L, int n)
{
LinkList p, s;
int i;
L = (LinkList)malloc(sizeof(Lnode);
L->data = 0;
L->next = NULL;
s = L;
for(i=0; i<n; i++)
{
p = (LinkList)malloc(sizeof(Lnode);
scanf("%d", &p->data);
p->next = NULL;
s->next = p; // 将 p 插入为 s 的后继
s = p; // 使 s 始终指向表尾
}
}
复杂度:T(n)=O(n)
(7)单链表适用于经常对数据进行插入/删除的操作的问题,不适合经常需要随机存取的问题。
2.3.2 循环链表
循环链表:表中最后一个结点的指针域指回头结点的链表。
从表中任一结点出发均可找到其他结点。与单链表的区别为在于算法的循环条件不同:
// 单链表
while(p!=NULL)
// 循环链表
while(p!=L)
2.3.3 双向链表
表中结点有 前驱指针域 和 后继指针域的链表。双向链表具有对称性。
typedef struct Dunode
{
struct Dunode *prior, *next; // 指针域
ElemType element; // 数据域
}DuLnode, *DuLinkList;
(1)删除:删除双向链表中指针 p 指向的结点
void del_dulist(DuLinkList p)
{
p->prior->next = p->next;
p->next->prior = p->prior;
free(p);
}
复杂度:T(n)=O(n)
(2)插入:在双向链表中指针 p 指向的结点前插入新结点(注意每一步的顺序)。
void ins_dulist(DuLinkList p, ElemType e)
{
DuLinkList s; // 新结点
s = (DuLinkList) malloc(sizeof(DuLnode));
s->element = e;
s->prior = p->prior;
p->prior->next = s;
s->next = p;
p->prior = s;
}
复杂度:T(n)=O(n)
2.3.4 静态链表
借助一维数组描述链式结构,使用游标模拟指针。每一个结点包括数据域和指针域,指针域的值为下一节点在数组中的位置(序号)。
静态链表的插入和删除操作不需要移动元素,仅需要修改“游标”即可。