链表的介绍

目录

  • 引言
  • 优缺点
  • 与链表相似的数据结构
  • 注意事项
  • 单向链表的实现
    • 基础实现
      • 创建类
      • 创建成员变量
      • 创建特殊方法
    • 增加数据
      • push_back方法
      • insert方法
    • 删除数据
      • del_back方法
      • del_index方法
    • clear方法
    • 查询数据
      • at方法与重载的中括号运算符
      • toArray方法
      • indexOf方法
    • 修改数据
    • 获取链表大小
    • 测试方法是否正常
    • 总结
  • 下篇预告

引言

大家好,我是链表。我呢,是一种存储非连续性数据的物理数据结构,我的每一个数据都用一个个节点来存储,我的每一个节点都用一条条的“链”来链接,因此,就被后人称作链表了。

优缺点

其中,我增加数据和删除数据的时间复杂度最坏是O(n),因为增加数据的话,就只要让我的最后一个节点接上存储新数据的节点就行了。而删除数据的话,就只要让我倒数第二个节点断开跟我最后一个节点的链接就行了。
虽然我非常擅长这些,但我也有一些坏的地方,如查询数据和修改数据,时间复杂度最好是O(n),查询的话,就需要让我遍历每一个节点,然后分别对每一个节点判断,才行;而修改的话,其实就是跟我查询数据的操作差不多,也是要先让我遍历每一个节点,然后分别对每一个节点判断才行。

与链表相似的数据结构

而与我相似的数据结构呢,有双向链表,有循环链表,还有他们俩的结合体——双向循环链表,跟C++里的STL容器list差不多。其中,我们将要实现的是单向链表,也就是我,如果你不知道它们长什么样的话,就看下下面的图片吧。

单向链表
...
head
数据a
数据b
双向链表
tail
head
数据a
数据b
...
循环链表
head
数据a
数据b
...
双向循环链表
head
数据a
数据b
...
tail

注意事项

不要用节点对象的引用去定位节点,因为这会让我的节点莫名消失!在实际用C/C++实现非连续性存储数据的数据结构的时候至关重要!

单向链表的实现

基础实现

创建类

首先,要想实现我,就需要两个模板类,一个是节点类,一个是单向链表类。

template<class T>
class mylist {
private:
	class node {
	public:
		T t;
		node* next;
		node(const T t = NULL, node* nextTo = nullptr) {
			this->t = t;
			this->next = nextTo; 
		}
	};
};

创建成员变量

然后,就要给这个单向链表类增加表示大小的成员变量ic和我的“头”——头结点head了。

template<class T>
class mylist {
private:
	class node {
	public:
		T t;
		node* next;
		node(const T t = NULL, node* nextTo = nullptr) {
			this->t = t;
			this->next = nextTo;
		}
	};
	int ic;
	node* head;
};

创建特殊方法

最后,在单向链表类里,我们需要做许多特殊的方法,分别是无参构造方法,数组构造方法,拷贝构造方法,重载赋值运算符和析构方法,其中,析构方法不用递归实现的话对我来说太复杂了,因此,就需要一个助于让我们析构的临时私有方法deletenode方法来销毁我。至此,这些特殊方法做完之后,我们就要准备要创建并实现我增加数据的方法了。

#pragma once
#include <iostream>
using namespace std;
template<class T>
class mylist {
private:
	class node {
	public:
		T t;
		node* next;
		node(const T t = NULL, node* nextTo = nullptr) {
			this->t = t;
			this->next = nextTo;
		}
	};
	unsigned int ic;
	node* head;
	void deletenode(node*& delnode) {
		if (nullptr != delnode->next) {
			deletenode(delnode->next);
		}
		delnode->next = nullptr;
		delete delnode;
	}
	T* arr;
public:
	mylist() {
		this->ic = 0;
		this->head = new node;
	}
	mylist(T* array, const unsigned int isize) {
		this->ic = isize;
		this->head = new node;
		node** addnode = &this->head;
		for (int i = 0; i < isize; i++) {
			(*addnode)->next = new node(array[i]);
			addnode = &(*addnode)->next;
		}
	}
	mylist(const mylist& list) {
		this->ic = list.ic;
		this->head = new node;
		*this = list;
	}
	mylist& operator=(const mylist& list) {
		node* searchnode = list.head;
		node** addnode = &this->head;
		while (nullptr != searchnode->next) {
			searchnode = searchnode->next;
			(*addnode)->next = new node(searchnode->t);
			addnode = &(*addnode)->next;
		}
		return *this;
	}
	~mylist() {
		deletenode(this->head);
	}
};

