单链表基础实现+单双链表数组模拟+STL使用+习题练习(超详解)

  1 什么是链表     

链表由一系列节点(链表中每一个元素称为节点)组成,节点在运行时动态生成 (malloc),每个节点包括两个部分:

     一个是存储数据元素的数据域data

     另一个是存储下一个节点地址的指针域next*

所以我们定义一个结构体,记录一下链表:

struct LinkNode
{
	int data;
	struct LinkNode* next;
};

2 数组与链表的优缺点

链表是通过节点把离散的数据链接成一个表,通过对节点的插入和删除操作从而实现 对数据的存取。而数组是通过开辟一段连续的内存来存储数据,这是数组和链表最大的区别。

数组的优点:

  1. 随机访问方便,可以通过下标直接访问元素。
  2. 存储效率高,因为连续的内存空间可以被CPU缓存,访问速度快。
  3. 可以实现一些基于数组的算法,如二分查找等。

数组的缺点:

  1. 数组的大小固定,无法动态扩展或缩小。
  2. 插入和删除元素时需要移动其他元素,效率较低。
  3. 内存空间可能被浪费,因为数组的大小固定,可能会预留过多的空间。

链表的优点:

  1. 动态扩展和缩小容易,不需要移动其他元素。
  2. 插入和删除元素时效率高。
  3. 内存空间利用率高,因为链表只会分配需要的空间。

链表的缺点:

  1. 随机访问元素效率低,需要遍历整个链表。
  2. 链表的存储空间比数组大,因为每个元素都需要一个指针指向下一个元素。
  3. 链表不支持一些基于数组的算法,如二分查找等。

3 链表的类型

1.双链表

2.循环链表


4 链表的基础使用(结构体加指针实现)

4.1 链表的初始化

我们先来看一下两个重要的概念:

1.头结点: 

链表中的头结点是指在链表的开始位置添加一个额外的节点,用于方便对链表进行操作。头结点并不存储实际的数据,它只是作为链表的起始点,用于指向第一个实际存储数据的节点。

为什么链表要有头结点的原因有以下几点:

  1. 方便插入和删除操作:在链表的头部插入或删除节点是一种常见的操作。如果没有头结点,插入和删除操作需要特殊处理,而有了头结点后,可以直接通过头结点进行插入和删除操作,代码实现更加简洁。

  2. 简化代码逻辑:有了头结点后,链表的第一个节点不再特殊,所有节点的操作可以统一处理,代码逻辑更加简单。

  3. 便于空链表的处理:当链表为空时,头结点可以用来表示空链表,避免了对空指针的处理。

 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变成了野指针,

野指针是指指向无效内存地址的指针,它可能导致程序崩溃、数据损坏或安全漏洞。野指针危害:

  1. 数据损坏:使用野指针可能会导致数据被错误地读取或写入,从而导致数据损坏。这可能会导致程序产生不可预测的结果或错误的计算。

  2. 内存泄漏:如果野指针指向动态分配的内存,并且没有正确释放,可能会导致内存泄漏。这意味着程序将无法再次访问这部分内存,从而导致内存资源的浪费。

  3. 安全漏洞:恶意用户可以利用野指针进行攻击,例如通过修改指针指向的内存来执行恶意代码或获取敏感信息。

  4. 会导致程序崩溃:当野指针被解引用时,程序可能会崩溃,因为它试图访问无效的内存地址。

总结:

//删除值为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;
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值