练题100天——DAY39:单链表练习题×5

五道单链表练习题,都不算特别简单,有一些也需要思考好一会才想得过来。

一.单链表就地逆置 ★★★☆☆

题目

试写一算法,对单链表实现就地逆置,注意时间复杂度最好能达到O(n)

思路1

从尾结点开始找每个结点的前驱,然后使每个结点的后继为其前驱

代码1

cpp 复制代码
void ReverseList(List plist) {
	assert(plist != NULL);
	if (plist == NULL) {
		return;
	}
	//找到尾结点,并记录
	Node* p = plist;
	while (p->next != NULL) {
		p = p->next;
	}
	Node* q = p;
	Node* d;
	while (true) {
		//找到q的前驱
		d = plist;
		while (d->next != q) {
			d = d->next;
		}
		//q的前驱不是头结点,即q不是第一个结点
		if (d != plist) {
			//使q的后继为d
			q->next = d;
			//使q指向d
			q = d;
		}
		else {
			//q是第一个结点
			//使其后继为NULL
			q->next = NULL;
			//使头结点后继为尾结点
			d->next = p;
			//退出循环,实现逆置
			break;
		}
	}
}
错误原因

实现了就地逆置(空间复杂度为O(1)),但是时间复杂度为O(n²)

思路2

使用一个Node*类型的数组保存下每个结点的地址,然后遍历链表,改变每个结点的后继

代码2

cpp 复制代码
bool ReverseList(List plist) {
	assert(plist != NULL);
	if (plist == NULL) {
		return false;
	}
	//求出链表有效数据长度
	int len = GetLength(plist);
	//创建一个Node*类型的数组,保存各个结点的地址
	Node** p = (Node**)malloc(sizeof(Node*) * len);
	assert(p != NULL);
	if (p == NULL) {
		return false;
	}
	//遍历链表,使p保存各个结点的地址
	Node* q = plist;
	for (int i = 0; i < len; i++) {
		p[i] = q->next;
		q = q->next;
	}
	//改变每个结点的next
	q = plist;
	for (int i = len - 1; i >= 0; i--) {
		q->next = p[i];
		q = q->next;
	}
	free(p);
	return true;
}
错误原因

没有对尾结点进行处理,导致链表成环,同时虽然时间复杂度满足了O(n),但不符合就地逆置

在free(p); 前加上 q->next=NULL,使得尾结点后继为NULL,修正链表为环问题

同时在free(p); 后面加上 p=NULL; ++将野指针置空,提高安全性++

思路3

使用三个指针p、q、d记录连续的三个结点,一开始,p表示头结点,q表示第一个结点,d表示第二个结点,使q的后继为NULL,然后遍历链表,将三个指针向后移动,每次都将q的后继改为p,实现逆置,当d为尾结点时结束遍历,使头结点后继为d,d的后继为q,实现整个链表的逆置。

代码3

cpp 复制代码
bool ReverseList(List plist) {
	assert(plist != NULL);
	if (plist == NULL) {
		return false;
	}
	//利用三指针记录连续的三个结点
	Node* p = plist;
	Node* q = p->next;
	Node* d = q->next;

	//使第一个结点的后继为NULL
	q->next = NULL;
	//遍历链表
	while (d->next != NULL) {
		//三个指针向后移动
		p = q;
		q = d;
		d = d->next;
		//使结点后继指向前驱
		q->next = p;
	}
	//当d为尾结点时结束循环
	//头结点后继改为尾结点
	plist->next = d;
	//尾结点后继指向前驱
	d->next = q;

	return true;
}
错误原因

为对空链表/单结点链表进行判断处理,会导致代码直接崩溃

修改
cpp 复制代码
bool ReverseList(List plist) {
	assert(plist != NULL);
	if (plist == NULL) {
		return false;
	}
	//利用三指针记录连续的三个结点
	Node* prio = NULL;
	Node* cur = plist->next;
	Node* next = NULL;
	
	//特殊情况的处理:
	//1.当链表为空时不执行循环
	//2.单结点链表时,头结点和第一个结点的后继不变
	while (cur != NULL) {
		next = cur->next;
		cur->next = prio;
		prio = cur;
		cur = next;
	}
	//循环结束后,prio为尾结点,使头结点后继为prio,实现逆置
	plist->next = prio;

	return true;
}

复杂度