增加数据

增加数据的方法呢,有两个,一个是push_back方法,一个是insert方法。

push_back方法

其中,push_back方法用于在我的最后一个节点插入存放新数据的节点,因此,push_back方法的形参就是要插入的新数据。实现它时,首先要创建一个指向我的头结点的节点对象的指针tailnode,用于定位我的最后一个节点,然后,用一个while循环将这个节点对象指针移到在我的“链”上的下一个节点的地址上,以此类推,最终会使这个节点对象指针定位到我的最后一个节点的地址上,最后,在定位好后,new出来一个节点,这个新节点的值为push_back方法中传入的新数据形参,并让tailnode这个节点指针指向的节点的下一个节点为这个刚new出来的新节点,以此让这个新节点在我的“链”上。之后呢,我的大小自增1,并返回这个新数据,因为这个新数据是泛型的,所以返回的类型就为T

T push_back(const T item) {
	node** addnode = &this->head;
	while (nullptr != (*addnode)->next) {
		addnode = &(*addnode)->next;
	}
	(*addnode)->next = new node(item);
	this->ic++;
	return item;
}

insert方法

insert方法,则用于在我的其中一个节点之前插入存放新数据的节点,跟push_back方法的用途差不多,返回的类型都是泛型T,只不过插入的位置的形参变了。形参呢,除了要插入的新数据之外,还有索引index;位置呢,很随便,哪里都可以插。但需要注意的是,这个索引,它不能是负数,所以这个索引的形参的类型就只能是unsigned int了,之后,我们在实现这个方法前,先要对这个索引形参判断一下,如果这个索引大于等于我的大小,那么就不能插入,直接返回NULL就行,之后,我们定义指向头结点的节点指针addnode,并通过while循环来定位addnode插入的位置,这里的while循环以索引index的值来循环,每循环一次之后就使它自减1,直到它为0为止。定位好后,就new一个节点对象newnode,它的值为要插入的新数据,它的下一个节点则为addnode指向的节点的下一个节点,之后,就用这个addnode节点指针指向的节点来连接节点对象newnode,使其在我的同一条“链”上,就行了,然后,大小自增1,返回新数据的值,整个insert方法也就正式完成了。

T insert(const T item, unsigned int index) {
	if (index >= this->ic) {
		return NULL;
	}
	node** addnode = &this->head;
	while (index--) {
		addnode = &(*addnode)->next;
	}
	node* newnode = new node(item, (*addnode)->next);
	(*addnode)->next = newnode;
	this->ic++;
}

接着,我们就要实现删除数据的一些方法了。

删除数据

删除数据的方法呢,我要的有三个,第一是del_back方法,第二是del_index方法,第三是clear方法。

del_back方法

del_back方法呢,无参,返回泛型T,对应了我最后一个节点的数据,在实现这个方法之前,像insert方法那样,也需要先判断一下,不过跟insert方法不同的是,del_back方法则先检测我的大小是否为0,如果是,就返回NULL,因为我的大小为0,相当于我只有“头”可以删我删了“头”等于我“英年早逝”,只有析构方法执行时,我老了,才能“逝”,所以直接返回NULL,不是就继续执行,然后定义一个指向我的头结点的节点指针nextIsTailNode,随即等它在通过while循环移到倒数第2个节点的时候,就让它定位好了,之后,用一个类型为泛型T的变量last存储nextIsTailNode指向的节点的下一个节点的值,并delete掉节点指针nextIsTailNode指向的节点的下一个节点——就是我的最后一个节点,让它指向的的下一个节点设为空指针,然后让我的大小自减1,最后一个节点也就彻底离开我了。

