线性表
线性表
1.顺序存储——顺序表
2.链式存储
2.1 单链表
2.2 双链表
2.3 循环链表
2.4 静态链表——借助数组实现
1.线性表
1.概念
1.具有相同数据类型的n个数据元素的有限序列,其中n为表长,当n = 0时线性表是一个空表。
2.特点:
1)每个数据元素所占空间一样大
2)有次序
3)除第一个元素外,每个元素有且仅有一个直接前驱;除了最后一个元素外,每个元素有且仅有一个直接后继
4)位序:线性表中的第i个元素,其中i即为位序,位序从1开始。
2.基本操作
//初始化,构造空的线性表,分配内存空间 InitList(&L); //销毁操作,释放内存空间 DestoryLit(&L); //插入删除 ListInsert(&L,i,e); ListDelete(&L,i,&e); //按值查找 LocateElem(L,e); //按位序查找 GetElem(L,i); //表长、打印链表、判空 Length(L); PrintList(L); Empty(L);
1.注意:对数的操作,——创销,增删改查
2.为什么要实现基本操作?
团队合作编程,封装思想,方便后续使用。
2.顺序表
1.概念与特点
1、顺序表——用顺序存储的方式实现线性表顺序存储。把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。
2、特点
1)随机访问
2)存储密度高
3)拓展容量不方便
4)插入删除元素不方便
2.实现方式——静态分配
#define MaxSize 10 typedef struct{ ElemType data[MaxSize]; int lenght; }SeqList;
1、在分配内存空间的时候,一定要进行初始化,否则内存中遗留的脏数据会导致一些很隐蔽的bug;
2、静态分配方式:定义一个静态数组存放顺序表的数据;
3、缺点:顺序表的大小固定,分配后无法更改;如果一开始就声明一个很大的内存空间,在使用过程中,仅仅用到了很小的一部分,则内存的利用率很低
3.实现方式——动态分配
#define InitSize 100 typedef struct{ ElemType *data; //指向动态分配数组的指针 int lenght; int capacity; //顺序表的最大容量 }SeqList; //初始化 void InitList(&L){ L.data = (ElemType*)malloc(sizeof(ElemType)*InitSize); L.length = 0; L.capacity = InitSize; } //增加动态数组的长度 void IncreaseSize(SeqList &L, int len){ int *p = L.data; L.data = (int *)malloc(L.capacity + len)*sizeof(int)); for(int i = 0; i < L.length; ++i){ L.data[i] = p[i]; } L.capacity = L.capacity + len; free(p); }
4.插入操作
1、复杂度
时间复杂度:最好O(1);最坏O(n);平均O(n/2)
//在L的位序i出插入元素e bool ListInsert(SeqList& L, int i , int e){ if(i < 1 || i > L.length + 1){ assert("插入位置非法"); return false; } if(i >= L.capacity){ assert("存储空间满,不能插入"); return false; } L.length++; //前面的判断已经说明插入位置合法 for(int j = L.length - 1; j >= i - 1; --j){ L.data[j + 1] = L.data[j]; } L.data[i - 1] = e; return true; }
2、注意:
1)L.length 除了表示顺序表的长度外,另外还表示顺序表中的最后一个元素的索引,不能是位序;
2)L.legth++写在for循环前,还是后,均正确;
如果写在for后面,如下当length = 0时,不会进入循环,直接执行L.data[i - 1] = e 和 L.length++;
L.data[i - 1] = e; L.length++;
如果写在for前面,代码则会更加直观,易于阅读,个人感觉
5.删除操作
1.复杂度
最好情况O(1);最坏情况O(n);平均情况O((n - 1)/2)
bool ListDelete(SeqList &L, int i, int &e){ if(i < 1 || i > L.length){ assert("删除位置非法") return false; } e = L.data[i - 1]; //向前移动 for(int j = i; j < L.length; ++j){ L.data[j - 1] = L.data[j]; } L.lenght--; return true; }
注意要删除的是第i个元素,还是数组下标为i的元素
6.查找操作
1.按位查找——GetElem
O(1)
//i表示位序,查找第i个位置的元素,通过e返回 bool GetElem(SeqList L, int i, int& e){ if(i < 1 || i > L.length){ assert("访问位置非法"); return false; } e = L.data[i - 1]; return true; }
2.按值查找——LocateElem
复杂度:最好O(1);最坏O(n);平均O(n/2)
//查找顺序表中第一个元素值等于val的元素,并返回其位序 bool LocateElem(SeqList L, int val, int index){ for(int i = 0; i < L.length; i++){ if(L.data[i] == val){ index = i + 1; return true; } } index = -1; return false; }
注意:此查找操作,只能用于查找基本类型,不能用于查找结构体类型,C++可以通过重载运算符来判断结构类型是否相等。
3.单链表——链表
1.概念和特点
1.定义:每个结点除了存放数据元素外,还要存储指向下一个结点的指针。
2.优点:不要求大片连续空间,改变容量方便
3.缺点:不可随机存取,要耗费一定空间存放指针
2.代码定义单链表:
typedef struct LNode{ ElemType data; struct LNode *next; }LNode,*LinkList;
3.不带头结点
//初始化 bool InitList(LinkList &L){ L = NULL; return true; } //判断单链表是否为空 bool Empty(LinkList L){ if(L == NULL){ return true; } else{ return false; } }
4.带头结点
//初始化 bool InitList(LinkList &L){ L = (LNode*)malloc(sizeof(LNode)); if(L == NULL) return false; L->next = NULL; //头结点之后暂时没有节点 return true; } //判断带头结点的单链表是否为空 bool Empty(LinkList L){ if(L->next == NULL) return true; else return false; }
注意:带头结点的代码写代码更方便
5.单链表的插入
1.按位序插入节点
最好O(1);最坏o(n);
//带头结点,在第i个位置插入元素e //此处的L指向头结点 bool ListInsert(LinkList &L, int i, ElemType e){ if(i < 1){ assert("插入位序非法"); return false; } LNode *p; //找到要插入位置的前一个节点的位置 int j = 0; p = L; //L指向头结点,头结点是第0个节点 while(p != NULL && j < i - 1){ p = p->next; j++; } if(p == NULL){ //第i-1 个节点不存在,插入位置非法 assert("插入位序非法"); return false; } LNode* s = (LNode*)malloc(sizeof(LNode)); s->data = e; s->next = p->next; p->next = s; return true; }
//不带头结点;在第i个位置插入元素e //区别在于带头结点的对于第1个位置的插入需要特殊讨论 //此时的L表示第一个节点,已经不是头结点了,代码需要注意 bool ListInsert(LinkList &L, int i, ElemType e){ if(i < 1){ assert("插入位序非法"); return false; } //注意,此时的L表示第一个节点,已经不是头结点了 if(i == 1){ LNode* s = (LNode*)malloc(sizeof(LNode)); s->data = e; s->next = L; L = s; return true; } LNode *p; //找到要插入位置的前一个节点的位置 int j = 1; //0处需要特殊讨论,此处从1开始 p = L; while(p != NULL && j < i - 1){ p = p->next; j++; } if(p == NULL){ //第i-1 个节点不存在,插入位置非法 assert("插入位序非法"); return false; } LNode* s = (LNode*)malloc(sizeof(LNode)); s->data = e; s->next = p->next; p->next = s; return true; }
2.指定节点的后插操作
//在p节点后插入元素e 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.指定节点的前插操作
//在p节点之前插入元素e bool InsertPriorNode(LNode *p, ElemType e){ if(p == NULL){ return false; } LNode *s = (LNode*)malloc(sizeof(LNode)); s->data = e; //先后插 s->next = p->next; p->next = s; //再交换数据 swap(s->data, p->data); return true; }
6.单链表的删除
此处指讨论带头结点的(不带头结点的链表删除时需要考虑第0个节点)
1.删除第i个位置的节点
O(n)
//删除第i个位置的节点 bool ListDelete(LinkList &L, int i, ElemType &e){ if(i < 1){ assert("删除位置非法"); return false; } LNode *p; //p指向要删除位置的前一个节点,第i - 1个节点 int j = 0; p = L; //L指向头结点 while(p != NULL && j < i - 1){ p = p->next; j++; } //要删除位置的前一个节点为空,删除位置非法 if(p == NULL){ return false; } //要删除位置的节点不存在,i - 1节点不为空 if(p->next == NULL){ assert("第 i - 1个节点之后已无其他节点") return false; } //删除第i个节点 LNode *q = p->next; e = q->data; p->next = q->next; free(q); return true; }
2.删除指定节点
//删除指定节点p bool DeleteNode(LNode* L,LNode *p){ if(p == NULL){ return false; } LNode *q = p->next; //如果是最后一个节点,则会报错 if(q == NULL){ q = L->next; while(q != NULL && q->next != p){ q = q->next; } if(q == NULL){ assert("删除非法,未找到p"); return false; } q->next = NULL; free(p); return true; } //p不是最后一个节点 p->data = p->next->data; p->next = q->next; free(q); return true;; }
7.单链表的查找
O(n)
1.按位查找
//按位查找,查找第i个节点,带头结点 LNode * GetElem(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; }
实现查找后,在第i个位置插入元素e可以简化为:
//带头结点,在第i个位置插入元素e //此处的L指向头结点 bool ListInsert(LinkList &L, int i, ElemType e){ if(i < 1){ return false; } //先查找第i - 1个结点 LNode* p = GetElem(L, i - 1); if(p == NULL){ assert("插入位置非法"); return false; } //在第p个节点后插 if(!InsertNextNode(p, e)){ assert("插入失败"); return false; } return true; }
同样的道理,可以简化删除第i个节点,先找到第i-1个节点,在进行删除
//删除第i个位置的节点 bool ListDelete(LinkList &L, int i, ElemType &e){ if(i < 1){ assert("删除位置非法"); return false; } LNode *p = GetElem(L, i - 1); //要删除位置的前一个节点为空,删除位置非法 if(p == NULL){ return false; } //要删除位置的节点不存在,i - 1节点不为空 if(p->next == NULL){ assert("第 i - 1个节点之后已无其他节点") return false; } //删除第i个节点 LNode *q = p->next; e = q->data; p->next = q->next; free(q); return true; }
2.按值查找
//按值查找,找到数据==e的节点 LNode* LocateElem(LinkList L, ElemType e){ LNode *p = L->next; while(p != NULL && p->data != e){ p = p->next; } //找到返回p,找不到返回NULL return p; }
注意,查找只能查找基本类型,对于结构类型,需要重载运算符
8.求表长
O(n)
int Length(LinkList L){ int len = 0; LNode *p = L; while(p->next != NULL){ p = p->next; len++; } return len; }
9.尾插法——建立单链表
O(1)
//尾插元素过多时,每次插入都需要遍历列表,因此可以设置一个尾指针 LinkList L; LinkList tail; InitList(L); //初始化 tail = L; //带头结点 bool List_TailInsert(LinkList &tail, ElemType e){ LNode * p = (LNode*)malloc(sizeof(LNode)); if(p == NULL){ return false; } p->data = e; p->next = NULL; tail->next = p; tail = p; return true; }
10.头插法——建立单链表
//头插法,带头结点 bool List_HeadInsert(LinkList &L, ElemType e){ LNode* s = (LNode*)malloc(sizeof(LNode)); if(s == NULL){ return false; } s->data = e; s->next = L->next; L->next = s; return true; }
4.双链表——链表
1.概念和特点
1.单链表缺点是无法进行逆检索,有时候不太方便
2.双链表,可进可退,存储密度更低一丢丢
2.代码定义双链表
typedef struct DNode{ ElemType data; struct DNode *prior,*next; }DNode,*DLinkList;
3.初始化双链表
//带头结点的双链表 bool InitDLinkList(DLinkList &L){ L = (DNode*)malloc(sizeof(DNode)); if(L == NULL) return false; L->prior = NULL; L->next = NULL; return true; }
4.双链表的插入
//在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; }
5.双链表的删除
//删除p节点的后继节点 bool DeleteNextDNode(DNode *p){ if(p == NULL){ return false; } DNode *q = p->next; if(q == NULL){ return false; //p无后继节点 } p->next = q->next; if(q->next != NULL){ q->next->prior = p; } free(q); return true; }
6.销毁双链表
//销毁双链表 void Destory(DLinkList &L){ //循环释放各个节点 while(L->next != NULL){ DeleteNextDNode(L); } free(L); //释放头结点 L = NULL; }
7.双链表的遍历
void Print(DLinkList p){ while(p != NULL){ p = p->next; std::cout << p->data << std::endl; } }
5.循环单链表——链表
从一个节点出发,可以找到其他任何一个节点
注意:如果一个链表的应用场景,是需要经常在表头和表尾进行操作,可以将L设置为指向表尾元素,因此在尾插元素时要移动L,时间复杂度为O(1)
1.定义
typedef struct LNode{ ElemType data; struct LNode *next; }LNode, *LinkList;
2.初始化
bool InitList(LinkList &L){ L = (LNode*)malloc(sizeof(LNode)); if(L == NULL){ return false; } L->next = L; //头结点的next指向头结点 return true; }
3.判空
bool Empty(LinkList L){ if(L->next == L){ return true; } return false; }
4.判断是否为尾节点
//判断节点p是否为循环单链表的表尾节点 bool isTail(LinkList L, LNode *p){ if(p->next == L){ return true; } return false; }
6.循环双链表
1.定义
表头节点的prior指向表尾节点
表尾节点的next指向表头节点
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; L->next = L; return true; } //判空 bool Empty(DLinkList L){ if(L->next == L) return true; return false; } //判断节点是否为循环链表的表尾节点 bool isTail(DLinkList L, DNode *p){ if(p->next == L){ return true; } return false; }
2.循环双链表的插入&删除
//在p节点后面插入s节点 bool InsertNextDNode(DNode *p, DNode *s){ if(p == NULL || s == NULL){ return false; } s->next = p->next; p->next->prior = s; s->prior = p; p->next = s; return true; } //删除p的后继节点 bool DeleteNextDNode(DNode *p){ if(p == NULL || p->next == NULL){ return false; } DNode *q = p->next; p->next = q->next; q->next->prior = p; //循环双链表此处不需要判断 free(q); return true; }
7.静态链表
1.概念
1.单链表,各个节点在内存中随机存储
2.静态链表:分配一整片连续的内存空间,各个节点集中安置
如下图
2.定义
#define MaxSize 10 typedef struct { ElemType data; int next; }SLinkList[MaxSize]; //SLinkList a; ===>a是一个数组,每个数组元素是一个struct
3.相关操作
文件分配表实质上是一个静态链表,因为文件分配表的大小和磁盘的容量相关,容量定了之后,文件分配表的大小也确定;
8.总结