N为单链表有效数据长度

时间复杂度:O(N)

空间复杂度:O(1)

思路4:"头插"法------重要

头插法是指链表的一种插入方法,每次需要插入一个新的元素,将其插入到头结点后,时间复杂度为O(1),通过头插法插入的元素,在链表中存储顺序跟插入顺序刚好想法,类似于逆置,所以可以采用类似于"头插"的方法实现链表的逆置。

使用"头插法"的大概思路:将单链表分为头结点部分和待逆置部分(除去头结点外的其它结点),记录待逆置部分的第一和第二个结点,然后将头结点的next置为NULL,使其称为空链表,然后将待逆置部分的结点依次用头插法插入到链表中。

代码

cpp 复制代码
bool ReverseList(List plist) {
	assert(plist != NULL);
	if (plist == NULL) {
		return false;
	}
	if (plist->next == NULL) {
		printf("链表为空\n");
		return true;
	}

	Node* p = plist->next;
	if (p->next == NULL) {
		return true;
	}
	Node* q = p->next;
	//将链表变为空链表
	plist->next = NULL;
	//不断头插
	while (p != NULL) {
		q = q->next;
		//绑线,先帮后面
		p->next = plist->next;
		//再绑前面
		plist->next = p;
		p = q;//p始终保存即将 头插 的结点
	}
	return true;
}

复杂度

N为单链表有效数据长度

时间复杂度:O(N)

空间复杂度:O(1)

运行测试

测试代码

cpp 复制代码
#include<stdio.h>
#include"list.h"

int main() {
	Node list;
	InitList(&list);
	
	printf("空链表逆置:\n");
	printf("逆置前:");
	Show(&list);
	ReverseList(&list);
	printf("逆置后:");
	Show(&list);

	printf("\n单结点链表逆置:\n");
	Insert_head(&list, 90);
	printf("逆置前:");
	Show(&list);
	ReverseList(&list);
	printf("逆置后:");
	Show(&list);

	printf("\n多结点链表逆置:\n");
	Insert_head(&list, -10);
	Insert_head(&list, 0);
	Insert_head(&list, 70);
	printf("逆置前:");
	Show(&list);
	ReverseList(&list);
	printf("逆置后:");
	Show(&list);

	Destory(&list);
	return 0;
}

截图

二.删除倒数第k个结点 ★☆☆☆☆

题目

给定单链表头结点,删除链表中倒数第k个结点

思路1

倒数第k个结点,就是正数第 len-k+1 个结点。从前往后遍历链表到第 len-k 个结点,即第 len-k+1 个结点的前驱,记为p,q表示倒数第k个结点,使p->next=q->next,然后释放结点q即可

代码1

cpp 复制代码
//删除倒数第k个结点
bool deleteKNode(List plist,int k) {
	assert(plist != NULL);
	if (plist == NULL) {
		return false;
	}
	int len = GetLength(plist);
	//对k进行判断
	if (k <= 0 || k > len) {
		printf("不存在倒数第%d个结点\n",k);
		return false;
	}
	//找到倒数第k个结点的前驱
	Node* p = plist;
	for (int i = 0; i < len - k; i++) {
		p = p->next;
	}
	//保存倒数第k个结点
	Node* q = p->next;
	p->next = q->next;
	//释放
	free(q);
	q = NULL;//防止野指针
	return true;
}
错误原因

对空链表的处理不清晰,单独处理空链表:提示信息更精准,便于快速定位问题;能减少不必要的性能开销,更高效;代码逻辑更清晰,可读性更强

修正

单独增加对空链表的处理

cpp 复制代码
//删除倒数第k个结点
bool deleteKNode(List plist,int k) {
	assert(plist != NULL);
	if (plist == NULL) {
		return false;
	}
	//空链表直接返回
	if (plist->next == NULL) {
		printf("链表为空,删除失败\n");
		return false;
	}
	int len = GetLength(plist);
	//对k进行判断
	if (k <= 0 || k > len) {
		printf("不存在倒数第%d个结点\n",k);
		return false;
	}
	//找到倒数第k个结点的前驱
	Node* p = plist;
	for (int i = 0; i < len - k; i++) {
		p = p->next;
	}
	//保存倒数第k个结点
	Node* q = p->next;
	p->next = q->next;
	//释放
	free(q);
	q = NULL;//防止野指针
	return true;
}

