目录
单链表的定义和表示
什么是链表
单链表的基本操作的实现
1、单链表的初始化
2、单链表的取值
3、单链表的按值查找
4、单链表的插入操作
5、单链表的删除元素操作
6、数组和单链表的效率PK
7、单链表的整表创建
8、单链表的整表删除
循环链表
双向链表
1、双向链表的描述:
2、双向链表的插入
3、双向链表的删除
顺序表和链表的比较
1、空间性能的比较
2、时间性能的比较
链表与数组的主要区别
数组的优点
数组的缺点
链表的优点
链表的缺点
本文部分内容摘自极客时间《数据结构与算法之美》和网络,仅供笔者学习和复习用。
1.和数组一样,链表也是一种线性表。
2.从内存结构来看,链表的内存结构是不连续的内存空间,是将一组零散的内存块串联起来,从而进行数据存储的数据结构。
一个链表有很多个节点,各个节点之间通过指针连接起来,所以各个结点之间的位置可以不连续,也就是可以放在不同的位置,所以在空间上可以是不连续的;但对于一个节点,因为节点内部是一个整体,所以就要占用连续的存储空间。
各个节点在链表中都是一个指针变量,也就是一个32位的字节。每一个指针变量都要分配内存,而这些指针变量的内存地址是连续的。每个指针所指向的地址可以是连续的,当然也可以不是连续的,这个顺序链表有着本质的区别。
3.链表中的每一个内存块被称为节点Node。节点除了存储数据外,还需记录链上下一个节点的地址,即后继指针next。
4.每个节点包括两个域:其中存储数据元素的域称为数据域,存储直接侯冀存储位置的域称为指针域. 指针域存储的信息称作指针或链。
5.n个节点(ai(1<=i<=n)的存储映像)链结成一个链表,即为线性表:
(a1,a2,a3,···an)
的链式存储结构。又由于此链表的节点中只包含一个指针域,故又称为线性链表或单链表。
单链表可由头指针唯一确定,在C语言中可以用“结构指针”来描述:
//单链表的存储结构
typedef struct LNode {
ElemType data ; //节点的数据域
struct LNode *next; //节点的指针域
}LNode ,*LinkList; //LinkList为指向结构体LNode的指针类型
为了提高程序的可阅读性,在此对同一结构体指针类型起了两个名称,LinkList与LNode*,两者本质上是相等的,通常习惯用LinkList定义单链表,强调定义某个单链表的头指针;用LNode*定义指向单链表的任意节点的指针变量。
下面对 首元结点、头结点、头指针三个容易混淆的概念加以说明:
Status InitList (LinkList &L ) {
L = new LNode ; //生成新的节点作为头结点,用头指针L指向头结点
L->next = Null; //头结点的指针置空
return OK;
}
Statua GetElem (LinkList L,int i ,ElemType &e) {
//再带头结点的单链表L中根据序号i获取元素的值,用e返回第i个元素中的值
p = L->next;j = 1; //初始化,p指向首元结点,计数器j初始值记为1
while (p&&jnext;
++j;
}
if (!p||j>i) { //i值不合法
return ERROR;
}
e = p->data; //取第i个结点的数据域
return OK;
}
LNode *LocateElem (LinkList L,ElemType e) {
p = L->next; //初始化,p指向首元结点
while (p && p->data !=e) { //顺着链域向后扫描,直到p为空或者p所指向的结点的数据域等于e
p = p ->next; //p指向下一个结点
}
return p; //擦汗信号成功返回值为e的结点的地址,查找失败p为NULL
}
Status ListInsert (LinkList L, int i,ElemType e){
//在带头结点的单链表L中第i个位置插入值为e的新结点
p = L; j = 0;
while (p && (j < i-1)) {
p = p->next; //查找第i-1个结点,p指向该结点
++j;
}
if (!p || j > i-1) { //i>n+1或者i<1
return ERROR;
}
s = new LNde; //生成新结点
//s = (LinkList)malloc(sizeof(Node));
s ->data = e; //将结点*是、的数据域置为e
s ->next = p ->next; //新结点s的指针域指向结点ai
p ->next = s; //把结点*p的指针域指向新结点*s
return OK;
}
当然了关于插入操作的话这有一个常考点:
上面插入操作代码中的
s ->next = p ->next;
p ->next = s;
两者顺序是否可以倒过来?
如果先执行p ->next = s的话,s的地址就覆盖了p ->next的地址,那么接下来在执行s ->next = p ->next的话就会发现s的next又指向了前面的p ->next;这样一来s的next又指向了自己,所以两者的顺序不能交换!
Status ListDelete (LinkList *L,int i ,ElemType *e) {
//删除第i个结点,并把第i个结点的数据域用*e接受
int j ;
LinkList p , q;
p = *L;
j = 1;
while (p ->next && jnext;
++j;
}
if (!p || j>i) { //如果为空表或者到达了表的末尾,返回错误
return ERROR;
}
q = p ->next; //把要删除节点的指针域给q
p ->next = q ->next; //然后把q的next指向原来p的next指向的结点
*e = p ->data; //接收删除元素
free(q); //释放内存
return OK;
}
学了单链表的插入和删除操作后,我发现两者无论是删除还是插入,它们无非都包含两部分:
从整个算法来说,我们很容易的推出它们的时间复杂度都是O(n);
详细来说啊,只要你不知道第i个元素的具体位置,你就得遍历查找她。
那么不就是说这个单链表数据结构在插入和删除操作上和线性表的顺序存储结构相比没啥太大的优势了么?
仔细一想啊,这个单链表还真有它存在的道理啊
在某些特殊的情况下,比如我想在第i个位置后面连续地插入10个、100个、1000个甚至更多元素的时候:
头插法(前插法)建立单链表
#include
using namespace std;
void CreateList_H (LinkList &L, int n) {
L = new LNode;
L ->next = NUll; //创建一个带头结点的空链表
for (i = 0;i>p ->data; //生成新结点p
p ->next = L ->next; //把原先头结点指针域指向的结点用新节点p指向
L ->next = p; //把新结点p查到头结点后面
}
}
尾插法(后插法)建立单链表
#include
using namespace std;
void CreateList_R (LinkList &L, int n) {
L = new LNode;
L ->next = NUll; //创建一个带头节点的空链表
r = L; //尾指针r指向头节点
for (i = 0;i>p ->data; //生成新节点p
p ->next = NULL; //把新节点p的指针指向NULL
r ->next = p; //把原先的尾指针指向新节点
r = p; //r指向新的尾节点p
}
}
Status ClearList (LinkList *L) {
LinkList p, q;
p = (*L) ->next; //把p设为首元结点
while (p) {
q = p ->next; //先把p的指针域丢给q
free (p); //再把p扔掉
p = q; //最后把刚才的q作为新p
}
(*L) ->next = null; //完事儿后记得把*L指向NULL
return OK;
}
循环链表是另一种形式的链式存储结构,它的特点是最后一个元素的指针域指向头结点,整个链表形成一个环。那么无论从那个结点开始都可以找到其他结点。
循环链表的操作和单链表的大同小异,但是当遍历链表的时候,判断当前指针是否指向尾结点的终止条件不同。在单链表中的条件是:p!=NUll或者p ->next !=NULL,循环单链表的终止条件不是p !=L 或者 p ->next !=L.
以上讨论的链式存储结构的结点中只有一个指示 直接后继的指针域,由此,从某个结点出发只能顺指针向后寻查其他结点。若要寻查结点的直接前驱,则必须从表头指针出发。换句话说,在单链表中,查找直接后继结点的执行时间为O(1),而查找直接前驱的执行时间为O(n)。为克服单链表这种单向性的缺点,可利用双向链表( Double Linked List )。
顾名思义,在双向链表的结点中有两个指针域,一个指向直接后继,另-个指向直接前驱。
typedef struct DuLNode {
ElemType data ; //当然得有数据域
struct DuLNode *prior; //指向前驱的指针域
struct DuLNode *next; //指向后驱的指针域
}DuLNode,*DuLinkList;
Status ListInsert_Dul (DuLinkList &L, int i ,ElemType e) {
//在带头结点的双向链表L的第i个元素前面插入元素e
if (!(p = GetElem (L,i))) //在L中确定第i个元素的位置指针
return ERROR; //p为NULL时,第i个元素不存在
s = new DuLNode ; //新建结点
s ->data = e; //新结点赋值
s ->prior = p ->prior; //把原先结点的前指针留给新结点
p ->prior ->next = s; //原先结点的前驱结点的指针域指向新结点s
s ->next = p; //把p塞到新结点s的后面
p ->prior = s; //原先结点的前指针指向新结点s
return OK;
}
Status ListInsert_Dul (DuLinkList &L, int i) {
//删除带头结点的双向链表L的第i个元素
if (!(p = GetElem (L,i))) //在L中确定第i个元素的位置指针
return ERROR; //p为NULL时,第i个元素不存在
p ->prior ->next = p ->next; //p结点的前驱结点的后继指针指向结点p的下一个结点
p ->next ->prior = p ->prior; //p节点的后继结点的前驱指针指向p结点的前驱结点
Delete p; //释放p
return OK;
}
查找
插入、删除和随机访问的时间复杂度