线性表的顺序存储结构或顺序映像:用一组地址连续的存储单元存储线性表的数据元素(物理顺序与逻辑顺序一致)。是一种随机存取的存储结构(只要确定了其起始位置,表中任一数据元素可随机存取)。
线性表:逻辑结构
顺序表:存储结构
1)顺序表的实现
typedef struct{
char no[20];
char name[10];
float price;
}book;
typedef struct{
book *elem;
int length;
}SqList;
2)初始化
Status initList(Sqlist &l)
{
//为l动态分配一个预定义大大小的数组,使elem指向这个空间的基地址
l.elem = new elemtype(MAXSIZE);
if(!L.elem) return ERROR;
//当前长度为0
l.length = 0;
return OK;
}
3)取值(取i位置的值)
Status findList(Sqlist l,int i,elemtype & e)
{
//判断取值下标是否合法
if(i<1 || i>l.length) return ERROR;
//将值取出赋值给e,这里体现了随机存取的特性
e = l.elem[i-1];
return OK;
}
4)查找(按值查找)
Status findList(Sqlist l,elemtype e)
{
int i ;
//找到了返回位置,没找到返回错误状态码
for(i = 0;i<l.length;i++)
if(l.elem[i]==e)
return i+1;
return ERROR;
}
5)插入(在i位置处插入元素)
0,1,2,3,4,5,6,7
1,2,3,4,5,6,7,8 要把9插在第i= 4 的位置(位置从1开始),即插在(下标从零开始)下标 3 的位置,即在下标为2的前面,所以要把下标为3**(i-1)至下标为l.length-1**的元素全部往后移
Status insertList(Sqlist &l,int i,elemtype e)
{
//判断插入下标是否合理
if(i<1 || i>l.length+1) return ERROR;
//如果表已满则无法插入
if(l.length==MAXSIZE) return ERROR;
/*将表后移
循环到最后是l.elem[i] = l.elem[i-1];
循环也可以写作
for(int j = l.length-1;j=i-1;j--)
{
l.elem[j+1] = l.elem[j];
}
上面j表示要移动的元素的下标,而下面这种写法表示移动后元素的下标*/
for(int j = l.length ;j>=i;j--)
{
l.elem[j] = l.elem[j-1];
}
l.elem[i-1] = e;
//线性表长度加一
l.legnth++;
return OK;
}
6)删除(i位置的元素)
Status deletList(Sqlist &l,int i,elemtype &e)
{
//判断删除下标是否合理
if(i<1 || i>length) return ERROR;
//记录要删除元素的数值
e = l.elem[i-1];
/*移动表
要把位置为i+1到位置为l.length的元素全部往前移,也就是下标为i到l.length-1
所以循环也可以写成
for(int j = i;j<=length-1;j++)
l.elem[j-1] = l.elem[j];
上面j表示要移动的下标,而下面的j表示移动后的下标*/
for(int j = i-1;j<=length-2;j++)
l.elem[j] = l.elem[j+1];
//长度减一
l.length--;
return OK;
}
7)销毁顺序表
void destroyList(Sqlist & l)
{
if(l.elem) delete[]l.elem;//释放存储空间
}
8)清空顺序表
void clearList(Sqlist & l)
{
l.length = 0;//将线性表长度置为零
}
9)求线性表长度
int getLength(Sqlist l)
{
return(l.length);
}
10)判断线性表是否为空
int isEmpty(Sqlist l)
{
//是空返回1,否则返回0
if(!l.length)
return1;
return 0;
}
查找、插入、删除算法的平均时间复杂度为O(n)
顺序表的空间复杂度S(n)=O(1)(没有占用辅助空间)
- 顺序表(顺序存储结构)的特点
(1)利用数据元素的存储位置表示线性表中相邻数据元素之间的前后关系,即线性表的逻辑结构与存储结构一致
(2)在访问线性表时,可以快速地计算出任何一个数据元素的存储地址。因此可以粗略地认为,访问每个元素所花时间相等
这种存取元素的方法被称为随机存取法
- 顺序表的优缺点
- 优点:
存储密度(结点本身所占存储量/结点结构所占存储量)大。
可以随机存取表中任一元素。- 缺点:
在插入、删除某一元素时,需要移动大量元素。
浪费存储空间。
属于静态存储形式,数据元素的个数不能自由扩充。
- 线性表链式存储
线性表的链式存储结构或非顺序映像或链式映像:结点在存储器中的位置是任意的(逻辑顺序与物理顺序不一定一致)。是一种顺序存取(访问时只能通过头指针进入链表,并通过每个结点的指针域向后扫描其余结点,所以寻找第一个结点和最后一个结点所花费的时间不等)的存取结构。
结点包括数据域和指针域(储存直接后继存储位置的域)。
链表: n 个结点由指针链组成一个链表。它是线性表的链式存储映像,称为线性表的链式存储结构。
**单链表(线性链表)**每个结点只包含一个指针域。可由头指针唯一确定。
双链表:有两个指针域的链表。
循环链表:首尾相接的链表称。
头指针是指向链表中第一个结点的指针。(若有头结点,则头指针指向头结点,没有头结点,指向首元结点。)
首元(开始)结点是指链表中存储第一个数据元素a1的结点。
头结点是在链表的首元结点之前附设的一个结点;数据域内只放空表标志和表长等信息。
设置头结点有什么好处
1.便于首元结点的处理
首元结点的地址保存在头结点的指针域中,所以在链表的第一个位置上的操作和其它位置一致,无须进行特殊处理;2.便于空表和非空表的统一处理
无论链表是否为空,头指针都是指向头结点的非空指针,因此空表和非空表的处理也就统一了。只要判断(L-next==NULL)即可
1)单链表存储结构定义
//单链表的存储结构
typedef struct LNode{
//结点的数据域
elemtype data;
//结点的指针域,指向下一个结点
struct LNode *next;
}LNode,*LinkList;
//这里LinkList为指向结构体的指针
-
*typedef int n 这里是给了 int*一个新名字为 n,其类型为指针;
*typedef struct LNode LinkList 是给了struct LNode*一个新名字 LinkList, 其类型为指针;
所以LNode** =LinkList* ,这里提高了程序可读性,LNode*定义指向链表任意结点的指针变量,而LinkList定义指向单链表的 头指针。
-
Lnode *p
指针变量p:表示结点地址
结点变量*p:表示一个结点
2)初始化
参数:要给哪个链表(指针)初始化,要改变其值,传引用
Status initList(LinkList &L)
{
//生成新的结点作为头结点,头指针L指向新的结点
L = new LNode;
//将指针域置为空
L->next = NULL;
return OK;
}
3)销毁(将每一个结点的内存释放)
参数:要销毁哪个链表?要改变其值,传引用
Status DelteList(LinkList &L)
{
/*暂存要释放的结点,因为如果直接释放,后面的结点会找不着了,链表无法移动
为什么用指针?因为指针里面存的是地址,delete释放的是指针所指的地址*/
LinkList p;
while(L)
{
p = L;
L = L->next;
delete p;
}
return OK;
}
4)清空(将链表置为空表,释放除了头指针外的其他结点)
参数:要清空哪个链表,要改变其值,传引用
Status ClearList(LinkList &L)
{
LinkList p,q;
p = L->next;
/*while(p)
{
q = p;
p = p->next;
delete q;
}*/
//也可以直接调用delete函数
DeleteList(p);
L->next = NULL;
return OK;
}
5)求表长(遍历)
参数:要求哪个链表的长度,不用改变链表,传值
int LengthList(LinkList L)
{
int length = 0;
/*
//指向第一个结点
LinkList p = L->next;
//循环最后p = NULL退出
while(p)
{
length++;
p = p->next;
}*/
L = L->next;
while(L)
{
length++;
L = L->next;
}
return length;
}
6)判断是否是空表
参数:要求判断哪个链表,不用改变链表,传值
int IsemptyList(LinkList L)
{
if(!L->next)
return 1;
else
return 0;
}
7)取值
参数:要取哪个链表的值,不用改变其值,传值;要找哪个位置的元素;要把那个元素的值得到,要改变已存在的量,引用
Status GetList(LinkList L,int i,Elemtype &e)
{
LinkList p;
p = L->next;j = 1;
//退出循环的条件:①j = i,这个时候已经遍历到了所需要的那个结点;②链表走到了尽头
while(p && j<i)
{
p = p->next;
j++;
}
if(!p && j>i)
return ERROR;//没有找到
e = p->data;
return OK;
}
8)按值查找
参数:要查找哪个链表里的,不改变其值,传值;要查找的值,不改变其值,传值
返回那个值的地址
LNode* LocateElem(LinkList L,Elemtype e)
{
p=L->next;
while(p &&p->data!=e)
p=p->next;
//不用判断是不是找到了,因为没找到自动返回NULL
return p;
/*也可以不用辅助指针,因为不改变L的值
L = L->next;
while(L && L->data!=e)
{
L = L->next;
}
return L;
*/
}
//还有一种写法,返回其位置
int LocateELem_L (LinkList L,Elemtype e) {
//返回L中值为e的数据元素的位置序号,查找失败返回0
p=L->next; j=1;
while(p &&p->data!=e)
{p=p->next; j++;}
if(p) return j;
else return 0;
}
9)插入
参数:要插入哪个链表中,改变其值,传引用;要插入的位置,不改变其值,传参;要插入的元素,不改变其值,传参
Status InsertList(LinkList &L,int i,ElemType e)
{
LinkList p = L;
//去查找i-1的位置
int j = 0;
while(p && j<i-1)
{
p = p->next;
}
//没找着
if(!p && j>i-1) return ERROR;
/*可以用之前的查找函数
那么就要把取值函数的返回值变成指针(地址)
*/
LNode* s;//也可以写成 s = new LNode;
s->data = e;
s->next = p->next;
p->next = s;
return OK;
}
10)删除(和插入相似,但是要记得释放被删除元素的内存)
参数:要删除哪个链表里面的元素,改变其值,传引用;要删除的那个元素的位置,传值;得到被删除元素的值,传引用
Status DeleteElem(LinkList &L,int i,Elemtype &e)
{
//还是先查找i-1个元素的位置
LinkList p = L;
j = 0;
while(p && j<i-1)
{
p = p->next;
}
if(!p && j>i-1) return ERROR;
//找一个暂存那个被删元素的地址
s = new LNode;
s = p->next;
e = s->data;
p->next = s->next;//改变删除结点前驱结点的指针域
delete s;
return OK;
}
11)单链表的建立
- 头插法
从一个空表开始,重复读入数据:
- 生成新结点
- 将读入数据存放到新结点的数据域中
- 将该新结点插入到链表的前端
参数:要建立哪个链表,改变其值,传引用;其中结点的个数,传值
void CreatList(LinkList & L,int n)
{
//先建立一个带头结点的空链表
L = new LNode;
L->next = NULL;
int i = 0;
for(i = 0;i<n;i++)
{
p = new LNode;
cin>>p->data;
//将新结点插在头结点之后
p->next = L->next;
L->next = p;
}
}
- 尾插法
- 从一个空表L开始,将新结点逐个插入到链表的尾部,尾指针r指向链表的尾结点。
- 初始时,r同L均指向头结点。每读入一个数据元素则申请一个新结点,将新结点插入到尾结点后,r指向新结点。
void CreatList(LinkList & L,int n)
{
//先建立一个带头结点的空链表
L = new LNode;
L->next = NULL;
//要生成一个尾指针,先指向头结点
r = new LNode;
r = L;
int i;
for(i =0;i<n;i++)
{
p = new LNode;
cin>>p->data;
//因为要把p放在尾部,所以p的next是NULL
p->next = NULL;
r->next = p;//p要放在最后
r = p;//r还是指向最后一个元素
}
}
链表的优点
- 数据元素的个数可以自由扩充
- 插入、删除等操作不必移动数据,只需修改链接指针,修改效率较高
缺点
- 存储密度小
- 存取效率不高,必须采用顺序存取,即存取数据元素时,只能按链表的顺序进行访问(顺藤摸瓜)
从循环链表中的任何一个结点的位置都可以找到其他所有结点。循环链表一般给出尾指针。
1)建立一个循环链表(尾插法)
参数:要建立哪个链表,改变其值,传引用;其中结点的个数,传值
void CreatList(LinkList &L,int n,LinkList &r)
{
//一个空表
L->next = L;
r = L;
int i;
//开始建立
for(i = 0;i<n;i++)
{
q = new LNode;
cin>>q->data;
r->next = q;
r = q;
}
r = L;
}
//另一种写法,返回值是尾指针
LNode* CreatList(LinkList &L,int n)
{
LinkList r;
r = L;
int i;
for(i = 0;i<n;i++)
{
q = new LNode;
cin>>q->data;
r->next = q;
r = q;
}
r = L;
return r;
}
2)两个循环链表的合并
参数:已知两个尾指针,要改变链表的值,传引用,第一个合并到第二个上去
//要把第二个的首元结点接到第一个尾结点上,第二个的尾指针指向第一个的头结点
void ConbineList(LinkList &r1,LinkList &r2)
{
p = new LNode;
p = r1->next;//表示头结点
r1->next = r2->next->next;//r2表头接r1表尾
delete r2->next;//释放r1的头指针
r2->next = p;//r2的尾指针指向头结点
}
- ###双向链表
1)双向链表存储结构定义
typedef struct DuLNode{
ElemType data;
//相比单链表多了一个指针域,指向前面一个元素
struct DuLNode *prior;
struct DuLNode *next;
}DuLNode, *DuLinkList
2)双向链表的建立
参数:要创建哪个链表,改变其值,传引用;多少个结点,传值
void CreatDuL(DuLinkList &L,int n)
{
/*头结点之前没有元素
初始化为空表*/
q = new DuLNode;
q = L;
int i;
for(i =0;i<n;i++)
{
//创建新的结点
p = new DuLNode;
cin>>p->data;
//新节点的prior指向前一个结点
p->prior = q;
//前一个结点的next指向p
q->next = p;
//移动q
q = p;
}
}
3)双向循环链表的创建
参数:要建立那个链表,改变其值,传引用;结点个数,传值
void CreatDDuL(DuLinkList &L,int n)
{
q = new DuLNode;
q = L;
for(i =0;i<n;i++)
{
//创建新的结点
p = new DuLNode;
cin>>p->data;
//新节点的prior指向前一个结点
p->prior = q;
//前一个结点的next指向新结点
q->next = p;
//移动q
q = p;
}
//首尾相接
L->prior = p;
p->next = L;
}
若d为双向链表中的某一结点则有 d->next->prior = d->prior->next = d;