复杂度

N为链表有效数据个数

时间复杂度:O(N)。求长度函数将链表遍历一次、找前驱遍历一次,所以最坏情况下的时间复杂度为O(N)+O(N)=O(N)

空间复杂度:O(1)。

思路2

两个指针p、q,p先走k步,然后两个指针一起走,最后当p走到尾结点时,q指针所在的结点就是倒数第k个结点的前驱,然后按照链表的套路删除倒数第k个结点即可

代码

cpp 复制代码
bool deleteKNode(List plist, int k) {
	assert(plist != NULL);
	if (plist == NULL) {
		return false;
	}
	//空链表直接返回
	if (plist->next == NULL) {
		printf("链表为空,删除失败\n");
		return false;
	}
	//对k进行判断
	if (k <= 0) {
		printf("不存在倒数第%d个结点\n",k);
		return false;
	}
	Node* p = plist;
	Node* q = plist;
	//p先走k步
	for (int i = 0; i < k; i++) {
		//没走到k步就到了尾结点 → k>链表长度
		if (p->next == NULL) {
			return false;
		}
		p = p->next;
	}
	//同时向后移动
	while (p->next != NULL) {
		p = p->next;
		q = q->next;
	}
	//保存结点
	Node* r = q->next;
	q->next = r->next;
	//释放结点
	free(r);
	r = NULL;
	return true;
}

复杂度

n为链表有效结点个数

时间复杂度:O(n)。虽然有两个指针遍历链表,但遍历属于同一级,其中p指针的时间复杂度为O(n),q指针遍历的时间复杂度为O(m),m=n-k,所以总的时间复杂度为O(n)

空间复杂度:O(1)。

三.监测是否有环 ★★☆☆☆

题目

给定单链表,监测是否有环

思路1:哈希表

遍历链表的结点,利用哈希表存储遍历到的结点,当遍历到的结点已存在与哈希表中时,说明该链表有环,反之无环。遍历次数最多为链表结点数+1。

代码

cpp 复制代码
bool isHaveLoop(List plist) {
	assert(plist != NULL);
	if (plist == NULL) {
		return false;
	}
	//空链表判断
	if (plist->next == NULL) {
		printf("该链表为空,没有环\n");
		return false;
	}
	//创建哈希表,记录每个结点
	//遍历链表,有环会再次遍历到哈希表中已有的结点
	int len = GetLength(plist) + 1;//包括头结点也要记录到哈希表中
	unordered_set<Node*> set(len);
	Node* p = plist;
	for (int i = 0; i < len + 1; i++) {
		//没有环,遍历到链表之外了
		if (p == NULL) {
			return false;
		}
		//哈希表中没有遍历到的结点
		if (set.find(p) == set.end()) {
			set.insert(p);//将该结点放入哈希表
			p = p->next;//移动结点
			continue;//继续遍历
		}
		//遍历到的结点已在哈希表中,说明有环
		return true;
	}
	//遍历完了还没有遇到环,说明没有环,返回false
	return false;
}
错误2
测试函数
错误原因

1.GetLength(plist)在有环链表场景下会陷入死循环:因为GetLength函数的循环遍历链表时,循环结束条件是 p==NULL,在手动添加环后,p==NULL不能发生,所以陷入死循环

cpp 复制代码
//获取长度
int GetLength(List plist) {
	assert(plist != NULL);
	if (plist == NULL) {
		return -1;
	}

	Node* p = plist->next;
	int count = 0;
	while (p != NULL) {
		count++;
		p = p->next;
	}

	return count;
}

2.for (int i = 0; i < len; i++)的遍历次数被len限制,无法检测到环:即使GetLength函数没有问题,len的值也只是表示无环链表的结点个数,但是最坏情况下(首尾结点连接成环),要找到环,即找到原来遍历过的结点,需要多遍历一次,所以for循环遍历次数错误

修正

抛弃GetLength求长度的逻辑,同时修改遍历逻辑:使用while循环,直到找到环或遍历到NULL

