目录
数据结构-单链表(第二章)的整理笔记,若有错误,欢迎指正。
线性表-顺序表(Ⅱ)
线性表的链式表示
单链表的定义
线性表的链式存储又称单链表,它是指通过一组任意的存储单元来存储线性表中的数据元素。为了建立数据元素之间的线性关系,对每个链表结点,除存放元素自身的信息外,还需要存放一个指向其后继的指针。
单链表中结点类型的描述
typedef struct LNode
{
ElemeType data; //数据域
struct Lnode *next; //指针域
}LNode, * LinkList;
//等价于
struct LNode
{
ElemeType data; //数据域
struct Lnode *next; //指针域
};
typedef struct LNode LNode;
typedef struct LNode *LinkList;
- 通常用头指针来标识一个单链表,如单链表L,头指针为NULL时表示一个空表。
- 此外,为了操作上的方便,在单链表第一个结点之前附加一个结点,称为头结点。头结点的数据域可以不设任何信息,也可以记录表长等信息。头结点的指针域指向线性表的第一个元素结点。
- 头结点和头指针的区分:不管带不带头结点,头指针都始终指向链表的第一个结点,而头结点是带头结点的链表中的第一个结点,结点内通常不存储信息。引入头结点后,可以带来两个优点:
- 由于第一个数据结点的位置被存放在头结点的指针域中,因此在链表的第一个位置上的操作和在表的其他位置上的操作一致,无须进行特殊处理。
- 无论链表是否为空,其头指针都指向头结点的非空指针(空表中头结点的指针域为空)因此空表和非空表的处理也就得到了统一。
单链表基本操作的实现
初始化线性表(时间复杂度:O(1))
带头结点
创建一个空的单链表,即创建一个头结点并将其next域设置为空。-InitList(L)
void InitList(LNode * &L)
{
L = (LNode *)malloc(sizeof(LNode)); //创建头结点
L->next = NULL; //其next域置为NULL
}
程序分析:
- 头结点的数据域可以不设任何信息,因此初始化时只对指针域赋值,不对数据域赋值。
不带头结点
void InitList(LNode * &L)
{ L = NULL; }
程序分析:
- 其不需要分配结点,直接令头指针为空
建立单链表
- 整体建立单链表的常用方法有两种:
- 头插法——从一个空表开始依次读取元素,生成一个新结点(由s指向它),将读取元素存放到该结点的数据域中,然后将其插入到当前链表的表头上(即头结点之后),直到所有元素读完为止。
- 尾插法——从一个空表开始依次读取元素,生成一个新结点s,将读取的存放到该结点的数据域中,然后将其插人到当前链表的表尾上,直到所有元素读完为止。为此需要增加一个尾指针r,其始终指向当前链表的尾结点,每插人一个新结点后让r指向这个新结点,最后还需要将r所指结点(尾结点)的next域置为空。
采用头插法建立单链表
带头结点
typedef struct LNode //定义单链表结点类型
{
ElemType data; //数据域
struct LNode *next; //指针域
}LNode, * LinkList;
void HeadInsert(LinkList &L) //逆向建立单链表
{
LNode *s;
L = (LinkList)malloc(sizeof(LNode)); //创建头结点
L->next = NULL; //初始为空链
for (int i = 1; i <= 5; i++)
{
s = (LNode *)malloc(sizeof(LNode));
s->data = i; //为数据域赋值
s->next = L->next;
L->next = s;
}
}
int main()
{
LNode *L;
HeadInsert(L); //调用函数,采用头插法建立单链表
L = L->next; //指向链表中的第一个结点
while (L)
{
printf("%d ", L->data);
L = L->next; //指针后移
}
return 0;
}
//输出结果:5 4 3 2 1
程序分析:
- 时间复杂度:O(n);空间复杂度:O(1)
不带头结点
void HeadInsert(LinkList &L) //逆向建立单链表
{
LNode *s;
int i;
for (i = 1; i <= 5; i++)
{
s = (LNode *)malloc(sizeof(LNode)); //创建新结点
s->data = i;
if (L == NULL)
{
L = s;
s->next = NULL;
}
else
{
s->next = L;
L = s;
}
}
}
采用尾插法建立单链表
带头结点
void TailInsert(LinkList &L)
{
L = (LinkList)malloc(sizeof(LNode));
L->next = NULL;
LNode *s, *r = L; //r为表尾指针
for (int i = 1; i <= 5; i++)
{
s = (LNode *)malloc(sizeof(LNode));
s->data = i;
r->next = s;
r = s; //r指向新的表尾结点
}
r->next = NULL; //尾结点指针置空
}
TailInsert(L); 调用函数,采用头插法建立单链表
//输出结果为:1 2 3 4 5
不带头结点
#include<stdio.h>
#include<malloc.h>
typedef int ElemType;
typedef struct LNode
{
ElemType data;
struct LNode* next;
}LNode, * LinkList;
void TailInsert(LinkList &L)
{
ElemType num;
L = (LinkList)malloc(sizeof(LNode));
scanf("%d", &num);
L->data = num;
L->next = NULL;
LNode* s, * r = L; //r为表尾指针
scanf("%d", &num);
while(num!=999)
{
s = (LNode*)malloc(sizeof(LNode));
s->data = num;
s->next = r->next;
r->next = s;
r = s;
scanf("%d", &num);
}
}
int main()
{
LNode *L;
TailInsert(L);
while (L != NULL)
{
printf("%d ", L->data);
L = L->next;
}
}
程序分析:
- 因为附设了一个指向表尾结点的指针,故时间复杂度和头插法的相同。
- 时间复杂度:O(n);空间复杂度:O(1)
销毁线性表(时间复杂度:O(n))
该运算释放单链表L占用的内存空间,即逐一释放全部结点的空间。其过程是让pre、p指向两个相邻的结点(初始时pre指向头结点,p指向首结点)。当p不为空时循环:释放结点pre,然后pre、p同步后移一个结点。循环结束后,pre指向尾结点,再将其释放。-DestoryList(L)
void DestoryList(LNode * &L)
{
LNode *pre = L, *p = L->next; //pre指向结点p的前驱结点
while (p != NULL) //扫描单链表L
{
free(pre); //释放free结点
pre = p; //pre、p同步后移一个结点
p = pre->next;
}
free(pre); //循环结束时,p为NULL,pre指向尾结点,释放它
}
判断单链表是否为空表(时间复杂度:O(1))
该运算在单链表L中没有数据结点时返回真,否则返回假。-ListEmpty(L)
带头结点
bool ListEmpty(LNode * &L)
{ return (L->next == NULL); }
不带头结点
bool ListEmpty(LNode * &L)
{ return (L == NULL); }
求单链表的长度(时间复杂度:O(n))
带头结点
该运算返回单链表L中数据结点的个数。由于单链表没有存放数据结点个数的信息,
需要通过遍历来统计。其过程是让p指向头结点,n用来累计数据结点个数(初始值为0)当p不为空时循环:n增1,p指向下一个结点,循环结束后返回n。-ListLength(L)
int ListLength(LinkList &L)
{
int n = 0;
LNode *p=L; //p指向头结点,n置为0(即头结点的序号为0)
while (p->next!=NULL)
{
n++;
p = p->next;
}
return n; //循环结束,p指向尾结点,其序号n为结点个数
}
程序分析:
求表长操作就是计算单链表中的数据结点(不含头结点)的个数。
不带头结点
int ListLength(LinkList &L)
{
int n = 0;
LNode *p=L; //p指向头结点,n置为0(即头结点的序号为0)
while (p!=NULL)
{
n++;
p = p->next;
}
return n; //循环结束,p指向尾结点,其序号n为结点个数
}
输出单链表(时间复杂度:O(n))
带头结点
void DispList(LNode * &L)
{
LNode *p = L->next; //p指向首结点
while (p!=NULL) //p不为NULL时
{
printf("%d ", p->data); //循环输出p结点的data域
p = p->next; //p移向下一个结点
}
}
不带头结点
void DispList(LinkList &L) //输出线性表
{
LNode *p = L; //p指向首结点
while (p != NULL)
{
cout<<p->data<<' '; //输出p结点的data域
p = p->next; //p移向下一个结点
}
}
求单链表中的某个数据元素值(时间复杂度:O(n))
带头结点
在单链表L中从头开始找到第i个结点,若存在第i个数据结点,则将其data域值赋给变量e。其过程是让p指向头结点,j用来累计遍历过的数据结点个数(初始值为0),当j<i且p不为空时循环:j增1,p指向下一个结点。
循环结束后有两种情况,若p为空,表示单链表L中没有第i个数据结点(参数i错误),返回 false;否则找到第i个数据结点,提取它的值并返回true。-Getelem(L,i,&e)
bool GetElem(LNode * &L, int i, ElemType &e)
{
LNode *p = L; //p指向头结点,j置0(即头结点的序号为0)
int j = 0;
if (i <= 0) return false; //i错误返回假
while (p!= NULL && j < i)
{
j++;
p = p->next; //p移向下一个结点
}
if (p==NULL) return false; //不存在第i个数据结点,返回false
else //存在第i个数据结点,返回true
{
e=p->data;
return true;
}
}
不带头结点
bool GetElem(LinkList& L, int i, ElemType& e) //求线性表中的某个数据元素值
{
LNode* p = L; //p指向头结点
int j = 1; //j置1(即头结点的序号为1)
if (i <= 0) return false; //i错误返回假
while (p!= NULL && j < i)
{
j++;
p = p->next; //p移向下一个结点
}
if (p == NULL) return false;//不存在第i个数据结点,返回false
else //存在第i个数据结点,返回true
{
e = p->data;
return true;
}
}
按元素值查找(时间复杂度:O(n))
带头结点
在单链表L中从头开始找第一个值域与e相等的结点,若存在这样的结点,则返回逻辑序号,否则返回0。LocateElem(L,e)
int LocateElem(LNode * &L, ElemType e)
{
LNode *p = L->next; //p指向首结点,i置为i(即首结点的序号为1)
ElemType i = 1;
while (p != NULL && p->data != e) //查找data值为e的结点,其序号为i
{
p = p->next; //p移向下一个结点
i++;
}
if (p == NULL) return 0; //不存在值为e的结点,返回0
else return i; //存在值为e的结点,返回其逻辑序号i
}
不带头结点
bool LocateElem(LinkList &L, ElemType e) //按元素值查找
{
LNode *p = L; //p指向首结点
int i = 1; //i置为i(即首结点的序号为1)
while (p != NULL && p->data != e) //查找data值为e的结点,其序号为i
{
i++;
p = p->next; //p移向下一个结点
}
if (p == NULL) return 0; //不存在值为e的结点,返回0
else return i; //存在值为e的结点,返回其逻辑序号i
}
插入数据元素(时间复杂度:O(n))
带头结点
先在单链表L中找到第iー1个结点,由p指向它。若存在这样的结点,将值为e的结点(s指向它)插入到p所指结点的后面。-ListInsert(&L,i,e)
bool ListInsert(LNode *L, int i, ElemType e)
{
LNode * p = L, * s; //p指向头结点,j置为0(即头结点的序号为0)
int j = 0;
if (i <= 0) return false; //i错误返回false
while (p != NULL && j < i-1) //查找第i-1个节点
{
j++;
p = p->next; //p移向下一个结点
}
if (p == NULL) //未找到第i-1个结点,返回false
return false;
else //找到第i-1个结点p,插入新结点并返回true
{
s = (LNode *)malloc(sizeof(LNode));
s->next = p->next;
s->data = e; //创建新结点s,其data域置为e
p->next = s; //将结点s插入到结点p之后
return true; //返回true表示成功插入第i个节点
}
}
不带头结点
if (i == 1) //首结点前插入元素
{
s = (LNode *)malloc(sizeof(LNode)); //开辟结点空间
s->data = e;
s->next = L;
L = s;
return true;
}
程序分析:
- 由于单链表不带头结点,不存在“第0个”结点,因此i=1时需要特殊处理。插入、删除第1个元素时,需要更改头指针L。i≠1时的逻辑和带头结点的一样。
- 如果要在不带头结点的第一个元素之前插入一个新元素,先开辟一个结点空间(s),其数据域里面的值为e,让s指向首结点(此时s变成了首结点),再让L指向s。
删除数据元素(时间复杂度:O(n))
先在单链表L中找到第iー1个结点,由p指向它。若存在这样的结点,且也存在后继结点(由p指向它),则删除q所指的结点,返回true;否则返回false,表示参数i错误。
带头结点
bool ListDelete(LNode*& L, int i, ElemType &e)
{
LNode* p = L, * q; //p指向头结点,j置为0(即头结点的序号为0)
int j = 0;
if (i <= 0) return false; //i错误返回false
while (p != NULL && j < i - 1) //查找第i-1个节点
{
j++;
p = p->next; //p移向下一个结点
}
if (p == NULL) return false; //未找到第i-1个结点,返回false
else //找到第i-1个结点p
{
q = p->next; //q指向第i个结点
if (q == NULL) //若不存在第i个结点,返回false
return false;
e = q->data;
p->next = q->next; //从单链表中删除q结点
free(q); //释放q结点
return true; //返回true表示成功删除第i个节点
}
}
不带头结点
if (i == 1) //删除首结点
{
L = L->next;
free(p);
return true;
}
程序分析:
- 如果要删除不带头结点中的第一个元素,首先让p指向首结点,然后L后移一个结点,最后释放p。
指定结点的后插操作(时间复杂度:O(1))
在指定结点后插入一结点,此时它的后驱结点是已知的。-InsertNextNode
bool InsertNextNode(LNode *p, ElemType e)
{
LNode *s;
if (p == NULL) return false;
s = (LNode *)malloc(sizeof(LNode)); //开辟结点空间
if (s == NULL) return false; //某些情况下有可能分配失败(如内存不足)
s->data = e; //用结点s保存数据元素e
s->next = p->next; //s指向p
p->next = s; //将结点s连接到p后
return true;
}
指定结点的前插操作(时间复杂度:O(1))
在指定结点前插入一结点,无法知晓它的前驱结点。-InsertPriorNode
bool InsertPriorNode(LNode *p, ElemType e)
{
LNode *s;
if (p == NULL) return false;
s = (LNode*)malloc(sizeof(LNode));
if (s == NULL) return false; //内存分配失败
s->data = p->data; //将p中元素赋值到s中
p->data = e; //p中元素置为e
s->next = p->next;
p->next = s; //新节点s连接到p之后
return true;
}
程序分析
指定结点的前插操作有两种方法:
- 传入头指针,循环查找p的前驱q,再对q后插。
- 后插,然后交换数据域中的值,并且使指针的指向发生改变。