2.3.1 单链表
每个节点除了存放数据元素外,还要存储指向下一个节点的指针
优点:不要求大片连续空间,改变容量方便
缺点:不可随机存取,要耗费一定空间存放指针
带头结点
操作详解
-
定义创建一个单链表
typedef struct LNode { //定义单链表结点类型 int data; //每个节点存放一个数据元素 struct LNode* next; //指针指向下一个节点 指向结构体类型为LNode的指针变量next }LNode, *LinkList; //struct LNode* next这意味着每个Node结构体的next成员可以指向另一个Node结构体,从而形成链表或其他递归结构。 int main(){ LinkList L; return 0; }
-
关于LNode和*LinkList
//上述代码相当于 struct LNode { int data; struct LNode* next; }; typedef struct LNode LNode; typedef struct LNode* LinkList; //这是一个指向struct LNode类型的指针 //注意LNode* LinkList 这样写更严谨一些;将LNode* 重新命名为LinkList /* 创建单链表的头指针L(L就是一个),然后指向头结点(俗称第0个结点)。第0个结点不保存数据,但是头结点的next指针域指向下一个节点。然后一个个链接起来 */
-
问题一:为什么要将LNode* 重新命名为LinkList?
- LNode* 等价与 LinkList
- LinkList 强调这是一个单链表或者头结点;
LinkList L;
LNode* 强调这是一个结点;LNode *p
-
问题二:为什么创建单链表使用
LinkList L
,而不使用LNode L
-
在创建单链表时,使用LinkList L 是为了方便对链表进行操作;LinkList是指向结点的指针类型的别名,而不是具体的结点类型。
-
如果使用LNode L 来表示链表,L就是具体的结点类型的变量,而不是指向结点的指针。
使用LinkList L,可以通过修改L的值来改变链表的头指针,方便操作整个链表。
-
-
内存状态
-
-
初始化单链表
// 初始化一个单链表 bool InitList(LinkList *L) { *L = (LNode*)malloc(sizeof(LNode)); /* L是指向指针的指针,表示传入的参数是一个指向链表头指针的指针 当分配内存失败时,L的值为NULL,表示没有足够的内存来创建链表头结点*/ if (*L == NULL) // 内存不足,分配失败 return false; (*L)->next = NULL; // 头结点下一个指向NULL 防止脏数据。每次初始化,都设置,养成好习惯 return true; } //对于这个地方bool InitList(LinkList* L),这样写bool InitList(LinkList *L)要好一些,便于理解 //相当于LNode* *L,所以可以直接用(*L)->data,来指向结构体里面内容,注意.和->的用法。复杂写法(**L).data
-
问题一:如何理解
*L = (LNode*)malloc(sizeof(LNode));
-
相当于是先申请了一个头结点(第0个结点)
-
头指针L 指向第0个结点
//等同于 bool InitList(LinkList* L) { LNode* p = (LNode*)malloc(sizeof(LNode)); //申请的头结点为p if (L == NULL) // 内存不足,分配失败 return false; *L = p; //头指针指向头结点p (*L)->next = NULL; // 将结点中的next指针域设置为NULL return true; }
-
-
问题二:为什么传入参数是
LinkList *L
- 在c语言中没用引用,main函数中初始化单链表得传入头指针L的地址
LinkList(&L)
- 在c语言中没用引用,main函数中初始化单链表得传入头指针L的地址
-
-
按位插入
插入操作,在表L中的第i个位置上插入指定元素e
//按位序插入 /*在第i个位置插入元素e 1.循环找到i的前一个结点,此时p为目标位置的前一个结点 2.申请一个节点s,依次修改指针 */ bool ListInsert(LinkList *L, int i, int e){ if(i<1) return false; LNode* p; //指针p指向当前扫描的结点 int j = 0; //当前p指向的是第几个结点 p = *L; //L指向头结点,头结点是第0个结点;p指向第0个结点 // LNode* p = *L; 熟练了可以直接这么写,让p直接指向头结点 while(p!=NULL&& j<i-1){ p = p->next; j++; } if(p==NULL) return false; LNode* s = (LNode*)malloc(sizeof(LNode)); s->data = e; s->next = p->next; //s的next指针域 指向 p的next指针域所指向的结点 p->next = s; return true; }
-
问题一:为什么要找到前一个结点
为了修改前一个结点的next指针指向位置,让他能够指向我插入的地方
-
问题二:修改步骤1和2反了会出现什么
由于p的next指针域先指向了s,导致
s->next = p->next;
s指向了自己
-
-
指定结点后插操作
在结点p之后,插入新的元素e(实际上将按位插入给拆开了)
// 后插操作:在p结点之后插入元素e bool InsertNextNode(LNode* p, int e) { if (p == NULL) return false; LNode* s = (LNode*)malloc(sizeof(LNode)); if (s == NULL) return false; s->next = p->next; p->next = s; s->data = e; return true; } bool ListInsert(LinkList* L, int i, int e) { if (i < 1) return false; //首先还是先找到结点p LNode* p; int j = 0; p = *L; while (p != NULL && j < i - 1) { p = p->next; j++; } return InsertNextNode(p, e); }
-
问题一:为什么传参使用LNode *p 不用LNode **p
这是因为链表的插入操作需要修改指针的指向,在第一种写法中,通过§->next来访问p指针所指向的结点的next指针域,p指针本身并没有被修改;而在第二种写法中,通过传递指针p的地址来修改p指针所指向的结点的next指针域
-
问题二:为什么 要写判断这个 if (p == NULL) return false;
如果传入的的p指向空链表L,L为空,p也为空,无法插入
-
-
指点结点前插操作
在结点p之前插入 元素e
采用方位为后插,然后交换数据
//指定结点前插操作 //由于是在p结点之前插入,找不到前驱结点(当然也可以传入单链表来找前驱结点,那么时间复杂度明显较高) //采用方式为,正常的后插操作,然后将p和s的元素进行互换,即完成前插操作 bool InsertPriorNode(LNode* p,int e){ if(p==NULL) return false; LNode* s = (LNode*)malloc(sizeof(LNode)); if(s==NULL) return false; s->next = p->next; p->next = s; s->data = p->data; p->data = e; return true; }
-
按位删除
删除第i个位置的结点,并将删除的数据返回到e中
//按位删除 bool ListDelete(LinkList *L,int i,int *e){ if(i<1) return false; //还是需要找到前一个结点 LNode* p = *L; int j = 0; while(p!=NULL&&j<i-1){ p = p->next; j++; } if(p==NULL) return false; if(p->next = NULL) return false; //如果p后面一个结点为NULL,那么就删除失败 LNode* q = p->next; //q指向要被删除的结点 *e = q->data; p->next = q->next; free(q); return true; } //最坏、平均时间复杂度:O(n) //最好时间复杂度:O(1)
-
指定结点的删除操作
给出结点p,删除结点中的数据
由于给出结点,没有前驱结点采用方式为:将p变为前驱结点,将后面的数据复制到p中,p的next指针域指向q的next指针域指向的结点,释放q
简单来说,就是交换数据,然后删除后面结点
//指定结点删除 bool DeleteNode(LNode *p){ if(p==NULL||p->next==NULL) return false; LNode* q = p->next; //令q指向p的后继结点 p->data = q->data; //将q结点中的数据给p p->next = q->next; //将q结点从链中断开 free(q); //释放q return true; } //此处删除操作仍存在小问题,对于最后一个结点,采用的方式是直接返回false,也就是说,未能实现删除最后一个结点,如果需要准确删除,需要传入单链表的头结点来一次遍历进行删除,时间复杂度也会相应提高
-
按位查找
返回第i个位置的结点p
// 单链表按位查找 LNode* GetNode(LinkList* L, int i) { if (i < 0) return NULL; LNode* p = *L; int j = 0; while (p != NULL && j < i) { p = p->next; j++; } return p; }
- 问题一:为什么是i<0,不是i<1
- 这是代头结点的单链表,i=0,可以认为是头结点;
- 之前写的按位插入,开头都是要找到第i个结点的前一个结点,那么就可以直接调用这个方法
LNode *p = GetNode(*L, i-1);
当插入的结点在第1个位置时,此时i-1为0 就体现出来了
- 问题一:为什么是i<0,不是i<1
-
按值查找
//按值查找,找到数据域==e 的结点 LNode *LocationElem(LinkList L,int e){ LNode* p = L->next; //从第一个节点开始查找数据域为e的结点 while(p!=NULL&&p->data!=e) p = p->next; return p; //找到后返回该结点指针,否则返回NULL }
-
求表长度
//求表长度 int Length(LinkList L){ int len = 0; LNode* p = L; while(p!=NULL){ p = p->next; len++; } return len; }
-
问题一:为什么是
LNode* p = L;
,不是LNode* p = L->next;
从第一个结点开始算起在下面的while循环中,先让p结点移动一次,然后再让
len++
,所以从第0个结点开始
-
-
建立单链表
尾插法:与按位插入类似,在每个节点后面插入新的节点
// 用尾插法建立单链表 LinkList List_tailInsert(LinkList* L) { int x; // 设element为整形 *L = (LNode*)malloc(sizeof(LNode)); // 建立头结点 LNode *s, *r = *L; scanf("%d", &x); while (x != -1) { s = (LNode*)malloc(sizeof(LNode)); s->data = x; // 申请的新结点s存储数据x r->next = s; // r的next指针域指向s r = s; // r指向s,相当于移到s这边 scanf("%d", &x); } r->next = NULL; //尾结点指针置空 return *L; // 注意:返回的是*L }
头插法:每次在链表的头部进行插入操作建立单链表
// 用头插法建立单链表 LinkList List_HeadInsert(LinkList *L){ LNode* s; int x; *L = (LNode*)malloc(sizeof(LNode)); if (*L == NULL) return NULL; (*L)->next = NULL; //头插法建立,这里需要将L的next指针域指向NULL scanf("%d", &x); while(x!=-1){ s = (LNode*)malloc(sizeof(LNode)); if(s==NULL) break; //申请内存失败 s->data = x; s->next = (*L)->next; (*L)->next = s; scanf("%d", &x); } return *L; }
完整代码
// 带头结点的单链表
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
typedef struct LNode {
int data;
struct LNode* next;
} LNode, *LinkList;
// 初始化一个单链表
bool InitList(LinkList *L) {
*L = (LNode*)malloc(sizeof(LNode));
/* L是指向指针的指针,表示传入的参数是一个指向链表头指针的指针
当分配内存失败时,L的值为NULL,表示没有足够的内存来创建链表头结点*/
if (*L == NULL) // 内存不足,分配失败
return false;
(*L)->next = NULL; // 头结点之后暂时还没有结点
return true;
}
bool Empty(LinkList* L) {
return (*L)->next == NULL;
}
// 后插操作:在p结点之后插入元素e
bool InsertNextNode(LNode* p, int e) {
if (p == NULL)
return false;
LNode* s = (LNode*)malloc(sizeof(LNode));
if (s == NULL)
return false;
s->next = p->next;
p->next = s;
s->data = e;
return true;
}
/*在第i个位置插入元素e(带头结点)
1.循环找到i的前一个结点,p指向他的next指针域,p=p->next
2.申请一个节点s,依次修改指针
*/
// 按位序插入(带头结点)
bool ListInsert(LinkList* L, int i, int e) {
if (i < 1)
return false;
LNode* p; // 指针p指向当前扫描的结点
int j = 0; // 当前p指向的是第几个结点
p = *L; // L指向头结点,头结点是第0个结点;
while (p != NULL && j < i - 1) {
p = p->next;
j++;
}
/* if(p==NULL)
return false;
LNode* s = (LNode*)malloc(sizeof(LNode));
s->data = e;
s->next = p->next;
p->next = s;
return true; */
return InsertNextNode(p, e);
}
// 前插操作:在p结点之前插入元素e
bool InsertPriorNode(LNode* p, int e) {
if (p == NULL)
return false;
LNode* s = (LNode*)malloc(sizeof(LNode));
if (s == NULL)
return false; // 内存分配失败
s->next = p->next;
p->next = s; // 将新结点s连到p之后
s->data = p->data; // 将p中元素复制到s中
p->data = e; // p中元素覆盖为e
return true;
}
// 指定结点删除
// 因为没有前一个结点,采用方式:申明指针q指向p的后继结点,然后夺舍p,释放q
bool DeleteNode(LNode* p) {
if (p == NULL||p->next==NULL)
return false;
LNode* q = p->next; // 令q指向*p的后继结点
p->data = p->next->data; // 将后继结点夺舍*p结点
p->next = q->next; // 将*q结点从链中断开
free(q); // 释放q
return true;
}
// 按位序删除(带头结点)
bool ListDelete(LinkList* L, int i, int* e) {
if (i < 1)
return false;
LNode* p; // 当前p指向当前扫描的结点
int j = 0; // 当前p指向第几个结点
p = *L; // p指向第0个结点
while (p != NULL && j < i - 1) {
// 循环找到第i-1个结点
p = p->next;
j++;
}
if (p == NULL)
return false; // i值不合法(超了)
if (p->next == NULL)
return false;
LNode* q = p->next; // q指向要被删除的结点,即p的下一个结点
*e = q->data; // 返回e的数值
p->next = q->next; // q指向的结点直接断开
free(q);
return true;
}
// 按位查找,返回第i个结点,带头结点
LNode* GetElem(LinkList L, int i) {
if (i < 0)
return NULL;
LNode* p;
int j = 0;
p = L;
while (p != NULL && j < i) {
// 循环找到第i个结点
p = p->next;
j++;
}
return p;
}
// 按值查找,找到数据域==e 的结点
LNode* LocationElem(LinkList L, int e) {
LNode* p = L->next;
// 从第一个节点开始查找数据域为e的结点
while (p != NULL && p->data != e)
p = p->next;
return p; // 找到后返回该结点指针,否则返回NULL
}
// 求表的长度
int Length(LinkList L) {
int len = 0;
LNode* p = L;
while (p != NULL) {
p = p->next;
len++;
}
return len;
}
// 用尾插法建立单链表
LinkList List_tailInsert(LinkList* L) {
int x; // 设element为整形
*L = (LNode*)malloc(sizeof(LNode)); // 建立头结点
LNode *s, *r = *L;
scanf("%d", &x);
while (x != -1) {
s = (LNode*)malloc(sizeof(LNode));
s->data = x; // 申请的新结点s存储数据x
r->next = s; // r的next指针域指向s
r = s; // r指向s,相当于移到s这边
scanf("%d", &x);
}
r->next = NULL; //尾结点指针置空
return *L; // 注意:返回的是*L
}
// 用头插法建立单链表
LinkList List_HeadInsert(LinkList *L){
LNode* s;
int x;
*L = (LNode*)malloc(sizeof(LNode));
if (*L == NULL)
return NULL;
(*L)->next = NULL; //头插法建立,这里需要将L的next指针域指向NULL
scanf("%d", &x);
while(x!=-1){
s = (LNode*)malloc(sizeof(LNode));
if(s==NULL)
break;
s->data = x;
s->next = (*L)->next;
(*L)->next = s;
scanf("%d", &x);
}
return *L;
}
// 输出单链表中的元素
void printList(LinkList L) {
LNode* p = L->next; // p此时指向第1个结点(跳过头结点)
int j = 1;
if (L == NULL)
return; // 如果链表为空,则直接返回
while (p != NULL) {
// 遍历单链表
printf("第%d个节点为:%d\n", j, p->data);
p = p->next; // 移动到下一个节点
j++;
}
}
int main() {
LinkList L;
List_HeadInsert(&L);
// List_tailInsert(&L);
ListInsert(&L, 2, 1314);
ListInsert(&L, 3, 520);
ListInsert(&L, 4, 0);
int e;
ListDelete(&L, 4, &e);
printf("被删除数据为%d\n", e);
printList(L);
system("pause");
}
不带头结点
-
创建与初始化
typedef struct LNode { int data; struct LNode* next; }LNode,*LinkList; //初始化 bool InitList(LinkList *L){ *L = NULL; return true; }
-
按位插入
对于不带头结点,由于没有头结点,第一个插入需要单独处理
//按位插入 bool ListInsert(LinkList *L,int i,int e){ if(i<1) return false; if(i==1){ LNode* s = (LNode*)malloc(sizeof(LNode)); if(s==NULL) return false; s->data = e; s->next = *L; *L = s; return true; } LNode* p = *L; // p指向当前扫描的结点 int j = 1; // 此时指向第一个节点 while(p!=NULL&&j<i-1){ p = p->next; j++; } if(p==NULL) return false; LNode* s = (LNode*)malloc(sizeof(LNode)); s->data = e; s->next = p->next; p->next = s; return true; }
-
问题一:为什么需要单独处理第一个插入位置
从方法看,插入是需要用p来找到前一个结点,由于第一个位置前面没有结点,所以需要修改头指针的指向来进行插入;而带头结点的单链表头指针始终指向于头结点,所以p可以直接找到
-
-
按位删除
根据位置,删除结点,同样需要对第一个节点进行单独处理
// 按位删除 bool ListDelete(LinkList* L, int i, int* e) { if (i < 1) return false; if (i == 1) { LNode* q = *L; //q指向头指针所指向的结点,即第一个结点 *L = q->next; //头指针指向 q的next指针域所指向的结点,如果没有第二个结点则为NULL *e = q->data; free(q); return true; } // 与之前删除相同 LNode* p = *L; int j = 1; while (p != NULL && j < i - 1) { p = p->next; j++; } // 检查第 i-1 个结点以及第 i 个节点是否存在 if (p == NULL || p->next == NULL) return false; LNode* q = p->next; p->next = q->next; *e = q->data; free(q); return true; }
具体操作会在栈和队列中,不带头结点用的较多