cpp 复制代码
bool isHaveLoop(List plist) {
	assert(plist != NULL);
	if (plist == NULL) {
		return false;
	}
	//空链表判断
	if (plist->next == NULL) {
		printf("该链表为空,没有环\n");
		return false;
	}
	//创建哈希表,记录每个结点
	//遍历链表,有环会再次遍历到哈希表中已有的结点
	unordered_set<Node*> set;
	Node* p = plist;
	//一直遍历链表,直到遍历完或遇到环
	while (p != NULL) {
		//在数组中查看是否已有该结点
		bool flag = false;
		for (int j = 0; j < set.size(); j++) {
			if (set.find(p) != set.end()) {
				flag = true;
				break;
			}
		}
		//数组中有该结点→有环
		if (flag) {
			return true;
		}
		//没有该结点,将其放入数组
		set.insert(p);
		//移动结点
		p = p->next;
	}
	//遍历完了还没有遇到环,说明没有环,返回false
	return false;
}

优化

冗余逻辑(内层 for 循环) :代码中 for (int j = 0; j < set.size(); j++) 是完全冗余的 ------set.find(p) 本身已经完成了「查找结点是否存在」的逻辑,内层 for 循环只是重复执行 set.find(p)

cpp 复制代码
bool isHaveLoop(List plist) {
	assert(plist != NULL);
	if (plist == NULL) {
		return false;
	}
	//空链表判断
	if (plist->next == NULL) {
		printf("该链表为空,没有环\n");
		return false;
	}
	//创建哈希表,记录每个结点
	//遍历链表,有环会再次遍历到哈希表中已有的结点
	unordered_set<Node*> set;
	Node* p = plist;
	//一直遍历链表,直到遍历完或遇到环
	while (p != NULL) {
		//在数组中查看是否已有该结点
		if (set.find(p) != set.end()) {
			//数组中有该结点→有环
			return true;
		}
		//没有该结点,将其放入数组
		set.insert(p);
		//移动结点
		p = p->next;
	}
	//遍历完了还没有遇到环,说明没有环,返回false
	return false;
}

复杂度

N为链表结点个数

时间复杂度:O(N)

空间复杂度:O(N)

思路2:数组

将思路1的哈希表换为动态数组即可

代码

cpp 复制代码
//2.利用数组保存已遍历结点
bool isHaveLoop(List plist) {
	assert(plist != NULL);
	if (plist == NULL) {
		return false;
	}
	//空链表判断
	if (plist->next == NULL) {
		printf("该链表为空,没有环\n");
		return false;
	}
	//创建数组保存结点
	vector<Node*> arr;
	Node* p = plist;
	//一直遍历链表,直到遍历完或遇到环
	int i = 0;
	while(p != NULL){
		//在数组中查看是否已有该结点
		bool flag = false;
		for (int j = 0; j < arr.size(); j++) {
			if (arr[j] == p) {
				flag = true;
				break;
			}
		}
		//数组中有该结点→有环
		if (flag) {
			return true;
		}
		//没有该结点,将其放入数组
		arr[i++] = p;
		//移动结点
		p = p->next;
	}
	return false;
}
错误1
使用的测试代码
cpp 复制代码
int main() {
	Node list;
	InitList(&list);

	printf("1.空链表测试:\n");
	Show(&list);
	isHaveLoop(&list);

	printf("2.无环链表测试:\n");
	InitList(&list);
	for (int i = 0; i < 10; i++)
	{
		Insert(&list, i, i);
	}
	Show(&list);
	isHaveLoop(&list);

	return 0;
}
出现错误

无法处理无环的单链表

表示:试图访问一个 vector 中不存在的下标位置,也就是下标超出了 vector 当前的有效范围

修改

arr[i]访问越界,因为初始化时arr默认为空,所以长度为0,无法通过下标访问

需要换为arr.push_back()添加元素

cpp 复制代码
bool isHaveLoop(List plist) {
	assert(plist != NULL);
	if (plist == NULL) {
		return false;
	}
	//空链表判断
	if (plist->next == NULL) {
		printf("该链表为空,没有环\n");
		return false;
	}
	//创建数组保存结点
	vector<Node*> arr;
	Node* p = plist;
	//一直遍历链表,直到遍历完或遇到环
	while(p != NULL){
		//在数组中查看是否已有该结点
		bool flag = false;
		for (int j = 0; j < arr.size(); j++) {
			if (arr[j] == p) {
				flag = true;
				break;
			}
		}
		//数组中有该结点→有环
		if (flag) {
			return true;
		}
		//没有该结点,将其放入数组
		arr.push_back(p);
		//移动结点
		p = p->next;
	}
	return false;
}