T del_back() {
	if (!this->ic) {
		return NULL;
	}
	node** nextIsTailNode = &this->head;
	while (nullptr != (*nextIsTailNode)->next->next) {
		nextIsTailNode = &(*nextIsTailNode)->next;
	}
	T last = (*nextIsTailNode)->next->t;
	delete (*nextIsTailNode)->next;
	(*nextIsTailNode)->next = nullptr;
	this->ic--;
	return last;
}

del_index方法

del_index方法,肯定是要有一个代表索引的类型为unsigned int的形参index的,如果你用指向某个节点的指针来删的话,那么这个指针所删除的节点及它后面的节点也一并不见了,相当于我骨折了。因此,我建议你们根据索引来删数据的时候,用节点指针来查找用节点对象来存储索引所对应的节点的下一个节点及它后面的节点。而且在这个方法实现之前,除了我的大小不能为0之外,如果索引是否大于等于我的大小,也直接返回NULL,不删,之后,根据刚才提到的建议,在后面定义一个指向头节点的节点指针searchnode用于查找,通过while循环执行index次循环,让searchnode通过一次次的转到它的下一个节点的地址来定位。之后定位好,就定义一个类型为泛型T的变量item来存储索引代表的节点的值,然后定义一个节点对象hasnodesearchnode指向的下一个节点的下一个节点及后面的所有节点,并deletesearchnode节点指针指向的节点及后面的所有节点,然后searchnode指向的节点跟hasnode节点对象连接一下,既能保证了数据的完整性,又使我只删除了一个节点,最后我的大小自减1,并返回变量item的值,这个del_index方法也实现完毕了。

T del_index(unsigned int index) {
	if (!this->ic || index >= this->ic) {
		return NULL;
	}
	node** searchnode = &this->head;
	while (index--) {
		searchnode = &(*searchnode)->next;
	}
	T item = (*searchnode)->next->t;
	node* hasnode = (*searchnode)->next->next;
	delete (*searchnode)->next;
	(*searchnode)->next = hasnode;
	this->ic--;
	return item;
}

clear方法

clear方法,就跟我的析构方法差不多,只是要删的对象有差别,析构方法是包括头节点,全都要删,而clear方法,除了头节点以外,全都要删,这就是区别。当然,如果要从头节点的下一个节点开始删的话,就要我的大小不为0才行,否则报错。之后删完全了,就给大小设为0就行了。

void clear() {
	if (this->ic) {
		this->ic = 0;
		deletenode(this->head->next);
		this->head->next = nullptr;
	}
}

到后面,就要开始来查询一下数据了。

查询数据

查询数据的方法呢,普通的有at方法,与之同等的重载中括号运算符,你要是想遍历我整个“链”上的元素,也可以带上toArray方法,你要是想查询我所存的元素在哪个节点上,这个节点又在我的哪个位置上,indexOf方法就足够。

at方法与重载的中括号运算符

先略讲at方法,因为at方法的实现跟重载的中括号运算符的实现差不多,所以实现起来很简单,只需要返回重载的中括号运算符返回的值就好。现在主要就是实现重载的中括号运算符。

T& at(const unsigned int index) {
	return (*this)[index];
}

重载的中括号运算符,有代表索引的类型为unsigned int的形参index。执行这个方法时,第一步,检测一下索引是否正常,如果索引超过我的大小,就直接返回NULL,反之就继续执行;第二步,初始化节点对象searchnode为我头节点的下一个节点,通过index次循环来定位,具体定位的方法前面都讲过了;第三步,也是最后一步,节点对象searchnode定位好后,返回节点对象searchnode的值,就行了。

T& operator[](unsigned int index) {
	if (index >= this->ic) {
		static T nulldata = NULL;
		return nulldata;
	}
	node* searchnode = this->head->next;
	while (index--) {
		searchnode = searchnode->next;
	}
	return searchnode->t;
}

toArray方法

