- 唯一头元素
- 唯一尾元素
- 除头元素外,都有一个直接前驱
- 除尾元素外,都有一个直接后继
语言定义
线性表是n个数据元素的有限序列。线性表中的数据元素可以由若干个数据项组成。
形式定义
线性表可以表示成n个数据元素的有限序列(a1,a2,a3……ai-1,ai,……an)
其中a1是头元素,an是尾元素,ai是第i个元素。ai-1是ai的直接前驱,ai是ai-1的直接后继。
当2 $\leq$ i $\leq$ n时,ai只有一个直接前驱
当1 $\leq$ i $\leq$ n-1时,ai只有一个直接
基本操作
InitList(&L)//构造空线性表L
DestroyList(&L)//销毁已存在的线性表L
ClearList(&L)//将L重置为空表
ListEmpty(L)//判断列表是否为空
ListLength(L)//获取列表长度
GetElem(L,i,&e)//返回L中的第i个元素到e
LocateElem(L,e,compare())//查找元素e的位置
PriorElem(L,cur_e,&pre_e)//查找前驱元素
NextElem(L,cur_e,&next_e)//查找后继元素
ListInsert(&L,i,e)//插入元素
ListDelete(&L,i,&e)//删除元素
ListTraverse(L,visit())//遍历元素
线性表的实现
顺序表示和实现
线性表的顺序表示是指用一组地址连续的存储单元一次存储线性表的数据元素,用物理位置相邻来表示逻辑关系相邻,任意数据元素都可随意存取(故又称随机存取结构)
readme
顺序表中元素下标从0开始
以下顺序表的实现可以直接运行
#include <stdio.h>
#include <malloc.h>
#include <stdlib.h>
#define LIST_INIT_SIZE 100//顺序表初始化长度
#define LIST_INCREMENT 10 //每次不足时新增长度
#define OVERFLOW 0 //分配空间失败
#define REFREE 0 //重复释放,释放空指针
#define OK 1
#define ERROR 0
typedef int ElemType;//需要时进行修改
typedef int Status;
template <typename ElemType>//使用模板方便更多数据类型的使用
//结构定义
class List
{
public:
typedef struct
{
ElemType *elem;//存储数据元素
int length;//表长,初始为0
int listsize;//表存储容量,也就是实际分配的存储空间
}SqList;
SqList L;//线性表
List();//构造函数
~List();//析构函数
Status List_Init();//线性表初始化函数
Status List_Insert(int i,ElemType e);//线性表插入元素
Status List_Delete(int i,ElemType &e);//线性表删除元素
Status List_Traverse();//线性表遍历
Status List_Destroy();//线性表销毁
Status List_Clear();//线性表清空
};
template <typename ElemType>
List<ElemType>::List()
{
List_Init();//含有指针变量,构造时需要分配空间,不过我们可以直接利用线性表的初始化函数
}
template <typename ElemType>
List<ElemType>::~List()
{
if(!L.elem)//避免我们之前调用过线性表的销毁函数,导致重复释放指针
free(L.elem);
}
//线性表的初始化
template <typename ElemType>
Status List<ElemType>::List_Init()
{
L.elem = (ElemType *)malloc(LIST_INIT_SIZE * sizeof(ElemType));
if(!L.elem)//指针为空时,说明分配失败,通常由于内存满了,但这种情况一般不会出现
exit(OVERFLOW);
L.length = 0;
L.listsize = LIST_INIT_SIZE;
return OK;
}
//插入元素e到顺序表i位置
//可以插入第0个位置一直到第n个位置(第n个位置也就是附加在结尾)
template <typename ElemType>
Status List<ElemType>::List_Insert(int i, ElemType e)
{
if(i<0||i>L.length)//插入位置错误
return ERROR;
if(L.length>=L.listsize)//空间不足时分配空间,相等时说明当前空间已满不能再插入元素了,所以也要分配空间
{
ElemType *newbase = (ElemType *)realloc(L.elem,(L.listsize+LIST_INCREMENT)*sizeof(ElemType));
if(!newbase)
exit(OVERFLOW);
L.elem = newbase;//上述重新分配时,如果后续空间充足则会扩展并返回原指针,否则会寻找大小适合的空间,返回新指针(并自动释放原内存),所以elem指针需要进行更改。
L.listsize += LIST_INCREMENT;//增加存储容量
}
ElemType *q = &(L.elem[i]), *p = &(L.elem[L.length]);//插入元素位置(终止条件),处理元素位置
while(p != q)//从后往前进行复制,p的起始位置实际上是当前元素末尾的后一个位置
*(p--) = *(p-1);
*q = e;
++L.length;
return OK;
}
//删除i位置元素
//i的范围为0-(n-1)
template <typename ElemType>
Status List<ElemType>::List_Delete(int i, ElemType &e)//这里的参数e可以根据实际需要决定是否保留
{
if(i<0||i>=L.length)//删除位置错误
return ERROR;
ElemType *p = &(L.elem[i]), *q = &(L.elem[L.length-1]);//删除元素位置(处理元素位置),终止条件
e = *p;//e存储删除元素的值
while( p != q)//从前往后复制
*(p++) = *(p+1);
--L.length;
return OK;
}
//线性表的遍历,如果为空,返回NULL
//这里我们需要具体化,因为不同的数据元素,遍历可能会有很大不同
template<>Status List<double>::List_Traverse()
{
printf("Traverse input:\n");
if(L.length)
{
int i = 0;
for(;i<L.length;++i)
printf("%lf ",L.elem[i]);
printf("\n");
}
else
printf("NULL\n");
return OK;
}
//线性表的销毁(主要是指针的释放)
template <typename ElemType>
Status List<ElemType>::List_Destroy()
{
if(!L.elem)//重复释放或释放空指针时抛出异常
exit(REFREE);
free(L.elem);
L.elem = NULL;//重要,释放后指针置空
L.length = 0;
L.listsize = 0;
return OK;
}
//线性表的清空
//清空后elem仍然存在,不过长度为0
template <typename ElemType>
Status List<ElemType>::List_Clear()
{
L.length = 0;
return OK;
}
链式表示和实现
线性表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素,存储单元可以是来连续的,也可以是非连续的。
链表的优点:数据元素的插入和删除相对方便;不必事先估计存储空间。
链表的缺点:不可随机存取表中任意数据元素,不可直接访问线性表的长度。
任何时刻,每个内存单元都要有指针牵住,否则会出现内存泄漏或者非法访问
- 结点:两部分信息组成,存储数据元素信息的数据域,存储直接后继存储位置信息的指针域。指针域信息为指针或链。
- 首元结点:链表存储的第一个数据元素的结点。
- 头结点:指向链表的第一个结点(不一定指向数据元素的结点)
- 头结点:为了避免在插入、删除等操作时对首元结点进行特殊化处理,通常在链表第一个结点之前附加一个结点,用于记录线性表的某些性质信息(比如长度)。
#include <stdio.h>
#include <malloc.h>
#include <math.h>
#include <stdlib.h>
#define OK 1
#define ERROR 0
#define OVERFLOW 0
#define REFREE 0
#define INPUTERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status;
template <typename ElemType>
class Link
{
public:
typedef struct LNode
{
ElemType data;
struct LNode* next;
}LNode, * LinkList;
LinkList L;
Link();//构造函数
~Link(); //析构函数
Status LinkList_Init();//链表初始化
Status LinkList_Insert(int i, ElemType e);//插入
Status LinkList_Delete(int i, ElemType& e);//删除元素
Status LinkList_Destroy();//销毁链表,清除所有结点
Status LinkList_Clear();//清空链表,保留头结点
Status LinkList_Traverse();//遍历链表
int Get_length();//求链表长度,可省略
};
//Link的构造函数
template <typename ElemType>
Link<ElemType>::Link()
{
Init_LinkList();
}
//Link的析构函数
template <typename ElemType>
Link<ElemType>::~Link()
{
if (L)
{
LNode* p = L;
while (L)
{
p = L;
L = L->next;
free(p);
}
L = NULL;//置空
}
}
//Link的初始化
template <typename ElemType>
Status Link<ElemType>::Init_LinkList()
{
L = (LinkList)malloc(sizeof(LNode));
if (!L)
exit(OVERFLOW);
L->next = NULL;
return OK;
}
template <typename ElemType>
Status Link<ElemType>::LinkList_Insert(int i, ElemType x)
{
LNode* p = L;
int j = 0;
while (p && j < i)
{
p = p->next;
++j;
}
if (!p || j > i)
{
printf("\nInsert Error!!\n");
return ERROR;
}
LNode* s = (LinkList)malloc(sizeof(LNode));
s->data = x;
s->next = p->next;
p->next = s;
return OK;
}
template <typename ElemType>
Status Link<ElemType>::LinkList_Delete(int i, ElemType& e)
{
if (!L)
{
printf("\nError!!NO Exist\n");
return ERROR;
}
LNode* p = L;
int j = 0;
while (p->next && j < i)
{
p = p->next;
++j;
}
if (!(p->next) || j > i)
{
printf("\nDelete Error!!\n");
return ERROR;
}
LNode* q = p->next;
p->next = q->next;
e = q->data;
free(q);
return OK;
}
template<typename ElemType>
Status Link<ElemType>::LinkList_Destroy()
{
if (!L)
{
printf("\nREFREE\n");
exit(REFREE);
}
LNode* p = L;
while (L)
{
p = L;
L = L->next;
free(p);
}
L = NULL;//置空
return OK;
}
template <typename ElemType>
Status Link<ElemType>::LinkList_Clear()
{
if (!L)
{
printf("\nError!!NO Exist\n");
exit(ERROR);
}
LNode* p = L->next, * q = p;
while (p)
{
q = p->next;
free(p);
p = q;
}
L->next = NULL;
return OK;
}
//由于数据类型的不同,遍历会有很大区别,需要具体化
//这里以double数据类型为例
template<>Status Link<double>::LinkList_Traverse()
{
//printf("\nVector Traverse:\n");
if (!L)
{
printf("\nError!!NO Exist\n");
exit(ERROR);
}
LNode* p = L->next;
if (!p)
printf("Vector NULL\n");
else
{
while (p)
{
printf("%.2lf ", p->data);
p = p->next;
}
printf("\n");
}
return OK;
}
template <typename ElemType>
int Link<ElemType>::Get_length()
{
if (!L)
{
printf("\nError!!NO Exist\n");
exit(ERROR);
}
LNode* p = L -> next;
int cnt = 0;
while (p)
{
++cnt;
p = p->next;
}
return cnt;
}
其他线性链表
静态链表
数组的一个分量代表一个结点,同时用游标(cur)代替next指针,记录下一结点在数组中的相对位置。数组的第0分量可以看作时头结点,指针域指向链表的第一个结点。
优点:做线性表的插入和删除操作时不需要移动元素,仅需修改指针缺点:需要预先分配一个较大空间
//静态链表的结构定义
define MAXSIZE 1000
typedef struct
{
ElemType data;
int cur;//游标
}component,SLinkList[MAXSIZE];
循环链表
循环链表的最后一个结点的指针域不为空,而是指向链表的头结点,从而形成了一个环。
优点:从链表的任意节点出发均可以找到表中其他节点。
和线性单链表的主要差别:循环结束条件不是p是否为空,而是p是否等于头结点。
而有时间(比如,顺次合并两个线性表),我们通常在循环链表中设立尾指针。
//含有尾指针的循环链表的顺次合并
p = tail2->next->next;//取得第二个链表的首元结点
tail2->next = tail1->next;//将第二个链表尾指针指向第一个链表头结点
tail1->next = p;//将第一个链表尾指针指向第二个链表的首元结点
双向链表
使用循环链表我们可以从任意节点出发找到其他节点,但是当我们找当前节点的前驱时,却需要遍历整个链表。所以,我们在循环链表中增加一个指针域,指向直接前驱,构成双向链表。
typedef struct DuLNode
{
ElemType data;//数据域
struct DuLNode *prior;
struct DuLNode *next;
}DuLNode,*DuLinkList;
//插入和删除和单链表、循环链表相比有一点不同
//插入,将s结点插到p之前
s->prior = p->prior;
s->next = p;
p->prior->next = s;
p->prior = s;
//删除,删除p结点
p->prior->next = p->next;
p->next->prior = p->prior;
free(p);
判断链表是否为空
1.不带头结点线性单链表:L == NULL
2.带头结点线性单链表:L->next == NULL
3.静态链表:L[0].cur=0
4.带头结点循环链表:L->next == L
5.带头结点双向链表:L->next = L->prior = L