① 单链表:用 链式存储 实现了线性结构。一个结点存储一个数据元素,各结点间的前后关系用一个指针表示。
② 特点:
优点:不要求大片连续空间,改变容量方便。
缺点:不可随机存取,要耗费一定空间存放指针。
③ 两种实现方式:
【1】带头结点,写代码更方便。头结点不存储数据,头结点指向的下一个结点才存放实际数据。
【2】不带头结点,麻烦。
对第一个数据结点与后续数据结点的处理需要用不同的代码逻辑
对空表和非空表的处理需要用不同的代码逻辑。
"LinkList"等价于"LNode * "前者强调这是链表,后者强调这是结点,合适的地方使用合适的名字,代码可读性更高
不带头结点的单链表 :
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
//初始化一个空的单链表
bool InitList(LinkList &L){
L = NULL; //空表,暂时还没有任何结点
return true;
}
void test(){
LinkList L; //声明一个指向单链表的头指针
//初始化一个空表
InitList(L);
...
}
//判断单链表是否为空
bool Empty(LinkList L){
return (L==NULL)
}
带头结点的单链表 :
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
// 初始化一个单链表(带头结点)
bool InitList(LinkList &L){
L = (LNode *)malloc(sizeof(LNode)); //分配一个头结点
if (L == NULL) //内存不足,分配失败
return false;
L->next = NULL; //头结点之后暂时还没有结点
return true;
}
void test(){
LinkList L; //声明一个指向单链表的头指针
//初始化一个空表
InitList(L);
...
}
//判断单链表是否为空
bool Empty(LinkList L){
if (L->next == NULL)
return true;
else
return false;
}
// 计算单链表的长度
int Length(LinkList L){
int len=0; //统计表长
LNode *p = L;
while(p->next != NULL){
p = p->next;
len++;
}
return len;
}
时间复杂度: O(n)
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
// 查找指定位序i的结点并返回
LNode * GetElem(LinkList L, int i){
if(i<0)
return NULL;
LNode *p;
int j=0;
p = L;
while(p!=NULL && j<i){
p = p->next;
j++;
}
return p;
}
时间复杂度:
- 最好时间复杂度: O(1)
- 最坏时间复杂度: O(n)
- 平均时间复杂度: O(n)
// 查找数据域为e的结点指针,否则返回NULL
LNode * LocateElem(LinkList L, ElemType e){
LNode *P = L->next;
// 从第一个结点开始查找数据域为e的结点
while(p!=NULL && p->data != e){
p = p->next;
}
return p;
}
时间复杂度:
- 最好时间复杂度: O(1)
- 最坏时间复杂度: O(n)
- 平均时间复杂度: O(n)
思路1:如果传入头指针,就可以循环整个链表找到指定结点p
的前驱结点q,再对q进行后插操作;
bool InsertPriorNode (LinkList L, LNode *p, ElemType e)
思路2:如果不传入头指针,在指定结点p后插入一个结点s,并交换两个结点所保存的数据,从而变相实现指定结点的前插操作
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
// 在结点p前插入元素e
bool InsertPriorNode(LNode *p, ElemType e){
if(p==NULL)
return false;
LNode *s = (LNode *)malloc(sizeof(LNode));
// 内存不足分配失败
if(s==NULL)
return false;
// 将s插入结点p之后
s->next = p->next;
p->next = s;
// 交换两个结点中的数据
s->data = p->data;
p->data = e;
return true;
}
把前面的函数用上:
思路:如果传入头指针,就可以循环整个链表找到指定结点p的前驱结点q,再对p进行删除操作
typedef struct LNode{
ElemType data;
struct LNode *next;}LNode, *LinkList;
// 删除第i个结点并将其所保存的数据存入e
bool ListDelete(LinkList &L, int i, ElemType &e){
if(i<1)
return false;
LNode *p; //指针p指向当前扫描到的结点
int j=0; //当前p指向的是第几个结点
p = L;
//循环找到第i-1个结点
while(p!=NULL && j<i-1){
//如果i>lengh,p和p的后继结点会等于NULL
p = p->next;
j++;
}
if(p==NULL)
return false;
if(p->next == NULL)
return false;
//令q暂时保存被删除的结点
LNode *q = p->next;
e = q->data;
p->next = q->next;
free(q)
return true;
}
时间复杂度:
- 最好时间复杂度: O(1)
- 最坏时间复杂度: O(n)
- 平均时间复杂度: O(n)
思路:
如果不传入头指针,可以把指定结点p的后继结点q删除,
并使结点p保存结点q存储的数据,从而变相实现删除指定结点的操作。
但是 如果指定结点p没有后继结点,这么做会报错
// 删除指定结点p
bool DeleteNode(LNode *p){
if(p==NULL)
return false;
LNode *q = p->next; // 令q指向p的后继结点
// 如果p是最后一个结点,则q指向NULL,继续执行就会报错
p->data = q->data;
p->next = q->next;
free(q);
return true;
}
时间复杂度: O(1)
// 使用尾插法建立单链表L
LinkList List_TailInsert(LinkList &L){
int x; //设ElemType为整型int
L = (LinkList)malloc(sizeof(LNode)); //建立头结点(初始化空表)
LNode *s, *r = L; //r为表尾指针
scanf("%d", &x); //输入要插入的结点的值
while(x!=9999){ //输入9999表示结束
s = (LNode *)malloc(sizeof(LNode));
s->data = x;
r->next = s;
r = s; //r指针指向新的表尾结点
scanf("%d", &x);
}
r->next = NULL; //尾结点指针置空
return L;
}
时间复杂度: O(n)
LinkList List_HeadInsert(LinkList &L){ //逆向建立单链表
LNode *s;
int x;
L = (LinkList)malloc(sizeof(LNode)); //建立头结点
L->next = NULL; //初始为空链表,这步很关键
scanf("%d", &x); //输入要插入的结点的值
while(x!=9999){ //输入9999表结束
s = (LNode *)malloc(sizeof(LNode));
s->data = x;
s->next = L->next;
L->next = s;
//将新结点插入表中,L为头指针
scanf("%d", &x);
}
return L;
}
时间复杂度: O(n)
// 将链表L中的数据逆置并返回
LNode *Inverse(LinkList L){
LNode *p, *r;
p = L->next; //p指针指向第一个结点
L->next = NULL; //断链
// 依次判断p结点中的数据并采用头插法插到L链表中
while (p != NULL){
r=p->next;
p->next=L->next;
L->next=p;
p=r;
}
return L;
}
具体解释详见:单链表逆置图解