实现这个方法,得先往我这个类里面定义一个将要成为动态数组的指向泛型T类型的arr指针,因为这将要为整个toArray方法提供地基来“搭建”。
之后,就用一个searchnode节点对象在我的“链”上遍历一下我的节点,并把遍历的节点一一转化成数据并输出在动态数组arr里面,最后返回arr这个动态数组的引用,就又好了一次。

T*& toArray() {
	delete[] this->arr;
	this->arr = new T[this->ic];
	node* searchnode = this->head;
	int i = 0;
	while (nullptr != searchnode->next) {
		searchnode = searchnode->next;
		this->arr[i++] = searchnode->t;
	}
	return this->arr;
}

啥?你说我的这个arr动态数组还没有创建和析构的方式?没事,在我的所有构造方法中,可以为动态数组arr new一小块大小为0的内存;在析构方法中,可以delete[]掉这个动态数组arr即使是大小为0的内存也原封不动地还给操作系统,就跟“借空气,还空气”差不多;在toArray方法中,也可以前面增加这两行代码,只要有了这些必备的代码,那你的构造方法,析构方法和toArray方法也就能放下心了,因为几乎没有任何的bug存在。在这之后,就可以去学最后一个特殊的方法——indexOf方法了。

delete[] this->arr;
this->arr = new T[this->ic];

indexOf方法

indexOf方法中,只要一个形参——代表要找的数据的类型为泛型Titem。开始执行后,先初始化一个节点对象searchnode为我的头结点,也可以理解成节点对象searchnode对我1:1地进行克隆,用于遍历节点,找到数据,并初始化一个代表索引的无符号整型变量index为0,以此来辅助这个indexOf方法找数据,然后searchnode节点对象遍历我的所有节点:先转到下一个节点,如果searchnode的值为要找的数据,那么就返回这个数据所对应的节点索引——就是刚才创建过的无符号整型index,如果不是,那么索引index自增1,再接着遍历,直到searchnode节点对象找到数据或者遍历完我,如果遍历完我,最后就返回-1,说明这个要找的数据并不在我的某一个节点里面。那为什么这个indexOf方法的返回类型是long long呢,这是我因为怕在返回-1的时候被unsigned int转为正数,从而让用户被我误导觉得要查找的数据在我的某一个节点上,并且unsigned int所能存储的最大值已经超过了int所能存储的最大值,如果以unsigned int的最大值来访问的话,可能就会因为int所能存储的最大值而被转换成负数。所以,就需要long long类型来返回这个index。好了,在indexOf方法做好之后,接下来就继续看吧。

long long indexOf(const T item) {
	node* searchnode = this->head;
	unsigned int index = 0;
	while (nullptr != searchnode->next) {
		searchnode = searchnode->next;
		if (item == searchnode->t) {
			return index;
		}
		index++;
	}
	return -1;
}

修改数据

修改数据的话,不需要什么方法,一般修改数据的办法就是借助我的查询方法来用赋值运算符来修改值,没有什么花招。如:

List[0] = 1;//这里的“List”是一个链表对象

获取链表大小

在做好我的最后,获取链表的大小的方法也要有,如size方法和isEmpty方法,不然,用户怎么能查询到我的大小呢?第一是size方法,只需要返回我的大小就行;第二是isEmpty方法,只需要返回我的大小是否为0就行。

unsigned int size() const {
	return this->ic;
}
bool isEmpty() const {
	return 0 == this->ic;
}

测试方法是否正常

现在我的所有方法都走做好了,我们也要测试一下了。就一口气测试我的全部方法好了。

#include <iostream>
#include "mylist2.hpp"
using namespace std;

