第二章 线性表
线性表的定义和基本操作
线性表的定义
线性表是具有相同数据类型的n(n≥0)个数据元素的有限序列,其中n为表长,当n=0时线性表是一个空表,若当L命名线性表,则其一般表示为L=(a1,a2,.…,an)
ai 是线性表中“第i个”元素线性表中的位序
a1 是表头元素,an 是表尾元素
出第一个元素外,每个元素有且仅有一个直接前驱;除最后一个元素外,每个元素有且仅有一个直接后继
线性表的基本操作
InitList(&L):初始化表。构造一个空的线性表L,分配内存空间
DestroyList(&L):销毁操作。销毁线性表,并释放线性表L所占用的内存空间
Listlnsert(&L,i,e):插入操作。在表L中的第i个位置上插入指定元素e.
ListDelete(&L,i,&e):删除操作。删除表L中第i个位置的元素,并用e返回删除元素的值。
LocateElem(L,e):按值查找操作。在表L中查找具有给定关键字值的元元素
GetElem(Li):按位查找操作。获取表L中第i个位置的元素的值。
Length(L):求表长。返回线性表L的长度,即L中数据元素的个数。
PrintList(L):输出操作。按前后顺序输出线性表L的所有元素值。
Empty(L):判空操作。若L为空表,则返回true,否则返回false。
顺序表的定义
顺序表——用顺序存储的方式实现线性表
顺序表的特点
- 随机访问,可以在O(1)时间内找到第i个元素
- 存储密度高,每个节点只存储数据元素
- 拓展容量不方便,静态不可以拓展,动态分配拓展时间复杂度高
- 插入,删除操作不方便,需要移动大量元素
顺序表的实现
顺序表的实现—静态分配(存储空间是静态的,无法更改)
#include<stdio.h> #define MaxSize 10 //定义最大长度 //创建静态顺序表的结构 typedef struct{ int data[MaxSize];//用静态的数组存放数据元素 int lengeh;//顺序表的长度 }SqList;//顺序表的类型定义 //初始化静态顺序表 void InitList(SqList &L){ for(int i=0;i<MaxSize;i++){ L.data[i]=0; } L.lengeh=0; } int main(){ SqList L; InitList(L); return 0; }
顺序表的实现—动态分配
//动态 顺序表 #include<stdio.h> #include<stdlib.h> #define InitSize 10//顺序表的初始长度 struct SqList { int* data;//只是动态分配数组的指针 int MaxSize;//顺序表的最大容量 int lengh;//顺序表的当前长度 }; void InitList(struct SqList* L) { L->data = (int*)malloc(sizeof(int) + InitSize); for (int i = 0; i < L->MaxSize; i++) { L->data[i] = 0; } L->lengh = 0; L->MaxSize = InitSize; } //增加动态数组的长度 void IncreaseList(struct SqList* L, int len) { //用malloc函数申请一片连续的存储空间 int* p = L->data; L->data = (int*)malloc(sizeof(int) * (L->MaxSize + len)); //将数据复制到新区域中 for (int i = 0; i < L->lengh; i++) { L->data[i] = p[i]; } L->MaxSize = L->MaxSize + len;//顺序表最长长度增加len } void ListInsert(struct SqList *L,int i ,int e) { for (int j = L->lengh; j >=i; j--) { L->data[j] = L->data[j - 1]; } L->data[i - 1] = e; L->lengh++; } int main() { struct SqList L = { 0 }; InitList(&L); printf("%d\n", L.lengh); printf("%d\n", L.MaxSize); for (int i = 0; i < L.MaxSize; i++) { (&L)->data[i] = i; } (&L)->lengh = L.MaxSize; for (int i = 0; i < L.lengh; i++) { printf("%d ", L.data[i]); } printf("\n"); IncreaseList(&L, 10); printf("%d\n", L.lengh); printf("%d\n", L.MaxSize); for (int i = 0; i < L.lengh; i++) { printf("%d ", L.data[i]); } ListInsert(&L, 2, 2); printf("%d\n", L.lengh); printf("%d\n", L.MaxSize); for (int i = 0; i < L.lengh; i++) { printf("%d ", L.data[i]); } return 0; }
顺序表的基本操作
顺序表元素的插入
void ListInsert(struct SqList *L,int i ,int e) { for (int j = L->lengh; j >=i; j--) { L->data[j] = L->data[j - 1]; } L->data[i - 1] = e; L->lengh++; }
最好情况 :新元素插入到表尾,不需要移动其他元素,循环0次,时间复杂度=O(1)
最坏情况 :新元素插入到表头,需要将原有的n个元素全部向后移动,循环n次,最坏时间复杂度=O(n)
平均情况 :假设新元素插入到任何一个位置的概率相同,即i=1,2,3,...length的概率都是p=1/(n+1),则平均循环次数=np+(n-1)p+....+p=n/2,平均时间复杂度=O(n)
顺序表元素的删除
int ListDelete(struct SqList *L,int i,int *e) { //判断 if (i<1||i>L->lengh)//判断i的值是否有效 { return 0; } else { *e = L->data[i - 1];//将删除的元素赋值给e for (int j = i; j < L->lengh; j++)//将第i个位置的元素前移 { L->data[j - 1] = L->data[j]; } L->lengh--;//有效长度减一 return 1; } }
最好情况 :删除表尾元素,不需要移动其他元素,循环0次,时间复杂度=O(1)
最坏情况 :删除表头元素,需要将后续的n-1个元素全部向前移动,循环n-1次,最坏时间复杂度=O(n)
平均情况 :假设删除任何一个元素的概率相同,即i=1,2,3,...length的概率都是p=1/n,则平均循环次数=(n-1)p+(n-2)p+....+p=(n-1)/2,平均时间复杂度=O(n)
顺序表的按位查找
GetElem(L,i)按位查找操作,获取表L中的第i位序的元素的值
ElemType GetElem(SeqList L,int i){ return L.data[i - 1]; }
顺序表的按值查找
Locate(SeqList L, ElemType e) 按值查找操作,在表L中查找具有给定关键字的元素
ElemType LocateElem(SeqList L, ElemType e) { for (int i = 0; i <= L.length; i++) { if (L.data[i] == e) { return i + 1; } } return -1; }
在注重考察语法的时候,基本数据类型可以用“==”比较
时间复杂度O(n)
单链表的定义
每个结点除了数据元素外,还可以存储指向下一个结点的指针
头结点和头指针的关系: 不管带不带头结点,头指针都始终指向链表的第一个结点,而头结点是带头结点的链表的第一个结点,结点内通常不存储信息。
引入头结点后,可以带来两个优点:
- 由于第一个数据节点的位置被存放在头结点的指针域中,因此在链表的第一个位置上的操作在表的其他位置上的操作一致,无须进行特殊处理
- 无论链表是否为空,其头指针都是指向头结点的非空指针(空表中头结点的指针域为空),因此空表和费控表的处理也就得到了统一
单链表的特点
优点:不要求大片连续空间,改变容量方方便
缺点:不可随机存取,要耗费一定空间存放指针
单链表的实现
点击查看代码
#include<stdio.h> #include<stdlib.h> typedef int ElemType; typedef struct LNode { ElemType data; struct LNode *next; } LNode, *LinkList; bool InitList(LinkList &L) { L = NULL; return true; } bool InitList_h(LinkList &L) { L = (LNode*)malloc(sizeof(LNode)); if (L == NULL) { return false; } L->next = NULL; return true; } bool ListInsert_h(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; } 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; } 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; } 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->data = p->data; p->data = e; return true; } //按位序删除(带头节点) 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; } if (p->next == NULL) { return false; } LNode *q = p->next; e = q->data; p->next = q->next; free(q); return true; } 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; } LNode* GetElem_h(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; } LNode* LocateElem(LinkList L, ElemType e) { LNode *p = L->next; //指向头结点的下一个节点 while (p != NULL && p->data != e) { p = p->next; } return p; } int Length_h(LinkList L) { int len = 0; LNode *p = L; while (p->next != NULL) { p = p->next; len++; } return len; } LinkList List_HeadInsert(LinkList &L) { LNode *s; int x; L = (LNode*)malloc(sizeof(LNode)); L->next = NULL; scanf("%d", &x); while (x != 9999) { s = (LNode*)malloc(sizeof(LNode)); s->data = x; s->next = L->next; L->next = s; scanf("%d", &x); } } LinkList List_TailInsert(LinkList &L) { int x; L = (LNode*)malloc(sizeof(LNode)); LNode *s, *r = L; scanf("%d", &x); while (x != 9999) { s = (LNode*)malloc(sizeof(LNode)); s->data = x; r->next = s; r = s; scanf("%d", &x); } r->next = NULL; return L; } int main() { LinkList L; return 0; }
结构体创建
typedef struct LNode { ElemType data; struct LNode *next; } LNode, *LinkList;typedef
初始化
带头结点
bool InitList_h(LinkList &L) { L = (LNode*)malloc(sizeof(LNode)); if (L == NULL) { return false; } L->next = NULL; return true; }
带头结点的单链表初始化时,需要创建一个头结点,并让头指针指向头结点。头结点的next域初始化为NULL
不带头结点
bool InitList(LinkList &L) { L = NULL; return true; }
不带头结点的单链表初始化时,只需将头指针L初始化为NULL
单链表的基本操作
按位序插入(带头节点)
bool ListInsert_h(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; }
本算法主要的时间开销在于查找第i-1
个元素,,时间复杂度为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; }
当链表不带头结点时,需要判断插入位置i是否为1,若是,则要做特殊处理,将头指针L指向新的首结点。当链表带头结点时,插入位置i为1时不做特殊处理。
指定结点的后插操作
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; }
在指定结点后插入新结点,时间复杂度为O(1)
指定结点的前插操作
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->data = p->data; p->data = e; //结点放后面,交换值 return true; }
在指定结点后插入新结点,时间复杂度为O(1)
按位序删除(带头节点)
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; } if(p->next==NULL){//确定第i个节点存在 return false; } LNode *q=p->next; e=q->data; p->next=q->next; free(q); return true; }
时间复杂度为O(n)
指定结点的删除
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; }
实质就是将其后继的值赋予其自身,然后再删除后继,也能使得时间复杂度为O(1).
这种算法存在缺陷,当要删除的结点是最后一个结点的时候,无法顺利删除,因为无法找到尾结点的后继
按位查找
LNode* GetElem_h(LinkList L, int i) { if (i < 0) { return NULL; } LNode *p; int j = 0; //头结点是第0个结点 p = L; while (p != NULL && j < i) { p = p->next; j++; } return p; }
时间复杂度O(n)
按值查找
LNode* LocateElem(LinkList L,ElemType e){ LNode *p=L->next;//指向头结点的下一个节点 while(p!=NULL&&p->date!=e){ p=p->next; } return p; }
时间复杂度O(n)
求表的长度
int Length_h(LinkList L) { int len = 0; LNode *p = L; while (p->next != NULL) { p = p->next; len++; } return len; }
带头结点的单链表中求表长len
的初始长度为0,因为头结点是第0个结点。不带头结点的单链表中求表长len
的初始长度为1,因为直接从第一个结点开始计数。
求表长操作的时间复杂度为O(n),
单链表的建立
尾插法
该方法将新结点插入到当前链表的表尾,为此必须增加一个尾指针r,使其始终指向当前链表的尾结点
LinkList List_TailInsert(LinkList &L){ int x; L=(LNode*)malloc(sizeof(LNode)); LNode *s,*r=L; scanf("%d",&x); while(x!9999){ s=(LNode*)malloc(sizeof(LNode)); s->data=x; r->next=s; r=s; scanf("%d",&x); } r->next=NULL; return L; }
头插法
该方法从一个空表开始,生成新结点,并将读取到的数据存放到新结点的数据域中,然后将新结点插入到当前链表的表头,即头结点之后
LinkList List_TailInsert(LinkList &L){ int x; L=(LNode*)malloc(sizeof(LNode)); LNode *s,*r=L; scanf("%d",&x); while(x!=9999){ s=(LNode*)malloc(sizeof(LNode)); s->data=x; r->next=s; r=s; scanf("%d",&x); } r->next=NULL; return L; }
采用头插法建立单链表时,读入数据的顺序与生成的链表中元素的顺序是相反的,可用来实现链表的逆置
双链表
双链表结点中有两个指prior
和next
,分别指向其直接前驱和直接后继
双链表的特点
双链表在单链表结点中增加了一个指向前驱的指针prior
,因此双链表的按值查找和按位查找的操作与单链表相同。但双链表在插入和删除操作的实现上,与单链表有着较大的不同,这是因为“链”变化时也需要对指针prior
做出修改,其关键是保证在修改的过程中不断链
双链表的实现
结构体创建
typedef struct DNode { ElemType data; struct DNode *prior, *next; } DNode, *DLinkList;
初始化
带头结点
bool InitLinkList_h(DLinkList &L){ L=(DNode*)malloc(sizeof(DNode)); if(L==NULL){ return false; } L->prior=NULL; L->next=NULL; }
不带头结点
bool IinitLinkList(DLinkList &L){ L=NULL; return true; }
双链表的基本操作
给定结点的后插操作
bool InsertNextDNode(DNode *p, DNode *s) { if (p == NULL || s == NULL) { return false; } s->next = p->next; if (p->next != NULL) { p->next->prior = s; } s->prior = p; p->next = s; return true; }
给定结点的前插操作
bool InsertPriorDNode(DNode *p, DNode *s) { if (p == NULL || s == NULL) { return false; } s->next = p; s->prior = p->prior; if (p->prior != NULL) { p->prior->next = s; } p->prior = s; return true; }
删除给定结点的后继结点
bool DeleteNextDNode(DNode *p) { 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; }
双链表的销毁
void DestroyList(DLinkList &L) { while (L->next != NULL) { DeleteNextDNode(L); } free(L); L = NULL; }
循环链表
循环单链表
在循环单链表中,表尾结点*r的next指针域指向L,故表中没有指针域为NULL的结点,因此,循环单链表的判空条件不是头结点的指针是否为空,而是它是否等于头指针L
初始化
bool InitList(LinkList &L) { L = (LNode*)malloc(sizeof(LNode)); if (L == NULL) { return false; } L->next = L;//头结点的next指向头结点 return true; }
判断给定结点是否为循环单链表的表尾结点
bool isTail(LinkList L, LNode *p) { if (p->next == L) { return true; } else { return false; } }
循环双链表
在循环双链表中,头结点的prior指针还要指向表尾结点,当某结点*p为尾结点时,p->next==L
;当循环双链表为空时,其头结点的prior域和next域都等于L
初始化
bool InitDLinkList(DLinkList &L) { L = (DNode*)malloc(sizeof(DNode)); if (L == NULL) { return false; } L->next = L; L->prior = L; return true; }
静态链表
静态链表是用数组来描述线性表的链式存储结构,结点也有数据域和指针域,这里的指针是结点在数组中的相对位置,又称游标,指针指向下一个结点的位置。
静态链表也要预先分配一块连续的内存空间。
typedef struct { ElemType data; int next; }SLinkList[MaxSize];//一个长度为MaxSize的数组
插入位序为i的结点
- 找到一个空的结点,存入输入元素
- 从头结点出发找到位序为i-1的结点
- 修改新结点的next
- 修改i-1号结点的next
优点:增删操作不需要大量移动元素
缺点:不能随机存取,只能从头结点开始一次往后查找,容量固定不可变
顺序表和链表的比较
逻辑结构
都属于线性表,都是线性结构
存储结构
顺序表
优点:支持随机存取,存储密度高
缺点:大片连续空间分配不方便,改变容量不方便
链表
优点:离散的小空间分配方便,改变容量方便
缺点:不可随机存取,存储密度小
基本操作
顺序表
创建:需要预分配大片连续空间,若分配空间过小,则之后不方便拓展容量;若分配空间过大,则浪费内存空间
插入、删除:要将后续元素都后移或前移,时间复杂度为O(n),主要开销是移动元素,若移动元素过大,则移动的时间代价很高
查找:按位查找:O(1) 按值查找O(n) 若表中元素有序,可在O(ln(n))时间内找到
链表
创建:只需分配一个头结点(也可以不用头结点,只声明一个头指针),之后方便拓展
插入,删除:只需修改指针,时间复杂度为O(n),时间开销主要来自查找目标元素,查找元素的时间代价更低
查找:按位查找O(n) 按值查找O(n)
表长难以估计,经常需要增加、删除元素 ——链表
表长可预估,查询操作较多 ——顺序表
本文来自博客园,作者:Eulbo_1018,转载请注明原文链接:https://www.cnblogs.com/eulbo-1018/p/18344328
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步