线性表
- 线性表是具有相同数据类型的n(n≥0)个数据元素的有限序列,其中n为表长,当n = 0时线性表是一个空表。若用L命名线性表,则其一般表示为L = (a1, a2, … , ai, ai+1, … , an)
- 几个概念:
- ai是线性表中的“第i个”元素线性表中的位序,a1是表头元素;an是表尾元素。
- 除第一个元素外,每个元素有且仅有一个直接前驱;除最后一个元素外,每个元素有且仅 有一个直接后继。
- 线性表的基本操作
- InitList(&L): 初始化表。构造一个空的线性表L,分配内存空间。
- DestroyList(&L): 销毁操作。销毁线性表,并释放线性表L所占用的内存空间。
- ListInsert(&L,i,e): 插入操作。在表L中的第i个位置上插入指定元素e。
- ListDelete(&L,i,&e): 删除操作。删除表L中第i个位置的元素,并用e返回删除元素的值。
- LocateElem(L,e): 按值查找操作。在表L中查找具有给定关键字值的元素。
- GetElem(L,i): 按位查找操作。获取表L中第i个位置的元素的值。
- 线性表的其他操作
- Length(L): 求表长。返回线性表L的长度,即L中数据元素的个数。
- PrintList(L): 输出操作。按前后顺序输出线性表L的所有元素值。
- Empty(L): 判空操作。若L为空表,则返回true,否则返回false。
- 注意: 线性表是一种逻辑结构,表示元素之间一对一的相邻关系。顺序表和链表是指存储结构,两者属于不同层面的概念,因此不要将其混淆。
顺序表
用顺序存储的方式实现线性表顺序存储,把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关 系由存储单元的邻接关系来体现
实现方法一(静态分配)
存储数据的顺序表的下标从0开始,L.data[0]中有数据。
头结点不是链表的第一个节点:数据结构中,在单链表的第一个结点之前附设一个结点,它没有直接前驱,称之为头结点。
#include<iostream>
#include<stdio.h>
#include<stdlib.h>
using namespace std;
# define maxSize 20 //最大存储空间
typedef int elemType;
typedef bool status;
typedef struct {
elemType data[maxSize];
elemType length;
}SqList;//定义线性表
//初始化
void InitList(SqList& L) {
for (int i = 0; i < maxSize; i++) {
L.data[i] = 0;
}//此代码可以不写,推荐不写
L.length = 0;
}
//在i的位置插入e
status Listinsert(SqList & L, int i, elemType e) {
if (i<0 || i>L.length) {
cout << "i不合法" << endl;
return false;
}
if (L.length == maxSize) {
cout << "表瞒了" << endl;
return false;
}
if (L.length== 0) {
L.data[0] = e;
L.length++;
cout << "插入成功" << endl;
return true;
}
else {
for (int j = L.length ; j >= i; j--)
{
L.data[j] = L.data[j-1];
}
L.data[i - 1] = e;
L.length++;
cout << "插入成功" << endl;
return true;
}
}
//删除第i个元素
status ListDelete(SqList& L, int i) {
if (i<1 || i>L.length) {
return false;
}
for (int j = i; j < L.length; j++) {
L.data[j-1] = L.data[j];
}
L.length--;
return true;
}
//返回第i个位置的值
status GetElem(SqList L, int i, elemType& e) {
if (i<1 || i>L.length) {
return false;
}
e = L.data[i - 1];
return true;
}
//返回和e相同的值得位置
int LocateElem(SqList L, elemType e) {
for (int i = 0; i < L.length; i++) {
if (L.data[i] == e) {
return i+1;//查找成功返回位置
}
}
return 0;//查找失败返回0
}
//循环遍历
void TraverseList(SqList L) {
for (int i = 0; i < L.length; i++) {
cout << L.data[i] << " ";
}cout << endl;
cout << "线性表长度为:" << L.length;
}
int main() {
SqList L;
int n;
InitList(L);
cout << "输入插入的数的个数" << endl;
cin >> n;
int x;
for (int i = 0; i <n; i++)
{
int x;
cin >> x;
Listinsert(L, i, x);
}
cout << "结果为" << endl;
TraverseList(L);
cout << "请输入删除第几个元素"<<endl;
int m;
cin >> m;
ListDelete(L, m);
TraverseList(L);
cout << "请输入获取的元素的值" << endl;
int elem, locate=-1;
cin >> elem;
locate=LocateElem(L, elem);
cout << "其位置为:" << locate ;
cout << "请输入获取那个位置的元素" << endl;
int index;
cin >> index;
GetElem(L, index, elem);
cout << "其值为:" << elem << endl;
TraverseList(L);
return 0;
}
实现方法二(动态分配)
动态分配与静态分配唯一区别是数据结构的不同以及动态分配在内存不够时再次分配空间与之前空间进行连接。
# define initSize 10//默认长度
typedef struct {
elemType *data;//指向动态分配数组的指针
elemType maxSize;//顺序表的最大容量
elemType length;//顺序表的当前容量
}SqList;//定义线性表
//初始化
void InitList(SqList& L) {
L.data = new elemType[initSize];//*data指向一个分配好的默认大小的空间
L.length = 0;
L.maxSize = initSize;
}
void IncreaseList(SqList& L, int len) {
elemType* p = L.data;//p指向之前L.data中的数据
L.data = new elemType[len]; //动态生成len空间大小的内存
for (int i = 0; i < L.length; i++) {
L.data[i] = p[i];//p中的值复制到心得data区域
}
L.maxSize = L.maxSize + len;//顺序表的最大长度增加
delete p;//释放p
}
//其他的操作方法与静态顺序表相同
时间复杂度的分析(自行参考上部代码)
插入操作
(1)最好情况:新元素插入到表尾,不需要移动元素 i = n+1,循环0次;最好时间复杂度 = O(1)
(2)最坏情况:新元素插入到表头,需要将原有的 n 个元素全都向后移动,i = 1,循环 n 次;最坏时间复杂度 = O(n);
(3)平均情况:
①假设新元素插入到任何一个位置的概率相同,即 i = 1,2,3, … , length+1 的概率都是 p =1/(n+1);
② n = 1,循环 n 次;i=2 时,循环 n-1 次;i=3,循环 n-2 次 …… i =n+1时,循环0次;
③平均循环次数 = np + (n-1)p + (n-2)p + …… + 1⋅p =[n(n+1)/2]⋅1/[(n+1)]===>平均时间复杂度 = O(n)
删除操作
(1)最好情况:删除表尾元素,不需要移动其他元素 i = n,循环 0 次;最好时间复杂度 = O(1)
(2)最坏情况:删除表头元素,需要将后续的 n-1 个元素全都向前移动 i = 1,循环 n-1 次;最坏时间复杂度 = O(n);
(3)平均复杂度:
①假设删除任何一个元素的概率相同,即 i = 1,2,3, … , length的概率都是 p =1/n;
②i = 1,循环 n-1 次;i=2 时,循环 n-2 次;i=3,循环 n-3 次 …… i =n 时,循环0次。
③平均循环次数 = (n-1)p + (n-2)p + …… + 1⋅p =[n(n-1)/ 2]*(1/n)=(n-1)/2===>平均时间复杂度 = O(n)
按值查找
时间复杂度就是O(1)
按位查找
(1)最好情况:目标元素在表头 循环1次;最好时间复杂度 = O(1)
(2)最坏情况:目标元素在表尾 循环 n 次;最坏时间复杂度 = O(n);
(3)平均情况:
①假设目标元素出现在任何一个位置的概率相同,都是1/n
②目标元素在第1位,循环1次;在第2位,循环2次;……;在第 n 位,循环 n 次。
③平均循环次数 = 1⋅(1/n)+2⋅(1/n)+……+n⋅(1/n)=(n+1)/2 O(n)
特点
- 随机访问
- 存储密度高(指针也需要存储)
- 扩展容量不方便
- 插入删除数据元素不方便
单链表
什么是单链表
线性表的链式存储又称单链表,它是指通过一组任意的存储单元来存储线性表中的数据元素。
单链表的定义
typedef int ElemType;
typedef struct LNode { //定义单链表结点类型
ElemType data; //每个节点存放-个数据元素
struct LNode *next; //指针指向下一个节点
}LNode,*LinkList;
单链表的初始化
bool InitList(LinkList& L) {
L = new LNode(); //生成新节点
L->next = NULL; //指向空
return true;
}
实现方法一(推荐:带头结点)
- 头结点data中不存储数据。(头结点不算第一个节点,插入操作的第一个接结点即第一个数据结点)
- 表空的判断方法为L->next==NULL。 写代码更方便。
头结点——>首元结点(第一个节点)——>第二个节点——>第三个节点——>……——>第n个节点
- 引入头结点后,可以带来两个优点:
①由于第一个数据结点的位置被存放在头结点的指针域中,因此在链表的第一个位置上的操作和在表的其他位置上的操作一致, 无须进行特殊处理。
②无论链表是否为空,其头指针都指向头结点的非空指针( 空表中头结点的指针域为空),因此空表和非空表的处理也就得到了统一。
采用头插法建立单链表
代码:
LinkList List_HeadInsert(LinkList &L) { //逆向建立单链表
LNode *s; //声明头结点
if (InitList(L)) { //初始化L
ElemType x;
cin >> x;
while (x != -1) {
s = new LNode(); //创建新节点
s->data = x;
s->next = L->next;
L->next = s;
cin >> x;
}
}
return L;
}
时间复杂度为O(n),n为链表长度
采用尾插法建立单链表
代码:
LinkList List_TailInsert(LinkList &L)
{
LNode *s,*r;
if(InitList(L))
{
r=L;
ElemType x;
cin>>x;
while(x!=-1)
{
s=new LNode();
s->data=x;
s->next=NULL;
r->next=s;
r=s;
cin>>x;
}
}
return L;
}
时间复杂度为O(n),n为链表长度
按序号查找结点值(返回第i个节点的指针)
在单链表中首元结点结点出发,顺指针next域逐个往下搜索,直到找到第i个结点为止,否则返回最后一个结点指针域NULL,头结点为第0个节点。
代码:
LNode* GetElem(LinkList L, int i) {
int j = 1;
LNode* p=L->next; //p指向了首元结点
if (i == 0) { //i=0,返回头结点
return L;
}
if (i < 1) { //i无效,返回空;
return NULL;
}
while (p && j < i) { //从第一个节点开始找,查找第i个节点
p = p->next;
j++;
}
return p; //返回第i个节点的指针,若大于表长,返回空;
}
按照序号查找的时间复杂度为O(n)
按值查找表节点
①用指针p指向首元结点。
②从首元结点开始依次顺着链域next向下查找,只要指向当前结点的指针p不为空,并且p所指结点的数据域不等于给定值e,则循环执行以下操作: p指向下一个结点。
③返回p。若查找成功,p此时即为结点的地址值,若查找失败,p的值即为NULL。
代码:
LNode* LocateElem(LinkList L, ElemType e) {
LNode* p = L->next;//p指向首元结点
while (p && p->data != e) {
p = p->next;
}
return p;
}
按照序号查找的时间复杂度为O(n)
单链表的插入操作(这是一个前插操作,将值为e的新结点插人到表的第 i 个结点的位置上,即插入到结点 ai-1 与 ai 之间)
1<=i<=n,当n=n+1是,相当于在尾部插入
①查找结点 ai-1 并由指针p指向该结点。
②生成一个新结点*s。
③将新结点 *s 的数据域置为e。
④将新结点 *s 的指针域指向结点 ai 。
⑤将结点 *p 的指针域指向新结点 *s 。
代码:
bool LinkListInsert(LinkList &L,int i,ElemType e)
{
LNode *s,*p=L;//此时p指向L是为了保证插入的结点可以是第一个节点之前,
int j=0; //如果p=L->next,则指向首元结点,第一个结点之前插入实现不了
while(p&&j<(i-1)) //此时是查找第i-1个结点
{
p=p->next;
j++;
}
//检验是否合法
if(!p||j>i-1)
return false;
s=new LNode();
s->data=e;
s->next=p->next;
p->next=s;
return true;
}
按照序号查找的时间复杂度为O(n)
单链表的删除操作(删除单链表的第i个结点ai)
①查找 ai 节点并由指针 p 指向该结点。
②临时保存待删除结点 ai 的地址在 q 中,以备释放。
③将结点 *p 的指针域指向 ai 的直接后继结点。
④释放结点 ai 的空间。
代码:
bool ListDelete(LinkList L,int i)
{
LNode*p=L;//此时p指向L是为了保证删除的结点可以是第一个节点,
int j=0; //如果p=L->next,则指向首元结点,第一个结点的删除就实现不了
while(p&&j<(i-1)) //查找第 i-1个结点,p指向该结点
{
p=p->next;
j++;
}
if((p->next)&&j<(i-1)) //当i>n或i<1时,删除位置不合理,与插入不同是为了防止空指针
{
return false;
}
q=p->next; //临时保存被删结点的地址以备释放
p->next=q->next; //改变删除结点前驱结点的指针域
delete q; //释放删除结点的空间
return true;
}
按照序号查找的时间复杂度为O(n)
实现方法二(不推荐:不带头结点)
- 表空的判断方法为L==NULL。
双链表
双链表:可进可退,存储密度更低一丢丢。
双链表的定义
代码
typedef int ElemType;
typedef struct DuLNode {
ElemType data;
struct DuLNode *prior;//前指针
struct DuLNode *next;//后指针
} DuLNode, *DuLinkList;
双链表的初始化及判断是否为空
代码
//带头结点
bool InitDuLinkList(DuLinkList &L) {
L = new DuLNode();
if(L == NULL) {
return false;
}
//前后指针为空
L->next = NULL;
L->prior = NULL;
return true;
}
bool IsEmpty(DuLinkList L) {
if(L->next == NULL) {
return true;
}
return false;
}
双链表的插入(前插 or 后插)
第i个位置插入,只需找到第i个位置对应的结点进行前插
代码
//在第i个位置前插操作
bool ListInsert_DuL(DuLinkList &L,int i,ElemType e){
DuLNode *p;
if(!p=GetElem_DuL(L,i)){//确定第i个元素的位置指针P
return false; //p为空时第i个元素不存在
}
DuLNode *s=new DuLNode();
s->data=e;
s->prior=p->prior;//对应①
p->prior->next=s;//对应②
s->next=p;//对应③
p->prior=s;//对应④
return true;
}
//在p结点之后插入s结点
bool InsertNextDNode(DuLNode *p, DuLNode *s) {
if (p == NULL | s == NULL) //非法参数
return false;
s - > next = p->next;
if (p->next != NULL) //如果p结点有后继结点
p->next->prior = s;
s->prior = p;
p->next = s;
return true;
}
双链表的删除
//删除带头结点的双向链表L中的第i个元素
Status ListDelete_ DuL (DuLinkList &L, int i) {
if(! (p = GetElem_ DuL(L, i))) //在L中确定第i个元素的位置指针p
return ERROR;//p为NULL时,第i个元素不存在
p->prior->next = p->next; //修改被删结点的前驱结点的后继指针,对应图①
p->next->prior = p->prior; //修改被删结点的后继结点的前驱指针,对应图②
delete p;//释放被删结点的空间
return OK;
}
//删除p结点的后继结点
bool DeleteNextDNode(DNode *p) {
if (p == NULL)
return false;
DuLNode *q = p->next; // 找到p的后继结点q
if (q == NULL)
return false; //p没有后继
p->next = q->next;
if (q->next != NULL) //q结点不是最后- 一个结点
q->next->prior = p;
delete p;//释放结点空间
return true ;
}
双链表的遍历 O(n)
//后向遍历
while (p != NULL) {
//对结点p 做相应处理,如打印
p = p->next;
}
//前向遍历
while (p != NULL) {
//对结点p 做相应处理,如打印
p = p->prior;
}
//前向遍历(跳过头结点)
while(p->prior != NULL) {
//对结点p 做相应处理,如打印
p = p->prior;
}
循环链表
循环单链表
① 表尾结点的next 指针指向头结点
② 从一个结点出发 可以找到其他任何一个结点
#include <iostream>
#include<stdio.h>
#include<stdlib.h>
using namespace std;
typedef int ElemType;
typedef struct LNode {
ElemType data;
struct LNode *next;
} LNode, *LinkList;
//初始化
bool InitList(LinkList &L) {
L = new LNode();
if(L == NULL) {
return false;
}
L->next = L; //头结点指针指向头结点
return true;
}
//判断循环链表是否为空
bool isEmpty(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;
}
}
循环双链表
① 表头结点的 prior 指向表尾结点; 表尾结点的 next 指向头结点
typedef int ElemType;
typedef struct DuLNode {
ElemType data;
struct DuLNode *prior;
struct DuLNode *next;
} DuLNode, *DuLinkList;
//双链表初始化
bool InitDuLinkList(DuLinkList &L) {
L = new DuLNode();
if(L == NULL) {
return false;
}
//前驱、后继结点均指向头结点
L->next = L;
L->prior = L;
return true;
}
//判断双循环链表是否为空
bool isEmpty(DuLinkList L) {
if(L->next == L) {
return true;
}
return false;
}
//判断节点p是否为双循环链表的表尾结点
bool isTail(DuLinkList L, DuLNode *p) {
if(p->next == L) {
return true;
} else {
return false;
}
}
循环双链表的插入和删除的比较(自行参考以上代码进行总结)
静态链表
什么是静态链表
静态链表:分配一整片连续的内存空间,各个结点集中安置
每个数据元素 4B,每个游标4B(每个结点共 8B)
设起始地址为 addr,e1 的存放地址为 addr + 8*2
如何定义静态链表
#define MaxSize 10
typedef int ElemType;//定义
typedef struct Node{
ElemType data;
int next;
}SLinkList[MaxSize];
//初始化
void InitSLinkList(SLinkList &S){
S[0].next=-1;
S[0].data=0;
}
静态链表的查找、删除、插入(自行解决)
顺序表与链表的比较
逻辑结构
- 都属于线性表,都是线性结构
存储结构
- 顺序表:顺序存储
- 链表:链式存储