int main() {
	mylist<int>m;//普通构造方法测试
	cout << "原始大小:" << m.size() << "    是否为空:" << (m.isEmpty() ? "true" : "false") << endl;//size方法与isEmpty方法测试1
	m.insert(1, 0);//insert方法测试1
	m.push_back(1);//push_back方法测试1
	m.push_back(2);//push_back方法测试2
	m.insert(3, 1);//insert方法测试2
	cout << "链表m增加数据后大小:" << m.size() << "    是否为空:" << (m.isEmpty() ? "true" : "false") << endl;//size方法与isEmpty方法测试2
	m.del_back();//del_back方法测试
	m.del_index(2);//del_index方法测试1
	m.del_index(1);//del_index方法测试2
	cout << "链表m删除数据后大小:" << m.size() << "    是否为空:" << (m.isEmpty() ? "true" : "false") << endl;//size方法与isEmpty方法测试3
	int arr[5] = { 1, 2, 3, 4, 5 };
	mylist<int>ma(arr, 5);//数组构造方法测试
	mylist<int>mb = ma;//拷贝构造方法测试
	const int* arra = ma.toArray();//toArray方法测试1
	const int* arrb = mb.toArray();//toArray方法测试2
	int i = 0;
	while (i < 5) {
		cout << "ma[" << i << "] == mb[" << i << "]:" << (arra[i] == arrb[i] ? "true" : "false") << endl;//toArray方法测试3
		i++;
	}
	cout << "ma[1] = " << ma[1] << "    ma[3] == ma.at(3):" << (ma[3] == ma.at(3) ? "true" : "false") << endl;//重载中括号运算符测试1 at方法测试
	cout << "5 == ma[ma.indexOf(5)]:" << (5 == ma[ma.indexOf(5)] ? "true" : "false") << endl;//重载中括号运算符测试2 indexOf方法测试1
	cout << "ma.indexOf(7) = " << ma.indexOf(7) << "    NULL == ma[-1]:" << (NULL == ma[-1] ? "true" : "false") << endl;//重载中括号运算符测试3 indexOf方法测试2
	m.clear();//clear方法测试
	cout << "链表m清空数据后大小:" << m.size() << "    是否为空:" << (m.isEmpty() ? "true" : "false") << endl;//size方法与isEmpty方法测试4
	return 0;
}

如果这段测试代码执行之后是这样打印的:

原始大小:0 是否为空:true
链表m增加数据后大小:3 ​ ​ ​ 是否为空:false
链表m删除数据后大小:1 ​ ​ ​ 是否为空:false
ma[0] == mb[0]:true
ma[1] == mb[1]:true
ma[2] == mb[2]:true
ma[3] == mb[3]:true
ma[4] == mb[4]:true
ma[1] = 2 ​ ​ ​ ma[3] == ma.at(3):true
5 == ma[ma.indexOf(5)]:true
ma.indexOf(7) = -1 ​ ​ ​ NULL == ma[-1]:true
链表m清空数据后大小:0 ​ ​ ​ 是否为空:true

那么,我也就完全实现好了。

总结

通过刚才实现我这个链表类的代码,我们可以知道,我增加数据的时间复杂度为O(n),因为要通过某一个指向节点对象的指针来定位;删除数据的时间复杂度也为O(n),或者以delnode方法为例时间复杂度也为O(1),内存复杂度却为O(n),因为在删除元素的时候,这两种方法都需要通过循环来定位节点,删除元素;查询数据的时间复杂度还为O(n),因为都需要通过循环来定位;修改数据的时间复杂度也为O(n),原因同查询数据的时间复杂度的原因,最后只有获取链表大小的时间复杂度为O(1),因为这一些方法都通过我私有的成员变量来获取
虽然来看,我不太行。但是能体现出我们链表优点的,最优是经过一些成长的双向循环链表,他能快速的往尾部插入数据时间复杂度为O(1)删除尾部元素时间复杂度也为O(1),是我们链表的骄傲!当我想到这个双向循环链表时,我不禁想到了以后的美好未来:在未来中,数据结构们的运行速度将会更快内存开销将会更少人类们的生活也将因为这些变得越来越便携,真是一个美好的未来啊!

下篇预告

粗心的连点器

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/961413.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【AIGC学习笔记】扣子平台——精选有趣应用,探索无限可能

