友情提示:
本文参考了程杰的《大话数据结构》这本书,写的属实不错,里面介绍的概念相当的牛逼,通俗易懂,然后代码是听网课写的,然后加上了自己对于代码的思考!!!
本文参考了程杰的《大话数据结构》这本书,写的属实不错,里面介绍的概念相当的牛逼,通俗易懂,然后代码是听网课写的,然后加上了自己对于代码的思考!!!
本文参考了程杰的《大话数据结构》这本书,写的属实不错,里面介绍的概念相当的牛逼,通俗易懂,然后代码是听网课写的,然后加上了自己对于代码的思考!!!
1.1单链表的基本概念
在链式结构中,除了要存储数据元素的信息外,还要存储它的后继元素的存储地址。因此,为了表示每个数据元素ai与其直接后继元素ai+1之间的逻辑关系,对数据ai来说,除了存储其本身的信息之外,还需要存储一个指示其直接后继的信息(即直接后继的存储位置)。
我们把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域。指针域中存储的信息称做指针或链。这两部分信息组成数据元素ai的存储映像,称为结点(Node)。
n个结点(ai的存储映像)链结成一个链表,即为线性表(a1, a2, …, an)的链式存储结构,因为此链表的每个结点中只包含一个指针域,所以叫做单链表。
如下图:
把链表中第一个结点的存储位置叫做头指针。
如下图:
有时为了方便对链表进行操作,会在单链表的第一个结点前附设一个结点,称为头结点,此时头指针指向的结点就是头结点。
如下图:
空链表,头结点的直接后继为空。
如下图:
假设p是指向线性表第i个数据元素的指针,p->data表示第i个位置的数据域,p->next表示第i+1个位置的指针域,则第p+i个数据元素可表示为如下图:
1.2线性表链式存储结构(链表)
typedef int datatype_t;
typedef struct node{
datatype_t data;//数据域保存有效数据
struct node *next;//指针域保存下一个节点的地址
//linknode_t是单链表的别名,可以使用它来像int声明整型那样来声明结构体类型
//然后不同的是int存放的是整型,而linknode_t存放的是结构体类型,里面有data和next
}linknode_t;
1.3单链表的操作
1.创建单链表
//创建单链表
linknode_t *create_empty_linklist(){
linknode_t *head;
//使用malloc给结构体分配一片内存空间
head = (linknode_t*)malloc(sizeof(linknode_t));
//如果内存分配失败会返回NULL
if(NULL == head){
printf("malloc is fail!\n");
return NULL;
//因为在malloc分配内存的时候,内存中有可能包含任意数据(即垃圾值),所以需要进行初始化的操作
memset(head,0,sizeof(linknode_t));
return head;
}
}
2.头插法插入数据
图解:
注意这里的图解和下面写的代码可能不是一样的,但是思路是一样的,因为图解是我以前自己画的,图解里面有C++相关的关键字,代码的话是后面进行写的,使用的C语言实现的,因为太懒了,所以直接把以前的图解拿过来就不用重新画了,不懂的可以评论区见。
//头插法:每次都在头结点之后插入一个数据,插入的数据和输出的数据是相反的特点
void head_insert_data(linknode_t *head,datatype_t data){
//申请一个要插入的节点空间
linknode_t *temp = (linknode_t *)malloc(sizeof(linknode_t));
if(NULL == temp){
printf("malloc is fail!\n");
return ;
}
//插入数据
temp->data = data;
//连接节点
temp->next = head->next;
head->next = temp;
return ;
}
运行结果
不用关注下面这个输出是怎么实现的,在后面有个完整代码里面有相应的main函数的内容,这里只需要关注确实成功了就行,看得出图中头插法的数据是逆序的。
3.尾插法插入数据
//尾插法
void tail_insert_data(linknode_t *head,datatype_t data){
//先生成一个需要插入的节点
linknode_t *temp = (linknode_t *)malloc(sizeof(linknode_t));
if(NULL == temp){
printf("malloc is fail!\n");
return ;
}
//插入数据
temp->data = data;
//找到尾节点
linknode_t *p = head;
while(p->next != NULL){
p = p->next;
}
//连接节点
temp->next = p->next;
p->next = temp;
}
图解
4.有序插入数据
//有序插入
void order_insert_data(linknode_t *head,datatype_t data){
//生成一个要插入的节点
linknode_t *temp = (linknode_t *)malloc(sizeof(linknode_t));
if(NULL == temp){
printf("malloc is fail!\n");
return ;
}
//插入数据
temp->data = data;
//辅助指针p,用p来遍历整个链表
linknode_t *p = head;
//p指针向后遍历
//p不为空,并且要插入的数据大于要比较的数据,则插入(这里是升序的,降序改一下条件<即可)
while(p->next != NULL && data > p->next->data){
p = p->next;
}
//在p节点后插入temp节点
temp->next = p->next;
p->next = temp;
return ;
}
验证代码是否正确,可以看出输入的数据不是有序的,但是输出的时候是有序的,测试完成
5.打印数据
这个就比较简单了,直接遍历整个链表,遇到节点就输出就行了
//输出链表中的内容
void print_data_linklist(linknode_t *head){
//声明一个辅助节点进行遍历操作
//指针q指向头结点,用q来遍历整个链表
linknode_t *q = head;
while(q->next != NULL){
printf("%d ",q->next->data);
q = q->next;
}
printf("\n");
return ;
}
6.判空
使用三目运算符简单点
//判空操作,若为空,则返回1,否则返回0
int is_empty_linklist(linknode_t *head){
return head->next == NULL ? 1 : 0;
}
7.删除数据
int delete_data_linklist(linknode_t *head,datatype_t data){
linknode_t *p = head;//辅助节点
linknode_t *q = NULL;//保存要删除的节点,方便释放(free)
int flag = 0;//用于判断是否找到了相关元素的
//若为空,返回错误号
if(is_empty_linklist(head)){
return -1;
}
//没有遍历到链表尾部
while(p->next != NULL){
if(p->next->data == data){
q = p->next;
//保存要删除节点的地址
p->next = q->next;
free(q);
//要将删除的指针置为NULL,防止出现潜在的内存错误或者是未定义的行为
q = NULL;
flag = 1;//flag为1说明有数据进行删除了
}else{
//如果没有找到要删除的数据就一直遍历链表就行了
p = p->next;
}
}
//flag如果还为0的意思就是如果找到链表尾部还没有找到相应的元素
//即没有进入while循环里面的flag = 1;这个语句
if(flag == 0){
return -2;//表示没有找到删除的数据
}else{
printf("删除%d是成功的.\n",data);
}
return 0;
}
图解
测试一下
8.逆序的操作
//单链表逆序
void reverse_data_linklist(linknode_t *head){
linknode_t *p = NULL;
linknode_t *q = NULL;
//1.保存第二个有效节点的地址,把第一个节点的指针域置为NULL
p = head->next->next;
head->next->next = NULL;
//2.从p节点开始利用头插法的思想在头结点之后插入数据
// q保存p后一个节点的地址
while(p != NULL){
//p是要插入头结点后面的节点,为了不丢失节点,先用q保存一下p的下一个节点
q = p->next;
//头插
p->next = head->next;
head->next = p;
//更新p和q节点,p和q还是一样的作用
p = q;
}
return ;
}
9.求表长
//求表长
int length_linklist(linknode_t *head){
//记录表长
int length = 0;
//从首元节点开始(不是头节点)
linknode_t *p = head->next;
//当节点不为空时继续查找
while(p != NULL){
length++;
p = p->next;
}
return length;
}
10.按位查找
图解
//按位查找
//这里我写的是返回的是指针类型的,失败就返回NULL,因为如果返回datatype_t类型的话,返回值是哪个数都不符合要求
//因为如果你返回-1或者其他的数,然后你插入数据里面有-1,到底是返回的值还是失败不就混了吗
//不知道有没有其他比较好的办法
datatype_t *find_data_linklist(linknode_t *head,int i){
int j = 1;//计数器,因为首元是第一个元素,所以j初值设置为1
linknode_t *p = head->next;//从首元节点开始
int len = length_linklist(head);
//找的位置不对
if(i < 1 || i > len){
return NULL;
}
//一直找
while(p != NULL && j < i){
p = p->next;
j++;
}
//没找到的情况
if(p == NULL || j > i){
return NULL;
}
return &(p->data);
}
测试如下
11.按值查找
图解
//按值查找
//返回的结果是:返回第一个与传入的数据data相等的位序(注意位序是从1开始的)
int find_index_linklist(linknode_t *head,datatype_t data){
//初始化首元节点的位序是1
int j = 1;
//从首元节点开始查找
linknode_t *p = head->next;
//当p不为空且没有找到指定的元素的时候,p要往后移动,位序+1
while(p != NULL && p->data != data){
p = p->next;
j++;
}
//检查是否找到了与data相等的元素
//如果p不为空,说明找到了,返回其位序j
//这里如果p刚开始就等于NULL的话,就直接返回-1失败,而不是返回初始值1
if(p != NULL)
return j;
else
return -1;
}
测试
12.销毁操作
//单链表清除,包括头结点也要删除掉
void clear_linklist(linknode_t *head){
linknode_t *p = head;//指向头结点
linknode_t *q = NULL;//要删除掉的辅助节点
while(p != NULL){
q = p->next;//记录要删除节点的后一个节点
print_data_linklist(p);//将删除掉的节点输出
free(p);//释放掉删除的节点
p = q;//更新节点
}
return ;
}
13.完整代码
我这里是分文件编写的,一共三个文件
linklist.c
#include"linklist.h"
//创建单链表
linknode_t *create_empty_linklist(){
linknode_t *head;
head = (linknode_t*)malloc(sizeof(linknode_t));
if(NULL == head){
printf("malloc is fail!\n");
return NULL;
memset(head,0,sizeof(linknode_t));
return head;
}
}
//头插法:每次都在头结点之后插入一个数据,插入的数据和输出的数据是相反的特点
void head_insert_data(linknode_t *head,datatype_t data){
//申请一个要插入的节点空间
linknode_t *temp = (linknode_t *)malloc(sizeof(linknode_t));
if(NULL == temp){
printf("malloc is fail!\n");
return;
}
//插入数据
temp->data = data;
//连接节点
temp->next = head->next;
head->next = temp;
return ;
}
//尾插法
void tail_insert_data(linknode_t *head,datatype_t data){
linknode_t *temp = (linknode_t *)malloc(sizeof(linknode_t));
if(NULL == temp){
printf("malloc is fail!\n");
return;
}
//插入数据
temp->data = data;
//找到尾节点
linknode_t *p = head;
while(p->next != NULL){
p = p->next;
}
//连接节点
temp->next = p->next;
p->next = temp;
}
//有序插入
void order_insert_data(linknode_t *head,datatype_t data){
linknode_t *temp = (linknode_t *)malloc(sizeof(linknode_t));
if(NULL == temp){
printf("malloc is fail!\n");
return;
}
//插入数据
temp->data = data;
linknode_t *p = head;
//p指针向后遍历
while(p->next != NULL && data > p->next->data){
p = p->next;
}
//在p节点后插入temp节点
temp->next = p->next;
p->next = temp;
return;
}
//输出链表中的内容
void print_data_linklist(linknode_t *head){
//声明一个辅助节点进行遍历操作
//指针q指向头结点
linknode_t *q = head;
while(q->next != NULL){
printf("%d ",q->next->data);
q = q->next;
}
printf("\n");
return ;
}
//判空操作,若为空,则返回1,否则返回0
int is_empty_linklist(linknode_t *head){
return head->next == NULL ? 1 : 0;
}
int delete_data_linklist(linknode_t *head,datatype_t data){
linknode_t *p = head;//辅助节点
linknode_t *q = NULL;//保存要删除的节点,方便释放free
int flag = 0;
//若为空,返回错误号
if(is_empty_linklist(head)){
return -1;
}
//没有遍历到链表尾部
while(p->next != NULL){
if(p->next->data == data){
q = p->next;
//保存要删除节点的地址
p->next = q->next;
free(q);
q = NULL;
flag = 1;
}else{
p = p->next;
}
}
if(flag == 0){
return -2;//表示没有找到删除的数据
}else{
printf("删除%d是成功的.\n",data);
}
return 0;
}
//单链表逆序
void reverse_data_linklist(linknode_t *head){
linknode_t *p = NULL;
linknode_t *q = NULL;
//1.保存第二个有效节点的地址,把第一个节点的指针域置为NULL
p = head->next->next;
head->next->next = NULL;
//2.从p节点开始利用头插法的思想在头结点之后插入数据
// q保存p后一个节点的地址
while(p != NULL){
//p是要插入头结点后面的节点,为了不丢失节点,先用q保存一下p的下一个节点
q = p->next;
//头插
p->next = head->next;
head->next = p;
//更新p和q节点,p和q还是一样的作用
p = q;
}
return ;
}
//求表长
int length_linklist(linknode_t *head){
//记录表长
int length = 0;
//从首元节点开始(不是头节点)
linknode_t *p = head->next;
//当节点不为空时继续查找
while(p != NULL){
length++;
p = p->next;
}
return length;
}
//按位查找
//这里我写的是返回的是指针类型的,失败就返回NULL,因为如果返回datatype_t类型的话,返回值是哪个数都不符合要求
//因为如果你返回-1或者其他的数,然后你插入数据里面有-1,到底是返回的值还是失败不就混了吗
//不知道有没有其他比较好的办法
datatype_t *find_data_linklist(linknode_t *head,int i){
int j = 1;//计数器,因为首元是第一个元素,所以j初值设置为1
linknode_t *p = head->next;//从首元节点开始
int len = length_linklist(head);
//找的位置不对
if(i < 1 || i > len){
return NULL;
}
//一直找
while(p != NULL && j < i){
p = p->next;
j++;
}
//没找到的情况
if(p == NULL || j > i){
return NULL;
}
return &(p->data);
}
//按值查找
//返回的结果是:返回第一个与传入的数据data相等的位序(注意位序是从1开始的)
int find_index_linklist(linknode_t *head,datatype_t data){
//初始化首元节点的位序是1
int j = 1;
//从首元节点开始查找
linknode_t *p = head->next;
//当p不为空且没有找到指定的元素的时候,p要往后移动,位序+1
while(p != NULL && p->data != data){
p = p->next;
j++;
}
//检查是否找到了与data相等的元素
//如果p不为空,说明找到了,返回其位序j
//这里如果p刚开始就等于NULL的话,就直接返回-1失败,而不是返回初始值1
if(p != NULL)
return j;
else
return -1;
}
//单链表清除,包括头结点也要删除掉
void clear_linklist(linknode_t *head){
linknode_t *p = head;//指向头结点
linknode_t *q = NULL;//要删除掉的辅助节点
while(p != NULL){
q = p->next;//记录要删除节点的后一个节点
print_data_linklist(p);//将删除掉的节点输出
free(p);//释放掉删除的节点
p = q;//更新节点
}
return ;
}
linklist.h
#ifndef __LINKLIST_H__
#define __LINKLIST_H__
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
typedef int datatype_t;
typedef struct node{
datatype_t data;//数据域保存有效数据
struct node *next;//指针域保存下一个节点的地址
}linknode_t;
extern linknode_t *create_empty_linklist();
extern void head_insert_data(linknode_t *head,datatype_t data);
extern void print_data_linklist(linknode_t *head);
extern void tail_insert_data(linknode_t *head,datatype_t data);
extern void order_insert_data(linknode_t *head,datatype_t);
extern int is_empty_linklist(linknode_t *head);
extern int delete_data_linklist(linknode_t *head,datatype_t data);
extern int length_linklist(linknode_t *head);
extern datatype_t *find_data_linklist(linknode_t *head,int i);
extern int find_index_linklist(linknode_t *head,datatype_t data);
extern void reverse_data_linklist(linknode_t *head);
extern void clear_linklist(linknode_t *head);
#endif
main.c
#include"linklist.h"
int main(){
int n = 0,i = 0;
datatype_t data;
//初始化单链表
linknode_t *head = NULL;
head = create_empty_linklist();
//头插/尾插/顺序插入法
printf("请输入你想要插入的数据的个数:");
scanf("%d",&n);
printf("请输入%d个数:",n);
for(i = 0;i < n;i++){
scanf("%d",&data);
//head_insert_data(head,data);
//tail_insert_data(head,data);
order_insert_data(head,data);
}
//printf("——————————————头插法插入的数据的输出————————————————————————");
//print_data_linklist(head);
//printf("——————————————尾插法插入的数据的输出————————————————————————");
//print_data_linklist(head);
printf("——————————————有序法插入的数据的输出————————————————————————");
print_data_linklist(head);
//判空
int isEmpty = is_empty_linklist(head);
printf("链表是否为空(1:空 0:非空):%d\n",isEmpty);
//求表长
printf("单链表的表长为:%d\n",length_linklist(head));
//按位查找
printf("请输入您要查找哪个位置的数据:");
scanf("%d",&data);
datatype_t *value;
value = find_data_linklist(head,data);
if(value == NULL)
printf("查找失败(位置不对/没有这个数据)\n");
else
printf("您要查找的第%d个位置的数据是%d\n",data,*value);
//按值查找
printf("请输入您要查找的值:");
scanf("%d",&data);
int index = find_index_linklist(head,data);
if(index < 0)
printf("没有您要查找的数据\n");
else
printf("您要查找的数据%d在单链表中第%d个位置。\n",data,index);
//删除数据
printf("请输入要删除的数据:");
scanf("%d",&data);
int isDelete = delete_data_linklist(head,data);
if(isDelete < 0){
printf("没有找到您要删除的%d数据.\n",data);
return -1;
}else{
printf("删除%d这个数据后,此时单链表中的数据为:",data);
print_data_linklist(head);
}
//逆序演示
reverse_data_linklist(head);
printf("链表中的数据逆序之后的结果为:");
print_data_linklist(head);
//清除单链表
printf("清除单链表中的数据,清除的数据输出过程如下:\n");
clear_linklist(head);
return 0;
}
13.运行演示
1.4优缺点
优点
-
动态性:单链表不需要预先分配固定大小的内存空间。可以根据需要动态地添加或删除节点,这使得单链表在处理动态数据时非常灵活。
-
插入和删除操作方便:在单链表中,插入和删除操作通常只需要改变相关节点的指针域,而不需要移动大量数据。这使得这些操作在单链表中相对较快。
-
空间利用率高:单链表不需要像数组那样为未使用的空间分配内存。它只需要为实际存储的数据分配空间,因此空间利用率更高。
缺点
-
访问特定元素效率低:在单链表中,访问特定元素需要从头节点开始,通过遍历链表来找到目标节点。这种操作的时间复杂度是O(n),其中n是链表的长度。因此,如果需要频繁地访问特定元素,单链表可能不是最佳选择。
-
额外的空间开销:每个节点都需要额外的空间来存储指针,这增加了链表的存储开销。然而,由于指针的大小通常是固定的(例如,在32位系统中为4字节,在64位系统中为8字节),这种开销通常是可以接受的。
-
无法直接访问前驱节点:由于单链表只包含指向下一个节点的指针,因此无法直接访问某个节点的前驱节点。如果需要访问前驱节点,通常需要从头节点开始遍历链表。
-
内存碎片化:在频繁地插入和删除节点时,单链表可能会导致内存碎片化。这是因为每次分配和释放节点时,操作系统可能会在不同的内存位置进行这些操作,从而导致内存空间的不连续和碎片化。
1.5循环链表
如果上面的单链表会了的话这个循环链表和下面的双向链表都是类似的了,不过多介绍,这里主要是截图《大话数据结构》这本书的内容,代码可以看看其他人有没有写,我这里就不自己写了,会单链表这个也很简单。
概念
将单链表中终端结点的指针端由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表。
其实循环链表和单链表的主要差异就在于循环的判断条件上,原来是判断p→next是否为空,现在则是p→next不等于头结点,则循环未结束。
在单链表中,我们有了头结点时,我们可以用O(1)的时间访问第一个结点,但是对于要访问到最后一个结点,却需要O(n)时间,因为我们需要将单链表全部扫描一遍。
那有没有可能用O(1)的时间由单链表指针访问到最后一个结点呢?当然可以!!!
不过要改一下这个循环链表,不用头指针,而是用指向终端结点的尾指针来表示循环链表,如下图,此时查找开始结点和终端结点都很方便了。
从图中可以看出,终端结点用尾指针rear指示,则查找终端结点是O(1),而开始结点,其实就是rear→next→next,其时间复杂度也为O(1)。
所以在使用循环链表的时候,通常设置的是尾指针指向尾结点
循环链表举个例子
将两个循环链表合并成一个表时,有了尾指针就非常简单了,比如下面的这两个单链表,他们的尾指针分别为rearA和rearB,如下图:
1.6双向链表
单链表的不足
在单链表中,有了next指针,这就使得我们要查找下一个结点的时间复杂度为O(1),可是如果我们要查找的是上一个结点的话,那最坏的时间复杂度就是O(n)了,难道就不可以正反遍历都可以吗?当然可以,只不过需要加点东西而已。
双向链表的概念
是在单链表的每个结点中,再设置一个指向其前驱结点的指针域。所以在双向链表中的结点都有两个指针域,一个指向直接后继,另一个指向直接前驱。
线性表的双向链表存储结构
typedef struct DulNodse{
DataType data;
struct DulNode *prior; //直接前驱指针
struct DulNode *next; //直接后继指针
} DulNode, *DuLinkList;
既然单链表也可以循环链表,那么双向链表当然也可以是循环链表
双向链表的带头结点的空链表如下图:
非空的循环的带头结点的双向链表如图:
双向链表,对于链表中的某一个结点p,它的后继的前驱 和 前驱的后继 都是他自己
即
p->next->prior = p = p->prior->next
双向链表和单链表的操作几乎相同,比如求长度,按位查找,按值查找等,因为这些操作只涉及一个方向的指针,另一个指针没有什么帮助。
双向链表在插入和删除的操作上有区别,因为要更改两个方向的指针!!!
双向链表的插入操作
//第一步:把p赋值给s的前驱
s->prior = p;
//第二步:把p->next赋值给s的后继
s->next = p->next
//第三步:把s赋值给p->next的前驱
p->next->prior = s;
//第四步:把s赋值给p的后继
p->next = s;
双向链表的删除操作
//第一步
p->next = q->next;
//第二步
q->next->prior = p;
//第三步:释放删除结点
free(q);