线性表的定义和基本操作
线性表的定义
线性表是具有相同数据类型的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];//将删除的元素赋值给efor (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)
表长难以估计,经常需要增加、删除元素 ——链表
表长可预估,查询操作较多 ——顺序表