背景介绍&#xff1a; 由于近期业务发展的需求&#xff0c;我开始接触并深入了解了扣子平台的相关知识&#xff0c;并且通过官方教程自学了简易PE工作流搭建的技巧。恰逢周会需要准备与工作相关的分享主题&#xff0c;而我作为一个扣子平台的初学者&#xff0c;也想探索一下这…

DeepSeek-R1部署教程(基于Ollama)

虽说在过年&#xff0c;但不能忘了学习。这几天科技圈最火的莫过于deepseek&#xff0c;我抽空也学习一下deepseek的部署过程。 1、下载Ollama并安装 https://github.com/ollama/ollama/releases/latest/download/OllamaSetup.exe 下载好后双击直接运行。 2、安装deepseek …

jenkins-k8s pod方式动态生成slave节点

一. 简述&#xff1a; 使用 Jenkins 和 Kubernetes (k8s) 动态生成 Slave 节点是一种高效且灵活的方式来管理 CI/CD 流水线。通过这种方式&#xff0c;Jenkins 可以根据需要在 Kubernetes 集群中创建和销毁 Pod 来执行任务&#xff0c;从而充分利用集群资源并实现更好的隔离性…

力扣面试150 快乐数 循环链表找环 链表抽象 哈希

Problem: 202. 快乐数 &#x1f469;‍&#x1f3eb; 参考题解 Code public class Solution {public int squareSum(int n) {int sum 0;while(n > 0){int digit n % 10;sum digit * digit;n / 10;}return sum;}public boolean isHappy(int n) {int slow n, fast squa…

【C++】设计模式详解:单例模式

文章目录 Ⅰ. 设计一个类&#xff0c;不允许被拷贝Ⅱ. 请设计一个类&#xff0c;只能在堆上创建对象Ⅲ. 请设计一个类&#xff0c;只能在栈上创建对象Ⅳ. 请设计一个类&#xff0c;不能被继承Ⅴ. 请设计一个类&#xff0c;只能创建一个对象&#xff08;单例模式&#xff09;&am…

LLM 推理

https://www.bilibili.com/video/BV16yqeYhELh/ 大模型推理加速目标&#xff1a;高吞吐、低延迟 TGI vLLM SGLang LMDeploy 商汤 和 上海人工智能实验室 一起开发 缺点 性能对比 分析总结 https://www.bilibili.com/video/BV16yqeYhELh/ 大模型推理加速目标&#xff1a;高吞吐…

UE(UltraEdit) 配置简易C/C++编译运行环境

该类型其他帖子 EmEditor 配置简易C/C 编译运行环境_emeditor 代码运行-CSDN博客 RJ TextEd 配置简易C/C 编译运行环境-CSDN博客 这种配置适合ACM竞赛&#xff0c;即要求不使用现代IDE&#xff0c;又想用一个比较好用、至少支持代码高亮的编辑器。 前提条件 1.Mingw GCC 已…

XSS 漏洞全面解析:原理、危害与防范

目录 前言​编辑 漏洞原理 XSS 漏洞的危害 检测 XSS 漏洞的方法 防范 XSS 漏洞的措施 前言 在网络安全的复杂版图中&#xff0c;XSS 漏洞&#xff0c;即跨站脚本攻击&#xff08;Cross - Site Scripting&#xff09;&#xff0c;是一类极为普遍且威胁巨大的安全隐患。随着互…

Alfresco Content Services dockerCompose自动化部署详尽操作

Alfresco Content Services docker社区部署文档 Alfresco Content Services简介 Alfresco Content Services&#xff08;简称ACS&#xff09;是一款功能完备的企业内容管理&#xff08;ECM&#xff09;解决方案&#xff0c;主要面向那些对企业级内容管理有高要求的组织。具体…

LCR 139.训练计划 I

目录 题目过程解法双指针法&#xff08;两端开始&#xff09;快慢指针 题目 教练使用整数数组 actions 记录一系列核心肌群训练项目编号。为增强训练趣味性&#xff0c;需要将所有奇数编号训练项目调整至偶数编号训练项目之前。请将调整后的训练项目编号以 数组 形式返回。 过…

AboutDialog组件的功能和用法

