文章目录
02. 线性表
2.1 线性表的定义和基本操作
定义
-
线性表是具有相同数据类型的 n ( n ≥ 0 ) n(n≥0) n(n≥0) 个数据元素的有限序列,其中 n n n 为表长,当 n = 0 n = 0 n=0 时线性表是一个空表。若用 L L L 命名线性表,则其一般表示为
L = ( a 1 , a 2 , … , a i , a i + 1 , … , a n ) L = (a_1,a_2,…,a_i,a_{i+1},…,a_n) L=(a1,a2,…,ai,ai+1,…,an)-
a
i
a_i
ai 是线性表中的“第
i
i
i 个”元素线性表中的位序
a 1 a_1 a1 是表头元素; a n a_n an 是表尾元素。 - 除第一个元素外,每个元素有且仅有一个直接前驱;除最后一个元素外,每个元素有且仅有一个直接后继
-
a
i
a_i
ai 是线性表中的“第
i
i
i 个”元素线性表中的位序
2.2 顺序表
2.2_1 顺序表的定义
-
顺序表:用顺序存储的方式实现线性表
-
特点:
- (最主要的特点)随机访问,即可以在 O ( 1 ) O(1) O(1) 时间内找到第 i i i 个元素。
- 存储密度高,每个节点只存储数据元素
- 拓展容量不方便(即便采用动态分配的方式实现,拓展长度的时间复杂度也比较高)
- 插入、删除操作不方便,需要移动大量元素
-
静态分配
#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; }
-
动态分配
#define InitSize 10 typedef struct{ int *data; int MaxSize; int length; }SeqList; // 初始化 void InitList(SeqList &L){ L.data=(int *)malloc(InitSize*sizeof(int)); L.length=0; L.MaxSize=InitSize; } // 动态增加长度 void IncreaseSize(SeqList &L, int len){ int *p=L.data; L.data= (int *)malloc((L.Maxsize+len)*sizeof(int)); for(int i=0;i<L.length;i++) L.data[i]=p[i]; L.MaxSize+=len; free(p); }
2.2.2_1 顺序表的增删查
#include<stdio.h>
#define MaxSize 10 //定义最大长度
typedef struct{
int data[MaxSize]; //用静态的“数组”存放数据元素
int length; //顺序表的当前长度
}SqList; //顺序表的类型定义(静态分配方式)
// 初始化
void ListInit(SqList &L){
for(int i=0; i<MaxSize; i++)
L.data[i] = 0;
L.length = 0;
}
// 创建顺序表
void ListSet(SqList &L,int *arr,int len){
L.length = len;
for(int i=0;i<len;i++){
L.data[i] = arr[i];
}
}
// 输出
void ListDisplay(SqList &L){
for(int i=0;i<L.length;i++)
printf("%d ",L.data[i]);
printf("\n");
}
// 插入:在L的位序i处插入元素e
// 时间复杂度:O(n)
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;
}
// 删除:删除L中位序为i的元素e
// 时间复杂度:O(n)
bool ListDelete(SqList &L, int i,int &e){
if(i < 1||i > L.length) //处理异常情况
return false;
e = L.data[i-1];
for(int j=i-1; j<L.length-1; j++){
L.data[j]=L.data[j+1];
}
L.length--;
return true;
}
// 按位查找
// 时间复杂度:O(1)
bool GetElem(SqList L,int i,int &e){
if(i < 1||i > L.length) //处理异常情况
return false;
e = L.data[i-1];
return true;
}
// 按值查找:查找第一个元素值等于e的元素,位序赋值给i
// 时间复杂度:O(n)
bool SelectElem(SqList L,int &i, int e){
for(int j=0;i<L.length;i++){
if(L.data[i] == e){
i = j;
return true;
}
}
return false;
}
2.3 链表
2.3_1 单链表的定义
-
顺序表与单链表的对比
-
顺序表
- 优点:可随机存取,存储密度高
- 缺点:要求大片连续空间,改变容量不方便
-
单链表
- 优点:不要求大片连续空间,改变容量方便
- 缺点:不可随机存取,要耗费一定空间存放指针
-
// 定义单链表结类型
typedef struct LNode{
ElemType data; // 数据域
struct LNode *next; // 指针域
}LNode, *LinkList;
//等价于
typedef struct LNode LNode;
typedef struct LNode *LinkList;
LNode *L; // 声明一个指向单链表第一个结点的指针
LinkList L; // 声明一个指向单链表第一个结点的指针
//增加一个新的结点:在内存中申请一个结点所需空间,并用指针 p 指向这个结点
struct LNode * p = (struct LNode *) malloc(sizeof(struct LNode));
- 不带头结点的单链表
bool InitList(LinkList &L){
L = NULL;
return true;
}
- 带头结点的单链表
bool InitList(LinkList &L){
L = (LNode *)malloc(sizeof(LNode));
if(L == NULL)
return false;
L->next = NULL;
return true;
}
2.3_2 单链表的增删查 建立
查找
- 带头结点 - 按位查找
LNode * GetElem(LinkList L,int i){
if(i < 0)
return NULL;
LNode *p = L;
int j = 0;
while(p != NULL && j < i){
p = p->next;
j++;
}
return p;
}
- 按值查找 - 带头结点
LNode * SelectElem(LinkList L,ElemType e){
LNode *p = L->Next;
while(p != NULL && p->data != e)
p = p->next;
return p;
}
- 求表长 - 带头结点
int length(LinkList L){
int len = 0;
LNode *p = L;
while(p->next != NULL){
p = p->next;
len++;
}
return len;
}
插入
- 带头结点 - 按位序插入
// 在第 i 个位置上插入元素e
// 时间复杂度:O(n)
bool ListInsert(LinkList &L,int i,ElemType e){
if(i < 1) // 处理不合法数据
return false;
LNode *p; // 指针p指向当前扫描到的结点
int j = 0; // 当前指针指向的是第几个结点
p = L; // L指向头结点
// p = GetElem(L,i-1);
while(p != NULL && j < i-1){ // 找到第i-1个结点
p = p->next;
j++;
}
if(p == NULL) // i值不合法
return false;
LNode *s = (LNode *)malloc(sizeof(LNode));
s->data = e;
s->next = p->next; //【注意】最后两句顺序不可颠倒
p->next = s;
return true;
}
- 不带头结点 - 按位序插入
// 在第 i 个位置上插入元素e
// 时间复杂度:O(n)
bool ListInsert(LinkList &L,int i,ElemType e){
if(i < 1) // 处理不合法数据
return false;
if(i == 1){ // 【注意】需要额外处理插入第1个结点的情况
LNode *s = (LNode *)malloc(sizeof(LNode));
s->data = e;
s->next = L;
L = s; // 头指针指向新结点
return true;
}
LNode *p; // 指针p指向当前扫描到的结点
int j = 1; // 当前指针指向的是第几个结点
p = L; // L指向头结点
// p = GetElem(L,i-1);
while(p != NULL && j < i-1){ // 找到第i-1个结点
p = p->next;
j++;
}
// InserNextNode(p,e);
if(p == NULL) // i值不合法
return false;
LNode *s = (LNode *)malloc(sizeof(LNode));
s->data = e;
s->next = p->next; //【注意】最后两句顺序不可颠倒
p->next = s;
return true;
}
- 指定结点的后插操作
// 后插操作:在p结点之后插入元素e
// 时间复杂度:O(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;
}
- 指定结点的前插操作
// 前插操作:在p结点之前插入元素e
bool InserPriorNode(LNode *p,Elemtype e){
if(p == NULL)
return false;
LNode *s = (LNode *)malloc(sizeof(LNode));
if(s == NULL)
reutrn false;
s->next = p->next; // 新结点s连接到p结点后
p->next = s;
s->data = p->data; // p->data数据赋值给s
p->data = e; // e赋值给p->data
}
删除
- 带头结点-按位序删除
// 删除第i个位置的元素,e返回删除的元素值
// 时间复杂度:O(n)
bool ListDelete(LinkList &L,int i,ElemType &e){
if(i < 1) // 处理不合法数据
return false;
LNode *p = L;
int i=0;
// GetElem(L,i-1);
while(p != NULL && j < i-1){ // 找到第i-1个结点
p = p->next;
j++;
}
if(p == NULL) // 值不合法
return fasle;
if(p->next == NULL) // 第i-1个结点后已无其他结点
return fasle;
e = p->next->data;
p->next = p->next->next;
return true;
}
-
指定结点删除:需要修改其前驱结点的next指针
-
法一:传入头指针,循环寻找p的前驱结点
-
法二:偷天换日(把p下一个结点的数据域赋值给p的数据域,删除p的下一个结点)
❗❗如果p是最后一个结点,则只能从头开始找到p的前驱
-
// 删除指定结点p
// 时间复杂度:O(1)
bool DeleteNode(LNode *p){
if(p == NULL)
return false;
LNode *q = p->next;
p->data = q->data;
p->next = q->next;
free(q);
return true;
}
单链表的建立
尾插法
- 带头结点
LinkList List_TailInsert(LinkList &L){
int x;
L = (LinkList)malloc(sizeof(LNode));
LNode *s,*p = L; // p指向当前已插入的最后一个结点
scanf("%d",&x);
while(x!=9999){
s = (LNode *)malloc(sizeof(LNode));
s->data = x;
p->next = s;
p = s; // p指向最后一个结点
scanf("%d",&x);
}
p->next = NULL;
return L;
}
头插法
- 带头结点
LinkList List_HeadInsert(LinkList &L){
int x;
L = (LinkList)malloc(sizeof(LNode));
L->next = NULL; // 【好习惯】只要是初始化单链表,就将头指针指向 NULL
LNode *s;
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;
}
- 不带头结点
void NoNode_HeadInsert(LinkList *L) {
LNode *s;//要插入的节点
int x;//要插入的元素
scanf("%d", &x);
while (x != 9999) {
s = (LNode *)malloc(1, sizeof(LNode));
s->data = x;
s->next = NULL;
if (NULL == *L) {
*L = s; //将新结点置位首节点也是尾结点
} else {
s->next = *L;
*L = s; //新结点置位尾结点
}
scanf("%d", &x);
}
}
- 【重要应用】头插法实现链表逆置
LinkList Reverse(LinkList &L){
LNode *p,*q;
p = L->next; // p指向当前准备插入的结点
L->next = NULL; // 重新构造单链表
while(p != NULL){
q = p;
p = p->next;
q->next = L->next; // 头插法
L->next = q;
}
return L;
}
2.3_3 双链表
typedef struct DNode{
ElemType data;
struct DNode *prior, *next;
}DNode, *DLinkList;
// 初始化双链表
bool InitDLinkList(DlinkList){
L = (DNode *)malloc(sizeof(DNode));
if(L == NULL)
return false;
L->prior = NULL;
L->next = NULL;
return true;
}
- 插入
// 在p结点之后插入s结点
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结点的后继结点
// 时间复杂度:O(1)
bool DeleteNextDNode(DNode *p){
if(p == NULL )
return false;
DNode *q = p->next; // q是p的后继结点
if(q == NULL) // p的后继结点存在
return false;
p->next = q->next; // 改变p的后继结点
if(q->next != NULL) // 如果q的后继结点存在
q->next->prior = p; // 改变q的后继结点的前驱结点
free(q);
return true;
}
// 销毁表
// 时间复杂度:O(1)
void DestoryList(DLinkList &L){
while(L->next != NULL)
DeleteNextDNode(L);
free(L); // 释放头结点
L = NULL; // 头指针指向NULL
}
- 双链表的遍历
2.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 = L; // 头结点next指向头结点
return true;
}
// 判空
bool Empty(LinkList L){
if(L->next == L) return true;
else return false;
}
// 判断结点p是否为循环单链表的表尾结点
bool isTail(LinkList L,LNode *p){
if(p->next == L) return true;
else return false;
}
- 单链表:从一个结点出发只能找到后续的各个结点
- 循环单链表:从一个结点出发可以找到其他任何一个结点
- 从头结点找到尾部,时间复杂度为 O ( n ) O(n) O(n)
- 从尾部找到头部,时间复杂度为O(1)
循环双链表
typedef struct DNode{
ElemType data;
struct DNode *prior, *next;
}DNode, *DLinkList;
// 初始化
bool InitDLinkList(DLinkList &L){
L = (DNode *)malloc(sizeof(DNode));
if(L == NULL)
return false;
L->prior = L; // 头结点 prior 指向头结点
L->next = L; // 头结点的 next 指向头结点
return true;
}
// 判空
bool Empty(DLinkList L){
if(L->next == L) return true;
else return false;
}
- 插入删除
// 在p结点之后插入s结点
s->next = p->next;
p->next->prior = s;
p->next = s;
s->prior = p;
// 删除p的后继结点q
p->next = q->next;
q->next->prior = p;
free(q);
2.3_5 静态链表
2.3_6 顺序表和链表的比较
-
问题:请描述顺序表和链表的bla bla bla…实现线性表时,用顺序表还是链表好?
- 顺序表和链表的逻辑结构都是线性结构,都属于线性表。
- 但是二者的存储结构不同,顺序表采用顺序存储…(特点,带来的优点缺点);链表采用链式存储…(特点、导致的优缺点)。
- 由于采用不同的存储方式实现,因此基本操作的实现效率也不同。当初始化时…;当插入一个数据元素时…;当删除一个数据元素时…;当查找一个数据元素时…
-
顺序表 链表 弹性(可扩容) 😭 😃 增、删 😭 😀 查 😀 😭 -
表长难以预估、经常要增加/删除元素——链表
-
表长可预估,查询(搜索)操作较多——顺序表