链表结构定义、操作以及经典题目

本文深入探讨链表数据结构,包括单向链表和双向链表的实现,以及插入、删除、翻转等操作。通过实例解析经典问题,如LeetCode的去重问题和约瑟夫环,强调哨兵节点在链表操作中的重要性。
摘要由CSDN通过智能技术生成

前言

复习一下数据结构和算法,今天开始把这些复习好的东西都写成blog发出来吧。也顺便可以方便其他想要学习的小伙伴。环境如果没有特殊说明,就代表用的是C++20 MSVC。默认情况下直接使用了using namespace std;(如cin, cout, endl, STL定义好的数据结构如list, vector等)。只有少数情况——可能定义的函数有大概率重合(如std::move, std::sort, std::max, …)才不会直接使用namespace std;

什么是链表(List/LinkedList)

链表的核心在于其数据结构内部自己定义了一个指向自己结构的指针。
链表常见的一般有单向链表、双向链表、循环链表。

按照我个人理解,可以将链表理解成为:A给你一个锦囊,让你去找B解决问题,你找到B后发现他不是这个领域的专家,于是B跟你说让你去找C,C可能知道如何解决这个问题,于是你继续去找C……直到找到你想要的可以解决你要找的问题的人。在下面的实现当中,我们直接简单地将“需要解决的问题”当做int整形来处理。为了方便,也不写模板了。

链表总体来讲比较简单,唯二需要注意的点就是1. 哨兵头节点的应用,以及2. 删除操作的实现。

链表的特点

  1. 头尾删除插入一般较快(双向链表O(1)),单向链表头部删除插入较快(O(1)),单向链表尾部插入为O(n)
  2. 遍历速度为O(n)

单向链表的实现

凡是

  1. 凡是和头结点操作相关的操作,都最好使用哨兵/哑头结点(dummyhead),这样可以大量减少和头结点相关的判断操作。注意使用哨兵节点后,事实上链表的第一个节点应当是dummyhead->next.
  2. 凡是和删除相关的操作,一定需要有一个指针停留在待删除结点的前面。

由于链表大多数时间不会存放过多的数据(否则遍历和查找都会比较慢),因此下面代码不考虑申请空间失败的情况。

其中dummyhead的应用非常广泛并且重要,大家可以在下面的代码当中感受其作用。

结构操作

其核心在于要杜绝出现内存泄漏问题、对空指针访问问题。

插入

  • 头插
  • index插入

头插较为简单,分为下列步骤:

  1. 新建节点
  2. 将新建节点的next指针指向dummyhead的next。
  3. 将dummyhead的next指向新建的节点

index插入:
新建节点->创建指针p指向dummyhead->移动index次p->新建节点next为p的next,p的next=node

删除

和插入一样,先定义 一个指针p指向dummyhead,随后移动index次p,此时p即为要删除节点的前一个节点。如果这里理不清楚的可以画个图看一看。

在删除时,需要将被删除节点的前一个节点的(也就是p指向的节点)next指针指向被删除节点的next指针,随后将被删除节点的内存释放。

翻转

先让cur指针指向head.next,随后让head.next为空。随后在cur不为空的循环内定义q指向cur的next,然后将cur.next指向head.next,再把head.next指向cur,再让cur=q。这样一来,头结点就完成了翻转,注意不能将head指向cur,因为head是不参与他们之间的操作的,只有head.next才是链表真正的头结点。这一块如果想不明白的,拿个纸画一画就明白了。

其他操作

其他操作包括返回链表长度、链表遍历等,比较简单,就不多说了。

单向链表代码

#include<iostream>
#include<vector>
#include<queue>
#include<cstdio>
#include<format>
#include<ctime>
#include<list>
#include<algorithm>
using namespace std;

class List {
private: 
	struct listnode {
		int mvalue;
		listnode* next;

		listnode() :mvalue(0), next(nullptr) {}
		listnode(int value) :mvalue(value), next(nullptr) {}
	};

private:
	listnode head;//dummyhead
	int mlength;
public:
	List() :mlength(0) {
		head.next = nullptr;
	}

	~List() {
		listnode* cur = head.next;
		while (cur) {
			listnode* del = cur;
			cur = cur->next;
			delete del;
		}
	}

	void push_front(int value) {
		listnode* node = new listnode(value);
		node->next = head.next;
		head.next = node;
		++mlength;
	}