文章目录 1 概念介绍2 使用方法3 示例代码 我们在上一章回中介绍了AlertDialog Widget相关的内容,本章回中将介绍AboutDialog Widget.闲话休提&#xff0c;让我们一起Talk Flutter吧。 1 概念介绍 我们在这里说的AboutDialog是一种弹出式窗口&#xff0c;和上一章回中介绍的Al…

Redis学习之哨兵二

一、API 1.sentinel masters:展示被监控的主节点状态及相关的统计信息 2.sentinel master <master name>:展示指定的主节点的状态以及相关的统计信息 3.sentinel slaves <master name>:展示指定主节点的从节点状态以及相关的统计信息 4.sentinel sentinels <mas…

03链表+栈+队列(D2_栈)

目录 讲解一&#xff1a;栈 一、基本介绍 二、代码示例 ------------------------------ 讲解二&#xff1a;单调栈 一、基本介绍 二、适用场景 三、情形示例 1. 寻找左边第一个小于它的数 2. 寻找左边第一个小于它的数的下标 3. 寻找右边第一个大于它的数 4. 寻找右…

春晚魔术中的数学知识

蛇年春晚刘谦魔术又和大家普及了一下编程中的冒泡排序法&#xff0c;思考深入一点&#xff0c;它还涉及到群论和组合数学中的一些知识。 游戏规则和操作步骤&#xff0c;任意打乱三种餐具作为初始状态&#xff1a; 1.筷子和左边的东西互换&#xff0c;如果筷子就在左边&#…

OpenCV:开运算

目录 1. 简述 2. 用腐蚀和膨胀实现开运算 2.1 代码示例 2.2 运行结果 3. 开运算接口 3.1 参数详解 3.2 代码示例 3.3 运行结果 4. 开运算应用场景 5. 注意事项 6. 总结 相关阅读 OpenCV&#xff1a;图像的腐蚀与膨胀-CSDN博客 OpenCV&#xff1a;闭运算-CSDN博客 …

基于Springboot的健身房管理系统【附源码】

基于Springboot的健身房管理系统 效果如下&#xff1a; 系统登陆页面 管理员主页面 器材类型管理页面 健身房管理页面 教练管理页面 用户管理页面 个人信息页面 课程管理页面 研究背景 随着健康意识的不断增强和人们生活水平的提高&#xff0c;健身房已经成为了现代城市中不…

扣子平台音频功能:让声音也能“智能”起来。扣子免费系列教程(14)

在数字化时代&#xff0c;音频内容的重要性不言而喻。无论是在线课程、有声读物&#xff0c;还是各种多媒体应用&#xff0c;音频都是传递信息、增强体验的关键元素。扣子平台的音频功能&#xff0c;为开发者和内容创作者提供了一个强大而灵活的工具&#xff0c;让音频的使用和…

初始Python篇(8)—— 异常

找往期文章包括但不限于本期文章中不懂的知识点&#xff1a; 个人主页&#xff1a;我要学编程(ಥ_ಥ)-CSDN博客 所属专栏&#xff1a; Python 目录 异常介绍 异常的处理 try-except try-except-else try-except-else-finally 异常的抛出 常见的异常类型 异常介绍 在…

SSM-MyBatis-总结

文章目录 一、Hello MyBatis1.1 流程1.2 总结 二、Crud 的一些注意点三、参数传递3.1 #{ } VS ${ }3.2 单、复参数传递&#xff08;1&#xff09;单参数&#xff08;2&#xff09;多参数 -- Param&#xff08;3&#xff09;总结 四、查询结果返回--结果封装4.1 ResultType 一般…

【算法设计与分析】实验1:字符串匹配问题的算法设计与求解

目录 一、实验目的 二、实验环境 三、实验内容 四、核心代码 五、记录与处理 六、思考与总结 七、完整报告和成果文件提取链接 一、实验目的 给定一个文本&#xff0c;在该文本中查找并定位任意给定字符串。 1、深刻理解并掌握蛮力法的设计思想&#xff1b; 2、提高应用…