408线性表【跨考小白学习笔记1】——单链表定义与基本操作 (包含指针的使用)
直接看指针部分:
点击跳到指针部分
线性表的链式表示-单链表
1. 定义数据类型(头结点)
typedef struct LNode { //定义单链表结点类型
Elemtype data; //每个结点存放一个数据元素
struct LNode* next; // 指针指向下一个结点
} LNode, *LinkList; // Linklist,单链表
//LinkList是用typedef定义的自定义链表类型,凡是用该类型定义的变量都是指针,不需要加*号定义,而用Linklist head与TYPE *head其实都是指链表的头指针,没有多大的区别
LinkList是用typedef定义的自定义链表类型,凡是用该类型定义的变量都是指针,不需要加*号定义,而用Linklist head与TYPE *head其实都是指链表的头指针,没有多大的区别
点击跳回指针
2. 单链表的定义
2.1 初始化
王道的代码(c++):
bool InitList(LinkList &L) {
L = (LNode*)malloc(sizeof(LNode)); //分配一个头结点
if (L == NULL)
return false; //分配失败
L->next = NULL; //头结点后暂时没有结点
return true;
}
修改后的c语言代码:
//初始化一个单链表(带头结点)
bool InitList(LinkList* L) {
*L = (LNode*)malloc(sizeof(LNode)); //分配一个头结点
if (*L == NULL)
return false; //分配失败
(*L)->next = NULL; //头结点后暂时没有结点
return true;
}
3. 单链表的基本操作
3.1 建立
3.1.1 头插法
逆向,每次把新的插在表头
王道的代码是这样的:(只建立,所以会new一个新的头结点给到原来的头指针)
更改后的c语言代码
//头插法建立单链表(逆向,每次都把新的插在最前面)
LinkList List_headInsert(LinkList* L) {
LNode* s;
Elemtype x;
// *L = (LinkList )malloc(sizeof(LNode)); //创建头结点
// (*L)->next = NULL; //error:Thread 1: EXC_BAD_ACCESS (code=1, address=0x8)
// 由于初始化将头结点的next已赋为null,解引用访问了已被释放的首元节点,出现野指针错误
//当某个对象被完全释放,再去通过该对象去调用其它的方法就会出现野指针错误。
printf("请输入元素值,按9999退出输入");
scanf("%d", &x);
while (x != 9999) {
s = (LNode*)malloc(sizeof(LNode)); //创建新结点
s->data = x;
s->next = (*L)->next;
(*L)->next = s;
printf("请输入元素值插入表头,按9999退出输入");
scanf("%d", &x);
}
return *L; //返回头结点
}
3.1.2 尾插法
正向,每次把新的插在末尾
王道的代码是这样的:(只建立,所以会new一个新的头结点给到原来的头指针)
//尾插法建立单链表(正向,每次把新的插在末尾)
LinkList List_TailInsert(LinkList &L) {
int x;
L = (LinkList)malloc(sizeof(LNode));
LNode *s,*r=L;
printf("请输入元素值插入表尾,按9999退出输入");
scanf("%d", &x);
while (x != 9999) {
s = (LNode*)malloc(sizeof(LNode)); //创建新结点
s->data = x;
r->next = s;
r = s; // r指向新的表尾结点
printf("请输入元素值插入表尾,按9999退出输入");
scanf("%d", &x);
}
r->next = NULL; //尾结点指针置空
return L;
}
完成更改后的c语言程序:(包括对已存在的表进行尾插)
#define Elemtype int //这里不用加分号
//尾插法建立单链表(正向,每次把新的插在末尾)
LinkList List_TailInsert(LinkList* L) {
Elemtype x;
// *L = (LinkList)malloc(sizeof(LNode));//这句要注释掉因为之前使用的L初始化后再次分配一个头结点将会使之前建立的结点都抛弃掉,重新弄一个新表
//如果是需要new一个新表可以不注释
LNode *s, *r = GetElem(*L, ListLength(*L));//遍历两遍,但还是O(N)
printf("请输入元素值插入表尾,按9999退出输入");
scanf("%d", &x);
while (x != 9999) {
s = (LNode*)malloc(sizeof(LNode)); //创建新结点
s->data = x;
r->next = s;
r = s; // r指向新的表尾结点
printf("请输入元素值插入表尾,按9999退出输入");
scanf("%d", &x);
}
r->next = NULL; //尾结点指针置空
return *L;
}
3.1.3 随机数头插法创建(源码出自大话数据结构)
/* 随机产生n个元素的值,建立带表头结点的单链线性表L(头插法) */
void CreateListHead(LinkList* L, int n) {
LinkList p;
int i;
srand(time(0)); /* 初始化随机数种子 */
// *L = (LinkList)malloc(sizeof(LNode));
// (*L)->next = NULL; /* 先建立一个带头结点的单链表
// */
for (i = 0; i < n; i++) {
p = (LinkList)malloc(sizeof(LNode)); /* 生成新结点 */
p->data = rand() % 100 + 1; /* 随机生成100以内的数字 */
p->next = (*L)->next;
(*L)->next = p; /* 插入到表头 */
}
}
3.1.4 随机数尾插法创建(源码出自大话数据结构)
/* 随机产生n个元素的值,建立带表头结点的单链线性表L(尾插法) */
void CreateListTail(LinkList * L, int n) {
LinkList p, r;
int i;
srand(time(0)); /* 初始化随机数种子 */
*L = (LinkList)malloc(sizeof(Node)); /* L为整个线性表 */
r = *L; /* r为指向尾部的结点 */
for (i = 0; i < n; i++) {
p = (Node*)malloc(sizeof(Node)); /* 生成新结点 */
p->data = rand() % 100 + 1; /* 随机生成100以内的数字 */
r->next = p; /* 将表尾终端结点的指针指向新结点 */
r = p; /* 将当前的新结点定义为表尾终端结点 */
}
r->next = NULL; /* 表示当前链表结束 */
}
3.2 查找或取值
3.2.1 按序号查找结点值
最好时间复杂度为 O ( 1 ) O(1) O(1):即 i = 1 i=1 i=1时
最坏时间复杂度 O ( N ) O(N) O(N):即 i = n i=n i=n时
平均复杂度 O ( N ) O(N) O(N)
代码
//按序号取值
LNode* GetElem(
LinkList L,
int i) { // LNode *强调返回的是一个结点;Linklist L强调是一个单链表
int j = 1;
LNode* p =
L->next; // p和next一样都指向的是下一个结点,结点包含数据域和指针域
//此时p存放的是下一个结点的地址
if (i == 0)
return L; // 返回头结点
if (i < 1)
return NULL; // 由于i此时不等于0,输入则为负数,故返回NULL
while (p != NULL && j < i) { // next指针不为空即非尾
p = p->next; // p->next 相当于(*p).next
j++;
}
return p;
}
3.2.2 按值查找表结点
代码:
//按值查找表结点
LNode* LocateElem(LinkList L, Elemtype e) {
LNode* p = L->next;
while (p != NULL && p->data != e) {
p = p->next;
}
return p; //找到后返回该结点,否则返回NULL
}
3.2.3 遍历链表并输出(源自大话数据结构)
代码:
/* 初始条件:链式线性表L已存在 */
/* 操作结果:依次对L的每个数据元素输出 */
bool ListTraverse(LinkList L) {
LinkList p = L->next;
while (p) {
visit(p->data);//error: visit is invalid in c99,改成printf即可,但无法输出位序
p = p->next;
}
printf("\n");
return true;
}
3.3 插入
3.3.1 后插
最好时间复杂度为 O ( 1 ) O(1) O(1):即 i = 1 i=1 i=1时
最坏时间复杂度 O ( N ) O(N) O(N):即 i = n i=n i=n时
平均复杂度 O ( N ) O(N) O(N)
王道代码:
更改后的c语言代码:
//插入结点(按位序后插)在第i个位置插入
bool Insert_NextNode(LinkList* L, int i, Elemtype e) {
if (i < 1)
return false; //位序不合法
LNode* p = GetElem(*L, i - 1); //找到要插入位置的前一个结点
LNode* s = (LNode*)malloc(
sizeof(LNode)); //要给s分配内存空间,如果不分配的话会报错内存崩溃
s->data = e;
s->next = p->next;
p->next = s;
return true;
}
3.3.2 前插
查找前驱结点再插在后面则时间复杂度为
O
(
N
)
O(N)
O(N)
设待插入结点为*s,将*s插入到*p的前面。我们仍然将*s插入到p的后面,然后将p->data与s->data交换,时间复杂度为
O
(
1
)
O(1)
O(1)
bool InsetPriorNode(LNode* p, Elemtype e) {
Elemtype temp;
if (p == NULL)
return false; //不能在头结点前插入
LNode* s = (LNode*)malloc(sizeof(LNode));
s->data = e;
s->next = p->next; //修改指针域,不能颠倒
p->next = s; //将s连接到p之后
temp = p->data; //交换数据域
p->data = s->data;
s->data = temp;
return true;
}
3.4 删除
3.4.1 按位序删除
//按位序删除(带头结点)
//先检查删除位置的合法性,后查找表中第i-1个结点,即被删结点的前驱结点,再将其删除。
bool Delete_by_order(LinkList* L, int i, Elemtype* e) {
if (i < 1)
return false; //删除位置不合法,不能删头结点
LNode* p = GetElem(*L, i - 1); //指针p为前驱结点
LNode* q = p->next; // q为被删结点
*e = q->data; //用e返回删除的数据
p->next = q->next; //让p结点断开与q的连接并连接第i+1位
free(q); //释放q
return true; //删除成功
}
3.4.2 按结点删除
查找前驱结点再删除该结点则时间复杂度为 O ( N ) O(N) O(N)
与后继结点交换数据并删除后继结点时间复杂度为 O ( 1 ) O(1) O(1)
但如果被删除的是最后一个结点,该种方法不适用,最后一个结点只能找前驱!
//按结点删除
//查找前驱结点则时间复杂度为O(N)
//与后继结点交换数据并删除后继结点时间复杂度为O(1)
bool Delete_LNode(LNode* p, Elemtype* e) {
if (p == NULL||p->next == NULL)
return false; //删除结点不合法;或被删除的是最后一个结点,该种方法不适用,最后一个结点只能找前驱!
*e = p->data; //用e返回删除的数据
LNode* q = p->next; // q为被删结点后继结点
p->next = q->next; //让p结点断开与q的连接并连接第i+1位
free(q); //释放q
return true; //删除成功
}
3.5 求表长
大话数据结构代码:
//获取链表长度
int ListLength(LinkList L) {
int i = 0;
LinkList p = L->next; /* p指向第一个结点 */
while (p) {
i++;
p = p->next;
}
return i;
}
更改后的代码:(增强了健壮性)
如对空表进行访问头结点next操作时将报错code=1,意为访问了已经释放的空间。
//获取链表长度
int ListLength(LinkList L) {
int i = 0;
if (L->next == NULL) {
return 0;
}
LinkList p = L->next; /* p指向第一个结点 */
while (p) {
i++;
p = p->next;
}
return i;
}
*调用和检验
int main() {
LNode* L; //声明一个单链表(头指针)
//此时系统自动分配一个头结点+首元节点,头结点data为0,next指向首元节点;首元节点data为空,next为null
printf("%d\n", Empty(L)); //此时显示非空表,因为头结点L->next不为null
InitList(&L); //初始化一个空表
/*初始化传入的是头指针的地址,形参如果是Linklist
* L,则是一级指针;但我们要改的是头指针指向的头结点,那么要用的是二级指针Linklist
* *L*/
/*问题:为何不可传L->next入一级指针LinkList L?
答:首先next是个指针,指针存放的是下一个结点的地址,指针作为实参传给指针形参,指针形参获得的是下一个结点的地址,更改的是下一个结点内容*/
/*再问:那么可否传入&(L->next)?
再答:这里传入的是存放下一个结点地址的指针next的地址,无法更改头结点data,一级指针也无法更改下一个结点内容*/
printf("%d\n", Empty(L)); //此时显示是空表
L = List_headInsert(&L);
CreateListHead(
&L, 4); //如果该函数建立头结点,则之前输入的值存在的结点被丢弃,下同
CreateListTail(&L, 3);
printf("链表长度为%d\n", ListLength(L));
L = List_TailInsert(&L);
ListTraverse(L); //遍历
Insert_NextNode(&L, 1, 5); //在第i个位置插入结点并传值,后插,已知位序
for (int i = 0; i < ListLength(L) + 1; i++) {
printf("链表第%d项是%d\n", i, GetElem(L, i)->data);
}
InsetPriorNode(GetElem(L, 1), 3); //前插
Elemtype e_deleted; //返回删除的值
Delete_by_order(&L, 1, &e_deleted); //按位序删除
printf("%d\n", e_deleted);
//按结点删除
Delete_LNode(GetElem(L, 1), &e_deleted);
printf("%d\n", e_deleted);
return 0;
}
4. 指针的使用
定义数据类型时(回看定义)定义了 LNode 和 *LinkList
*LinkList L <==> LNode *L (意为等价)
在main()中声明了头指针
LNode *L;
以「初始化中的指针使用 」为例,辨析下列几种情况:
//初始化一个单链表(带头结点)
bool InitList(LinkList* L) {
*L = (LNode*)malloc(sizeof(LNode)); //分配一个头结点
if (*L == NULL)
return false; //分配失败
(*L)->next = NULL; //头结点后暂时没有结点
return true;
}
int main() {
LNode* L; //声明一个单链表(头指针)
//此时系统自动分配一个头结点+首元节点,头结点data为0,next指向首元节点;首元节点data为空,next为null
printf("%d\n", Empty(L)); //此时显示非空表,因为头结点L->next不为null
printf("%d\n", InitList(&L)); //初始化一个空表
//初始化传入的是头指针的地址,形参如果是Linklist L,则是一级指针;但我们要改的是头指针指向的头结点,那么要用的是二级指针Linklist *L
printf("%d\n", Empty(L)); //此时显示是空表
声明头指针时,
LNode* L;
此时系统自动分配一个头结点+首元节点,头结点data为0,next指向首元节点;首元节点data为空,next为null
初始化以清除有可能的脏数据
InitList(&L)
此处实参为L头指针的地址,形参为LinkList *L二级指针。(相当于下面的LinkList * L_inner )
假设:
LNode头结点地址为0xA0
L头指针地址为0xB0
LinkList * L_inner 地址为0xC0
也即:
&LNode = 0xA0
&L = 0xB0
&L_inner = 0xC0
此时,令
LinkList *L_inner = &L;
或
LinkList *L_inner;
Linner = &L;
则:
L = &LNode = 0xA0
*L_inner = L = &LNode = 0xA0
**L_inner = *L = LNode
由于LinkList *L取得头指针的地址,所以在函数作用域中(*L)可以直接指向头结点的地址,对其进行的操作不会因函数结束内存释放,可以带出去
但注意!函数中(*L)->next 不同于 *L->next
前者:
1.先对L一级解引用 *
2.后再 -> 解引用找到头结点next指针域
后者则是
1.先去找二级指针L的成员next
(显然此时L的data存放的是头指针的地址,next是null)
2.然后对有可能存在的next中存放的地址进行解引用
(当然会报错,因为null不能被解引用)
问题:为何不可传L->next入一级指针LinkList L?
试答:首先next是个指针,指针存放的是下一个结点的地址,指针作为实参传给指针形参,指针形参获得的是下一个结点的地址,更改的是下一个结点内容
再问:那么可否传入&(L->next)?
再答:这里传入的是存放下一个结点地址的指针next的地址,无法更改头结点data,一级指针也无法更改下一个结点内容