链表的基本理论
链表属于线性表的元一种,与顺序表不同的是,虽然链表在逻辑上具有连续性,但其的物理存储却是不一定连续的,链表的每一个元素又称之为一个结点,每一个结点都是我们随机在内存中开辟的空间,正是由于这一特点,所以链表的长度是不固定的,为了构成链式的逻辑结构,即一个结点链接一个的结点的的逻辑结构,我们每个结点除了要储存元素的信息外,还要储存下一个结点的地址(指针),以便我们可以链式访问每一个元素。
为了同时存储元素的信息和下一个结点的地址(指针),所以我们用结构体来定义链表的结点
struct Node
{
int data;//存储元素的信息
struct Node* next;//所存储的下一个结点的类型为结构体,所以我们用结构体指针来存储
};
A结点指向B结点的具体含义:A结点的指针域存储了B结点的内存地址(指针)
链表的基本操作
1.插入操作:在第K个位置插入元素
2.删除操作:删除第K个位置的元素
3.查找操作:查找第K个元素或查找值为X的元素
链表的实现方式有多种,这里我们选择实现带头指针的链表,头指针的类型为结构体指针,仅用来存储所指向的第一个元素的地址,不存储任何元素信息
struct Node* head; //头指针的创建
链表的初始化
init(&head);//将我们要初始化的链表的头指针作为参数传过去
由于头指针的初始化一定会成功,我们不需要他返回成功与否的信息,故函数的返回值类型为void
void init(struct Node** phead)
{
*phead = NULL;//将头结点初始化
}
因为我们的链表还没有插入元素,所以我们在将头指针在初始化时要置为空,这其实是对我们传过来的指针的一种修改,对于传过来的变量的地址(指针),我们可以直接操作该变量的指针对该变量进行修改,同样的道理,若我们想要直接对指针变量进行修改,我们需要将指针变量的地址(指针)传过来,所以我们要用二级指针来承接。
链表结点的创建
在我们操作链表时,每次插入都需要先创建一个结点,故我们把他封装成一个函数,便于提高代码的可读性,该函数的返回类型是我们所创建结点的地址(指针)
struct Node* createNode(int x)//x表示我们要插入结点元素的值
{
struct Node* t;//创建该结点类型的指针
t = (struct Node*)malloc(sizeof(struct Node));
t->next = NULL;//一个好习惯是所有的指针域都不能保持为未赋值状态
t->data = x;
return t;
}
求链表的长度
该函数的调用
getLength(head)
我们在求长度时进行的是只读的操作,不会改变链表,所以仅传递该链表的头指针而没有传递该头指针的地址
int getLength(struct Node* head)//该返回值类型用于返回链表的长度
{
int len = 0;//该变量用于记录链表的长度
while (head != NULL)//若当前指针指向的结点不为空,len++
{
len++;
head = head->next;//改变的是局部变量(head的形参,是真正head的一份临时拷贝),没有改真正的head值
}
return len;//返回链表的长度
}
查找链表的第K个元素
我们在第K个位置插入元素时,首先要判断其插入的合法性,当他的第K-1个位置上有元素时,该操作是一定合法的,所以我们调用该函数查找第K-1个元素
查找函数的调用
findKth(*phead,k-1)//与求长度函数一样,我们在传参时仅需要传递头指针
接下来我们分析一下这个函数
struct Node* findKth(struct Node* head,int k)
{
int count=1;//用于控制查找元素时循环的停止
struct Node* p=head;
while(p!=NULL&&count<k)
{
p=p->next;
count++;
}
return p;
}
当链表为空时,我们的查找一定是失败的,所以while循环一定会结束,直接返回空指针NULL,当链表中只有一个元素时,我们的头指针是指向第一个元素的,我们查找第一个元素时while循环也会不满足count<1这一条件而结束,从而返回我们的头指针,也就是我们第一个元素的地址,我们经常会遇到写循坏条件时取不取等的问题,一个很好的方法就是找一些特殊情况和我们手动进行一些小范围的模拟循坏来判断我们的循坏条件到底怎么写。
链表的插入
因为我们的插入操作是有可能改变头指针的,所以我们要传入我们链表头指针的地址,以下是插入函数的调用
insert(&head, 1, 11)//在链表的第一个位置上插入元素为11的结点
关于插入函数的合法性其实也是可以用getLength()函数来判断的:if(k<0||k>getLength(head)) ,但是我们的插入操作的时间复杂度已经是O(N)了,如果在判断合法性的时候调用同样也为O(N)的getLength()函数,这就使得插入函数的效率变得很低了,而我们直接使用时间复杂度为O(N)的findKth()函数,函数找到第K-1个元素后会返回该元素的地址,我们就可以直接利用该地址进行插入操作,我们的时间复杂度也就仅为O(N)了 ,同时我们的插入操作可能会失败,所以我们的函数返回值为int型,返回值为1表示操作成功,返回值为0表示插入失败。
int insert(struct Node** phead, int k, int x)
{
if (k < 1) { //不合法,操作失败
return 0;
}
else if (k == 1)
{
struct Node* t;
t=createNode(x);
t->next = *phead;//如若不这样就会把原来的结点都丢失
*phead = t;
return 1;//操作成功,返回1
}
else
{
struct Node* p;
p = findKth(*phead,k-1);
//例如:在只有3个元素的链表中,在第5个位置插入元素也是不合法的,
//调用查找函数时返回值是NULL,所以插入会失败
if (p) //可以插入的话,p是不为空的
{
struct Node* t;
t = createNode(x);
t->next = p->next;
p->next = t;
return 1;//操作成功,返回1
}
else
{
return 0;
}
}
}
我们对该函数进行一下分析:
在第一个位置上插入元素是一种特殊情况,是需要单独考虑的,因为第一个元素之前是没有元素的,一个最重要的操作就是先创建一个结点,使用该结点承接插入操作之前头指针指向的地址,防止原来的结点丢失,然后将我们创建结点的地址赋给头指针,让这段“链条”重新接起来,同理在往其他位置插入元素时,也是需要先用我们创建好的结点去保存我们前一个结点的next指针的(t->next = p->next),这样可以防止我们丢失原来的结点,因为我们每个结点的链接都是通过地址来链接的,然后将我们前一个指针的next值赋值为我们创建结点的地址(p->next = t),使我们整个的链表连接起来。
链表的删除
函数的调用:在进行函数的删除操作时,我们先定义一个变量x,在调用删除函数时将其指针传过去,目的是为了让其记录我们所删除元素的值,至于为什么要传该参数的指针,我们在上一篇顺序表的实现中就讲过,这里就不再赘述,同样的道理,我们的删除操作是有可能要修改头指针的,所以我们在传参时要将头指针的地址传过去
int x;//用x来获取删除点的值
int k=removeNode(&head, 5, &x);//删除单链表中第5个位置上的元素
和插入操作相同,删除操作同样要进行合法性的判断,并且讨论删除第一个位置元素的这种特殊情况,同样删除操作也可能会失败,所以我们函数的返回类型我们定义为int型,操作成功返回1,失败返回0。具体的一些细节我们写在了注释中
int removeNode(struct Node** phead, int k, int* px)
{
if (k < 1) //不合法,返回0
{
return 0;
}
else if(k==1)//删除第一个点的讨论
{
if (*phead != NULL)//单链表不为空
{
//先将我们要删除的结点的值赋给我们之前传参传过来的变量
*px = (*phead)->data;
//(*phead)->next是第一个结点的后一个结点的地址(指针)
*phead = (*phead)->next;
return 1;
}
else
return 0;//空链表不存在删除第一个节点
}
else
{
struct Node* p;
p = findKth(*phead, k - 1);
//删除第K个,先找K-1,但找到K-1之后,若K-1为最后一个,第K个也不能删除
if (p == NULL||p->next==NULL)
{
return 0;
}
else
{
struct Node* t;
//p为找到的第k-1位置的结点地址,t为第K位置结点的地址
t = p->next;
//删除第K位置的结点就是将第K-1个结点的指针指向第K+1个结点
p->next = t->next;
*px = t->data;//将我们要删除的结点的值赋给我们之前传参传过来的变量
//将被我们删除的第K个结点的申请的内存空间释放掉,这也是我们定义变量t的意义所在
free(t);
return 1;
}
}
}
链表的输出
我们可以写一个打印函数来感受我们链表操作的具体效果
void printLList(struct Node* head)
{
while (head != NULL)
{
printf("%d, ", head->data);
head = head->next;
}
}
效果测试
以下是完整测试代码及效果
#include<stdio.h>
#include<stdlib.h>
struct Node
{
int data;
struct Node* next;
};
void init(struct Node** phead)
{
*phead = NULL;//将头结点初始化
}
struct Node* createNode(int x)//x表示我们要插入结点元素的值
{
struct Node* t;//创建该结点类型的指针
t = (struct Node*)malloc(sizeof(struct Node));
t->next = NULL;//一个好习惯是所有的指针域都不能保持为未赋值状态
t->data = x;
return t;
}
int getLength(struct Node* head)//该返回值类型用于返回链表的长度
{
int len = 0;//该变量用于记录链表的长度
while (head != NULL)//若当前指针指向的结点不为空,len++
{
len++;
head = head->next;//改变的是局部变量(head的形参,是真正head的一份临时拷贝),没有改真正的head值
}
return len;//返回链表的长度
}
struct Node* findKth(struct Node* head,int k)
{
int count=1;//用于控制查找元素时循环的停止
struct Node* p=head;
while(p!=NULL&&count<k)
{
p=p->next;
count++;
}
return p;
}
int insert(struct Node** phead, int k, int x)
{
if (k < 1) { //不合法,操作失败
return 0;
}
else if (k == 1)
{
struct Node* t;
t=createNode(x);
t->next = *phead;//如若不这样就会把原来的结点都丢失
*phead = t;
return 1;//操作成功,返回1
}
else
{
struct Node* p;
p = findKth(*phead,k-1);
//例如:在只有3个元素的链表中,在第5个位置插入元素也是不合法的,
//调用查找函数时返回值是NULL,所以插入会失败
if (p) //可以插入的话,p是不为空的
{
struct Node* t;
t = createNode(x);
t->next = p->next;
p->next = t;
return 1;//操作成功,返回1
}
else
{
return 0;
}
}
}
int removeNode(struct Node** phead, int k, int* px)
{
if (k < 1) //不合法,返回0
{
return 0;
}
else if(k==1)//删除第一个点的讨论
{
if (*phead != NULL)//单链表不为空
{
//先将我们要删除的结点的值赋给我们之前传参传过来的变量
*px = (*phead)->data;
//(*phead)->next是第一个结点的后一个结点的地址(指针)
*phead = (*phead)->next;
return 1;
}
else
return 0;//空链表不存在删除第一个节点
}
else
{
struct Node* p;
p = findKth(*phead, k - 1);
//删除第K个,先找K-1,但找到K-1之后,若K-1为最后一个,第K个也不能删除
if (p == NULL||p->next==NULL)
{
return 0;
}
else
{
struct Node* t;
//p为找到的第k-1位置的结点地址,t为第K位置结点的地址
t = p->next;
//删除第K位置的结点就是将第K-1个结点的指针指向第K+1个结点
p->next = t->next;
*px = t->data;//将我们要删除的结点的值赋给我们之前传参传过来的变量
//将被我们删除的第K个结点的申请的内存空间释放掉,这也是我们定义变量t的意义所在
free(t);
return 1;
}
}
}
void printLList(struct Node* head)
{
while (head != NULL)
{
printf("%d, ", head->data);
head = head->next;
}
}
int main()
{
struct Node* head;
init(&head);
insert(&head, 1, 11);
insert(&head, 2, 22);
insert(&head, 3, 33);
insert(&head, 4, 44);
insert(&head, 5, 55);
printf("该链表的长度是:%d\n",getLength(head));
printLList(head);
printf("\n");
int x;//用x来获取删除点的值
removeNode(&head, 5, &x);
printf("删除的元素是%d\n", x);
printLList(head);
printf("\n");
removeNode(&head, 1, &x);
printf("删除的元素是%d\n", x);
printLList(head);
printf("\n");
return 0;
}
链表的主要操作我们都已经实现完毕,其他的一些操作都可以基于我们现有的操作进行实现,且实现方法很简单,这里就不在赘述,鉴于笔者水平有限,本文可能存在一些错误,笔者也会虚心学习,请大家多多指正!