目录
0基础概念
n个结点链结成一个链表,即为线性表(a1、a2、.....、an)的链式存储结构,因为此链表的每个结点中只包含一个指针域,
所以叫单链表,(在双向链表中,链表的每个结点多了一个前驱指针,prior)
链表正式通过每个结点的指针域将线性表的数据元素按其逻辑次序链接在一起
(图源:大话数据结构)
而在后期,因为要将数据信息隐藏,把数据信息和指针域进行了分离,产生了一种更加方便和实用的链表形式。
链表结点有一个(后驱)或两个指针(前驱和后驱)
头结点部分除了创建头结点外,还会包含一些链表的属性特征:如尾部指针,大小等。
包括头结点部分(左上角)在内,链表结点类型(右下角),只提供结构体名称即可。
next指针指向的是用户定义类型的地址也是用户自定义类型的首个成员——NODE(链表结点)的地址
开发者也不用知道用户在自定义类型中定义了多少个其他类型的变量,只要用户自定义类型的第一个成员是链表结点类型的变量即可。开发者会让所有的函数接口,输入量全部转换为void *类型的指针,也就是自定义类型元素的地址,如此,就没有必要在乎用户自定义类型中到底存储了什么类型的元素了,之后在接口的内部,将这个传入的万能参数,强制转换为链表结点类型,恰巧用户自定义类型的第一个成员也是链表结点类型,相当于操作的还是链表的结点。
如此做的意义,更多的在于,链表中的元素作用域是由用户去把握的(不用在接口中使用new或malloc给插入元素分配空间)
(此部分见:数据结构:单向链表Part1:https://blog.csdn.net/qq_41605114/article/details/104396149)
而且方便数据隐藏
typedef struct Test
{
int age;
QString name;
}T;
T test1 = {1,"123"};
qDebug()<<"&test1"<<&test1;
qDebug()<<"&(test1.age)"<<&(test1.age);
从上述代码就能看出,和指针一样,结构体的首地址就是结构体首成员的地址。
下面通过一个例子对上述数据结构的设置做出解释:
.h
//链表节点数据类型
struct LinkContent
{
struct LinkContent * next;
};
//链表数据类型
struct LinkProperty
{
struct LinkContent Content;
int size;
};
typedef void * LinkListTwo;//为了更好的让用户理解函数
typedef struct Test
{
LinkContent node;
QString name;
int age;
}T;
数据结构和上述相同(只是部分变量名称进行了替换)
.cpp 具体操作如下:
T t1 = {nullptr,"1",10};
T t2 = {nullptr,"2",20};
T t3 = {nullptr,"3",30};
T t4 = {nullptr,"4",40};
T t5 = {nullptr,"5",50};
qDebug()<<"初始化结果:"<<t1.node.next;
qDebug()<<"初始化结果:"<<t2.node.next;
qDebug()<<"初始化结果:"<<t3.node.next;
qDebug()<<"初始化结果:"<<t4.node.next;
qDebug()<<"初始化结果:"<<t5.node.next;
首先是初始化,我们可以看见,链表中的next指针目前全部都被初始化为空了
qDebug()<<"t1首地址:"<<&t1;
qDebug()<<"t2首地址:"<<&t2;
qDebug()<<"t3首地址:"<<&t3;
qDebug()<<"t4首地址:"<<&t4;
qDebug()<<"t5首地址:"<<&t5;
LinkContent *myContent1 = (LinkContent*)(&t1);
LinkContent *myContent2 = (LinkContent*)(&t2);
LinkContent *myContent3 = (LinkContent*)(&t3);
LinkContent *myContent4 = (LinkContent*)(&t4);
LinkContent *myContent5 = (LinkContent*)(&t5);
myContent1->next = myContent2;
myContent2->next = myContent3;
myContent3->next = myContent4;
myContent4->next = myContent5;
myContent5->next = nullptr;
之后我们取出插入元素的首地址
并将其强制转换类型,转换为结构体第一个元素的类型
t1是T类型,取地址(&t1),变成了指针,指向结构体的第一个元素,也就是LinkContent类型的指针
这也就是为什么一定要强调把 链表结点类型结构体变量 定义在用户自定义结构体中第一位的原因
强转后,就可以访问到每个要插入数据的next指针了,然后直接进行赋值操作
之后进行输出,并对比输出内容
qDebug()<<"强转类型后t1.node.next:"<<t1.node.next;
qDebug()<<"强转类型后t2.node.next:"<<t2.node.next;
qDebug()<<"强转类型后t3.node.next:"<<t3.node.next;
qDebug()<<"强转类型后t4.node.next:"<<t4.node.next;
qDebug()<<"强转类型后t5.node.next:"<<t5.node.next;
可见,要插入元素之间的连接已经完成,那么以上只是演示
言外之意是,在开发者眼里,不管用户自定义类型有多少个成员变量,只要第一个成员变量是链表结点类型结构体变量,在开发者写各类接口的时候,直接将传入接口的参数强制转换成链表结点类型结构体变量即可,然后就可以完成链表元素之间的连接。
到时候只需要给用户提供:
- 链表结点类型结构体名称
- 头结点结构体名称(初始化和调用接口的时候用)
- 各个接口名称和输入要求
即可
做到了面向对象编程的基本要求
1链表结构
1.1链表的基本内容(链表结点类型结构体):
struct linklistnode
{
linklistnode * next;
};
对比以前,此版本增加了前驱指针,prior
1.2头结点内容:
struct linklistProperty
{
linklistnode header;
int size;
};
除了头结点中的头指针外(hearer.next) ,还有链表大小。
注意: 头结点,最好是linklistnode类型,因为头结点不存放任何的插入元素,只是在内存中占用一块地址就行
让整个链表有起始点,方便进行各类操作的产物,如果定义为linklistnode * 类型,这个指针无法指向任何内容,失去了指针的意义。
1.3用户数据结构(插入元素的数据结构)
typedef struct linklistData
{
linklistnode node;
QString name;
int age;
}LLD;
用户自定义类型中,要求第一个成员必须是linklistnode类型(链表类型结构体变量 )
为了更好的开发和应用分离,就是面对对象编程
在开发过程中不知道用户会传递什么样的数据进来,为了让用户传递任何类型的数据进来,程序都能应对,采用如此办法。
下面通过一个例子对上述数据结构的设置做出解释:
.h
//链表节点数据类型
struct LinkContent
{
struct LinkContent * next;
};
//链表数据类型
struct LinkProperty
{
struct LinkContent Content;
int size;
};
typedef void * LinkListTwo;//为了更好的让用户理解函数
typedef struct Test
{
LinkContent node;
QString name;
int age;
}T;
数据结构和上述相同(只是部分变量名称进行了替换)
.cpp 具体操作如下:
T t1 = {nullptr,"1",10};
T t2 = {nullptr,"2",20};
T t3 = {nullptr,"3",30};
T t4 = {nullptr,"4",40};
T t5 = {nullptr,"5",50};
qDebug()<<"初始化结果:"<<t1.node.next;
qDebug()<<"初始化结果:"<<t2.node.next;
qDebug()<<"初始化结果:"<<t3.node.next;
qDebug()<<"初始化结果:"<<t4.node.next;
qDebug()<<"初始化结果:"<<t5.node.next;
首先是初始化,我们可以看见,链表中的next指针目前全部都被初始化为空了
qDebug()<<"t1首地址:"<<&t1;
qDebug()<<"t2首地址:"<<&t2;
qDebug()<<"t3首地址:"<<&t3;
qDebug()<<"t4首地址:"<<&t4;
qDebug()<<"t5首地址:"<<&t5;
LinkContent *myContent1 = (LinkContent*)(&t1);
LinkContent *myContent2 = (LinkContent*)(&t2);
LinkContent *myContent3 = (LinkContent*)(&t3);
LinkContent *myContent4 = (LinkContent*)(&t4);
LinkContent *myContent5 = (LinkContent*)(&t5);
myContent1->next = myContent2;
myContent2->next = myContent3;
myContent3->next = myContent4;
myContent4->next = myContent5;
myContent5->next = nullptr;
之后我们取出插入元素的首地址
并将其强制转换类型,转换为结构体第一个成员的类型
t1是T类型,取地址(&t1),变成了指针,指向结构体的第一个成员,也就是LinkContent类型的指针
这也就是为什么一定要强调把 链表类型结构体变量 定义在用户自定义结构体中第一位的原因
强转后,就可以访问到每个要插入数据的next指针了,然后直接进行赋值操作
之后进行输出,并对比输出内容
qDebug()<<"强转类型后t1.node.next:"<<t1.node.next;
qDebug()<<"强转类型后t2.node.next:"<<t2.node.next;
qDebug()<<"强转类型后t3.node.next:"<<t3.node.next;
qDebug()<<"强转类型后t4.node.next:"<<t4.node.next;
qDebug()<<"强转类型后t5.node.next:"<<t5.node.next;
可见,要插入元素之间的连接已经完成,那么以上只是演示
言外之意是,在开发者眼里,不管用户自定义类型有多少个成员变量,只要第一个成员变量是链表类型结构体变量,在开发者写各类接口的时候,直接将传入接口的参数强制转换成链表类型结构体变量即可,然后就可以完成链表元素之间的连接。
到时候只需要给用户提供:
- 链表类型结构体名称
- 头结点结构体名称(初始化和调用接口的时候用)
- 各个接口名称和输入要求
即可
做到了面向对象编程的基本要求
2初始化和销毁
typedef void * LLHeader;
为了让用户更好理解,增加可读性。
初始化过程中,需要先堆上要一部分内存空间
在new和malloc之间,选择原则很简单,需要扩容,用malloc,就像动态数组,及线性表的顺序存储,用malloc显然更好操作:
数据结构:线性表-动态数组:https://blog.csdn.net/qq_41605114/article/details/104315027
C/C++:细说new与malloc的10点区别 :https://blog.csdn.net/qq_41605114/article/details/104342587
//初始化
LLHeader Inite_LL()
{
linklistProperty * mylist = new linklistProperty;//申请空间
mylist->size = 0;
mylist->header.next = nullptr;
return mylist;
}
初始化一定要注意:
后驱(MyList->header.next = nullptr)
在单链表,循环链表和双向链表中,最重要的,就是链表的结果,后驱指向哪儿,前驱指向哪儿?
销毁也是一样的
//销毁
void Destroy_LL(LLHeader LL)
{
if(nullptr == LL)
return;
linklistProperty * mylist = (linklistProperty *)LL;
delete mylist;
}
3插入
链表的插入,整体上的思路都是找到要插入位置的上一个位置,进行插入操作,在循环链表和双向链表中,
因为能够非常简单的访问到尾部指针,所以插入要分情况讨论,完整代码如下:
插入的过程示意图如下图所示:
核心问题:找到要插入位置的前一个位置,也就是图中的ai,之后再进行后驱指针的更新即可
//插入
void Insert_LL(LLHeader LL,int place,void * data)
{
if(nullptr == LL)
return;
linklistProperty * mylist = (linklistProperty *)LL;
linklistnode * pCurrent = &(mylist->header);//从第一个结点开始
linklistnode * InsertData = (linklistnode *)data;
if(place<=0)
return;
if(place>mylist->size)
{
place = mylist->size+1;
}
for(int i = 0;i<place-1;++i)
{
pCurrent = pCurrent->next;
}
InsertData->next = pCurrent->next;
pCurrent->next = InsertData;
mylist->size++;
}
4遍历
遍历的关键:出发点是第一个结点还是头结点,是会影响到判断的,需要注意
//遍历
void Foreach_LL(LLHeader LL,void(*Print)(void *))//第二个参数是遍历的遍数
{
if(nullptr == LL)
return;
linklistProperty * mylist = (linklistProperty *)LL;
linklistnode * pCurrent = mylist->header.next;//从第一个结点开始
while(pCurrent!=nullptr)
{
Print(pCurrent);
pCurrent = pCurrent->next;
}
}
因为开发过程中不知道用户输入的数据类型,所以输出部分需要用户写一个回调函数:
//打印函数
void PrintDATA_LL(void * data)
{
if(nullptr == data)
return;
linklistData * mydata = (linklistData *)data;
qDebug()<<"age:"<<mydata->age<<"name:"<<mydata->name;
}
5删除
删除的关键和插入一样,找到要操作结点的前一个结点。
//按照位置删除
void Delete_LL(LLHeader LL,int place)
{
if(nullptr == LL)
return;
linklistProperty * mylist = (linklistProperty *)LL;
if(place<=0)
return;
linklistnode * pCurrent = &(mylist->header);
for(int i = 0;i<place-1;++i)
{
pCurrent = pCurrent->next;
}
linklistnode * pDel = pCurrent->next;//从第一个结点开始
pCurrent->next = pDel->next;
pDel->next = nullptr;
mylist->size--;
}
程序验证
qDebug()<<"/*****************************进入单向链表部分*****************************/";
qDebug()<<"初始化";
linklistProperty * MyLinkList = (linklistProperty * )Inite_LL();//申请空间
LLD lld1 = {nullptr,"111",1};
LLD lld2 = {nullptr,"222",2};
LLD lld3 = {nullptr,"333",3};
LLD lld4 = {nullptr,"444",4};
LLD lld5 = {nullptr,"555",5};
LLD lld6 = {nullptr,"666",6};
qDebug()<<"插入";
Insert_LL(MyLinkList,1,&lld1);
Insert_LL(MyLinkList,2,&lld2);
Insert_LL(MyLinkList,3,&lld3);
Insert_LL(MyLinkList,4,&lld4);
Insert_LL(MyLinkList,5,&lld5);
Insert_LL(MyLinkList,6,&lld6);
qDebug()<<"个数"<<Size_LL(MyLinkList);
qDebug()<<"遍历";
Foreach_LL(MyLinkList,PrintDATA_LL);
此部分是验证初始化,插入,和遍历的程序
qDebug()<<"删除第三个元素";
Delete_LL(MyLinkList,3);
qDebug()<<"个数"<<Size_LL(MyLinkList);
qDebug()<<"遍历";
Foreach_LL(MyLinkList,PrintDATA_LL);
qDebug()<<"删除元素lld6";
DeleteItem_LL(MyLinkList,&lld6);
qDebug()<<"个数"<<Size_LL(MyLinkList);
qDebug()<<"遍历";
Foreach_LL(MyLinkList,PrintDATA_LL);
此部分验证了删除接口
Insert_LL(MyLinkList,5,&lld3);
Insert_LL(MyLinkList,4,&lld6);
qDebug()<<"个数"<<Size_LL(MyLinkList);
qDebug()<<"遍历";
Foreach_LL(MyLinkList,PrintDATA_LL);
此部分再次验证插入接口,输入的位置就是插入新元素后该元素所在的位置
LLear_LL(MyLinkList);
qDebug()<<"个数"<<Size_LL(MyLinkList);
qDebug()<<"遍历";
Foreach_LL(MyLinkList,PrintDATA_LL);
qDebug()<<"销毁";
Destroy_LL(MyLinkList);
最后是验证清除,即断开所有链接的部分
附录
.h
#ifndef LINKLIST_H
#define LINKLIST_H
#include <QWidget>
#include<QDebug>
class Linklist : public QWidget
{
Q_OBJECT
public:
explicit Linklist(QWidget *parent = nullptr);
signals:
public slots:
};
struct linklistnode
{
linklistnode * next;
};
struct linklistProperty
{
linklistnode header;
int size;
};
//包含数据的内容
typedef struct linklistData
{
linklistnode node;
QString name;
int age;
}LLD;
typedef void * LLHeader;
//初始化
LLHeader Inite_LL();
//插入
void Insert_LL(LLHeader LL,int place,void * data);
//遍历
void Foreach_LL(LLHeader LL,void(*Print)(void *));//第二个参数是遍历的遍数
//打印函数
void PrintDATA_LL(void * data);
//按照位置删除
void Delete_LL(LLHeader LL,int place);
//按照元素删除
void DeleteItem_LL(LLHeader LL,void * data);
//销毁
void Destroy_LL(LLHeader LL);
//清空
void LLear_LL(LLHeader LL);
//个数
int Size_LL(LLHeader LL);
#endif // LINKLIST_H
.cpp
#include "linklist.h"
Linklist::Linklist(QWidget *parent) : QWidget(parent)
{
qDebug()<<"/*****************************进入单向链表部分*****************************/";
qDebug()<<"初始化";
linklistProperty * MyLinkList = (linklistProperty * )Inite_LL();//申请空间
LLD lld1 = {nullptr,"111",1};
LLD lld2 = {nullptr,"222",2};
LLD lld3 = {nullptr,"333",3};
LLD lld4 = {nullptr,"444",4};
LLD lld5 = {nullptr,"555",5};
LLD lld6 = {nullptr,"666",6};
qDebug()<<"插入";
Insert_LL(MyLinkList,1,&lld1);
Insert_LL(MyLinkList,2,&lld2);
Insert_LL(MyLinkList,3,&lld3);
Insert_LL(MyLinkList,4,&lld4);
Insert_LL(MyLinkList,5,&lld5);
Insert_LL(MyLinkList,6,&lld6);
qDebug()<<"个数"<<Size_LL(MyLinkList);
qDebug()<<"遍历";
Foreach_LL(MyLinkList,PrintDATA_LL);
qDebug()<<"删除第三个元素";
Delete_LL(MyLinkList,3);
qDebug()<<"个数"<<Size_LL(MyLinkList);
qDebug()<<"遍历";
Foreach_LL(MyLinkList,PrintDATA_LL);
qDebug()<<"删除元素lld6";
DeleteItem_LL(MyLinkList,&lld6);
qDebug()<<"个数"<<Size_LL(MyLinkList);
qDebug()<<"遍历";
Foreach_LL(MyLinkList,PrintDATA_LL);
Insert_LL(MyLinkList,5,&lld3);
Insert_LL(MyLinkList,4,&lld6);
qDebug()<<"个数"<<Size_LL(MyLinkList);
qDebug()<<"遍历";
Foreach_LL(MyLinkList,PrintDATA_LL);
LLear_LL(MyLinkList);
qDebug()<<"个数"<<Size_LL(MyLinkList);
qDebug()<<"遍历";
Foreach_LL(MyLinkList,PrintDATA_LL);
qDebug()<<"销毁";
Destroy_LL(MyLinkList);
}
//初始化
LLHeader Inite_LL()
{
linklistProperty * mylist = new linklistProperty;//申请空间
mylist->size = 0;
mylist->header.next = nullptr;
return mylist;
}
//插入
void Insert_LL(LLHeader LL,int place,void * data)
{
if(nullptr == LL)
return;
linklistProperty * mylist = (linklistProperty *)LL;
linklistnode * pCurrent = &(mylist->header);//从第一个结点开始
linklistnode * InsertData = (linklistnode *)data;
if(place<=0)
return;
if(place>mylist->size)
{
place = mylist->size+1;
}
for(int i = 0;i<place-1;++i)
{
pCurrent = pCurrent->next;
}
InsertData->next = pCurrent->next;
pCurrent->next = InsertData;
mylist->size++;
}
//遍历
void Foreach_LL(LLHeader LL,void(*Print)(void *))//第二个参数是遍历的遍数
{
if(nullptr == LL)
return;
linklistProperty * mylist = (linklistProperty *)LL;
linklistnode * pCurrent = mylist->header.next;//从第一个结点开始
while(pCurrent!=nullptr)
{
Print(pCurrent);
pCurrent = pCurrent->next;
}
}
//打印函数
void PrintDATA_LL(void * data)
{
if(nullptr == data)
return;
linklistData * mydata = (linklistData *)data;
qDebug()<<"age:"<<mydata->age<<"name:"<<mydata->name;
}
//按照位置删除
void Delete_LL(LLHeader LL,int place)
{
if(nullptr == LL)
return;
linklistProperty * mylist = (linklistProperty *)LL;
if(place<=0)
return;
linklistnode * pCurrent = &(mylist->header);
for(int i = 0;i<place-1;++i)
{
pCurrent = pCurrent->next;
}
linklistnode * pDel = pCurrent->next;//从第一个结点开始
pCurrent->next = pDel->next;
pDel->next = nullptr;
mylist->size--;
}
//按照元素删除
void DeleteItem_LL(LLHeader LL,void * data)
{
if(nullptr == LL)
return;
linklistProperty * mylist = (linklistProperty *)LL;
linklistnode * pDElitem = (linklistnode *)data;
linklistnode * pCurrent = mylist->header.next;
int place = 1;
while(pCurrent!=nullptr)
{
if(pCurrent == pDElitem)
{
Delete_LL(LL,place);
break;
}
place++;
pCurrent = pCurrent->next;
}
}
//销毁
void Destroy_LL(LLHeader LL)
{
if(nullptr == LL)
return;
linklistProperty * mylist = (linklistProperty *)LL;
delete mylist;
}
//清空
void LLear_LL(LLHeader LL)
{
if(nullptr == LL)
return;
linklistProperty * mylist = (linklistProperty *)LL;
linklistnode * pCurrent = &(mylist->header);
linklistnode * pClear = nullptr;
while(pCurrent != nullptr)
{
pClear = pCurrent;
pCurrent = pCurrent->next;
pClear->next = nullptr;
}
mylist->size = 0;
}
//个数
int Size_LL(LLHeader LL)
{
if(nullptr == LL)
return 0;
linklistProperty * mylist = (linklistProperty *)LL;
return mylist->size;
}