	void insert(int value, int index) {
		if (index<0 || index>mlength) return;//failed to insert
		listnode* node = new listnode(value);
		listnode* p = &head;
		while (index--) {
			p = p->next;
		}
		node->next = p->next;
		p->next = node;
		++mlength;
	}

	void erase(int index) {
		if (index < 0 || index >= mlength) return;//The node needed to be deleted cannot be nullptr;
		listnode* p = &head, * del;
		while (index--) {
			p = p->next;
		}
		del = p->next;
		p->next = del->next;
		delete del;
		--mlength;
	}

	void reverse_list() {
		listnode* cur = head.next, * next;
		head.next = nullptr;
		while (cur) {
			next = cur->next;
			cur->next = head.next;
			head.next = cur;
			cur = next;
		}
	}

	void traverse() {
		listnode* cur = head.next;
		cout << "list: { ";
		while (cur) {
			cout << cur->mvalue << "->";
			cur = cur->next;
		}
		cout << "nullptr }\n\n\n";
	}

	int length() {
		return mlength;
	}

};
int main() {
	srand(time(0));

	List l;
	for (int i = 0; i < 20; ++i) {
		int value = rand() % 100, operation = rand() % 10, index = rand() % (l.length() + 2);
		//push_front test
		if (operation == 0) {
			cout << std::format("push_front() was called, value {} was inserted\n", value);
			l.push_front(value);
			l.traverse();
		}
		//insert test
		else if (operation <= 6) {
			cout << std::format("insert() was called, value {} within index (if legal) {} was inserted\n", value, index);
			l.insert(value, index);
			l.traverse();
		}
		//erase
		else {
			cout << std::format("erase() was called, node (if existed) at {} was erased\n", index);
			l.erase(index);
			l.traverse();
		}
	}
	l.reverse_list();
	l.traverse();
	return 0;
}

经典例题

https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/

class Solution {
public:
	ListNode* deleteDuplicates(ListNode* head) {
		if (head == nullptr || head->next == nullptr) return head;
		ListNode* dummy = new ListNode(-101, head);
		ListNode* p = dummy;
		while (p->next) {
			ListNode *q = p->next->next;
			while (q && q->val == p->next->val) {
				q = q->next;
			}
			if (q == p->next->next) {
				p = p->next;
			}
			else {
				ListNode* temp = p->next;
				/*while (temp != q) {
					ListNode* delete_node = temp;
					temp = temp->next;
					delete delete_node;
				}*/
				p->next = q;
			}
		}
		ListNode* realhead = dummy->next;
		delete dummy;
		return realhead;
	}
};

注释掉的代码块由于会影响效率就注释掉了。

单向循环链表

只需要把上面的代码里面的

	List() :mlength(0) {
		head.next = nullptr;
	}

改成List() :mlength(0) { head.next = &head; }

经典例题

约瑟夫环问题。
这个问题其实有很多更好的方法解决,放在这里主要是为了解决单向循环链表的一个坑。
注意:如果用dummyhead你会怎样跳过dummyhead呢?(这是一个小坑)

双向链表实现(Doubly-Linked List)

比单向链表更好理解和操作。除了指向下一个之外,还可以指向上一个节点,极大方便了删除操作,还能够迅速地插入/删除尾结点了。废话不多说,直接上代码。

class DList {
private:
	struct dlistnode {
		int mvalue;
		dlistnode* next, * pre;

