前言
在文章开始前我们先做如下定义,这会方便我们后续解释和理解代码所表达的含义
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status; /* Status是函数的类型,其值是函数结果状态代码,如OK等 */
一、线性表的顺序存储结构
大家应该都遇到过图书馆占位的情况,其实顺序存储和占位一样,现在我们想象图书馆就是计算机的内存,馆里的座位就是一个个存储空间,而顺序存储结构就是拿着几本书,但是不一定全部坐满了人,对应到计算机内部就是在线性表建立时开辟一段连续内存空间给占了,在然后把相同的数据存放其中。
(一)存储结构
以下便是用c++语言在程序中表示一个顺序存储的线性表的结构体:
#define MAXSIZE 20 /* 存储空间初始分配量 */
typedef int ElemType; /* ElemType类型根据实际情况而定,这里假设为int */
typedef struct
{
ElemType data[MAXSIZE]; /* 数组,存储数据元素 */
int length = 0; /* 线性表当前长度 */
}SqList;
数组是最简单的顺序结构的顺序表,没错,我们用一个最简单的顺序表实现了一个稍稍复杂的顺序表 (好像很水)
(二)操作
1. 初始化顺序表
Status InitList(SqList &L){
L.data = new Elemtype[MAXSIZE];
if(!L.data)exit(OVERFLOW)
L.length = 0;
return OK;
}
2. 销毁顺序表
void DestorList(SqList &L){
if(L.data) delete L;
}
3. 清空顺序表
void ClearList(SqList &L){
L.length = 0;
}
4. 获取顺序表长度
int GetLength(SqList L){
if(L.length) return L.length;
}
5. 判断顺序表是否为空
bool IsEmpty(SqList L){
if(L.length == 0) return 1;
return 0;
}
6. 查找元素的位置
整个函数的思路就是遍历整个顺序表,如果找到了值,那就将他返回,如果没找到就返回0。这时有人可能会问那不需要考虑顺序表为空的时候吗,如果顺序表为空那么i=L.length,也就是说连循环都不会进去,其实这个函数已经隐含考虑了这个问题
int LocateElem(Sqlist L,ElemType e){
for(int i = 0; i < L.length; ++i){
if(e == L.data[i]) return i + 1;
}
return 0
}
7. 顺序表的插入
要向一个顺序表插入一个值,首先要考虑以下两个问题,顺序表满没满?插入的位置合不合理?如果这两个问题的回答都是肯定的,那么就从最后一个元素开始依次将每个元素向后移一位,直到需要插入元素的位置被空了出来
Status ListInsert(SqList &L, int i, ElemType e){
if(L.length == MAXSIZE) return ERROR;
if(i < 1 || i > L.length + 1) return ERROR;
for(int j = L.length-1; j > i-1; --j){
L.data[j+1] = L.data[j];
}
L.data[i-1] = e;
++L.length;
return OK;
}
8. 顺序表的删除
Status ListDelete(SqList &L, int i, ElemType e){
if(i < 1 || i > L.length) return ERROR;
e = L.data[i-1];
for(int j = i; j < L.length-1); ++j{
L.data[j-1] = L.data[j];
}
--L.length;
return OK;
}
9. 顺序表的取值
查找顺序表某一位置的值的时候,要考虑查顺序表有没有元素(L.length==0),查找的位置是否合法(1<i<L.length)
Status GetElem(SqList L,int i,ElemType &e)
{
if(L.length==0 || i<1 || i>L.length)
return ERROR;
e=L.data[i-1];
return OK;
}
10. 遍历顺序表
Status ListTraverse(SqList &L){
if(L.length == 0) return ERROR;
for(int i = 0; i < L.length; ++i){
cout << L.data[i] << '\t';
}
return OK;
}
二、线性表的链式存储结构(单链表)
顺序存储结构虽然简单,但却有一个很大的缺点,就是在移动大量元素时很浪费时间,这是因为它们的物理结构是连续的,增删元素的话为了保证内存空间上的连续性,就必须移动元素腾出或者覆盖位置,那有没有什么方法可以解决这个问题?答案很简单,那就是破坏掉数据在内存空间上的连续性,那数据在内存空间的连续性被破坏了,我们怎么去找元素呢?这只需要在每个数据节点新增一个指针域,用以存储该节点的下一节点元素的位置就可以完美解决这个问题,这就是大名鼎鼎的链式存储结构。
(一)存储结构
typedef struct LNode{
ElemType data;
struct LNode *next;
}*LinkList
我们知道在结构体后面写名字是给这个变量起别名,那么带个‘*’是什么意思呢,用这个别名声明变量就代表该变量是指向这个结构体的指针
LNode L; // L是一个LNode类型的结构体
LinkList L; // L是一个指向LNode结构体的指针
(二)操作
1. 单链表的初始化
Status InitList(LinkList &L){
L = new LNode ;
if(!L) return ERROR;
L -> next = NULL;
return OK;
}
2. 判断单链表是否为空
判断一个单链表是否为空只需要查找头节点的下一元素是否为空就好,这里的L是一个指向LNode的指针,这个被指向的LNode就是头结点,一般不存储任何数据,即数据域为空,通过箭头可以直接摸到该指针指向的节点的指针域,因而可以直接判断单链表的状态(maybe,说的有点绕)
bool ListEmpty(LinkList L){
if(L -> next == NULL) return TRUE;
else return FALSE
}
3. 销毁单链表
单链表的销毁不想线性表,我们需要一个一个节点销毁,这里我们选择从头开始,用一个指针q指向我们当前想要删除的节点,然后不断更新L就好
Status DestoryLis(LinkList &L){
LinkList q;
while(L -> next){
q = L;
L = L -> next;
delete q;
}
return OK;
}
4. 清空单链表
原理同上,只不过清空单链表还会留下一个头结点
Status ClearList(LinkList &L){
LinkList p, q;
p = L -> next;
while(p){
q = p;
p = p -> next;
delete q;
}
L = L -> next;
return OK;
}
5. 获取单链表的长度
原理同上,只是在遍历单链表的同时加了一个计数器
int ListLength(LinkList L){
LinkList p;
p = L -> next;
int i = 0;
while(p){
++i;
p = p -> next;
}
return i;
}
6. 单链表的取值
通过计数器j来计算当前位置,当j达到取值位置时或者p为空就结束循环,然后判断,先前结束循环的原因,如果p确实为空了或者j小于i都说明还未达到位置就跳出来循环,所以返回错误值,如果不是,则说明找到了目标数据,将他通过引用带出返回正常。要注意的是,凡是涉及单链表先找位置再操作的函数(与之相对应的是先找值再操作),如果找到,所有指针p指向的都是要操作位置的前一个,原因看这里1
Status GetElem(LinkList L, int i, ElemType &e){
p = L -> next;
int j = 1;
while(p && j < i){
p = p -> next;
++j;
}
if(!p || j < i) return ERROR;
e = p -> next;
return OK;
}
7. 单链表的查找(返回数据指针)
LinkList LocateElem(LinkList L, ElemType e, LinkList &p){
p = L -> next;
while(p && p -> data != e) p = p -> next;
return p;
}
8. 单链表的查找(返回数据位置)
int LocateElem(LinkList L, ElemType e, int &j){
p = L -> next;
j = 1;
while(p && p -> data != e){
p = p -> next;
++j;
}
if(p) return j;
return 0;
}
9. 单链表的插入
当我们找到需要插入的位置后(默认位置合法),我们新建一个节点,将数据传入数据域,然后我们需要将这个新的节点插进去,但是我们不能直接将s赋值给当前p指向的节点(插入位置的前一个节点),这么做会导致p节点下一个节点的丢失,那咋办嘞?我们得把下一个节点的地址记下来,那我们需要再新建一个指针来保存吗?不用,我们新建的节点就是一个完美的指针保存器,只需要先把下一节点的位置赋值给s的指针域然后就可以安心的将s赋值给p的指针域了
Status ListInsert(LinkList &L, int i, ElemType e){
LinkList p = L;
int j = 1;
while(p && j < i){
p = p -> next;
++j;
}
if(!p || j < i) return ERROR;
s = new LNode;
s.data = e;
s.next = p -> next;
p -> next = s;
return OK;
}
10. 单链表的删除
但是删除不一样,我们确实需要一个用来存储需要删除节点的指针但是删除前要注意,删除该节点后他的下一节点又会丢失,所有我们要先把下一节点存到删除节点的上一节点,也就是当前p所指向的节点
Status ListDelete(LinkList &L, int i, ElemType e){
LinkList p = L;
int j = 1;
while(p && j < i){
p = p -> next;
++j;
}
if(!p || j < i) return ERROR;
LinkList q = p -> next;
p -> next = q -> next;
e = q -> data;
delete q;
return OK;
}
11. 单链表的建立(头插法)
主要讲讲插入过程。新建完节点后,我们有两个东西,一个是原先的链表,另一个是待插入的节点,我们只需要先把L的下一节点给到p的指针域,然后将p的地址给到L的指针域就好
void CreatListHead(LinkList &L, int n){
L = new LNode;
L -> next = NULL;
for(int i = 0; i < n; ++i){
p = new LNode;
cin >> p -> data;
p -> next = L -> next;
L -> next = p;
}
}
12. 单链表的建立(尾插法)
如果是尾插法我们需要新增一个指向为节点的指针,来表示尾节点所在的位置,然后将尾节点的指针域赋值为新增节点的地址,然后更新尾节点为p就ok了
void CreatListTail(LinkList &L, int n){
L = new LNode;
L -> next = NULL;
LinkList r = L;
for(itn i = 0; i < n; ++i){
p = new LNode;
cin >> p -> data;
p -> next = NULL;
r -> next = p;
r = p
}
}
13. 单链表的遍历
Status ListTraverse(LinkList L){
LinkList p = L;
while(p){
cout << p -> data;
p = p -> next;
}
return OK;
}
线性表和单链表对比
比较点 | 线性表 | 单链表 |
---|---|---|
存储空间 | 预先分配,可能会闲置或溢出 | 动态分配 |
存储密度 | 不需要为节点之间的 逻辑关系而增加额外的存储空间,存储密度为1 | 需要借助指针来 记录节点之间的逻辑关系, 存储密度小于1 |
改查时间 | 索引查值复杂度为O(1), 元素查值复杂度为O(n) | 索引和元素查值的复杂度均为O(1) |
增删 | 复杂度为O(n) | 复杂度为O(1) |
适用情况 | 表长可预测,常查询少增删 | 表长不定,经常需要增删元素 |