链表的底层存储结构
数组需要一块连续的内存空间来存储,对内存的要求比较高。如果我们申请一个 100MB 大小的数组,当内存中没有连续的、足够大的存储空间时,即便内存的剩余总可用空间大于 100MB,仍然会申请失败。而链表恰恰相反,它并不需要一块连续的内存空间,它通过“指针”将一组零散的内存块串联起来使用,所以如果我们申请的是 100MB 大小的链表,根本不会有问题
![img](https://tc.pengyuhao715.work/20201217144457.jpeg)
单链表
链表通过指针将一组零散的内存块串联在一起。其中,我们把内存块称为链表的“结点”。为了将所有的结点串起来,每个链表的结点除了存储数据之外,还需要记录链上的下一个结点的地址。如图所示,我们把这个记录下个结点地址的指针叫作后继指针 next。
![img](https://tc.pengyuhao715.work/20201217145455.jpeg)
其中有两个结点是比较特殊的,它们分别是第一个结点和最后一个结点。我们习惯性地把第一个结点叫作头结点,把最后一个结点叫作尾结点。其中,头结点用来记录链表的基地址。有了它,我们就可以遍历得到整条链表。而尾结点特殊的地方是:指针不是指向下一个结点,而是指向一个空地址 NULL,表示这是链表上最后一个结点。
与数组一样,链表也支持数据的查找、插入和删除操作
在进行数组的插入、删除操作时,为了保持内存数据的连续性,需要做大量的数据搬移,所以时间复杂度是 O(n)。而在链表中插入或者删除一个数据,我们并不需要为了保持内存的连续性而搬移结点,因为链表的存储空间本身就不是连续的。所以,在链表中插入和删除一个数据是非常快速的
从图中我们可以看出,针对链表的插入和删除操作,我们只需要考虑相邻结点的指针改变,所以对应的时间复杂度是 O(1)
![img](https://tc.pengyuhao715.work/20201217145758.jpeg)
弊端:链表随机访问的性能没有数组好,需要 O(n) 的时间复杂度
链表要想随机访问第 k 个元素,就没有数组那么高效了。因为链表中的数据并非连续存储的,所以无法像数组那样,根据首地址和下标,通过寻址公式就能直接计算出对应的内存地址,而是需要根据指针一个结点一个结点地依次遍历,直到找到相应的结点。
循环链表:一种特殊的单链表
它跟单链表唯一的区别就在尾结点,单链表的尾结点指针指向空地址,而循环链表的尾结点指针是指向链表的头结点
![img](https://tc.pengyuhao715.work/20201217150347.jpeg)
和单链表相比,循环链表的优点是从链尾到链头比较方便。当要处理的数据具有环型结构特点时,就特别适合采用循环链表。比如著名的约瑟夫问题。
双向链表
单向链表只有一个方向,结点只有一个后继指针 next 指向后面的结点。而双向链表,顾名思义,它支持两个方向,每个结点不止有一个后继指针 next 指向后面的结点,还有一个前驱指针 prev 指向前面的结点。
![img](https://tc.pengyuhao715.work/20201217150804.jpeg)
双向链表需要额外的两个空间来存储后继结点和前驱结点的地址。所以,如果存储同样多的数据,双向链表要比单链表占用更多的内存空间。虽然两个指针比较浪费存储空间,但可以支持双向遍历,这样也带来了双向链表操作的灵活性。
相比单链表,双向链表适合解决这些问题:
- O(1) 时间复杂度的情况下找到前驱结点
- 删除给定指针指向的结点时直接得出前驱节点,而不需要遍历一次寻找到当前节点的前驱是谁
- 插入给定指针指向的结点时做到O(1)复杂度,单链表需要O(n),道理同上
- 对于有序链表,的查询的效率比单链表高。因为,可以记录上次查找的位置 p,每次查询时,根据要查找的值与 p 的大小关系,决定是往前还是往后查找,平均只需要查找一半的数据
Java中的LinkedHashMap运用到了双向链表
双链表插入操作
假设将p所指结点后插入一个s节点,要首先将s的后继节点作位原先p节点后继的前驱,其中第四行代码必须最后实现,不然就找不到p的后继节点。
s->next = p->next;
p->next->prior= s;
s->prior = p;
p->next = s;
![image-20201218174325087](https://tc.pengyuhao715.work//image-20201218174325087.png)
用空间换时间
当内存空间充足的时候,如果我们更加追求代码的执行速度,我们就可以选择空间复杂度相对较高、但时间复杂度相对很低的算法或者数据结构。相反,如果内存比较紧缺,比如代码跑在手机或者单片机上,这个时候,就要反过来用时间换空间的设计思路
缓存实际上就是利用了空间换时间的设计思想。如果我们把数据存储在硬盘上,会比较节省内存,但每次查找数据都要询问一次硬盘,会比较慢。但如果我们通过缓存技术,事先将数据加载在内存中,虽然会比较耗费内存空间,但是每次数据查询的速度就大大提高了
对于执行较慢的程序,可以通过消耗更多的内存(空间换时间)来进行优化;而消耗过多内存的程序,可以通过消耗更多的时间(时间换空间)来降低内存的消耗
双向循环链表
![img](https://tc.pengyuhao715.work/20201217151735.jpeg)
链表和数组性能的对比
![img](https://tc.pengyuhao715.work/20201217151810.jpeg)
数组简单易用,在实现上使用的是连续的内存空间,可以借助 CPU 的缓存机制,预读数组中的数据,所以访问效率更高。而链表在内存中并不是连续存储,所以对 CPU 缓存不友好,没办法有效预读
数组的缺点是大小固定,一经声明就要占用整块连续内存空间。如果声明的数组过大,系统可能没有足够的连续内存空间分配给它,导致“内存不足(out of memory)”。如果声明的数组过小,则可能出现不够用的情况。这时只能再申请一个更大的内存空间,把原数组拷贝进去,非常费时。链表本身没有大小的限制,天然地支持动态扩容,看作是二者最大的区别。
指针
将某个变量赋值给指针,实际上就是将这个变量的地址赋值给指针,或者反过来说,指针中存储了这个变量的内存地址,指向了这个变量,通过指针就能找到这个变量
p->next=q 这行代码是说,p 结点中的 next 指针存储了 q 结点的内存地址
p->next=p->next->next 这行代码表示,p 结点的 next 指针存储了 p 结点的下下一个结点的内存地址
插入结点时,一定要注意操作的顺序,要先将结点 x 的 next 指针指向结点 b,再把结点 a 的 next 指针指向结点 x,这样才不会丢失指针,导致内存泄漏。
![img](https://tc.pengyuhao715.work/20201217192343.jpeg)
x->next = p->next; // 将x的结点的next指针指向b结点;
p->next = x; // 将p的next指针指向x结点;
删除链表结点时,也一定要记得手动释放内存空间,否则,也会出现内存泄漏的问题
添加“哨兵”简化代码难度
new_node->next = p->next;
p->next = new_node;
当向一个空链表中插入第一个结点,需要进行下面这样的特殊处理,其中 head 表示链表的头结点。所以,从这段代码可以发现,对于单链表的插入操作,第一个结点和其他结点的插入逻辑是不一样的。
if (head == null) {
head = new_node;
}
同样,对于删除链表中的最后一个结点,需要做如下特殊处理
if (head->next == null) {
head = null;
}
但如果要删除结点 p(非尾结点) 的后继结点,我们只需要一行代码就可以搞定
p->next = p->next->next;
我们可以看出,针对链表的插入、删除操作,需要对插入第一个结点和删除最后一个结点的情况进行特殊处理。有一说一有点鸹貔
带头链表
为了解决鸹貔问题,就引入了哨兵结点,在任何时候,不管链表是不是空,head 指针都会一直指向这个哨兵结点。我们也把这种有哨兵结点的链表叫带头链表。相反,没有哨兵结点的链表就叫作不带头链表。
![img](https://tc.pengyuhao715.work/20201217193611.jpeg)
注意,哨兵结点是不存储数据的,这样一来插入第一个结点和插入其他结点,删除最后一个结点和删除其他结点,都可以统一为相同的代码实现逻辑。利用哨兵简化编程难度的技巧,在很多代码实现中都有用到,比如插入排序、归并排序、动态规划等
检查链表代码是否正确的边界条件
- 如果链表为空时,代码是否能正常工作?
- 如果链表只包含一个结点时,代码是否能正常工作?
- 如果链表只包含两个结点时,代码是否能正常工作?
- 代码逻辑在处理头结点和尾结点的时候,是否能正常工作?
顺序表示例代码
#include "stdio.h"
#include "windows.h"
#include "stdlib.h"
#define MAXSIZE 20//顺序表最大长度
/*定义顺序表*/
typedef struct {
int data[MAXSIZE];
int length;
}SeqList;
void InitList(SeqList *l)
{
l->length = 0;
}
/*建立顺序表*/
int CreatList(SeqList *l, int a[], int n) {
if (n > MAXSIZE)
{
printf("空间不够,无法建立顺序表。\n");
return 0;
}
for (int k = 0; k < n; k++)
{
l->data[k] = a[k];
}
l->length = n;
return 1;
}
/*判空操作*/
int Empty(SeqList *l)
{
if (l->length == 0)
return 1;
else
return 0;
}
/*求顺序表长度*/
int Length(SeqList *l)
{
return l->length;
}
/*遍历操作*/
void PrintList(SeqList *l)
{
for (int i = 0; i < l->length; i++)
printf("%d ", (l->data[i]));
}
/*按值查找*/
int Locate(SeqList *l,int x)
{
for (int i = 0; i < l->length; i++)
{
if (l->data[i] == x)
{
return i + 1;
}
return 0;
}
return 1;
}
/*按位查找*/
int Get(SeqList *l, int x,int *ptr)
{//若查找成功,则通过指针参数ptr返回值
if ( x <1 || x>l->length){
printf("查找位置非法,查找错误\n");
return 0;
}
else
{
*ptr = l->data[x];
return 1;
}
}
/*插入操作*/
int Insert(SeqList *l, int i, int x)
{
if (l->length > MAXSIZE)
{
printf("上溢错误!");
return 0;
}
if (i<1 || i>l->length)
{
printf("插入位置错误!");
return 0;
}
for (int k = l->length; k > i; k--)
{
l->data[k] = l->data[k - 1];
}
l->data[i] = x;
l->length++;
return 1;
}
/*删除操作*/
int Delete(SeqList *l, int i, int *ptr)
{
if (l->length == 0)
{
printf("发生下溢错误,即将要访问顺序表之前的地址.\n");
return 0;
}
if (i > l->length || i < 1)
{
printf("删除位置错误!\n");
return 0;
}
*ptr = l->data[i - 1];//把要删除的数据返回
for (int j = i; j < l->length; j++)
{
l->data[j - 1] = l->data[j];
}
l->length--;
return 1;
}
/*修改操作*/
int Modify(SeqList *l, int i, int x)
{
if (i > l->length || i < 1)
{
printf("位置错误!\n");
return 0;
}
l->data[i] = x;
return 1;
}
int main()
{
int a[5] = {1,2,3,4,5};
int i, x;
SeqList list1;
InitList(&list1);//初始化顺序表
if (Empty(&list1))
{
printf("初始化顺序表成功!\n");
}
printf("给顺序表赋值:1 2 3 4 5\n遍历并输出顺序表:\n");
CreatList(&list1,a,5 );//建立一个长度为5的线性表
PrintList(&list1);//遍历输出此顺序表
printf("\n在第三位后插入一个100:\n");
Insert(&list1, 3, 100);
PrintList(&list1);
if (Modify(&list1, 3, 50) == 1) {
printf("\n把第三位改成50\n");
PrintList(&list1);
}
if (Delete(&list1, 4, &x) == 1) {
printf("\n把第四位删除,删除的值是%d\n",x);
PrintList(&list1);
}
system("pause");
return 0;
}
单链表示例代码
#include "stdio.h"
#include "windows.h"
#include "stdlib.h"
typedef int ElemType;
typedef struct lnode
{
ElemType data;
struct lnode *next;
} LinkNode;
//均采用带头节点的单链表
//头插法创建单链表,l是头指针
void CreateListF(LinkNode *&l, ElemType a[], int n)
{
LinkNode *s;
l = (LinkNode *)malloc(sizeof(LinkNode));
l->next = NULL;
for (int i = 0; i < n; i++)
{
s = l;
s->data = a[i];
s->next = l->next;
l->next = s;
}
}
//尾插法创建单链表,r是尾指针
void CreateListR(LinkNode *&l, ElemType a[], int n)
{
LinkNode *s, *r;
l = (LinkNode *)malloc(sizeof(LinkNode));
r = l;
for (int i = 0; i < n; i++)
{
s = (LinkNode *)malloc(sizeof(LinkNode));
s->data = a[i];
r->next = s;
r = s;
}
r->next = NULL;
}
//初始化线性表
void InitList(LinkNode *&l)
{
l = (LinkNode *)malloc(sizeof(LinkNode));
l->next = NULL;
}
//销毁线性表
void DestroyList(LinkNode *&l)
{
LinkNode *pre = l, *p = l->next; //pre指向p的前驱结点
while (p != NULL)
{
free(pre);
pre = p;
p = pre->next;
}
free(pre);
}
//判断是否为空表
bool ListEmpty(LinkNode *l)
{
return (l->next == NULL);
}
//求线性表长度
int ListLength(LinkNode *L)
{
int n = 0;
LinkNode *p = L;
while (p != NULL)
{
n++;
p = p->next;
}
return n;
}
//输出线性表
void DispList(LinkNode *L)
{
LinkNode *p = L->next;
while (p != NULL)
{
printf("%d ", p->data);
p = p->next;
}
printf("\n");
}
//求某个元素元素值
bool GetElem(LinkNode *L, int i, ElemType &e)
{
int j = 0;
LinkNode *p = L;
if (i <= 0)
return false;
while (j < i && p != NULL)
{
j++;
p = p->next;
}
if (p == NULL)
{
return false;
}
else
{
e = p->data;
return true;
}
}
//按照元素值查找节点
int LocateElem(LinkNode *L, ElemType e)
{
int i = 1;
LinkNode *p = L->next;
while (p != NULL && p->data != e)
{
p = p->next;
i++;
}
if (p == NULL)
return 0;
else
return i;
}
//插入数据元素
bool ListInsert(LinkNode *&L, int i, ElemType e)
{
int j = 0;
LinkNode *p = L, *s;
if (i <= 0)
return false;
while (j < i - 1 && p != NULL)
{
j++;
p = p->next;
}
if (p == NULL)
{
return false;
}
else
{
s = (LinkNode *)malloc(sizeof(LinkNode));
s->data = e;
s->next = p->next;
p->next = s;
return true;
}
}
//删除数据元素
bool ListDelete(LinkNode *&L, int i, ElemType &e)
{
int j = 0;
LinkNode *p = L, *q;
if (i <= 0)
return false;
while (j < i - 1 && p != NULL)
{
j++;
p = p->next;
}
if (p == NULL)
{
return false;
}
else
{
q = p->next;
if (q == NULL)
{
return false;
}
e = q->data;
p->next = q->next;
free(q);
return true;
}
}
int main()
{
LinkNode *L;
ElemType tmp;
printf("初始化链表\n");
InitList(L);
printf("判断是否为空\n");
if (ListEmpty(L))
{
printf("链表为空!\n\n");
}
else
{
printf("链表非空\n\n");
}
//利用a数组创建链表
int a[] = {1, 2, 3, 4, 5};
int lenA = sizeof(a) / sizeof(a[0]);
printf("利用a数组创建链表\n");
CreateListR(L, a, lenA);
printf("\n");
printf("判断创建后的链表是否为空\n");
if (ListEmpty(L))
{
printf("该链表为空\n\n");
}
else
{
printf("该链表非空\n\n");
}
printf("现有链表:");
DispList(L);
printf("\n");
printf("现有链表长度:%d\n\n", ListLength(L));
GetElem(L, 2, tmp);
printf("第二个位置上的元素是:%d\n", tmp);
printf("\n");
printf("获取元素值为3的元素在链表中的位置:\n");
printf("元素值为3的元素在链表中的位置是:%d\n\n", LocateElem(L, 3));
printf("向第1个位置插入元素246:\n");
ListInsert(L, 2, 99);
printf("插入成功!\n\n");
printf("输出插入元素后的链表:\n");
DispList(L);
printf("\n");
printf("删除第二个位置的元素:\n");
ListDelete(L, 2, tmp);
printf("successful!\n\n");
printf("删除元素后的链表:\n");
DispList(L);
printf("\n");
printf("销毁链表\n");
DestroyList(L);
printf("销毁成功\n");
return 0;
}
双向链表示例代码
#include <stdio.h>
#include <malloc.h>
typedef int ElemType;
typedef struct DNode
{
ElemType data;
struct DNode *prior; //指向前驱结点
struct DNode *next; //指向后继结点
} DLinkNode;
//声明双链表结点类型
void CreateListF(DLinkNode *&L, ElemType a[], int n) //头插法建双链表
{
DLinkNode *s;
L = (DLinkNode *)malloc(sizeof(DLinkNode)); //创建头结点
L->prior = L->next = NULL;
for (int i = 0; i < n; i++)
{
s = (DLinkNode *)malloc(sizeof(DLinkNode)); //创建新结点
s->data = a[i];
s->next = L->next; //将结点s插在原开始结点之前,头结点之后
if (L->next != NULL)
L->next->prior = s;
L->next = s;
s->prior = L;
}
}
//尾插法建双链表
void CreateListR(DLinkNode *&L, ElemType a[], int n)
{
DLinkNode *s, *r;
L = (DLinkNode *)malloc(sizeof(DLinkNode)); //创建头结点
r = L; //r始终指向终端结点,开始时指向头结点
for (int i = 0; i < n; i++)
{
s = (DLinkNode *)malloc(sizeof(DLinkNode)); //创建新结点
s->data = a[i];
r->next = s;
s->prior = r; //将结点s插入结点r之后
r = s;
}
r->next = NULL; //尾结点next域置为NULL
}
//初始化线性表
void InitList(DLinkNode *&L)
{
L = (DLinkNode *)malloc(sizeof(DLinkNode)); //创建头结点
L->prior = L->next = NULL;
}
//销毁线性表
void DestroyList(DLinkNode *&L)
{
DLinkNode *pre = L, *p = pre->next;
while (p != NULL)
{
free(pre);
pre = p; //pre、p同步后移一个结点
p = pre->next;
}
free(p);
}
//判线性表是否为空表
bool ListEmpty(DLinkNode *L)
{
return (L->next == NULL);
}
//求线性表的长度
int ListLength(DLinkNode *L)
{
DLinkNode *p = L;
int i = 0; //p指向头结点,i设置为0
while (p->next != NULL) //找尾结点p
{
i++; //i对应结点p的序号
p = p->next;
}
return (i);
}
//输出线性表
void DispList(DLinkNode *L)
{
DLinkNode *p = L->next;
while (p)
{
printf("%d ", p->data);
p = p->next;
}
printf("\n");
}
//求线性表中第i个元素值
bool GetElem(DLinkNode *L, int i, ElemType &e)
{
int j = 0;
DLinkNode *p = L;
if (i <= 0)
return false; //i错误返回假
while (j < i && p != NULL) //查找第i个结点p
{
j++;
p = p->next;
}
if (p == NULL) //没有找到返回假
return false;
else //找到了提取值并返回真
{
e = p->data;
return true;
}
}
//查找第一个值域为e的元素序号
int LocateElem(DLinkNode *L, ElemType e)
{
int i = 1;
DLinkNode *p = L->next;
while (p != NULL && p->data != e) //查找第一个值域为e的结点p
{
i++; //i对应结点p的序号
p = p->next;
}
if (p == NULL) //没有找到返回0
return (0);
else //找到了返回其序号
return (i);
}
//插入第i个元素
bool ListInsert(DLinkNode *&L, int i, ElemType e)
{
int j = 0;
DLinkNode *p = L, *s; //p指向头结点,j设置为0
if (i <= 0)
return false; //i错误返回假
while (j < i - 1 && p != NULL) //查找第i-1个结点p
{
j++;
p = p->next;
}
if (p == NULL) //未找到第i-1个结点
return false;
else //找到第i-1个结点p
{
s = (DLinkNode *)malloc(sizeof(DLinkNode)); //创建新结点s
s->data = e;
s->next = p->next; //将结点s插入到结点p之后
if (p->next != NULL)
p->next->prior = s;
s->prior = p;
p->next = s;
return true;
}
}
//删除第i个元素
bool ListDelete(DLinkNode *&L, int i, ElemType &e)
{
int j = 0;
DLinkNode *p = L, *q; //p指向头结点,j设置为0
if (i <= 0)
return false; //i错误返回假
while (j < i - 1 && p != NULL) //查找第i-1个结点p
{
j++;
p = p->next;
}
if (p == NULL) //未找到第i-1个结点
return false;
else //找到第i-1个节p
{
q = p->next; //q指向第i个结点
if (q == NULL) //当不存在第i个结点时返回false
return false;
e = q->data;
p->next = q->next; //从双链表中删除结点q
if (p->next != NULL) //若p结点存在后继结点,修改其前驱指针
p->next->prior = p;
free(q); //释放q结点
return true;
}
}
int main()
{
DLinkNode *L;
ElemType tmp;
printf("初始化链表\n");
InitList(L);
printf("判断是否为空\n");
if (ListEmpty(L))
{
printf("链表为空!\n\n");
}
else
{
printf("链表非空\n\n");
}
//利用a数组创建链表
int a[] = {11, 22, 33, 44, 55};
int lenA = sizeof(a) / sizeof(a[0]);
printf("利用a数组创建链表,数组长度为:%d\n", lenA);
CreateListR(L, a, lenA);
printf("\n");
printf("判断创建后的链表是否为空\n");
if (ListEmpty(L))
{
printf("该链表为空\n\n");
}
else
{
printf("该链表非空\n\n");
}
printf("现有链表:");
DispList(L);
printf("\n");
printf("现有链表长度:%d\n\n", ListLength(L));
GetElem(L, 2, tmp);
printf("第二个位置上的元素是:%d\n", tmp);
printf("\n");
printf("获取元素值为3的元素在链表中的位置:\n");
printf("元素值为3的元素在链表中的位置是:%d\n\n", LocateElem(L, 3));
printf("向第1个位置插入元素99:\n");
ListInsert(L, 2, 99);
printf("插入成功!\n\n");
printf("输出插入元素后的链表:\n");
DispList(L);
printf("删除第二个位置的元素:\n");
ListDelete(L, 2, tmp);
printf("successful!\n\n");
printf("删除元素后的链表:\n");
DispList(L);
printf("\n");
printf("销毁链表\n");
DestroyList(L);
printf("销毁成功\n");
return 0;
}