复杂度

n为链表结点个数

时间复杂度:O(n²)。嵌套循环:外层循环遍历每一次数组,内层循环每一次都需要遍历数组中已存储2的结点,所以总的时间复杂度为O(n)+O(0 + 1 + 2 + ... + (n-1))=O(n²)。

空间复杂度:O(n)。额外空间开销主要是数组arr的空间,最坏情况下,需要存储链表的所有结点,所以空间复杂度为O(n)。

思路3:快慢指针

使用两个指针,一快一慢,快指针fast每次移动到下下个结点,慢指针slow每次移动到下一个结点,如果链表没有环,那么最后肯定会出现fast->next==NULL或fast==NULl的情况,如果没环,fast肯定会和slow相遇。

代码

cpp 复制代码
bool isHaveLoop(List plist) {
	assert(plist != NULL);
	if (plist == NULL) {
		return false;
	}
	//空链表判断
	if (plist->next == NULL) {
		printf("该链表为空,没有环\n");
		return false;
	}
	//快慢指针
	Node* slow = plist->next;
	Node* fast = plist->next->next;
	//链表被遍历完
	while (fast != NULL && fast->next != NULL) {
		//快慢指针相遇→有环
		if (slow == fast) {
			return true;
		}
		//慢指针每次走一步
		slow = slow->next;
		//快指针每次走两步
		fast = fast->next->next;
	}
	//循环结束→无环
	return false;
}

复杂度

N为链表结点数

时间复杂度:O(N)。遍历次数与N成正比,所以时间复杂度就是O(N)。

空间复杂度:O(1)。仅使用了两个指针变量,指针变量的数量和占用的内存大小固定,不随链表结点数 n 变化,所以空间复杂度为O(1)。

四.相交链表 ★★☆☆☆

题目

给定两个单链表(head1,head2),检测两个链表是否有交点,如果有返回第一个交点地址

这道题在练题100天------DAY25:升序合并文件+相交链表+多数元素-CSDN博客中做过,当时使用了 哈希表 的方法,官方题解利用了 "使两个指针在链表上移动距离一样" 的思路,这里补充另外一个思路

思路

创建两个指针,分别遍历两个链表,但是如果两个链表有交点,想要使两个指针相遇,就需要使它们到交点的距离一致。因为交点后的长度一致,所以需要使交点前移动的距离一致,即可以使在较长的链表上的指针先移动一段距离,使得两个指针同时移动到交点的距离一致。

注意:如果两个链表没有交点,在两个交点相等时,表示两个指针移动到了尾结点的next,即NULL,所以++不能仅仅通过两个指针相等来判断是否有交点++

代码

cpp 复制代码
Node* IsIntersected(Node* head1, Node* head2) {
	assert(head1 != NULL && head2 != NULL);
	if (head1 == NULL || head1 == NULL) {
		return NULL;
	}
	//创建两个指针,分别遍历两个链表
	//如果有交点,两个指针与交点距离一样,才能在交点相遇
	//所以更长的一个链表的指针需要先移动一段距离
	int len1 = GetLength(head1);//链表1的长度
	int len2 = GetLength(head2);//链表2的长度
	Node* p = head1;
	Node* q = head2;
	//移动指针,使两指针一起移动时移动距离一致
	while (len1 > len2) {
		p = p->next;
		len1--;
	}
	while (len2 > len1) {
		q = q->next;
		len2--;
	}
	while (p != NULL && q != NULL) {
		//相遇/相等时退出循环
		if (p == q) {
			break;
		}
		//一起移动
		p = p->next;
		q = q->next;
	}
	//在交点相遇
	if (p == q && p != NULL) {
		return p;
	}
	//一起移动到末尾,无交点
	return NULL;
}

复杂度

m、n分别为两个链表的长度

时间复杂度:O(m+n)。最坏情况:无交点以及交点在尾结点,都需要遍历完两个链表,所以总的时间复杂度为O(m+n)。

空间复杂度:O(1)。

五.求链表中环的第一个结点 ★★★☆☆

题目

给定单链表,如果有环,返回从头结点进入环的第一个结点

思路1:哈希表

利用判断链表中是否有环的思路,遇到哈希表中已有的结点,直接返回

代码

