线性表的定义和基本操作
线性表的定义
- 线性表是具有相同数据类型的 n 个数据元素的有限序列,其中 n 为表长,n=0 时是一个空表
- 线性表的逻辑特性:
- 除表头元素外,每一个元素都有且仅有一个直接前驱
- 除表尾元素外,每一个元素都有且仅有一个直接后继
- 线性表的特点如下:
- 表中元素个数有限
- 表中元素具有逻辑上的顺序性,即表中元素有先后次序
- 表中元素都是数据元素,每个元素都是单个元素
- 表中元素的数据类型相同,即每个元素占有相同大小的存储空间
- 表中元素具有抽象性,即只讨论元素之间的逻辑关系而不考虑元素的具体内容
- 注意,线性表是一种逻辑结构,表示元素之间一对一的相邻关系,而顺序表和链表指是指存储结构
线性表的基本操作
InitList(&L):初始化表,构造一个空的线性表Length(L):求表长,返回表的长度,即 L 中元素的个数LocateElem(L, e):按值查找,在表中查找具有给定关键字值的元素GetElem(L, i):按位查找,查找在 L 中第 i 个位置上的元素ListInsert(&L, i, e):插入操作,在 L 中的第 i 个位置上插入元素 eListDelete(&L, i, &e):删除操作,删除 L 中的第 i 个位置上的元素,并用 e 返回元素值PrintList(L):输出表Empty(L):判空操作,若 L 为空表则返回 true,若不为空表则返回 falseDestroyList(&L):销毁操作,释放 L 所占用的内存空间
InitList(&L):初始化表,构造一个空的线性表
Length(L):求表长,返回表的长度,即 L 中元素的个数
LocateElem(L, e):按值查找,在表中查找具有给定关键字值的元素
GetElem(L, i):按位查找,查找在 L 中第 i 个位置上的元素
ListInsert(&L, i, e):插入操作,在 L 中的第 i 个位置上插入元素 e
ListDelete(&L, i, &e):删除操作,删除 L 中的第 i 个位置上的元素,并用 e 返回元素值
PrintList(L):输出表
Empty(L):判空操作,若 L 为空表则返回 true,若不为空表则返回 false
DestroyList(&L):销毁操作,释放 L 所占用的内存空间
线性表的顺序表示
顺序表的定义
-
线性表的顺序存储结构又称为顺序表
-
它的特点是:
- 逻辑顺序与物理顺序相同,所以删除和插入操作需要移动大量元素
- 可以实现随机存取,即通过首地址和元素与序号可在 O(1) 内找到指定的元素
- 存储密度高,每个结点只存储数据元素
-
线性表的顺序存储结构描述为
#define MaxSize 50 // 定义线性表的最大长度 typedef struct { int data[MaxSize]; // 顺序表的元素 int Length; // 顺序表的当前长度 } SqList; // 顺序表的类型定义 -
数组可以静态分配,也可以动态分配,动态分配时要开辟一块更大的存储空间,来替换原来的存储空间
#define MaxSize 50 // 表长度的初始定义 typedef struct { int *data; // 动态分配数组的指针 int MaxSize, length; // 数组的最大容量和当前个数 } SeqList; // 动态分配数组顺序表的类型定义 -
动态分配语句
L.data = (int *)malloc(sizeof(int)*InitSize); // C 语言的动态分配语句 L.data = new int[InitSize]; // C++ 的动态分配语句 -
注意,动态分配并不是链式存储,它同样属于顺序存储结构,物理结构没有变化,依然是随机存取方式,只是分配的空间大小可以在运行时动态决定
顺序表的基本操作
-
插入操作
-
在 L 上的第 i(1≤i≤L.length+1)个位置插入新元素 e
-
代码如下:
bool ListInsert(SqList &L, int i, int e) { if(i<1 || i>L.length+1) // 判断 i 的位置是否合法 return false; if(L.length >= MaxSize) // 判断存储空间是否已满 return false; for(int j=L.length; j>=i; j--) // 将第 i 个位置以及之后的元素都后移 L.data[j] = L.data[j-1]; L.data[i-1] = e; // 在第 i 个位置处插入元素 e L.length ++; // 线性表的长度加 1 return true; } -
时间复杂度:O(n)
-
-
删除操作
-
在 L 上的第 i(1≤i≤L.length)个位置删除元素,并用 e 返回
-
代码如下:
bool ListDelete(SqList &L, int i, int &e) { if(i<1 || i>L.length) // 判断 i 的位置是否合法 return false; e = L.data[i-1]; // 将删除的元素赋值给 e for(int j=i; j<L.length; j++) // 将第 i 个位置以及之后的元素都前移 L.data[j-1] = L.data[j]; L.length --; return true; } // 时间复杂度:O(n) -
时间复杂度:O(n)
-
-
按值查找(顺序查找)
-
在 L 中查找第一个值为 e 的元素,并返回其次序
-
代码如下:
int LocateElem(SqList L, int e) { for(int i=0; i<L.length; i++) if(L.data[i] == e) return i+1; // 若查找成功,返回位序 i+1 return 0; } // 时间复杂度:O(n) -
时间复杂度:O(n)
-
线性表的链式表示
单链表的定义
单链表的结点类型描述如下:
typedef struct LNode { // 定义单链表结点类型
int data; // 数据域
struct LNode *next; // 指针域
} LNode, *LinkList;
单链表的基本操作
-
头插法建立单链表
LinkList List_HeadInsert(LinkList &L) { // 逆向建立单链表 L = (LinkList)malloc(sizeof(LNode)); // 创建头结点 L->next = NULL; // 初始为空链表 LNode *s; int x; scanf("%d", &x); // 输入结点的值 while(x != 9999) { // 输入 9999 表示结束 s = (LNode *)malloc(sizeof(LNode)); // 创建新结点 s->data = x; s->next = L->next; L->next = s; // 将新结点插入 x 中,L 为头指针 scanf("%d", &x); } return L; }- 头插法建立单链表时,读入数据的顺序与生成的链表的元素的顺序是相反的
- 时间复杂度:O(n)
-
尾插法建立单链表
LinkList List_TailInsert(LinkList &L) { // 正向建立单链表 L = (LinkList)malloc(sizeof(LNode)); // 创建头结点 LNode *s, *r=L; // r 为表尾指针 int x; scanf("%d", &x); // 输入结点的值 while(x != 9999) { // 输入 9999 表示结束 s = (LNode *)malloc(sizeof(LNode)); // 创建新结点 s->data = x; r->next = s; // r 指向新的表尾结点 scanf("%d", &x); } r->next = NULL; // 尾结点置空 return L; } -
按位查找(查找第 i 个结点)
LNode *GetElem(LinkList L, int i) { int j = 1; // 计数,初始为 1 LNode *p = L->next; // 头结点指针赋给 p if(i == 0) return L; // 返回头结点 if(i < 1) return NULL; // i 无效时返回 NULL while(p && j<i) { // 从第一个结点开始找,查找第 i 个结点 p = p->next; j ++; } return p; // 返回第 i 个结点的指针,若 i 大于表长,则返回 NULL }- 时间复杂度:O(n)
-
插入结点
p = GetElem(L, i-1); // 查找插入位置的前驱结点 s->next = p->next; p->next = s;-
时间复杂度:O(n)
-
前插操作
s->next = p->next; // 修改指针域 p->next = s; temp = p->data; p->data = s->data; s->data = temp;
-
-
删除结点
p = GetElem(L, i-1); // 查找删除位置的前驱结点 q = p->next; // q 指向被删除结点 p->next = q->next; // 将 *q 结点从链中断开 free(q); // 释放结点空间-
时间复杂度:O(n)
-
删除操作的另一种方法
q = p->next; // q 指向被删除结点 p->data = p->next->data; // 和后继结点交换数据域 p->next = q->next; // 将 *q 结点从链中断开 free(q); // 释放结点空间
-
双链表
-
单链表只能从头依次顺序地向后遍历,双链表可以克服单链表的这个缺点
-
双链表的结点类型描述如下:
typedef struct DNode{ // 定义双链表结点类型 int data; // 数据域 struct DNode *prior, *next; // 前驱和后继指针 } DNode, *DLinkList; -
双链表的插入操作
s->next = p->next; p->next->prior = s; s->prior = p; p->next = s;- 语句顺序不唯一,但前两步必须在第四步之前
-
双链表的删除操作
p->next = q->next; q->next->prior = p; free(q); // 释放结点空间
循环链表
- 循环单链表
- 循环单链表的最后一个结点的指针不是 NULL,而是指向头结点,形成了一个环
- 循环单链表判空的条件是:头结点是否等于头指针
- 循环单链表在所有位置上的插入和删除操作都是等价的,不需要判断是否在表尾,因此,对循环单链表不设头指针而设尾指针,因为
r->next就是头指针
- 循环双链表
- 循环双链表中,对尾结点 *p 有:
p->next = L - 循环双链表为空时,其头结点的 prior 域和 next 域都等于 L
- 循环双链表中,对尾结点 *p 有:
静态链表
-
静态链表用数组来描述线性表的链式存储结构,它的结点也有数据域 data 和指针域 next,但指针是结点的相对地址(数组下标,又称游标)
-
与顺序表一样,静态链表也需要预先分配一块连续的内存空间
-
静态链表的结点类型描述如下:
#define MaxSize 50 // 静态链表的最大长度 typedef struct { // 定义静态链表结点类型 int data; // 存储数据元素 int next; // 下一个元素的数组下标 } SLinkList[MaxSize]; -
静态链表以
next==-1作为结束的标志 -
它的插入和删除操作与动态链表相同,只需要修改指针而不需要移动元素
-
在不支持指针的高级语言(如 Basic)中会方便一些