		dlistnode() :mvalue(0), next(nullptr), pre(nullptr) {}
		dlistnode(int value) :mvalue(value), next(nullptr), pre(nullptr) {}
	};

private:
	dlistnode head, tail;
	int mlength;

public:
	DList() :mlength(0) {
		head.next = &tail;
		tail.pre = &head;
		head.pre = tail.next = nullptr;
	}
	~DList() {
		dlistnode* cur = head.next;
		while (cur != &tail) {
			dlistnode* del = cur;
			cur = cur->next;
			delete del;
		}
	}
	void push_front(int value) {
		dlistnode* node = new dlistnode(value);
		node->next = head.next;
		head.next->pre = node;
		node->pre = &head;
		head.next = node;
		++mlength;
	}
	void push_back(int value) {
		dlistnode* node = new dlistnode(value);
		node->next = &tail;
		tail.pre->next = node;
		node->pre = tail.pre;
		tail.pre = node;
		++mlength;
	}
	void insert(int value, int index) {
		if (index<0 || index>mlength) return;
		dlistnode* p = &head;
		while (index--) p = p->next;
		dlistnode* node = new dlistnode(value);
		node->next = p->next;
		p->next = node;
		node->pre = p;
		node->next->pre = node;
		++mlength;
	}
	void erase(int index) {
		if (index < 0 || index >= mlength) return;
		dlistnode* p = &head;
		while (index--) p = p->next;
		dlistnode* del = p->next;
		p->next = del->next;
		del->next->pre = p;
		delete del;
		--mlength;
	}
	void reverse() {
		dlistnode* cur = head.next, * next;
		head.next = &tail;
		while (cur != &tail) {
			next = cur->next;
			cur->pre = cur->next;
			cur->next = head.next;
			head.next = cur;
			cur = next;
		}
	}
	void traverse() {
		dlistnode* cur = head.next;
		cout << "list: { nullptr<->";
		while (cur != &tail) {
			cout << cur->mvalue << "<->";
			cur = cur->next;
		}
		cout << "nullptr }\n\n\n";
	}
	int length() {
		return mlength;
	}
};
int main() {
	srand(time(0));

	DList l;
	for (int i = 0; i < 20; ++i) {
		int value = rand() % 100, operation = rand() % 10, index = rand() % (l.length() + 2);
		//push_front test
		if (operation == 0) {
			cout << std::format("push_front() was called, value {} was inserted\n", value);
			l.push_front(value);
			l.traverse();
		}
		//insert test
		else if (operation <= 6) {
			cout << std::format("insert() was called, value {} within index (if legal) {} was inserted\n", value, index);
			l.insert(value, index);
			l.traverse();
		}
		//erase
		else {
			cout << std::format("erase() was called, node (if existed) at {} was erased\n", index);
			l.erase(index);
			l.traverse();
		}
	}
	l.reverse();
	l.traverse();
	return 0;
}

经典例题

LRUCache
LRU不难,但是非常经典,并且很喜欢考。
请添加图片描述
这个题比较恶心。。。目前没有重写的愿望,是很久以前写的,感觉看上去有点傻傻的……

主要思路在于创建静态链表(速度更快),将他们排序后依靠双向链表的特性还联系在一起,然后从后往前将答案输出。

#include<iostream>
#include<algorithm>
using namespace std;

typedef struct Node
{
	int index, value;
	struct Node* next, * prev;
}Node;



bool mycmp(Node a, Node b)
{
	if (a.value < b.value)
		return true;
	return false;
}

Node operation(Node* operatee)
{
	Node node;
	node.index = 0, node.next = NULL, node.prev = NULL, node.value = 0;
	if (operatee->prev->prev == NULL)
	{
		node.value = operatee->next->value - operatee->value;
		node.index = operatee->next->index;
		operatee->prev->next = operatee->next;
		operatee->next->prev = operatee->prev;
	}
	else if (operatee->next == NULL)
	{
		node.value = operatee->value - operatee->prev->value;
		node.index = operatee->prev->index;
		operatee->prev->next = operatee->next;
	}
	else
	{
		int downresult = operatee->value - operatee->prev->value,
			upresult = operatee->next->value - operatee->value;
		if (downresult <= upresult)
		{
			node.value = downresult, node.index = operatee->prev->index;
			operatee->prev->next = operatee->next;
			operatee->next->prev = operatee->prev;
		}
		else
		{
			node.value = upresult, node.index = operatee->next->index;
			operatee->prev->next = operatee->next;
			operatee->next->prev = operatee->prev;
		}
	}
	return node;
}

int main()
{
	int n;
	cin >> n;
	Node* arr1 = new Node[n + 5]();
	Node* arr2 = new Node[n + 5]();
	for (int i = 1; i <= n; ++i)
	{
		scanf("%d", &arr1[i].value);
		arr1[i].index = i;
	}
	sort(arr1 + 1, arr1 + n + 1, mycmp);
	for (int i = 1; i <= n; ++i)
	{
		arr1[i].prev = &arr1[i - 1];
		arr1[i].next = arr1[i - 1].next;
		arr1[i - 1].next = &arr1[i];
	}


	int* loc = new int[n + 5]();
	for (int i = 1; i <= n; ++i)
		loc[arr1[i].index] = i;
	for (int i = n; i >= 2; --i)
	{
		arr2[i] = operation(&arr1[loc[i]]);
	}
	for (int i = 2; i <= n; ++i)
	{
		printf("%d %d\n", arr2[i].value, arr2[i].index);
	}
	delete[] arr1;
	delete[] arr2;
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值