链表
一. 前言
在学了顺序表之后,我们发现顺序表有一定的缺陷。第一个缺陷,从头部和中间的插入删除,都要移动后面的数据,时间复杂度为O(N)。第二个缺陷,增容需要申请新空间,拷贝数据,释放旧空间,会有不小的消耗。第三个缺陷,增容一般是呈2倍的增长,这会造成一定的空间浪费。比如说当前顺序表数据有1024个,容量也恰好是1024,这时我们只想插入一个数据,但是扩容却要扩大到2048个,这样有1023个空间大小就浪费了。刚刚提到的这些问题,链表就能很好地解决。下面我们就来一起学习一下链表,看看链表是怎么去解决这些问题的,链表又存在什么缺陷?
二. 链表的定义
2.1 概念
- 链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。链表的所有节点都是一个一个单独通过
malloc 向内存申请的,用的时候再申请。从下图我们可以看出,链表的每个节点都有一个next 指针指向下一个节点的地址,从逻辑上每个节点都是链接起来的。从内存的地址上看,每一个节点地址之间的距离大小都是不一样的,所以物理结构上他们不在的空间是不连续的。
2.2 分类
三. 单向无头不循环链表
3.1 概念和说明
- 单向无头不循环链表是链表中结构最简单的。如下图所示,每一个节点有一个
data 和一个next ,data 是用来存放数据的,next 是一个指向下一个节点地址的指针,最后一个节点的next 指向NULL 。在实现链表上,一个创建了三个文件,分别是SList.h ,SList.c ,main.c ,下面内容我们先定义链表的结构体和实现各个函数接口的代码,最后再把三个文件的代码展示出来。
3.2 定义链表结构体
typedef int SLTDataType;
typedef struct SListNode
{
struct SListNode* next;
SLTDataType data;
}SListNode;
- 为什么要重定义数据类型名?因为链表储存的元素类型不单单是
int 型,后面要存储char 型的或者其他类型的数据,需要把代码里面的int 都改一遍,非常麻烦。如果我们重新定义了类型名,并且在代码里用重新定义好的名字,下次需要存储其他类型的数据,直接在重定义那里把int 改成想存储的类型就好了。
3.3 函数接口
3.3.1 申请节点
SListNode* BuySListNode(SLTDataType x)
{
SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
assert(newnode);
newnode->next = NULL;
newnode->data = x;
return newnode;
}
3.3.2 链表头插
void SListPushFront(SListNode** pplist, SLTDataType x)
{
assert(pplist);
SListNode* newnode = BuySListNode(x);
if (*pplist == NULL)
{
*pplist = newnode;
}
else
{
newnode->next = *pplist;
*pplist = newnode;
}
}
*pplist = newnode;
newnode->next = next;
newnode->next = next;
*pplist = newnode;
3.3.3 链表尾插
void SListPushBack(SListNode** pplist, SLTDataType x)
{
assert(pplist);
SListNode* newnode = BuySListNode(x);
if (*pplist == NULL)
{
*pplist = newnode;
}
else
{
SListNode* tail = *pplist;
while (tail->next)
{
tail = tail->next;
}
tail->next = newnode;
}
}
3.3.4 在pos节点之后插入
void SListInsertAfter(SListNode* pos, SLTDataType x)
{
assert(pos);
SListNode* newnode = BuySListNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
3.3.5 在pos节点之前插入
void SListInsertBefore(SListNode** pplist, SListNode* pos, SLTDataType x)
{
assert(pplist);
assert(pos);
SListNode* newnode = BuySListNode(x);
if (*pplist == pos)
{
newnode->next = pos;
*pplist = newnode;
}
else
{
SListNode* prev = *pplist;
while (prev->next != pos)
{
prev = prev->next;
}
newnode->next = pos;
prev->next = newnode;
}
}
3.3.6 链表头删
void SListPopFront(SListNode** pplist)
{
assert(pplist);
assert(*pplist);
SListNode* temp = *pplist;
*pplist = (*pplist)->next;
free(temp);
temp = NULL;
}
3.3.7 链表尾删
void SListPopBack(SListNode** pplist)
{
assert(pplist);
assert(*pplist);
if ((*pplist)->next == NULL)
{
free(*pplist);
*pplist = NULL;
}
else
{
SListNode* tail = *pplist;
while (tail->next->next)
{
tail = tail->next;
}
SListNode* temp = tail->next;
tail->next = NULL;
free(temp);
temp = NULL;
}
}
3.3.8 删去pos节点
void SListErase(SListNode** pplist, SListNode* pos)
{
assert(pplist);
assert(*pplist);
if (*pplist == pos)
{
free(*pplist);
*pplist = NULL;
}
else
{
SListNode* posPrev = *pplist;
while (posPrev->next != pos)
{
posPrev = posPrev->next;
}
posPrev->next = pos->next;
free(pos);
pos = NULL;
}
}
3.3.9 链表查找
SListNode* SListFind(SListNode* plist, SLTDataType x)
{
if (plist == NULL)
{
return NULL;
}
SListNode* find = plist;
while (find)
{
if (find->data == x)
{
return find;
}
find = find->next;
}
return NULL;
}
3.3.10 链表修改
void SListModify(SListNode* pos, SLTDataType x)
{
assert(pos);
pos->data = x;
}
3.3.11销毁链表
void SListDestroy(SListNode** pplist)
{
assert(pplist);
SListNode* temp = NULL;
while (*pplist)
{
temp = *pplist;
*pplist = (*pplist)->next;
free(temp);
}
temp = NULL;
}
- 不知道大家是否记得上次顺序表的销毁是一次性把整块给销毁的,而这次的链表则要一个一个单独释放。因为顺序表我们是向内存申请一整块连续的空间,而链表这边是一个一个单独申请的,且他们一般情况下是不连续的,所以释放得单独释放。
3.4 SList.h文件代码
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLTDataType;
typedef struct SListNode
{
struct SListNode* next;
SLTDataType data;
}SListNode;
void SListPrint(SListNode* plist);
SListNode* BuySListNode(SLTDataType x);
void SListPushFront(SListNode** pplist, SLTDataType x);
void SListPushBack(SListNode** pplist, SLTDataType x);
void SListInsertBefore(SListNode** pplist, SListNode* pos, SLTDataType x);
void SListInsertAfter(SListNode* pos, SLTDataType x);
void SListPopfront(SListNode** pplist);
void SListPopBack(SListNode** pplist);
void SListErase(SListNode** pplist, SListNode* pos);
SListNode* SListFind(SListNode* plist, SLTDataType x);
void SListModify(SListNode* pos, SLTDataType x);
void SListDestroy(SListNode** pplist)
3.5SList.c文件代码
#define _CRT_SECURE_NO_WARNINGS
#include"SList.h"
void SListPrint(SListNode* plist)
{
while (plist)
{
printf("%d", plist->data);
printf("-->");
plist = plist->next;
}
printf("NULL");
}
SListNode* BuySListNode(SLTDataType x)
{
SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
assert(newnode);
newnode->next = NULL;
newnode->data = x;
return newnode;
}
void SListPushFront(SListNode** pplist, SLTDataType x)
{
assert(pplist);
SListNode* newnode = BuySListNode(x);
if (*pplist == NULL)
{
*pplist = newnode;
}
else
{
newnode->next = *pplist;
*pplist = newnode;
}
}
void SListPushBack(SListNode** pplist, SLTDataType x)
{
assert(pplist);
SListNode* newnode = BuySListNode(x);
if (*pplist == NULL)
{
*pplist = newnode;
}
else
{
SListNode* tail = *pplist;
while (tail->next)
{
tail = tail->next;
}
tail->next = newnode;
}
}
void SListInsertBefore(SListNode** pplist, SListNode* pos, SLTDataType x)
{
assert(pplist);
assert(pos);
SListNode* newnode = BuySListNode(x);
if (*pplist == pos)
{
newnode->next = pos;
*pplist = newnode;
}
else
{
SListNode* prev = *pplist;
while (prev->next != pos)
{
prev = prev->next;
}
newnode->next = pos;
prev->next = newnode;
}
}
void SListInsertAfter(SListNode* pos, SLTDataType x)
{
assert(pos);
SListNode* newnode = BuySListNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
void SListPopFront(SListNode** pplist)
{
assert(pplist);
assert(*pplist);
SListNode* temp = *pplist;
*pplist = (*pplist)->next;
free(temp);
temp = NULL;
}
void SListPopBack(SListNode** pplist)
{
assert(pplist);
assert(*pplist);
if ((*pplist)->next == NULL)
{
free(*pplist);
*pplist = NULL;
}
else
{
SListNode* tail = *pplist;
while (tail->next->next)
{
tail = tail->next;
}
SListNode* temp = tail->next;
tail->next = NULL;
free(temp);
temp = NULL;
}
}
void SListErase(SListNode** pplist, SListNode* pos)
{
assert(pplist);
assert(*pplist);
if (*pplist == pos)
{
*pplist = pos->next;
free(pos);
}
else
{
SListNode* posPrev = *pplist;
while (posPrev->next != pos)
{
posPrev = posPrev->next;
}
posPrev->next = pos->next;
free(pos);
pos = NULL;
}
}
SListNode* SListFind(SListNode* plist, SLTDataType x)
{
if (plist == NULL)
{
return NULL;
}
SListNode* find = plist;
while (find)
{
if (find->data == x)
{
return find;
}
find = find->next;
}
return NULL;
}
void SListModify(SListNode* pos, SLTDataType x)
{
assert(pos);
pos->data = x;
}
void SListDestroy(SListNode** pplist)
{
assert(pplist);
SListNode* temp = NULL;
while (*pplist)
{
temp = *pplist;
*pplist = (*pplist)->next;
free(temp);
}
temp = NULL;
}
3.6 main.c文件代码
#define _CRT_SECURE_NO_WARNINGS
#include"SList.h"
void test1()
{
SListNode* plist = NULL;
SListPushBack(&plist, 4);
SListPushFront(&plist, 3);
SListPushFront(&plist, 2);
SListPushFront(&plist, 1);
SListPushBack(&plist, 5);
SListPrint(plist);
SListDestroy(&plist);
}
void test2()
{
SListNode* plist = NULL;
SListPushBack(&plist, 4);
SListPushFront(&plist, 3);
SListPushFront(&plist, 2);
SListPushFront(&plist, 1);
SListPushBack(&plist, 5);
SListPopFront(&plist);
SListPopFront(&plist);
SListPopFront(&plist);
SListPopFront(&plist);
SListPopFront(&plist);
SListPopFront(&plist);
SListPrint(plist);
SListDestroy(&plist);
}
void test3()
{
SListNode* plist = NULL;
SListPushFront(&plist, 2);
SListPushFront(&plist, 1);
SListNode* pos = SListFind(plist, 1);
SListModify(pos, 100000000);
SListPrint(plist);
SListDestroy(&plist);
}
int main()
{
test3();
return 0;
}
3.7 为什么要传二级指针
-
在学习函数的时候,我们知道函数传参有两种,一种是传值,另外一种是传址。传值就是传变量的值过去,向下面的左边部分,我们实际上只是把10 传过去给a ,传20 给b 。当我们在函数里把a 和b 里面的值交换了,肯定不会影响到外面的x ,y 了。而传址是变量的地址传过去,就像下图的右边,我们分别传x 的地址给a 指针,传y 的地址给b 指针。而当我们解引用时,*a 等价于x 变量。所以当我们在函数里面对*a 和*b 修改,实际上就是直接对x 和y 做修改,以实现交换两个变量的值。 -
那为什么这里我们需要传二级指针,plist 本身不是一个指针吗? -
如下图所示,头插是需要改变plist 的值的,一开始plist 里面装的值是NULL ,想要实现头插就是要改变plist 里面的值,即改变plist 指向的地址。plist 是一个指针变量,就像上面说的例子一样,想要改变变量的值就要传变量的地址过去。所以这里传参需要传plist 的地址过去,而plist 是一个一级指针变量,它的地址需要一个二级指针去接收。下图我们&plist 就是取出plist 的地址,传过去给二级指针pplist ,在头插函数里面,我们通过解引用*pplist 就相当于plist ,这样就实现了在函数改变plist 的值。可以简单地理解为,函数传参,如果需要改变某个变量的值,就要传这个变量地址过去。接收那边,如果是普通变量的地址,比如说int ,char 这些类型变量的地址,接收那边就要拿对应int* ,char* 这种类型的一级指针去接收。要是传过去的是像int* ,char* 这些类型的一级指针的地址,接收的就要是对应的int** ,char** 这样的二级指针去接收。以此类推二级三级这些指针变量的地址,也是需要对应类型的三级四级指针去接收。有了以上的结论,再看我们这里,我们的plist的类型是SListNode* 类型,也就是一个一级指针变量,接收的就要是一个SListNode** 的变量去接收,也就一个二级指针变量。说了那么多,最重要的还是要记住这段话标粗的句子,像我们上面的查找函数接口,它不需要改变plist 的值,所以就不用传地址,把plist 的值传过去就ok了。
四. 双向带头循环链表
4.1 概念和说明
双向带头循环链表是链表里面比较复杂的结构了,双向说的是它一个节点有两个指针。如图所示,一个是prev 另外一个是next 。一般情况下prev 指向的是前一个节点的地址,next 指向的是后面节点的地址。特殊情况,我们本次讨论的链表结构就是,第一个节点的prev 指向的是最后一个节点,最后的一个节点的next 指向第一个节点,这样就实现来循环。在下图的监视窗口我们也能看出,红色箭头指向的内存地址已经开始循环了,如果一直展开,也是这几个节点再循环。带头的意思是有一个头节点,像图中的head 节点,它不会用来存储数据,这个节点也叫哨兵位。在实现链表上,一个创建了三个文件,分别是List.h ,List.c ,main.c ,下面内容我们先定义链表的结构体和实现各个函数接口的代码,最后再把三个文件的代码展示出来。大家也不要被名字给唬住了,这个结构虽然复杂了点,但是实现起来比上面的链表简单多了,下面我们一起学习一下吧。
4.2 定义链表结构体
typedef int LTDataType;
typedef struct ListNode
{
struct ListNode* prev;
struct ListNode* next;
LTDataType data;
}ListNode;
4.3 函数接口
4.3.1 初始化
ListNode* ListInit()
{
ListNode* head = (ListNode*)malloc(sizeof(ListNode));
assert(head);
head->prev = head;
head->next = head;
return head;
}
4.3.2 打印链表
void ListPrint(ListNode* head)
{
ListNode* cur = head->next;
while (cur != head)
{
printf("%d --> ", cur->data);
cur = cur->next;
}
printf("\n");
}
4.3.3 申请节点
ListNode* BuyListNode(LTDataType x)
{
ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
newnode->data = x;
newnode->next = NULL;
newnode->prev = NULL;
return newnode;
}
4.3.4 链表头插
void ListPushFront(ListNode* head, LTDataType x)
{
assert(head);
ListNode* newnode = BuyListNode(x);
newnode->next = head->next;
head->next->prev = newnode;
newnode->prev = head;
head->next = newnode;
}
4.3.5 链表尾插
void ListPushBack(ListNode* head, LTDataType x)
{
assert(head);
ListNode* newnode = BuyListNode(x);
newnode->prev = head->prev;
head->prev->next = newnode;
newnode->next = head;
head->prev = newnode;
}
4.3.6 在pos之前插入
void ListInsert(ListNode* pos, LTDataType x)
{
assert(pos);
ListNode* newnode = BuyListNode(x);
newnode->prev = pos->prev;
pos->prev->next = newnode;
newnode->next = pos;
pos->prev = newnode;
}
4.3.7 链表头删
void ListPopFront(ListNode* head)
{
assert(head);
assert(head->next != head);
ListNode* temp = head->next;
head->next = head->next->next;
head->next->prev = head;
free(temp);
temp = NULL;
}
4.3.8 链表尾删
void ListPopBack(ListNode* head)
{
assert(head);
assert(head->next != head);
ListNode* temp = head->prev;
head->prev = head->prev->prev;
head->prev->next = head;
free(temp);
temp = NULL;
}
4.3.9 删除pos节点
void ListErase(ListNode* pos)
{
assert(pos);
assert(pos->next != pos);
pos->prev->next = pos->next;
pos->next->prev = pos->prev;
free(pos);
pos = NULL;
}
4.3.10 查找
ListNode* ListFind(ListNode* head, LTDataType x)
{
assert(head);
ListNode* find = head->next;
while (find != head)
{
if (find->data == x)
{
return find;
}
find = find->next;
}
return NULL;
}
4.3.11 修改
void ListModify(ListNode* pos, LTDataType x)
{
assert(pos);
pos->data = x;
}
4.3.12 链表的销毁
void ListDstroy(ListNode* head)
{
assert(head);
ListNode* temp = NULL;
while (head->next != head)
{
temp = head->next;
head->next = head->next->next;
head->next->prev = head;
free(temp);
}
free(head);
head = NULL;
temp = NULL;
}
4.4 List.h文件代码
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int LTDataType;
typedef struct ListNode
{
struct ListNode* prev;
struct ListNode* next;
LTDataType data;
}ListNode;
ListNode* ListInit();
void ListPrint(ListNode* head);
ListNode* BuyListNode(LTDataType x);
void ListPushFront(ListNode* head, LTDataType x);
void ListPushBack(ListNode* head, LTDataType x);
void ListInsert(ListNode* pos, LTDataType x);
void ListPopFront(ListNode* head);
void ListPopBack(ListNode* head);
void ListErase(ListNode* pos);
ListNode* ListFind(ListNode* head, LTDataType x);
void ListModify(ListNode* pos, LTDataType x);
void ListDstroy(ListNode* head);
4.5 List.c文件代码
#define _CRT_SECURE_NO_WARNINGS
#include"List.h"
ListNode* ListInit()
{
ListNode* head = (ListNode*)malloc(sizeof(ListNode));
assert(head);
head->prev = head;
head->next = head;
return head;
}
void ListPrint(ListNode* head)
{
ListNode* cur = head->next;
while (cur != head)
{
printf("%d --> ", cur->data);
cur = cur->next;
}
printf("\n");
}
ListNode* BuyListNode(LTDataType x)
{
ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
newnode->data = x;
newnode->next = NULL;
newnode->prev = NULL;
return newnode;
}
void ListPushFront(ListNode* head, LTDataType x)
{
assert(head);
ListNode* newnode = BuyListNode(x);
newnode->next = head->next;
head->next->prev = newnode;
newnode->prev = head;
head->next = newnode;
}
void ListPushBack(ListNode* head, LTDataType x)
{
assert(head);
ListNode* newnode = BuyListNode(x);
newnode->prev = head->prev;
head->prev->next = newnode;
newnode->next = head;
head->prev = newnode;
}
void ListInsert(ListNode* pos, LTDataType x)
{
assert(pos);
ListNode* newnode = BuyListNode(x);
newnode->prev = pos->prev;
pos->prev->next = newnode;
newnode->next = pos;
pos->prev = newnode;
}
void ListPopFront(ListNode* head)
{
assert(head);
assert(head->next != head);
ListNode* temp = head->next;
head->next = head->next->next;
head->next->prev = head;
free(temp);
temp = NULL;
}
void ListPopBack(ListNode* head)
{
assert(head);
assert(head->next != head);
ListNode* temp = head->prev;
head->prev = head->prev->prev;
head->prev->next = head;
free(temp);
temp = NULL;
}
void ListErase(ListNode* pos)
{
assert(pos);
assert(pos->next != pos);
pos->prev->next = pos->next;
pos->next->prev = pos->prev;
free(pos);
pos = NULL;
}
ListNode* ListFind(ListNode* head, LTDataType x)
{
assert(head);
ListNode* find = head->next;
while (find != head)
{
if (find->data == x)
{
return find;
}
find = find->next;
}
return NULL;
}
void ListModify(ListNode* pos, LTDataType x)
{
assert(pos);
pos->data = x;
}
void ListDstroy(ListNode* head)
{
assert(head);
ListNode* temp = NULL;
while (head->next != head)
{
temp = head->next;
head->next = head->next->next;
head->next->prev = head;
free(temp);
}
free(head);
head = NULL;
temp = NULL;
}
4.6 main.c文件代码
#define _CRT_SECURE_NO_WARNINGS
#include"List.h"
void Test1()
{
ListNode* head = ListInit();
ListPushFront(head, 3);
ListPushFront(head, 2);
ListPushFront(head, 1);
ListPushBack(head, 4);
ListPushBack(head, 5);
ListPushBack(head, 6);
ListPrint(head);
ListDstroy(head);
printf("over\n");
}
void Test2()
{
ListNode* head = ListInit();
ListPushBack(head, 4);
ListPushBack(head, 5);
ListPushBack(head, 6);
ListPopBack(head);
ListPopBack(head);
ListPopBack(head);
ListPrint(head);
ListDstroy(head);
printf("over\n");
}
void Test3()
{
ListNode* head = ListInit();
ListPushBack(head, 1);
ListPushBack(head, 2);
ListPushBack(head, 3);
ListNode* pos = ListFind(head, 2);
ListErase(pos);
ListPrint(head);
ListDstroy(head);
printf("over\n");
}
int main()
{
Test3();
return 0;
}
五. 顺序表和链表的优缺点
5.1 顺序表
5.1.1 顺序表的优点
- 支持随机访问(用下标访问)。
- CPU高速缓存命中率更高(下面5.3有解释)。
5.1.2 顺序表的缺点
- 头部中部插入删除时间效率低,O(N)。
- 扩容有一定程度的性能消耗,扩容一般是按倍数去阔,用不完会造成一定的空间浪费。
5.2 链表
5.2.1 链表的优点
- 任意位置插入删除效率高,O(1)。
- 按需申请和释放空间,不会造成空间浪费。
5.2.2 链表的缺点
- 不支持随机访问,意味这一些排序和二分查找在链式结构上不适用。
- 每个节点除了要存数据还需要指针去存其他节点的地址。
- CPU高速缓存命中率更低。
5.3 CPU高速缓存命中率
- 目前主流的计算机存储系统大致分为主存和外存,外存就是U盘,磁盘,硬盘这些,主存就是我们经常说的内存了。买电脑的时候我们会发现,目前主流的电脑,硬盘动不动就是以TB为单位,而内存还是GB,一般是4GB,8GB,或者16GB。为什么他们相差那么大呢,因为内存比较贵,当然好处就是速度也比他们快很多。虽然目前计算机内存的速度速度已经很不错了,但是CPU的发展比内存更快,速度远远超过了内存。为了电脑的整体性能,人们又在CPU引入了跟其速度相当的高速缓存,如下图的Cache,它们的容量非常小,一般是以MB为单位,而且是个位数或者十位数的。高速缓存一般又分为一级缓存,二级缓存,和三级缓存,一级缓存速度最快,也最接近CPU。一般电脑在运行时,电脑会从内存中调用一部分到高速缓存中,CPU需要的数据首先会到高速缓存中找,找到不到才会到内存中。在高速缓存中找到就叫做命中,那为什么上面我们说顺序表的命中率呢?因为顺序表本质上是数组,是内存中一块连续的空间,而链表它在内存中不是一块连续的空间。顺序表里面的数据命中了就很有可能连续被命中,链表一个数据被命中了,下一个可能不在高速缓存就不会命中,所以命中率就低了。
六. 总结
从上面我们可以看出,顺序表和链表各有优缺点,一般顺序表的优点就链表的缺点,反之顺序表的缺点就是链表的优点。这两种结构是相互弥补的,在实际开发中如果需要频繁插入和删除大量数据的话链表会更优一些,如果需要按照下标访问又是顺序表更加合适。所以说不能说他们谁最好,要看自己的需求。顺序表和链表是数据结构的开端,也是非常重要的知识,学好这两种结构,有助于我们更好地后面学习和理解其他结构。这两部分包括后续的章节对C语言的指针,函数,结构体和动态内存规划这几个方面的知识要求比较高,特别是指针。所以建议大家要把这几个方面的知识掌握扎实,这样才能更好地学习数据结构。最后,本文是我学完这章内容后的个人总结,要是文章里有什么错误还望各位大神指正。或者对我的文章排版和其他方面有什么建议,也可以在评论区告诉我。如果我的文章对你的学习有帮助,或者觉得写得不错的话记得分享给你的朋友,非常感谢。
|