文章目录
链表是一种常见的数据结构,它由一系列节点组成,每个节点包含数据和指向下一个节点的指针。
链表与数组相比具有以下特点:
-
链式存储:链表的节点在内存中不需要连续的存储空间,每个节点通过指针将其与下一个节点连接起来。
-
动态性:链表的长度可以根据需要进行动态调整。可以方便地在链表头部或尾部插入或删除节点,不需要像数组那样移动其他元素。
-
随机访问困难:链表中的元素不具有直接的下标访问方式,需要从头节点开始按顺序遍历到目标节点,因此访问效率较低。
-
空间效率相对较低:相对于数组,链表需要额外的指针空间来存储节点间的连接关系,会占用一定的额外存储空间。
链表常见的类型有单链表、双链表和循环链表。
-
单链表(Single Linked List):每个节点只有一个指向下一个节点的指针。
-
双链表(Double Linked List):每个节点同时有指向下一个节点和上一个节点的指针,可以实现双向遍历。
-
循环链表(Circular Linked List):尾节点指向头节点,形成一个环状结构。
链表适用于频繁插入和删除的场景下,尤其是在事先不知道数据大小的情况下。它可以方便地动态调整存储空间,并且插入和删除操作的效率较高。然而,链表的缺点是访问效率相对较低,无法通过下标直接访问元素,需要顺序遍历。因此,在需要快速随机访问元素的情况下,数组可能更合适。
1单链表
此结构为链式存储:单链表中的元素通过指针将它们连接起来,每个节点包含一个数据域和一个指向下一个节点的指针。也就是说它不连续,无顺序,想找下一个乖乖靠指针噢。
为了方便理解 我们假想一个结构:
1.因为链表节点的存储位置没有连续要求,所以每个节点可以在内存的任意位置。这意味着链表节点可能分布在不同的内存区域中,不需要连续的物理空间。这也是链表的一个优点,因为它允许动态地分配和释放节点,提供了更灵活的存储空间管理方式。所以他的实际样子我们很难表达,也不知道散落成什么样子,只能假想成这个中规中矩的样子;同时我们应该明白数据结构的美难以刻画,只可意会。
2.一个数据一个指针共同构成了一个节点。通过节点的数据和指针的组合,链表可以存储和操作各种类型的数据,同时保持节点之间的链接关系。
到这里,我们开始着手用程序刻画这种关系;节点呢通常就用结构体实例化来表达,我们先创建一个结构体:
typedef int ListDataType; //方便修改定义的类型
struct SListNode
{
ListDataType data;
struct SListNode* index;
};
typedef struct SListNode ListNode; //老写struct....太麻烦了,给他取个别名。
接下来的话同样测试是老三样:初始化、打印、存数据
1.1 初始化
1.这里我们只需要将指针index指向空即可(不是指向无效地址)
ListNode* plist = NULL;
2.数据data就不必去管:基本数据类型(如整数、字符等),如果你不为其赋初值,它们会被自动初始化为默认值,根据不同的编程语言和编译器而定。通常情况下,这样的默认值是零值(例如 0 或者 ‘\0’)
1.2 打印
打印就得从头到尾遍历一遍,首先虽然链表不支持随机访问,但是头结点我们是知道的,然后顺着头结点的指针找到下一个节点,再通过…,那这里的遍历其实就是顺藤摸瓜。我们使用循环,先建立一个指针指向头结点,然后“顺藤摸瓜”,直到我们摸到了“NULL”(因为最后一个节点的指针指向的是NULL)就打印完成。
注意一下,程序里面的head指针是指向链表头结点的,也是已知的。
void ListPrint(ListNode* head)
{
ListNode* cur = head; //创建指针指向头结点
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->index;
}
printf("NULL\n");
}
1.3存数据-尾插
在这里我们要考虑的问题有两个:
1.建立新节点存储数据时候的增容问题。
2.新节点和链表末尾节点的链接问题,主要是要找到最后一个节点的位置。
首先问题一,这里是要分配指定字节大小的空间,得用malloc了吧,开辟以后节点里的data就是新数据了,指针即将成为链表的末位指针,该指向NULL。考虑到头插等一些接口也是要扩容,不如索性写一个扩充节点的函数:
ListNode* AddListNode(ListDateType x)
{
ListNode* newnode = (ListNode*)malloc(sizeof(ListNode)); //开辟一个新节点
newnode->data = x; //存入新数据
newnode->index = NULL;
return newnode;
}
接下来我们解决问题二,让扩容前的链表末尾节点的指针指向新节点,我们首先就要找到旧的末尾节点,也没啥好办法,还得是遍历啊,不过这时候我们考虑一下两种情况:1.还没存到数据,那就简单了,把新开辟的节点作为第一个节点就好了。2.就是已经存过数据了,就遍历。
下面就是程序,这里注意一下,你要把一级指针传过来,那形参是二级指针。因为在C语言中,传递指针参数时,是按值传递的方式。如果你的函数中只使用一级指针,函数内对指针的修改不会影响到原来的指针。
而如果你使用二级指针,函数中对指针的修改会直接影响外部传入的指针。
void ListPushBack(ListNode**phead, ListDateType x) //尾插
{
ListNode*newnode = AddListNode(x);
if (*phead == NULL)
{
*phead = newnode;
}
else
{
//找尾节点的指针
ListNode* tail = *phead;
while (tail->index != NULL)
{
tail = tail->index;
}
//尾节点,链接新节点
tail->index = newnode;
}
}
1.4 存数据-头插
头插相比于尾插就容易一点,因为我们不需要遍历去找节点了,我们只需要已知的头结点指针head,暂且称为头指针吧,我们要做的就是,让头指针指向新的节点,让新节点里面的指针指向旧链表的头节点:
void ListPushFront(ListNode**phead, ListDateType x) //头插
{
ListNode*newnode = AddListNode(x); //跟头插一样,增个节点
newnode->index = *phead;
*phead = newnode;
}
1.5 删数据-尾删
在进行链表的尾删操作时,我们需要释放最后一个节点的内存空间,同时还需要更新倒数第二个节点的 index 指针,将其指向空,以确保链表仍然保持正确的结构。还是得遍历找到最后的节点指针;尾删呢,我们分为三种情况:1.链表为空;2.只有一个节点;3.多于一个节点;
void ListNodePopBack(ListNode**head) //尾删
{
//3种情况 1.空 2.一个节点 3.大于一个节点
if (*head == NULL)
{
return; //直接结束
}
else if ((*head)->index == NULL)
{
free(*head);
*head = NULL;
}
else
{
ListNode* prev = NULL;
ListNode*tail = *head;
while (tail->index != NULL) //让prev成为倒数第二个节点的指针
{
prev = tail;
tail = tail->index;
}
free(tail);
prev->index = NULL;
}
}
1.6 删数据-头删
这个又轻松了,free掉*head的空间,让链表第二个节点成为第一个:
void ListNodePopFront(ListNode**head) //头删
{
ListNode* next = (*head)->index;
free(*head);
*head = next;
}
1.7 在链表指定位置插入
就比如:我想在3的前面插入7,那我是不是应该先找到3所在的节点:
我们先在头结点处创建一个指针从首位向后面遍历,直到遇到我们想要的数值。
ListNode* ListFind(ListNode* head, ListDateType x)
{
ListNode* cur = head;
while (cur)
{
if (cur->data == x)
{
return cur;
}
cur = cur->index;
}
return NULL;
}
要注意这个返回的是指针,而非数值,我们可以这样验证:
ListNode* pNode = ListFind(plist, 5);
printf("指针的值为:%p\n", (void*)pNode);
当找到的时候,会返回指针地址,找不到就返回0;像这样:
好了,想要的位置我们已经找到了,接下来应该插入我们想要输入的数据了;我们要考虑一种情况:如果我们就想要头插,是不是直接就用之前的程序就可以了。接下来我们分析一下:我们在头结点创建一个指针prev,然后让这个指针遍历到2所在节点的位置,这时候我们只要让prev->index=newnode;
让newnode->index=pos就可以了
ListNode*ListInsert(ListNode** head, ListNode* pos, ListDateType x)//在pos位置的前面插入数据x
{
if (pos == *head)
{
ListPushFront(head, x);
}
else
{
ListNode*newnode = AddListNode(x);
ListNode*prev = *head;
while (prev->index!=pos)
{
prev = prev->index;
}
prev->index = newnode;
newnode->index = pos;
}
}
另外说一下这个程序为什么第一个形参用二级指针,第一个形参用一级指针:
这是因为传递链表头节点时,我们需要修改链表头的指向,即需要修改 head 指针本身的值。而对于 pos 参数,我们仅需要获取它的值,不需要修改它的指向。
当我们传递一个一级指针作为参数时,函数内部的修改只会影响到这个指针的副本,不会影响到原始的指针。如果我们想要修改原始指针,就需要传递指针的指针(即二级指针)作为参数,函数内部通过二级指针修改一级指针指向的值。
1.8 删除指定位置的数据
我们的思路应该是
(1).先在头节点处创建一个指针,遍历到pos(指定位置);
(2).然后让prev节点处的index指向4处节点:prev->index->pos->index;
(3).然后free掉pos所在空间;
注意2和3顺序可不能颠倒,如果先进行3,那pos->index就没了,也就是说pos位置后面的节点就找不着了。
void ListErase(ListNode**head, ListNode* pos)
{
if (pos == *head)
{
ListNodePopFront(head);
}
else
{
ListNode*prev = *head;
while (prev->index != pos)
{
prev = prev->index;
}
prev->index = pos->index;
free(pos);
}
}
1.9完整程序
1.头文件程序
#pragma once
#include<stdio.h>
#include<stdlib.h>
typedef int ListDateType;
struct SListNode
{
ListDateType data;
struct SListNode* index;
};
typedef struct SListNode ListNode;
void ListPrint(ListNode* head); //打印
void ListPushBack(ListNode**head, ListDateType x); //尾插
void ListPushFront(ListNode**head, ListDateType x); //头插
void ListNodePopBack(ListNode**head); //尾删
void ListNodePopFront(ListNode**head); //头删
ListNode* ListFind(ListNode* head, ListDateType x); //找到数据所在位置的指针
ListNode*ListInsert(ListNode* head, ListNode* pos, ListDateType x);//在pos位置的前面插入数据x
void ListErase(ListNode**head, ListNode* pos);
2.源文件(函数程序)
#include"List.h"
void ListPrint(ListNode* head)
{
ListNode* cur = head;
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->index;
}
printf("NULL\n");
}
ListNode* AddListNode(ListDateType x)
{
ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
newnode->data = x;
newnode->index = NULL;
return newnode;
}
void ListPushBack(ListNode**phead, ListDateType x) //尾插
{
ListNode*newnode = AddListNode(x);
if (*phead == NULL)
{
*phead = newnode;
}
else
{
//找尾节点的指针
ListNode* tail = *phead;
while (tail->index != NULL)
{
tail = tail->index;
}
//尾节点,链接新节点
tail->index = newnode;
}
}
void ListPushFront(ListNode**phead, ListDateType x) //头插
{
ListNode*newnode = AddListNode(x); //跟头插一样,增个节点
newnode->index = *phead;
*phead = newnode;
}
void ListNodePopBack(ListNode**head) //尾删
{
//3种情况 1.空 2.一个节点 3.大于一个节点
if (*head == NULL)
{
return; //直接结束
}
else if ((*head)->index == NULL)
{
free(*head);
*head = NULL;
}
else
{
ListNode* prev = NULL;
ListNode*tail = *head;
while (tail->index != NULL)
{
prev = tail;
tail = tail->index;
}
free(tail);
prev->index = NULL;
}
}
void ListNodePopFront(ListNode**head) //头删
{
ListNode* next = (*head)->index;
free(*head);
*head = next;
}
ListNode* ListFind(ListNode* head, ListDateType x)
{
ListNode* cur = head;
while (cur)
{
if (cur->data == x)
{
return cur;
}
cur = cur->index;
}
return NULL;
}
ListNode*ListInsert(ListNode** head, ListNode* pos, ListDateType x)//在pos位置的前面插入数据x
{
if (pos == *head)
{
ListPushFront(head, x);
}
else
{
ListNode*newnode = AddListNode(x);
ListNode*prev = *head;
while (prev->index!=pos)
{
prev = prev->index;
}
prev->index = newnode;
newnode->index = pos;
}
}
//删除pos位置的值
void ListErase(ListNode**head, ListNode* pos)
{
if (pos == *head)
{
ListNodePopFront(head);
}
else
{
ListNode*prev = *head;
while (prev->index != pos)
{
prev = prev->index;
}
prev->index = pos->index;
free(pos);
}
}
3.部分测试
#include"List.h"
void _testList1()
{
ListNode* plist = NULL;
ListPushBack(&plist, 1);
ListPushBack(&plist, 2);
ListPushBack(&plist, 3);
ListPushBack(&plist, 4);
ListPushBack(&plist, 5);
ListPushBack(&plist, 6);
ListPushFront(&plist, 0);
ListPrint(plist);
/*ListNode* pos = ListFind(plist, 3);
ListInsert(&plist, pos, 30);
ListPrint(plist);*/
/*ListInsert(&plist, pos, 7);
ListPrint(plist);*/
/*ListNode* pNode = ListFind(plist, 3);
printf("指针的值为:%p\n", (void*)pNode);
ListNode* ppNode = ListFind(plist, 45);
printf("指针的值为:%p\n", (void*)ppNode);*/
}
int main()
{
_testList1();
return 0;
}
2.双链表
链表的种类有多种,常用的分为单链表和双链表。又有带头、循环的buff,那我们这样分有8种:
这里我们把buff叠满,讲一下双向带头循环链表;
2.1 结构介绍
双向带头循环链表是一种特殊类型的链表,它具有以下特点:
-
双向性(Doubly Linked):每个节点除了包含指向下一个节点的指针,还有指向前一个节点的指针。这使得在链表中可以方便地进行向前和向后的遍历和操作。
-
带头节点(Head Node):在链表的头部添加一个额外的节点作为头节点。头节点不存储数据,仅用于指向第一个节点,这样可以简化链表的操作并且更容易进行插入和删除操作。
-
循环性(Circular):链表的最后一个节点的指针指向头节点,形成一个循环结构。这样可以方便实现循环遍历链表,即当遍历到最后一个节点时,直接跳转到头节点进行下一次遍历。
双向带头循环链表可以充分利用双向指针的特点,使得在链表中进行插入、删除和遍历操作更加高效和方便。它适用于需要频繁在链表中进行插入和删除操作,并且需要双向遍历链表的场景。
根据我们的图我们先整一个结构体:
typedef int ListDataType;
typedef struct ListNode
{
struct ListNode* next;
struct ListNode* prev;
ListDataType data;
}ListNode;
2.2 初始化
我们看到结构体三个元素,图中还有“带头的”,我们的初始化肯定很针对指针这个复杂的东西了,这时候应该只有一个“头”了(如下图),在此之前我们还是先弄一个增容的函数,不然没东西初始化给我就尴尬了。那我们还是让增容里面的指针指向NULL(注意NULL和无效的区别,这里NULL是为了防止无效)
ListNode* AddListNode(ListDataType x)
{
ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
newnode->data = x;
newnode->next = NULL;
newnode->prev = NULL;
return newnode;
}
增容以后我们要初始化成图中的样子,就是两个指针都指向自己:
ListNode* ListInit()
{
ListNode*head = AddListNode(0);
head->next = head;
head->prev = head;
return head;
}
2.3 销毁
销毁其实就是顺着head头指针找到其他指针并销毁,这里我们知道这是一个环形链表,我们不能先销毁head指针,这样就找不到其他节点了,所以我们先留着,反正是环形结构,总会遇见的,不如留到最后,刚好也可以作为循环结束的条件,所以用while比较好吧
void ListDestory(ListNode*head)
{
assert(head);
ListNode* index = head->next;
while (index != head)
{
ListNode*next = index->next;
free(index);
index = next;
}
free(head);
head = NULL;
}
至于这里为什么先用一个断言呢,是因为防止传入空指针,导致无法进入循环,而程序又不报错,不过也要注意断言通常在调试和开发阶段使用,它的目的是帮助程序员发现并修复错误。在发布版本的生产环境中,断言通常会被禁用或移除,以避免中断程序的执行。
2.4 打印
打印和前面的销毁都是需要遍历的,只不过销毁是找到指针并free掉,而打印是找到存储的数据打印出来,道理是一样的。
void ListPrint(ListNode*head)
{
assert(head);
ListNode* index = head->next;
while (index != head)
{
printf("%d ", index->data);
index = index->next;
}
printf("\n");
}
2.5 尾插
图中a4所在的节点就是咱们新加的,新开辟的节点要加入到链表中,就得适应链表中指针的规则。我们按照图中的指针走向进行定义就可以了:
void ListPushBack(ListNode*head, ListDataType x)
{
assert(head);
ListNode*tail= head->prev;
ListNode*newnode = AddListNode(x);
tail->next = newnode;
newnode->prev = tail;
newnode->next = head;
head->prev = newnode;
}
2.6 头插
这个程序呢,其实也是看图写就好了:
void ListPushFront(ListNode*head, ListDataType x)
{
assert(head);
ListNode* newnode = AddListNode(x);
newnode->next = head->next;
head->next = newnode;
newnode->prev = head;
}
2.7 尾删/头删
这个链表的删除其实就是指针的销毁和指针的改向;
//尾删
void ListPopBack(ListNode*head)
{
assert(head->next != head);
ListNode*tail = head->prev;
ListNode*prev = tail->prev;
prev->next = head;
head->prev = prev;
free(tail);
tail = NULL;
/*assert(head);
ListErase(head->next);*/
}
//头删
void ListPopFront(ListNode*head)
{
assert(head->next != head);
ListNode* first = head->next;
ListNode*second = first->next;
head->next = second;
second->prev = head;
free(first);
first = NULL;
/*assert(head);
ListErase(head->next);*/
}
2.8 查找
这个查找还是需要遍历一下去找到我们想要的值,当然返回的是指向该节点的指针。如果没有找到的话就返回空;
ListNode* ListFind(ListNode*head, ListDataType x)
{
assert(head);
ListNode* index = head->next;
while (index != head)
{
if (index->data == x)
{
return index;
}
index = index->next;
}
return NULL;
}
注意因为返回的是指针,我们测试的时候可以测试指针的地址,如果找不到就返回0,找到的话会有一个地址,这样测试:
ListNode* pos=ListFind(list, 3);
printf("指针的值为:%p\n", (void*)pos);
2.9 在任意位置(pos查找)之前插入
这次咱们还是看图写程序,断言pos是防止pos为空
void ListInsert(ListNode* pos, ListDataType x)
{
assert(pos);
ListNode* prev = pos->prev;
ListNode*newnode = AddListNode(x);
prev->next = newnode;
newnode->prev = prev;
newnode->next = pos;
pos->prev = newnode;
}
2.10 删除pos位置的值
这里也是先使用查找函数找到pos的位置,然后整理指针的指向问题,注意不要先free掉pos,因为pos如果先没了,那他后面的节点就“失联”了。
void ListErase(ListNode*pos)
{
assert(pos);
ListNode*prev = pos->prev;
ListNode*next = pos->next;
prev->next = next;
next->prev = prev;
free(pos);
}
1.12完整程序
1.12.1头文件
#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>
typedef int ListDataType;
//带头双向循环==任意位置插入删除数据都是0(1)
typedef struct ListNode
{
struct ListNode* next;
struct ListNode* prev;
ListDataType data;
}ListNode;
ListNode* ListInit();
void ListDestory(ListNode*head);
void ListPrint(ListNode*head);
void ListPushBack(ListNode*head, ListDataType x);
void ListPushFront(ListNode*head, ListDataType x);
void ListPopBack(ListNode*head);
void ListPopFront(ListNode*head);
ListNode* ListFind(ListNode*head0, ListDataType x);
void ListInsert(ListNode* pos, ListDataType x);
void ListErase(ListNode*pos);
1.12.2源文件
#include"DouList.h"
ListNode* AddListNode(ListDataType x)
{
ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
newnode->data = x;
newnode->next = NULL;
newnode->prev = NULL;
return newnode;
}
ListNode* ListInit()
{
ListNode*head = AddListNode(0);
head->next = head;
head->prev = head;
return head;
}
void ListDestory(ListNode*head)
{
assert(head);
ListNode* index = head->next;
while (index != head)
{
ListNode*next = index->next;
free(index);
index = next;
}
free(head);
head = NULL;
}
void ListPrint(ListNode*head)
{
assert(head);
ListNode* index = head->next;
while (index != head)
{
printf("%d ", index->data);
index = index->next;
}
printf("\n");
}
void ListPushBack(ListNode*head, ListDataType x)
{
assert(head);
ListNode*tail= head->prev;
ListNode*newnode = AddListNode(x);
tail->next = newnode;
newnode->prev = tail;
newnode->next = head;
head->prev = newnode;
//ListInsert(head, x);
}
void ListPushFront(ListNode*head, ListDataType x)
{
assert(head);
ListNode* newnode = AddListNode(x);
newnode->next = head->next;
head->next = newnode;
newnode->prev = head;
//ListInsert(head->next, x);
}
void ListPopBack(ListNode*head)
{
assert(head->next != head);
ListNode*tail = head->prev;
ListNode*prev = tail->prev;
prev->next = head;
head->prev = prev;
free(tail);
tail = NULL;
/*assert(head);
ListErase(head->next);*/
}
void ListPopFront(ListNode*head)
{
assert(head->next != head);
ListNode* first = head->next;
ListNode*second = first->next;
head->next = second;
second->prev = head;
free(first);
first = NULL;
/*assert(head);
ListErase(head->next);*/
}
ListNode* ListFind(ListNode*head, ListDataType x)
{
assert(head);
ListNode* index = head->next;
while (index != head)
{
if (index->data == x)
{
return index;
}
index = index->next;
}
return NULL;
}
//pos位置之前插入x
void ListInsert(ListNode* pos, ListDataType x)
{
assert(pos);
ListNode* prev = pos->prev;
ListNode*newnode = AddListNode(x);
prev->next = newnode;
newnode->prev = prev;
newnode->next = pos;
pos->prev = newnode;
}
void ListErase(ListNode*pos)
{
assert(pos);
ListNode*prev = pos->prev;
ListNode*next = pos->next;
prev->next = next;
next->prev = prev;
free(pos);
}
1.12.3 测试文件(部分)
#include"DouList.h"
void TestList()
{
ListNode* list = ListInit();
ListPushBack(list, 1);
ListPushBack(list, 2);
ListPushBack(list, 3);
ListPushBack(list, 4);
ListPrint(list);
ListPushBack(list, 5);
ListPushFront(list, 0);
ListPrint(list);
ListPopBack(list);
ListPopFront(list);
ListPrint(list);
ListNode* pos=ListFind(list, 3);
printf("指针的值为:%p\n", (void*)pos);
ListDestory(list);
}
int main()
{
TestList();
return 0;
}