文章目录
1.链表
1.1 线性表
概念:
零个或多个数据元素的有限序列
线性表的数据集合为{a1,a2,…,an},假设每个元素的类型均为DataType。其中,除第一个元素a1外,每一个元素有且只有一个直接前驱元素,除了最后一个元素an外,每一个元素有且只有一个直接后继元素。数据元素之间的关系是一对一的关系。
前驱:
如 a1 是 a2的前驱,a2 是 a3的前驱
后继
如 a2 是 a1的后继,a3 是 a2的后继
线性表的特点
- 有限的序列
- 序列中的每一个元素都有唯一的前驱和后继,除了开头和结尾两个节点
上网
线性表
线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列等,线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构(存储结构)上并不一定是连续的,线性表在物理上存储时,通常以顺序表和链式结构的形式存储。
线性表的顺序存储—>顺序表
是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
线性表的链式存储
线性表中的数据结点在内存中的位置是任意的,即逻辑上相邻的数据元素在物理位置(内存存储的位置)上不一定相邻。
1.2 顺序表
概念:
用一组地址连续的存储单元依次存储线性表的数据元素,这种存储结构的线性表称为顺序表。
特点:
逻辑上相邻的数据元素,物理次序也是相邻的。
只要确定好了存储线性表的起始位置,线性表中任一数据元素都可以随机存取,所以线性表的顺序存储结构是一种随机存取的储存结构,因为高级语言中的数组类型也是有随机存取的特性,所以通常我们都使用数组来描述数据结构中的顺序储存结构,用动态分配的一维数组表示线性表。
顺序表的优缺点
优点:
1.无须为表中元素之间的逻辑关系而增加额外的存储空间;可以快速的存取表中任一位置的元素。
2.存储密度为1最高,因为没有指针域空间利用率高
3.随机存取,按位置访问元素的时间复杂度为O(1),直接根据数组下标访问元素
缺点:
插入和删除操作需要移动大量元素;当线性表长度较大时,难以确定存储空间的容量;造成存储空间的“碎片”。
动态顺序表增容会付出一定性能消耗,其次可能还是会存在一定的空间浪费(不可能扩容的刚刚好)
在头部或者中部左右的插入删除,需要移动元素,时间复杂度为O(N),效率低。
1.3 单链表
链表简介
在链式结构中,除了要存储数据元素的信息外,还要存储它的后继元素的存储地址。因此,为了表示每个数据元素 ai 与其直接后继元素 ai+1 之间的逻辑关系,对数据ai来说,除了存储其本身的信息之外,还需要存储一个指示其直接后继的信息(即直接后继的存储位置)。我们把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域。指针域中存储的信息称做指针或链。这两部分信息组成数据元素ai的存储映像,称为结点(Node)。
n个结点(ai的存储映像)链结成一个链表,即为线性表(a1, a2, …, an)的链式存储结构,因为此链表的每个结点中只包含一个指针域,所以叫做单链表。
简单来讲:内存是不连续的,元素会各自被分配一块内存,内存和内存之间用指针进行相连
链式存储结构的优点:
空间利用率高需要一个空间就分配一个空间
数据元素的逻辑次序靠节点的指针来指示,插入和删除时不需要移动数据结点,任意位置插入删除时间复杂度为O(1)
链式存储结构的缺点:
存储密度小,每个结点的指针域需要额外占用存储空间。当每个结点的数据域所占字节不多时,指针域所占空间的比重显得很大,存储密度大空间利用率越大。
顺序表因为只有数据域,没有指针域所以它的存储密度为最大1
不过这个问题,一个结点也就多几个指针,最多8个字节,所以若是在现代计算机这点空间已经不算什么,不过如果是像单片机这种嵌入式设备内存较小,所以还是会有一点点影响的
链式存储结构是非随机存取结构,对任一结点的操作都要从头指针依次查找到该节点,算法复杂度较高。
总结:
顺序表的缺点就是链表的优点,而链表的缺点就是顺序表的优点,所以说不能说链表一定就比顺序表高级,我们要视情况而定。
单链表的介绍
线性表的链式存储
线性表中的数据结点在内存中的位置是任意
的,即逻辑上是线性
的数据元素在物理位置(内存存储的位置)上不一定相邻。
结点为什么在内存中是随机存储的呢?
因为我们产生一个结点要给他分配内存是动态分配出来的(malloc),而分配的结点的内存的地址是随机的,所以结点的地址是随机的,也就是说结点在内存中的存储是随机的。
头节点
概念
头结点:
是虚拟出来的一个节点,不保存数据。头结点的next指针指向链表中的第一个节点。对于头结点,数据域可以不存储任何信息,也可存储如链表长度等附加信息。头结点不是链表所必需的。
头指针:
是指向第一个结点的指针,如果链表没有引入头结点,那么头指针指向的是链表的第一个结点。头指针是链表所必需的。
[注意]无论是否有头结点,头指针始终指向链表的第一个结点。如果有头结点,头指针就指向头结点。
为何引入头结点
1)对链表的删除、插入操作时,第一个结点的操作更方便
如果链表没有头结点,那么头指针指向的是链表的第一个结点,当在第一个结点前插入一个节点时,那么头指针要相应指向新插入的结点,把第一个结点删除时,头指针的指向也要更新。也就是说如果没有头结点,我们需要维护着头指针的指向更新。因为头指针指向的是链表的第一个结点,如果引入头结点的话,那么头结点的next始终都是链表的第一个结点。
带头结点的单链表:
不带头结点的单链表:
引入头结点后,头指针指向头结点,那么无论链表是否为空,头指针均不为空。
2)统一空表和非空表的处理
有了头结点之后头指针指向头结点,不论链表是否为空,头指针总是非空,而且头结点的设置使得对链表的第一个位置上的操作与在表中其它位置上的操作一致,即统一空表和非空表的处理。
当你需要创造一条新链表的时候,可以使用头结点简化边界情况的处理。
比如说,让你把两条有序链表合并成一条新的有序链表,是不是要创造一条新链表?再比你想把一条链表分解成两条链表,是不是也在创造新链表?这些情况都可以使用虚拟头结点简化边界情况的处理。
单链表的基本操作
注意,后面的基本都是带有头节点的
不带头结点单向不循序链表:
当链表为空时,头指针指向空,当链表不为空时头指针必须指向第一个结点
创建一个单链表
对于每个链表结点,除了存放元素自身的信息外,还需要存放一个指向其后继的指针。
typedef struct Node
{ //定义单链表结点类型
int data; //数据域,可以是别的各种数据类型,本文统一用int类型
struct Node *next; //指针域
}Node;
初始化
通常会用头指针来标识一个单链表,头指针为NULL时表示一个空表。但是,为了操作方便,会在单链表的第一个结点之前附加一个结点,称为头结点。
头结点的数据域可以不设任何信息,也可以记录表长等信息。
头结点的指针域指向线性表的第一个元素结点。如下图所示:
头结点和头指针的区分:
不管带不带头结点,头指针始终指向单链表的第一个结点,而头结点是带头结点的单链表中的第一个结点,结点内通常不存储信息。
注意,以下的都是包含了头节点的
那么单链表的初始化操作就是申请一个头结点,将指针域置空。
/* 申请内存 */
/* 列表初始数据为0 */
/* 列表的下一项指向空 */
Node* InitList()
{
Node* list = (Node*)malloc(sizeof(Node));
list -> data = 0;
list -> next = NULL;
return list;
}
头插法
所谓头插法建立单链表是说将新结点插入到当前链表的表头,即头结点之后。如图所示:
头插法建立的单链表中结点的次序和输入数据的顺序不一致,是相反的。若希望两者的顺序是一致的,则可采用尾插法建立单链表。
可以理解为:
1 2 3 中要用头插法插入一个数字4,那么插入后的排序应该是4 1 2 3
/*
申请内存
插入数据为形参中待插入的数据
更新指向第一项的位置
更新原先列表的下一项指向待插入数据
总数据数++
*/
void headInsert(Node* list, int data)
{
Node* node = (Node*)malloc(sizeof(Node));
node -> data = data;
node -> next = list -> next;
list -> next = node;
list -> data++;
}
尾插法
所谓尾插法建立单链表是说将新结点插入到当前链表的表尾,即头结点之后。如图所示:
可以理解为:
0 1 2 3 中要用头插法插入一个数字4,那么插入后的排序应该是 0 1 2 3 4
/*
申请内存
插入数据为形参中待插入的数据
待插入的指针的下一项指向NULL
找到列表的最后一项,然后当list -> next为NULL的时候退出while
更新原先列表的下一项 指向 Node
总数据数++
*/
void tailInsert(Node* list, int data)
{
Node* head = list;
Node* node = (Node*)malloc(sizeof(Node));
node -> data = data;
node -> next = NULL;
list = list -> next;
while(list -> next)
{
list = list -> next;
}
list -> next = node;
head ->data ++;
}
删除操作
将单链表的第 i 个结点删除。
可以理解为:
0 1 2 3 4中要删除一个数字2,那么插入后的排序应该是 0 1 3 4
0 1 2 2 3 4中要删除一个数字2,那么插入后的排序应该是 0 1 2 3 4
/*
先保存当先列表与列表的第一项的位置
然后找到待删除的地点
将列表的下一项 指向 列表下一项的下一项
释放列表的下一项的内存
之后将指针都后移一个
之后列表数据总数相减
*/
void delete(Node* list, int data)
{
Node* preNode = list;
Node* node = list -> next;
while(node)
{
if(node -> data == data)
{
preNode -> next = node->next;
free(node);
list -> data --;
break;
}
preNode = node;
node = node -> next;
}
}
打印
void printfList(Node* list)
{
list = list -> next;
while(list)
{
printf("%d",list -> data);
list = list -> next;
}
printf("\n");
}
总代码
#include <stdio.h>
#include "stdlib.h"
typedef struct Node
{
int data;
struct Node *next;
}Node;
Node* InitList()
{
Node* list = (Node*)malloc(sizeof(Node));
list -> data = 0;
list -> next = NULL;
return list;
}
void headInsert(Node* list, int data)
{
Node* node = (Node*)malloc(sizeof(Node));
node -> data = data;
node -> next = list -> next;
list -> next = node;
printf("headInsert中list -> next的地址为%p\r\n",list -> next);
list -> data++;
}
void tailInsert(Node* list, int data)
{
Node* head = list;
Node* node = (Node*)malloc(sizeof(Node));
node -> data = data;
node -> next = NULL;
list = list -> next;
printf("tailInsert中list -> next的地址为%p\r\n",list -> next);
while(list -> next)
{
list = list -> next;
}
list -> next = node;
head ->data ++;
}
void delete(Node* list, int data)
{
Node* preNode = list;
Node* node = list -> next;
while(node)
{
if(node -> data == data)
{
preNode -> next = node->next;
free(node);
node=NU
list -> data --;
break;
}
preNode = node;
node = node -> next;
}
}
void printfList(Node* list)
{
list = list -> next;
while(list)
{
printf("%d",list -> data);
list = list -> next;
}
printf("\n");
}
void main()
{
Node* list = InitList();
headInsert(list,1);
headInsert(list,2);
headInsert(list,3);
headInsert(list,3);
tailInsert(list,4);
tailInsert(list,5);
tailInsert(list,6);
printfList(list);
delete(list,3);
printfList(list);
}
1.4 单链表循环
循环单链表是单链表的另一种形式,其结构特点链表中最后一个结点的指针域不再是结束标记,而是指向整个链表的第一个结点,从而使链表形成一个环。
循环单链表的表尾结点的next指针总是指向头结点。
所以在初始化循环单链表的时候,需要记得将头结点的next指针指向头结点自己:
#include <stdio.h>
#include "stdlib.h"
typedef struct Node
{
int data;
struct Node *next;
}Node;
/*所以在初始化循环单链表的时候,需要记得将头结点的next指针指向头结点自己:*/
Node* InitList()
{
Node* list = (Node*)malloc(sizeof(Node));
list -> data = 0;
list -> next = list;
return list;
}
void headInsert(Node* list, int data)
{
Node* node = (Node*)malloc(sizeof(Node));
node -> data = data;
node -> next = list -> next;
list -> next = node;
list -> data++;
}
void tailInsert(Node* list, int data)
{
Node* head = list;
Node* node = (Node*)malloc(sizeof(Node));
node -> data = data;
while(list -> next != head)
{
list = list -> next;
}
node -> next = head;
list -> next = node;
head ->data ++;
}
void delete(Node* list, int data)
{
Node* preNode = list;
Node* node = list -> next;
while(node != list)
{
if(node -> data == data)
{
preNode -> next = node->next;
free(node);
list -> data --;
break;
}
preNode = node;
node = node -> next;
}
}
void printfList(Node* list)
{
Node* head = list;
list = list -> next;
while(list != head)
{
printf("%d->", list -> data);
list = list -> next;
}
printf("\n");
}
void main()
{
Node* list = InitList();
headInsert(list,1);
headInsert(list,2);
headInsert(list,3);
headInsert(list,3);
tailInsert(list,4);
tailInsert(list,5);
tailInsert(list,6);
printfList(list);
delete(list,3);
printfList(list);
}
判断结点是否是循环单链表的表尾结点
只需判断该结点的next指针是否指向头结点即可,因为循环链表的最后一个结点的next指针总是指向头结点的:
判断循环单链表是否为空
只要判断头结点的next指针是否指向自己就可以了,因为循环链表的最后一个结点的next指针总是指向头结点的,如果为空,那就只能是头结点的next指针指向自己了:
循环单链表有一些重要的性质:
1、从一个结点出发,无论这个结点位于链表的哪里,都可以找到其他任何一个结点,而单链表不行。
2、在一些情况下,我们需要频繁对链表的头部和尾部进行操作,此时使用循环单链表就很有用,原因是对于单链表,已知头结点,想找到最后一个结点的话,时间复杂度是O(n);但是对于循环单链表,我们可以一开始就把头指针指向尾结点,这样找到尾结点所需时间复杂度是O(1),因为尾结点的next指针总是指向头结点,所以找到头结点的时间复杂度也是O(1)。因此,循环单链表在这种情况下的表现明显优于普通的单链表。
1.5 双链表
双链表的定义
为什么引入双链表?
单链表的结点中只有一个指向其后继的指针,使得单链表要访问某个结点的前驱结点时,只能从头开始遍历,访问后驱结点的复杂度为O(1),访问前驱结点的复杂度为O(n)。为了克服上述缺点,引入了双链表。
双链表的结点中有两个指针prior和next,分别指向前驱结点和后继结点。
双链表中结点类型的描述:
typedef struct Node
{
int data;
struct Node *next;
struct Node *pre;
}Node;
双链表上的操作
双链表与单链表一样,为了操作方便也可以加入头结点,那么初始化双链表其实就是定义一个头结点,然后将指针域置空。
初始化
/*
数据总数data 清零
同时列表的前端与后继都指向为空
*/
Node* InitList()
{
Node* list = (Node*)malloc(sizeof(Node));
list -> data = 0;
list -> next = NULL;
list -> pre = NULL;
return list;
}
插入操作
在双链表中p所指的结点之后插入结点*s,其指针的变化过程如下图所示:
头插法
/*
注意,这是头插法,
所以待插入列表数据的前端永远指向原先列表
待插入列表数据的后端如果是第一次插入则指向为空,后续都指向下一个列表
这里涉及到了4条“线”的连接
*/
void headInsert(Node* list, int data)
{
Node* node = (Node*)malloc(sizeof(Node));
node -> data = data;
node -> next = list -> next;
node -> pre = list;
/*
* 注意,这里是以防第一次插入的时候
* 不能是NULL直接指向node
*/
if(list -> next)
{
list -> next ->pre = node;
}
list -> next = node;
list -> data++;
}
尾插法
/*
这里涉及到了4条“线”的连接
尾插法最主要是找到最后一项的位置
待插入列表数据的后继永远指向NULL,也就是list -> next
*/
void tailInsert(Node* list, int data)
{
Node* head = list;
Node* node = (Node*)malloc(sizeof(Node));
node -> data = data;
while(list -> next)
{
list = list -> next;
}
node -> next = list -> next;
node -> pre = list;
list -> next = node;
head ->data ++;
}
删除操作
void delete(Node* list, int data)
{
Node* preNode = list;
Node* node = list -> next;
while(node)
{
if(node -> data == data)
{
preNode -> next = node->next;
/*防止最后一项删除不掉,如果是最后一项的话,不需要执行这一步操作,如果执行了就是NULL前项指向preNode了*/
if(node -> next)
node -> next -> pre = preNode;
free(node);
list -> data--;
break;
}
preNode = node;
node = node -> next;
}
}
总代码
#include <stdio.h>
#include "stdlib.h"
typedef struct Node
{
int data;
struct Node *next;
struct Node *pre;
}Node;
Node* InitList()
{
Node* list = (Node*)malloc(sizeof(Node));
list -> data = 0;
list -> next = NULL;
list -> pre = NULL;
return list;
}
void headInsert(Node* list, int data)
{
Node* node = (Node*)malloc(sizeof(Node));
node -> data = data;
node -> next = list -> next;
node -> pre = list;
if(list -> next)
{
list -> next ->pre = node;
}
list -> next = node;
list -> data++;
}
void tailInsert(Node* list, int data)
{
Node* head = list;
Node* node = (Node*)malloc(sizeof(Node));
node -> data = data;
while(list -> next)
{
list = list -> next;
}
node -> next = list -> next;
node -> pre = list;
list -> next = node;
head ->data ++;
}
void delete(Node* list, int data)
{
Node* preNode = list;
Node* node = list -> next;
while(node)
{
if(node -> data == data)
{
preNode -> next = node->next;
if(node -> next)
node -> next -> pre = preNode;
free(node);
list -> data--;
break;
}
preNode = node;
node = node -> next;
}
}
void printfList(Node* list)
{
Node* head = list;
list = list -> next;
while(list != NULL)
{
printf("%d->", list -> data);
list = list -> next;
}
printf("NULL\n");
}
void main()
{
Node* list = InitList();
headInsert(list,1);
headInsert(list,2);
headInsert(list,3);
headInsert(list,3);
tailInsert(list,4);
tailInsert(list,5);
tailInsert(list,6);
printfList(list);
delete(list,3);
delete(list,6);
printfList(list);
}
双链表循环
头插法
void headInsert(Node* list, int data)
{
Node* node = (Node*)malloc(sizeof(Node));
node -> data = data;
node -> next = list -> next;
node -> pre = list;
list -> next ->pre = node;
list -> next = node;
list -> data++;
}
注意顺序:
list -> next ->pre = node;
list -> next = node;
如果是第一次进入函数:
- 此时list -> next为list
- list -> next ->pre = list->pre ,所以list的前一项为node
- 之后才更新list -> next = node;所以list的后一项为node
如果是第n次进入函数:
- 此时list -> next为上一次进入的node,而上面的node -> next = list -> next;把上一次进入的node挤到第二项
- list -> next ->pre = (上一次的)node->pre ,也就是第二项指向第一项
- 之后才更新list -> next = node;
根据1,2步的操作,才成功将新头插进入的函数与原先函数进行连接
正确插入4个数:1,2,3,3
void headInsert(Node* list, int data)
{
Node* node = (Node*)malloc(sizeof(Node));
node -> data = data;
node -> next = list -> next;
node -> pre = list;
list -> next ->pre = node;
list -> next = node;
printf("list -> %p\r\n",list);
printf("list->pre -> %p\r\n",list->pre);
printf("list->next -> %p\r\n",list->next);
printf("node->pre -> %p\r\n",node->pre);
printf("node->next -> %p\r\n",node->next);
printf("\r\n");
list -> data++;
}
但是如果是反过来的话:
void headInsert(Node* list, int data)
{
Node* node = (Node*)malloc(sizeof(Node));
node -> data = data;
node -> next = list -> next;
node -> pre = list;
list -> next = node;
list -> next ->pre = node;
printf("list -> %p\r\n",list);
printf("list->pre -> %p\r\n",list->pre);
printf("list->next -> %p\r\n",list->next);
printf("node->pre -> %p\r\n",node->pre);
printf("node->next -> %p\r\n",node->next);
printf("\r\n");
list -> data++;
}
如果是第一次进入函数:
- 更新list -> next = node
- 此时list -> next = node,而node->pre = node
- 那这样的话就是node指向node了
- 然后因为初始化的时候是list->pre还是指向list,这个并没有改变所以list的上一项还是指向list
如果是第N次进入函数:
- list上一项永远指向list,node的上一项永远指向node
- 只有两条“线”成立,其他两条线是错的
错误插入:
删除操作简化
void delete(Node* list, int data)
{
Node* node = list -> next;
while(node != list)
{
if(node -> data == data)
{
node -> pre -> next = node->next;
node -> next -> pre = node -> pre;
free(node);
list -> data--;
break;
}
node = node -> next;
}
}
总代码
#include <stdio.h>
#include "stdlib.h"
typedef struct Node
{
int data;
struct Node *next;
struct Node *pre;
}Node;
Node* InitList()
{
Node* list = (Node*)malloc(sizeof(Node));
list -> data = 0;
list -> next = list;
list -> pre = list;
return list;
}
void headInsert(Node* list, int data)
{
Node* node = (Node*)malloc(sizeof(Node));
node -> data = data;
node -> next = list -> next;
node -> pre = list;
list -> next ->pre = node;
list -> next = node;
list -> data++;
}
void tailInsert(Node* list, int data)
{
Node* head = list;
Node* node = (Node*)malloc(sizeof(Node));
node -> data = data;
while(list -> next != head)
{
list = list -> next;
}
node -> next = list -> next;
node -> pre = list;
list -> next = node;
head -> pre = node;
head ->data ++;
}
void delete(Node* list, int data)
{
Node* node = list -> next;
printf("node->%p\r\n",node);
while(node != list)
{
if(node -> data == data)
{
node -> pre -> next = node->next;
node -> next -> pre = node -> pre;
free(node);
list -> data--;
break;
}
node = node -> next;
}
}
void printfList(Node* list)
{
Node* head = list;
list = list -> next;
while(list != head)
{
printf("%d->", list -> data);
list = list -> next;
}
printf("NULL\n");
}
void main()
{
Node* list = InitList();
headInsert(list,1);
headInsert(list,2);
headInsert(list,3);
headInsert(list,3);
tailInsert(list,4);
tailInsert(list,5);
tailInsert(list,6);
printfList(list);
delete(list,3);
delete(list,6);
printfList(list);
}
标签:node,Node,结点,list,next,链表,总集,数据结构,data
From: https://blog.csdn.net/qq_62548908/article/details/142446768