1、链表
(1)概念
- 结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻
- n 个结点由指针链组成一个链表。它是线性表的链式存储映像,称为线性表的链式存储结构
(2)结点组成
- 数据域:存储元素数值数据
- 指针域:存储直接后继结点的存储位置
(3)单链表、双链表与循环链表:
- 结点只有一个指针域的链表,称为单链表或线性链表
- 有两个指针域的链表,称为双链表。双向链表能够克服单链表查询前驱结点必须从表头出发的问题,双向链表有两个指针域分别指向自己的前驱结点和后继结点,查找的时间复杂度为O(1)
- 首尾相接的链表称为循环链表,从循环链表中的任何一个结点的位置都可以找到其他所有结点,而单链表做不到
(4)头指针、头结点和首元结点
- 头指针是指向链表中第一个结点的指针
- 首元结点是指链表中存储第一个数据元素a1的结点
- 头结点是在链表的首元结点之前附设的一个结点,数据域内只放空表标志和表长等信息。头结点的数据域可以为空,也可存放线性表长度等附加信息,但此结点不能计入链表长度值。
(5)在链表中设置头结点有什么好处
- 便于首元结点的处理:首元结点的地址保存在头结点的指针域中,所以在链表的第一个位置上的操作和其它位置一致,无须进行特殊处理
- 便于空表和非空表的统一处理:无论链表是否为空,头指针都是指向头结点的非空指针,因此空表和非空表的处理也就统一了
(6)特点
- 结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻
- 访问时只能通过头指针进入链表,并通过每个结点的指针域向后扫描其余结点,所以寻找第一个结点和最后一个结点所花费的时间不等
(7)优缺点
优点:
- 数据元素的个数可以自由扩充
- 插入、删除等操作不必移动数据,只需修改链接指针,修改效率较高
缺点:
- 存储密度小
- 存取效率不高,必须采用顺序存取,即存取数据元素时,只能按链表的顺序进行访问
(8)结构定义
typedef struct Lnode { ElemType data; //数据域 struct LNode *next; //指针域 }LNode,*LinkList; // *LinkList为Lnode类型的指针
(9)初始化
Status InitList_L(LinkList &L){ L=new LNode; //生成新结点作头结点,用头指针L指向头结点。 L->next=NULL; //头结点的指针域置空 return OK; }
2、链表的基本操作
(1)销毁
Status DestroyList_L(LinkList &L) { LinkList p; while(L) { p=L; L=L->next; delete p; } return OK; }
(2)清空
Status ClearList(LinkList & L){ // 将L重置为空表 LinkList p,q; p=L->next; //p指向第一个结点 while(p) //没到表尾 {
q=p->next;
delete p;
p=q;
} L->next=NULL; //头结点指针域为空 return OK; }
与销毁不同,清空的时候会保留头结点
(3)求表长
p=L->next; i=0; while(p)
{i++;
p=p->next;
}
int ListLength_L(LinkList L){ //返回L中数据元素个数 LinkList p; p=L->next; //p指向第一个结点 i=0; while(p){//遍历单链表,统计结点数 i++; p=p->next;
} return i; }
(4)判断是否为空
int ListEmpty(LinkList L) { //若L为空表,则返回1,否则返回0 if(L->next) //非空 return 0; else return 1; }
3、对链表的操作
(1)添加元素:在L中第i个元素之前插入数据元素e
Status ListInsert_L(LinkList &L,int i,ElemType e){ p=L;j=0; while(p&&j<i−1){p=p->next;++j;} //寻找第i−1个结点 if(!p||j>i−1)return ERROR; //i大于表长 + 1或者小于1 s=new LNode; //生成新结点s s->data=e; //将结点s的数据域置为e s->next=p->next; //将结点s插入L中 p->next=s; return OK; }//ListInsert_L
时间复杂度:O(1)
(2)删除元素:将线性表L中第i个数据元素删除
Status ListDelete_L(LinkList &L,int i,ElemType &e){ p=L;j=0; while(p->next &&j<i-1){ //寻找第i个结点,并令p指向其前驱 p=p->next; ++j; } if(!(p->next)||j>i-1) return ERROR; //删除位置不合理 q=p->next; //临时保存被删结点的地址以备释放 p->next=q->next; //改变删除结点前驱结点的指针域 e=q->data; //保存删除结点的数据域 delete q; //释放删除结点的空间 return OK; }//ListDelete_L
时间复杂度:O(1)
(3)查找:在线性表L中查找值为e的数据元素
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; }
时间复杂度:O(n)
4、单链表的创建
(1)前插法
void CreateList_F(LinkList &L,int n){ L=new LNode; L->next=NULL; //先建立一个带头结点的单链表 for(i=n;i>0;--i){ p=new LNode; //生成新结点 cin>>p->data; //输入元素值 p->next=L->next;L->next=p; //插入到表头 } }//CreateList_F
- 生成新结点
- 将读入数据存放到新结点的数据域中
- 将该新结点插入到链表的前端
(2)尾插法
void CreateList_L(LinkList &L,int n){ //正位序输入n个元素的值,建立带表头结点的单链表L L=new LNode; L->next=NULL; r=L; //尾指针r指向头结点 for(i=0;i<n;++i){ p=new LNode; //生成新结点 cin>>p->data; //输入元素值 p->next=NULL; r->next=p; //插入到表尾 r=p; //r指向新的尾结点 } }//CreateList_L
- 从一个空表L开始,将新结点逐个插入到链表的尾部,尾指针r指向链表的尾结点
- 初始时,r同L均指向头结点。每读入一个数据元素则申请一个新结点,将新结点插入到尾结点后,r指向新结点
5、线性表和链表的比较
(1)空间
- 存储空间:顺序表需要预先分配,会导致空间闲置或溢出现象,链表动态分配,不会出现存储空间闲置或溢出现象
- 存储密度:线性表等于1,链表小于1
(2)时间
- 取数据:顺序表是随机存取,时间复杂度为O(1),链表是顺序存取,时间复杂度为O(n)
- 插入删除:顺序表需要移动元素,时间复杂度为O(n),链表不需要,时间复杂度为O(1)
(3)适用范围
线性表
表长变化不大,且能事先确定变化的范围
很少进行插入或删除操作,经常按元素位置序号访问数据元素
链表
长度变化较大
频繁进行插入或删除操作