数据结构笔记——线性表
目录
一、线性表的类型定义
线性表是最常用且最简单的一种数据结构。一个线性表是n个数据元素的有限序列。线性表中的每个元素可以是一个数、一个符号甚至更复杂的内容,但是同一线性表中元素必定有相同的特性,即属同一数据对象,相邻数据元素之间存在着序偶关系。
除此之外,线性表一定满足线性结构的特点。
线性结构的特点:在数据元素的非空有限集中,(1)存在唯一的一个被称做“第一个”的数据元素;(2)存在唯一的一个被称做“最后一个”的数据元素;(3)除第一个之外,集合中的每个数据元素均只有一个前驱;(4)除最后一个之外,集合中每个数据元素均只有一个后继。
线性表有两种表示方式(存储方式):一种是顺序结构存储,我们叫做顺序表;另一种是链式结构存储,我们叫做链表。这会在后面分别介绍。
线性表的基本操作:
- InitList(&L) // 构造一个空的线性表L
- DestroyList(&L) // 销毁已存在的线性表L
- ClearList(&L) // 将L重置为空表
- ListInsert(&L, i, e) // 在线性表L中的第i个位置插入新元素e
- ListDelete(&L, i, &e) // 删除线性表L中的第i个元素,并用e返回其值
- IsEmpty(L) // 判断L是否为空表
- ListLength(L) // 返回线性表L中的数据元素个数
- LocateElem(L, e) // 查找与给定值e满足特定关系的数据元素的位序,若不存在则返回0
- GetElem(L, i, &e) // 用e返回线性表L中第i个数据元素的值
二、顺序表
顺序表是用一组地址连续的存储单元依次存储线性表的数据元素。
假设线性表的每个元素占 个字节,那么根据顺序表的特性,可以得到第 i 个元素的地址和第 1 个元素的地址关系为
顺序表的图示结构:
接下来看看顺序表的代码实现:
1.存储结构:
- #define LIST_INIT_SIZE 100 // 线性表存储空间的初始分配量
- #define LISTINCREMENT 10 // 线性表存储空间的分配增量
- typedef struct{
- ElemType *elem; // 存储空间基址
- int length; // 当前长度
- int listsize; // 当前分配的存储容量(以sizeof(ElemType)为单位)
- }SqList;
2.初始化操作:
- Status InitList_Sq(SqList &L){
- // 构造一个空的线性表L
- L.elem = (ElemType *)malloc(LIST_INIT_SIZE * sizeof(ElemType)); // malloc()为头文件stdlib.h下的分配地址函数
- if(!L.elem) exit(OVERFLOW); // 存储分配失败
- L.length = 0; // 空表长度为0
- L.listsize = LIST_INIT_SIZE; // 初始存储容量
- return OK;
- } // InitList_Sq
时间复杂度:
3.销毁操作:
- void DestroyList_Sq(SqList &L){
- // 销毁线性表L
- if(L.elem) free(L.elem); // 释放存储空间
- } // DestroyList_Sq
时间复杂度:
4.清空操作:
- void ClearList_Sq(SqList &L){
- // 将线性表L重置为空表
- L.length = 0; // 将线性表长度重置为0
- } // ClearList_Sq
时间复杂度:
5.插入操作:
- Status ListInsert_Sq(SqList &L, int i, ElemType e){
- // 在顺序线性表L中第i个位置之前插入新的元素e
- // i的合法值为1<=i<=ListLength(L)+1
- if(i < 1 || i > L.length + 1) return ERROR; // i值不合法
- if(L.length >= L.listsize){ // 当前存储空间已满,增加分配
- newbase = (ElemType *)realloc(L.elem, (L.listsize + LISTINCREMENT) * sizeof(ElemType)); // realloc()为头文件stdlib.h下的重新分配地址函数
- if(!newbase) exit(OVERFLOW); // 存储分配失败
- L.elem = newbase; // 新基址
- L.listsize += LISTINCREMENT; // 增加存储容量
- }
- q = &(L.elem[i - 1]); // q为插入位置
- for(p = &(L.elem[L.length - 1]); p >= q; --p) *(p + 1) = *p; // 插入位置及之后的元素右移
- *q = e; // 插入e
- ++L.length; // 表长增1
- return OK;
- } // ListInsert_Sq
平均操作次数:
时间复杂度:
6.删除操作:
- Status ListDelete_Sq(SqList &L, int i, ElemType &e){
- // 在顺序线性表L中删除第i个元素,并用e返回其值
- // i的合法值为1<=i<=ListLength(L)
- if(i < 1 || i > L.length) return ERROR; // i值不合法
- p = &(L.elem[i - 1]); // p为被删除元素的位置
- e = *p; // 被删除元素的值赋给e
- q = L.elem + L.length - 1; // 表尾元素的位置
- for(++p; p <= q; ++p) *(p - 1) = *p; // 被删除元素之后的元素左移
- --L.length; // 表长减1
- return OK;
- } // ListDelete_Sq
平均操作次数:
时间复杂度:
7.判断空表:
- int IsEmpty_Sq(SqList L){
- // 判断线性表L是否为空
- if(L.length == 0) return TRUE; // 数据元素长度为0,返回1;反之返回0
- else return FALSE;
- } // IsEmpty_Sq
时间复杂度:
8.求长度操作:
- int ListLength_Sq(SqList L){
- // 求线性表L的长度
- return L.length;
- } // ListLength_Sq
时间复杂度:
9.查找操作:
- int LocateElem_Sq(SqList L, ElemType e){
- // 在顺序线性表L中查找第1个值与e满足compare()的元素的位序
- // 若找到,则返回其在L中的位序,否则返回0
- i = 1; // i的初值为第1个元素的位序
- p = L.elem; // p的初值为第1个元素的存储位置
- while(i <= L.length && !compare(*p++, e)) ++i; // compare()为查找的条件
- if(i <= L.length) return i; // 若找到则返回数据元素的位序
- else return 0;
- } // LocateElem_Sq
平均操作次数:(其中n为顺序表中数据元素的个数)
时间复杂度:
10.取值操作:
- Status GetElem_Sq(SqList L, int i, ElemType &e){
- // 用e返回顺序表L中第i个元素的值
- if(i < 1 || i > L.length) return ERROR; // 判断i的值是否合理
- e = L.elem[i - 1]; // 将第i个元素值赋给e
- return OK;
- } // GetElem_Sq
时间复杂度:
总结
顺序表的优点:
(1)存储密度大(结点本身所占存储量/结点结构所占存储量)
(2)可以随机存取表中任意元素
顺序表的缺点:
(1)再插入、删除某一元素时,需要移动大量元素
(2)浪费存储空间
(3)属于静态存储形式,数据元素的个数不能自由扩充
三、链表
链表是用一组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的)。
链表中每个数据都有两部分信息,一部分是自身元素的信息(数据域),另一部分是指示前继或后继的信息(指针域),这两部分组成的数据元素的存储映像称为结点。指针域中存储的信息称为指针或链。
1.单链表(线性链表)
单链表是每个结点只有一个数据域和一个指针域构成,且指针域指示后继结点的信息,尾元结点的指针为空(NULL)。
单链表的图示结构:
接下来看看单链表的代码实现:
1.存储结构:
- typedef struct LNode{
- ElemType data; // 数据元素
- struct LNode *next; // 指针元素
- }LNode, *LinkList;
2.初始化操作:
- Status InitList_L(LinkList &L){
- // 初始化单链表L
- L = (LinkList)malloc(sizeof(LNode)); // 生成头结点
- L->next = NULL; // 头结点指针初始化为空
- return OK;
- } // InitList_L
时间复杂度:
3.销毁操作:
- Status DestroyList_L(LinkList &L){
- // 销毁单链表L
- LNode *p; // 或LinkList p;
- while(L){ // 从头结点开始依次释放结点
- p = L;
- L = L->next;
- free(p); // 释放结点
- }
- return OK;
- } // DestroyList_L
时间复杂度:
4.清空操作:
- Status ClearList_L(LinkList &L){
- // 清空单链表L
- LNode *p, *q; // 或LinkList p, q;
- p = L->next;
- while(p){ // 循环单链表,从首元结点开始释放结点
- q = p->next;
- free(p);
- p = q;
- }
- L->next = NULL; // 头结点指针变为空
- return OK;
- } // ClearList_L
时间复杂度:
5.插入操作:
- Status ListInsert_L(LinkList &L, int i, ElemType e){
- // 在带头结点的单链线性表L中第i个位置之前插入元素e
- p = L; j = 0;
- while(p && j < i - 1){ // 寻找第i-1个结点
- p = p->next; ++j;
- }
- if(!p || j > i - 1) return ERROR; // i小于1或者大于表长加1
- s = (LinkList)malloc(sizeof(LNode)); // 生成新结点
- s->data = e; // 插入L中
- s->next = p->next;
- p->next = s;
- return OK;
- } // ListInsert_L
时间复杂度:查找部分,插入部分
6.删除操作:
- Status ListDelete_L(LinkList &L, int i, ElemType &e){
- // 在带头结点的单链线性表L中,删除第i个元素,并由e返回其值
- p = L; j = 0;
- while(p->next && j < i - 1){ // 寻找第i个结点,并令p指向其前驱
- p = p->next; ++j;
- }
- if(!(p->next) || j > i - 1) return ERROR; // 删除位置不合理
- q = p->next; // 删除并释放结点
- p->next = q->next;
- e = q->data;
- free(q);
- return OK;
- } // ListDelete_L
时间复杂度:查找部分,删除部分
7.判断空表:
- int IsEmpty_L(LinkList L){
- // 判断链表L是否为空
- if(L->next) return FALSE; // 非空
- else return TRUE;
- } // IsEmpty_L
时间复杂度:
8.求长度操作:
- int ListLength_L(LinkList L){
- // 求单链表L的表长
- LNode *p;
- p = L->next; // p指向首元结点
- i = 0;
- while(p){ // 遍历单链表,统计结点数
- i++;
- p = p->next;
- }
- return i; // 返回结点数
- } // ListLength_L
时间复杂度:
9.查找操作:
- LNode *LocateElem_L(LinkList L, ElemType e){
- // 在单链表L中查找满足条件compare()的数据元素
- // 若找到则返回该数据元素的地址,否则返回NULL
- p = L->next;
- // int j = 0; 若要返回位序则定义一个j记数据的位置
- while(p && !compare(p->data, e)){
- p = p-> next;
- // j++;
- }
- if(L->next != NULL) return p; // 若要返回位序,则return j;
- else return NULL;
- } // LocateElem_L
时间复杂度:
10.取值操作:
- Status GetElem_L(LinkList L, int i, ElemType &e){
- // L为带头结点的单链表的头指针。
- // 当第i个元素存在时,其值赋给e并返回OK,否则返回ERROR
- p = L -> next; j = 1; // 初始化,p指向第1个结点,j为计数器
- while(p && j < i){ // 顺指针向后查找,直到p指向第i个元素或p为空
- p = p->next; ++j;
- }
- if(!p || j > i) return ERROR; // 第i个元素不存在
- e = p->data; // 取第i个元素
- return OK;
- } // GetElem_L
时间复杂度: 查找部分,取值部分
11.头插法创建单链表
- void CreateList_H(LinkList &L, int n){
- // 逆位序输入n个元素的值,建立带表头结点的单链线性表L
- L = (LinkList)malloc(sizeof(LNode));
- L->next = NULL; // 先建立一个带头结点的单链表
- for(i = n; i > 0; --i){
- p = (LinkList)malloc(sizeof(LNode)); // 生成新结点
- scanf(&p->data); // 输入元素值
- p->next = L->next; // 插入到表头
- L->next = p;
- }
- } // CreateList_H
时间复杂度:创建 n 个元素的表为,每次插入元素为
12.尾插法创建单链表
- void CreateList_R(LinkList &L, int n){
- // 正位序输入n个元素的值,建立带表头结点的单链表L
- L = (LinkList)malloc(sizeof(LNode));
- L->next = NULL; // 先建立一个带头结点的单链表
- r = L;
- for(i = 0; i < n; ++i){
- p = (LinkList)malloc(sizeof(LNode)); // 生成新结点
- scanf(&p->data); // 输入元素值
- p->next = NULL;
- r->next = p; // 插入到表尾
- r = p; // 指向新的尾元结点
- }
- } // CreateList_R
时间复杂度:创建 n 个元素的表为,每次插入元素为
2.循环链表
循环链表是每个结点只有一个数据域和一个指针域构成,且指针域指示后继结点的信息,尾元结点的指针域指示头结点(或首元结点)的信息。
循环链表有两种表示形式:
第一种是用头指针表示,那么表示如图所示:
这种方法查找的时间复杂度为
,但查找
的时间复杂度为
,实用性较低;
第二种是用尾指针表示,表示如图所示:
这种方法查找不管是查找还是
时间复杂度都是
,实用性较高。
循环链表的大部分操作都与单链表类似,这里就不再赘述了,可以自行进行分析。
3.双向链表
双向链表是每个结点只有一个数据域和两个指针域构成,指针域指示前驱结点和后继结点的信息,头结点的前驱指针为NULL,尾元结点的后继指针为NULL。
双向链表的图示结构:
双向循环链表的图示结构:
这里重点来看双向(循环)链表存储结构和双向循环链表的插入、删除操作:
1.存储结构:
- typedef struct DuLNode{
- ElemType data; // 数据元素
- struct DuLNode *prior; // 指向前驱的指针
- struct DuLNode *next; // 指向后继的指针
- }DuLNode, *DuLinkList;
2.插入操作:
- Status ListInsert_DuL(DuLinkList &L, int i, ElemType e){
- // 在带头结点的双链循环线性表L中第i个位置之前插入元素e
- // i的合法值为1<=i<=表长+1
- if(!(p == GetElemP_DuL(L, i))) // 在L中确定插入位置
- return ERROR; // p = NULL,即插入位置不合法
- if(!(s == (DuLinkList)malloc(sizeof(DuLNode)))) return ERROR;
- s->data = e;
- s->prior = p->prior; p->prior->next = s;
- s->next = p; p->prior = s;
- return OK;
- } // ListInsert_DuL
时间复杂度:
3.删除操作:
- Status ListDelete_DuL(DuLinkList &L, int i, ElemType &e){
- // 删除带头结点的双链循环线性表L的第i个元素,i的合法值为1<=i<=表长
- if(!(p == GetElemP_DuL(L, i))) // 在L中确定删除位置
- return ERROR; // p = NULL,即插入位置不合法
- e = p->data;
- p->prior->next = p->next;
- p->next->prior = p->prior;
- free(p);
- return OK;
- } // ListDelete_DuL
时间复杂度:
总结
链表的优点:
(1)能灵活地分配内存空间
(2)插入或删除元素效率高,每次操作仅需的时间
链表的缺点:
(1)不像数组一样通过下表读取元素,而是每次都要从表头开始一个一个读取
(2)查询第k个元素需要的时间