cpp 复制代码
Node* FirstCircleNode(List plist) {
	assert(plist != NULL);
	if (plist == NULL) {
		return NULL;
	}
	//空链表判断
	if (plist->next == NULL) {
		printf("该链表为空,没有环\n");
		return NULL;
	}
	//创建哈希表,记录每个结点
	//遍历链表,有环会再次遍历到哈希表中已有的结点
	unordered_set<Node*> set;
	Node* p = plist;
	//一直遍历链表,直到遍历完或遇到环
	while (p != NULL) {
		//在数组中查看是否已有该结点
		if (set.find(p) != set.end()) {
			//数组中有该结点→有环
			return p;
		}
		//没有该结点,将其放入数组
		set.insert(p);
		//移动结点
		p = p->next;
	}
	//遍历完了还没有遇到环,说明没有环,返回NULL
	return NULL;
}

复杂度

n为链表的结点数

时间复杂度:O(n)。外层循环,每个结点被遍历一次,总遍历次数为n;内层,哈希表操作(set.find(p) / set.insert(p))的平均时间复杂度为 O (1),所以总的时间复杂度为O(n)*O(1)=O(n)。

空间复杂度:O(n)。哈希表存储n个结点。

思路2 ★★★★☆

首先利用快慢指针判断该链表是否是环。

然后一指针从快慢指针相遇的位置开始,另一指针从头结点开始,同时移动,它们相遇的地方就是环的第一个结点。

这一结论的推导过程大致如下:

1.设头结点A到环的第一个结点B这段链表共有a个结点,从环的第一个结点B到快慢指针相遇的结点C这段链表共有b个结点,环共有c个结点

2.快指针每次移动两下,慢指针每次移动一下,它们同时开始移动,则可以得知:慢指针移动距离*2=快指针移动距离。慢指针移动距离为a+b,快指针移动距离为a+b+k*c,其中k为快指针绕环的圈数,则有2*(a+b)=a+b+k*c,化简得 a=k*c-b

3.由 a=k*c-b可以推得,如果一个指针 r 从头结点A出发,一个指针 p 从快慢指针相遇处C出发,在 p 指针在环上跑了(k-1)圈,由多跑了(c-b),即总路程为(k*c-b)个结点时,p指针跑到环的第一个结点B处,r指针跑的路程为a,也到了环的第一个结点B处,两指针相遇,由此可得出环的第一个结点地址。

代码

cpp 复制代码
Node* FirstCircleNode(List plist) {
	assert(plist != NULL);
	if (plist == NULL) {
		return NULL;
	}
	//空链表判断
	if (plist->next == NULL) {
		return NULL;
	}
	//快慢指针
	Node* p = plist->next->next;
	Node* q = plist->next;
	while (p != NULL && p->next != NULL && p != q) {
		p = p->next->next;
		q = q->next;
	}
	//p和q没有相遇→没有环
	if (p == NULL || p->next == NULL) {
		return NULL;
	}
	//一指针从头结点出发
	Node* r = plist;
	//同时出发,最后会在环的第一个结点相遇
	while (r != p) {
		r = r->next;
		p = p->next;
	}
	return r;
}

复杂度

n为链表有效结点个数

时间复杂度:O(n)。两次线性遍历(找相遇点 + 找入口),总次数与链表结点数成正比。

空间复杂度:O(1)。仅使用 3 个指针变量,无额外动态内存开销。

相关推荐
txinyu的博客2 小时前
布隆过滤器
数据结构·算法·哈希算法
52Hz1182 小时前
力扣240.搜索二维矩阵II、160.相交链表、206.反转链表
python·算法·leetcode
blueSatchel2 小时前
bus_register源码研究
linux·c语言
程序员zgh2 小时前
C语言 弱定义机制 解读
c语言·开发语言·c++
We་ct2 小时前
LeetCode 380. O(1) 时间插入、删除和获取随机元素 题解
前端·算法·leetcode·typescript
老鼠只爱大米2 小时前
LeetCode经典算法面试题 #234:回文链表(双指针法、栈辅助法等多种方法详细解析)
算法·leetcode·链表·递归·双指针·快慢指针·回文链表
独自破碎E2 小时前
【动态规划】兑换零钱(一)
算法·动态规划
Sarvartha2 小时前
顺序表笔记
算法
宵时待雨2 小时前
数据结构(初阶)笔记归纳6:双向链表的实现
c语言·开发语言·数据结构·笔记·算法·链表