?、目录总览
1、线性表
定义:线性表是具有相同数据类型的n(n≥0)个数据元素的有限序列,其中n为表长,当n = 0 时线性表是一个空表。若用L命名线性表,则其一般表示为:
L = (a1,a2,....,ai,ai+1,.....,an)
线性表的特点:
- 线性表中的元素的个数是有限的
- 线性表中的元素有先后次序
- 线性表中的数据类型都相同,这意味着每个元素占有相同大小的存储空间
- ai 是线性表中的 “第i个” 元素线性表中的位序(位序是从1开始,数组下标从0开始)
- a1是表头元素,an是表尾元素
- 除第一个元素外,每个元素有且仅有一个直接前驱;除最后一个元素外,每个元素有且仅有一个直接后继
2、顺序表
定义:把用顺序存储的方式实现的线性表叫做顺序表
顺序存储:把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。
顺序表有两种实现方式:静态分配实现和动态分配实现
2.1、静态分配实现
#define MaxSize 10;
typedef struct{
ElemType data[MaxSize];
int length;
}SqList;
2.2.1、初始化顺序表
#include<stdio.h>
#define MaxSize 10;
typedef struct{
ElemType data[MaxSize];
int length;
}SqList;
void InitList(SqList &L){
for(int i=0;i<MaxSize;i++)
{
L.data[i] = 0;
}
L.length = 0;
}
int main(){
SqList L;
InitList(L);
return 0;
}
2.2、动态分配实现
#define MaxSize 10;
typedef struct{
ElemType *data;
int MaxSize;
int length;
}SeqList;
2.2.1、初始化顺序表
#define MaxSize 10;
#include<stdio.h>
#include<stdlib.h>
typedef struct{
ElemType *data;
int MaxSize;
int length;
}SeqList;
void InitList(SeqList &L){
L.data=(ElemType *)malloc(sizeof(ElemTyoe)*InitSize);
L.length = 0;
L.MaxSize = InitSize;
}
void IncreaseSize(SeqList &L,int len){
int *p = L.data;
L.data=(ElemType *)malloc(sizeof(ElemType)*(L.MaxSize+len));
for(int i=0; i<L.length; i++){
L.data[i] = p[i];
}
L.MaxSize = L.MaxSize + len;
free(p);
}
int main(){
SeqList L;
InitList(L);
IncreaseSize(L,5);
return 0;
}
2.3、顺序表的特点
-
随机访问:即可以在O(1)时间内中找到第i个元素 -
存储密度高,每个节点只存储数据元素 -
扩展容量不方便 -
插入、删除操作不方便,需要移动大量元素
2.4、顺序表的插入
ListInsert(&L,i,e) :插入操作,在表L中的第 i 个位置上插入指定元素e
#define MaxSize 10;
typedef struct{
ElemType data[MaxSize];
int length;
}SqList;
bool ListInsert(SqList &L,int i,int e){
if(i<1 || i>L.length+1)
return false;
if(L.length >= MaxSize)
return false;
for(int j=L.length;j>=i;j--)
{
L.data[j] = L.data[j-1];
}
L.data[i-1]=e;
L.length++;
return true;
}
int main(){
SqList L;
InitList(L);
ListInsert(L,3,3);
return 0;
}
2.4.1、插入操作时间复杂度
只需关注最深层循环语句的执行次数与问题规模n的关系
for(int j=L.length;j>=i;j--)
{
L.data[j] = L.data[j-1];
}
- 最好情况:新元素插入到表尾,不需要移动元素,时间复杂度O(1)
- 最坏情况:新元素插入到表头,需要将原有的n个元素全都向后移动,循环n次,时间复杂度O(n)
- 平均情况:假设新元素插入到任何一个位置的概率相同,时间复杂度O(n)
2.5、顺序表的删除
ListDelete(&L,i,&e) :删除操作。删除表L中第i个位置的元素,并用e返回删除元素的值
bool ListDelete(SqList &L,int i,int &e){
if(i<1 || i>L.length+1)
return false;
e = L.data[i-1];
for(int j=i;j<length;j++)
{
L.data[j-1] = L.data[j];
}
L.length--;
return true;
}
int main(){
SqList L;
InitList(L);
int e = -1;
if(ListDelete(L,3,e)){
print("已删除第3个元素,删除元素值为=%d\n",e);
}else{
print("位序i不合法,删除失败\n");
}
return 0;
}
- 这里函数定义中的参数e加了引用符号,目的是使得在此时声明的变量e和main函数中声明的变量e是同一片内存空间
2.5.1、删除操作时间复杂度
只需关注最深层循环语句的执行次数与问题规模n的关系
for(int j=i;j<length;j++)
{
L.data[j-1] = L.data[j];
}
- 最好情况:删除表尾元素,不需要移动其他元素,最好时间复杂度O(1)
- 最坏情况:删除表头元素,需要将后续的 n-1 个元素全都向前移动。循环n-1 次,最坏时间复杂度O(n)
- 平均情况:平均时间复杂度O(n)
2.5.2、总结
2.6、顺序表的查找
2.6.1、静态分配按位查找
GetElem(L,i) :按位查找操作。获取表L中第i个位置的元素的值
typedef struct{
ElemType data[MaxSize];
int length;
}SqList;
ElemType GetElem(SqList L,int i){
return L.data[i-1];
}
2.6.2、动态分配按位查找
#define InitSize 10;
typedef struct{
ElemType *data;
int MaxSize;
int length;
}SeqList;
ElemType GetElem(SqList L,int i){
return L.data[i-1];
}
2.6.3、按位查找时间复杂度
由于顺序表的各个数据元素在内存中连续存放,因此可以根据起始地址和数据元素大小立即找到第i个元素?"随机存取"特性,按位查找的时间复杂度为O(1)
2.6.4、按值查找
LocateElem(L,e) :按值查找操作。在表L中查找值为e的元素
#define InitSize 10;
typedef struct{
ElemType *data;
int MaxSize;
int length;
}SeqList;
int LocateElem(SeqList L,ElemType e){
for(int i=0;i<L.length;i++)
{
if(L.data[i]==e)
{
return i+1;
}
return 0;
}
}
2.6.5、按值查找时间复杂度
关注最深层循环语句的执行次数与问题规模n的关系,问题规模n=L.length(表长)
int LocateElem(SeqList L,ElemType e){
for(int i=0;i<L.length;i++)
{
if(L.data[i]==e)
{
return i+1;
}
return 0;
}
}
- 最好情况:目标元素在表头,只需要循环1次,最好的时间复杂度为O(1)
- 最坏情况:目标元素在表尾,需要循环n次,最坏时间复杂度为O(n)
- 平均情况:O(n)
2.6.6、总结
3、单链表
顺序表:用顺序存储结构实现的线性表叫做顺序表
- 顺序表优点:可随机存取,存储密度高
- 顺序表缺点:要求大片连续空间,改变容量不方便
链表:用链式存储结构实现的线性表叫做链表,链表分为:
单链表优点:不要求大片连续空间,改变容量方便
单链表缺点:不可随机存取,要耗费一定空间存放指针
3.1、单链表的定义
typedef struct LNode{
ElemType data;
struct LNode * next;
}LNode,*LinkList;
LNode *p =(LNode *)malloc(sizeof(LNode));
struct LNode{
ElemType data;
struct LNode *next;
};
typedef struct LNode LNode;
typedef struct LNode * LinkList;
struct LNode *p =(struct LNode *)malloc(sizeof(struct LNode))
要表示一个单链表时,只需声明一个头指针L,指向单链表的第一个结点
LNode *L;
LinkList L;
上述两种声明方式有什么区别呢?
LNode * : 强调这是一个结点LinkList :强调这是一个单链表
3.2、初始化不带头结点的单链表
typedef struct LNode{
ElemType data;
struct LNode * next;
}LNode,*LinkList;
bool InitList(LinkList &L){
L = NULL;
return true;
}
void test(){
LinkList L;
InitList(L);
}
3.3、不带头结点的单链表是否为空
bool Empty(LinkList L){
return (L==NULL);
}
3.4、初始化带头结点的单链表
typedef struct LNode{
ElemType data;
struct LNode * next;
}LNode,*LinkList;
bool InitList(LinkList &L){
L =(LNode *)malloc(sizeof(LNode));
if(L==NULL)
{
return false;
}
L->next = NULL;
return true;
}
void test(){
LinkList L;
InitList(L);
}
3.5、带头结点的单链表是否为空
bool Empty(LinkList L){
if(L->next == NULL){
return true;
}else{
return false;
}
}
3.6、单链表的插入
3.6.1、带头结点按位序插入
ListInsert(&L,i,e) :插入操作。在表L中的第i个位置上插入指定元素e
(找到第i-1个结点,将新结点插入其后)
bool ListInsert(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(1)
- 如果 i = 3(也就是在表中插入元素)
- 如果 i = 5(也就是在表尾插入元素):时间复杂度O(n)
3.6.2、不带头结点按位序插入
ListInsert(&L,i,e) :插入操作。在表L中的第i个位置上插入指定元素e
(找到第i-1个结点,将新结点插入其后)
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;
}
}
- 结论:不带头结点写代码不方便,推荐用带头结点
- 注意:考试中带头、不带头都有可能考察,注意审题
3.7、指定结点的后插操作
后插操作:在结点之后插入元素
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;
}
3.8、指定结点的前插操作
前插操作:在结点之前插入元素
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 InsertPriorNode(LNode *p,LNode *s){
if(p==NULL || s==NULL)
{
return false;
}
s->next = p->next;
p->next = s;
ElemType temp = p->data;
p->data = s->data;
s->data = temp;
return true;
}
3.9、单链表的删除
3.9.1、带头结点按位序删除
ListDelete(&L,i,&e) :删除操作。删除表L中第i个位置的元素,并用e返回删除元素的值(找到第i-1个结点,将其指针指向第i+1个结点,并释放第i个结点)
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;
}
3.9.2、指定结点的删除
bool DeleteNode(LNode *p) :删除结点p
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;
}
3.10、单链表的查找
3.10.1、按位查找
GetElem(L,i) :按位查找操作。获取表L中第i个位置的元素的值。
LNode *GetElem(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;
}
- 如果i=0
- 如果 i = 8(i大于链表长度)
王道书版本:
LNode *GetElem(LinkList L,int i){
int j=1;
LNode *p = L->next;
if(i==0)
{
return L;
}
if(i<1)
{
return NULL;
}
while(p!=NULL && j<i)
{
p = p->next;
j++;
}
return p;
}
3.10.2、按值查找
LocateElem(L,e) :按值查找操作。在表L中查找具有给定关键字值的元素
LNode *LocateElem(LinkList L,ElemType e){
LNode *p = L->next;
while(p != NULL && p->data != e)
{
p = p->next;
}
return p;
}
3.10.3、求单链表长度
int Length(LinkList L)
{
int len = 0;
LNode *p = L;
while(p->next !=NULL)
{
p = p->next;
len++;
}
return len;
}
3.10.4、总结
3.11、单链表的建立
如果给你很多个数据元素,要把它们存到一个单链表里边,咋弄呢?
- 初始化一个单链表
- 每次取一个数据元素,插入到表尾/表头
3.11.1、尾插法建立单链表
LinkList List_TailInsert(LinkList &L)
{
int x;
L = (LinkList)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;
}
尾插法的时间复杂度:O(n)
3.11.2、头插法
头插法:对头结点进行后插操作
LinkList List_HeadInsert(LinkList &L){
LNode *s;
int x;
L = (LinkList)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);
}
return L;
}
3.11.3、总结
4、双链表
4.1、双链表的定义
typedef struct DNode{
ElemType data;
struct DNode *prior,*next;
}DNode,*DLinklist;
4.2、双链表的初始化
bool InitDLinkList(DLinklist &L){
L = (DNode *)malloc(sizeof(DNode));
if(L==NULL)
{
return false;
}
L->prior = NULL;
L->next = NULL;
return true;
}
viod testDLinkList(){
DLinklist L;
InitDLinkList(L);
}
DLinklist 等价于DNode * DLinklist 强调这是一个链表DNode * 强调这是一个结点
4.3、判断双链表是否为空
判断双链表是否为空:判断头结点的下一个是否为空
bool Empty(DLinklist L){
if(L->next == NULL)
{
return true;
}else{
return false;
}
}
4.4、双链表的插入
4.4.1、后插操作
如果p结点是最后一个结点(特殊情况):
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;
}
如果p是中间一个结点:
4.4.2、按位序插入
按位序插入:只需从头结点开始,找到某一个位序的前驱结点,然后对这个前驱结点执行后插操作
4.4.3、前插操作
前插操作:只需找到此结点的前驱结点,然后对其前驱结点进行后插操作,即为前插操作
4.5、双链表的删除
当q结点不是最后一个结点时:
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;
}
当q结点是最后一个结点时:
4.6、双链表的销毁
void DestoryList(DLinklist &L){
while(L->next !=NULL)
{
DeleteNextDNode(L);
}
free(L);
L=NULL;
}
4.7、双链表的遍历
后向遍历:
while(p != NULL){
p = p->next;
}
前向遍历:
while(p != NULL){
p = p->prior;
}
前向遍历(跳过头结点):
while(p->prior!=NULL){
p = p->prior;
}
4.8、总结
5、循环链表
5.1、循环单链表
头结点next指针指向头结点:
5.1.1、初始化循环单链表
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode,*LinkList;
bool InitList(LinkList &L){
L = (LNode *)malloc(sizeof(LNode));
if(L == NULL)
{
return false;
}
L->next = L;
return true;
}
5.1.2、判断循环单链表是否为空
判断循环单链表是否为空:判断头结点的next指针是否指向它自己
bool Empty(LinkList L){
if(L->next == L)
{
return true;
}else{
return false;
}
}
5.1.3、判断循环单链表表尾结点
bool isTail(LinkList L,LNode *p){
if(p->next == L)
{
return true;
}else{
return false;
}
}
5.2、循环双链表
- 双链表:表头结点的prior指向NULL,表尾结点的next指向NULL
- 循环双链表:表头结点的prior指向表尾结点,表尾结点的next指向头结点
5.2.1、初始化循环双链表
初始化循环双链表:让头结点的prior和next指针都指向头结点
bool InitDLinkList(DLinklist &L){
L = (DNode *)malloc(sizeof(DNode));
if(L == NULL)
{
return false;
}
L->prior = L;
L->next = L;
return true;
}
void testDLinkList(){
DLinklist L;
InitDLinkList(L);
}
5.2.2、判断循环双链表是否为空
判断循环双链表是否为空:头结点的next指针是否指向了它自身
bool Empty(DLinkList L){
if(L->next == L)
{
return true;
}else{
return false;
}
}
5.2.3、判断循环双链表表尾结点
判断循环双链表表尾结点:这个结点的next指针是否指向了头结点
bool isTail(DLinkList L,DNode *p){
if(p->next == L)
{
return true;
}else{
return false;
}
}
5.3、循环双链表的插入
bool InsertNextDNode(DNode *p,DNode *s){
s->next = p->next;
p->next->prior = s;
s->prior = p;
p->next = s;
}
5.4、循环双链表的删除
p->next = q->next;
q->next->prior = p;
free(q);
5.5、总结
6、静态链表
单链表:各个结点在内存中星罗棋布、散落天涯。
静态链表:分配一整片连续的内存空间,各个结点集中安置。静态链表中的每个结点包含数据元素和下一个结点的数组下标。
我们看上图:
- 静态链表中数组下标为 0 的结点充当头结点的角色,而头结点的下一个结点是存储在数组下标为 2 的位置,数组下标为 2 的位置结点数据元素为 e1,下一个结点为数组下标为 1 的结点,也就是数据元素为 e2 的结点。
- 单链表中表尾指针是指向NULL,静态链表中游标为 -1 表示已经到达表尾。
- 假如每个数据元素 4B,每个游标 4B(每个结点共8B),设起始地址为 addr,则 e1 的存放地址为 addr + 8 * 2
6.1、代码定义静态链表
#define MaxSize 10
typedef struct{
ElemType data;
int next;
}SLinkList[MaxSize];
上述代码其实等价于下面代码:
#define MaxSize 10
struct Node{
ElemType data;
int next;
};
typedef struct Node SLinkList[MaxSize];
SlinkList a;
7、第一章总结
- 顺序表和链表都是线性结构,都属于线性表
线性表 | 优点 | 缺点 |
---|
顺序表(顺序存储) | 支持随机存取、存储密度高 | 大片连续空间分配不方便,改变容量不方便 | 链表(链式存储) | 离散的小空间分配方便,改变容量方便 | 不可随机存取,存储密度低 |
- 创建
- 销毁
- 增删
- 查找
-
表长难以预估、经常要增加/删除元素——链表 -
表长可预估、查询(搜索)操作较多——顺序表
|