1 什么是链表
链表由一系列节点(链表中每一个元素称为节点)组成,节点在运行时动态生成 (malloc),每个节点包括两个部分:
一个是存储数据元素的数据域data
另一个是存储下一个节点地址的指针域next*
所以我们定义一个结构体,记录一下链表:
struct LinkNode
{
int data;
struct LinkNode* next;
};
2 数组与链表的优缺点
链表是通过节点把离散的数据链接成一个表,通过对节点的插入和删除操作从而实现 对数据的存取。而数组是通过开辟一段连续的内存来存储数据,这是数组和链表最大的区别。
数组的优点:
- 随机访问方便,可以通过下标直接访问元素。
- 存储效率高,因为连续的内存空间可以被CPU缓存,访问速度快。
- 可以实现一些基于数组的算法,如二分查找等。
数组的缺点:
- 数组的大小固定,无法动态扩展或缩小。
- 插入和删除元素时需要移动其他元素,效率较低。
- 内存空间可能被浪费,因为数组的大小固定,可能会预留过多的空间。
链表的优点:
- 动态扩展和缩小容易,不需要移动其他元素。
- 插入和删除元素时效率高。
- 内存空间利用率高,因为链表只会分配需要的空间。
链表的缺点:
- 随机访问元素效率低,需要遍历整个链表。
- 链表的存储空间比数组大,因为每个元素都需要一个指针指向下一个元素。
- 链表不支持一些基于数组的算法,如二分查找等。
3 链表的类型
1.双链表
2.循环链表
4 链表的基础使用(结构体加指针实现)
4.1 链表的初始化
我们先来看一下两个重要的概念:
1.头结点:
链表中的头结点是指在链表的开始位置添加一个额外的节点,用于方便对链表进行操作。头结点并不存储实际的数据,它只是作为链表的起始点,用于指向第一个实际存储数据的节点。
为什么链表要有头结点的原因有以下几点:
-
方便插入和删除操作:在链表的头部插入或删除节点是一种常见的操作。如果没有头结点,插入和删除操作需要特殊处理,而有了头结点后,可以直接通过头结点进行插入和删除操作,代码实现更加简洁。
-
简化代码逻辑:有了头结点后,链表的第一个节点不再特殊,所有节点的操作可以统一处理,代码逻辑更加简单。
-
便于空链表的处理:当链表为空时,头结点可以用来表示空链表,避免了对空指针的处理。
2.尾部指针:
链表的尾部指针可以提高链表的操作效率。如果没有尾部指针,每次在链表尾部添加元素时都需要遍历整个链表来找到尾节点,时间复杂度为O(n)。而有了尾部指针,就可以直接在尾部插入元素,时间复杂度为O(1)。
现在我们进行链表的初始化:我们函数的返回值要返回头结点,有了头结点就抓住了关键。
struct LinkNode* Init_LinkNode()
{
return header;
}
struct LinkNode* header = (struct LinkNode*)malloc(sizeof(struct LinkNode));
header->data = -1;
header->next = NULL;
补充:malloc函数:
malloc 是 C 语言中的一个函数,用于动态分配内存空间。它的声明如下:
void* malloc(size_t size);
malloc 函数接受一个参数 size,表示需要分配的内存空间的大小(以字节为单位)。它会在堆中分配一块连续的内存空间,并返回指向该空间起始地址的指针。如果分配失败,则返回 NULL。
但是malloc返回为void,所以要在malloc前面强制转换(struct LinkNode*)类型
补充:
calloc()、realloc()
1.
calloc
是 C 语言中的一个内存分配函数,用于动态分配内存空间并初始化为零。它的函数原型如下:
void *calloc(size_t num, size_t size);
calloc
接受两个参数:num
表示要分配的元素个数,size
表示每个元素的大小(以字节为单位)。calloc
返回一个指向分配内存的指针,如果分配失败则返回 NULL
。
2.
realloc 是一个用于重新分配内存块大小的函数。它可以用于动态调整先前使用 malloc 或 calloc 分配的内存块的大小。realloc 函数接受两个参数:一个指向先前分配的内存块的指针,以及要重新分配的新大小。
realloc 的语法如下:
void* realloc(void* ptr, size_t size);
其中,ptr 是指向先前分配的内存块的指针,size 是要重新分配的新大小。realloc 函数返回一个指向重新分配内存块的指针。如果内存块的重新分配失败,则返回 NULL。
1.初始化尾部指针:
struct LinkNode* pRear = header;
2.其实初始化已经完成,下面插入输入的数,并插入到所创立的新的节点:
int val = -1;
while (1)
{
printf("请输入插入的数据:\n");
scanf_s("%d", &val);
if (val == -1)
{
break;
}
//创建新节点
struct LinkNode* newnode = (struct LinkNode*)malloc(sizeof(struct LinkNode));
newnode->data = val;
newnode->next = NULL;
//new节点插入到链表中
pRear->next = newnode;
pRear = newnode;
}
Q:如何解释pRear->next = newnode;
pRear = newnode;
图解:
总结:
struct LinkNode* Init_LinkNode()
{
struct LinkNode* header = (struct LinkNode*)malloc(sizeof(struct LinkNode));
header->data = -1;
header->next = NULL;
//尾部指针
struct LinkNode* pRear = header;
int val = -1;
while (1)
{
printf("请输入插入的数据:\n");
scanf_s("%d", &val);
if (val == -1)
{
break;
}
//创建新节点
struct LinkNode* newnode = (struct LinkNode*)malloc(sizeof(struct LinkNode));
newnode->data = val;
newnode->next = NULL;
//new节点插入到链表中
pRear->next = newnode;
pRear = newnode;
}
return header;
}
4.2 遍历链表
void Foreach_LinkList(struct LinkNode* header)
{
if (header == NULL)
{
return;
}
}
我们需要引入一个辅助指针变量。每次迭代时,将该指针移动到下一个节点,直到遍历完整个链表。
//辅助指针变量
struct LinkNode* pCurrent = header->next;
然后就是代码实现:
while (pCurrent != NULL)
{
printf("%d\n", pCurrent->data);
pCurrent = pCurrent->next;
}
Q;pCurrent = pCurrent->next?
就是pCurret向前移动了一格。
总结:
//遍历
void Foreach_LinkList(struct LinkNode* header)
{
if (header == NULL)
{
return;
}
//辅助指针变量
struct LinkNode* pCurrent = header->next;
while (pCurrent != NULL)
{
printf("%d\n", pCurrent->data);
pCurrent = pCurrent->next;
}
}
4.3 插入链表
void InsertByValue_LinkList(struct LinkNode* header, int oldval, int newval)
{
if (header == NULL)//判断一下输入的头结点不为空
{
return;
}
}
在oldval前面插入newval,我们需要传入参数:header,oldval,newval,我们思考如何插入:
1.我们应该遍历链表,找到oldval。
2.我们需要让oldval指向newval,再让newval指向oldval原本后面的那个表。
原来:
之后:
由上面的想法,我们意识到我们可能需要两个指针,这样才能在插入后,链接原链表的两头。
struct LinkNode* pPrev = header;
struct LinkNode* pCurrent = pPrev->next;
我们设立一个pPrev和PCurrent,一个在后,一个在前,然后遍历链表(此时代码如下:)。当pCurrent指向oldval时候,我们找到了oldval,然后创立一个新的节点newnode,将newnode的next指向Current,pPrev的next指向newnode。然后如果我们没有找到oldval,说明oldval不存在。我们看一下如何实现:
while (pPcurrent != NULL)//找到oldval
{
if (pPcurrent->data == oldval)
{
break;
}
pPrev = pPcurrent;
pPcurrent = pPcurrent->next;
}
if (pPcurrent==NULL)//pCurrent为空,则不存在
{
return;
}
//创建新结点
struct LinkNode* newnode = (struct LinkNode*)malloc(sizeof(struct LinkNode));
newnode->data = newval;
newnode->next = NULL;
//新结点插入
newnode->next = pPcurrent;
pPrev->next = newnode;
}
Q:pPrev = pPcurrent;
pPcurrent = pPcurrent->next;如何理解?就是向前移动
总结:
/在oldval前面插入一个新的数据newval;
void InsertByValue_LinkList(struct LinkNode* header, int oldval, int newval)
{
//判断参数
if (header == NULL)//插到数据尾部也可以
{
return;//+
}
//双指针,一个pPrev,一个pCurrent
struct LinkNode* pPrev = header;
struct LinkNode* pPcurrent = pPrev->next;
while (pPcurrent != NULL)//赵到oldval
{
if (pPcurrent->data == oldval)
{
break;
}
pPrev = pPcurrent;
pPcurrent = pPcurrent->next;
}
if (pPcurrent==NULL)//pCurrent为空,则不存在
{
return;
}
//创建新结点
struct LinkNode* newnode = (struct LinkNode*)malloc(sizeof(struct LinkNode));
newnode->data = newval;
newnode->next = NULL;
//新结点插入
newnode->next = pPcurrent;
pPrev->next = newnode;
}
4.4 删除链表
void RemoveByValue_LinkList(struct LinkNode* header,int(delvalue))
{
if (header == NULL)
{
return;
}
}
目标:找到data为delvalue的链表,删除(其实这里的删除可以视为,把这个要删除的数,前面的节点与后面的节点相连,如图)。
和刚刚的插入一样,我们需要映引入两个辅助的指针,做到连接。
我们的思路同上,还是需要先遍历,如果没有找到就返回:
while (pCurrent != NULL)
{
if (pCurrent->data == delvalue)
{
return;
}
pPrev= pCurrent;
pCurrent = pCurrent->next;
}
if (pCurrent == NULL)
{
return;
}
找到之后重新建立联系就好。但是需要将pCurrent指向的那个要删除的节点空间释放。
//重新建立带删除节点的前端和后端
pPrev->next = pCurrent->next;//就是跳过中间的那个区域,把前后两个节点相连,就相当于删除
free(pCurrent);
pCurrent = NULL;
free()是C语言中释放内存空间的函数,通常与申请内存空间的函数malloc()结合使用,可以释放由 malloc()、calloc()、realloc() 等函数申请的内存空间。free过后,pCurrent变成了野指针,
野指针是指指向无效内存地址的指针,它可能导致程序崩溃、数据损坏或安全漏洞。野指针危害:
-
数据损坏:使用野指针可能会导致数据被错误地读取或写入,从而导致数据损坏。这可能会导致程序产生不可预测的结果或错误的计算。
-
内存泄漏:如果野指针指向动态分配的内存,并且没有正确释放,可能会导致内存泄漏。这意味着程序将无法再次访问这部分内存,从而导致内存资源的浪费。
-
安全漏洞:恶意用户可以利用野指针进行攻击,例如通过修改指针指向的内存来执行恶意代码或获取敏感信息。
-
会导致程序崩溃:当野指针被解引用时,程序可能会崩溃,因为它试图访问无效的内存地址。
总结:
//删除值为val的节点
void RemoveByValue_LinkList(struct LinkNode* header,int(delvalue))
{
if (header == NULL)
{
return;
}
//两个辅助指针
struct LinkNode* pPrev = header;
struct LinkNode* pCurrent = pPrev->next;
while (pCurrent != NULL)
{
if (pCurrent->data== delvalue)
{
return;
}
pPrev= pCurrent;
pCurrent = pCurrent->next;
}
if (pCurrent == NULL)
{
return;
}
//重新建立带删除节点的前端和后端
pPrev->next = pCurrent->next;//就是跳过中间的那个区域,把前后两个节点相连,就相当于删除
free(pCurrent);
pCurrent = NULL;
}
后面的原理与前面几乎相同,就不再赘述。
4.5 清空链表
void Clear_LinkList(struct LinkNode* header)
{
if (header == NULL)
{
return 0;
}
struct LinkNode* pCurrent = header->next;
while (pCurrent != NULL)
{
//先保存下一个节点的地址
struct LinkNode* pNext = pCurrent->next;
//释放当前内存
free(pCurrent);
pCurrent = pNext;
}
header->next = NULL;
}
4.6清空链表
//清空
void Clear_LinkList(struct LinkNode* header)
{
if (header == NULL)
{
return 0;
}
struct LinkNode* pCurrent = header->next;
while (pCurrent != NULL)
{
//先保存下一个节点的地址
struct LinkNode* pNext = pCurrent->next;
//释放当前内存
free(pCurrent);
pCurrent = pNext;
}
header->next = NULL;
}
5 主函数里内容引用
这里以遍历与销毁为例:
int main()
{
struct LinkNode* header = Init_LinkNode();
Foreach_LinkList(header);
void InsertByValue_LinkList(struct LinkNode* header, int oldval, int newval);
return 0;
}
5.单双链表数组模拟
5.1 单链表数组模拟
看思路:
看代码:
int head, e[N], en[N],idx;
void init() {
head = -1;
idx = 0;
}
void add_head(int x) {
e[idx] = x;
en[idx] = head;
head = idx;
idx++;
}
void add(int k, int x) {
e[idx] = x;
en[idx] = en[k];
en[k] = idx;
idx++;
}
void del(int k) {
en[k] = en[en[k]];
}
5.2 双链表数组模拟
int e[N], l[N], r[N], idx;
// 0是左端点,1是右端点
r[0] = 1, l[1] = 0;
idx = 2;
// 在节点a的右边插入一个数x
void insert(int a, int x)
{
e[idx] = x;
l[idx] = a, r[idx] = r[a];
l[r[a]] = idx, r[a] = idx++;
}
// 删除节点a
void remove(int a)
{
l[r[a]] = l[a];
r[l[a]] = r[a];
}
6.STL:
构造
一般常用:
list<int> l
赋值和交换
大小
插入删除
特殊
遍历
it是迭代器,可以大致理解为一个指针。
7.题目
1.约瑟夫问题
n 个人围成一圈,从第一个人开始报数,数到 m 的人出列,再由下一个人重新从 11 开始报数,数到 m 的人再出圈,依次类推,直到所有的人都出圈,请输出依次出圈人的编号。
1.stl的写法:
#include <iostream>
using namespace std;
#include <list>
int n,m;
list<int> l;
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++) {
l.push_back(i);
}
list<int>::iterator it=l.begin();
while(l.size()>1){
for(int i=1;i<m;i++){
it++;
if(it==l.end()) it=l.begin();//一旦到结尾就返回开头(毕竟是环状)
}
cout<<*it<<" ";
list<int>::iterator next=++it;
if(next==l.end()) next=l.begin();
l.erase(--it);
it=next;
}
cout<<*it;
return 0;
}
2.队列安排
#include <iostream>
using namespace std;
#include <list>
typedef list<int>::iterator Iter;
const int MAX=1e5+10;//记得扩大范围
Iter pos[MAX];//非常有意思,开一个迭代器数组,专门记录位置
list<int> l;
bool j[MAX];
int main(){
int n;
cin>>n;
l.push_front(1);
pos[1]=l.begin();
j[1]=1;
int k,p;
for(int i=2;i<=n;i++) {
cin >> k >> p;
if (p == 0) {
pos[i]=l.insert(pos[k], i);//其实应该是在pos前面
}
else{
pos[i]=l.insert(next(pos[k]),i);
}
j[i]=1;
}
int m;
cin>>m;
for(int i=0;i<m;i++){
int ch;
cin>>ch;
if(j[ch]) {
l.erase(pos[ch]);
j[ch]=0;
}
}
bool first = true;
for (int x: l)
{
if (!first)
putchar(' ');
first = false;
printf("%d", x);
}
cout<<endl;
return 0;
}